diff --git a/apps/dokploy/server/api/routers/patch.ts b/apps/dokploy/server/api/routers/patch.ts index 43486038e..4c8dfefe5 100644 --- a/apps/dokploy/server/api/routers/patch.ts +++ b/apps/dokploy/server/api/routers/patch.ts @@ -32,90 +32,6 @@ import { apiUpdatePatch, } from "@/server/db/schema"; -// Helper to get git config from application -const getApplicationGitConfig = ( - app: Awaited>, -) => { - switch (app.sourceType) { - case "github": - return { - gitUrl: `https://github.com/${app.owner}/${app.repository}.git`, - gitBranch: app.branch || "main", - sshKeyId: null, - }; - case "gitlab": - return { - gitUrl: `https://gitlab.com/${app.gitlabOwner}/${app.gitlabRepository}.git`, - gitBranch: app.gitlabBranch || "main", - sshKeyId: null, - }; - case "gitea": - return { - gitUrl: app.gitea?.giteaUrl - ? `${app.gitea.giteaUrl}/${app.giteaOwner}/${app.giteaRepository}.git` - : "", - gitBranch: app.giteaBranch || "main", - sshKeyId: null, - }; - case "bitbucket": - return { - gitUrl: `https://bitbucket.org/${app.bitbucketOwner}/${app.bitbucketRepository}.git`, - gitBranch: app.bitbucketBranch || "main", - sshKeyId: null, - }; - case "git": - return { - gitUrl: app.customGitUrl || "", - gitBranch: app.customGitBranch || "main", - sshKeyId: app.customGitSSHKeyId, - }; - default: - return null; - } -}; - -// Helper to get git config from compose -const getComposeGitConfig = ( - compose: Awaited>, -) => { - switch (compose.sourceType) { - case "github": - return { - gitUrl: `https://github.com/${compose.owner}/${compose.repository}.git`, - gitBranch: compose.branch || "main", - sshKeyId: null, - }; - case "gitlab": - return { - gitUrl: `https://gitlab.com/${compose.gitlabOwner}/${compose.gitlabRepository}.git`, - gitBranch: compose.gitlabBranch || "main", - sshKeyId: null, - }; - case "gitea": - return { - gitUrl: compose.gitea?.giteaUrl - ? `${compose.gitea.giteaUrl}/${compose.giteaOwner}/${compose.giteaRepository}.git` - : "", - gitBranch: compose.giteaBranch || "main", - sshKeyId: null, - }; - case "bitbucket": - return { - gitUrl: `https://bitbucket.org/${compose.bitbucketOwner}/${compose.bitbucketRepository}.git`, - gitBranch: compose.bitbucketBranch || "main", - sshKeyId: null, - }; - case "git": - return { - gitUrl: compose.customGitUrl || "", - gitBranch: compose.customGitBranch || "main", - sshKeyId: compose.customGitSSHKeyId, - }; - default: - return null; - } -}; - export const patchRouter = createTRPCRouter({ // CRUD Operations create: protectedProcedure @@ -222,70 +138,10 @@ export const patchRouter = createTRPCRouter({ type: z.enum(["application", "compose"]), }), ) - .mutation(async ({ input, ctx }) => { - if (input.type === "application") { - const app = await findApplicationById(input.id); - if ( - app.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - - const gitConfig = getApplicationGitConfig(app); - if (!gitConfig || !gitConfig.gitUrl) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Application does not have a git source configured", - }); - } - - return await ensurePatchRepo({ - appName: app.appName, - type: "application", - gitUrl: gitConfig.gitUrl, - gitBranch: gitConfig.gitBranch, - sshKeyId: gitConfig.sshKeyId, - serverId: app.serverId, - }); - } - - if (input.type === "compose") { - const compose = await findComposeById(input.id); - if ( - compose.environment.project.organizationId !== - ctx.session.activeOrganizationId - ) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this compose", - }); - } - - const gitConfig = getComposeGitConfig(compose); - if (!gitConfig || !gitConfig.gitUrl) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Compose does not have a git source configured", - }); - } - - return await ensurePatchRepo({ - appName: compose.appName, - type: "compose", - gitUrl: gitConfig.gitUrl, - gitBranch: gitConfig.gitBranch, - sshKeyId: gitConfig.sshKeyId, - serverId: compose.serverId, - }); - } - - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Either application or compose must be provided", + .mutation(async ({ input }) => { + return await ensurePatchRepo({ + type: input.type, + id: input.id, }); }), @@ -432,48 +288,14 @@ export const patchRouter = createTRPCRouter({ }); } - // Generate patch diff - const patchContent = await generatePatch({ - codePath: input.repoPath, - filePath: input.filePath, - newContent: input.content, - serverId, - }); - - if (!patchContent.trim()) { - // No changes - remove existing patch if any - const existingPatch = await findPatchByFilePath( - input.filePath, - input.id, - input.type, - ); - if (existingPatch) { - await deletePatch(existingPatch.patchId); - } - return { deleted: true, patchId: null }; - } - - // Check if patch exists - const existingPatch = await findPatchByFilePath( - input.filePath, - input.id, - input.type, - ); - - if (existingPatch) { - await updatePatch(existingPatch.patchId, { content: patchContent }); - return { deleted: false, patchId: existingPatch.patchId }; - } - const newPatch = await createPatch({ filePath: input.filePath, - content: patchContent, - enabled: true, + content: input.content, applicationId: input.type === "application" ? input.id : undefined, composeId: input.type === "compose" ? input.id : undefined, }); - return { deleted: false, patchId: newPatch.patchId }; + return newPatch; }), // Cleanup diff --git a/packages/server/src/services/patch-repo.ts b/packages/server/src/services/patch-repo.ts index 98ac201a0..f2af7ce25 100644 --- a/packages/server/src/services/patch-repo.ts +++ b/packages/server/src/services/patch-repo.ts @@ -1,16 +1,18 @@ -import path, { join } from "node:path"; +import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; -import { findSSHKeyById } from "@dokploy/server/services/ssh-key"; import { TRPCError } from "@trpc/server"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; +import { cloneBitbucketRepository } from "../utils/providers/bitbucket"; +import { cloneGitRepository } from "../utils/providers/git"; +import { cloneGiteaRepository } from "../utils/providers/gitea"; +import { cloneGithubRepository } from "../utils/providers/github"; +import { cloneGitlabRepository } from "../utils/providers/gitlab"; +import { findApplicationById } from "./application"; +import { findComposeById } from "./compose"; interface PatchRepoConfig { - appName: string; type: "application" | "compose"; - gitUrl: string; - gitBranch: string; - sshKeyId?: string | null; - serverId?: string | null; + id: string; } /** @@ -18,98 +20,51 @@ interface PatchRepoConfig { * Returns path to the repo */ export const ensurePatchRepo = async ({ - appName, type, - gitUrl, - gitBranch, - sshKeyId, - serverId, + id, }: PatchRepoConfig): Promise => { - const { PATCH_REPOS_PATH, SSH_PATH } = paths(!!serverId); - const repoPath = join(PATCH_REPOS_PATH, type, appName); - const knownHostsPath = path.join(SSH_PATH, "known_hosts"); + let serverId: string | null = null; - // Check if repo exists - const checkCommand = `test -d "${repoPath}/.git" && echo "exists" || echo "not_exists"`; + if (type === "application") { + const application = await findApplicationById(id); + serverId = application.buildServerId || application.serverId; + } else { + const compose = await findComposeById(id); + serverId = compose.serverId; + } + + const application = + type === "application" + ? await findApplicationById(id) + : await findComposeById(id); + + const { PATCH_REPOS_PATH } = paths(!!serverId); + const repoPath = join(PATCH_REPOS_PATH, type, application.appName); + + const applicationEntity = { + ...application, + type, + serverId: serverId, + outputPathOverride: repoPath, + }; + + let command = "set -e;"; + if (application.sourceType === "github") { + command += await cloneGithubRepository(applicationEntity); + } else if (application.sourceType === "gitlab") { + command += await cloneGitlabRepository(applicationEntity); + } else if (application.sourceType === "gitea") { + command += await cloneGiteaRepository(applicationEntity); + } else if (application.sourceType === "bitbucket") { + command += await cloneBitbucketRepository(applicationEntity); + } else if (application.sourceType === "git") { + command += await cloneGitRepository(applicationEntity); + } - let exists = false; if (serverId) { - const result = await execAsyncRemote(serverId, checkCommand); - exists = result.stdout.trim() === "exists"; + await execAsyncRemote(serverId, command); } else { - const result = await execAsync(checkCommand); - exists = result.stdout.trim() === "exists"; - } - - // Setup SSH if needed - let sshSetup = ""; - if (sshKeyId) { - const sshKey = await findSSHKeyById(sshKeyId); - const temporalKeyPath = "/tmp/patch_repo_id_rsa"; - sshSetup = ` -echo "${sshKey.privateKey}" > ${temporalKeyPath}; -chmod 600 ${temporalKeyPath}; -export GIT_SSH_COMMAND="ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath} -o StrictHostKeyChecking=accept-new"; -`; - } - - if (!exists) { - // Clone the repo - const cloneCommand = ` -set -e; -${sshSetup} -mkdir -p "${repoPath}"; -git clone --branch ${gitBranch} --progress "${gitUrl}" "${repoPath}"; -echo "Repository cloned successfully"; -`; - - try { - if (serverId) { - await execAsyncRemote(serverId, cloneCommand); - } else { - await execAsync(cloneCommand); - } - } catch (error) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Failed to clone repository: ${error}`, - }); - } - } else { - // Repo exists - check if on correct branch and update - const updateCommand = ` -set -e; -cd "${repoPath}"; -${sshSetup} - -# Fetch all updates including tags -git fetch origin --tags --force - -# Checkout the target (branch or tag) - this handles switching branches/tags -git checkout --force "${gitBranch}" - -# If it's a branch that corresponds to a remote branch, hard reset to match remote -# This ensures we pull the latest changes for that branch. -# If it's a tag, we are already at the correct commit after checkout. -if git rev-parse --verify "origin/${gitBranch}" >/dev/null 2>&1; then - git reset --hard "origin/${gitBranch}" -fi - -echo "Updated repository to ${gitBranch}" -`; - - try { - if (serverId) { - await execAsyncRemote(serverId, updateCommand); - } else { - await execAsync(updateCommand); - } - } catch (error) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Failed to update repository: ${error}`, - }); - } + await execAsync(command); } return repoPath; @@ -132,8 +87,6 @@ export const readPatchRepoDirectory = async ( // Use git ls-tree to get tracked files only const command = `cd "${repoPath}" && git ls-tree -r --name-only HEAD`; - console.log("command", command); - let stdout: string; try { if (serverId) { diff --git a/packages/server/src/services/patch.ts b/packages/server/src/services/patch.ts index 095e9a9d1..0be127a58 100644 --- a/packages/server/src/services/patch.ts +++ b/packages/server/src/services/patch.ts @@ -11,8 +11,6 @@ import { and, eq, isNotNull } from "drizzle-orm"; export type Patch = typeof patch.$inferSelect; -// CRUD Operations - export const createPatch = async (input: typeof apiCreatePatch._type) => { if (!input.applicationId && !input.composeId) { throw new TRPCError({ @@ -25,9 +23,8 @@ export const createPatch = async (input: typeof apiCreatePatch._type) => { .insert(patch) .values({ ...input, - content: input.content.endsWith("\n") - ? input.content - : `${input.content}\n`, + content: input.content, + enabled: true, }) .returning() .then((value) => value[0]); @@ -187,95 +184,3 @@ export const applyPatches = async ({ await execAsync(command); } }; - -interface GeneratePatchOptions { - codePath: string; - filePath: string; - newContent: string; - serverId?: string | null; -} - -/** - * Generate a patch from modified file content using git diff - */ -export const generatePatch = async ({ - codePath, - filePath, - newContent, - serverId, -}: GeneratePatchOptions): Promise => { - const fullPath = join(codePath, filePath); - - // Write new content to the file - const encodedContent = Buffer.from(newContent).toString("base64"); - const writeCommand = `echo "${encodedContent}" | base64 -d > "${fullPath}"`; - - if (serverId) { - await execAsyncRemote(serverId, writeCommand); - } else { - await execAsync(writeCommand); - } - - // Generate diff - const diffCommand = `cd "${codePath}" && git diff -- "${filePath}"`; - - let diffResult: string; - if (serverId) { - const result = await execAsyncRemote(serverId, diffCommand); - diffResult = result.stdout; - } else { - const result = await execAsync(diffCommand); - diffResult = result.stdout; - } - - // Reset the file to original state - const resetCommand = `cd "${codePath}" && git checkout -- "${filePath}"`; - if (serverId) { - await execAsyncRemote(serverId, resetCommand); - } else { - await execAsync(resetCommand); - } - - return diffResult; -}; - -interface ApplyPatchToContentOptions { - originalContent: string; - patchContent: string; -} - -/** - * Apply a patch to content in memory (for preview purposes) - * Returns the patched content or throws an error if patch fails - */ -export const applyPatchToContent = async ({ - originalContent, - patchContent, -}: ApplyPatchToContentOptions): Promise => { - // Create temp files and apply patch - const tempDir = "/tmp/patch_preview_" + Date.now(); - const tempFile = `${tempDir}/file`; - const patchFile = `${tempDir}/patch.diff`; - - const encodedOriginal = Buffer.from(originalContent).toString("base64"); - const encodedPatch = Buffer.from(patchContent).toString("base64"); - - const command = ` -mkdir -p "${tempDir}"; -echo "${encodedOriginal}" | base64 -d > "${tempFile}"; -echo "${encodedPatch}" | base64 -d > "${patchFile}"; -cd "${tempDir}" && patch -p0 < "${patchFile}" 2>/dev/null; -cat "${tempFile}"; -rm -rf "${tempDir}"; -`; - - try { - const result = await execAsync(command); - return result.stdout; - } catch { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Failed to apply patch to content", - }); - } -}; diff --git a/packages/server/src/utils/providers/bitbucket.ts b/packages/server/src/utils/providers/bitbucket.ts index 57d6de3bc..5a7072198 100644 --- a/packages/server/src/utils/providers/bitbucket.ts +++ b/packages/server/src/utils/providers/bitbucket.ts @@ -86,6 +86,7 @@ interface CloneBitbucketRepository { enableSubmodules: boolean; serverId: string | null; type?: "application" | "compose"; + outputPathOverride?: string; } export const cloneBitbucketRepository = async ({ @@ -101,6 +102,7 @@ export const cloneBitbucketRepository = async ({ bitbucketId, enableSubmodules, serverId, + outputPathOverride, } = entity; const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); @@ -115,7 +117,7 @@ export const cloneBitbucketRepository = async ({ return command; } const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); + const outputPath = outputPathOverride ?? join(basePath, appName, "code"); command += `rm -rf ${outputPath};`; command += `mkdir -p ${outputPath};`; const repoToUse = entity.bitbucketRepositorySlug || bitbucketRepository; diff --git a/packages/server/src/utils/providers/git.ts b/packages/server/src/utils/providers/git.ts index 8e640892d..4c0610921 100644 --- a/packages/server/src/utils/providers/git.ts +++ b/packages/server/src/utils/providers/git.ts @@ -14,6 +14,7 @@ interface CloneGitRepository { enableSubmodules?: boolean; serverId: string | null; type?: "application" | "compose"; + outputPathOverride?: string; } export const cloneGitRepository = async ({ @@ -28,6 +29,7 @@ export const cloneGitRepository = async ({ customGitSSHKeyId, enableSubmodules, serverId, + outputPathOverride, } = entity; const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); @@ -47,7 +49,7 @@ export const cloneGitRepository = async ({ `; } const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); + const outputPath = outputPathOverride ?? join(basePath, appName, "code"); const knownHostsPath = path.join(SSH_PATH, "known_hosts"); if (!isHttpOrHttps(customGitUrl)) { diff --git a/packages/server/src/utils/providers/gitea.ts b/packages/server/src/utils/providers/gitea.ts index 1555e7713..4d26a9212 100644 --- a/packages/server/src/utils/providers/gitea.ts +++ b/packages/server/src/utils/providers/gitea.ts @@ -130,6 +130,7 @@ interface CloneGiteaRepository { enableSubmodules: boolean; serverId: string | null; type?: "application" | "compose"; + outputPathOverride?: string; } export const cloneGiteaRepository = async ({ @@ -145,6 +146,7 @@ export const cloneGiteaRepository = async ({ giteaRepository, enableSubmodules, serverId, + outputPathOverride, } = entity; const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId); @@ -162,7 +164,7 @@ export const cloneGiteaRepository = async ({ } const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); + const outputPath = outputPathOverride ?? join(basePath, appName, "code"); command += `rm -rf ${outputPath};`; command += `mkdir -p ${outputPath};`; diff --git a/packages/server/src/utils/providers/github.ts b/packages/server/src/utils/providers/github.ts index 5b7763df7..e7907cb47 100644 --- a/packages/server/src/utils/providers/github.ts +++ b/packages/server/src/utils/providers/github.ts @@ -121,6 +121,7 @@ interface CloneGithubRepository { type?: "application" | "compose"; enableSubmodules: boolean; serverId: string | null; + outputPathOverride?: string; } export const cloneGithubRepository = async ({ type = "application", @@ -136,6 +137,7 @@ export const cloneGithubRepository = async ({ githubId, enableSubmodules, serverId, + outputPathOverride, } = entity; const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId); @@ -155,7 +157,7 @@ export const cloneGithubRepository = async ({ const githubProvider = await findGithubById(githubId); const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); + const outputPath = outputPathOverride ?? join(basePath, appName, "code"); const octokit = authGithub(githubProvider); const token = await getGithubToken(octokit); const repoclone = `github.com/${owner}/${repository}.git`; diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 22e5df3ae..1ab1ddabd 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -107,6 +107,7 @@ interface CloneGitlabRepository { enableSubmodules: boolean; serverId: string | null; type?: "application" | "compose"; + outputPathOverride?: string; } export const cloneGitlabRepository = async ({ @@ -121,6 +122,7 @@ export const cloneGitlabRepository = async ({ gitlabPathNamespace, enableSubmodules, serverId, + outputPathOverride, } = entity; const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); @@ -141,7 +143,7 @@ export const cloneGitlabRepository = async ({ } const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); + const outputPath = outputPathOverride ?? join(basePath, appName, "code"); command += `rm -rf ${outputPath};`; command += `mkdir -p ${outputPath};`; const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);