Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

merge-resolver.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

merge-resolver.tsBlame204 lines · 2 contributors
e883329Claude1/**
2 * Automated merge conflict resolution using Claude.
3 *
4 * When a merge has conflicts, this module:
5 * 1. Detects conflicting files
6 * 2. Sends each conflict to Claude for resolution
7 * 3. Applies the resolved content and completes the merge
8 */
9
242097bccantynz-alt10import { getAnthropic } from "./ai-client";
e883329Claude11import { getRepoPath } from "../git/repository";
12
13interface ConflictFile {
14 path: string;
15 content: string;
16}
17
18interface ResolvedFile {
19 path: string;
20 content: string;
21}
22
23interface MergeResult {
24 success: boolean;
25 resolvedFiles: string[];
26 error?: string;
27 commitSha?: string;
28}
29
30async function exec(
31 cmd: string[],
32 opts?: { cwd?: string; env?: Record<string, string> }
33): Promise<{ stdout: string; stderr: string; exitCode: number }> {
34 const proc = Bun.spawn(cmd, {
35 cwd: opts?.cwd,
36 env: { ...process.env, ...opts?.env },
37 stdout: "pipe",
38 stderr: "pipe",
39 });
40 const [stdout, stderr] = await Promise.all([
41 new Response(proc.stdout).text(),
42 new Response(proc.stderr).text(),
43 ]);
44 const exitCode = await proc.exited;
45 return { stdout, stderr, exitCode };
46}
47
48/**
49 * Attempt to merge with automatic conflict resolution via Claude.
50 *
51 * This works in a temporary worktree to avoid disturbing the bare repo state.
52 */
53export async function mergeWithAutoResolve(
54 owner: string,
55 repo: string,
56 baseBranch: string,
57 headBranch: string,
58 mergeMessage: string
59): Promise<MergeResult> {
60 const repoDir = getRepoPath(owner, repo);
61 const worktree = `${repoDir}/_merge_worktree_${Date.now()}`;
62
63 try {
64 // Create a temporary worktree on the base branch
65 const addWt = await exec(
66 ["git", "worktree", "add", worktree, baseBranch],
67 { cwd: repoDir }
68 );
69 if (addWt.exitCode !== 0) {
70 return { success: false, resolvedFiles: [], error: `Failed to create worktree: ${addWt.stderr}` };
71 }
72
73 // Attempt the merge
74 const merge = await exec(
75 ["git", "merge", "--no-commit", "--no-ff", `origin/${headBranch}`],
76 { cwd: worktree, env: { GIT_AUTHOR_NAME: "GlueCron AI", GIT_AUTHOR_EMAIL: "ai@gluecron.com", GIT_COMMITTER_NAME: "GlueCron AI", GIT_COMMITTER_EMAIL: "ai@gluecron.com" } }
77 );
78
79 // If merge succeeded clean (no conflicts), commit it
80 if (merge.exitCode === 0) {
81 const commit = await exec(
82 ["git", "commit", "-m", mergeMessage],
83 { cwd: worktree, env: { GIT_AUTHOR_NAME: "GlueCron AI", GIT_AUTHOR_EMAIL: "ai@gluecron.com", GIT_COMMITTER_NAME: "GlueCron AI", GIT_COMMITTER_EMAIL: "ai@gluecron.com" } }
84 );
85
86 // Get the merge commit SHA
87 const { stdout: sha } = await exec(["git", "rev-parse", "HEAD"], { cwd: worktree });
88
89 // Update the bare repo's base branch ref
90 await exec(
91 ["git", "update-ref", `refs/heads/${baseBranch}`, sha.trim()],
92 { cwd: repoDir }
93 );
94
95 return { success: true, resolvedFiles: [], commitSha: sha.trim() };
96 }
97
98 // There are conflicts — get the list of conflicting files
99 const { stdout: statusOut } = await exec(["git", "diff", "--name-only", "--diff-filter=U"], { cwd: worktree });
100 const conflictPaths = statusOut.trim().split("\n").filter(Boolean);
101
102 if (conflictPaths.length === 0) {
103 return { success: false, resolvedFiles: [], error: "Merge failed but no conflicts detected" };
104 }
105
106 // Read each conflicting file and resolve with Claude
107 const resolvedFiles: string[] = [];
108 for (const filePath of conflictPaths) {
109 const { stdout: conflictContent } = await exec(["cat", filePath], { cwd: worktree });
110 const resolved = await resolveConflict(filePath, conflictContent);
111
112 if (resolved) {
113 // Write resolved content
114 await Bun.write(`${worktree}/${filePath}`, resolved.content);
115 await exec(["git", "add", filePath], { cwd: worktree });
116 resolvedFiles.push(filePath);
117 } else {
118 // Could not resolve this file — abort
119 await exec(["git", "merge", "--abort"], { cwd: worktree });
120 return {
121 success: false,
122 resolvedFiles: [],
123 error: `Could not auto-resolve conflict in ${filePath}`,
124 };
125 }
126 }
127
128 // All conflicts resolved — commit
129 const commit = await exec(
130 ["git", "commit", "-m", `${mergeMessage}\n\nAuto-resolved conflicts in: ${resolvedFiles.join(", ")}`],
131 { cwd: worktree, env: { GIT_AUTHOR_NAME: "GlueCron AI", GIT_AUTHOR_EMAIL: "ai@gluecron.com", GIT_COMMITTER_NAME: "GlueCron AI", GIT_COMMITTER_EMAIL: "ai@gluecron.com" } }
132 );
133
134 if (commit.exitCode !== 0) {
135 return { success: false, resolvedFiles, error: `Commit failed: ${commit.stderr}` };
136 }
137
138 const { stdout: sha } = await exec(["git", "rev-parse", "HEAD"], { cwd: worktree });
139
140 // Update the bare repo ref
141 await exec(
142 ["git", "update-ref", `refs/heads/${baseBranch}`, sha.trim()],
143 { cwd: repoDir }
144 );
145
146 return { success: true, resolvedFiles, commitSha: sha.trim() };
147 } finally {
148 // Clean up the worktree
a28cedeClaude149 await exec(["git", "worktree", "remove", "--force", worktree], { cwd: repoDir }).catch((err) => {
150 console.warn(
151 `[merge-resolver] worktree cleanup failed for ${worktree}:`,
152 err instanceof Error ? err.message : err
153 );
154 });
e883329Claude155 }
156}
157
158/**
159 * Use Claude to resolve a single file's merge conflicts.
160 */
161async function resolveConflict(
162 filePath: string,
163 conflictContent: string
164): Promise<ResolvedFile | null> {
242097bccantynz-alt165 const client = getAnthropic();
e883329Claude166
167 try {
168 const message = await client.messages.create({
9769a35Claude169 model: "claude-sonnet-4-6",
e883329Claude170 max_tokens: 8192,
171 messages: [
172 {
173 role: "user",
174 content: `You are resolving a git merge conflict in the file "${filePath}".
175
176The file contains conflict markers (<<<<<<< HEAD, =======, >>>>>>> branch). Your job is to produce the correctly merged version of the file.
177
178Rules:
179- Keep BOTH sides' changes when they don't contradict
180- When changes truly conflict, choose the version that preserves correctness and doesn't break functionality
181- Remove ALL conflict markers (<<<<<<< HEAD, =======, >>>>>>>)
182- The output must be valid, working code
183- Output ONLY the resolved file content, no explanation, no code fences
184
185File content with conflicts:
186${conflictContent}`,
187 },
188 ],
189 });
190
191 const text = message.content[0].type === "text" ? message.content[0].text : "";
192
193 // Verify no conflict markers remain
194 if (text.includes("<<<<<<<") || text.includes(">>>>>>>")) {
195 console.error(`[merge-resolver] Claude left conflict markers in ${filePath}`);
196 return null;
197 }
198
199 return { path: filePath, content: text };
200 } catch (err) {
201 console.error(`[merge-resolver] Failed to resolve ${filePath}:`, err);
202 return null;
203 }
204}