From ef0cac750952ae459f2bbe9944e14424105b0a95 Mon Sep 17 00:00:00 2001 From: Volodymyr Vriukalo <0@zitro.id> Date: Sat, 22 Aug 2026 01:01:41 +0200 Subject: [PATCH] branch: do not track a start point with no ref Forcing a branch to a commit that no ref points at aborts when both `submodule.recurse` and `submodule.propagateBranches` are set and the repository has a remote configured: BUG: refspec.c:442: refspec_find_match: need either src or dst Aborted (core dumped) `create_branches_recursively()` resolves the start point through `dwim_branch_start()`, which leaves `branch_point` NULL when the start point names no ref -- an object id, or a revision expression such as `HEAD~0`. That NULL becomes `tracking_name`, and the `setup_tracking()` call below it is guarded on `track` alone. `setup_tracking()` assigns it to `tracking.spec.dst` without checking, then hands the spec to `for_each_remote()`, so `refspec_find_match()` receives a query with neither src nor dst and trips its assertion. `for_each_remote()` never reaches that callback where no remote is configured, which is why the abort needs one. 961b130d20 (branch: add --recurse-submodules option for branch creation, 2022-01-28) added the call with no guard at all. 75388bf5b4 (branch: support more tracking modes when recursing, 2022-03-29) added the guard on `track`. Updating the branch happens before the abort, so the command does what was asked and then exits 134. Callers that check the exit status therefore see a failure that did not happen, and one that rolls back on failure would undo a successful update. `create_branch()` already declines this: it calls `setup_tracking()` under `if (real_ref && track)`, leaving tracking unset when the start point resolved to no ref. Make the recursive path agree. Checking for NULL inside `setup_tracking()` would also silence the abort, but it would put the decision in the callee for one caller that has the answer already, and leave the two creation paths disagreeing about when tracking is set up. Reproducing it needs all four of: - `submodule.recurse=true` - `submodule.propagateBranches=true` - a configured remote - a start point that is not a ref name Submodules take no part, so the new test builds a repository with neither a submodule nor a `.gitmodules`, where `propagateBranches` is set and has nothing to propagate to. Assisted-by: An LLM. Signed-off-by: Volodymyr Vriukalo <0@zitro.id> Signed-off-by: Junio C Hamano --- branch.c | 2 +- t/t3207-branch-submodule.sh | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/branch.c b/branch.c index 22f4f46b96..e26301197d 100644 --- a/branch.c +++ b/branch.c @@ -840,7 +840,7 @@ void create_branches_recursively(struct repository *r, const char *name, * tedious to determine whether or not tracking was set up in the * superproject. */ - if (track) + if (tracking_name && track) setup_tracking(name, tracking_name, track, quiet); for (i = 0; i < submodule_entry_list.entry_nr; i++) { diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh index 4afb2d3198..97d057959d 100755 --- a/t/t3207-branch-submodule.sh +++ b/t/t3207-branch-submodule.sh @@ -98,6 +98,23 @@ test_expect_success 'should respect submodule.recurse when creating branches' ' ) ' +test_expect_success 'should move a branch to a start point that names no ref' ' + test_when_finished "rm -rf no-submodules" && + git init no-submodules && + ( + cd no-submodules && + test_commit one && + test_commit two && + git remote add origin . && + git config submodule.propagateBranches true && + git config submodule.recurse true && + git branch branch-a HEAD~1 && + oid=$(git rev-parse HEAD) && + git branch -f branch-a "$oid" && + test_cmp_rev HEAD branch-a + ) +' + test_expect_success 'should ignore submodule.recurse when not creating branches' ' test_when_finished "reset_test" && (