{"thread":{"id":"66681","subject":"[PATCH] remote: resolve \"simple\" @{push} when pushing to another remote","startedAt":"2026-10-27T12:43:59Z","lastAt":"2026-11-01T01:09:29Z","messageCount":9,"participants":["Harald Nordgren via GitGitGadget","D. Ben Knoble","Harald Nordgren","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"556502","messageId":"pull.2466.git.git.1793105039624.gitgitgadget@gmail.com","threadId":"66681","inReplyTo":null,"subject":"[PATCH] remote: resolve \"simple\" @{push} when pushing to another remote","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-27T12:43:59Z","receivedAt":"2026-10-27T12:43:59Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nWith push.default left at \"simple\", a branch whose upstream is on one\nremote and whose push remote is another gets pushed by \"git push\" to a\nbranch of the same name on the push remote. But @{push} for that\nbranch fails with \"cannot resolve 'simple' push to a single\ndestination\". As a result \"git status\" shows the \"git push\" hint under\nthe comparison with the upstream, even though \"git push\" goes\nelsewhere, and leaves out the comparison with the push branch when\nstatus.compareBranches asks for it.\n\n\"git push\" learned to handle \"simple\" this way in ed2b18292b (push:\nchange `simple` to accommodate triangular workflows, 2013-06-19). When\n@{push} was added in e291c75a95 (remote.c: add branch_get_push,\n2015-05-21), its \"simple\" case only covered pushing back to the remote\nthe branch is fetched from, where the upstream must have the same name.\n\nMake @{push} follow \"git push\". When the push remote is not the remote\nof the branch, resolve to the branch of the same name on the push\nremote, as \"current\" does.\n\nThe test added in 9bf9eed093 (remote: fix leak in branch_get_push_1()\nwith invalid \"simple\" config, 2026-01-19) relied on a different push\nremote being invalid for \"simple\". Use an upstream with a different\nname on the same remote instead, so that it still exercises the error\npath.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    remote: resolve \"simple\" @{push} when pushing to another remote\n    \n    With push.default=simple and remote.pushDefault pointing at a fork, git\n    push pushes the branch to the fork, but @{push} fails with \"cannot\n    resolve 'simple' push to a single destination\".\n    \n    So git status cannot compare the branch with where it was pushed. Make\n    @{push} resolve to where git push goes.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2466%2FHaraldNordgren%2Fat-push-simple-triangular-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2466/HaraldNordgren/at-push-simple-triangular-v1\nPull-Request: https://github.com/git/git/pull/2466\n\n remote.c                  |  4 ++++\n t/for-each-ref-tests.sh   |  4 +++-\n t/t1514-rev-parse-push.sh | 11 +++++++++++\n 3 files changed, 18 insertions(+), 1 deletion(-)\n\ndiff --git a/remote.c b/remote.c\nindex 49ee2ea318..fd434a3a10 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2016,6 +2016,10 @@ static char *branch_get_push_1(struct repository *repo,\n \t\t\tconst char *up;\n \t\t\tchar *cur;\n \n+\t\t\tif (strcmp(remote->name,\n+\t\t\t\t   remotes_remote_for_branch(remote_state, branch, NULL)))\n+\t\t\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n+\n \t\t\tup = branch_get_upstream(branch, err);\n \t\t\tif (!up)\n \t\t\t\treturn NULL;\ndiff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh\nindex b1a10dd933..4991a82b91 100644\n--- a/t/for-each-ref-tests.sh\n+++ b/t/for-each-ref-tests.sh\n@@ -1745,9 +1745,11 @@ test_expect_success ':remotename and :remoteref' '\n '\n \n test_expect_success '%(push) with an invalid push-simple config' '\n+\ttest_when_finished \"git config branch.main.merge refs/heads/main\" &&\n+\tgit config branch.main.merge refs/heads/not-main &&\n \techo \"refs/heads/main \" >expect &&\n \tgit -c push.default=simple \\\n-\t    -c remote.pushdefault=myfork \\\n+\t    -c remote.pushdefault=origin \\\n \t    for-each-ref \\\n \t    --format=\"%(refname) %(push)\" refs/heads/main >actual &&\n \ttest_cmp expect actual\ndiff --git a/t/t1514-rev-parse-push.sh b/t/t1514-rev-parse-push.sh\nindex 5a4f16867a..4187e2a504 100755\n--- a/t/t1514-rev-parse-push.sh\n+++ b/t/t1514-rev-parse-push.sh\n@@ -60,6 +60,17 @@ test_expect_success '@{push} with pushremote defined' '\n \tresolve topic@{push} refs/remotes/other/topic\n '\n \n+test_expect_success 'triangular @{push} with default=simple and remote.pushdefault' '\n+\ttest_config push.default simple &&\n+\ttest_config remote.pushdefault other &&\n+\ttest_when_finished \"git checkout main\" &&\n+\tgit checkout topic &&\n+\ttest_commit simple-triangular &&\n+\tgit push &&\n+\tresolve topic@{push} refs/remotes/other/topic &&\n+\ttest_cmp_rev topic topic@{push}\n+'\n+\n test_expect_success '@{p} is short for @{push}' '\n \ttest_config push.default current &&\n \ttest_config branch.topic.pushremote other &&\n\nbase-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e\n-- \ngitgitgadget\n\n"},{"id":"556714","messageId":"CALnO6CBxMrv01NMqEPCLbGbCPXX9T2Jgjax-L5s2z9s-eUpNDQ@mail.gmail.com","threadId":"66681","inReplyTo":"pull.2466.git.git.1793105039624.gitgitgadget@gmail.com","subject":"Re: [PATCH] remote: resolve \"simple\" @{push} when pushing to another remote","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-10-28T22:24:51Z","receivedAt":"2026-10-28T22:24:51Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Tue, Oct 27, 2026 at 8:48 AM Harald Nordgren via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n>\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> With push.default left at \"simple\", a branch whose upstream is on one\n> remote and whose push remote is another gets pushed by \"git push\" to a\n> branch of the same name on the push remote. But @{push} for that\n> branch fails with \"cannot resolve 'simple' push to a single\n> destination\".\n\nAt first I was a bit confused about the need for a different push\nremote, but then I realized: simple's documentation says that if the\npush and pull remote are the *same* you need the upstream to have the\nsame name! So in the differing case, it might behave quite differently\nthan I'm used to.\n\n> \"git push\" learned to handle \"simple\" this way in ed2b18292b (push:\n> change `simple` to accommodate triangular workflows, 2013-06-19). When\n> @{push} was added in e291c75a95 (remote.c: add branch_get_push,\n> 2015-05-21), its \"simple\" case only covered pushing back to the remote\n> the branch is fetched from, where the upstream must have the same name.\n>\n> Make @{push} follow \"git push\". When the push remote is not the remote\n> of the branch, resolve to the branch of the same name on the push\n> remote, as \"current\" does.\n\nWhatever else we do, making @{push} work in more scenarios seems good\nto me so we can be consistent.\n\n> The test added in 9bf9eed093 (remote: fix leak in branch_get_push_1()\n> with invalid \"simple\" config, 2026-01-19) relied on a different push\n> remote being invalid for \"simple\". Use an upstream with a different\n> name on the same remote instead, so that it still exercises the error\n> path.\n>\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n>     remote: resolve \"simple\" @{push} when pushing to another remote\n>\n>     With push.default=simple and remote.pushDefault pointing at a fork, git\n>     push pushes the branch to the fork, but @{push} fails with \"cannot\n>     resolve 'simple' push to a single destination\".\n>\n>     So git status cannot compare the branch with where it was pushed. Make\n>     @{push} resolve to where git push goes.\n>\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2466%2FHaraldNordgren%2Fat-push-simple-triangular-v1\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2466/HaraldNordgren/at-push-simple-triangular-v1\n> Pull-Request: https://github.com/git/git/pull/2466\n>\n>  remote.c                  |  4 ++++\n>  t/for-each-ref-tests.sh   |  4 +++-\n>  t/t1514-rev-parse-push.sh | 11 +++++++++++\n>  3 files changed, 18 insertions(+), 1 deletion(-)\n>\n> diff --git a/remote.c b/remote.c\n> index 49ee2ea318..fd434a3a10 100644\n> --- a/remote.c\n> +++ b/remote.c\n> @@ -2016,6 +2016,10 @@ static char *branch_get_push_1(struct repository *repo,\n>                         const char *up;\n>                         char *cur;\n>\n> +                       if (strcmp(remote->name,\n> +                                  remotes_remote_for_branch(remote_state, branch, NULL)))\n> +                               return tracking_for_push_dest(repo, remote, branch->refname, err);\n\nThis certainly matches the logic described in the log message, and I\nthink it's probably right based on that description. One thing I can't\ndecide is whether it should come before or after the current set of\ncases, and I'm undecided. On one hand, it's a new case, so I would\nthink it should come after. OTOH, it's a fairly straightforward case,\nand I usually like discharging those early so I can focus on the\ncomplex stuff.\n\nSo, I would say this is probably fine as-is?\n\n[Skipping the tests for now]\n\n"},{"id":"556761","messageId":"CAHwyqnX3+nwL4d7-eE_jkWjQJdLwZ0zeC+eqW_7Mmt0ncbSfMQ@mail.gmail.com","threadId":"66681","inReplyTo":"CALnO6CBxMrv01NMqEPCLbGbCPXX9T2Jgjax-L5s2z9s-eUpNDQ@mail.gmail.com","subject":"Re: [PATCH] remote: resolve \"simple\" @{push} when pushing to another remote","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-10-29T10:34:14Z","receivedAt":"2026-10-29T10:34:14Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> So, I would say this is probably fine as-is?\n\nThanks!\n\n\nHarald\n\n"},{"id":"556805","messageId":"xmqq8q3g4cv1.fsf@gitster.g","threadId":"66681","inReplyTo":"CALnO6CBxMrv01NMqEPCLbGbCPXX9T2Jgjax-L5s2z9s-eUpNDQ@mail.gmail.com","subject":"Re: [PATCH] remote: resolve \"simple\" @{push} when pushing to another remote","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-29T17:48:18Z","receivedAt":"2026-10-29T17:48:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"D. Ben Knoble\" <ben.knoble@gmail.com> writes:\n\n>> diff --git a/remote.c b/remote.c\n>> index 49ee2ea318..fd434a3a10 100644\n>> --- a/remote.c\n>> +++ b/remote.c\n>> @@ -2016,6 +2016,10 @@ static char *branch_get_push_1(struct repository *repo,\n>>                         const char *up;\n>>                         char *cur;\n>>\n>> +                       if (strcmp(remote->name,\n>> +                                  remotes_remote_for_branch(remote_state, branch, NULL)))\n>> +                               return tracking_for_push_dest(repo, remote, branch->refname, err);\n>\n> This certainly matches the logic described in the log message, and I\n> think it's probably right based on that description. One thing I can't\n> decide is whether it should come before or after the current set of\n> cases, and I'm undecided. On one hand, it's a new case, so I would\n> think it should come after. OTOH, it's a fairly straightforward case,\n> and I usually like discharging those early so I can focus on the\n> complex stuff.\n\nI actually wondered if the existing case for \"simple\" becomes a\nsubset of this new code?  After all, tracking_for_push_dest() asks\nwhere the thing gets pushed to, no?\n\nIn any case, all the case arms in this function is getting a bit\nunmanageably long and this block is, with an extra artificial block,\nunnecessarily deeply nested by one extra level.  I wonder if we can\nmove the code for \"case PUSH_DEFAULT_SIMPLE\" case to a separate\nfunction and make it a bit more readable?\n\nI agree that the end result the commit wants to see is sensible and\nconsistent with the earlier \"what should happen with 'simple' when\ntriangular?\" topic.\n\nThanks.\n\n"},{"id":"556837","messageId":"CAHwyqnWSAu6ZqC2HcL8a0HSMKRHZGwvfqx0bi_vRMWc=t_-b+Q@mail.gmail.com","threadId":"66681","inReplyTo":"xmqq8q3g4cv1.fsf@gitster.g","subject":"Re: [PATCH] remote: resolve \"simple\" @{push} when pushing to another remote","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-10-29T22:15:07Z","receivedAt":"2026-10-29T22:15:07Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> >> +                       if (strcmp(remote->name,\n> >> +                                  remotes_remote_for_branch(remote_state, branch, NULL)))\n> >> +                               return tracking_for_push_dest(repo, remote, branch->refname, err);\n> >\n> > This certainly matches the logic described in the log message, and I\n> > think it's probably right based on that description. One thing I can't\n> > decide is whether it should come before or after the current set of\n> > cases, and I'm undecided. On one hand, it's a new case, so I would\n> > think it should come after. OTOH, it's a fairly straightforward case,\n> > and I usually like discharging those early so I can focus on the\n> > complex stuff.\n>\n> I actually wondered if the existing case for \"simple\" becomes a\n> subset of this new code?  After all, tracking_for_push_dest() asks\n> where the thing gets pushed to, no?\n\nYes, both resolve to the same branch. The same remote case only adds a\ncheck that the upstream has the same name, because \"git push\" refuses\notherwise.\n\n> In any case, all the case arms in this function is getting a bit\n> unmanageably long and this block is, with an extra artificial block,\n> unnecessarily deeply nested by one extra level.  I wonder if we can\n> move the code for \"case PUSH_DEFAULT_SIMPLE\" case to a separate\n> function and make it a bit more readable?\n\nGood point.\n\n\nHarald\n\n"},{"id":"556994","messageId":"pull.2466.v2.git.git.1793433841.gitgitgadget@gmail.com","threadId":"66681","inReplyTo":"pull.2466.git.git.1793105039624.gitgitgadget@gmail.com","subject":"[PATCH v2 0/2] remote: resolve \"simple\" @{push} when pushing to another remote","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-31T08:03:59Z","receivedAt":"2026-10-31T08:03:59Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"With push.default=simple and remote.pushDefault pointing at a fork, git push\npushes the branch to the fork, but @{push} fails with \"cannot resolve\n'simple' push to a single destination\".\n\nSo git status cannot compare the branch with where it was pushed. Make\n@{push} resolve to where git push goes.\n\nChanges in v2:\n\n * Moved the \"simple\" @{push} code out of branch_get_push_1() into its own\n   function in a new preparatory commit.\n\nHarald Nordgren (2):\n  remote: move \"simple\" @{push} resolution into a helper\n  remote: resolve \"simple\" @{push} when pushing to another remote\n\n remote.c                  | 43 +++++++++++++++++++++++----------------\n t/for-each-ref-tests.sh   |  4 +++-\n t/t1514-rev-parse-push.sh | 13 +++++++++++-\n 3 files changed, 41 insertions(+), 19 deletions(-)\n\n\nbase-commit: b9e5cff1803ab24582201ef059516eb16014b9d9\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2466%2FHaraldNordgren%2Fat-push-simple-triangular-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2466/HaraldNordgren/at-push-simple-triangular-v2\nPull-Request: https://github.com/git/git/pull/2466\n\nRange-diff vs v1:\n\n -:  ---------- > 1:  151fef69b9 remote: move \"simple\" @{push} resolution into a helper\n 1:  d2b1a00d23 ! 2:  c58afcc4b1 remote: resolve \"simple\" @{push} when pushing to another remote\n     @@ Commit message\n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## remote.c ##\n     -@@ remote.c: static char *branch_get_push_1(struct repository *repo,\n     - \t\t\tconst char *up;\n     - \t\t\tchar *cur;\n     +@@ remote.c: static char *branch_get_push_simple(struct repository *repo,\n     + \t\t\t\t    struct branch *branch,\n     + \t\t\t\t    struct strbuf *err)\n     + {\n     +-\tconst char *up;\n     ++\tconst char *up = NULL;\n     + \tchar *cur;\n       \n     -+\t\t\tif (strcmp(remote->name,\n     -+\t\t\t\t   remotes_remote_for_branch(remote_state, branch, NULL)))\n     -+\t\t\t\treturn tracking_for_push_dest(repo, remote, branch->refname, err);\n     -+\n     - \t\t\tup = branch_get_upstream(branch, err);\n     - \t\t\tif (!up)\n     - \t\t\t\treturn NULL;\n     +-\tup = branch_get_upstream(branch, err);\n     +-\tif (!up)\n     +-\t\treturn NULL;\n     ++\tif (!strcmp(remote->name,\n     ++\t\t    remotes_remote_for_branch(repo->remote_state, branch, NULL))) {\n     ++\t\tup = branch_get_upstream(branch, err);\n     ++\t\tif (!up)\n     ++\t\t\treturn NULL;\n     ++\t}\n     + \tcur = tracking_for_push_dest(repo, remote, branch->refname, err);\n     + \tif (!cur)\n     + \t\treturn NULL;\n     +-\tif (strcmp(cur, up)) {\n     ++\tif (up && strcmp(cur, up)) {\n     + \t\tfree(cur);\n     + \t\treturn error_buf(err,\n     + \t\t\t\t _(\"cannot resolve 'simple' push to a single destination\"));\n      \n       ## t/for-each-ref-tests.sh ##\n      @@ t/for-each-ref-tests.sh: test_expect_success ':remotename and :remoteref' '\n     @@ t/for-each-ref-tests.sh: test_expect_success ':remotename and :remoteref' '\n       \ttest_cmp expect actual\n      \n       ## t/t1514-rev-parse-push.sh ##\n     +@@ t/t1514-rev-parse-push.sh: test_expect_success '@{push} with default=simple' '\n     + \tresolve main@{pUSh} refs/remotes/origin/main\n     + '\n     + \n     +-test_expect_success 'triangular @{push} fails with default=simple' '\n     ++test_expect_success '@{push} with default=simple fails when upstream has a different name' '\n     + \ttest_config push.default simple &&\n     + \ttest_must_fail git rev-parse topic@{push}\n     + '\n      @@ t/t1514-rev-parse-push.sh: test_expect_success '@{push} with pushremote defined' '\n       \tresolve topic@{push} refs/remotes/other/topic\n       '\n\n-- \ngitgitgadget\n\n"},{"id":"556995","messageId":"151fef69b9ec221b39fdbab1d46f0b123f9079e4.1793433841.git.gitgitgadget@gmail.com","threadId":"66681","inReplyTo":"pull.2466.v2.git.git.1793433841.gitgitgadget@gmail.com","subject":"[PATCH v2 1/2] remote: move \"simple\" @{push} resolution into a helper","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-31T08:04:00Z","receivedAt":"2026-10-31T08:04:00Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nThe \"simple\" arm is the longest in the switch of branch_get_push_1()\nand needs an extra block to declare its variables. Give it its own\nfunction so that it reads at one level of indentation and the next\ncommit can teach it about pushing to another remote without nesting\nit further.\n\nNo functional change.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 40 +++++++++++++++++++++++-----------------\n 1 file changed, 23 insertions(+), 17 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 49ee2ea318..d15d34aad7 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1967,6 +1967,28 @@ static char *tracking_for_push_dest(struct repository *repo,\n \treturn ret;\n }\n \n+static char *branch_get_push_simple(struct repository *repo,\n+\t\t\t\t    struct remote *remote,\n+\t\t\t\t    struct branch *branch,\n+\t\t\t\t    struct strbuf *err)\n+{\n+\tconst char *up;\n+\tchar *cur;\n+\n+\tup = branch_get_upstream(branch, err);\n+\tif (!up)\n+\t\treturn NULL;\n+\tcur = tracking_for_push_dest(repo, remote, branch->refname, err);\n+\tif (!cur)\n+\t\treturn NULL;\n+\tif (strcmp(cur, up)) {\n+\t\tfree(cur);\n+\t\treturn error_buf(err,\n+\t\t\t\t _(\"cannot resolve 'simple' push to a single destination\"));\n+\t}\n+\treturn cur;\n+}\n+\n static char *branch_get_push_1(struct repository *repo,\n \t\t\t       struct branch *branch, struct strbuf *err)\n {\n@@ -2012,23 +2034,7 @@ static char *branch_get_push_1(struct repository *repo,\n \n \tcase PUSH_DEFAULT_UNSPECIFIED:\n \tcase PUSH_DEFAULT_SIMPLE:\n-\t\t{\n-\t\t\tconst char *up;\n-\t\t\tchar *cur;\n-\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(repo, remote, branch->refname, err);\n-\t\t\tif (!cur)\n-\t\t\t\treturn NULL;\n-\t\t\tif (strcmp(cur, up)) {\n-\t\t\t\tfree(cur);\n-\t\t\t\treturn error_buf(err,\n-\t\t\t\t\t\t _(\"cannot resolve 'simple' push to a single destination\"));\n-\t\t\t}\n-\t\t\treturn cur;\n-\t\t}\n+\t\treturn branch_get_push_simple(repo, remote, branch, err);\n \t}\n \n \tBUG(\"unhandled push situation\");\n-- \ngitgitgadget\n\n\n"},{"id":"556996","messageId":"c58afcc4b166d33489feee41d57e65738581a0da.1793433841.git.gitgitgadget@gmail.com","threadId":"66681","inReplyTo":"pull.2466.v2.git.git.1793433841.gitgitgadget@gmail.com","subject":"[PATCH v2 2/2] remote: resolve \"simple\" @{push} when pushing to another remote","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-31T08:04:01Z","receivedAt":"2026-10-31T08:04:01Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nWith push.default left at \"simple\", a branch whose upstream is on one\nremote and whose push remote is another gets pushed by \"git push\" to a\nbranch of the same name on the push remote. But @{push} for that\nbranch fails with \"cannot resolve 'simple' push to a single\ndestination\". As a result \"git status\" shows the \"git push\" hint under\nthe comparison with the upstream, even though \"git push\" goes\nelsewhere, and leaves out the comparison with the push branch when\nstatus.compareBranches asks for it.\n\n\"git push\" learned to handle \"simple\" this way in ed2b18292b (push:\nchange `simple` to accommodate triangular workflows, 2013-06-19). When\n@{push} was added in e291c75a95 (remote.c: add branch_get_push,\n2015-05-21), its \"simple\" case only covered pushing back to the remote\nthe branch is fetched from, where the upstream must have the same name.\n\nMake @{push} follow \"git push\". When the push remote is not the remote\nof the branch, resolve to the branch of the same name on the push\nremote, as \"current\" does.\n\nThe test added in 9bf9eed093 (remote: fix leak in branch_get_push_1()\nwith invalid \"simple\" config, 2026-01-19) relied on a different push\nremote being invalid for \"simple\". Use an upstream with a different\nname on the same remote instead, so that it still exercises the error\npath.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                  | 13 ++++++++-----\n t/for-each-ref-tests.sh   |  4 +++-\n t/t1514-rev-parse-push.sh | 13 ++++++++++++-\n 3 files changed, 23 insertions(+), 7 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex d15d34aad7..98665a3085 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1972,16 +1972,19 @@ static char *branch_get_push_simple(struct repository *repo,\n \t\t\t\t    struct branch *branch,\n \t\t\t\t    struct strbuf *err)\n {\n-\tconst char *up;\n+\tconst char *up = NULL;\n \tchar *cur;\n \n-\tup = branch_get_upstream(branch, err);\n-\tif (!up)\n-\t\treturn NULL;\n+\tif (!strcmp(remote->name,\n+\t\t    remotes_remote_for_branch(repo->remote_state, branch, NULL))) {\n+\t\tup = branch_get_upstream(branch, err);\n+\t\tif (!up)\n+\t\t\treturn NULL;\n+\t}\n \tcur = tracking_for_push_dest(repo, remote, branch->refname, err);\n \tif (!cur)\n \t\treturn NULL;\n-\tif (strcmp(cur, up)) {\n+\tif (up && strcmp(cur, up)) {\n \t\tfree(cur);\n \t\treturn error_buf(err,\n \t\t\t\t _(\"cannot resolve 'simple' push to a single destination\"));\ndiff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh\nindex b1a10dd933..4991a82b91 100644\n--- a/t/for-each-ref-tests.sh\n+++ b/t/for-each-ref-tests.sh\n@@ -1745,9 +1745,11 @@ test_expect_success ':remotename and :remoteref' '\n '\n \n test_expect_success '%(push) with an invalid push-simple config' '\n+\ttest_when_finished \"git config branch.main.merge refs/heads/main\" &&\n+\tgit config branch.main.merge refs/heads/not-main &&\n \techo \"refs/heads/main \" >expect &&\n \tgit -c push.default=simple \\\n-\t    -c remote.pushdefault=myfork \\\n+\t    -c remote.pushdefault=origin \\\n \t    for-each-ref \\\n \t    --format=\"%(refname) %(push)\" refs/heads/main >actual &&\n \ttest_cmp expect actual\ndiff --git a/t/t1514-rev-parse-push.sh b/t/t1514-rev-parse-push.sh\nindex 5a4f16867a..dc2df7eb80 100755\n--- a/t/t1514-rev-parse-push.sh\n+++ b/t/t1514-rev-parse-push.sh\n@@ -39,7 +39,7 @@ test_expect_success '@{push} with default=simple' '\n \tresolve main@{pUSh} refs/remotes/origin/main\n '\n \n-test_expect_success 'triangular @{push} fails with default=simple' '\n+test_expect_success '@{push} with default=simple fails when upstream has a different name' '\n \ttest_config push.default simple &&\n \ttest_must_fail git rev-parse topic@{push}\n '\n@@ -60,6 +60,17 @@ test_expect_success '@{push} with pushremote defined' '\n \tresolve topic@{push} refs/remotes/other/topic\n '\n \n+test_expect_success 'triangular @{push} with default=simple and remote.pushdefault' '\n+\ttest_config push.default simple &&\n+\ttest_config remote.pushdefault other &&\n+\ttest_when_finished \"git checkout main\" &&\n+\tgit checkout topic &&\n+\ttest_commit simple-triangular &&\n+\tgit push &&\n+\tresolve topic@{push} refs/remotes/other/topic &&\n+\ttest_cmp_rev topic topic@{push}\n+'\n+\n test_expect_success '@{p} is short for @{push}' '\n \ttest_config push.default current &&\n \ttest_config branch.topic.pushremote other &&\n-- \ngitgitgadget\n\n"},{"id":"557052","messageId":"xmqq5wyh2w8m.fsf@gitster.g","threadId":"66681","inReplyTo":"pull.2466.v2.git.git.1793433841.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 0/2] remote: resolve \"simple\" @{push} when pushing to another remote","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-11-01T01:09:29Z","receivedAt":"2026-11-01T01:09:29Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> With push.default=simple and remote.pushDefault pointing at a fork, git push\n> pushes the branch to the fork, but @{push} fails with \"cannot resolve\n> 'simple' push to a single destination\".\n>\n> So git status cannot compare the branch with where it was pushed. Make\n> @{push} resolve to where git push goes.\n>\n> Changes in v2:\n>\n>  * Moved the \"simple\" @{push} code out of branch_get_push_1() into its own\n>    function in a new preparatory commit.\n\nLooking good.  Will queue.\n\nThanks.\n\n"}]}