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