Skip to contentSkip to conversation

checkout: add remoteBranchTemplate config for DWIM branch names

PATCH In review Pasteley Absurda via GitGitGadget 3 participants 5 messages Dec 21, 2025 – Dec 23, 2025 Documentation

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 checkout: add remoteBranchTemplate config for DWIM branch names

From: pasteley <[email protected]>

Add checkout.remoteBranchTemplate to apply a template pattern when searching for remote branches during checkout DWIM and when creating remote branches with push.autoSetupRemote.

Template uses printf-style placeholders (%s for branch name). For example, with "feature/%s", checking out "foo"
searches for "origin/feature/foo" and creates local "foo" tracking it. Pushing with autoSetupRemote creates "origin/feature/bar" from local "bar".

Useful when remote branches use prefixes but local branches don't.

Works with git-checkout, git-worktree --guess-remote, and git-push.

---
checkout: add remoteBranchTemplate config for DWIM branch names

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2136%2Fpasteley%2Fremote-branch-template-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2136/pasteley/remote-branch-template-v1
Pull-Request: https://github.com/git/git/pull/2136

 Documentation/config/checkout.adoc |  10 ++
 Documentation/config/push.adoc     |   3 +
 builtin/push.c                     |  23 ++++
 builtin/worktree.c                 |   7 ++
 checkout.c                         |  49 ++++++++-
 checkout.h                         |   9 ++
 t/t1402-check-ref-format.sh        |   3 +
 t/t2024-checkout-dwim.sh           | 162 +++++++++++++++++++++++++++++
 t/t2400-worktree-add.sh            |  64 ++++++++++++
 t/t5528-push-default.sh            |  53 ++++++++++
 10 files changed, 382 insertions(+), 1 deletion(-)
Documentation/config/checkout.adoc +10 -0
index e35d212969..4b083d5c0c 100644
@@ -22,6 +22,16 @@ commands or functionality in the future.
 	option in `git checkout` and `git switch`. See
 	linkgit:git-switch[1] and linkgit:git-checkout[1].
 
+`checkout.remoteBranchTemplate`::
+	Template pattern applied to remote ref names during checkout DWIM
+	and when pushing with `push.autoSetupRemote`. Uses `%s` for the
+	branch name and `%%` for a literal `%`.
++
+With `feature/%s`, `git checkout <branch>` searches for `origin/feature/<branch>`.
++
+Useful when remote branches expects specific prefixes (e.g., `feature/`, `user/`).
+Invalid templates (missing `%s`) trigger a warning and are ignored.
+
 `checkout.workers`::
 	The number of parallel workers to use when updating the working tree.
 	The default is one, i.e. sequential execution. If set to a value less
Documentation/config/push.adoc +3 -0
index d9112b2260..ae2031d069 100644
@@ -8,6 +8,9 @@
 	to be set. Workflows most likely to benefit from this option are
 	`simple` central workflows where all branches are expected to
 	have the same name on the remote.
++
+When combined with `checkout.remoteBranchTemplate`, creates remote
+branches using the templated name.
 
 `push.default`::
 	Defines the action `git push` should take if no refspec is
builtin/push.c +23 -0
index 5b6cebbb85..c43d9dd7f8 100644
@@ -7,6 +7,7 @@
 #include "builtin.h"
 #include "advice.h"
 #include "branch.h"
+#include "checkout.h"
 #include "config.h"
 #include "environment.h"
 #include "gettext.h"
@@ -281,6 +282,28 @@ static void setup_default_push_refspecs(int *flags, struct remote *remote)
 	if ((*flags & TRANSPORT_PUSH_AUTO_UPSTREAM) && branch->merge_nr == 0)
 		*flags |= TRANSPORT_PUSH_SET_UPSTREAM;
 
+	/*
+	 * Apply template to destination ref if configured and we're setting
+	 * up upstream (either automatically or explicitly with -u).
+	 */
+	if (branch->merge_nr == 0 && (*flags & (TRANSPORT_PUSH_SET_UPSTREAM | TRANSPORT_PUSH_AUTO_UPSTREAM))) {
+		const char *short_name;
+		char *templated_dst = NULL;
+
+		if (skip_prefix(dst, "refs/heads/", &short_name)) {
+			char *template_result = expand_remote_branch_template(short_name);
+			if (template_result) {
+				templated_dst = xstrfmt("refs/heads/%s", template_result);
+				free(template_result);
+				dst = templated_dst;
+			}
+		}
+
+		refspec_appendf(&rs, "%s:%s", branch->refname, dst);
+		free(templated_dst);
+		return;
+	}
+
 	refspec_appendf(&rs, "%s:%s", branch->refname, dst);
 }
 
builtin/worktree.c +7 -0
index fbdaf2eb2e..9dd131d977 100644
@@ -764,6 +764,13 @@ static char *dwim_branch(const char *path, char **new_branch)
 	if (guess_remote) {
 		struct object_id oid;
 		char *remote = unique_tracking_name(*new_branch, &oid, NULL);
+		char *templated_name = expand_remote_branch_template(*new_branch);
+
+		if (!remote && templated_name) {
+			die(_("No remote branch found for '%s' (set to '%s' by checkout.remoteBranchTemplate)"),
+			    *new_branch, templated_name);
+		}
+		free(templated_name);
 		return remote;
 	}
 	return NULL;
checkout.c +48 -1

71 lines not on this page · raw message

checkout.h +9 -0
index 55920e7aeb..2549e8de30 100644
@@ -3,6 +3,15 @@
 
 #include "hash.h"
 
+/*
+ * If checkout.remoteBranchTemplate is set, expand it using printf-style
+ * substitution:
+ *   %s -> the branch name
+ *   %% -> a literal %
+ * Returns a newly allocated string, or NULL if unset/invalid.
+ */
+char *expand_remote_branch_template(const char *name);
+
 /*
  * Check if the branch name uniquely matches a branch name on a remote
  * tracking branch.  Return the name of the remote if such a branch
t/t1402-check-ref-format.sh +3 -0
index cabc516ae9..f174bbf2cc 100755
@@ -58,6 +58,9 @@ invalid_ref 'foo.lock/bar'
 invalid_ref 'foo.lock///bar'
 valid_ref 'heads/foo@bar'
 invalid_ref 'heads/v@{ation'
+valid_ref 'heads/foo%bar'
+valid_ref 'heads/foo%s'
+valid_ref 'heads/100%special'
 invalid_ref 'heads/foo\bar'
 invalid_ref "$(printf 'heads/foo\t')"
 invalid_ref "$(printf 'heads/foo\177')"
t/t2024-checkout-dwim.sh +162 -0

166 lines not on this page · raw message

t/t2400-worktree-add.sh +64 -0

70 lines not on this page · raw message

t/t5528-push-default.sh +53 -0

57 lines not on this page · raw message

base-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda

Signature · 1 line
gitgitgadget
#1 lore raw
Quoted 6 lines · Pasteley Absurda via GitGitGadget

From: pasteley <[email protected]>

Add checkout.remoteBranchTemplate to apply a template pattern when searching for remote branches during checkout DWIM and when creating remote branches with push.autoSetupRemote.

Quoted 7 lines

Template uses printf-style placeholders (%s for branch name). For example, with "feature/%s", checking out "foo"
searches for "origin/feature/foo" and creates local "foo" tracking it. Pushing with autoSetupRemote creates "origin/feature/bar" from local "bar".

Useful when remote branches use prefixes but local branches don't.

It fells that this is presented backwards. The usefulness of the layout that names local branches deliberately differently from their remote counterparts needs to be justified first. Only after that, we can consider adding extra mechanism to support such a layout.

Once "git checkout foo" is taught to do the same as "git checkout -b extra-foo -t origin/foo", it would create

[branch "extra-foo"]
remote = origin
merge = refs/heads/foo

but the push side would need extra work, and that is why you needed to muck with the push refspec. But then what should happen when the user is using "we do not bother remembering what branches to push there; the remote repository remembers that for us", aka "matching push"?

Most of the problems is what you are creating by using an unusual layout to name local branches differently from the remote counterpart. You do not have to, and then all the problems you created with that layout goes away, without this patch.

So, I am not sure if this is a good idea to begin with. At least, I am not yet convinced.

Thanks.

#2 lore raw

You're right that same-named branches are the ideal Git workflow, and I agree this patch should not encourage drifting away from that model.

The motivation here is for cases where the name mismatch is imposed by external constraints, not chosen by developers. For example:

1. Server-side policies/hooks that require a namespace on the remote (e.g. team/*, users/<id>/*, release/*).
2. Hosting / mirroring setups where remote branches live under a fixed prefix for organizational or access-control reasons.
3. Migrations where the remote branch layout is constrained by the target system, while local developer workflows assume short names.

In these scenarios developers do not create the problem, they inherit it. The alternative today is to type the prefixed remote name everywhere and give up DWIM convenience (e.g. git checkout foo no longer does the "natural" thing).

This remains opt-in via checkout.remoteBranchTemplate, so only workflows that explicitly configure it change behavior; defaults stay unchanged. Explicit operations still bypass the template (e.g. -b/-c <name> and an explicit push refspec keep full user control).

Git already supports name mismatches in a few places:
* remote.<name>.fetch allows arbitrary mappings for remote refs.
* branch.<name>.merge can track a differently named remote branch.
* push.default=upstream pushes to the configured upstream even if names differ.

However, configuring remote.<name>.push does not solve the DWIM checkout
problem: users still need to know the full remote branch name to check it out, and wildcard push refspecs can have surprising scope (they can match many branches unless the user is always explicit). This patch keeps the scope narrow: it only affects cases where Git is already "guessing" the remote side (checkout/switch/worktree --guess-remote, and automatic upstream setup).

Quoted 6 lines

Once "git checkout foo" is taught to do the same as "git checkout -b extra-foo -t origin/foo", it would create:

[branch "extra-foo"]
remote = origin
merge = refs/heads/foo

Yes, but only for the DWIM path where Git derives the remote branch. When the user explicitly names the local branch, we do not apply the template.

Quoted 1 line

But then what should happen when the user is using "matching push"?

While push.default=matching has been deprecated since Git 2.0 (~ 2014), we still can handle this corner case by detecting the incompatibility and providing a clear error message.

Thanks for the thorough review,
pasteley

Quoted 44 lines · Junio C Hamano
Quoted 6 lines · Pasteley Absurda via GitGitGadget

From: pasteley <[email protected]>

Add checkout.remoteBranchTemplate to apply a template pattern when searching for remote branches during checkout DWIM and when creating remote branches with push.autoSetupRemote.

Quoted 7 lines

Template uses printf-style placeholders (%s for branch name). For example, with "feature/%s", checking out "foo"
searches for "origin/feature/foo" and creates local "foo" tracking it. Pushing with autoSetupRemote creates "origin/feature/bar" from local "bar".

Useful when remote branches use prefixes but local branches don't.

It fells that this is presented backwards. The usefulness of the layout that names local branches deliberately differently from their remote counterparts needs to be justified first. Only after that, we can consider adding extra mechanism to support such a layout.

Once "git checkout foo" is taught to do the same as "git checkout -b extra-foo -t origin/foo", it would create

[branch "extra-foo"]
remote = origin
merge = refs/heads/foo

but the push side would need extra work, and that is why you needed to muck with the push refspec. But then what should happen when the user is using "we do not bother remembering what branches to push there; the remote repository remembers that for us", aka "matching push"?

Most of the problems is what you are creating by using an unusual layout to name local branches differently from the remote counterpart. You do not have to, and then all the problems you created with that layout goes away, without this patch.

So, I am not sure if this is a good idea to begin with. At least, I am not yet convinced.

Thanks.

#3 lore raw
Quoted 8 lines · pasteley

1. Server-side policies/hooks that require a namespace on the remote (e.g. team/*, users/<id>/*, release/*).
2. Hosting / mirroring setups where remote branches live under a fixed prefix for organizational or access-control reasons.
3. Migrations where the remote branch layout is constrained by the target system, while local developer workflows assume short names.

In these scenarios developers do not create the problem, they inherit it.

Nobody stops you from interacting with projects like the above, and locally name branches you store your work in users/pasteley/topic-1 instead of topic-1, no?

Quoted 3 lines

While push.default=matching has been deprecated since Git 2.0 (~ 2014), we still can handle this corner case by detecting the incompatibility and providing a clear error message.

I do not know if you are confused, or if you are citing somebody else's description that is confused, but the word "deprecated" in the above statement is misused. So is "corner case".

When we talk about deprecation, the deprecated feature is something that is not useful for anybody to adopt because there are better alternatives available, we wish there is no need to support the users, and we hope we can remove it eventually someday. The matching push does not fall into that category at all.

What we did in Git 2.0 was to change the default from matching to simple, because the matching is the most useful for those who publish their changes, and for those who are merely participating somebody else's project, it is not suited. The matching push is also harder to use properly if a project has multiple people who push to a single repository (i.e., central repository approach). The default was switched to make life easier for more people. It did not diminish the usefulness of the matching mode for the developers for whom the matching was the most useful mode.

#4 lore raw
Quoted 9 lines · Junio C Hamano
Quoted 7 lines · pasteley

1. Server-side policies/hooks that require a namespace on the remote (e.g. team/*, users/<id>/*, release/*).
2. Hosting / mirroring setups where remote branches live under a fixed prefix for organizational or access-control reasons.
3. Migrations where the remote branch layout is constrained by the target system, while local developer workflows assume short names.

Quoted 6 lines
Quoted 1 line

In these scenarios developers do not create the problem, they inherit it.

Nobody stops you from interacting with projects like the above, and locally name branches you store your work in users/pasteley/topic-1 instead of topic-1, no?

I would say: forced to handle it this way. And guessing I’m not the only one struggling. Meanwhile, there’s a simple quality-of-life improvement that would help avoid all the hacky custom aliases and shell workarounds for what should be a relatively straightforward remote refspec format adjustment.
I’d really like to see this discussion move toward practical, constructive steps, though I’m not sure what it would take to get there.