{"thread":{"id":"66040","subject":"[PATCH 0/2] remote: resolve url push tracking","startedAt":"2026-07-20T09:10:20Z","lastAt":"2026-07-23T00:52:53Z","messageCount":23,"participants":["Harald Nordgren via GitGitGadget","Junio C Hamano","Harald Nordgren","D. Ben Knoble","Ben Knoble"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"548657","messageId":"pull.2358.git.git.1784538618.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":null,"subject":"[PATCH 0/2] remote: resolve url push tracking","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-20T09:10:16Z","receivedAt":"2026-07-20T09:10:20Z","isPatch":true,"body":"After renaming remotes, git status may stop showing the push branch even\nthough Git still pushes to the right URL, use the remote with the same URL\nto find it.\n\nHarald Nordgren (2):\n  remote: pass repository to push tracking helper\n  remote: resolve URL-valued push tracking remotes\n\n Documentation/revisions.adoc |   3 +\n remote.c                     |  36 ++++++++++--\n remote.h                     |   2 +\n t/t5505-remote.sh            | 104 +++++++++++++++++++++++++++++++++++\n transport.c                  |   5 +-\n 5 files changed, 144 insertions(+), 6 deletions(-)\n\n\nbase-commit: 41365c2a9ba347870b80881c0d67454edd22fd49\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2358%2FHaraldNordgren%2Fremote-resolve-url-push-tracking-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2358/HaraldNordgren/remote-resolve-url-push-tracking-v1\nPull-Request: https://github.com/git/git/pull/2358\n-- \ngitgitgadget\n"},{"id":"548658","messageId":"fc70895732f406ecdbaea7a5b9a3fda4fb03df67.1784538618.git.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.git.git.1784538618.gitgitgadget@gmail.com","subject":"[PATCH 1/2] remote: pass repository to push tracking helper","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-20T09:10:17Z","receivedAt":"2026-07-20T09:10:21Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nThe push tracking helper currently only needs the push remote. However,\nresolving a URL-valued remote requires access to the repository's list\nof configured remotes.\n\nPass the repository through the existing callers and mark the parameter\nas unused for now. This prepares the helper for that lookup without\nchanging its behavior.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 11 ++++++-----\n 1 file changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex e6c52c850c..89d0f9e2d8 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1887,7 +1887,8 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n \treturn branch->merge[0]->dst;\n }\n \n-static char *tracking_for_push_dest(struct remote *remote,\n+static char *tracking_for_push_dest(struct repository *repo UNUSED,\n+\t\t\t\t    struct remote *remote,\n \t\t\t\t    const char *refname,\n \t\t\t\t    struct strbuf *err)\n {\n@@ -1925,13 +1926,13 @@ static char *branch_get_push_1(struct repository *repo,\n \t\t\t\t\t _(\"push refspecs for '%s' do not include '%s'\"),\n \t\t\t\t\t remote->name, branch->name);\n \n-\t\tret = tracking_for_push_dest(remote, dst, err);\n+\t\tret = tracking_for_push_dest(repo, remote, dst, err);\n \t\tfree(dst);\n \t\treturn ret;\n \t}\n \n \tif (remote->mirror)\n-\t\treturn tracking_for_push_dest(remote, branch->refname, err);\n+\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n \n \tswitch (push_default) {\n \tcase PUSH_DEFAULT_NOTHING:\n@@ -1939,7 +1940,7 @@ static char *branch_get_push_1(struct repository *repo,\n \n \tcase PUSH_DEFAULT_MATCHING:\n \tcase PUSH_DEFAULT_CURRENT:\n-\t\treturn tracking_for_push_dest(remote, branch->refname, err);\n+\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n \n \tcase PUSH_DEFAULT_UPSTREAM:\n \t\treturn xstrdup_or_null(branch_get_upstream(branch, err));\n@@ -1953,7 +1954,7 @@ static char *branch_get_push_1(struct repository *repo,\n \t\t\tup = branch_get_upstream(branch, err);\n \t\t\tif (!up)\n \t\t\t\treturn NULL;\n-\t\t\tcur = tracking_for_push_dest(remote, branch->refname, err);\n+\t\t\tcur = tracking_for_push_dest(repo, remote, branch->refname, err);\n \t\t\tif (!cur)\n \t\t\t\treturn NULL;\n \t\t\tif (strcmp(cur, up)) {\n-- \ngitgitgadget\n\n"},{"id":"548661","messageId":"ff645b21591a4b365b30acaf67a295510889141c.1784538618.git.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.git.git.1784538618.gitgitgadget@gmail.com","subject":"[PATCH 2/2] remote: resolve URL-valued push tracking remotes","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-20T09:10:18Z","receivedAt":"2026-07-20T09:10:24Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nA branch may name its push destination with a URL instead of a\nconfigured remote. This is useful in fork workflows, where the original\nremote is renamed to \"upstream\", the fork is added as \"origin\", and an\nexisting branch.<name>.pushRemote continues to contain the fork URL.\n\nGit can still push through the anonymous remote created for that URL.\nHowever, the anonymous remote has no fetch refspec. Git therefore cannot\nresolve @{push} to origin/<branch> or update that remote-tracking branch\nafter a push. The push can succeed, or report that everything is up to\ndate, while status continues to compare against a stale tracking ref or\ncannot show the push branch at all.\n\nA uniquely matching configured remote already provides the missing\nmapping. Use its fetch refspec when resolving the push tracking branch\nand when updating tracking refs after a push. This changes neither the\npush destination nor configuration. Keep the existing behavior when no\nremote matches or multiple remotes share the URL, since either case is\nambiguous.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/revisions.adoc |   3 +\n remote.c                     |  27 ++++++++-\n remote.h                     |   2 +\n t/t5505-remote.sh            | 104 +++++++++++++++++++++++++++++++++++\n transport.c                  |   5 +-\n 5 files changed, 139 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/revisions.adoc b/Documentation/revisions.adoc\nindex 6ea6c7cead..b691691c8c 100644\n--- a/Documentation/revisions.adoc\n+++ b/Documentation/revisions.adoc\n@@ -127,6 +127,9 @@ some output processing may assume ref names in UTF-8.\n   `git push` were run while `branchname` was checked out (or the current\n   `HEAD` if no branchname is specified). Like for '@\\{upstream\\}', we report\n   the remote-tracking branch that corresponds to that branch at the remote.\n+  If the push remote is specified as a URL, the fetch refspec of a uniquely\n+  matching configured remote is used to find and update the remote-tracking\n+  branch.\n +\n Here's an example to make it more clear:\n +\ndiff --git a/remote.c b/remote.c\nindex 89d0f9e2d8..03908dfe8d 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1887,13 +1887,38 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n \treturn branch->merge[0]->dst;\n }\n \n-static char *tracking_for_push_dest(struct repository *repo UNUSED,\n+struct remote *repo_remote_for_push_tracking(struct repository *repo,\n+\t\t\t\t\t     struct remote *remote)\n+{\n+\tstruct remote *first_match = NULL;\n+\tstruct remote_state *remote_state = repo->remote_state;\n+\n+\tif (remote->origin != REMOTE_UNCONFIGURED || remote->url.nr != 1)\n+\t\treturn remote;\n+\n+\tfor (int i = 0; i < remote_state->remotes_nr; i++) {\n+\t\tstruct remote *candidate = remote_state->remotes[i];\n+\n+\t\tif (!candidate || candidate == remote ||\n+\t\t    !remote_is_configured(candidate, 0) ||\n+\t\t    !remote_has_url(candidate, remote->url.v[0]))\n+\t\t\tcontinue;\n+\t\tif (first_match)\n+\t\t\treturn remote;\n+\t\tfirst_match = candidate;\n+\t}\n+\n+\treturn first_match ? first_match : remote;\n+}\n+\n+static char *tracking_for_push_dest(struct repository *repo,\n \t\t\t\t    struct remote *remote,\n \t\t\t\t    const char *refname,\n \t\t\t\t    struct strbuf *err)\n {\n \tchar *ret;\n \n+\tremote = repo_remote_for_push_tracking(repo, remote);\n \tret = apply_refspecs(&remote->fetch, refname);\n \tif (!ret)\n \t\treturn error_buf(err,\ndiff --git a/remote.h b/remote.h\nindex 72a54d84ad..cca02033b9 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -345,6 +345,8 @@ char *remote_ref_for_branch(struct branch *branch, int for_push);\n \n const char *repo_default_remote(struct repository *repo);\n const char *repo_remote_from_url(struct repository *repo, const char *url);\n+struct remote *repo_remote_for_push_tracking(struct repository *repo,\n+\t\t\t\t\t     struct remote *remote);\n \n /* returns true if the given branch has merge configuration given. */\n int branch_has_merge_config(struct branch *branch);\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex e592c0bcde..e16b3f320a 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -24,6 +24,28 @@ setup_repository () {\n \t)\n }\n \n+setup_url_pushremote () {\n+\trm -rf fork.git client &&\n+\tgit clone --bare one fork.git &&\n+\tgit clone one client &&\n+\tfork_url=\"$TRASH_DIRECTORY/fork.git\" &&\n+\t(\n+\t\tcd client &&\n+\t\tgit checkout -b topic --track origin/main &&\n+\t\tgit commit --allow-empty -m topic-change &&\n+\t\tgit config push.default current &&\n+\t\tgit config status.compareBranches \"@{upstream} @{push}\" &&\n+\t\tgit config branch.topic.pushRemote \"$fork_url\" &&\n+\t\tgit push\n+\t)\n+}\n+\n+check_status () {\n+\tgit -C client status >actual &&\n+\tcat >expected &&\n+\ttest_cmp expected actual\n+}\n+\n tokens_match () {\n \techo \"$1\" | tr ' ' '\\012' | sort | sed -e '/^$/d' >expect &&\n \techo \"$2\" | tr ' ' '\\012' | sort | sed -e '/^$/d' >actual &&\n@@ -1018,6 +1040,88 @@ test_expect_success 'rename a remote renames repo remote.pushDefault but keeps g\n \t)\n '\n \n+test_expect_success 'URL-valued pushRemote without matching remote is not trackable' '\n+\tsetup_url_pushremote &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'adding fork remote makes URL-valued pushRemote trackable' '\n+\tsetup_url_pushremote &&\n+\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\"\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'up-to-date URL push refreshes stale tracking branch' '\n+\tsetup_url_pushremote &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\" &&\n+\t\tgit commit --allow-empty -m another-topic-change &&\n+\t\tgit -C ../fork.git fetch ../client topic:topic\n+\t) &&\n+\n+\tcheck_status <<-EOF &&\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.\n+\n+\tYour branch is ahead of ${SQ}origin/topic${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\n+\tgit -C client push >actual 2>&1 &&\n+\ttest_grep \"Everything up-to-date\" actual &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'duplicate remote URL leaves URL-valued pushRemote ambiguous' '\n+\tsetup_url_pushremote &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\" &&\n+\t\tgit remote add duplicate \"$fork_url\"\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n test_expect_success 'rename handles remote without fetch refspec' '\n \tgit clone --bare one no-refspec.git &&\n \t# confirm assumption that bare clone does not create refspec\ndiff --git a/transport.c b/transport.c\nindex fc144f0aed..30a4ab2cd5 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1553,8 +1553,11 @@ int transport_push(struct repository *r,\n \tif (!(flags & (TRANSPORT_PUSH_DRY_RUN |\n \t\t       TRANSPORT_RECURSE_SUBMODULES_ONLY))) {\n \t\tstruct ref *ref;\n+\t\tstruct remote *tracking_remote = repo_remote_for_push_tracking(\n+\t\t\tr, transport->remote);\n+\n \t\tfor (ref = remote_refs; ref; ref = ref->next)\n-\t\t\ttransport_update_tracking_ref(transport->remote, ref, verbose);\n+\t\t\ttransport_update_tracking_ref(tracking_remote, ref, verbose);\n \t}\n \n \tif (porcelain && !push_ret)\n-- \ngitgitgadget\n"},{"id":"548687","messageId":"xmqqfr1dcygh.fsf@gitster.g","threadId":"66040","inReplyTo":"fc70895732f406ecdbaea7a5b9a3fda4fb03df67.1784538618.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 1/2] remote: pass repository to push tracking helper","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-20T18:23:10Z","receivedAt":"2026-07-20T18:23:14Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> The push tracking helper currently only needs the push remote. However,\n> resolving a URL-valued remote requires access to the repository's list\n> of configured remotes.\n\nIt is unclear to me what 'resolving a URL-valued remote' means.\nCould you describe what you are trying to achieve, without relying\non unexplained terms like 'to resolve' and 'URL-valued remote',\nwhich seem to carry specialized meanings in this context?\n\nThanks.\n\n> Pass the repository through the existing callers and mark the parameter\n> as unused for now. This prepares the helper for that lookup without\n> changing its behavior.\n>\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n>  remote.c | 11 ++++++-----\n>  1 file changed, 6 insertions(+), 5 deletions(-)\n>\n> diff --git a/remote.c b/remote.c\n> index e6c52c850c..89d0f9e2d8 100644\n> --- a/remote.c\n> +++ b/remote.c\n> @@ -1887,7 +1887,8 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n>  \treturn branch->merge[0]->dst;\n>  }\n>  \n> -static char *tracking_for_push_dest(struct remote *remote,\n> +static char *tracking_for_push_dest(struct repository *repo UNUSED,\n> +\t\t\t\t    struct remote *remote,\n>  \t\t\t\t    const char *refname,\n>  \t\t\t\t    struct strbuf *err)\n>  {\n> @@ -1925,13 +1926,13 @@ static char *branch_get_push_1(struct repository *repo,\n>  \t\t\t\t\t _(\"push refspecs for '%s' do not include '%s'\"),\n>  \t\t\t\t\t remote->name, branch->name);\n>  \n> -\t\tret = tracking_for_push_dest(remote, dst, err);\n> +\t\tret = tracking_for_push_dest(repo, remote, dst, err);\n>  \t\tfree(dst);\n>  \t\treturn ret;\n>  \t}\n>  \n>  \tif (remote->mirror)\n> -\t\treturn tracking_for_push_dest(remote, branch->refname, err);\n> +\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n>  \n>  \tswitch (push_default) {\n>  \tcase PUSH_DEFAULT_NOTHING:\n> @@ -1939,7 +1940,7 @@ static char *branch_get_push_1(struct repository *repo,\n>  \n>  \tcase PUSH_DEFAULT_MATCHING:\n>  \tcase PUSH_DEFAULT_CURRENT:\n> -\t\treturn tracking_for_push_dest(remote, branch->refname, err);\n> +\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n>  \n>  \tcase PUSH_DEFAULT_UPSTREAM:\n>  \t\treturn xstrdup_or_null(branch_get_upstream(branch, err));\n> @@ -1953,7 +1954,7 @@ static char *branch_get_push_1(struct repository *repo,\n>  \t\t\tup = branch_get_upstream(branch, err);\n>  \t\t\tif (!up)\n>  \t\t\t\treturn NULL;\n> -\t\t\tcur = tracking_for_push_dest(remote, branch->refname, err);\n> +\t\t\tcur = tracking_for_push_dest(repo, remote, branch->refname, err);\n>  \t\t\tif (!cur)\n>  \t\t\t\treturn NULL;\n>  \t\t\tif (strcmp(cur, up)) {\n"},{"id":"548688","messageId":"xmqq4ihtcx8g.fsf@gitster.g","threadId":"66040","inReplyTo":"ff645b21591a4b365b30acaf67a295510889141c.1784538618.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/2] remote: resolve URL-valued push tracking remotes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-20T18:49:35Z","receivedAt":"2026-07-20T18:49:38Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> A branch may name its push destination with a URL instead of a\n> configured remote. This is useful in fork workflows, where the original\n> remote is renamed to \"upstream\", the fork is added as \"origin\", and an\n> existing branch.<name>.pushRemote continues to contain the fork URL.\n>\n> Git can still push through the anonymous remote created for that URL.\n> However, the anonymous remote has no fetch refspec. Git therefore cannot\n> resolve @{push} to origin/<branch> or update that remote-tracking branch\n> after a push. The push can succeed, or report that everything is up to\n> date, while status continues to compare against a stale tracking ref or\n> cannot show the push branch at all.\n\nLet me try to think aloud, rephrasing the explanation with a\nslightly more concrete illustration, to see whether I understand\nwhat you are trying to achieve.\n\nThe current system allows you to set:\n\n     [branch \"mytopic\"]\n        pushRemote = https://hosting.site/users/me/mine.git/\n     [remote \"notlinked\"]\n        url = https://hosting.site/users/me/mine.git/\n        push = refs/heads/mytopic\n        fetch = refs/heads/*:refs/remotes/notlinked/*\n\nbut when on the 'mytopic' branch, @{push} cannot determine which\nbranch at the remote repository to update, so it cannot map it back\nto our remote-tracking branch ('refs/remotes/notlinked/mytopic' in\nthe above illustration).\n\nA question.  Do we currently accept a string that is not a remote\nname as the value for 'branch.<name>.pushRemote' by design?\n\nThe 'git config --help' output explains that:\n\n - 'branch.<name>.pushRemote' overrides 'branch.<name>.remote' and\n   'remote.pushDefault'; and\n\n - 'branch.<name>.remote' and 'remote.pushDefault' tell 'git fetch'\n   and 'git push' which remote to work with.\n\nIt therefore seems clear that setting a string that is not a remote\nname (such as a URL) as the value for these three variables is a\nmisconfiguration in the current system.\n\nI am not saying that it should stay that way forever.  But please\nre-read your first sentence and tell me whether it is clear that the\npatch extends the current system with a new feature.  It was far\nfrom clear to me and caused significant confusion.  Writing it like\nthis:\n\n    Under the current system, a branch cannot name its push\n    destination using a URL.  If we were to extend the system\n    to allow this, such and such benefits would become\n    possible.\n\nwould have been far less confusing.\n\nIf that is what you are doing, that is.\n\n> A uniquely matching configured remote already provides the missing\n> mapping.\n\nA very good consideration.  It was the first thing that came to my\nmind while I was thinking aloud, constructing an illustration with\n'notlinked', wondering \"what if there is another remote, with the\nsame URL, but different 'push' configuration?\".\n\n> Use its fetch refspec when resolving the push tracking branch\n> and when updating tracking refs after a push.\n\nIs this not needless, and is mentioning it not confusing?  If I\nunderstand correctly, what the change entails is:\n\n * If the value of 'branch.<name>.pushRemote' (call it X) is 'not' a\n   remote name, try to see whether there is a unique remote that\n   has either (1) a 'pushurl' whose value matches X, or (2) no\n   'pushurl' but a 'url' whose value matches X.  If no such remote\n   exists, simply abort and refuse to proceed.\n\n * If there is such a remote, pretend that the value of\n   'branch.<name>.pushRemote' were the name of that remote, and do\n   everything else as usual.\n\nAnd mapping the current branch name to its push destination via\n'remote.<name>.push' to find the name of the destination branch at\nthe remote, and then mapping it back to our remote-tracking branch\nusing 'remote.<name>.fetch', is not something new that this topic\nneeds to update, no?\n\nThanks.  Once I understand what you are trying to achieve, I will\noffer further comments on the implementation, as I find this topic\npotentially quite interesting.\n"},{"id":"548689","messageId":"CAHwyqnV=ZbthekwTcmrK5twCOgNETW+0Z5uj=w3oKjUK6Hv47g@mail.gmail.com","threadId":"66040","inReplyTo":"xmqq4ihtcx8g.fsf@gitster.g","subject":"Re: [PATCH 2/2] remote: resolve URL-valued push tracking remotes","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-07-20T19:56:25Z","receivedAt":"2026-07-20T19:57:06Z","isPatch":true,"body":"Thanks for your continued support on all my topics!\n\nYes, I should clarify in the commit message what the actual motivation\nis, which is for me to handle remote renames in a smoother way, since\n'gh' renmames remotes when forking a repo which is messing with\n@{push} and compareBranches for 'git status'.\n\n\nHarald\n"},{"id":"548705","messageId":"xmqqmrvl9q7k.fsf@gitster.g","threadId":"66040","inReplyTo":"CAHwyqnV=ZbthekwTcmrK5twCOgNETW+0Z5uj=w3oKjUK6Hv47g@mail.gmail.com","subject":"Re: [PATCH 2/2] remote: resolve URL-valued push tracking remotes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-20T23:49:35Z","receivedAt":"2026-07-20T23:49:38Z","isPatch":true,"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> Thanks for your continued support on all my topics!\n>\n> Yes, I should clarify in the commit message what the actual motivation\n> is, which is for me to handle remote renames in a smoother way, since\n> 'gh' renmames remotes when forking a repo which is messing with\n> @{push} and compareBranches for 'git status'.\n\n\nYeah, it would be a good thing to do in an updated version.\n\nThanks.\n"},{"id":"548717","messageId":"pull.2358.v2.git.git.1784624306.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.git.git.1784538618.gitgitgadget@gmail.com","subject":"[PATCH v2 0/2] remote: renamed remote push tracking","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-21T08:58:24Z","receivedAt":"2026-07-21T08:58:29Z","isPatch":true,"body":"Keep git status showing the push branch after remotes are renamed by finding\nthe configured remote with the same URL.\n\nChanges in v3:\n\n * Revamp commit messages to clarify motivation.\n\nChanges in v2:\n\n * Clarify that URL push destinations already work and that this change only\n   restores their tracking information.\n * Document URL values for branch.<name>.pushRemote and their @{push}\n   behavior.\n\nHarald Nordgren (2):\n  remote: pass repository to push tracking helper\n  remote: find tracking branches for URL push destinations\n\n Documentation/config/branch.adoc |   2 +\n Documentation/revisions.adoc     |   3 +\n remote.c                         |  36 +++++++++--\n remote.h                         |   2 +\n t/t5505-remote.sh                | 104 +++++++++++++++++++++++++++++++\n transport.c                      |   5 +-\n 6 files changed, 146 insertions(+), 6 deletions(-)\n\n\nbase-commit: 48bbf81c29ca9a4479ec7850fe206518682cdb2f\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2358%2FHaraldNordgren%2Fremote-resolve-url-push-tracking-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2358/HaraldNordgren/remote-resolve-url-push-tracking-v2\nPull-Request: https://github.com/git/git/pull/2358\n\nRange-diff vs v1:\n\n 1:  fc70895732 ! 1:  b1ac49de87 remote: pass repository to push tracking helper\n     @@ Metadata\n       ## Commit message ##\n          remote: pass repository to push tracking helper\n      \n     -    The push tracking helper currently only needs the push remote. However,\n     -    resolving a URL-valued remote requires access to the repository's list\n     -    of configured remotes.\n     +    The next commit needs tracking_for_push_dest() to inspect the\n     +    repository's configured remotes. Pass the repository through the\n     +    existing callers and mark the new parameter as unused.\n      \n     -    Pass the repository through the existing callers and mark the parameter\n     -    as unused for now. This prepares the helper for that lookup without\n     -    changing its behavior.\n     +    No change in behavior.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n 2:  ff645b2159 ! 2:  6e924a7fec remote: resolve URL-valued push tracking remotes\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    remote: resolve URL-valued push tracking remotes\n     +    remote: find tracking branches for URL push destinations\n      \n     -    A branch may name its push destination with a URL instead of a\n     -    configured remote. This is useful in fork workflows, where the original\n     -    remote is renamed to \"upstream\", the fork is added as \"origin\", and an\n     -    existing branch.<name>.pushRemote continues to contain the fork URL.\n     +    Git already accepts a repository URL as branch.<name>.pushRemote and\n     +    can push to it. When a configured remote has the same URL, however,\n     +    \"git status\" cannot show that remote's push branch.\n      \n     -    Git can still push through the anonymous remote created for that URL.\n     -    However, the anonymous remote has no fetch refspec. Git therefore cannot\n     -    resolve @{push} to origin/<branch> or update that remote-tracking branch\n     -    after a push. The push can succeed, or report that everything is up to\n     -    date, while status continues to compare against a stale tracking ref or\n     -    cannot show the push branch at all.\n     +    This can happen in fork workflows when the original remote is renamed\n     +    to \"upstream\", the fork is added as \"origin\", and an existing\n     +    pushRemote value still contains the fork URL. The URL still points to\n     +    the right repository, so pushing works. However, @{push} is unavailable\n     +    because Git does not connect the URL to \"origin\". As a result,\n     +    \"git status\" cannot show the push branch, and an up-to-date push can\n     +    leave its local tracking information stale.\n      \n     -    A uniquely matching configured remote already provides the missing\n     -    mapping. Use its fetch refspec when resolving the push tracking branch\n     -    and when updating tracking refs after a push. This changes neither the\n     -    push destination nor configuration. Keep the existing behavior when no\n     -    remote matches or multiple remotes share the URL, since either case is\n     -    ambiguous.\n     +    When exactly one configured remote has the URL as one of its\n     +    remote.<name>.url values, use its fetch refspec to find and refresh the\n     +    push branch. Keep the URL as the push destination so the configured\n     +    remote's push settings do not change existing behavior. Keep the\n     +    current behavior when no remote matches or multiple remotes match.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     + ## Documentation/config/branch.adoc ##\n     +@@ Documentation/config/branch.adoc: This option defaults to `never`.\n     + \trepository), you would want to set `remote.pushDefault` to\n     + \tspecify the remote to push to for all branches, and use this\n     + \toption to override it for a specific branch.\n     ++\tThe value may be the name of a configured remote or a repository\n     ++\tURL. A URL is used directly as the push destination.\n     + \n     + `branch.<name>.merge`::\n     + \tDefines, together with `branch.<name>.remote`, the upstream branch\n     +\n       ## Documentation/revisions.adoc ##\n      @@ Documentation/revisions.adoc: some output processing may assume ref names in UTF-8.\n         `git push` were run while `branchname` was checked out (or the current\n         `HEAD` if no branchname is specified). Like for '@\\{upstream\\}', we report\n         the remote-tracking branch that corresponds to that branch at the remote.\n     -+  If the push remote is specified as a URL, the fetch refspec of a uniquely\n     -+  matching configured remote is used to find and update the remote-tracking\n     -+  branch.\n     ++  If the push destination is a URL and exactly one configured remote has the\n     ++  same `remote.<name>.url`, '@\\{push}' reports the remote-tracking branch for\n     ++  that remote.\n       +\n       Here's an example to make it more clear:\n       +\n\n-- \ngitgitgadget\n"},{"id":"548718","messageId":"b1ac49de87c2a8b88aa2aed60b18847734fa906e.1784624306.git.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.v2.git.git.1784624306.gitgitgadget@gmail.com","subject":"[PATCH v2 1/2] remote: pass repository to push tracking helper","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-21T08:58:25Z","receivedAt":"2026-07-21T08:58:30Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nThe next commit needs tracking_for_push_dest() to inspect the\nrepository's configured remotes. Pass the repository through the\nexisting callers and mark the new parameter as unused.\n\nNo change in behavior.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 11 ++++++-----\n 1 file changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b17648d6ef..0dc36956c3 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1887,7 +1887,8 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n \treturn branch->merge[0]->dst;\n }\n \n-static char *tracking_for_push_dest(struct remote *remote,\n+static char *tracking_for_push_dest(struct repository *repo UNUSED,\n+\t\t\t\t    struct remote *remote,\n \t\t\t\t    const char *refname,\n \t\t\t\t    struct strbuf *err)\n {\n@@ -1925,13 +1926,13 @@ static char *branch_get_push_1(struct repository *repo,\n \t\t\t\t\t _(\"push refspecs for '%s' do not include '%s'\"),\n \t\t\t\t\t remote->name, branch->name);\n \n-\t\tret = tracking_for_push_dest(remote, dst, err);\n+\t\tret = tracking_for_push_dest(repo, remote, dst, err);\n \t\tfree(dst);\n \t\treturn ret;\n \t}\n \n \tif (remote->mirror)\n-\t\treturn tracking_for_push_dest(remote, branch->refname, err);\n+\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n \n \tswitch (push_default) {\n \tcase PUSH_DEFAULT_NOTHING:\n@@ -1939,7 +1940,7 @@ static char *branch_get_push_1(struct repository *repo,\n \n \tcase PUSH_DEFAULT_MATCHING:\n \tcase PUSH_DEFAULT_CURRENT:\n-\t\treturn tracking_for_push_dest(remote, branch->refname, err);\n+\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n \n \tcase PUSH_DEFAULT_UPSTREAM:\n \t\treturn xstrdup_or_null(branch_get_upstream(branch, err));\n@@ -1953,7 +1954,7 @@ static char *branch_get_push_1(struct repository *repo,\n \t\t\tup = branch_get_upstream(branch, err);\n \t\t\tif (!up)\n \t\t\t\treturn NULL;\n-\t\t\tcur = tracking_for_push_dest(remote, branch->refname, err);\n+\t\t\tcur = tracking_for_push_dest(repo, remote, branch->refname, err);\n \t\t\tif (!cur)\n \t\t\t\treturn NULL;\n \t\t\tif (strcmp(cur, up)) {\n-- \ngitgitgadget\n\n"},{"id":"548719","messageId":"6e924a7fecb364780a5e11d27c20a08599e0d21e.1784624306.git.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.v2.git.git.1784624306.gitgitgadget@gmail.com","subject":"[PATCH v2 2/2] remote: find tracking branches for URL push destinations","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-21T08:58:26Z","receivedAt":"2026-07-21T08:58:32Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nGit already accepts a repository URL as branch.<name>.pushRemote and\ncan push to it. When a configured remote has the same URL, however,\n\"git status\" cannot show that remote's push branch.\n\nThis can happen in fork workflows when the original remote is renamed\nto \"upstream\", the fork is added as \"origin\", and an existing\npushRemote value still contains the fork URL. The URL still points to\nthe right repository, so pushing works. However, @{push} is unavailable\nbecause Git does not connect the URL to \"origin\". As a result,\n\"git status\" cannot show the push branch, and an up-to-date push can\nleave its local tracking information stale.\n\nWhen exactly one configured remote has the URL as one of its\nremote.<name>.url values, use its fetch refspec to find and refresh the\npush branch. Keep the URL as the push destination so the configured\nremote's push settings do not change existing behavior. Keep the\ncurrent behavior when no remote matches or multiple remotes match.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/branch.adoc |   2 +\n Documentation/revisions.adoc     |   3 +\n remote.c                         |  27 +++++++-\n remote.h                         |   2 +\n t/t5505-remote.sh                | 104 +++++++++++++++++++++++++++++++\n transport.c                      |   5 +-\n 6 files changed, 141 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc\nindex a4db9fa5c8..e22b6c846d 100644\n--- a/Documentation/config/branch.adoc\n+++ b/Documentation/config/branch.adoc\n@@ -55,6 +55,8 @@ This option defaults to `never`.\n \trepository), you would want to set `remote.pushDefault` to\n \tspecify the remote to push to for all branches, and use this\n \toption to override it for a specific branch.\n+\tThe value may be the name of a configured remote or a repository\n+\tURL. A URL is used directly as the push destination.\n \n `branch.<name>.merge`::\n \tDefines, together with `branch.<name>.remote`, the upstream branch\ndiff --git a/Documentation/revisions.adoc b/Documentation/revisions.adoc\nindex 6ea6c7cead..670fc66053 100644\n--- a/Documentation/revisions.adoc\n+++ b/Documentation/revisions.adoc\n@@ -127,6 +127,9 @@ some output processing may assume ref names in UTF-8.\n   `git push` were run while `branchname` was checked out (or the current\n   `HEAD` if no branchname is specified). Like for '@\\{upstream\\}', we report\n   the remote-tracking branch that corresponds to that branch at the remote.\n+  If the push destination is a URL and exactly one configured remote has the\n+  same `remote.<name>.url`, '@\\{push}' reports the remote-tracking branch for\n+  that remote.\n +\n Here's an example to make it more clear:\n +\ndiff --git a/remote.c b/remote.c\nindex 0dc36956c3..2e07bd998f 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1887,13 +1887,38 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n \treturn branch->merge[0]->dst;\n }\n \n-static char *tracking_for_push_dest(struct repository *repo UNUSED,\n+struct remote *repo_remote_for_push_tracking(struct repository *repo,\n+\t\t\t\t\t     struct remote *remote)\n+{\n+\tstruct remote *first_match = NULL;\n+\tstruct remote_state *remote_state = repo->remote_state;\n+\n+\tif (remote->origin != REMOTE_UNCONFIGURED || remote->url.nr != 1)\n+\t\treturn remote;\n+\n+\tfor (int i = 0; i < remote_state->remotes_nr; i++) {\n+\t\tstruct remote *candidate = remote_state->remotes[i];\n+\n+\t\tif (!candidate || candidate == remote ||\n+\t\t    !remote_is_configured(candidate, 0) ||\n+\t\t    !remote_has_url(candidate, remote->url.v[0]))\n+\t\t\tcontinue;\n+\t\tif (first_match)\n+\t\t\treturn remote;\n+\t\tfirst_match = candidate;\n+\t}\n+\n+\treturn first_match ? first_match : remote;\n+}\n+\n+static char *tracking_for_push_dest(struct repository *repo,\n \t\t\t\t    struct remote *remote,\n \t\t\t\t    const char *refname,\n \t\t\t\t    struct strbuf *err)\n {\n \tchar *ret;\n \n+\tremote = repo_remote_for_push_tracking(repo, remote);\n \tret = apply_refspecs(&remote->fetch, refname);\n \tif (!ret)\n \t\treturn error_buf(err,\ndiff --git a/remote.h b/remote.h\nindex 72a54d84ad..cca02033b9 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -345,6 +345,8 @@ char *remote_ref_for_branch(struct branch *branch, int for_push);\n \n const char *repo_default_remote(struct repository *repo);\n const char *repo_remote_from_url(struct repository *repo, const char *url);\n+struct remote *repo_remote_for_push_tracking(struct repository *repo,\n+\t\t\t\t\t     struct remote *remote);\n \n /* returns true if the given branch has merge configuration given. */\n int branch_has_merge_config(struct branch *branch);\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 6f5e86dede..2c86661294 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -24,6 +24,28 @@ setup_repository () {\n \t)\n }\n \n+setup_url_pushremote () {\n+\trm -rf fork.git client &&\n+\tgit clone --bare one fork.git &&\n+\tgit clone one client &&\n+\tfork_url=\"$TRASH_DIRECTORY/fork.git\" &&\n+\t(\n+\t\tcd client &&\n+\t\tgit checkout -b topic --track origin/main &&\n+\t\tgit commit --allow-empty -m topic-change &&\n+\t\tgit config push.default current &&\n+\t\tgit config status.compareBranches \"@{upstream} @{push}\" &&\n+\t\tgit config branch.topic.pushRemote \"$fork_url\" &&\n+\t\tgit push\n+\t)\n+}\n+\n+check_status () {\n+\tgit -C client status >actual &&\n+\tcat >expected &&\n+\ttest_cmp expected actual\n+}\n+\n tokens_match () {\n \techo \"$1\" | tr ' ' '\\012' | sort | sed -e '/^$/d' >expect &&\n \techo \"$2\" | tr ' ' '\\012' | sort | sed -e '/^$/d' >actual &&\n@@ -1018,6 +1040,88 @@ test_expect_success 'rename a remote renames repo remote.pushDefault but keeps g\n \t)\n '\n \n+test_expect_success 'URL-valued pushRemote without matching remote is not trackable' '\n+\tsetup_url_pushremote &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'adding fork remote makes URL-valued pushRemote trackable' '\n+\tsetup_url_pushremote &&\n+\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\"\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'up-to-date URL push refreshes stale tracking branch' '\n+\tsetup_url_pushremote &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\" &&\n+\t\tgit commit --allow-empty -m another-topic-change &&\n+\t\tgit -C ../fork.git fetch ../client topic:topic\n+\t) &&\n+\n+\tcheck_status <<-EOF &&\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.\n+\n+\tYour branch is ahead of ${SQ}origin/topic${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\n+\tgit -C client push >actual 2>&1 &&\n+\ttest_grep \"Everything up-to-date\" actual &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'duplicate remote URL leaves URL-valued pushRemote ambiguous' '\n+\tsetup_url_pushremote &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\" &&\n+\t\tgit remote add duplicate \"$fork_url\"\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n test_expect_success 'rename handles remote without fetch refspec' '\n \tgit clone --bare one no-refspec.git &&\n \t# confirm assumption that bare clone does not create refspec\ndiff --git a/transport.c b/transport.c\nindex fc144f0aed..30a4ab2cd5 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1553,8 +1553,11 @@ int transport_push(struct repository *r,\n \tif (!(flags & (TRANSPORT_PUSH_DRY_RUN |\n \t\t       TRANSPORT_RECURSE_SUBMODULES_ONLY))) {\n \t\tstruct ref *ref;\n+\t\tstruct remote *tracking_remote = repo_remote_for_push_tracking(\n+\t\t\tr, transport->remote);\n+\n \t\tfor (ref = remote_refs; ref; ref = ref->next)\n-\t\t\ttransport_update_tracking_ref(transport->remote, ref, verbose);\n+\t\t\ttransport_update_tracking_ref(tracking_remote, ref, verbose);\n \t}\n \n \tif (porcelain && !push_ret)\n-- \ngitgitgadget\n"},{"id":"548724","messageId":"CALnO6CAY2x-adAxSXW1f_+OHjV_tVhLmkN7D+wE39rj3wc8LEQ@mail.gmail.com","threadId":"66040","inReplyTo":"pull.2358.v2.git.git.1784624306.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 0/2] remote: renamed remote push tracking","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-07-21T14:28:37Z","receivedAt":"2026-07-21T14:28:50Z","isPatch":true,"body":"Hi Harald,\n\nOn Tue, Jul 21, 2026 at 5:08 AM Harald Nordgren via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n>\n> Keep git status showing the push branch after remotes are renamed by finding\n> the configured remote with the same URL.\n>\n> Changes in v3:\n>\n>  * Revamp commit messages to clarify motivation.\n>\n> Changes in v2:\n>\n>  * Clarify that URL push destinations already work and that this change only\n>    restores their tracking information.\n>  * Document URL values for branch.<name>.pushRemote and their @{push}\n>    behavior.\n>\n> Harald Nordgren (2):\n>   remote: pass repository to push tracking helper\n>   remote: find tracking branches for URL push destinations\n>\n>  Documentation/config/branch.adoc |   2 +\n>  Documentation/revisions.adoc     |   3 +\n>  remote.c                         |  36 +++++++++--\n>  remote.h                         |   2 +\n>  t/t5505-remote.sh                | 104 +++++++++++++++++++++++++++++++\n>  transport.c                      |   5 +-\n>  6 files changed, 146 insertions(+), 6 deletions(-)\n>\n>\n> base-commit: 48bbf81c29ca9a4479ec7850fe206518682cdb2f\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2358%2FHaraldNordgren%2Fremote-resolve-url-push-tracking-v2\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2358/HaraldNordgren/remote-resolve-url-push-tracking-v2\n> Pull-Request: https://github.com/git/git/pull/2358\n>\n> Range-diff vs v1:\n>\n>  1:  fc70895732 ! 1:  b1ac49de87 remote: pass repository to push tracking helper\n>      @@ Metadata\n>        ## Commit message ##\n>           remote: pass repository to push tracking helper\n>\n>      -    The push tracking helper currently only needs the push remote. However,\n>      -    resolving a URL-valued remote requires access to the repository's list\n>      -    of configured remotes.\n>      +    The next commit needs tracking_for_push_dest() to inspect the\n>      +    repository's configured remotes. Pass the repository through the\n>      +    existing callers and mark the new parameter as unused.\n>\n>      -    Pass the repository through the existing callers and mark the parameter\n>      -    as unused for now. This prepares the helper for that lookup without\n>      -    changing its behavior.\n>      +    No change in behavior.\n>\n>           Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n>\n>  2:  ff645b2159 ! 2:  6e924a7fec remote: resolve URL-valued push tracking remotes\n>      @@ Metadata\n>       Author: Harald Nordgren <haraldnordgren@gmail.com>\n>\n>        ## Commit message ##\n>      -    remote: resolve URL-valued push tracking remotes\n>      +    remote: find tracking branches for URL push destinations\n>\n>      -    A branch may name its push destination with a URL instead of a\n>      -    configured remote. This is useful in fork workflows, where the original\n>      -    remote is renamed to \"upstream\", the fork is added as \"origin\", and an\n>      -    existing branch.<name>.pushRemote continues to contain the fork URL.\n>      +    Git already accepts a repository URL as branch.<name>.pushRemote and\n>      +    can push to it. When a configured remote has the same URL, however,\n>      +    \"git status\" cannot show that remote's push branch.\n>\n>      -    Git can still push through the anonymous remote created for that URL.\n>      -    However, the anonymous remote has no fetch refspec. Git therefore cannot\n>      -    resolve @{push} to origin/<branch> or update that remote-tracking branch\n>      -    after a push. The push can succeed, or report that everything is up to\n>      -    date, while status continues to compare against a stale tracking ref or\n>      -    cannot show the push branch at all.\n>      +    This can happen in fork workflows when the original remote is renamed\n>      +    to \"upstream\", the fork is added as \"origin\", and an existing\n>      +    pushRemote value still contains the fork URL. The URL still points to\n>      +    the right repository, so pushing works. However, @{push} is unavailable\n>      +    because Git does not connect the URL to \"origin\". As a result,\n>      +    \"git status\" cannot show the push branch, and an up-to-date push can\n>      +    leave its local tracking information stale.\n\nI'm a bit confused about the problem scenario here: if the pushRemote\nvalue contains a URL, then renaming a remote has nothing to do with\nit, right?\n\nAnd if the pushRemote value contains a remote name, then renaming the\nremote should propagate there as well, right? (At least, that's my\nrecollection of renaming; when I have used the GitHub CLI in the past\nit has worked pretty well in that case, but maybe they've changed\nthings recently?)\n\nI do think the URL<->remote matching for user display is a nice touch,\nso I'm not against the series! Just want to understand the problem\nstatement well. Maybe I should read over the test cases, or you could\nsuggest a \"how I hit this in the real world\" recipe? (Explicit\ncommands are easier for me than natural language in that case.)\n\n-- \nD. Ben Knoble\n"},{"id":"548726","messageId":"xmqqecgw726b.fsf@gitster.g","threadId":"66040","inReplyTo":"ff645b21591a4b365b30acaf67a295510889141c.1784538618.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 2/2] remote: resolve URL-valued push tracking remotes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-21T16:11:40Z","receivedAt":"2026-07-21T16:11:42Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> A branch may name its push destination with a URL instead of a\n> configured remote. This is useful in fork workflows, where the original\n> remote is renamed to \"upstream\", the fork is added as \"origin\", and an\n> existing branch.<name>.pushRemote continues to contain the fork URL.\n>\n> Git can still push through the anonymous remote created for that URL.\n> However, the anonymous remote has no fetch refspec. Git therefore cannot\n> resolve @{push} to origin/<branch> or update that remote-tracking branch\n> after a push. The push can succeed, or report that everything is up to\n> date, while status continues to compare against a stale tracking ref or\n> cannot show the push branch at all.\n>\n> A uniquely matching configured remote already provides the missing\n> mapping. Use its fetch refspec when resolving the push tracking branch\n> and when updating tracking refs after a push. This changes neither the\n> push destination nor configuration. Keep the existing behavior when no\n> remote matches or multiple remotes share the URL, since either case is\n> ambiguous.\n> ...\n> +struct remote *repo_remote_for_push_tracking(struct repository *repo,\n> +\t\t\t\t\t     struct remote *remote)\n> +{\n> +\tstruct remote *first_match = NULL;\n> +\tstruct remote_state *remote_state = repo->remote_state;\n> +\n> +\tif (remote->origin != REMOTE_UNCONFIGURED || remote->url.nr != 1)\n> +\t\treturn remote;\n\nI briefly wondered what should happen when a caller passes NULL as\nthe remote parameter to this function, but it turns out that no\ncaller passes NULL.  One caller is tracking_for_push_dest(),\nwhich is called from branch_get_push_1().  The latter refuses to\nproceed when !remote is true and does not call\ntracking_for_push_dest(), meaning it cannot pass NULL to this\nfunction.  The other caller is transport_push(), which passes\ntransport->remote.  This value comes from transport_get(), which\nensures transport->remote is not NULL before returning, so it\ncannot pass NULL to this function either.\n\nTherefore, it is OK to assume remote is not NULL, and let the\nprogram crash loudly if that assumption is violated.  Adding an\nexplicit BUG() check would be overkill here:\n\n    if (!repo || !remote)\n            BUG(\"...\");\n\n> +\tfor (int i = 0; i < remote_state->remotes_nr; i++) {\n> +\t\tstruct remote *candidate = remote_state->remotes[i];\n> +\n> +\t\tif (!candidate || candidate == remote ||\n> +\t\t    !remote_is_configured(candidate, 0) ||\n> +\t\t    !remote_has_url(candidate, remote->url.v[0]))\n> +\t\t\tcontinue;\n\nThis check, as well as the safety uniqueness check at the beginning\nof the function, only pays attention to the url member.  However, it\nshould also consider the pushurl member and, when it exists, ignore\nthe url member.  The upfront check would then look something like\nthis (please sanity check the details):\n\n\tconst char *check_url = NULL;\n\n\tif (remote->origin != REMOTE_UNCONFIGURED)\n\t\treturn remote;\n\n\tif (remote->pushurl.nr) {\n\t\tif (remote->pushurl.nr != 1)\n\t\t\treturn remote;\n\t\tcheck_url = remote->pushurl.v[0];\n\t} else if (remote->url.nr != 1) {\n\t\treturn remote;\n\t} else {\n\t\tcheck_url = remote->url.v[0];\n\t}\n\nThe test inside the loop would then use check_url:\n\n\t\t!remote_has_url(candidate, check_url)\n\ninstead of testing remote->url.v[0] directly.\n\nThanks.\n\n"},{"id":"548737","messageId":"CAHwyqnVu5vmJMcXFRXHG27MZtL=eoX6LjhYJ6_SZffhn3JqUWQ@mail.gmail.com","threadId":"66040","inReplyTo":"CALnO6CAY2x-adAxSXW1f_+OHjV_tVhLmkN7D+wE39rj3wc8LEQ@mail.gmail.com","subject":"Re: [PATCH v2 0/2] remote: renamed remote push tracking","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-07-21T20:10:21Z","receivedAt":"2026-07-21T20:10:59Z","isPatch":true,"body":"It's very valid and I need to explain it better if you are confused.\n\n\n\nHarald\n"},{"id":"548739","messageId":"pull.2358.v3.git.git.1784664859.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.v2.git.git.1784624306.gitgitgadget@gmail.com","subject":"[PATCH v3 0/2] remote: url-based pushRemote with renamed remotes","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-21T20:14:17Z","receivedAt":"2026-07-21T20:14:22Z","isPatch":true,"body":"Fix git status not showing the push branch after remotes are renamed, when\nbranch.<name>.pushRemote is a URL matching exactly one configured remote.\n\nChanges in v4:\n\n * Match against the actual push URL, including pushurl and pushInsteadOf.\n * Clarify how rearranging remotes exposes the git status tracking problem.\n * Simplify and correct the documentation for URL-valued pushRemote.\n\nChanges in v3:\n\n * Revamp commit messages to clarify motivation.\n\nChanges in v2:\n\n * Clarify that URL push destinations already work and that this change only\n   restores their tracking information.\n * Document URL values for branch.<name>.pushRemote and their @{push}\n   behavior.\n\nHarald Nordgren (2):\n  remote: pass repository to push tracking helper\n  remote: find tracking branches for URL push destinations\n\n Documentation/config/branch.adoc |   1 +\n Documentation/revisions.adoc     |   3 +\n remote.c                         |  43 +++++++++--\n remote.h                         |   2 +\n t/t5505-remote.sh                | 124 +++++++++++++++++++++++++++++++\n transport.c                      |   5 +-\n 6 files changed, 172 insertions(+), 6 deletions(-)\n\n\nbase-commit: 48bbf81c29ca9a4479ec7850fe206518682cdb2f\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2358%2FHaraldNordgren%2Fremote-resolve-url-push-tracking-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2358/HaraldNordgren/remote-resolve-url-push-tracking-v3\nPull-Request: https://github.com/git/git/pull/2358\n\nRange-diff vs v2:\n\n 1:  b1ac49de87 = 1:  b1ac49de87 remote: pass repository to push tracking helper\n 2:  6e924a7fec ! 2:  a343af9d50 remote: find tracking branches for URL push destinations\n     @@ Metadata\n       ## Commit message ##\n          remote: find tracking branches for URL push destinations\n      \n     -    Git already accepts a repository URL as branch.<name>.pushRemote and\n     -    can push to it. When a configured remote has the same URL, however,\n     -    \"git status\" cannot show that remote's push branch.\n     +    Git accepts a repository URL as branch.<name>.pushRemote and can push\n     +    to it. This branch setting takes precedence over remote.pushDefault.\n      \n     -    This can happen in fork workflows when the original remote is renamed\n     -    to \"upstream\", the fork is added as \"origin\", and an existing\n     -    pushRemote value still contains the fork URL. The URL still points to\n     -    the right repository, so pushing works. However, @{push} is unavailable\n     -    because Git does not connect the URL to \"origin\". As a result,\n     +    A branch can be configured with a URL-valued pushRemote before any push\n     +    occurs. If the remotes are later rearranged with \"git remote rename\" and\n     +    \"git remote add\", the newly added remote may use that URL. The URL value\n     +    is unaffected by the rename and continues to take precedence over\n     +    remote.pushDefault. The URL and the remote then point to the same\n     +    repository, but Git does not connect them for tracking. Pushing works,\n     +    but @{push} cannot identify the remote's tracking branch. As a result,\n          \"git status\" cannot show the push branch, and an up-to-date push can\n     -    leave its local tracking information stale.\n     +    leave its tracking information stale.\n      \n     -    When exactly one configured remote has the URL as one of its\n     -    remote.<name>.url values, use its fetch refspec to find and refresh the\n     -    push branch. Keep the URL as the push destination so the configured\n     -    remote's push settings do not change existing behavior. Keep the\n     -    current behavior when no remote matches or multiple remotes match.\n     +    When exactly one configured remote uses the push destination URL, use\n     +    that remote for push tracking. Continue to push to the URL so the\n     +    configured remote's push settings do not change existing behavior. Keep\n     +    the current behavior when no remote matches or multiple remotes match.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     @@ Documentation/config/branch.adoc: This option defaults to `never`.\n       \trepository), you would want to set `remote.pushDefault` to\n       \tspecify the remote to push to for all branches, and use this\n       \toption to override it for a specific branch.\n     -+\tThe value may be the name of a configured remote or a repository\n     -+\tURL. A URL is used directly as the push destination.\n     ++\tThe value may be the name of a configured remote or a repository URL.\n       \n       `branch.<name>.merge`::\n       \tDefines, together with `branch.<name>.remote`, the upstream branch\n     @@ Documentation/revisions.adoc: some output processing may assume ref names in UTF\n         `git push` were run while `branchname` was checked out (or the current\n         `HEAD` if no branchname is specified). Like for '@\\{upstream\\}', we report\n         the remote-tracking branch that corresponds to that branch at the remote.\n     -+  If the push destination is a URL and exactly one configured remote has the\n     -+  same `remote.<name>.url`, '@\\{push}' reports the remote-tracking branch for\n     -+  that remote.\n     ++  If the push destination is a URL and exactly one configured remote has\n     ++  that URL among its `remote.<name>.url` values, '@\\{push}' reports that\n     ++  remote's remote-tracking branch.\n       +\n       Here's an example to make it more clear:\n       +\n     @@ remote.c: const char *branch_get_upstream(struct branch *branch, struct strbuf *\n      +struct remote *repo_remote_for_push_tracking(struct repository *repo,\n      +\t\t\t\t\t     struct remote *remote)\n      +{\n     ++\tconst struct strvec *push_urls;\n      +\tstruct remote *first_match = NULL;\n      +\tstruct remote_state *remote_state = repo->remote_state;\n     ++\tconst char *check_url;\n      +\n     -+\tif (remote->origin != REMOTE_UNCONFIGURED || remote->url.nr != 1)\n     ++\tif (remote->origin != REMOTE_UNCONFIGURED)\n      +\t\treturn remote;\n      +\n     ++\tpush_urls = push_url_of_remote(remote);\n     ++\tif (push_urls->nr != 1)\n     ++\t\treturn remote;\n     ++\tcheck_url = push_urls->v[0];\n     ++\n      +\tfor (int i = 0; i < remote_state->remotes_nr; i++) {\n      +\t\tstruct remote *candidate = remote_state->remotes[i];\n      +\n      +\t\tif (!candidate || candidate == remote ||\n      +\t\t    !remote_is_configured(candidate, 0) ||\n     -+\t\t    !remote_has_url(candidate, remote->url.v[0]))\n     ++\t\t    !remote_has_url(candidate, check_url))\n      +\t\t\tcontinue;\n      +\t\tif (first_match)\n      +\t\t\treturn remote;\n     @@ t/t5505-remote.sh: setup_repository () {\n      +\trm -rf fork.git client &&\n      +\tgit clone --bare one fork.git &&\n      +\tgit clone one client &&\n     -+\tfork_url=\"$TRASH_DIRECTORY/fork.git\" &&\n     ++\tfork_url=\"file://$TRASH_DIRECTORY/fork.git\" &&\n      +\t(\n      +\t\tcd client &&\n      +\t\tgit checkout -b topic --track origin/main &&\n     @@ t/t5505-remote.sh: test_expect_success 'rename a remote renames repo remote.push\n      +\tEOF\n      +'\n      +\n     -+test_expect_success 'adding fork remote makes URL-valued pushRemote trackable' '\n     ++test_expect_success 'adding matching remote makes URL-valued pushRemote trackable' '\n      +\tsetup_url_pushremote &&\n      +\n      +\t(\n     @@ t/t5505-remote.sh: test_expect_success 'rename a remote renames repo remote.push\n      +\tEOF\n      +'\n      +\n     ++test_expect_success 'pushInsteadOf URL pushRemote is trackable' '\n     ++\tsetup_url_pushremote &&\n     ++\t(\n     ++\t\tcd client &&\n     ++\t\tgit remote rename origin upstream &&\n     ++\t\tgit remote add -f origin \"$fork_url\" &&\n     ++\t\tgit config \"url.$fork_url.pushInsteadOf\" fork: &&\n     ++\t\tgit config branch.topic.pushRemote fork:\n     ++\t) &&\n     ++\n     ++\tcheck_status <<-EOF\n     ++\tOn branch topic\n     ++\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n     ++\n     ++\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n     ++'\n     ++\n      +test_expect_success 'up-to-date URL push refreshes stale tracking branch' '\n      +\tsetup_url_pushremote &&\n      +\t(\n\n-- \ngitgitgadget\n"},{"id":"548740","messageId":"b1ac49de87c2a8b88aa2aed60b18847734fa906e.1784664859.git.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.v3.git.git.1784664859.gitgitgadget@gmail.com","subject":"[PATCH v3 1/2] remote: pass repository to push tracking helper","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-21T20:14:18Z","receivedAt":"2026-07-21T20:14:23Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nThe next commit needs tracking_for_push_dest() to inspect the\nrepository's configured remotes. Pass the repository through the\nexisting callers and mark the new parameter as unused.\n\nNo change in behavior.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 11 ++++++-----\n 1 file changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b17648d6ef..0dc36956c3 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1887,7 +1887,8 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n \treturn branch->merge[0]->dst;\n }\n \n-static char *tracking_for_push_dest(struct remote *remote,\n+static char *tracking_for_push_dest(struct repository *repo UNUSED,\n+\t\t\t\t    struct remote *remote,\n \t\t\t\t    const char *refname,\n \t\t\t\t    struct strbuf *err)\n {\n@@ -1925,13 +1926,13 @@ static char *branch_get_push_1(struct repository *repo,\n \t\t\t\t\t _(\"push refspecs for '%s' do not include '%s'\"),\n \t\t\t\t\t remote->name, branch->name);\n \n-\t\tret = tracking_for_push_dest(remote, dst, err);\n+\t\tret = tracking_for_push_dest(repo, remote, dst, err);\n \t\tfree(dst);\n \t\treturn ret;\n \t}\n \n \tif (remote->mirror)\n-\t\treturn tracking_for_push_dest(remote, branch->refname, err);\n+\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n \n \tswitch (push_default) {\n \tcase PUSH_DEFAULT_NOTHING:\n@@ -1939,7 +1940,7 @@ static char *branch_get_push_1(struct repository *repo,\n \n \tcase PUSH_DEFAULT_MATCHING:\n \tcase PUSH_DEFAULT_CURRENT:\n-\t\treturn tracking_for_push_dest(remote, branch->refname, err);\n+\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n \n \tcase PUSH_DEFAULT_UPSTREAM:\n \t\treturn xstrdup_or_null(branch_get_upstream(branch, err));\n@@ -1953,7 +1954,7 @@ static char *branch_get_push_1(struct repository *repo,\n \t\t\tup = branch_get_upstream(branch, err);\n \t\t\tif (!up)\n \t\t\t\treturn NULL;\n-\t\t\tcur = tracking_for_push_dest(remote, branch->refname, err);\n+\t\t\tcur = tracking_for_push_dest(repo, remote, branch->refname, err);\n \t\t\tif (!cur)\n \t\t\t\treturn NULL;\n \t\t\tif (strcmp(cur, up)) {\n-- \ngitgitgadget\n\n"},{"id":"548741","messageId":"a343af9d500a598826c5fe9a3abbe9df2f5916e8.1784664859.git.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.v3.git.git.1784664859.gitgitgadget@gmail.com","subject":"[PATCH v3 2/2] remote: find tracking branches for URL push destinations","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-21T20:14:19Z","receivedAt":"2026-07-21T20:14:24Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nGit accepts a repository URL as branch.<name>.pushRemote and can push\nto it. This branch setting takes precedence over remote.pushDefault.\n\nA branch can be configured with a URL-valued pushRemote before any push\noccurs. If the remotes are later rearranged with \"git remote rename\" and\n\"git remote add\", the newly added remote may use that URL. The URL value\nis unaffected by the rename and continues to take precedence over\nremote.pushDefault. The URL and the remote then point to the same\nrepository, but Git does not connect them for tracking. Pushing works,\nbut @{push} cannot identify the remote's tracking branch. As a result,\n\"git status\" cannot show the push branch, and an up-to-date push can\nleave its tracking information stale.\n\nWhen exactly one configured remote uses the push destination URL, use\nthat remote for push tracking. Continue to push to the URL so the\nconfigured remote's push settings do not change existing behavior. Keep\nthe current behavior when no remote matches or multiple remotes match.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/branch.adoc |   1 +\n Documentation/revisions.adoc     |   3 +\n remote.c                         |  34 ++++++++-\n remote.h                         |   2 +\n t/t5505-remote.sh                | 124 +++++++++++++++++++++++++++++++\n transport.c                      |   5 +-\n 6 files changed, 167 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc\nindex a4db9fa5c8..5a85fde8de 100644\n--- a/Documentation/config/branch.adoc\n+++ b/Documentation/config/branch.adoc\n@@ -55,6 +55,7 @@ This option defaults to `never`.\n \trepository), you would want to set `remote.pushDefault` to\n \tspecify the remote to push to for all branches, and use this\n \toption to override it for a specific branch.\n+\tThe value may be the name of a configured remote or a repository URL.\n \n `branch.<name>.merge`::\n \tDefines, together with `branch.<name>.remote`, the upstream branch\ndiff --git a/Documentation/revisions.adoc b/Documentation/revisions.adoc\nindex 6ea6c7cead..78f96fe8b0 100644\n--- a/Documentation/revisions.adoc\n+++ b/Documentation/revisions.adoc\n@@ -127,6 +127,9 @@ some output processing may assume ref names in UTF-8.\n   `git push` were run while `branchname` was checked out (or the current\n   `HEAD` if no branchname is specified). Like for '@\\{upstream\\}', we report\n   the remote-tracking branch that corresponds to that branch at the remote.\n+  If the push destination is a URL and exactly one configured remote has\n+  that URL among its `remote.<name>.url` values, '@\\{push}' reports that\n+  remote's remote-tracking branch.\n +\n Here's an example to make it more clear:\n +\ndiff --git a/remote.c b/remote.c\nindex 0dc36956c3..4a29669443 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1887,13 +1887,45 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n \treturn branch->merge[0]->dst;\n }\n \n-static char *tracking_for_push_dest(struct repository *repo UNUSED,\n+struct remote *repo_remote_for_push_tracking(struct repository *repo,\n+\t\t\t\t\t     struct remote *remote)\n+{\n+\tconst struct strvec *push_urls;\n+\tstruct remote *first_match = NULL;\n+\tstruct remote_state *remote_state = repo->remote_state;\n+\tconst char *check_url;\n+\n+\tif (remote->origin != REMOTE_UNCONFIGURED)\n+\t\treturn remote;\n+\n+\tpush_urls = push_url_of_remote(remote);\n+\tif (push_urls->nr != 1)\n+\t\treturn remote;\n+\tcheck_url = push_urls->v[0];\n+\n+\tfor (int i = 0; i < remote_state->remotes_nr; i++) {\n+\t\tstruct remote *candidate = remote_state->remotes[i];\n+\n+\t\tif (!candidate || candidate == remote ||\n+\t\t    !remote_is_configured(candidate, 0) ||\n+\t\t    !remote_has_url(candidate, check_url))\n+\t\t\tcontinue;\n+\t\tif (first_match)\n+\t\t\treturn remote;\n+\t\tfirst_match = candidate;\n+\t}\n+\n+\treturn first_match ? first_match : remote;\n+}\n+\n+static char *tracking_for_push_dest(struct repository *repo,\n \t\t\t\t    struct remote *remote,\n \t\t\t\t    const char *refname,\n \t\t\t\t    struct strbuf *err)\n {\n \tchar *ret;\n \n+\tremote = repo_remote_for_push_tracking(repo, remote);\n \tret = apply_refspecs(&remote->fetch, refname);\n \tif (!ret)\n \t\treturn error_buf(err,\ndiff --git a/remote.h b/remote.h\nindex 72a54d84ad..cca02033b9 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -345,6 +345,8 @@ char *remote_ref_for_branch(struct branch *branch, int for_push);\n \n const char *repo_default_remote(struct repository *repo);\n const char *repo_remote_from_url(struct repository *repo, const char *url);\n+struct remote *repo_remote_for_push_tracking(struct repository *repo,\n+\t\t\t\t\t     struct remote *remote);\n \n /* returns true if the given branch has merge configuration given. */\n int branch_has_merge_config(struct branch *branch);\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 6f5e86dede..983aff6552 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -24,6 +24,28 @@ setup_repository () {\n \t)\n }\n \n+setup_url_pushremote () {\n+\trm -rf fork.git client &&\n+\tgit clone --bare one fork.git &&\n+\tgit clone one client &&\n+\tfork_url=\"file://$TRASH_DIRECTORY/fork.git\" &&\n+\t(\n+\t\tcd client &&\n+\t\tgit checkout -b topic --track origin/main &&\n+\t\tgit commit --allow-empty -m topic-change &&\n+\t\tgit config push.default current &&\n+\t\tgit config status.compareBranches \"@{upstream} @{push}\" &&\n+\t\tgit config branch.topic.pushRemote \"$fork_url\" &&\n+\t\tgit push\n+\t)\n+}\n+\n+check_status () {\n+\tgit -C client status >actual &&\n+\tcat >expected &&\n+\ttest_cmp expected actual\n+}\n+\n tokens_match () {\n \techo \"$1\" | tr ' ' '\\012' | sort | sed -e '/^$/d' >expect &&\n \techo \"$2\" | tr ' ' '\\012' | sort | sed -e '/^$/d' >actual &&\n@@ -1018,6 +1040,108 @@ test_expect_success 'rename a remote renames repo remote.pushDefault but keeps g\n \t)\n '\n \n+test_expect_success 'URL-valued pushRemote without matching remote is not trackable' '\n+\tsetup_url_pushremote &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'adding matching remote makes URL-valued pushRemote trackable' '\n+\tsetup_url_pushremote &&\n+\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\"\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'pushInsteadOf URL pushRemote is trackable' '\n+\tsetup_url_pushremote &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\" &&\n+\t\tgit config \"url.$fork_url.pushInsteadOf\" fork: &&\n+\t\tgit config branch.topic.pushRemote fork:\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'up-to-date URL push refreshes stale tracking branch' '\n+\tsetup_url_pushremote &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\" &&\n+\t\tgit commit --allow-empty -m another-topic-change &&\n+\t\tgit -C ../fork.git fetch ../client topic:topic\n+\t) &&\n+\n+\tcheck_status <<-EOF &&\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.\n+\n+\tYour branch is ahead of ${SQ}origin/topic${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\n+\tgit -C client push >actual 2>&1 &&\n+\ttest_grep \"Everything up-to-date\" actual &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'duplicate remote URL leaves URL-valued pushRemote ambiguous' '\n+\tsetup_url_pushremote &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\" &&\n+\t\tgit remote add duplicate \"$fork_url\"\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n test_expect_success 'rename handles remote without fetch refspec' '\n \tgit clone --bare one no-refspec.git &&\n \t# confirm assumption that bare clone does not create refspec\ndiff --git a/transport.c b/transport.c\nindex fc144f0aed..30a4ab2cd5 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1553,8 +1553,11 @@ int transport_push(struct repository *r,\n \tif (!(flags & (TRANSPORT_PUSH_DRY_RUN |\n \t\t       TRANSPORT_RECURSE_SUBMODULES_ONLY))) {\n \t\tstruct ref *ref;\n+\t\tstruct remote *tracking_remote = repo_remote_for_push_tracking(\n+\t\t\tr, transport->remote);\n+\n \t\tfor (ref = remote_refs; ref; ref = ref->next)\n-\t\t\ttransport_update_tracking_ref(transport->remote, ref, verbose);\n+\t\t\ttransport_update_tracking_ref(tracking_remote, ref, verbose);\n \t}\n \n \tif (porcelain && !push_ret)\n-- \ngitgitgadget\n"},{"id":"548747","messageId":"xmqqqzkwt2fg.fsf@gitster.g","threadId":"66040","inReplyTo":"a343af9d500a598826c5fe9a3abbe9df2f5916e8.1784664859.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 2/2] remote: find tracking branches for URL push destinations","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-21T22:15:15Z","receivedAt":"2026-07-21T22:15:18Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> Git accepts a repository URL as branch.<name>.pushRemote and can push\n> to it. This branch setting takes precedence over remote.pushDefault.\n>\n> A branch can be configured with a URL-valued pushRemote before any push\n> occurs. If the remotes are later rearranged with \"git remote rename\" and\n> \"git remote add\", the newly added remote may use that URL. The URL value\n> is unaffected by the rename and continues to take precedence over\n> remote.pushDefault. The URL and the remote then point to the same\n> repository, but Git does not connect them for tracking. Pushing works,\n> but @{push} cannot identify the remote's tracking branch. As a result,\n> \"git status\" cannot show the push branch, and an up-to-date push can\n> leave its tracking information stale.\n>\n> When exactly one configured remote uses the push destination URL, use\n> that remote for push tracking. Continue to push to the URL so the\n> configured remote's push settings do not change existing behavior. Keep\n> the current behavior when no remote matches or multiple remotes match.\n>\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n>  Documentation/config/branch.adoc |   1 +\n>  Documentation/revisions.adoc     |   3 +\n>  remote.c                         |  34 ++++++++-\n>  remote.h                         |   2 +\n>  t/t5505-remote.sh                | 124 +++++++++++++++++++++++++++++++\n>  transport.c                      |   5 +-\n>  6 files changed, 167 insertions(+), 2 deletions(-)\n\nHmph, the changes since the previous round look a bit incoherent.\n\n> diff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc\n> index a4db9fa5c8..5a85fde8de 100644\n> --- a/Documentation/config/branch.adoc\n> +++ b/Documentation/config/branch.adoc\n> @@ -55,6 +55,7 @@ This option defaults to `never`.\n>  \trepository), you would want to set `remote.pushDefault` to\n>  \tspecify the remote to push to for all branches, and use this\n>  \toption to override it for a specific branch.\n> +\tThe value may be the name of a configured remote or a repository URL.\n>  \n>  `branch.<name>.merge`::\n>  \tDefines, together with `branch.<name>.remote`, the upstream branch\n> diff --git a/Documentation/revisions.adoc b/Documentation/revisions.adoc\n> index 6ea6c7cead..78f96fe8b0 100644\n> --- a/Documentation/revisions.adoc\n> +++ b/Documentation/revisions.adoc\n> @@ -127,6 +127,9 @@ some output processing may assume ref names in UTF-8.\n>    `git push` were run while `branchname` was checked out (or the current\n>    `HEAD` if no branchname is specified). Like for '@\\{upstream\\}', we report\n>    the remote-tracking branch that corresponds to that branch at the remote.\n> +  If the push destination is a URL and exactly one configured remote has\n> +  that URL among its `remote.<name>.url` values, '@\\{push}' reports that\n> +  remote's remote-tracking branch.\n\nWe claim we use remote.<name>.url here.\n\n>  Here's an example to make it more clear:\n>  +\n> diff --git a/remote.c b/remote.c\n> index 0dc36956c3..4a29669443 100644\n> --- a/remote.c\n> +++ b/remote.c\n> @@ -1887,13 +1887,45 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n>  \treturn branch->merge[0]->dst;\n>  }\n>  \n> -static char *tracking_for_push_dest(struct repository *repo UNUSED,\n> +struct remote *repo_remote_for_push_tracking(struct repository *repo,\n> +\t\t\t\t\t     struct remote *remote)\n> +{\n> +\tconst struct strvec *push_urls;\n> +\tstruct remote *first_match = NULL;\n> +\tstruct remote_state *remote_state = repo->remote_state;\n> +\tconst char *check_url;\n> +\n> +\tif (remote->origin != REMOTE_UNCONFIGURED)\n> +\t\treturn remote;\n> +\n> +\tpush_urls = push_url_of_remote(remote);\n> +\tif (push_urls->nr != 1)\n> +\t\treturn remote;\n> +\tcheck_url = push_urls->v[0];\n\nBut we correctly pay attention to both .url and .pushurl, giving\nprecedence to the latter.\n\n> +\tfor (int i = 0; i < remote_state->remotes_nr; i++) {\n> +\t\tstruct remote *candidate = remote_state->remotes[i];\n> +\n> +\t\tif (!candidate || candidate == remote ||\n> +\t\t    !remote_is_configured(candidate, 0) ||\n> +\t\t    !remote_has_url(candidate, check_url))\n> +\t\t\tcontinue;\n> +\t\tif (first_match)\n> +\t\t\treturn remote;\n> +\t\tfirst_match = candidate;\n> +\t}\n> +\n> +\treturn first_match ? first_match : remote;\n> +}\n\n> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\n> index 6f5e86dede..983aff6552 100755\n> --- a/t/t5505-remote.sh\n> +++ b/t/t5505-remote.sh\n> @@ -24,6 +24,28 @@ setup_repository () {\n>  \t)\n>  }\n>  \n> +setup_url_pushremote () {\n> +\trm -rf fork.git client &&\n> +\tgit clone --bare one fork.git &&\n> +\tgit clone one client &&\n> +\tfork_url=\"file://$TRASH_DIRECTORY/fork.git\" &&\n> +\t(\n> +\t\tcd client &&\n> +\t\tgit checkout -b topic --track origin/main &&\n> +\t\tgit commit --allow-empty -m topic-change &&\n> +\t\tgit config push.default current &&\n> +\t\tgit config status.compareBranches \"@{upstream} @{push}\" &&\n> +\t\tgit config branch.topic.pushRemote \"$fork_url\" &&\n> +\t\tgit push\n> +\t)\n> +}\n> +\n> +check_status () {\n> +\tgit -C client status >actual &&\n> +\tcat >expected &&\n> +\ttest_cmp expected actual\n> +}\n> +\n>  tokens_match () {\n>  \techo \"$1\" | tr ' ' '\\012' | sort | sed -e '/^$/d' >expect &&\n>  \techo \"$2\" | tr ' ' '\\012' | sort | sed -e '/^$/d' >actual &&\n> @@ -1018,6 +1040,108 @@ test_expect_success 'rename a remote renames repo remote.pushDefault but keeps g\n>  \t)\n>  '\n>  \n> +test_expect_success 'URL-valued pushRemote without matching remote is not trackable' '\n> +\tsetup_url_pushremote &&\n> +\n> +\tcheck_status <<-EOF\n> +\tOn branch topic\n> +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n> +\t  (use \"git push\" to publish your local commits)\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +'\n> +\n> +test_expect_success 'adding matching remote makes URL-valued pushRemote trackable' '\n> +\tsetup_url_pushremote &&\n> +\n> +\t(\n> +\t\tcd client &&\n> +\t\tgit remote rename origin upstream &&\n> +\t\tgit remote add -f origin \"$fork_url\"\n> +\t) &&\n> +\n> +\tcheck_status <<-EOF\n> +\tOn branch topic\n> +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n> +\n> +\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +'\n\nBut the test does not seem to exercise remote.<name>.pushURL\nanywhere.\n\n> +test_expect_success 'pushInsteadOf URL pushRemote is trackable' '\n> +\tsetup_url_pushremote &&\n> +\t(\n> +\t\tcd client &&\n> +\t\tgit remote rename origin upstream &&\n> +\t\tgit remote add -f origin \"$fork_url\" &&\n> +\t\tgit config \"url.$fork_url.pushInsteadOf\" fork: &&\n> +\t\tgit config branch.topic.pushRemote fork:\n> +\t) &&\n\nTesting insteadof is a nice touch, though.\n\n> +test_expect_success 'duplicate remote URL leaves URL-valued pushRemote ambiguous' '\n> +\tsetup_url_pushremote &&\n> +\t(\n> +\t\tcd client &&\n> +\t\tgit remote rename origin upstream &&\n> +\t\tgit remote add -f origin \"$fork_url\" &&\n> +\t\tgit remote add duplicate \"$fork_url\"\n> +\t) &&\n> +\n> +\tcheck_status <<-EOF\n> +\tOn branch topic\n> +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n> +\t  (use \"git push\" to publish your local commits)\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +'\n\nSo is a test that checks non-unique case where the machinery should\nnot kick in.\n\nThanks.\n"},{"id":"548771","messageId":"3FE12592-B663-438C-B33E-A251AE08E146@gmail.com","threadId":"66040","inReplyTo":"pull.2358.v3.git.git.1784664859.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 0/2] remote: url-based pushRemote with renamed remotes","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-07-22T10:35:16Z","receivedAt":"2026-07-22T10:35:28Z","isPatch":true,"body":"\n> Le 21 juil. 2026 à 16:14, Harald Nordgren via GitGitGadget <gitgitgadget@gmail.com> a écrit :\n> \n> ﻿Fix git status not showing the push branch after remotes are renamed, when\n> branch.<name>.pushRemote is a URL matching exactly one configured remote.\n> \n> Changes in v4:\n> \n> * Match against the actual push URL, including pushurl and pushInsteadOf.\n> * Clarify how rearranging remotes exposes the git status tracking problem.\n> * Simplify and correct the documentation for URL-valued pushRemote.\n> \n> Changes in v3:\n> \n> * Revamp commit messages to clarify motivation.\n> \n> Changes in v2:\n> \n> * Clarify that URL push destinations already work and that this change only\n>   restores their tracking information.\n> * Document URL values for branch.<name>.pushRemote and their @{push}\n>   behavior.\n> \n> Harald Nordgren (2):\n>  remote: pass repository to push tracking helper\n>  remote: find tracking branches for URL push destinations\n> \n> Documentation/config/branch.adoc |   1 +\n> Documentation/revisions.adoc     |   3 +\n> remote.c                         |  43 +++++++++--\n> remote.h                         |   2 +\n> t/t5505-remote.sh                | 124 +++++++++++++++++++++++++++++++\n> transport.c                      |   5 +-\n> 6 files changed, 172 insertions(+), 6 deletions(-)\n> \n> \n> base-commit: 48bbf81c29ca9a4479ec7850fe206518682cdb2f\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2358%2FHaraldNordgren%2Fremote-resolve-url-push-tracking-v3\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2358/HaraldNordgren/remote-resolve-url-push-tracking-v3\n> Pull-Request: https://github.com/git/git/pull/2358\n> \n> Range-diff vs v2:\n> \n> 1:  b1ac49de87 = 1:  b1ac49de87 remote: pass repository to push tracking helper\n> 2:  6e924a7fec ! 2:  a343af9d50 remote: find tracking branches for URL push destinations\n>     @@ Metadata\n>       ## Commit message ##\n>          remote: find tracking branches for URL push destinations\n> \n>     -    Git already accepts a repository URL as branch.<name>.pushRemote and\n>     -    can push to it. When a configured remote has the same URL, however,\n>     -    \"git status\" cannot show that remote's push branch.\n>     +    Git accepts a repository URL as branch.<name>.pushRemote and can push\n>     +    to it. This branch setting takes precedence over remote.pushDefault.\n> \n>     -    This can happen in fork workflows when the original remote is renamed\n>     -    to \"upstream\", the fork is added as \"origin\", and an existing\n>     -    pushRemote value still contains the fork URL. The URL still points to\n>     -    the right repository, so pushing works. However, @{push} is unavailable\n>     -    because Git does not connect the URL to \"origin\". As a result,\n>     +    A branch can be configured with a URL-valued pushRemote before any push\n>     +    occurs. If the remotes are later rearranged with \"git remote rename\" and\n>     +    \"git remote add\", the newly added remote may use that URL. The URL value\n>     +    is unaffected by the rename and continues to take precedence over\n>     +    remote.pushDefault. The URL and the remote then point to the same\n>     +    repository, but Git does not connect them for tracking. Pushing works,\n>     +    but @{push} cannot identify the remote's tracking branch. As a result,\n>          \"git status\" cannot show the push branch, and an up-to-date push can\n>     -    leave its local tracking information stale.\n>     +    leave its tracking information stale.\n> \n>     -    When exactly one configured remote has the URL as one of its\n>     -    remote.<name>.url values, use its fetch refspec to find and refresh the\n>     -    push branch. Keep the URL as the push destination so the configured\n>     -    remote's push settings do not change existing behavior. Keep the\n>     -    current behavior when no remote matches or multiple remotes match.\n>     +    When exactly one configured remote uses the push destination URL, use\n>     +    that remote for push tracking. Continue to push to the URL so the\n>     +    configured remote's push settings do not change existing behavior. Keep\n>     +    the current behavior when no remote matches or multiple remotes match.\n> \n>          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n\nI find this explanation makes more sense ; it’s not how I use remote renames, since I don’t usually use URL-valued remotes, but I can see how things arise now. Thanks!"},{"id":"548789","messageId":"pull.2358.v4.git.git.1784743738.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.v3.git.git.1784664859.gitgitgadget@gmail.com","subject":"[PATCH v4 0/2] remote: url-based pushRemote with renamed remotes","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-22T18:08:56Z","receivedAt":"2026-07-22T18:09:04Z","isPatch":true,"body":"Fix git status not showing the push branch after remotes are renamed, when\nbranch.<name>.pushRemote is a URL matching exactly one configured remote.\n\nChanges in v4:\n\n * Match configured remotes by effective push URL, preferring pushurl over\n   url.\n * Update the documentation and rationale to describe where the remote would\n   push.\n\nChanges in v3:\n\n * Revamp commit messages to clarify motivation.\n\nChanges in v2:\n\n * Clarify that URL push destinations already work and that this change only\n   restores their tracking information.\n * Document URL values for branch.<name>.pushRemote and their @{push}\n   behavior.\n\nHarald Nordgren (2):\n  remote: pass repository to push tracking helper\n  remote: find tracking branches for URL push destinations\n\n Documentation/config/branch.adoc |   1 +\n Documentation/revisions.adoc     |   3 +\n remote.c                         |  54 ++++++++++--\n remote.h                         |   2 +\n t/t5505-remote.sh                | 144 +++++++++++++++++++++++++++++++\n transport.c                      |   5 +-\n 6 files changed, 203 insertions(+), 6 deletions(-)\n\n\nbase-commit: 5d2e7709234afea1b6ddb25cd4f60d3d5fb3c200\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2358%2FHaraldNordgren%2Fremote-resolve-url-push-tracking-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2358/HaraldNordgren/remote-resolve-url-push-tracking-v4\nPull-Request: https://github.com/git/git/pull/2358\n\nRange-diff vs v3:\n\n 1:  b1ac49de87 = 1:  494287bade remote: pass repository to push tracking helper\n 2:  a343af9d50 ! 2:  08c432a2d4 remote: find tracking branches for URL push destinations\n     @@ Commit message\n          \"git status\" cannot show the push branch, and an up-to-date push can\n          leave its tracking information stale.\n      \n     -    When exactly one configured remote uses the push destination URL, use\n     -    that remote for push tracking. Continue to push to the URL so the\n     -    configured remote's push settings do not change existing behavior. Keep\n     -    the current behavior when no remote matches or multiple remotes match.\n     +    When exactly one configured remote would push to the same URL, use that\n     +    remote for push tracking. Continue to push to the URL so the configured\n     +    remote's push settings do not change existing behavior. Keep the current\n     +    behavior when no remote matches or multiple remotes match.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     @@ Documentation/revisions.adoc: some output processing may assume ref names in UTF\n         `git push` were run while `branchname` was checked out (or the current\n         `HEAD` if no branchname is specified). Like for '@\\{upstream\\}', we report\n         the remote-tracking branch that corresponds to that branch at the remote.\n     -+  If the push destination is a URL and exactly one configured remote has\n     -+  that URL among its `remote.<name>.url` values, '@\\{push}' reports that\n     -+  remote's remote-tracking branch.\n     ++  If the push destination is a URL and exactly one configured remote uses\n     ++  that URL for pushing, '@\\{push}' reports that remote's remote-tracking\n     ++  branch.\n       +\n       Here's an example to make it more clear:\n       +\n      \n       ## remote.c ##\n     +@@ remote.c: struct strvec *push_url_of_remote(struct remote *remote)\n     + \treturn remote->pushurl.nr ? &remote->pushurl : &remote->url;\n     + }\n     + \n     ++static bool remote_has_push_url(struct remote *remote, const char *url)\n     ++{\n     ++\tconst struct strvec *push_urls = push_url_of_remote(remote);\n     ++\n     ++\tfor (size_t i = 0; i < push_urls->nr; i++) {\n     ++\t\tif (!strcmp(push_urls->v[i], url))\n     ++\t\t\treturn true;\n     ++\t}\n     ++\treturn false;\n     ++}\n     ++\n     + void ref_push_report_free(struct ref_push_report *report)\n     + {\n     + \twhile (report) {\n      @@ remote.c: const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n       \treturn branch->merge[0]->dst;\n       }\n     @@ remote.c: const char *branch_get_upstream(struct branch *branch, struct strbuf *\n      +\n      +\t\tif (!candidate || candidate == remote ||\n      +\t\t    !remote_is_configured(candidate, 0) ||\n     -+\t\t    !remote_has_url(candidate, check_url))\n     ++\t\t    !remote_has_push_url(candidate, check_url))\n      +\t\t\tcontinue;\n      +\t\tif (first_match)\n      +\t\t\treturn remote;\n     @@ t/t5505-remote.sh: test_expect_success 'rename a remote renames repo remote.push\n      +\tEOF\n      +'\n      +\n     ++test_expect_success 'configured pushurl makes URL-valued pushRemote trackable' '\n     ++\tsetup_url_pushremote &&\n     ++\n     ++\t(\n     ++\t\tcd client &&\n     ++\t\tgit remote rename origin upstream &&\n     ++\t\tgit remote add -f origin ../fork.git &&\n     ++\t\tgit remote set-url --push origin \"$fork_url\"\n     ++\t) &&\n     ++\n     ++\tcheck_status <<-EOF\n     ++\tOn branch topic\n     ++\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n     ++\n     ++\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n     ++'\n     ++\n      +test_expect_success 'pushInsteadOf URL pushRemote is trackable' '\n      +\tsetup_url_pushremote &&\n      +\t(\n\n-- \ngitgitgadget\n"},{"id":"548790","messageId":"494287bade1d4072ccbe8374028ca0c7cece2a80.1784743738.git.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.v4.git.git.1784743738.gitgitgadget@gmail.com","subject":"[PATCH v4 1/2] remote: pass repository to push tracking helper","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-22T18:08:57Z","receivedAt":"2026-07-22T18:09:06Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nThe next commit needs tracking_for_push_dest() to inspect the\nrepository's configured remotes. Pass the repository through the\nexisting callers and mark the new parameter as unused.\n\nNo change in behavior.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 11 ++++++-----\n 1 file changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b17648d6ef..0dc36956c3 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1887,7 +1887,8 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n \treturn branch->merge[0]->dst;\n }\n \n-static char *tracking_for_push_dest(struct remote *remote,\n+static char *tracking_for_push_dest(struct repository *repo UNUSED,\n+\t\t\t\t    struct remote *remote,\n \t\t\t\t    const char *refname,\n \t\t\t\t    struct strbuf *err)\n {\n@@ -1925,13 +1926,13 @@ static char *branch_get_push_1(struct repository *repo,\n \t\t\t\t\t _(\"push refspecs for '%s' do not include '%s'\"),\n \t\t\t\t\t remote->name, branch->name);\n \n-\t\tret = tracking_for_push_dest(remote, dst, err);\n+\t\tret = tracking_for_push_dest(repo, remote, dst, err);\n \t\tfree(dst);\n \t\treturn ret;\n \t}\n \n \tif (remote->mirror)\n-\t\treturn tracking_for_push_dest(remote, branch->refname, err);\n+\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n \n \tswitch (push_default) {\n \tcase PUSH_DEFAULT_NOTHING:\n@@ -1939,7 +1940,7 @@ static char *branch_get_push_1(struct repository *repo,\n \n \tcase PUSH_DEFAULT_MATCHING:\n \tcase PUSH_DEFAULT_CURRENT:\n-\t\treturn tracking_for_push_dest(remote, branch->refname, err);\n+\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n \n \tcase PUSH_DEFAULT_UPSTREAM:\n \t\treturn xstrdup_or_null(branch_get_upstream(branch, err));\n@@ -1953,7 +1954,7 @@ static char *branch_get_push_1(struct repository *repo,\n \t\t\tup = branch_get_upstream(branch, err);\n \t\t\tif (!up)\n \t\t\t\treturn NULL;\n-\t\t\tcur = tracking_for_push_dest(remote, branch->refname, err);\n+\t\t\tcur = tracking_for_push_dest(repo, remote, branch->refname, err);\n \t\t\tif (!cur)\n \t\t\t\treturn NULL;\n \t\t\tif (strcmp(cur, up)) {\n-- \ngitgitgadget\n\n"},{"id":"548791","messageId":"08c432a2d4f52c202a2bebaa72330a17e94aedd1.1784743738.git.gitgitgadget@gmail.com","threadId":"66040","inReplyTo":"pull.2358.v4.git.git.1784743738.gitgitgadget@gmail.com","subject":"[PATCH v4 2/2] remote: find tracking branches for URL push destinations","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-22T18:08:58Z","receivedAt":"2026-07-22T18:09:07Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nGit accepts a repository URL as branch.<name>.pushRemote and can push\nto it. This branch setting takes precedence over remote.pushDefault.\n\nA branch can be configured with a URL-valued pushRemote before any push\noccurs. If the remotes are later rearranged with \"git remote rename\" and\n\"git remote add\", the newly added remote may use that URL. The URL value\nis unaffected by the rename and continues to take precedence over\nremote.pushDefault. The URL and the remote then point to the same\nrepository, but Git does not connect them for tracking. Pushing works,\nbut @{push} cannot identify the remote's tracking branch. As a result,\n\"git status\" cannot show the push branch, and an up-to-date push can\nleave its tracking information stale.\n\nWhen exactly one configured remote would push to the same URL, use that\nremote for push tracking. Continue to push to the URL so the configured\nremote's push settings do not change existing behavior. Keep the current\nbehavior when no remote matches or multiple remotes match.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/branch.adoc |   1 +\n Documentation/revisions.adoc     |   3 +\n remote.c                         |  45 +++++++++-\n remote.h                         |   2 +\n t/t5505-remote.sh                | 144 +++++++++++++++++++++++++++++++\n transport.c                      |   5 +-\n 6 files changed, 198 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc\nindex a4db9fa5c8..5a85fde8de 100644\n--- a/Documentation/config/branch.adoc\n+++ b/Documentation/config/branch.adoc\n@@ -55,6 +55,7 @@ This option defaults to `never`.\n \trepository), you would want to set `remote.pushDefault` to\n \tspecify the remote to push to for all branches, and use this\n \toption to override it for a specific branch.\n+\tThe value may be the name of a configured remote or a repository URL.\n \n `branch.<name>.merge`::\n \tDefines, together with `branch.<name>.remote`, the upstream branch\ndiff --git a/Documentation/revisions.adoc b/Documentation/revisions.adoc\nindex 6ea6c7cead..3fbfbd3d5f 100644\n--- a/Documentation/revisions.adoc\n+++ b/Documentation/revisions.adoc\n@@ -127,6 +127,9 @@ some output processing may assume ref names in UTF-8.\n   `git push` were run while `branchname` was checked out (or the current\n   `HEAD` if no branchname is specified). Like for '@\\{upstream\\}', we report\n   the remote-tracking branch that corresponds to that branch at the remote.\n+  If the push destination is a URL and exactly one configured remote uses\n+  that URL for pushing, '@\\{push}' reports that remote's remote-tracking\n+  branch.\n +\n Here's an example to make it more clear:\n +\ndiff --git a/remote.c b/remote.c\nindex 0dc36956c3..3a6abf1258 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -954,6 +954,17 @@ struct strvec *push_url_of_remote(struct remote *remote)\n \treturn remote->pushurl.nr ? &remote->pushurl : &remote->url;\n }\n \n+static bool remote_has_push_url(struct remote *remote, const char *url)\n+{\n+\tconst struct strvec *push_urls = push_url_of_remote(remote);\n+\n+\tfor (size_t i = 0; i < push_urls->nr; i++) {\n+\t\tif (!strcmp(push_urls->v[i], url))\n+\t\t\treturn true;\n+\t}\n+\treturn false;\n+}\n+\n void ref_push_report_free(struct ref_push_report *report)\n {\n \twhile (report) {\n@@ -1887,13 +1898,45 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n \treturn branch->merge[0]->dst;\n }\n \n-static char *tracking_for_push_dest(struct repository *repo UNUSED,\n+struct remote *repo_remote_for_push_tracking(struct repository *repo,\n+\t\t\t\t\t     struct remote *remote)\n+{\n+\tconst struct strvec *push_urls;\n+\tstruct remote *first_match = NULL;\n+\tstruct remote_state *remote_state = repo->remote_state;\n+\tconst char *check_url;\n+\n+\tif (remote->origin != REMOTE_UNCONFIGURED)\n+\t\treturn remote;\n+\n+\tpush_urls = push_url_of_remote(remote);\n+\tif (push_urls->nr != 1)\n+\t\treturn remote;\n+\tcheck_url = push_urls->v[0];\n+\n+\tfor (int i = 0; i < remote_state->remotes_nr; i++) {\n+\t\tstruct remote *candidate = remote_state->remotes[i];\n+\n+\t\tif (!candidate || candidate == remote ||\n+\t\t    !remote_is_configured(candidate, 0) ||\n+\t\t    !remote_has_push_url(candidate, check_url))\n+\t\t\tcontinue;\n+\t\tif (first_match)\n+\t\t\treturn remote;\n+\t\tfirst_match = candidate;\n+\t}\n+\n+\treturn first_match ? first_match : remote;\n+}\n+\n+static char *tracking_for_push_dest(struct repository *repo,\n \t\t\t\t    struct remote *remote,\n \t\t\t\t    const char *refname,\n \t\t\t\t    struct strbuf *err)\n {\n \tchar *ret;\n \n+\tremote = repo_remote_for_push_tracking(repo, remote);\n \tret = apply_refspecs(&remote->fetch, refname);\n \tif (!ret)\n \t\treturn error_buf(err,\ndiff --git a/remote.h b/remote.h\nindex 72a54d84ad..cca02033b9 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -345,6 +345,8 @@ char *remote_ref_for_branch(struct branch *branch, int for_push);\n \n const char *repo_default_remote(struct repository *repo);\n const char *repo_remote_from_url(struct repository *repo, const char *url);\n+struct remote *repo_remote_for_push_tracking(struct repository *repo,\n+\t\t\t\t\t     struct remote *remote);\n \n /* returns true if the given branch has merge configuration given. */\n int branch_has_merge_config(struct branch *branch);\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 6f5e86dede..9c2f140d5a 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -24,6 +24,28 @@ setup_repository () {\n \t)\n }\n \n+setup_url_pushremote () {\n+\trm -rf fork.git client &&\n+\tgit clone --bare one fork.git &&\n+\tgit clone one client &&\n+\tfork_url=\"file://$TRASH_DIRECTORY/fork.git\" &&\n+\t(\n+\t\tcd client &&\n+\t\tgit checkout -b topic --track origin/main &&\n+\t\tgit commit --allow-empty -m topic-change &&\n+\t\tgit config push.default current &&\n+\t\tgit config status.compareBranches \"@{upstream} @{push}\" &&\n+\t\tgit config branch.topic.pushRemote \"$fork_url\" &&\n+\t\tgit push\n+\t)\n+}\n+\n+check_status () {\n+\tgit -C client status >actual &&\n+\tcat >expected &&\n+\ttest_cmp expected actual\n+}\n+\n tokens_match () {\n \techo \"$1\" | tr ' ' '\\012' | sort | sed -e '/^$/d' >expect &&\n \techo \"$2\" | tr ' ' '\\012' | sort | sed -e '/^$/d' >actual &&\n@@ -1018,6 +1040,128 @@ test_expect_success 'rename a remote renames repo remote.pushDefault but keeps g\n \t)\n '\n \n+test_expect_success 'URL-valued pushRemote without matching remote is not trackable' '\n+\tsetup_url_pushremote &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'adding matching remote makes URL-valued pushRemote trackable' '\n+\tsetup_url_pushremote &&\n+\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\"\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'configured pushurl makes URL-valued pushRemote trackable' '\n+\tsetup_url_pushremote &&\n+\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin ../fork.git &&\n+\t\tgit remote set-url --push origin \"$fork_url\"\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'pushInsteadOf URL pushRemote is trackable' '\n+\tsetup_url_pushremote &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\" &&\n+\t\tgit config \"url.$fork_url.pushInsteadOf\" fork: &&\n+\t\tgit config branch.topic.pushRemote fork:\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'up-to-date URL push refreshes stale tracking branch' '\n+\tsetup_url_pushremote &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\" &&\n+\t\tgit commit --allow-empty -m another-topic-change &&\n+\t\tgit -C ../fork.git fetch ../client topic:topic\n+\t) &&\n+\n+\tcheck_status <<-EOF &&\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.\n+\n+\tYour branch is ahead of ${SQ}origin/topic${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\n+\tgit -C client push >actual 2>&1 &&\n+\ttest_grep \"Everything up-to-date\" actual &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 2 commits.\n+\n+\tYour branch is up to date with ${SQ}origin/topic${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'duplicate remote URL leaves URL-valued pushRemote ambiguous' '\n+\tsetup_url_pushremote &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote rename origin upstream &&\n+\t\tgit remote add -f origin \"$fork_url\" &&\n+\t\tgit remote add duplicate \"$fork_url\"\n+\t) &&\n+\n+\tcheck_status <<-EOF\n+\tOn branch topic\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n test_expect_success 'rename handles remote without fetch refspec' '\n \tgit clone --bare one no-refspec.git &&\n \t# confirm assumption that bare clone does not create refspec\ndiff --git a/transport.c b/transport.c\nindex fc144f0aed..30a4ab2cd5 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1553,8 +1553,11 @@ int transport_push(struct repository *r,\n \tif (!(flags & (TRANSPORT_PUSH_DRY_RUN |\n \t\t       TRANSPORT_RECURSE_SUBMODULES_ONLY))) {\n \t\tstruct ref *ref;\n+\t\tstruct remote *tracking_remote = repo_remote_for_push_tracking(\n+\t\t\tr, transport->remote);\n+\n \t\tfor (ref = remote_refs; ref; ref = ref->next)\n-\t\t\ttransport_update_tracking_ref(transport->remote, ref, verbose);\n+\t\t\ttransport_update_tracking_ref(tracking_remote, ref, verbose);\n \t}\n \n \tif (porcelain && !push_ret)\n-- \ngitgitgadget\n"},{"id":"548794","messageId":"xmqqpl0eoniz.fsf@gitster.g","threadId":"66040","inReplyTo":"08c432a2d4f52c202a2bebaa72330a17e94aedd1.1784743738.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 2/2] remote: find tracking branches for URL push destinations","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-22T19:03:00Z","receivedAt":"2026-07-22T19:03:03Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> +static bool remote_has_push_url(struct remote *remote, const char *url)\n> +{\n> +\tconst struct strvec *push_urls = push_url_of_remote(remote);\n> +\n> +\tfor (size_t i = 0; i < push_urls->nr; i++) {\n> +\t\tif (!strcmp(push_urls->v[i], url))\n> +\t\t\treturn true;\n> +\t}\n> +\treturn false;\n> +}\n\nA new helper is very much welcome.\n\n>  void ref_push_report_free(struct ref_push_report *report)\n>  {\n>  \twhile (report) {\n> @@ -1887,13 +1898,45 @@ const char *branch_get_upstream(struct branch *branch, struct strbuf *err)\n>  \treturn branch->merge[0]->dst;\n>  }\n>  \n> -static char *tracking_for_push_dest(struct repository *repo UNUSED,\n> +struct remote *repo_remote_for_push_tracking(struct repository *repo,\n> +\t\t\t\t\t     struct remote *remote)\n> +{\n> +\tconst struct strvec *push_urls;\n> +\tstruct remote *first_match = NULL;\n> +\tstruct remote_state *remote_state = repo->remote_state;\n> +\tconst char *check_url;\n> +\n> +\tif (remote->origin != REMOTE_UNCONFIGURED)\n> +\t\treturn remote;\n> +\n> +\tpush_urls = push_url_of_remote(remote);\n> +\tif (push_urls->nr != 1)\n> +\t\treturn remote;\n> +\tcheck_url = push_urls->v[0];\n> +\n> +\tfor (int i = 0; i < remote_state->remotes_nr; i++) {\n> +\t\tstruct remote *candidate = remote_state->remotes[i];\n> +\n> +\t\tif (!candidate || candidate == remote ||\n> +\t\t    !remote_is_configured(candidate, 0) ||\n> +\t\t    !remote_has_push_url(candidate, check_url))\n\nThis part used to use remote_has_url(candidate, remote->url.v[0]),\nwhich only looked at the .url and ignored .pushurl.  Now it uses\nremote_has_push_url() so we grab the effective push URL for the\nremote we are dealing with and match it against the effective push\nURL of the candidates.  Looks correct.\n\n> diff --git a/transport.c b/transport.c\n> index fc144f0aed..30a4ab2cd5 100644\n> --- a/transport.c\n> +++ b/transport.c\n> @@ -1553,8 +1553,11 @@ int transport_push(struct repository *r,\n>  \tif (!(flags & (TRANSPORT_PUSH_DRY_RUN |\n>  \t\t       TRANSPORT_RECURSE_SUBMODULES_ONLY))) {\n>  \t\tstruct ref *ref;\n> +\t\tstruct remote *tracking_remote = repo_remote_for_push_tracking(\n> +\t\t\tr, transport->remote);\n\nPersonally, I would have line-wrapped the above more like this:\n\n\t\tstruct remote *tracking_remote =\n\t\t\trepo_remote_for_push_tracking(r, transport->remote);\n\nThis is just for a future reference; it is certainly not critical\nenough to warrant a new iteration just for this.\n\nThanks.\n\n"},{"id":"548800","messageId":"xmqqjyqmmsrh.fsf@gitster.g","threadId":"66040","inReplyTo":"pull.2358.v4.git.git.1784743738.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 0/2] remote: url-based pushRemote with renamed remotes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-23T00:52:50Z","receivedAt":"2026-07-23T00:52:53Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Fix git status not showing the push branch after remotes are renamed, when\n> branch.<name>.pushRemote is a URL matching exactly one configured remote.\n>\n> Changes in v4:\n>\n>  * Match configured remotes by effective push URL, preferring pushurl over\n>    url.\n>  * Update the documentation and rationale to describe where the remote would\n>    push.\n\nMy cursory review did not spot anything obviously wrong anymore.\n\nAs it somehow seems to be a slow week, I do not expect to see many\neyeballs from others helping to review the topics in flight as\nquickly as we would have liked to move them forward, though.\n\nThanks.\n"}]}