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 1/2] 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" && ( From 9d721b060686f309b3a719fdb9f261124ca954fd Mon Sep 17 00:00:00 2001 From: Volodymyr Vriukalo <0@zitro.id> Date: Sat, 22 Aug 2026 01:01:42 +0200 Subject: [PATCH 2/2] branch: allow recursion with no tracking name Creating a branch across submodules from a commit that no ref points at fails, with the helper's usage text reprinted as an error: submodule 'sub': usage: git submodule--helper create-branch [...] fatal: submodule 'sub': cannot create branch 'branch-a' `submodule_create_branch()` runs the helper in a child process because `install_branch_config_multiple_remotes()` cannot write config into a submodule, and passes the branch name, the start oid and the tracking name as three positionals. `dwim_branch_start()` leaves the tracking name NULL where the start point named no ref, and `strvec_pushl()` stops at the first NULL, so the child receives two positionals. `module_create_branch()` requires exactly three and prints its usage. Make the third positional optional, since a start point that named no ref has no tracking name to give and the recursion has nothing to track in the submodule either. Push it separately in the caller too: relying on `strvec_pushl()` to stop early leaves the argument dropped by accident rather than by intent, and a reader has to know where the terminator falls to see that it can go missing at all. 961b130d20 (branch: add --recurse-submodules option for branch creation, 2022-01-28) introduced both sides. This is the same NULL tracking name as the previous patch, reached one step earlier: the dry-run pass over the submodules runs before the superproject's own `setup_tracking()` call, so with a submodule present this failure hides the abort that patch removes. The new test therefore needs that patch under it. Assisted-by: An LLM. Signed-off-by: Volodymyr Vriukalo <0@zitro.id> Signed-off-by: Junio C Hamano --- branch.c | 10 +++++++++- builtin/submodule--helper.c | 7 ++++--- t/t3207-branch-submodule.sh | 13 +++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/branch.c b/branch.c index e26301197d..b5182f2bb7 100644 --- a/branch.c +++ b/branch.c @@ -760,7 +760,15 @@ static int submodule_create_branch(struct repository *r, break; } - strvec_pushl(&child.args, name, start_oid, tracking_name, NULL); + /* + * The tracking name is absent when the start point named no ref. + * Push it separately: strvec_pushl() stops at the first NULL, so + * passing it inline would drop the argument by accident rather + * than by intent. + */ + strvec_pushl(&child.args, name, start_oid, NULL); + if (tracking_name) + strvec_push(&child.args, tracking_name); if ((ret = start_command(&child))) return ret; diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index e7cd3225fa..1eb73de674 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -3336,7 +3336,7 @@ static int module_create_branch(int argc, const char **argv, const char *prefix, OPT_END() }; const char *const usage[] = { - N_("git submodule--helper create-branch [-f|--force] [--create-reflog] [-q|--quiet] [-t|--track] [-n|--dry-run] "), + N_("git submodule--helper create-branch [-f|--force] [--create-reflog] [-q|--quiet] [-t|--track] [-n|--dry-run] []"), NULL }; struct repo_config_values *cfg = repo_config_values(the_repository); @@ -3345,13 +3345,14 @@ static int module_create_branch(int argc, const char **argv, const char *prefix, track = cfg->branch_track; argc = parse_options(argc, argv, prefix, options, usage, 0); - if (argc != 3) + if (argc < 2 || argc > 3) usage_with_options(usage, options); if (!quiet && !dry_run) printf_ln(_("creating branch '%s'"), argv[0]); - create_branches_recursively(the_repository, argv[0], argv[1], argv[2], + create_branches_recursively(the_repository, argv[0], argv[1], + argc > 2 ? argv[2] : NULL, force, reflog, quiet, track, dry_run); return 0; } diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh index 97d057959d..4dd26c6c0f 100755 --- a/t/t3207-branch-submodule.sh +++ b/t/t3207-branch-submodule.sh @@ -115,6 +115,19 @@ test_expect_success 'should move a branch to a start point that names no ref' ' ) ' +test_expect_success 'should recurse into submodules from a start point that names no ref' ' + test_when_finished "reset_test" && + ( + cd super && + oid=$(git rev-parse HEAD) && + git branch --recurse-submodules branch-a "$oid" && + git rev-parse branch-a && + git -C sub rev-parse branch-a && + git -C sub/sub-sub rev-parse branch-a && + git -C second/sub rev-parse branch-a + ) +' + test_expect_success 'should ignore submodule.recurse when not creating branches' ' test_when_finished "reset_test" && (