{"thread":{"id":"64659","subject":"[PATCH] checkout: add remoteBranchTemplate config for DWIM branch names","startedAt":"2025-12-21T16:00:00Z","lastAt":"2025-12-23T02:39:46Z","messageCount":5,"participants":["Pasteley Absurda via GitGitGadget","Junio C Hamano","pasteley"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"532606","messageId":"pull.2136.git.git.1766332796836.gitgitgadget@gmail.com","threadId":"64659","inReplyTo":null,"subject":"[PATCH] checkout: add remoteBranchTemplate config for DWIM branch names","fromName":"Pasteley Absurda via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-21T15:59:56Z","receivedAt":"2025-12-21T16:00:00Z","isPatch":true,"sender":{"key":"name:Pasteley Absurda","avatar":null},"body":"From: pasteley <ceasebeing@gmail.com>\n\nAdd checkout.remoteBranchTemplate to apply a template pattern when\nsearching for remote branches during checkout DWIM and when creating\nremote branches with push.autoSetupRemote.\n\nTemplate uses printf-style placeholders (%s for branch name). For\nexample, with \"feature/%s\", checking out \"foo\"\nsearches for \"origin/feature/foo\" and creates local \"foo\"\ntracking it. Pushing with autoSetupRemote creates \"origin/feature/bar\"\nfrom local \"bar\".\n\nUseful when remote branches use prefixes but local branches don't.\n\nWorks with git-checkout, git-worktree --guess-remote, and git-push.\n\nSigned-off-by: pasteley <ceasebeing@gmail.com>\n---\n    checkout: add remoteBranchTemplate config for DWIM branch names\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2136%2Fpasteley%2Fremote-branch-template-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2136/pasteley/remote-branch-template-v1\nPull-Request: https://github.com/git/git/pull/2136\n\n Documentation/config/checkout.adoc |  10 ++\n Documentation/config/push.adoc     |   3 +\n builtin/push.c                     |  23 ++++\n builtin/worktree.c                 |   7 ++\n checkout.c                         |  49 ++++++++-\n checkout.h                         |   9 ++\n t/t1402-check-ref-format.sh        |   3 +\n t/t2024-checkout-dwim.sh           | 162 +++++++++++++++++++++++++++++\n t/t2400-worktree-add.sh            |  64 ++++++++++++\n t/t5528-push-default.sh            |  53 ++++++++++\n 10 files changed, 382 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/checkout.adoc b/Documentation/config/checkout.adoc\nindex e35d212969..4b083d5c0c 100644\n--- a/Documentation/config/checkout.adoc\n+++ b/Documentation/config/checkout.adoc\n@@ -22,6 +22,16 @@ commands or functionality in the future.\n \toption in `git checkout` and `git switch`. See\n \tlinkgit:git-switch[1] and linkgit:git-checkout[1].\n \n+`checkout.remoteBranchTemplate`::\n+\tTemplate pattern applied to remote ref names during checkout DWIM\n+\tand when pushing with `push.autoSetupRemote`. Uses `%s` for the\n+\tbranch name and `%%` for a literal `%`.\n++\n+With `feature/%s`, `git checkout <branch>` searches for `origin/feature/<branch>`.\n++\n+Useful when remote branches expects specific prefixes (e.g., `feature/`, `user/`).\n+Invalid templates (missing `%s`) trigger a warning and are ignored.\n+\n `checkout.workers`::\n \tThe number of parallel workers to use when updating the working tree.\n \tThe default is one, i.e. sequential execution. If set to a value less\ndiff --git a/Documentation/config/push.adoc b/Documentation/config/push.adoc\nindex d9112b2260..ae2031d069 100644\n--- a/Documentation/config/push.adoc\n+++ b/Documentation/config/push.adoc\n@@ -8,6 +8,9 @@\n \tto be set. Workflows most likely to benefit from this option are\n \t`simple` central workflows where all branches are expected to\n \thave the same name on the remote.\n++\n+When combined with `checkout.remoteBranchTemplate`, creates remote\n+branches using the templated name.\n \n `push.default`::\n \tDefines the action `git push` should take if no refspec is\ndiff --git a/builtin/push.c b/builtin/push.c\nindex 5b6cebbb85..c43d9dd7f8 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -7,6 +7,7 @@\n #include \"builtin.h\"\n #include \"advice.h\"\n #include \"branch.h\"\n+#include \"checkout.h\"\n #include \"config.h\"\n #include \"environment.h\"\n #include \"gettext.h\"\n@@ -281,6 +282,28 @@ static void setup_default_push_refspecs(int *flags, struct remote *remote)\n \tif ((*flags & TRANSPORT_PUSH_AUTO_UPSTREAM) && branch->merge_nr == 0)\n \t\t*flags |= TRANSPORT_PUSH_SET_UPSTREAM;\n \n+\t/*\n+\t * Apply template to destination ref if configured and we're setting\n+\t * up upstream (either automatically or explicitly with -u).\n+\t */\n+\tif (branch->merge_nr == 0 && (*flags & (TRANSPORT_PUSH_SET_UPSTREAM | TRANSPORT_PUSH_AUTO_UPSTREAM))) {\n+\t\tconst char *short_name;\n+\t\tchar *templated_dst = NULL;\n+\n+\t\tif (skip_prefix(dst, \"refs/heads/\", &short_name)) {\n+\t\t\tchar *template_result = expand_remote_branch_template(short_name);\n+\t\t\tif (template_result) {\n+\t\t\t\ttemplated_dst = xstrfmt(\"refs/heads/%s\", template_result);\n+\t\t\t\tfree(template_result);\n+\t\t\t\tdst = templated_dst;\n+\t\t\t}\n+\t\t}\n+\n+\t\trefspec_appendf(&rs, \"%s:%s\", branch->refname, dst);\n+\t\tfree(templated_dst);\n+\t\treturn;\n+\t}\n+\n \trefspec_appendf(&rs, \"%s:%s\", branch->refname, dst);\n }\n \ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex fbdaf2eb2e..9dd131d977 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -764,6 +764,13 @@ static char *dwim_branch(const char *path, char **new_branch)\n \tif (guess_remote) {\n \t\tstruct object_id oid;\n \t\tchar *remote = unique_tracking_name(*new_branch, &oid, NULL);\n+\t\tchar *templated_name = expand_remote_branch_template(*new_branch);\n+\n+\t\tif (!remote && templated_name) {\n+\t\t\tdie(_(\"No remote branch found for '%s' (set to '%s' by checkout.remoteBranchTemplate)\"),\n+\t\t\t    *new_branch, templated_name);\n+\t\t}\n+\t\tfree(templated_name);\n \t\treturn remote;\n \t}\n \treturn NULL;\ndiff --git a/checkout.c b/checkout.c\nindex 1588b116ee..daa38b74b7 100644\n--- a/checkout.c\n+++ b/checkout.c\n@@ -8,6 +8,7 @@\n #include \"checkout.h\"\n #include \"config.h\"\n #include \"strbuf.h\"\n+#include \"gettext.h\"\n \n struct tracking_name_data {\n \t/* const */ char *src_ref;\n@@ -52,14 +53,23 @@ char *unique_tracking_name(const char *name, struct object_id *oid,\n {\n \tstruct tracking_name_data cb_data = TRACKING_NAME_DATA_INIT;\n \tconst char *default_remote = NULL;\n+\tchar *templated_name = NULL;\n+\tconst char *search_name;\n+\n \tif (!repo_config_get_string_tmp(the_repository, \"checkout.defaultremote\", &default_remote))\n \t\tcb_data.default_remote = default_remote;\n-\tcb_data.src_ref = xstrfmt(\"refs/heads/%s\", name);\n+\n+\ttemplated_name = expand_remote_branch_template(name);\n+\tsearch_name = templated_name ? templated_name : name;\n+\n+\tcb_data.src_ref = xstrfmt(\"refs/heads/%s\", search_name);\n \tcb_data.dst_oid = oid;\n \tfor_each_remote(check_tracking_name, &cb_data);\n \tif (dwim_remotes_matched)\n \t\t*dwim_remotes_matched = cb_data.num_matches;\n \tfree(cb_data.src_ref);\n+\tfree(templated_name);\n+\n \tif (cb_data.num_matches == 1) {\n \t\tfree(cb_data.default_dst_ref);\n \t\tfree(cb_data.default_dst_oid);\n@@ -73,3 +83,40 @@ char *unique_tracking_name(const char *name, struct object_id *oid,\n \t}\n \treturn NULL;\n }\n+\n+char *expand_remote_branch_template(const char *name)\n+{\n+\tconst char *tpl = NULL;\n+\tconst char *fmt;\n+\tstruct strbuf out = STRBUF_INIT;\n+\tint saw_placeholder = 0;\n+\n+\tif (repo_config_get_string_tmp(the_repository,\n+\t\t\t\t       \"checkout.remoteBranchTemplate\",\n+\t\t\t\t       &tpl))\n+\t\treturn NULL;\n+\n+\tfmt = tpl;\n+\twhile (strbuf_expand_step(&out, &fmt)) {\n+\t\tif (skip_prefix(fmt, \"%\", &fmt)) {\n+\t\t\tstrbuf_addch(&out, '%');\n+\t\t} else if (skip_prefix(fmt, \"s\", &fmt)) {\n+\t\t\tstrbuf_addstr(&out, name);\n+\t\t\tsaw_placeholder = 1;\n+\t\t} else {\n+\t\t\t/*\n+\t\t\t * Unknown placeholder: keep '%' literal to avoid\n+\t\t\t * surprising behavior (e.g., \"%x\" stays \"%x\").\n+\t\t\t */\n+\t\t\tstrbuf_addch(&out, '%');\n+\t\t}\n+\t}\n+\n+\tif (!saw_placeholder) {\n+\t\tstrbuf_release(&out);\n+\t\twarning(\"%s\", _(\"checkout.remoteBranchTemplate missing '%%s' placeholder; ignoring\"));\n+\t\treturn NULL;\n+\t}\n+\n+\treturn strbuf_detach(&out, NULL);\n+}\ndiff --git a/checkout.h b/checkout.h\nindex 55920e7aeb..2549e8de30 100644\n--- a/checkout.h\n+++ b/checkout.h\n@@ -3,6 +3,15 @@\n \n #include \"hash.h\"\n \n+/*\n+ * If checkout.remoteBranchTemplate is set, expand it using printf-style\n+ * substitution:\n+ *   %s -> the branch name\n+ *   %% -> a literal %\n+ * Returns a newly allocated string, or NULL if unset/invalid.\n+ */\n+char *expand_remote_branch_template(const char *name);\n+\n /*\n  * Check if the branch name uniquely matches a branch name on a remote\n  * tracking branch.  Return the name of the remote if such a branch\ndiff --git a/t/t1402-check-ref-format.sh b/t/t1402-check-ref-format.sh\nindex cabc516ae9..f174bbf2cc 100755\n--- a/t/t1402-check-ref-format.sh\n+++ b/t/t1402-check-ref-format.sh\n@@ -58,6 +58,9 @@ invalid_ref 'foo.lock/bar'\n invalid_ref 'foo.lock///bar'\n valid_ref 'heads/foo@bar'\n invalid_ref 'heads/v@{ation'\n+valid_ref 'heads/foo%bar'\n+valid_ref 'heads/foo%s'\n+valid_ref 'heads/100%special'\n invalid_ref 'heads/foo\\bar'\n invalid_ref \"$(printf 'heads/foo\\t')\"\n invalid_ref \"$(printf 'heads/foo\\177')\"\ndiff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh\nindex a3b1449ef1..f409bc07c1 100755\n--- a/t/t2024-checkout-dwim.sh\n+++ b/t/t2024-checkout-dwim.sh\n@@ -347,4 +347,166 @@ test_expect_success 'disambiguate dwim branch and checkout path (2)' '\n \tgrep bar dwim-arg2\n '\n \n+test_expect_success 'setup for remoteBranchTemplate tests' '\n+\t(\n+\t\tcd repo_a &&\n+\t\tgit checkout -b feature/newbar &&\n+\t\ttest_commit a_feature_newbar &&\n+\t\tgit checkout -b \"100%special\" &&\n+\t\ttest_commit a_special &&\n+\t\tgit checkout -b template_test &&\n+\t\ttest_commit a_template_test\n+\t) &&\n+\tgit fetch repo_a\n+'\n+\n+test_expect_success 'checkout.remoteBranchTemplate with prefix' '\n+\tgit checkout -B main &&\n+\tgit reset --hard &&\n+\ttest_might_fail git branch -D newbar &&\n+\ttest_config checkout.remoteBranchTemplate \"feature/%s\" &&\n+\n+\tgit checkout newbar &&\n+\tstatus_uno_is_clean &&\n+\ttest_branch newbar &&\n+\ttest_cmp_rev remotes/repo_a/feature/newbar HEAD &&\n+\ttest_branch_upstream newbar repo_a feature/newbar\n+'\n+\n+test_expect_success 'checkout.remoteBranchTemplate handles literal %%' '\n+\tgit checkout -B main &&\n+\tgit reset --hard &&\n+\ttest_might_fail git branch -D special &&\n+\ttest_config checkout.remoteBranchTemplate \"100%%%s\" &&\n+\n+\tgit checkout special &&\n+\tstatus_uno_is_clean &&\n+\ttest_branch special &&\n+\ttest_cmp_rev remotes/repo_a/100%special HEAD &&\n+\ttest_branch_upstream special repo_a 100%special\n+'\n+\n+test_expect_success 'checkout.remoteBranchTemplate without %s is ignored with warning' '\n+\tgit checkout -B main &&\n+\tgit reset --hard &&\n+\ttest_might_fail git branch -D template_test &&\n+\ttest_config checkout.remoteBranchTemplate \"fixed-name\" &&\n+\n+\tgit checkout template_test 2>stderr &&\n+\tstatus_uno_is_clean &&\n+\ttest_branch template_test &&\n+\ttest_cmp_rev remotes/repo_a/template_test HEAD &&\n+\ttest_grep \"missing.*%s.*placeholder\" stderr\n+'\n+\n+test_expect_success 'checkout.remoteBranchTemplate with multiple %s placeholders' '\n+\t(\n+\t\tcd repo_a &&\n+\t\tgit checkout -b user/multi/multi &&\n+\t\ttest_commit a_multi_placeholder\n+\t) &&\n+\tgit fetch repo_a &&\n+\n+\tgit checkout -B main &&\n+\tgit reset --hard &&\n+\ttest_might_fail git branch -D multi &&\n+\ttest_config checkout.remoteBranchTemplate \"user/%s/%s\" &&\n+\n+\tgit checkout multi &&\n+\tstatus_uno_is_clean &&\n+\ttest_branch multi &&\n+\ttest_cmp_rev remotes/repo_a/user/multi/multi HEAD &&\n+\ttest_branch_upstream multi repo_a user/multi/multi\n+'\n+\n+test_expect_success 'checkout.remoteBranchTemplate + defaultRemote resolves ambiguity' '\n+\t(\n+\t\tcd repo_a &&\n+\t\tgit checkout -b team/shared &&\n+\t\ttest_commit a_team_shared\n+\t) &&\n+\t(\n+\t\tcd repo_b &&\n+\t\tgit checkout -b team/shared &&\n+\t\ttest_commit b_team_shared\n+\t) &&\n+\tgit fetch --multiple repo_a repo_b &&\n+\n+\tgit checkout -B main &&\n+\tgit reset --hard &&\n+\ttest_might_fail git branch -D shared &&\n+\ttest_config checkout.remoteBranchTemplate \"team/%s\" &&\n+\ttest_config checkout.defaultRemote repo_b &&\n+\n+\tgit checkout shared &&\n+\tstatus_uno_is_clean &&\n+\ttest_branch shared &&\n+\ttest_cmp_rev remotes/other_b/team/shared HEAD &&\n+\ttest_branch_upstream shared repo_b team/shared\n+'\n+\n+test_expect_success 'checkout.remoteBranchTemplate with no matches fails' '\n+\tgit checkout -B main &&\n+\ttest_might_fail git branch -D nonexistent &&\n+\ttest_config checkout.remoteBranchTemplate \"feature/%s\" &&\n+\n+\ttest_must_fail git checkout nonexistent &&\n+\ttest_must_fail git rev-parse --verify refs/heads/nonexistent &&\n+\ttest_branch main\n+'\n+\n+test_expect_success 'checkout.remoteBranchTemplate still fails on ambiguity' '\n+\tgit checkout -B main &&\n+\ttest_might_fail git branch -D shared &&\n+\ttest_config checkout.remoteBranchTemplate \"team/%s\" &&\n+\n+\ttest_must_fail git checkout shared 2>stderr &&\n+\ttest_grep \"matched multiple.*remote tracking branches\" stderr &&\n+\ttest_must_fail git rev-parse --verify refs/heads/shared &&\n+\ttest_branch main\n+'\n+\n+test_expect_success 'checkout.remoteBranchTemplate respects --no-guess' '\n+\tgit checkout -B main &&\n+\tgit reset --hard &&\n+\ttest_might_fail git branch -D newbar &&\n+\ttest_config checkout.remoteBranchTemplate \"feature/%s\" &&\n+\n+\ttest_must_fail git checkout --no-guess newbar &&\n+\ttest_must_fail git rev-parse --verify refs/heads/newbar &&\n+\ttest_branch main\n+'\n+\n+test_expect_success 'checkout.remoteBranchTemplate respects checkout.guess = false' '\n+\tgit checkout -B main &&\n+\tgit reset --hard &&\n+\ttest_might_fail git branch -D newbar &&\n+\ttest_config checkout.remoteBranchTemplate \"feature/%s\" &&\n+\ttest_config checkout.guess false &&\n+\n+\ttest_must_fail git checkout newbar &&\n+\ttest_must_fail git rev-parse --verify refs/heads/newbar &&\n+\ttest_branch main\n+'\n+\n+test_expect_success 'checkout.remoteBranchTemplate with unknown placeholder kept literal' '\n+\t(\n+\t\tcd repo_a &&\n+\t\tgit checkout -b \"%d/literal\" &&\n+\t\ttest_commit a_literal_placeholder\n+\t) &&\n+\tgit fetch repo_a &&\n+\n+\tgit checkout -B main &&\n+\tgit reset --hard &&\n+\ttest_might_fail git branch -D literal &&\n+\ttest_config checkout.remoteBranchTemplate \"%d/%s\" &&\n+\n+\tgit checkout literal &&\n+\tstatus_uno_is_clean &&\n+\ttest_branch literal &&\n+\ttest_cmp_rev remotes/repo_a/%d/literal HEAD &&\n+\ttest_branch_upstream literal repo_a %d/literal\n+'\n+\n test_done\ndiff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh\nindex 023e1301c8..8b890d4b1e 100755\n--- a/t/t2400-worktree-add.sh\n+++ b/t/t2400-worktree-add.sh\n@@ -730,6 +730,70 @@ test_expect_success 'git worktree --no-guess-remote option overrides config' '\n \t)\n '\n \n+test_expect_success 'git worktree add --guess-remote with remoteBranchTemplate' '\n+\ttest_when_finished rm -rf repo_a repo_b bar &&\n+\tgit init repo_a &&\n+\t(\n+\t\tcd repo_a &&\n+\t\ttest_commit repo_a_main &&\n+\t\tgit checkout -b feature/bar &&\n+\t\ttest_commit feature_bar\n+\t) &&\n+\tgit init repo_b &&\n+\t(\n+\t\tcd repo_b &&\n+\t\ttest_commit repo_b_main &&\n+\t\tgit remote add repo_a ../repo_a &&\n+\t\tgit config remote.repo_a.fetch \"refs/heads/*:refs/remotes/repo_a/*\" &&\n+\t\tgit fetch --all &&\n+\t\tgit config checkout.remoteBranchTemplate \"feature/%s\" &&\n+\t\tgit worktree add --guess-remote ../bar\n+\t) &&\n+\t(\n+\t\tcd bar &&\n+\t\ttest_branch_upstream bar repo_a feature/bar &&\n+\t\ttest_cmp_rev refs/remotes/repo_a/feature/bar refs/heads/bar\n+\t)\n+'\n+\n+test_expect_success 'git worktree add --guess-remote with remoteBranchTemplate handles %%' '\n+\ttest_when_finished rm -rf repo_a repo_b special &&\n+\tgit init repo_a &&\n+\t(\n+\t\tcd repo_a &&\n+\t\ttest_commit repo_a_main &&\n+\t\tgit checkout -b \"100%special\" &&\n+\t\ttest_commit percent_special\n+\t) &&\n+\tgit init repo_b &&\n+\t(\n+\t\tcd repo_b &&\n+\t\ttest_commit repo_b_main &&\n+\t\tgit remote add repo_a ../repo_a &&\n+\t\tgit config remote.repo_a.fetch \"refs/heads/*:refs/remotes/repo_a/*\" &&\n+\t\tgit fetch --all &&\n+\t\tgit config checkout.remoteBranchTemplate \"100%%%s\" &&\n+\t\tgit worktree add --guess-remote ../special\n+\t) &&\n+\t(\n+\t\tcd special &&\n+\t\ttest_branch_upstream special repo_a 100%special &&\n+\t\ttest_cmp_rev refs/remotes/repo_a/100%special refs/heads/special\n+\t)\n+'\n+\n+test_expect_success 'git worktree add --guess-remote with remoteBranchTemplate and no match fails' '\n+\ttest_when_finished rm -rf repo_a repo_b nomatch &&\n+\tsetup_remote_repo repo_a repo_b &&\n+\t(\n+\t\tcd repo_b &&\n+\t\tgit config checkout.remoteBranchTemplate \"feature/%s\" &&\n+\t\ttest_must_fail git worktree add --guess-remote ../nomatch 2>err &&\n+\t\ttest_grep \"No remote branch found for\" err &&\n+\t\ttest_grep \"feature/nomatch\" err\n+\t)\n+'\n+\n test_dwim_orphan () {\n \tlocal info_text=\"No possible source branch, inferring '--orphan'\" &&\n \tlocal fetch_error_text=\"fatal: No local or remote refs exist despite at least one remote\" &&\ndiff --git a/t/t5528-push-default.sh b/t/t5528-push-default.sh\nindex 2bd8759a68..e0d1c3cf02 100755\n--- a/t/t5528-push-default.sh\n+++ b/t/t5528-push-default.sh\n@@ -294,4 +294,57 @@ test_expect_success 'default triangular behavior acts like \"current\"' '\n \ttest_push_success \"\" main repo2\n '\n \n+test_expect_success 'push.autoSetupRemote + remoteBranchTemplate with push.default=simple' '\n+\tgit checkout -b template-simple &&\n+\ttest_config push.autoSetupRemote true &&\n+\ttest_config push.default simple &&\n+\ttest_config checkout.remoteBranchTemplate \"feature/%s\" &&\n+\ttest_config branch.template-simple.remote parent1 &&\n+\ttest_commit simple-commit &&\n+\tgit push &&\n+\tgit --git-dir=repo1 show-ref refs/heads/feature/template-simple &&\n+\ttest_cmp_rev HEAD parent1/feature/template-simple &&\n+\techo \"refs/heads/feature/template-simple\" >expect &&\n+\tgit config branch.template-simple.merge >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'push.autoSetupRemote + remoteBranchTemplate with push.default=upstream' '\n+\tgit checkout -b template-upstream &&\n+\ttest_config push.autoSetupRemote true &&\n+\ttest_config push.default upstream &&\n+\ttest_config checkout.remoteBranchTemplate \"feature/%s\" &&\n+\ttest_config branch.template-upstream.remote parent1 &&\n+\ttest_commit upstream-commit &&\n+\tgit push &&\n+\tgit --git-dir=repo1 show-ref refs/heads/feature/template-upstream &&\n+\ttest_cmp_rev HEAD parent1/feature/template-upstream &&\n+\techo \"refs/heads/feature/template-upstream\" >expect &&\n+\tgit config branch.template-upstream.merge >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'push.autoSetupRemote + remoteBranchTemplate with push.default=current' '\n+\tgit checkout -b template-current &&\n+\ttest_config push.autoSetupRemote true &&\n+\ttest_config push.default current &&\n+\ttest_config checkout.remoteBranchTemplate \"feature/%s\" &&\n+\ttest_commit current-commit &&\n+\tgit push parent1 &&\n+\tgit --git-dir=repo1 show-ref refs/heads/feature/template-current &&\n+\ttest_cmp_rev HEAD parent1/feature/template-current &&\n+\techo \"refs/heads/feature/template-current\" >expect &&\n+\tgit config branch.template-current.merge >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'remoteBranchTemplate ignored with push.default=nothing' '\n+\tgit checkout -b template-nothing &&\n+\ttest_config push.default nothing &&\n+\ttest_config checkout.remoteBranchTemplate \"feature/%s\" &&\n+\ttest_commit nothing-commit &&\n+\ttest_must_fail git push 2>err &&\n+\ttest_grep \"No configured push destination\" err\n+'\n+\n test_done\n\nbase-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda\n-- \ngitgitgadget\n"},{"id":"532611","messageId":"xmqqtsxjruwi.fsf@gitster.g","threadId":"64659","inReplyTo":"pull.2136.git.git.1766332796836.gitgitgadget@gmail.com","subject":"Re: [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-22T04:40:45Z","receivedAt":"2025-12-22T04:40:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Pasteley Absurda via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: pasteley <ceasebeing@gmail.com>\n>\n> Add checkout.remoteBranchTemplate to apply a template pattern when\n> searching for remote branches during checkout DWIM and when creating\n> remote branches with push.autoSetupRemote.\n>\n> Template uses printf-style placeholders (%s for branch name). For\n> example, with \"feature/%s\", checking out \"foo\"\n> searches for \"origin/feature/foo\" and creates local \"foo\"\n> tracking it. Pushing with autoSetupRemote creates \"origin/feature/bar\"\n> from local \"bar\".\n>\n> Useful when remote branches use prefixes but local branches don't.\n\nIt fells that this is presented backwards.  The usefulness of the\nlayout that names local branches deliberately differently from their\nremote counterparts needs to be justified first.  Only after that,\nwe can consider adding extra mechanism to support such a layout.\n\n\nOnce \"git checkout foo\" is taught to do the same as \"git checkout -b\nextra-foo -t origin/foo\", it would create\n\n\t[branch \"extra-foo\"] \n\t\tremote = origin\n\t\tmerge = refs/heads/foo\n\nbut the push side would need extra work, and that is why you needed\nto muck with the push refspec.  But then what should happen when the\nuser is using \"we do not bother remembering what branches to push\nthere; the remote repository remembers that for us\", aka \"matching\npush\"?\n\nMost of the problems is what you are creating by using an unusual\nlayout to name local branches differently from the remote\ncounterpart.  You do not have to, and then all the problems you\ncreated with that layout goes away, without this patch.\n\nSo, I am not sure if this is a good idea to begin with.  At least, I\nam not yet convinced.\n\nThanks.\n\n"},{"id":"532626","messageId":"7246f693-2047-4218-8103-8143e01032f0@gmail.com","threadId":"64659","inReplyTo":"xmqqtsxjruwi.fsf@gitster.g","subject":"Re: [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names","fromName":"pasteley","fromEmail":"ceasebeing@gmail.com","sentAt":"2025-12-22T18:27:44Z","receivedAt":"2025-12-22T18:27:47Z","isPatch":true,"sender":{"key":"ceasebeing@gmail.com","avatar":null},"body":"You're right that same-named branches are the ideal Git workflow, and I\nagree this patch should not encourage drifting away from that model.\n\nThe motivation here is for cases where the name mismatch is imposed by\nexternal constraints, not chosen by developers. For example:\n\n1. Server-side policies/hooks that require a namespace on the remote\n    (e.g. `team/*`, `users/<id>/*`, `release/*`).\n2. Hosting / mirroring setups where remote branches live under a fixed\n    prefix for organizational or access-control reasons.\n3. Migrations where the remote branch layout is constrained by the\n    target system, while local developer workflows assume short names.\n\nIn these scenarios developers do not create the problem, they inherit it.\nThe alternative today is to type the prefixed remote name everywhere and\ngive up DWIM convenience (e.g. `git checkout foo` no longer does the\n\"natural\" thing).\n\nThis remains opt-in via `checkout.remoteBranchTemplate`, so only workflows\nthat explicitly configure it change behavior; defaults stay unchanged.\nExplicit operations still bypass the template (e.g. `-b/-c <name>` and an\nexplicit push refspec keep full user control).\n\nGit already supports name mismatches in a few places:\n* `remote.<name>.fetch` allows arbitrary mappings for remote refs.\n* `branch.<name>.merge` can track a differently named remote branch.\n* `push.default=upstream` pushes to the configured upstream even if names\n   differ.\n\nHowever, configuring `remote.<name>.push` does not solve the DWIM checkout\nproblem: users still need to know the full remote branch name to check it\nout, and wildcard push refspecs can have surprising scope (they can match\nmany branches unless the user is always explicit). This patch keeps the\nscope narrow: it only affects cases where Git is already \"guessing\" the\nremote side (checkout/switch/worktree --guess-remote, and automatic\nupstream setup).\n\n> Once \"git checkout foo\" is taught to do the same as \"git checkout -b\n> extra-foo -t origin/foo\", it would create:\n>\n> [branch \"extra-foo\"]\n> remote = origin\n> merge = refs/heads/foo\n\nYes, but only for the DWIM path where Git derives the remote branch. When\nthe user explicitly names the local branch, we do not apply the template.\n\n> But then what should happen when the user is using \"matching push\"?\n\nWhile `push.default=matching` has been deprecated since Git 2.0 (~ 2014),\nwe still can handle this corner case by detecting the incompatibility\nand providing a clear error message.\n\nThanks for the thorough review,\npasteley\n\nOn 22/12/2025 5:40 AM, Junio C Hamano wrote:\n> \"Pasteley Absurda via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>\n>> From: pasteley <ceasebeing@gmail.com>\n>>\n>> Add checkout.remoteBranchTemplate to apply a template pattern when\n>> searching for remote branches during checkout DWIM and when creating\n>> remote branches with push.autoSetupRemote.\n>>\n>> Template uses printf-style placeholders (%s for branch name). For\n>> example, with \"feature/%s\", checking out \"foo\"\n>> searches for \"origin/feature/foo\" and creates local \"foo\"\n>> tracking it. Pushing with autoSetupRemote creates \"origin/feature/bar\"\n>> from local \"bar\".\n>>\n>> Useful when remote branches use prefixes but local branches don't.\n> It fells that this is presented backwards.  The usefulness of the\n> layout that names local branches deliberately differently from their\n> remote counterparts needs to be justified first.  Only after that,\n> we can consider adding extra mechanism to support such a layout.\n>\n>\n> Once \"git checkout foo\" is taught to do the same as \"git checkout -b\n> extra-foo -t origin/foo\", it would create\n>\n> \t[branch \"extra-foo\"]\n> \t\tremote = origin\n> \t\tmerge = refs/heads/foo\n>\n> but the push side would need extra work, and that is why you needed\n> to muck with the push refspec.  But then what should happen when the\n> user is using \"we do not bother remembering what branches to push\n> there; the remote repository remembers that for us\", aka \"matching\n> push\"?\n>\n> Most of the problems is what you are creating by using an unusual\n> layout to name local branches differently from the remote\n> counterpart.  You do not have to, and then all the problems you\n> created with that layout goes away, without this patch.\n>\n> So, I am not sure if this is a good idea to begin with.  At least, I\n> am not yet convinced.\n>\n> Thanks.\n>\n"},{"id":"532633","messageId":"xmqqy0muqaut.fsf@gitster.g","threadId":"64659","inReplyTo":"7246f693-2047-4218-8103-8143e01032f0@gmail.com","subject":"Re: [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-23T00:51:22Z","receivedAt":"2025-12-23T00:51:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"pasteley <ceasebeing@gmail.com> writes:\n\n> 1. Server-side policies/hooks that require a namespace on the remote\n>     (e.g. `team/*`, `users/<id>/*`, `release/*`).\n> 2. Hosting / mirroring setups where remote branches live under a fixed\n>     prefix for organizational or access-control reasons.\n> 3. Migrations where the remote branch layout is constrained by the\n>     target system, while local developer workflows assume short names.\n>\n> In these scenarios developers do not create the problem, they inherit it.\n\nNobody stops you from interacting with projects like the above, and\nlocally name branches you store your work in users/pasteley/topic-1\ninstead of topic-1, no?\n\n> While `push.default=matching` has been deprecated since Git 2.0 (~ 2014),\n> we still can handle this corner case by detecting the incompatibility\n> and providing a clear error message.\n\nI do not know if you are confused, or if you are citing somebody\nelse's description that is confused, but the word \"deprecated\" in\nthe above statement is misused.  So is \"corner case\".\n\nWhen we talk about deprecation, the deprecated feature is something\nthat is not useful for anybody to adopt because there are better\nalternatives available, we wish there is no need to support the\nusers, and we hope we can remove it eventually someday.  The\nmatching push does not fall into that category at all.\n\nWhat we did in Git 2.0 was to change the default from matching to\nsimple, because the matching is the most useful for those who\npublish their changes, and for those who are merely participating\nsomebody else's project, it is not suited.  The matching push is\nalso harder to use properly if a project has multiple people who\npush to a single repository (i.e., central repository approach).\nThe default was switched to make life easier for more people.  It\ndid not diminish the usefulness of the matching mode for the\ndevelopers for whom the matching was the most useful mode.\n"},{"id":"532635","messageId":"1959d147-1a2d-40b4-9ca6-b85a461d2568@gmail.com","threadId":"64659","inReplyTo":"xmqqy0muqaut.fsf@gitster.g","subject":"Re: [PATCH] checkout: add remoteBranchTemplate config for DWIM branch names","fromName":"pasteley","fromEmail":"ceasebeing@gmail.com","sentAt":"2025-12-23T02:39:43Z","receivedAt":"2025-12-23T02:39:46Z","isPatch":true,"sender":{"key":"ceasebeing@gmail.com","avatar":null},"body":"On 23/12/2025 1:51 AM, Junio C Hamano wrote:\n> pasteley <ceasebeing@gmail.com> writes:\n> \n>> 1. Server-side policies/hooks that require a namespace on the remote\n>>      (e.g. `team/*`, `users/<id>/*`, `release/*`).\n>> 2. Hosting / mirroring setups where remote branches live under a fixed\n>>      prefix for organizational or access-control reasons.\n>> 3. Migrations where the remote branch layout is constrained by the\n>>      target system, while local developer workflows assume short names.\n>>\n>> In these scenarios developers do not create the problem, they inherit it.\n> \n> Nobody stops you from interacting with projects like the above, and\n> locally name branches you store your work in users/pasteley/topic-1\n> instead of topic-1, no?\n> \n\nI would say: forced to handle it this way. And guessing I’m not the only\none struggling. Meanwhile, there’s a simple quality-of-life improvement\nthat would help avoid all the hacky custom aliases and shell workarounds\nfor what should be a relatively straightforward remote refspec format\nadjustment.\nI’d really like to see this discussion move toward practical,\nconstructive steps, though I’m not sure what it would take to get there.\n\n"}]}