/ opencodehub / src / lib / conflicts.ts
swadhinbiswas feat: OpenCodeHub v1.1.0 - Enterprise Features & Storage Adapters · 2/15/2026
719a1cb
169 lines · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169

import { getDatabase } from "../db";
import * as schema from "../db/schema";
import { eq, and } from "drizzle-orm";
import { acquireRepo, releaseRepo } from "./git-storage";
import { simpleGit } from "simple-git";
import { logger } from "./logger";
import fs from "fs/promises";
import path from "path";

const db = getDatabase();

export interface ConflictFile {
    path: string;
    content: string; // The file content with <<<<<<< marks
}

export interface Resolution {
    path: string;
    content: string;
}

/**
 * Checks for conflicts by attempting a dry-run merge (no commit)
 * Returns a list of conflicted files with their content markings.
 */
export async function checkConflicts(repoId: string, queueItemId: string): Promise<ConflictFile[]> {
    // 1. Fetch Queue Item & PR Info
    const queueItem = await db.query.mergeQueueItems.findFirst({
        where: eq(schema.mergeQueueItems.id, queueItemId),
        with: {
            repository: { with: { owner: true } },
            pullRequest: true
        }
    });

    if (!queueItem) throw new Error("Queue item not found");

    const repoOwner = queueItem.repository.owner.username;
    const repoName = queueItem.repository.name;
    const { headBranch, baseBranch } = queueItem.pullRequest;

    // 2. Acquire Repo
    const repoPath = await acquireRepo(repoOwner, repoName);
    const git = simpleGit(repoPath);

    try {
        // 3. Prepare Branches
        await git.fetch();
        // Reset to base
        await git.checkout(baseBranch);
        await git.pull();

        // 4. Attempt Merge
        try {
            await git.merge([`origin/${headBranch}`, "--no-commit", "--no-ff"]);
            // If merge succeeds without error, there are no conflicts?
            // Actually simple-git throws on conflict usually.
            return [];
        } catch (e: any) {
            // Merge failed, likely conflicts
            const status = await git.status();
            const conflictedFiles = status.conflicted;

            if (conflictedFiles.length === 0) {
                // Failed for other reason?
                logger.error("Merge failed but no conflicts detected via status", e);
                return [];
            }

            const results: ConflictFile[] = [];

            for (const file of conflictedFiles) {
                const content = await fs.readFile(path.join(repoPath, file), "utf-8");
                results.push({
                    path: file,
                    content: content
                });
            }

            // Abort the merge to clean up
            await git.merge(["--abort"]).catch(() => { });
            await git.checkout(baseBranch); // Clean reset

            return results;
        }

    } finally {
        // Cleanup: Abort merge if pending
        try {
            await git.merge(["--abort"]);
        } catch { }
        await releaseRepo(repoOwner, repoName, false);
    }
}

/**
 * Applies resolutions and commits the merge.
 */
export async function resolveConflicts(repoId: string, queueItemId: string, resolutions: Resolution[]) {
    // 1. Fetch Queue Item & PR Info (Same as check)
    const queueItem = await db.query.mergeQueueItems.findFirst({
        where: eq(schema.mergeQueueItems.id, queueItemId),
        with: {
            repository: { with: { owner: true } },
            pullRequest: true
        }
    });

    if (!queueItem) throw new Error("Queue item not found");

    const repoOwner = queueItem.repository.owner.username;
    const repoName = queueItem.repository.name;
    const { headBranch, baseBranch } = queueItem.pullRequest;

    // 2. Acquire Repo
    const repoPath = await acquireRepo(repoOwner, repoName);
    const git = simpleGit(repoPath);

    try {
        // 3. Prepare Branches
        await git.fetch();
        await git.checkout(baseBranch);
        await git.pull();

        // 4. Reproduce Merge Conflict
        try {
            await git.merge([`origin/${headBranch}`, "--no-commit", "--no-ff"]);
        } catch (e) {
            // Expected to fail
        }

        // 5. Apply Resolutions
        for (const res of resolutions) {
            const absPath = path.join(repoPath, res.path);
            await fs.writeFile(absPath, res.content);
            await git.add(res.path);
        }

        // Verify no conflicts remain
        const status = await git.status();
        if (status.conflicted.length > 0) {
            throw new Error(`Not all conflicts resolved. Remaining: ${status.conflicted.join(", ")}`);
        }

        // 6. Commit
        await git.commit(`Merge pull request #${queueItem.pullRequest.number} from ${headBranch} (Conflict Resolved)`);

        // 7. Push (We are pushing to base branch directly here effectively merging it)
        // Wait, normally queue worker merges to a specific commit or base.
        // If we commit here, we are updating the local repo.
        // We need to return success/failure so the caller can update DB.

        // Actually, conflicts usually happen when we try to merge PR -> Base.
        // If we resolve it here, we have effectively performed the merge.
        // We should push the update to 'baseBranch' remote.
        await git.push("origin", baseBranch);

        return { success: true };

    } catch (e) {
        logger.error("Failed to resolve conflicts", e);
        try { await git.merge(["--abort"]); } catch { }
        throw e;
    } finally {
        await releaseRepo(repoOwner, repoName, true);
    }
}