Skip to content

Commit 4ad340d

Browse files
committed
fix(merge-dialog): resolve HEAD before caching diff base
getWorktreeStatus, getBranchLog, and getBranchCommits passed the literal 'HEAD' to detectDiffBase, which used it verbatim in the 30s cache key and propagated it through refineDiffBaseWithCherryPick. When refine returned `{sha:'HEAD', ref:'HEAD'}` for a fully patch-equivalent branch, that value survived a new commit — callers ran `git log HEAD..HEAD` and reported has_committed_changes=false even with real new work, while ChangedFilesList (which calls pinHead first) showed the file correctly. Resolve 'HEAD' to its SHA at the entry of detectDiffBase so the cache key tracks HEAD movement and downstream values reference real commits. Also clear MergeDialog's createResource data via mutate(undefined) on open so unguarded reads of worktreeStatus()/mergeStatus() don't flash the previous open's snapshot during the refetch window.
1 parent b626f6a commit 4ad340d

2 files changed

Lines changed: 30 additions & 14 deletions

File tree

electron/ipc/git.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,14 @@ async function detectDiffBase(
446446
baseBranch?: string,
447447
): Promise<PickedMergeBase> {
448448
const branch = baseBranch ?? (await detectMainBranch(repoRoot));
449-
const headRef = head ?? 'HEAD';
449+
// Resolve the literal 'HEAD' to its commit SHA so the cache key tracks
450+
// HEAD movement. Otherwise a cached `{sha:'HEAD', ref:'HEAD'}` (returned
451+
// by refineDiffBaseWithCherryPick when the branch was fully patch-
452+
// equivalent to base) survives a new commit on the branch — callers then
453+
// run `git log HEAD..HEAD` against the literal and see no commits, even
454+
// though HEAD just moved forward.
455+
const requestedHead = head ?? 'HEAD';
456+
const headRef = requestedHead === 'HEAD' ? await pinHead(repoRoot) : requestedHead;
450457
const key = `${cacheKey(repoRoot)}:${branch}:${headRef}`;
451458
const cached = diffBaseCache.get(key);
452459
if (cached) {

src/components/MergeDialog.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,25 @@ export function MergeDialog(props: MergeDialogProps) {
2828

2929
const resourceSource = () =>
3030
props.open ? { path: props.task.worktreePath, baseBranch: props.task.baseBranch } : null;
31-
const [branchLog, { refetch: refetchBranchLog }] = createResource(resourceSource, (src) =>
32-
invoke<string>(IPC.GetBranchLog, { worktreePath: src.path, baseBranch: src.baseBranch }),
33-
);
34-
const [worktreeStatus, { refetch: refetchWorktreeStatus }] = createResource(
31+
const [branchLog, { refetch: refetchBranchLog, mutate: mutateBranchLog }] = createResource(
3532
resourceSource,
3633
(src) =>
34+
invoke<string>(IPC.GetBranchLog, { worktreePath: src.path, baseBranch: src.baseBranch }),
35+
);
36+
const [worktreeStatus, { refetch: refetchWorktreeStatus, mutate: mutateWorktreeStatus }] =
37+
createResource(resourceSource, (src) =>
3738
invoke<WorktreeStatus>(IPC.GetWorktreeStatus, {
3839
worktreePath: src.path,
3940
baseBranch: src.baseBranch,
4041
}),
41-
);
42-
const [mergeStatus, { refetch: refetchMergeStatus }] = createResource(resourceSource, (src) =>
43-
invoke<MergeStatus>(IPC.CheckMergeStatus, {
44-
worktreePath: src.path,
45-
baseBranch: src.baseBranch,
46-
}),
42+
);
43+
const [mergeStatus, { refetch: refetchMergeStatus, mutate: mutateMergeStatus }] = createResource(
44+
resourceSource,
45+
(src) =>
46+
invoke<MergeStatus>(IPC.CheckMergeStatus, {
47+
worktreePath: src.path,
48+
baseBranch: src.baseBranch,
49+
}),
4750
);
4851

4952
const hasConflicts = () => (mergeStatus()?.conflicting_files.length ?? 0) > 0;
@@ -67,9 +70,15 @@ export function MergeDialog(props: MergeDialogProps) {
6770
setRebaseSuccess(false);
6871
setMerging(false);
6972
setRebasing(false);
70-
// Force fresh data on every open — covers edge cases where
71-
// createResource source tracking alone misses a refresh
72-
// (e.g. external rebase by AI agent while dialog was closed).
73+
// Drop the previous open's cached data so accessors return undefined
74+
// during refetch — otherwise unguarded reads (uncommitted-changes
75+
// warning, branch-mismatch banner) flash the stale snapshot until the
76+
// new fetch resolves. Then trigger refetch as a safety net for cases
77+
// where source tracking alone misses (e.g. external rebase by AI
78+
// agent while dialog was closed).
79+
mutateBranchLog(undefined);
80+
mutateMergeStatus(undefined);
81+
mutateWorktreeStatus(undefined);
7382
refetchBranchLog();
7483
refetchMergeStatus();
7584
refetchWorktreeStatus();

0 commit comments

Comments
 (0)