Skip to content

Commit 220af80

Browse files
authored
Add PR merge conflict detection (#16)
* Add PR conflict detection using GitHub API mergeable field - Fetch mergeable field from GitHub API in GitService - Add has_conflicts getter to PRStatus model - Display warning emoji (⚠️) for PRs with conflicts - Prioritize conflicts in highlighting system * Fix PR conflict detection to use string values from GitHub API - Changed mergeable type from boolean to string (MERGEABLE/CONFLICTING/UNKNOWN) - Updated has_conflicts getter to check for 'CONFLICTING' string value - Fixed is_ready_to_merge to require mergeable === 'MERGEABLE' - Added comments documenting the possible enum values * Rework PR conflict detection using local git operations - Added async checkMergeConflictsAsync() method to GitService - Uses local git test merge to detect conflicts (no GitHub API needed) - Added has_merge_conflicts field to GitStatus model - Created async getGitStatusAsync() method with parallel operations - Display conflicts in CHANGES column with warning emoji (⚠️) - Added async git status refresh every 5 seconds - Conflicts given high priority (Priority 2) in highlighting - Removed mergeable field from PRStatus (was using GitHub API) Benefits: - Works without network/GitHub API access - Faster local detection - Shows conflicts even for branches without PRs - Non-blocking async updates - Conflicts shown in actionable CHANGES column * Revert to GitHub API-based conflict detection - Restored mergeable field to PRStatus model (MERGEABLE/CONFLICTING/UNKNOWN) - Added back mergeable field fetching in GitService PR methods - Display conflicts in PR column with ⚠️ emoji (not CHANGES column) - Restored PR-based highlighting priorities for conflicts - Removed async git status methods and local conflict detection - Removed unused useGitStatusAsync hook - Conflicts now shown in PR column as originally implemented This reverts back to using GitHub's built-in mergeable API field for conflict detection instead of local git operations.
1 parent 396d2c4 commit 220af80

3 files changed

Lines changed: 25 additions & 12 deletions

File tree

src/components/views/MainView.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export default function MainView(props: Props) {
9191
const pr = w.pr;
9292
let prStr = '';
9393
if (pr?.number) {
94-
const badge = (pr.is_merged || pr.state === 'MERGED') ? '⟫' : pr.checks === 'passing' ? '✓' : pr?.checks === 'failing' ? '✗' : pr?.checks === 'pending' ? '⏳' : '';
94+
const badge = pr.has_conflicts ? '⚠️' : (pr.is_merged || pr.state === 'MERGED') ? '⟫' : pr.checks === 'passing' ? '✓' : pr?.checks === 'failing' ? '✗' : pr?.checks === 'pending' ? '⏳' : '';
9595
prStr = `#${pr.number}${badge}`;
9696
} else if (pr !== undefined) {
9797
prStr = '-'; // PR data loaded, no PR exists
@@ -185,7 +185,7 @@ export default function MainView(props: Props) {
185185
const pr = w.pr;
186186
let prStr = '';
187187
if (pr?.number) {
188-
const badge = (pr.is_merged || pr.state === 'MERGED') ? '⟫' : pr.checks === 'passing' ? '✓' : pr?.checks === 'failing' ? '✗' : pr?.checks === 'pending' ? '⏳' : '';
188+
const badge = pr.has_conflicts ? '⚠️' : (pr.is_merged || pr.state === 'MERGED') ? '⟫' : pr.checks === 'passing' ? '✓' : pr?.checks === 'failing' ? '✗' : pr?.checks === 'pending' ? '⏳' : '';
189189
prStr = `#${pr.number}${badge}`;
190190
} else if (pr !== undefined) {
191191
prStr = '-'; // PR data loaded, no PR exists
@@ -249,31 +249,37 @@ export default function MainView(props: Props) {
249249
}
250250
// PRIORITY 4+: PR-related priorities (only if PR status has been loaded)
251251
else if (pr !== undefined) {
252-
// PRIORITY 4: PR needs attention (failing checks, conflicts, etc.)
253-
if (pr.needs_attention) {
252+
// PRIORITY 4: PR has merge conflicts (highest PR priority)
253+
if (pr.has_conflicts) {
254+
highlightIndex = COLUMNS.PR;
255+
highlightColor = COLORS.RED;
256+
// pr-conflicts';
257+
}
258+
// PRIORITY 5: PR needs attention (failing checks, etc.)
259+
else if (pr.checks === 'failing') {
254260
highlightIndex = COLUMNS.PR;
255261
highlightColor = COLORS.RED;
256262
// pr-needs-attention';
257263
}
258-
// PRIORITY 5: PR ready to merge (positive action available)
264+
// PRIORITY 6: PR ready to merge (positive action available)
259265
else if (pr.is_ready_to_merge) {
260266
highlightIndex = COLUMNS.PR;
261267
highlightColor = COLORS.GREEN;
262268
// pr-ready-to-merge';
263269
}
264-
// PRIORITY 6: PR exists but no urgent action (informational)
270+
// PRIORITY 7: PR exists but no urgent action (informational)
265271
else if (pr.is_open && pr.number) {
266272
highlightIndex = COLUMNS.PR;
267273
highlightColor = COLORS.YELLOW;
268274
// pr-informational';
269275
}
270-
// PRIORITY 6.5: PR successfully merged (completed work)
276+
// PRIORITY 7.5: PR successfully merged (completed work)
271277
else if (pr.is_merged && pr.number) {
272278
highlightIndex = COLUMNS.PR;
273279
highlightColor = COLORS.GREEN;
274280
// pr-merged';
275281
}
276-
// PRIORITY 7: Claude idle - ready for work (when nothing else needs attention)
282+
// PRIORITY 8: Claude idle - ready for work (when nothing else needs attention)
277283
else if (w.session?.attached && (cs.includes('idle') || cs.includes('active'))) {
278284
highlightIndex = COLUMNS.AI;
279285
highlightColor = COLORS.GREEN;

src/models.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,22 @@ export class PRStatus {
3434
url?: string | null;
3535
head?: string | null;
3636
title?: string | null;
37+
mergeable?: string | null; // MERGEABLE, CONFLICTING, UNKNOWN
3738
constructor(init: Partial<PRStatus> = {}) {
3839
this.number = null;
3940
this.state = null;
4041
this.checks = null;
4142
this.loading = false;
4243
this.url = null;
4344
this.title = null;
45+
this.mergeable = null;
4446
Object.assign(this, init);
4547
}
4648
get is_merged(): boolean { return this.state === 'MERGED'; }
4749
get is_open(): boolean { return this.state === 'OPEN'; }
48-
get needs_attention(): boolean { return this.checks === 'failing'; }
49-
get is_ready_to_merge(): boolean { return this.state === 'OPEN' && this.checks === 'passing' && !this.loading; }
50+
get has_conflicts(): boolean { return this.mergeable === 'CONFLICTING'; }
51+
get needs_attention(): boolean { return this.checks === 'failing' || this.has_conflicts; }
52+
get is_ready_to_merge(): boolean { return this.state === 'OPEN' && this.checks === 'passing' && this.mergeable === 'MERGEABLE' && !this.loading; }
5053
}
5154

5255
export class SessionInfo {

src/services/GitService.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export class GitService {
120120
return status;
121121
}
122122

123+
123124
createWorktree(project: string, featureName: string, branchName?: string): boolean {
124125
const mainRepo = path.join(this.basePath, project);
125126
const branchesDir = path.join(this.basePath, `${project}${DIR_BRANCHES_SUFFIX}`);
@@ -224,7 +225,7 @@ export class GitService {
224225
// PR Status Methods
225226
batchFetchPRData(repoPath: string, opts: {includeChecks?: boolean; includeTitle?: boolean} = {}): Record<string, PRStatus> {
226227
const prByBranch: Record<string, PRStatus> = {};
227-
const fields = ['number', 'state', 'headRefName'];
228+
const fields = ['number', 'state', 'headRefName', 'mergeable'];
228229
const includeChecks = opts.includeChecks !== false;
229230
const includeTitle = opts.includeTitle !== false;
230231

@@ -249,6 +250,7 @@ export class GitService {
249250
}
250251
if (pr.url) (status as any).url = pr.url;
251252
if (includeTitle && pr.title) (status as any).title = pr.title;
253+
(status as any).mergeable = pr.mergeable ?? null;
252254

253255
prByBranch[branch] = status;
254256
}
@@ -259,7 +261,7 @@ export class GitService {
259261

260262
async batchFetchPRDataAsync(repoPath: string, opts: {includeChecks?: boolean; includeTitle?: boolean} = {}): Promise<Record<string, PRStatus>> {
261263
const prByBranch: Record<string, PRStatus> = {};
262-
const fields = ['number', 'state', 'headRefName'];
264+
const fields = ['number', 'state', 'headRefName', 'mergeable'];
263265
const includeChecks = opts.includeChecks !== false;
264266
const includeTitle = opts.includeTitle !== false;
265267

@@ -284,6 +286,7 @@ export class GitService {
284286
}
285287
if (pr.url) (status as any).url = pr.url;
286288
if (includeTitle && pr.title) (status as any).title = pr.title;
289+
(status as any).mergeable = pr.mergeable ?? null;
287290

288291
prByBranch[branch] = status;
289292
}
@@ -395,6 +398,7 @@ export class GitService {
395398
}
396399
}
397400

401+
398402
private parseBranchCandidates(output: string, existing: string[], base: string): Array<[string, string]> {
399403
const candidates: Array<[string, string]> = [];
400404
const seen = new Set<string>();

0 commit comments

Comments
 (0)