Repository navigation
fix(core): keep branch commits made by a failing post-checkout hook - #11300
Conversation
gitCreateBranch treated any non-zero git checkout -b result as a half-created branch and unconditionally ran git branch -D during rollback. When a post-checkout hook had already created a commit on the new branch, that delete discarded the commit (unreachable via any ref). On rollback, compare the new branch's HEAD with the start commit and keep the branch when it has moved — matching raw git's behavior of leaving the branch in place. Branches without new commits are still removed as before. Fixes QwenLM#11253
yiliang114
left a comment
There was a problem hiding this comment.
Approving at this head.
The round-3 bot CR has been stale since the author's fixes landed and every thread was resolved with a cited SHA. I re-checked the one Critical at this head rather than trusting the resolution:
- R3-1 — the keep-guard no longer requires a non-empty
startCommit. At headgit-branches.ts:481it readsif (newHead && newHead !== startCommit), so when the pre-checkoutrev-parsecollapsed to''(timeout, contended object store, or the TOCTOU fetch race),newHead !== ''is always true for a resolvednewHeadand the code takes the keep arm (:482-484, which also logs the warning) instead of the destructivebranch -Dat:486. That is exactly the minimal fix the finding proposed — the destructive arm now fires only whennewHeaditself fails to resolve or the head genuinely did not move. - R1-1 (
startCommitcaptured with an unpeeledgit rev-parse), R2-1 (||vs??), R2-2/R3-2/R3-3 — all resolved with author replies citing17e7833b/327b38a/c197a04aeand concrete mechanisms; round-3 ledger isfloor: "o"with no live Critical.
The change stays in its stated scope (one guard + its tests, +159/-2).
Local verification — merge-ready, no blockersI rebuilt the failure from #11253 in a real environment rather than reading the diff: real repositories, real The fix is correct, effective and well covered. I found nothing blocking. The one thing I would follow up on is that a sibling copy of the same rollback lives in the CLI and is untouched (N1). What I ran
Scenario matrix9 of 21 scenarios flip from silent data loss to preserved, 10 are identical on both arms, none regressed. Wherever the hook creates a commit, the head arm's outcome now matches what plain Two notes that keep the severity honest:
Does the added test discriminate the fix?Yes, in both directions. Three of the four added tests fail against the base's production source, and three targeted mutations of the fix are each killed by the suite. The single failure present in every run ( End to end on a real daemon
Non-blocking observationsN1 — the same rollback shape survives in the CLI. N2 — on the only production caller, the keep is completely silent. Round 1 already noted the N3 — the guard tests "the ref moved", not "the branch gained history". const rewound = startCommit
? await runGit(cwd, ['merge-base', '--is-ancestor', newHead, startCommit], env)
.then(() => true)
.catch(() => false)
: false;
if (newHead && newHead !== startCommit && !rewound) {Your call whether that earns an extra subprocess on the failure path. N4 — two shapes the new tests do not cover. The rewind shape from N3, and Not verified
Status at
|
|
@qwen-code /triage |
main landed QwenLM#11300, which stopped deleting a freshly created branch when a failing post-checkout hook had committed to it. This branch fixes the same bug with an atomic compare-and-delete plus typed rollback errors, which subsumes that guard, so keep this implementation and retain main's debug warning on both paths that preserve the branch. Both sides also opened the same rollback describe block in the tests, so keep main's warn-spy reset and its four tests alongside this branch's nine.



What this PR does
Fixes a data-loss path in branch creation rollback.
gitCreateBranchtreats any non-zerogit checkout -bresult as a half-created branch and unconditionally runsgit branch -D <name>during rollback. Butgit checkout -bcreates the ref and switches HEAD before running thepost-checkouthook, so a failing hook leaves the workspace already on the new branch — and if that hook created a commit there, the force-delete discards it (git fsck --unreachablethen reports the commit as unreachable from any ref).On rollback, the new branch's HEAD is now compared against the start commit (the resolved
startPoint, or the pre-checkoutHEAD). The branch is only force-deleted when its HEAD has not moved; when a failing hook has moved it (created commits), the branch is kept and adebugLogger.warnis emitted instead.Why it's needed
A failing
post-checkouthook currently causesgitCreateBranchto delete a branch that may already carry hook-created commits, silently losing that work. Rawgit checkout -b <name> --with the same failing hook leaves the branch checked out and keeps the commit, so the rollback was stricter than git itself.Reviewer Test Plan
How to verify
Install a
post-checkouthook that commits then exits non-zero, then callgitCreateBranch(repo, 'topic'):topicis force-deleted — the hook-created commit becomes unreachable.topicis kept because it carries the hook-created commit, which stays reachable from the branch.Covered by
packages/core/src/utils/git-branches.test.ts: the newkeeps a branch that a failing post-checkout hook committed tofails before this change and passes after; the existingrolls back a branch created before a failing post-checkout hook(hook makes no commit) still passes, confirming the no-commit path is unchanged.Evidence (Before & After)
N/A (core git utility; unit tests above). Verified locally on Windows with the minimal hook from the issue:
git checkout -b topic --exits1with the branch already created and pointing at the hook-created commit, and the old rollback deleted it (git fsck --unreachableconfirmed the commit was orphaned).Tested on
Environment (optional)
N/A — unit tests only (
npx vitest run src/utils/git-branches.test.ts).Risk & Scope
Linked Issues
Fixes #11253
中文说明
本 PR 修复分支创建回滚中的一处数据丢失。
gitCreateBranch把任何非零的git checkout -b结果都当作"半创建分支",回滚时无条件执行git branch -D。但git checkout -b会先创建 ref 并切换 HEAD、再运行post-checkout钩子,所以钩子失败时工作区其实已在新分支上——若钩子已在新分支上创建了提交,强制删除会丢弃该提交(git fsck --unreachable会显示它不再可从任何引用到达)。修复后,回滚时比较新分支的 HEAD 与起点 commit(解析后的
startPoint,或 checkout 前的HEAD):仅当 HEAD 未移动时才强制删除分支;若失败的钩子已移动它(创建了提交),则保留分支并通过debugLogger.warn记录,而不是丢弃这些工作。之所以需要:当前失败的
post-checkout钩子会让gitCreateBranch删掉一个可能已带钩子提交的分支,静默丢失工作。原生git checkout -b <name> --在同样的钩子失败下会保留分支和提交,因此原回滚比 git 本身更激进。验证方式:安装一个"先提交再以非零退出"的
post-checkout钩子,然后调用gitCreateBranch(repo, 'topic')。修复前:调用拒绝、HEAD 恢复、topic被强删、钩子提交变为不可达;修复后:调用仍拒绝、HEAD 恢复,但topic因携带钩子提交而被保留、提交仍可达。新增测试keeps a branch that a failing post-checkout hook committed to修复前失败、修复后通过;既有测试(钩子不产生提交时删除分支)前后均通过。已在 Windows 上用 issue 中的最小钩子实测复现。