Skip to contentSkip to conversation

branch: move multiple branches in a single --force

PATCH In review Andrea Stacchiotti via GitGitGadget 3 participants 10 messages Jun 10, 2025 – Jun 12, 2025 Documentation

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 branch: move multiple branches in a single --force

From: Andrea Stacchiotti <[email protected]>

Using either the 1-arg or 2-args form of --force it is possible to only move one branch at a time, to HEAD and <arg2> respectively.

Allow moving multiple branches to a single target by giving 'git branch --force b1 b2 b3 ... dest' cp-like semantics, all the branches are moved/created to 'dest'.

The convention extends the 2-args form in the same way 'cp a b c ... dest' would do.

There could be another potential interpretation of --force a b c: moving all 3 to HEAD, but the 2-args form already changed the semantics to cp-like instead of appending an implicit HEAD, so this seems the least surprising way to support multiple moves.

No such change is done to the move/copy paths,
as such paths would error out anyway by trying
to create multiple branches of the same name.

---
branch: Move multiple branches in a single --force

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1992%2F12345ieee%2Fmaster-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1992/12345ieee/master-v1
Pull-Request: https://github.com/git/git/pull/1992

 Documentation/git-branch.adoc |  2 +-
 builtin/branch.c              | 26 ++++++++++++++------------
 t/t3200-branch.sh             |  7 +++++++
 3 files changed, 22 insertions(+), 13 deletions(-)
Documentation/git-branch.adoc +1 -1
index c0afddc424d..817b4a5d3f6 100644
@@ -17,7 +17,7 @@ git branch [--color[=<when>] | --no-color] [--show-current]
 	   [(-r|--remotes) | (-a|--all)]
 	   [--list] [<pattern>...]
 git branch [--track[=(direct|inherit)] | --no-track] [-f]
-	   [--recurse-submodules] <branch-name> [<start-point>]
+	   [--recurse-submodules] <branch-name>... [<start-point>]
 git branch (--set-upstream-to=<upstream>|-u <upstream>) [<branch-name>]
 git branch --unset-upstream [<branch-name>]
 git branch (-m|-M) [<old-branch>] <new-branch>
builtin/branch.c +14 -12
index c150131bd9f..8ba04568c15 100644
@@ -31,7 +31,7 @@
 
 static const char * const builtin_branch_usage[] = {
 	N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),
-	N_("git branch [<options>] [-f] [--recurse-submodules] <branch-name> [<start-point>]"),
+	N_("git branch [<options>] [-f] [--recurse-submodules] <branch-name>... [<start-point>]"),
 	N_("git branch [<options>] [-l] [<pattern>...]"),
 	N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
 	N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"),
@@ -992,9 +992,9 @@ int cmd_branch(int argc,
 		strbuf_addf(&buf, "branch.%s.merge", branch->name);
 		git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE);
 		strbuf_release(&buf);
-	} else if (!noncreate_actions && argc > 0 && argc <= 2) {
-		const char *branch_name = argv[0];
-		const char *start_name = argc == 2 ? argv[1] : head;
+	} else if (!noncreate_actions && argc > 0) {
+		const char *start_name = argc == 1 ? head : argv[argc - 1];
+		int iters = argc == 1 ? 1 : argc - 1;
 
 		if (filter.kind != FILTER_REFS_BRANCHES)
 			die(_("the -a, and -r, options to 'git branch' do not take a branch name.\n"
@@ -1003,15 +1003,17 @@ int cmd_branch(int argc,
 		if (track == BRANCH_TRACK_OVERRIDE)
 			die(_("the '--set-upstream' option is no longer supported. Please use '--track' or '--set-upstream-to' instead"));
 
-		if (recurse_submodules) {
-			create_branches_recursively(the_repository, branch_name,
-						    start_name, NULL, force,
-						    reflog, quiet, track, 0);
-			ret = 0;
-			goto out;
+		for (int i = 0; i < iters; i++) {
+			const char *branch_name = argv[i];
+
+			if (recurse_submodules)
+				create_branches_recursively(the_repository, branch_name,
+								start_name, NULL, force,
+								reflog, quiet, track, 0);
+			else
+				create_branch(the_repository, branch_name, start_name, force, 0,
+						reflog, quiet, track, 0);
 		}
-		create_branch(the_repository, branch_name, start_name, force, 0,
-			      reflog, quiet, track, 0);
 	} else
 		usage_with_options(builtin_branch_usage, options);
 
t/t3200-branch.sh +7 -0
index f3e720dc10d..7cd31ca7820 100755
@@ -60,6 +60,13 @@ test_expect_success 'git branch --force abc should succeed when abc exists' '
 	test_cmp expect actual
 '
 
+test_expect_success 'git branch --force br1 br2 abc should create 2 new branches' '
+	git branch --force br1 br2 abc &&
+	test_ref_exists refs/heads/br1 &&
+	test_ref_exists refs/heads/br2 &&
+	git branch -d br1 br2
+'
+
 test_expect_success 'git branch a/b/c should create a branch' '
 	git branch a/b/c &&
 	test_ref_exists refs/heads/a/b/c

base-commit: 4c0e625c091d4c648cec7319bafaed3cc81658e5

Signature · 1 line
gitgitgadget
#1 lore raw

"Andrea Stacchiotti via GitGitGadget" <[email protected]>

Quoted 5 lines · writes:

From: Andrea Stacchiotti <[email protected]>

Using either the 1-arg or 2-args form of --force
it is possible to only move one branch at a time,
to HEAD and <arg2> respectively.

If you are renaming (or "moving") a branch that is not checked out anywhere to a new name that is not in use, you do not even need to force. You can just do:

git branch -m old new

You are not moving branches without "-m".

What you are doing is to point a branch A to point at a commit X with

git branch A X

Your proposed log message talks about "--force" too much; if you are creating a branch, you need "--force" only when the name you want to use is already taken. Pointing the branch tip to a commit is not inherently tied to "--force", but your description gives a false impression that you are adding a special feature when "--force" is used. The proposed log message needs rewritten.

If there is not yet a branch A, you do not even need "--force" on this command line. Also take a special note that "X" does not have to be a branch name. It only has to resolve to a commit, so this is also valid:

git branch [--force] A X~4

I can understand that it may appear to be handy to be able to set multiple branches at the same time with

git branch A X~4 B X~3 C X~2 (* does not exist *)

with or without "--force". If none of A, B, or C exist, they can be created from these three comits X~4, X~3, and X~2.

Or you could propose a different syntax to create branches pointing at the same commit

git branch A B C origin/master (* does not exist *)

But either syntax to create multiple branches feel somewhat inadequate. What should happen to their associated configuration data like branch.A.remote and branch.B.merge? Should they all point at the same remote & a branch at the remote? How would that make having multiple of them useful?

#2 lore raw

Thank you for your review, the concerns are fair, I'll reply point-to-point.

The general issue is that AFAIK the only way git has to say:
"point non-checked-out branch B to commit-ish X, leave B's config intact, if B doesn't exist create it" is git branch --force B X.

This is likely an abuse of branch -f, it might be better giving it another flag
altogether, like -p/--point-to.

branch --force is massively overloaded and most of its functionality is tied to --move or --copy, so changing branch names, not repointing branches.

This patch aims to make repointing multiple branches to the same commit-ish easier, currently it needs a shell loop.

I'd like this functionality for gitops repos, where each branch is an env and I want to update dozens of envs to the last config state.

Il giorno mar 10 giu 2025 alle ore 23:25 Junio C Hamano <[email protected]> ha scritto:

Quoted 14 lines

"Andrea Stacchiotti via GitGitGadget" <[email protected]>

Quoted 5 lines · writes:

From: Andrea Stacchiotti <[email protected]>

Using either the 1-arg or 2-args form of --force
it is possible to only move one branch at a time,
to HEAD and <arg2> respectively.

If you are renaming (or "moving") a branch that is not checked out anywhere to a new name that is not in use, you do not even need to force. You can just do:

Quoted 8 lines

git branch -m old new

You are not moving branches without "-m".

What you are doing is to point a branch A to point at a commit X with

git branch A X

Indeed, this is about repointing, not changing branch names.

Quoted 6 lines

Your proposed log message talks about "--force" too much; if you are creating a branch, you need "--force" only when the name you want to use is already taken. Pointing the branch tip to a commit is not inherently tied to "--force", but your description gives a false impression that you are adding a special feature when "--force" is used. The proposed log message needs rewritten.

Right, it's that I'm using "force create branch A onto X, delete A if it existed"
as "repoint branch A to X, create it if needed", I likely have a skewed view on it.
This usecase might be better served by a new flag.

Quoted 6 lines

If there is not yet a branch A, you do not even need "--force" on this command line. Also take a special note that "X" does not have to be a branch name. It only has to resolve to a commit, so this is also valid:

git branch [--force] A X~4

Yeah, it was not clear from my message, nothing about the change is about renaming a branch, my usecase is about repointing to a generic commit-ish, like you say.

Quoted 7 lines

I can understand that it may appear to be handy to be able to set multiple branches at the same time with

git branch A X~4 B X~3 C X~2 (* does not exist *)

with or without "--force". If none of A, B, or C exist, they can be created from these three commits X~4, X~3, and X~2.

I'm not opposed to implement this if it's preferred, but I liked the cp-like syntax as it's less repeating and the average shell user is already exposed to it via cp or mv.

Quoted 11 lines

Or you could propose a different syntax to create branches pointing at the same commit

git branch A B C origin/master (* does not exist *)

But either syntax to create multiple branches feel somewhat inadequate. What should happen to their associated configuration data like branch.A.remote and branch.B.merge? Should they all point at the same remote & a branch at the remote? How would that make having multiple of them useful?

This is closer to the intended syntax and use of the feature, I'm more focused on branch repoints but I'd also use this when I create, say, the staging and prod env of client newclient via
git branch [-f] newclient-prod newclient-stag <commit-ish>

The branches are useful because they are then supposed to evolve independently and track the real world state of the two envs.

If <commit-ish> is a remote branch the implementation sets up tracking, but this is really not the intended usecase, once the branches are created their remote tracking
is set at push time to <default remote>/<branch name> as per default push config.

Thanks for your review, I hope my intent is clearer.

#3 lore raw
Quoted 2 lines · Andrea Stacchiotti

This patch aims to make repointing multiple branches to the same commit-ish easier, currently it needs a shell loop.

Or "update-ref --stdin"?

#4 lore raw

Il giorno mer 11 giu 2025 alle ore 02:22 Junio C Hamano <[email protected]> ha scritto:

Quoted 7 lines
Quoted 2 lines · Andrea Stacchiotti

This patch aims to make repointing multiple branches to the same commit-ish easier, currently it needs a shell loop.

Or "update-ref --stdin"?

I learned something new, but I'd still like to keep advocating for a syntax like branch --some-flag A B C X instead of feeding by hand update-ref commands.

#5 lore raw
Quoted 5 lines · Andrea Stacchiotti

Il giorno mer 11 giu 2025 alle ore 02:22 Junio C Hamano <[email protected]> ha scritto:

Quoted 3 lines

Andrea Stacchiotti <[email protected]> writes:

Quoted 8 lines
Quoted 4 lines
Quoted 2 lines

This patch aims to make repointing multiple branches to the same commit-ish easier, currently it needs a shell loop.

Or "update-ref --stdin"?

I learned something new, but I'd still like to keep advocating for a syntax like branch --some-flag A B C X instead of feeding by hand update-ref commands.

I am personally not interested in such a mode, I do not know why you think "--some-flag" is needed when the command can figure out from the number of things on the command line being more than 2 just fine.

But my comment was targetted against "it needs a shell loop" in the justification in the proposed log message, which is not quite correct.

#6 lore raw

So, if I may ask, is the proposed patch as written (branch -f A B C X) acceptable and you just need me to rewrite the commit message or are you not interested in it at all?

Il giorno mer 11 giu 2025 alle ore 17:26 Junio C Hamano <[email protected]> ha scritto:

Quoted 26 lines
Quoted 5 lines · Andrea Stacchiotti

Il giorno mer 11 giu 2025 alle ore 02:22 Junio C Hamano <[email protected]> ha scritto:

Quoted 3 lines

Andrea Stacchiotti <[email protected]> writes:

Quoted 8 lines
Quoted 4 lines
Quoted 2 lines

This patch aims to make repointing multiple branches to the same commit-ish easier, currently it needs a shell loop.

Or "update-ref --stdin"?

I learned something new, but I'd still like to keep advocating for a syntax like branch --some-flag A B C X instead of feeding by hand update-ref commands.

I am personally not interested in such a mode, I do not know why you think "--some-flag" is needed when the command can figure out from the number of things on the command line being more than 2 just fine.

But my comment was targetted against "it needs a shell loop" in the justification in the proposed log message, which is not quite correct.

#7 lore raw
Quoted 3 lines · Andrea Stacchiotti

So, if I may ask, is the proposed patch as written (branch -f A B C X) acceptable and you just need me to rewrite the commit message or are you not interested in it at all?

As I said, I personally am not interested in such a mode, but you may be able to interest other people in it, and with wider support I may change my mind. But I do not think the feature should not be tied to "--force" option at all. "git branch A B C X" should be able to create three new branches A B C that all tracks X if none of them exist without "--force".

#8 lore raw
Quoted 2 lines · Junio C Hamano

I may change my mind. But I do not think the feature should not be tied to "--force" option at all.

Sorry for a double-negation failure. What I think is that the feature should be orthogonal to "--force".

#9 lore raw

Il giorno gio 12 giu 2025 alle ore 17:48 Junio C Hamano <[email protected]> ha scritto:

Quoted 8 lines
Quoted 2 lines · Junio C Hamano

I may change my mind. But I do not think the feature should not be tied to "--force" option at all.

Sorry for a double-negation failure. What I think is that the feature should be orthogonal to "--force".

No worries, I got it, it makes sense.
I can implement the revised request if I see some replies expressing interest.