Skip to content

main…± reports a full diff for branches ⊂ says are already integrated #3842

Description

@max-sixty

A squash-merged branch shows with its row dimmed, and +305 -42 in main…± immediately beside it. Both are right: is a content check against main's tip, main…± is the three-dot diff from the merge-base, and a squash merge doesn't move the merge-base. Read together they contradict each other, and the bright counts carry further than the dim symbol.

The idea: measure main…± as what merging would add rather than what the branch wrote, i.e. git diff <target-tree> <tree from merge-tree --write-tree target branch>. That is the quantity the integration check already computes as a boolean (merge_adds_nothing), so the column and the symbol beside it would be reading the same thing.

Performance

merge-tree already runs on every row. Status is a default column, its required_tasks() include MergeTreeConflicts and WouldMergeAdd, the in-memory merge_tree map collapses both to a single subprocess, and MergeTreeOutcome::Clean already carries the resulting tree SHA. So the added operation is one tree-to-tree diff, and it replaces the three-dot diff rather than adding to it.

repo tree-to-tree (proposed) three-dot (today)
259 commits 11.2 ms ± 0.5 10.9 ms ± 0.4
git.git, 81,908 commits 11.8 ms ± 0.3 11.5 ms ± 1.4

For scale, a cold wt list over 17 worktrees measured 390 ms against ~160 ms warm, so the entire cold git budget is ~230 ms and this shifts nothing within it.

Caching

Cache the LineDiff, never the tree SHA. merge-tree --write-tree writes objects that nothing references, so a persisted tree SHA can be pruned by git gc and a later read would fail; the computed stats carry no such dependency.

The natural key is merge-add-probe's own, (target_sha, branch_sha). That can be a new kind beside it or a second field on MergeProbeResult. read_json treats a deserialize failure as a miss, so adding a field silently invalidates every existing merge-add-probe entry, where a new kind leaves them intact; both self-heal within one cold run. Warm runs then spawn no extra subprocess at all, exactly as diff-stats behaves today. The diff-stats entries for (main, branch) pairs would go unused and age out through the 5000-entry LRU.

Against it

The number moves for ordinary branches too, not only squash-merged ones. Merging resolves the branch against main's newer content, so wherever main has touched the same regions the totals shift by a few lines.

Measured across one repo's branches
ahead / behind three-dot merge-add
1 / 40 +77 -5 +77 -5
1 / 4 +379 -47 +382 -47
3 / 6 +627 -51 +662 -46
4 / 4 +1564 -281 +1571 -280
1 / 4 (squash-merged) +305 -42 +0 -0

Being behind is not what moves it; the branch 40 commits behind is identical. Overlap is.

It also doesn't cover every squash merge. When merge-tree conflicts there is no tree to diff, and those rows fall back to the three-dot number. Some are , but patch_id_match rows are integrated precisely because merge-tree conflicted, which is the squash merge where main later touched the same files. So the one integration reason reached through a conflict keeps its inflated count.

Two smaller costs. main…± is named for the three-dot form and preview tab 3 shows that diff, so both would want renaming. And if [list] columns narrows to main…± without Status, merge-tree becomes newly required, where BranchDiff needs only its own task today.

Smaller alternative

Dim or blank the count cells when main_state is integrated. It is known before render and already dims the rest of the row, so extending it to the counts needs no new git work and leaves ordinary branches' numbers untouched. It also covers every integration reason, patch_id_match included, which the merge-tree approach cannot. That settles the contradiction without deciding what the column should mean.

#3280 moved these columns onto the upstream-aware base "the integration column already uses". This is the same idea one step further: taking that check's result, not only its base.

Measurements are from macOS, on a 259-commit repo and on git.git.

This was written by Claude Code on behalf of max-sixty

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions