Skip to content

gc: detect squash-merged branches - #25

Merged
github-actions[bot] merged 2 commits into
mainfrom
improve/F-1-gc-detect-squash-merged-branches
Nov 1, 2026
Merged

github-actions[bot] merged 2 commits into
mainfrom
improve/F-1-gc-detect-squash-merged-branches

Conversation

@zolotarev

@zolotarev zolotarev commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Friction

In a project where merge requests into the main branch are squash-merged, dpcli gc --remove printed "no merged branches" after the module was merged. The module branch and its task branch were never ancestors of the main branch, so git branch --merged did not list them, and their copies and branches had to be removed by hand.

Seen 1 time: one module, 2 branches (module branch + task branch), ledger entry F-1. A squash merge into the main branch is the default in that project, so every module ends this way.

Change

squash_merged() in commands/branches.py, used by gc:

  • A branch counts as squash-merged when its net diff from the merge-base (a temporary commit-tree of its tree) has a patch-id equivalent in the main branch (git cherry prints -).
  • Branches that are ancestors of a squash-merged branch also count (task branches merged into the module branch).
  • A branch whose net diff is empty is never matched.
  • Matched branches are deleted with branch -D, because -d refuses them. The gc_live_reason and dirty-copy checks still apply.
  • Cost: a few git calls per unmerged local branch, and one dangling commit object each, collected by git gc. This is noted in the docstring.

Tests

tests/test_gc.py:

  • test_squash_merged: a module branch and its task branch are removed after a squash; an unrelated branch is kept.
  • test_squash_then_more_work_and_empty_diff_kept: a branch that got a commit after its squash is kept, and so is a branch whose net diff is empty.

Full suite: 207 OK. VERSION 35, CHANGELOG line.

@zolotarev zolotarev added the feedback Friction reported from real use label Oct 31, 2026
@github-actions

Copy link
Copy Markdown

VERDICT: rework

  • Not anonymized: the PR body names a project ("ashoo") and quotes the friction in Russian. Public repo rule: no project data. Rewrite the body as an English, anonymized conclusion.
  • Evidence is thin: "Seen ×0 in 0 task(s)". The friction (squash MRs leave copies and branches that gc does not see) is plausible. Link a feedback issue or give a real count before this merges.
  • lib/dpcli/commands/branches.py:~254: branch -D is now used for every squash-detected branch. Detection is a patch-id match, and the second pass treats any ancestor of a squashed branch as safe. Both are heuristics that decide whether work gets force-deleted. Add a test where the branch gets one more commit after the squash and must be kept. Add one for an empty-diff branch (tree equals merge-base tree), whose git cherry result is not obvious.
  • squash_merged runs commit-tree once per unmerged local branch on every gc. That writes a dangling object each time and costs a few git calls per branch. Acceptable, but say so in the docstring, or skip branches that have a worktree with uncommitted work.
  • Process is fine: VERSION bumped to 35 and a CHANGELOG line added. The CHANGELOG line is long, so shorten it. I did not run the tests in this review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zolotarev

Copy link
Copy Markdown
Owner Author

Rework done (76c44ee):

  • The body is anonymized, in English, and gives the real count (1 module, 2 branches).
  • New test for a branch with work after the squash (kept) and a branch with an empty net diff (kept, with an explicit guard).
  • The docstring notes the cost and the dangling objects.
  • The CHANGELOG line is shortened.

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

VERDICT: accept

  • Real friction (feedback label, ledger F-1): squash-merged module and task branches stayed because git branch --merged cannot see them. The fix is small and local to gc.
  • Safe: -D is used only for branches whose whole net diff has a patch-id match in main (git cherry). Branches with later commits or an empty diff are never matched. gc_live_reason, the dirty-copy and symlink checks still apply. A false negative only keeps a branch.
  • Tests: test_squash_merged and test_squash_then_more_work_and_empty_diff_kept cover the match, the ancestor (task branch) case, an unrelated branch, work after the squash, and an empty diff.
  • Process: VERSION 35 and a CHANGELOG line are present, in English, with no project data.
  • Nit: b not in out in the ancestor comprehension at lib/dpcli/commands/branches.py:216 is redundant. rest is already filtered, and the any(...) over out is enough.
  • Nit: squash_merged runs on every gc call, including the dry run, at a few git calls per unmerged branch. That is acceptable, and the docstring says so.
  • No tools/budgets.json change. The +21 lines look reasonable for the feature.

@github-actions github-actions Bot removed the rework label Nov 1, 2026
@github-actions
github-actions Bot merged commit 34d21a5 into main Nov 1, 2026
3 checks passed
@github-actions
github-actions Bot deleted the improve/F-1-gc-detect-squash-merged-branches branch November 1, 2026 00:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feedback Friction reported from real use

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant