diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index 5c202fccf5abc..3c58675d6f6d9 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -5,6 +5,7 @@ import { assert, describe, it } from "vite-plus/test"; import { buildMenuItems, requiresDefaultBranchConfirmation, + resolveChangesTotals, resolveDefaultBranchActionDialogCopy, resolveLiveThreadBranchUpdate, resolveQuickAction, @@ -247,3 +248,53 @@ describe("resolveAutoFeatureBranchName", () => { assert.equal(ref, "feature/ticket-1-2"); }); }); + +describe("resolveChangesTotals", () => { + it("does not let an empty branchChanges hide a dirty working tree", () => { + assert.deepEqual( + resolveChangesTotals( + status({ + hasWorkingTreeChanges: true, + branchChanges: { baseRef: "main", insertions: 0, deletions: 0 }, + workingTree: { + files: [{ path: "a.ts", insertions: 5, deletions: 1 }], + insertions: 5, + deletions: 1, + }, + }), + ), + { insertions: 5, deletions: 1 }, + ); + }); + + it("keeps non-zero branchChanges over the working tree", () => { + assert.deepEqual( + resolveChangesTotals( + status({ + branchChanges: { baseRef: "main", insertions: 20, deletions: 3 }, + workingTree: { + files: [{ path: "a.ts", insertions: 1, deletions: 0 }], + insertions: 1, + deletions: 0, + }, + }), + ), + { insertions: 20, deletions: 3 }, + ); + }); + + it("falls back to the working tree when branchChanges is omitted", () => { + assert.deepEqual( + resolveChangesTotals( + status({ + workingTree: { + files: [{ path: "a.ts", insertions: 2, deletions: 0 }], + insertions: 2, + deletions: 0, + }, + }), + ), + { insertions: 2, deletions: 0 }, + ); + }); +}); diff --git a/apps/web/src/components/GitActionsControl.logic.ts b/apps/web/src/components/GitActionsControl.logic.ts index 98a55c3577e01..ca396893accda 100644 --- a/apps/web/src/components/GitActionsControl.logic.ts +++ b/apps/web/src/components/GitActionsControl.logic.ts @@ -425,3 +425,28 @@ export function resolveLiveThreadBranchUpdate(input: { branch: input.gitStatus.refName, }; } + +export type ChangesLineTotals = { + readonly insertions: number; + readonly deletions: number; +}; + +// `branchChanges ?? workingTree` treats `{0,0}` as present, so a zero branch total hid a dirty +// working tree and the Changes row read +0 while Diff still showed the work. +export function resolveChangesTotals( + status: VcsStatusResult | null | undefined, +): ChangesLineTotals | null { + if (!status) return null; + const branch = status.branchChanges; + const tree = status.workingTree; + if (branch && (branch.insertions > 0 || branch.deletions > 0)) { + return { insertions: branch.insertions, deletions: branch.deletions }; + } + if (tree.insertions > 0 || tree.deletions > 0) { + return { insertions: tree.insertions, deletions: tree.deletions }; + } + if (branch) { + return { insertions: branch.insertions, deletions: branch.deletions }; + } + return { insertions: tree.insertions, deletions: tree.deletions }; +} diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index a125ceacaed24..6afb76c3760e9 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -52,6 +52,7 @@ import { type GitQuickAction, type DefaultBranchConfirmableAction, requiresDefaultBranchConfirmation, + resolveChangesTotals, resolveDefaultBranchActionDialogCopy, resolveFeatureBranchUnavailableReason, resolveGitActionProgressPresentation, @@ -1048,7 +1049,10 @@ export default function GitActionsControl({ const isRepo = gitStatus?.isRepo ?? true; const hasPrimaryRemote = gitStatus?.hasPrimaryRemote ?? false; const gitStatusForActions = gitStatus; - const changesTotals = gitStatusForActions?.branchChanges ?? gitStatusForActions?.workingTree; + const changesTotals = resolveChangesTotals(gitStatusForActions) ?? { + insertions: 0, + deletions: 0, + }; const allFiles = gitStatusForActions?.workingTree.files ?? []; // Changed-file paths are repository-relative, so they must resolve against @@ -1118,8 +1122,8 @@ export default function GitActionsControl({ gitActionProgress === null && !gitStatusError && quickAction.disabled && - !changesTotals?.insertions && - !changesTotals?.deletions; + !changesTotals.insertions && + !changesTotals.deletions; const pendingDefaultBranchActionCopy = pendingDefaultBranchAction ? resolveDefaultBranchActionDialogCopy({ action: pendingDefaultBranchAction.action, @@ -1636,19 +1640,17 @@ export default function GitActionsControl({ - +{formatDiffCount(changesTotals?.insertions ?? 0)} + +{formatDiffCount(changesTotals.insertions)} - -{formatDiffCount(changesTotals?.deletions ?? 0)} + -{formatDiffCount(changesTotals.deletions)}