git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names
@ 2025-12-21 15:59 Pasteley Absurda via GitGitGadget
  2025-12-22  4:40 ` Junio C Hamano
  0 siblings, 1 reply; 5+ messages in thread
From: Pasteley Absurda via GitGitGadget @ 2025-12-21 15:59 UTC (permalink / raw)
  To: git; +Cc: Pasteley Absurda, pasteley

From: pasteley <ceasebeing@gmail.com>

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.

Signed-off-by: pasteley <ceasebeing@gmail.com>
---
    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(-)

diff --git a/Documentation/config/checkout.adoc b/Documentation/config/checkout.adoc
index e35d212969..4b083d5c0c 100644
--- a/Documentation/config/checkout.adoc
+++ b/Documentation/config/checkout.adoc
@@ -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
diff --git a/Documentation/config/push.adoc b/Documentation/config/push.adoc
index d9112b2260..ae2031d069 100644
--- a/Documentation/config/push.adoc
+++ b/Documentation/config/push.adoc
@@ -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
diff --git a/builtin/push.c b/builtin/push.c
index 5b6cebbb85..c43d9dd7f8 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -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);
 }
 
diff --git a/builtin/worktree.c b/builtin/worktree.c
index fbdaf2eb2e..9dd131d977 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -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;
diff --git a/checkout.c b/checkout.c
index 1588b116ee..daa38b74b7 100644
--- a/checkout.c
+++ b/checkout.c
@@ -8,6 +8,7 @@
 #include "checkout.h"
 #include "config.h"
 #include "strbuf.h"
+#include "gettext.h"
 
 struct tracking_name_data {
 	/* const */ char *src_ref;
@@ -52,14 +53,23 @@ char *unique_tracking_name(const char *name, struct object_id *oid,
 {
 	struct tracking_name_data cb_data = TRACKING_NAME_DATA_INIT;
 	const char *default_remote = NULL;
+	char *templated_name = NULL;
+	const char *search_name;
+
 	if (!repo_config_get_string_tmp(the_repository, "checkout.defaultremote", &default_remote))
 		cb_data.default_remote = default_remote;
-	cb_data.src_ref = xstrfmt("refs/heads/%s", name);
+
+	templated_name = expand_remote_branch_template(name);
+	search_name = templated_name ? templated_name : name;
+
+	cb_data.src_ref = xstrfmt("refs/heads/%s", search_name);
 	cb_data.dst_oid = oid;
 	for_each_remote(check_tracking_name, &cb_data);
 	if (dwim_remotes_matched)
 		*dwim_remotes_matched = cb_data.num_matches;
 	free(cb_data.src_ref);
+	free(templated_name);
+
 	if (cb_data.num_matches == 1) {
 		free(cb_data.default_dst_ref);
 		free(cb_data.default_dst_oid);
@@ -73,3 +83,40 @@ char *unique_tracking_name(const char *name, struct object_id *oid,
 	}
 	return NULL;
 }
+
+char *expand_remote_branch_template(const char *name)
+{
+	const char *tpl = NULL;
+	const char *fmt;
+	struct strbuf out = STRBUF_INIT;
+	int saw_placeholder = 0;
+
+	if (repo_config_get_string_tmp(the_repository,
+				       "checkout.remoteBranchTemplate",
+				       &tpl))
+		return NULL;
+
+	fmt = tpl;
+	while (strbuf_expand_step(&out, &fmt)) {
+		if (skip_prefix(fmt, "%", &fmt)) {
+			strbuf_addch(&out, '%');
+		} else if (skip_prefix(fmt, "s", &fmt)) {
+			strbuf_addstr(&out, name);
+			saw_placeholder = 1;
+		} else {
+			/*
+			 * Unknown placeholder: keep '%' literal to avoid
+			 * surprising behavior (e.g., "%x" stays "%x").
+			 */
+			strbuf_addch(&out, '%');
+		}
+	}
+
+	if (!saw_placeholder) {
+		strbuf_release(&out);
+		warning("%s", _("checkout.remoteBranchTemplate missing '%%s' placeholder; ignoring"));
+		return NULL;
+	}
+
+	return strbuf_detach(&out, NULL);
+}
diff --git a/checkout.h b/checkout.h
index 55920e7aeb..2549e8de30 100644
--- a/checkout.h
+++ b/checkout.h
@@ -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
diff --git a/t/t1402-check-ref-format.sh b/t/t1402-check-ref-format.sh
index cabc516ae9..f174bbf2cc 100755
--- a/t/t1402-check-ref-format.sh
+++ b/t/t1402-check-ref-format.sh
@@ -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')"
diff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh
index a3b1449ef1..f409bc07c1 100755
--- a/t/t2024-checkout-dwim.sh
+++ b/t/t2024-checkout-dwim.sh
@@ -347,4 +347,166 @@ test_expect_success 'disambiguate dwim branch and checkout path (2)' '
 	grep bar dwim-arg2
 '
 
+test_expect_success 'setup for remoteBranchTemplate tests' '
+	(
+		cd repo_a &&
+		git checkout -b feature/newbar &&
+		test_commit a_feature_newbar &&
+		git checkout -b "100%special" &&
+		test_commit a_special &&
+		git checkout -b template_test &&
+		test_commit a_template_test
+	) &&
+	git fetch repo_a
+'
+
+test_expect_success 'checkout.remoteBranchTemplate with prefix' '
+	git checkout -B main &&
+	git reset --hard &&
+	test_might_fail git branch -D newbar &&
+	test_config checkout.remoteBranchTemplate "feature/%s" &&
+
+	git checkout newbar &&
+	status_uno_is_clean &&
+	test_branch newbar &&
+	test_cmp_rev remotes/repo_a/feature/newbar HEAD &&
+	test_branch_upstream newbar repo_a feature/newbar
+'
+
+test_expect_success 'checkout.remoteBranchTemplate handles literal %%' '
+	git checkout -B main &&
+	git reset --hard &&
+	test_might_fail git branch -D special &&
+	test_config checkout.remoteBranchTemplate "100%%%s" &&
+
+	git checkout special &&
+	status_uno_is_clean &&
+	test_branch special &&
+	test_cmp_rev remotes/repo_a/100%special HEAD &&
+	test_branch_upstream special repo_a 100%special
+'
+
+test_expect_success 'checkout.remoteBranchTemplate without %s is ignored with warning' '
+	git checkout -B main &&
+	git reset --hard &&
+	test_might_fail git branch -D template_test &&
+	test_config checkout.remoteBranchTemplate "fixed-name" &&
+
+	git checkout template_test 2>stderr &&
+	status_uno_is_clean &&
+	test_branch template_test &&
+	test_cmp_rev remotes/repo_a/template_test HEAD &&
+	test_grep "missing.*%s.*placeholder" stderr
+'
+
+test_expect_success 'checkout.remoteBranchTemplate with multiple %s placeholders' '
+	(
+		cd repo_a &&
+		git checkout -b user/multi/multi &&
+		test_commit a_multi_placeholder
+	) &&
+	git fetch repo_a &&
+
+	git checkout -B main &&
+	git reset --hard &&
+	test_might_fail git branch -D multi &&
+	test_config checkout.remoteBranchTemplate "user/%s/%s" &&
+
+	git checkout multi &&
+	status_uno_is_clean &&
+	test_branch multi &&
+	test_cmp_rev remotes/repo_a/user/multi/multi HEAD &&
+	test_branch_upstream multi repo_a user/multi/multi
+'
+
+test_expect_success 'checkout.remoteBranchTemplate + defaultRemote resolves ambiguity' '
+	(
+		cd repo_a &&
+		git checkout -b team/shared &&
+		test_commit a_team_shared
+	) &&
+	(
+		cd repo_b &&
+		git checkout -b team/shared &&
+		test_commit b_team_shared
+	) &&
+	git fetch --multiple repo_a repo_b &&
+
+	git checkout -B main &&
+	git reset --hard &&
+	test_might_fail git branch -D shared &&
+	test_config checkout.remoteBranchTemplate "team/%s" &&
+	test_config checkout.defaultRemote repo_b &&
+
+	git checkout shared &&
+	status_uno_is_clean &&
+	test_branch shared &&
+	test_cmp_rev remotes/other_b/team/shared HEAD &&
+	test_branch_upstream shared repo_b team/shared
+'
+
+test_expect_success 'checkout.remoteBranchTemplate with no matches fails' '
+	git checkout -B main &&
+	test_might_fail git branch -D nonexistent &&
+	test_config checkout.remoteBranchTemplate "feature/%s" &&
+
+	test_must_fail git checkout nonexistent &&
+	test_must_fail git rev-parse --verify refs/heads/nonexistent &&
+	test_branch main
+'
+
+test_expect_success 'checkout.remoteBranchTemplate still fails on ambiguity' '
+	git checkout -B main &&
+	test_might_fail git branch -D shared &&
+	test_config checkout.remoteBranchTemplate "team/%s" &&
+
+	test_must_fail git checkout shared 2>stderr &&
+	test_grep "matched multiple.*remote tracking branches" stderr &&
+	test_must_fail git rev-parse --verify refs/heads/shared &&
+	test_branch main
+'
+
+test_expect_success 'checkout.remoteBranchTemplate respects --no-guess' '
+	git checkout -B main &&
+	git reset --hard &&
+	test_might_fail git branch -D newbar &&
+	test_config checkout.remoteBranchTemplate "feature/%s" &&
+
+	test_must_fail git checkout --no-guess newbar &&
+	test_must_fail git rev-parse --verify refs/heads/newbar &&
+	test_branch main
+'
+
+test_expect_success 'checkout.remoteBranchTemplate respects checkout.guess = false' '
+	git checkout -B main &&
+	git reset --hard &&
+	test_might_fail git branch -D newbar &&
+	test_config checkout.remoteBranchTemplate "feature/%s" &&
+	test_config checkout.guess false &&
+
+	test_must_fail git checkout newbar &&
+	test_must_fail git rev-parse --verify refs/heads/newbar &&
+	test_branch main
+'
+
+test_expect_success 'checkout.remoteBranchTemplate with unknown placeholder kept literal' '
+	(
+		cd repo_a &&
+		git checkout -b "%d/literal" &&
+		test_commit a_literal_placeholder
+	) &&
+	git fetch repo_a &&
+
+	git checkout -B main &&
+	git reset --hard &&
+	test_might_fail git branch -D literal &&
+	test_config checkout.remoteBranchTemplate "%d/%s" &&
+
+	git checkout literal &&
+	status_uno_is_clean &&
+	test_branch literal &&
+	test_cmp_rev remotes/repo_a/%d/literal HEAD &&
+	test_branch_upstream literal repo_a %d/literal
+'
+
 test_done
diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh
index 023e1301c8..8b890d4b1e 100755
--- a/t/t2400-worktree-add.sh
+++ b/t/t2400-worktree-add.sh
@@ -730,6 +730,70 @@ test_expect_success 'git worktree --no-guess-remote option overrides config' '
 	)
 '
 
+test_expect_success 'git worktree add --guess-remote with remoteBranchTemplate' '
+	test_when_finished rm -rf repo_a repo_b bar &&
+	git init repo_a &&
+	(
+		cd repo_a &&
+		test_commit repo_a_main &&
+		git checkout -b feature/bar &&
+		test_commit feature_bar
+	) &&
+	git init repo_b &&
+	(
+		cd repo_b &&
+		test_commit repo_b_main &&
+		git remote add repo_a ../repo_a &&
+		git config remote.repo_a.fetch "refs/heads/*:refs/remotes/repo_a/*" &&
+		git fetch --all &&
+		git config checkout.remoteBranchTemplate "feature/%s" &&
+		git worktree add --guess-remote ../bar
+	) &&
+	(
+		cd bar &&
+		test_branch_upstream bar repo_a feature/bar &&
+		test_cmp_rev refs/remotes/repo_a/feature/bar refs/heads/bar
+	)
+'
+
+test_expect_success 'git worktree add --guess-remote with remoteBranchTemplate handles %%' '
+	test_when_finished rm -rf repo_a repo_b special &&
+	git init repo_a &&
+	(
+		cd repo_a &&
+		test_commit repo_a_main &&
+		git checkout -b "100%special" &&
+		test_commit percent_special
+	) &&
+	git init repo_b &&
+	(
+		cd repo_b &&
+		test_commit repo_b_main &&
+		git remote add repo_a ../repo_a &&
+		git config remote.repo_a.fetch "refs/heads/*:refs/remotes/repo_a/*" &&
+		git fetch --all &&
+		git config checkout.remoteBranchTemplate "100%%%s" &&
+		git worktree add --guess-remote ../special
+	) &&
+	(
+		cd special &&
+		test_branch_upstream special repo_a 100%special &&
+		test_cmp_rev refs/remotes/repo_a/100%special refs/heads/special
+	)
+'
+
+test_expect_success 'git worktree add --guess-remote with remoteBranchTemplate and no match fails' '
+	test_when_finished rm -rf repo_a repo_b nomatch &&
+	setup_remote_repo repo_a repo_b &&
+	(
+		cd repo_b &&
+		git config checkout.remoteBranchTemplate "feature/%s" &&
+		test_must_fail git worktree add --guess-remote ../nomatch 2>err &&
+		test_grep "No remote branch found for" err &&
+		test_grep "feature/nomatch" err
+	)
+'
+
 test_dwim_orphan () {
 	local info_text="No possible source branch, inferring '--orphan'" &&
 	local fetch_error_text="fatal: No local or remote refs exist despite at least one remote" &&
diff --git a/t/t5528-push-default.sh b/t/t5528-push-default.sh
index 2bd8759a68..e0d1c3cf02 100755
--- a/t/t5528-push-default.sh
+++ b/t/t5528-push-default.sh
@@ -294,4 +294,57 @@ test_expect_success 'default triangular behavior acts like "current"' '
 	test_push_success "" main repo2
 '
 
+test_expect_success 'push.autoSetupRemote + remoteBranchTemplate with push.default=simple' '
+	git checkout -b template-simple &&
+	test_config push.autoSetupRemote true &&
+	test_config push.default simple &&
+	test_config checkout.remoteBranchTemplate "feature/%s" &&
+	test_config branch.template-simple.remote parent1 &&
+	test_commit simple-commit &&
+	git push &&
+	git --git-dir=repo1 show-ref refs/heads/feature/template-simple &&
+	test_cmp_rev HEAD parent1/feature/template-simple &&
+	echo "refs/heads/feature/template-simple" >expect &&
+	git config branch.template-simple.merge >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'push.autoSetupRemote + remoteBranchTemplate with push.default=upstream' '
+	git checkout -b template-upstream &&
+	test_config push.autoSetupRemote true &&
+	test_config push.default upstream &&
+	test_config checkout.remoteBranchTemplate "feature/%s" &&
+	test_config branch.template-upstream.remote parent1 &&
+	test_commit upstream-commit &&
+	git push &&
+	git --git-dir=repo1 show-ref refs/heads/feature/template-upstream &&
+	test_cmp_rev HEAD parent1/feature/template-upstream &&
+	echo "refs/heads/feature/template-upstream" >expect &&
+	git config branch.template-upstream.merge >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'push.autoSetupRemote + remoteBranchTemplate with push.default=current' '
+	git checkout -b template-current &&
+	test_config push.autoSetupRemote true &&
+	test_config push.default current &&
+	test_config checkout.remoteBranchTemplate "feature/%s" &&
+	test_commit current-commit &&
+	git push parent1 &&
+	git --git-dir=repo1 show-ref refs/heads/feature/template-current &&
+	test_cmp_rev HEAD parent1/feature/template-current &&
+	echo "refs/heads/feature/template-current" >expect &&
+	git config branch.template-current.merge >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'remoteBranchTemplate ignored with push.default=nothing' '
+	git checkout -b template-nothing &&
+	test_config push.default nothing &&
+	test_config checkout.remoteBranchTemplate "feature/%s" &&
+	test_commit nothing-commit &&
+	test_must_fail git push 2>err &&
+	test_grep "No configured push destination" err
+'
+
 test_done

base-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names
  2025-12-21 15:59 [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names Pasteley Absurda via GitGitGadget
@ 2025-12-22  4:40 ` Junio C Hamano
  2025-12-22 18:27   ` pasteley
  0 siblings, 1 reply; 5+ messages in thread
From: Junio C Hamano @ 2025-12-22  4:40 UTC (permalink / raw)
  To: Pasteley Absurda via GitGitGadget; +Cc: git, Pasteley Absurda

"Pasteley Absurda via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: pasteley <ceasebeing@gmail.com>
>
> 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.

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.


^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names
  2025-12-22  4:40 ` Junio C Hamano
@ 2025-12-22 18:27   ` pasteley
  2025-12-23  0:51     ` Junio C Hamano
  0 siblings, 1 reply; 5+ messages in thread
From: pasteley @ 2025-12-22 18:27 UTC (permalink / raw)
  To: Junio C Hamano, Pasteley Absurda via GitGitGadget; +Cc: git

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).

> 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.

> 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

On 22/12/2025 5:40 AM, Junio C Hamano wrote:
> "Pasteley Absurda via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
>> From: pasteley <ceasebeing@gmail.com>
>>
>> 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.
> 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.
>

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names
  2025-12-22 18:27   ` pasteley
@ 2025-12-23  0:51     ` Junio C Hamano
  2025-12-23  2:39       ` pasteley
  0 siblings, 1 reply; 5+ messages in thread
From: Junio C Hamano @ 2025-12-23  0:51 UTC (permalink / raw)
  To: pasteley; +Cc: Pasteley Absurda via GitGitGadget, git

pasteley <ceasebeing@gmail.com> writes:

> 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?

> 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.

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names
  2025-12-23  0:51     ` Junio C Hamano
@ 2025-12-23  2:39       ` pasteley
  0 siblings, 0 replies; 5+ messages in thread
From: pasteley @ 2025-12-23  2:39 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Pasteley Absurda via GitGitGadget, git

On 23/12/2025 1:51 AM, Junio C Hamano wrote:
> pasteley <ceasebeing@gmail.com> writes:
> 
>> 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?
> 

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.


^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2025-12-23  2:39 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-12-21 15:59 [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names Pasteley Absurda via GitGitGadget
2025-12-22  4:40 ` Junio C Hamano
2025-12-22 18:27   ` pasteley
2025-12-23  0:51     ` Junio C Hamano
2025-12-23  2:39       ` pasteley

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).