{"thread":{"id":"64668","subject":"[PATCH] status: show default branch comparison when tracking non-default branch","startedAt":"2025-12-23T00:53:39Z","lastAt":"2026-03-09T15:10:58Z","messageCount":259,"participants":["Harald Nordgren via GitGitGadget","Junio C Hamano","Harald Nordgren","Chris Torek","Yee Cheng Chin","brian m. carlson","Ben Knoble","Kristoffer Haugsbakk","Phillip Wood","Nico Williams","D. Ben Knoble","Patrick Steinhardt","Jeff King"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"532634","messageId":"pull.2138.git.git.1766451217075.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":null,"subject":"[PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-23T00:53:37Z","receivedAt":"2025-12-23T00:53:39Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nWhen a branch tracks a non-default remote branch (e.g.,\norigin/feature), git status now also displays how the branch\ncompares to the default branch (origin/main or upstream/main).\nThis helps users understand if their branch has drifted from the\nmain development line even when it's in sync with its tracking\nbranch.\n\nThe comparison is shown as a separate line after the tracking\nbranch status:\n- \"Ahead of 'origin/main' by N commits\" when purely ahead\n- \"Behind 'origin/main' by N commits\" when purely behind\n- \"Diverged from 'origin/main' by N commits\" when diverged\n\nExample output when tracking a feature branch:\n\n    On branch feature\n    Your branch is ahead of 'origin/feature' by 2 commits.\n      (use \"git push\" to publish your local commits)\n\n    Ahead of 'origin/main' by 5 commits.\n\nThe default branch is determined dynamically by checking:\n1. refs/remotes/upstream/HEAD (if upstream remote exists)\n2. refs/remotes/origin/HEAD (fallback)\n\nThis works with any default branch name (main, master, develop,\netc.) as long as the symbolic ref is configured. The comparison\nis also shown when the branch is up-to-date with its tracking\nbranch but differs from the default branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    status: show default branch comparison when tracking non-default branch\n    \n    When a branch tracks a non-default remote branch (e.g., origin/feature),\n    git status now also displays how the branch compares to the default\n    branch (origin/main or upstream/main). This helps users understand if\n    their branch has drifted from the main development line even when it's\n    in sync with its tracking branch.\n    \n    The comparison is shown as a separate line after the tracking branch\n    status:\n    \n     * \"Ahead of 'origin/main' by N commits\" when purely ahead\n     * \"Behind 'origin/main' by N commits\" when purely behind\n     * \"Diverged from 'origin/main' by N commits\" when diverged\n    \n    The default branch is determined dynamically by checking:\n    \n     1. refs/remotes/upstream/HEAD (if upstream remote exists)\n     2. refs/remotes/origin/HEAD (fallback)\n    \n    This works with any default branch name (main, master, develop, etc.) as\n    long as the symbolic ref is configured. The comparison is also shown\n    when the branch is up-to-date with its tracking branch but differs from\n    the default branch.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v1\nPull-Request: https://github.com/git/git/pull/2138\n\n remote.c                 | 101 ++++++++++++++++\n t/t6040-tracking-info.sh | 246 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 347 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..9be5e5aa22 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,95 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static const char *get_default_remote_ref(char **full_ref_out)\n+{\n+\tint flag;\n+\tconst char *resolved;\n+\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n+\tint i;\n+\n+\tfor (i = 0; remotes[i]; i++) {\n+\t\tstruct strbuf head_ref = STRBUF_INIT;\n+\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n+\n+\t\tresolved = refs_resolve_ref_unsafe(\n+\t\t\tget_main_ref_store(the_repository),\n+\t\t\thead_ref.buf,\n+\t\t\tRESOLVE_REF_READING,\n+\t\t\tNULL, &flag);\n+\n+\t\tstrbuf_release(&head_ref);\n+\n+\t\tif (resolved && (flag & REF_ISSYMREF)) {\n+\t\t\tif (full_ref_out)\n+\t\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\t\treturn refs_shorten_unambiguous_ref(\n+\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t\t}\n+\t}\n+\n+\treturn NULL;\n+}\n+\n+static int is_default_remote_branch(const char *name)\n+{\n+\tchar *default_full = NULL;\n+\tconst char *default_short;\n+\tint result = 0;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn 0;\n+\n+\tresult = !strcmp(name, default_short);\n+\n+\tfree(default_full);\n+\treturn result;\n+}\n+\n+static void format_default_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint default_ours = 0, default_theirs = 0;\n+\tchar *default_full = NULL;\n+\tconst char *default_short;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn;\n+\n+\tif (stat_branch_pair(branch_refname, default_full,\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n+\t\tfree(default_full);\n+\t\treturn;\n+\t}\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (default_ours > 0 && default_theirs == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   default_ours),\n+\t\t\tdefault_short, default_ours);\n+\t} else if (default_theirs > 0 && default_ours == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   default_theirs),\n+\t\t\tdefault_short, default_theirs);\n+\t} else if (default_ours > 0 && default_theirs > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   default_ours + default_theirs),\n+\t\t\tdefault_short, default_ours + default_theirs);\n+\t}\n+\n+\tfree(default_full);\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2248,6 +2337,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tint show_default_branch_comparison;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2258,6 +2348,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n+\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2269,6 +2362,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2285,6 +2380,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2296,6 +2393,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2310,6 +2409,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n \tfree(base);\n \treturn 1;\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..e2bd48f858 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,6 +21,7 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -292,4 +293,249 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup for ahead of non-main tracking branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature origin/main &&\n+\t\tadvance feature1 &&\n+\t\tgit push origin feature &&\n+\t\tgit checkout -b work --track origin/feature &&\n+\t\tadvance work1 &&\n+\t\tadvance work2\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main >/dev/null &&\n+\t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+Switched to branch '\\''work'\\''\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 >/dev/null &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n+\ttest_grep ! \"Ahead of\" actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b oldfeature &&\n+\t\tadvance oldfeature1 &&\n+\t\tgit push origin oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_newer &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout -b work2 --track origin/oldfeature &&\n+\t\tadvance work2_commit\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2\n+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for diverged from tracked but behind main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b work2b &&\n+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_extra &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2b >/dev/null &&\n+\t\tgit status --long -b | head -6\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2b\n+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n+and have 1 and 1 different commits each, respectively.\n+  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+Behind '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for behind tracked but ahead of main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b feature3 &&\n+\t\tadvance feature3_1 &&\n+\t\tadvance feature3_2 &&\n+\t\tadvance feature3_3 &&\n+\t\tgit push origin feature3 &&\n+\t\tgit checkout -b work3 --track origin/feature3 &&\n+\t\tgit reset --hard HEAD~2\n+\t)\n+'\n+\n+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work3 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work3\n+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n+  (use \"git pull\" to update your local branch)\n+\n+Ahead of '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote preference' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t)\n+'\n+\n+test_expect_success 'status prefers upstream remote over origin for comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''upstream/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''upstream/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote remove upstream &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead2 &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature2\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature3 --track origin/feature\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda\n-- \ngitgitgadget\n"},{"id":"532639","messageId":"xmqqy0mtpxti.fsf@gitster.g","threadId":"64668","inReplyTo":"pull.2138.git.git.1766451217075.gitgitgadget@gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-23T05:32:57Z","receivedAt":"2025-12-23T05:33:00Z","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> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> When a branch tracks a non-default remote branch (e.g.,\n> origin/feature), git status now also displays how the branch\n> compares to the default branch (origin/main or upstream/main).\n\n\"now\" meaning what\"?\n\nThe usual way to compose a log message of this project is to\n\n - Give an observation on how the current system works in the\n   present tense (so no need to say \"Currently X is Y\", or\n   \"Previously X was Y\" to describe the state before your change;\n   just \"X is Y\" is enough), and discuss what you perceive as a\n   problem in it.\n\n - Propose a solution (optional---often, problem description\n   trivially leads to an obvious solution in reader's minds).\n\n - Give commands to somebody editing the codebase to \"make it so\",\n   instead of saying \"This commit does X\".\n\nin this order.\n\nSo if you are following the convention, \"now also displays\" ought to\nbe about what the current code without this patch does, but I am\nsensing that it probably is not the case.\n\nStart your explanation by decribing what the users see in \"git\nstatus\" output without this patch.  Perhaps like\n\n    \"git status\" on a branch that follows a remote branch compares\n    commits on the current branch and the remote-tracking branch it\n    builds upon, to show \"ahead\" (i.e. you have built new history,\n    while others are not touching it), \"behind\" (i.e. you haven't\n    added any work since you were in-sync, while others have added\n    their work on the branch), \"diverged\" (i.e. you have commits\n    that you haven't pushed out, while others have added commits).\n\nThat is the \"giving an observation\" part.  And then describe why\nthat comparison with a single remote branch may be insufficient to\nlearn the current status.  Your reasoning might be something like\n\n    When you fork a branch 'feature' from the 'main' branch of the\n    remote, but then create 'feature' branch at the remote and push\n    there, while you still occasionally pull from or rebase onto\n    their 'main', you'd _also_ want to know how much you have\n    diverged from 'main', in addition to how your 'feature' and\n    their 'feature' compares.  Currently the comparison with 'main'\n    is not given.\n\nThat's the \"discuss your problem with the status quo\" part.\n\nOnly after that, propose to show two sets of comparison.\n\n> This helps users understand if their branch has drifted from the\n> main development line even when it's in sync with its tracking\n> branch.\n\nDescribe what does it help to know that after that sentence, like\n\"... to get the feel of when to start thinking about rebasing\", or\nsomething.\n\n> The comparison is shown as a separate line after the tracking\n> branch status:\n> - \"Ahead of 'origin/main' by N commits\" when purely ahead\n> - \"Behind 'origin/main' by N commits\" when purely behind\n> - \"Diverged from 'origin/main' by N commits\" when diverged\n\nIn other words, exactly the same way as what we show with the\ntracking branch?\n\nThe triangular workflow involves two remote things.  One is where\nyou pull from to catch up.  After building on top, you push to\nsomewhere else to publish your work.  This may be a different branch\nin the same repository you pull from, or a branch in a completely\ndifferent repository.  What you pushed out may be processed by\nothers and may come back in the branch you pull from eventually to\ncomplete the triangle.\n\nIn such a triangular workflow, comparison with these two remote\nthings may be needed. One with the branch you forked your work from\nto know how much work _other_ people added to the branch to learn\nwhen to start thinking about catching up, and with the branch you\nare pushing your work to to know how much work you are holding\nlocally without pushing out.\n\nI am not sure what you mean by the word \"default\" here, though.\n\nYou seem to be using the \"what would a new user get when they clone\nthe remote (by virtue of their HEAD pointing at that branch)\", but\nI am not sure if that is a good way to determine the other remote\nthing to compare with.\n\nEven if one remote branch you pull from (but not push to) has a name\nthat is not one of those usual ones like 'main', 'master', 'trunk',\n'default', you would want to compare with it in addition to where\nyou are pushing to.  So branch.<name>.merge + branch.<name>.remote\nthat defines where you pull from is one thing to compare with.  To\nlearn the other, the destination of a push of this branch, would\ninvolve poking at remote.pushdefault, branch.<name>.pushRemote,\nbranch.<name>.remote to find out which remote repository it goes,\nand then remote.<remote>.push to find out where this branch goes,\nbut the helper functions to learn all that are already available.\n\nSo, I think the topic addresses a good problem, its presentation\nneeds a bit more work, and its design (not the implementation) of\nhow to figure out the other thing to compare I am not sure about.\n\nThanks.\n"},{"id":"532640","messageId":"20251223102422.36853-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqy0mtpxti.fsf@gitster.g","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-23T10:24:22Z","receivedAt":"2025-12-23T10:24:25Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> In other words, exactly the same way as what we show with the\n> tracking branch?\n> \n> The triangular workflow involves two remote things.  One is where\n> you pull from to catch up.  After building on top, you push to\n> somewhere else to publish your work.  This may be a different branch\n> in the same repository you pull from, or a branch in a completely\n> different repository.  What you pushed out may be processed by\n> others and may come back in the branch you pull from eventually to\n> complete the triangle.\n> \n> In such a triangular workflow, comparison with these two remote\n> things may be needed. One with the branch you forked your work from\n> to know how much work _other_ people added to the branch to learn\n> when to start thinking about catching up, and with the branch you\n> are pushing your work to to know how much work you are holding\n> locally without pushing out.\n\nYes, it's the same as when tracking master/main, but with far less complexity for the end-user. For many years I have had the habit of running these on my feature branches:\n\n\tgit checkout -b feature_branch\n\tgit branch --set-upstream-to origin/$(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@')\n\n\t# To merge in other's code early\n\tgit pull --rebase origin $(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@')\n\n\t# To push to correct branch, because I'm tracking default instead \"feature_branch\"\n\tgit push origin $(git rev-parse --abbrev-ref HEAD)\n\nI have found this very hard to explain the benefits of this to other developers. Git is already scary to many, they are afraid of losing work, so they would rather not mess with the tracking branches and break the regular \"git push\" functionality.\n\nFor me, I feel blind of I can't see how my branch compares the master/main at all times.\n\n> I am not sure what you mean by the word \"default\" here, though.\n> \n> You seem to be using the \"what would a new user get when they clone\n> the remote (by virtue of their HEAD pointing at that branch)\", but\n> I am not sure if that is a good way to determine the other remote\n> thing to compare with.\n>\n> Even if one remote branch you pull from (but not push to) has a name\n> that is not one of those usual ones like 'main', 'master', 'trunk',\n> 'default'\n\nAgreed, this should be as agnostic as possible. \"Default\" might be me using GitHub terminology. However, it seems that 'git symbolic-ref refs/remotes/upstream/HEAD' always produces the desires result, so for the sake of discussion we can call it \"upstream/HEAD\" instead of \"default\".\n\nHarald\n"},{"id":"532653","messageId":"20251223113602.63000-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20251223102422.36853-1-haraldnordgren@gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-23T11:36:02Z","receivedAt":"2025-12-23T11:36:05Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"(Adding another comment to my own thread to reply to Junio C Hamano)\n\n> branch.<name>.merge + branch.<name>.remote\n> that defines where you pull from is one thing to compare with.  To\n> learn the other, the destination of a push of this branch, would\n> involve poking at remote.pushdefault, branch.<name>.pushRemote,\n> branch.<name>.remote to find out which remote repository it goes,\n> and then remote.<remote>.push to find out where this branch goes,\n> but the helper functions to learn all that are already available.\n\nWhen a new branch is created it has no push settings:\n\n\tgit checkout -b ahead_of_main_status__tmp2\n\n\tgit push\n\tfatal: The current branch ahead_of_main_status__tmp2 has no upstream branch.\n\tTo push the current branch and set the remote as upstream, use\n\n    \tgit push --set-upstream origin ahead_of_main_status__tmp2\n\n\tTo have this happen automatically for branches without a tracking\n\tupstream, see 'push.autoSetupRemote' in 'git help config'.\n\nOnce the users runs that suggested command\n\n\tgit push --set-upstream origin ahead_of_main_status__tmp2\n\nthen the 'branch.<name>.merge' and 'branch.<name>.remote' no longer hold the reference to \"upstream/HEAD\".\n\nFor sure, it would be great to re-use previous logic for this, but can it really be done without new logic?\n\nHarald\n"},{"id":"532654","messageId":"CAPx1GvfrQNao78WYfadttM=B8iyXKfxYaxTyX5w_MLZt2_bU4w@mail.gmail.com","threadId":"64668","inReplyTo":"20251223113602.63000-1-haraldnordgren@gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Chris Torek","fromEmail":"chris.torek@gmail.com","sentAt":"2025-12-23T12:23:39Z","receivedAt":"2025-12-23T12:23:52Z","isPatch":true,"sender":{"key":"chris.torek@gmail.com","avatar":"https://avatars.githubusercontent.com/u/16826774?v=4"},"body":"On Tue, Dec 23, 2025 at 3:36 AM Harald Nordgren\n<haraldnordgren@gmail.com> wrote:\n> Once the users runs that suggested command\n>\n>         git push --set-upstream origin ahead_of_main_status__tmp2\n>\n> then the 'branch.<name>.merge' and 'branch.<name>.remote' no longer hold the reference to \"upstream/HEAD\".\n\nRight.\n\nAnd, as Junio noted, to:\n\n>> learn the other, the destination of a push of this branch, would\n>> involve poking at remote.pushdefault, branch.<name>.pushRemote,\n>> branch.<name>.remote to find out which remote repository it goes,\n>> and then remote.<remote>.push to find out where this branch goes,\n\nThat is, there are some separate configuration items that can change\nwhere `git push` goes.\n\nUsing `git push --set-upstream` sets ones that affect `git pull`, `git status`,\nand `git push`, but it's possible to set ones that affect only `git push`.\n\nThat still leaves `git status` with the problem you (Harald) have observed,\nso perhaps the path forward is to have `git status` check things like\nbranch.<name>.pushRemote to see if they exist and differ from\nbranch.<name>.remote.\n\nChris\n"},{"id":"532656","messageId":"xmqqtsxhpblq.fsf@gitster.g","threadId":"64668","inReplyTo":"20251223102422.36853-1-haraldnordgren@gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-23T13:32:49Z","receivedAt":"2025-12-23T13:32:51Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> You seem to be using the \"what would a new user get when they clone\n>> the remote (by virtue of their HEAD pointing at that branch)\", but\n>> I am not sure if that is a good way to determine the other remote\n>> thing to compare with.\n>>\n>> Even if one remote branch you pull from (but not push to) has a name\n>> that is not one of those usual ones like 'main', 'master', 'trunk',\n>> 'default'\n>\n> Agreed, this should be as agnostic as possible. \"Default\" might be\n> me using GitHub terminology. However, it seems that 'git\n> symbolic-ref refs/remotes/upstream/HEAD' always produces the\n> desires result, so for the sake of discussion we can call it\n> \"upstream/HEAD\" instead of \"default\".\n\nI didn't exactly question the terminology, but was wondering more\nabout the wisdom of using remotes/*/HEAD.\n\nIf a project uses the same remote repository to maintain its\nmaintenance and development tracks, their HEAD might point at the\n'main' (used for development), but some of your branches you used to\nwork on fixes that can later be merged to the maintenance track, it\nis likely that you'll fork from their 'maint', and while you keep\npolishing your fixes, you may push your 'fix' branch to their 'fix'\nbranch.  Comparing your 'fix' with their 'fix' is what we already\ndo, and it gives two thirds of what you need, but the missing\ncomparison is with their 'maint', not with their HEAD that points at\ntheir 'main'.\n\nThanks.\n"},{"id":"532659","messageId":"20251223140924.69410-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqtsxhpblq.fsf@gitster.g","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-23T14:09:24Z","receivedAt":"2025-12-23T14:09:27Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> If a project uses the same remote repository to maintain its\n> maintenance and development tracks, their HEAD might point at the\n> 'main' (used for development), but some of your branches you used to\n> work on fixes that can later be merged to the maintenance track, it\n> is likely that you'll fork from their 'maint', and while you keep\n> polishing your fixes, you may push your 'fix' branch to their 'fix'\n> branch.  Comparing your 'fix' with their 'fix' is what we already\n> do, and it gives two thirds of what you need, but the missing\n> comparison is with their 'maint', not with their HEAD that points at\n> their 'main'.\n\nThat's a fair point. I am used to trunk-based development, but I recognize\nthat there are other paradigms out there.\n\nOne possibly solution could be to make this adjustable in repo-wide config\n(maybe 'repo.settings.defaultBranch=maint') and when unset it compares to\n'refs/remotes/upstream/HEAD'?\n"},{"id":"532660","messageId":"20251223141857.70087-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"CAPx1GvfrQNao78WYfadttM=B8iyXKfxYaxTyX5w_MLZt2_bU4w@mail.gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-23T14:18:57Z","receivedAt":"2025-12-23T14:19:00Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> That still leaves `git status` with the problem you (Harald) have observed,\n> so perhaps the path forward is to have `git status` check things like\n> branch.<name>.pushRemote to see if they exist and differ from\n> branch.<name>.remote.\n\nI played around with pushRemote and I'm not getting it to do something\nthat's useful for me here. Granted I'm not an expert there so happy to take\nsome hints on how to use it more specifically.\n\nI checked some of my other projects and it seems pushRemote doesn't get\nset, even when working with one fork and one upstream repo:\n\n\t$ git remote -v\n\tHaraldNordgren\tgit@github.com:HaraldNordgren/brew.git (fetch)\n\tHaraldNordgren\tgit@github.com:HaraldNordgren/brew.git (push)\n\torigin\tgit@github.com:Homebrew/brew.git (fetch)\n\torigin\tgit@github.com:Homebrew/brew.git (push)\n\nHarald\n"},{"id":"532661","messageId":"CAPx1Gvesh8dsw39GV5ejVP_mvdFRYGTV3GZqf=fE4rwbasLhZA@mail.gmail.com","threadId":"64668","inReplyTo":"20251223141857.70087-1-haraldnordgren@gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Chris Torek","fromEmail":"chris.torek@gmail.com","sentAt":"2025-12-23T14:22:06Z","receivedAt":"2025-12-23T14:22:20Z","isPatch":true,"sender":{"key":"chris.torek@gmail.com","avatar":"https://avatars.githubusercontent.com/u/16826774?v=4"},"body":"On Tue, Dec 23, 2025 at 6:18 AM Harald Nordgren\n<haraldnordgren@gmail.com> wrote:\n> I played around with pushRemote and I'm not getting it to do something\n> that's useful for me here. Granted I'm not an expert there so happy to take\n> some hints on how to use it more specifically.\n\nI've never actually used it myself. You do have to set it manually\n(with `git config` for instance).\n\nI've never been completely convinced that Git's triangular workflow\nsetups are The Right Way, or even a Good Way. I've always just\ndone everything manually. But they're documented, so they ought\nto work.\n\nChris\n"},{"id":"532665","messageId":"pull.2138.v2.git.git.1766530448.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.git.git.1766451217075.gitgitgadget@gmail.com","subject":"[PATCH v2 0/2] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-23T22:54:06Z","receivedAt":"2025-12-23T22:54:11Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com\n\nHarald Nordgren (2):\n  status: show comparison with upstream default branch\n  Simplify default branch comparison logic\n\n remote.c                 |  83 +++++++++++++\n t/t6040-tracking-info.sh | 246 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 329 insertions(+)\n\n\nbase-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v2\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v1:\n\n 1:  c27a806dc9 ! 1:  a3800aed18 status: show default branch comparison when tracking non-default branch\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    status: show default branch comparison when tracking non-default branch\n     +    status: show comparison with upstream default branch\n      \n     -    When a branch tracks a non-default remote branch (e.g.,\n     -    origin/feature), git status now also displays how the branch\n     -    compares to the default branch (origin/main or upstream/main).\n     -    This helps users understand if their branch has drifted from the\n     -    main development line even when it's in sync with its tracking\n     -    branch.\n     +    \"git status\" on a branch that follows a remote branch compares\n     +    commits on the current branch and the remote-tracking branch it\n     +    builds upon, to show \"ahead\" (i.e. you have built new history,\n     +    while others are not touching it), \"behind\" (i.e. you haven't\n     +    added any work since you were in-sync, while others have added\n     +    their work on the branch), \"diverged\" (i.e. you have commits\n     +    that you haven't pushed out, while others have added commits).\n      \n     -    The comparison is shown as a separate line after the tracking\n     -    branch status:\n     +    When you fork a branch 'feature' from the 'main' branch of the\n     +    remote, but then create 'feature' branch at the remote and push\n     +    there, while you still occasionally pull from or rebase onto\n     +    their 'main', you'd also want to know how much you have diverged\n     +    from 'main', in addition to how your 'feature' and their\n     +    'feature' compares. Currently the comparison with 'main' is not\n     +    given, making it hard to know when to start thinking about\n     +    rebasing onto the upstream default branch.\n     +\n     +    Show two sets of comparison: one with the tracking branch (as\n     +    before), and another with the upstream's default branch (what\n     +    their HEAD points to, typically 'main' or 'master'). The latter\n     +    comparison appears on a separate line after the tracking branch\n     +    status, using the same format:\n          - \"Ahead of 'origin/main' by N commits\" when purely ahead\n          - \"Behind 'origin/main' by N commits\" when purely behind\n          - \"Diverged from 'origin/main' by N commits\" when diverged\n      \n          Example output when tracking a feature branch:\n     -\n              On branch feature\n              Your branch is ahead of 'origin/feature' by 2 commits.\n                (use \"git push\" to publish your local commits)\n      \n              Ahead of 'origin/main' by 5 commits.\n      \n     -    The default branch is determined dynamically by checking:\n     +    The upstream default branch is determined by checking symbolic refs:\n          1. refs/remotes/upstream/HEAD (if upstream remote exists)\n          2. refs/remotes/origin/HEAD (fallback)\n      \n          This works with any default branch name (main, master, develop,\n          etc.) as long as the symbolic ref is configured. The comparison\n          is also shown when the branch is up-to-date with its tracking\n     -    branch but differs from the default branch.\n     +    branch but differs from the upstream default branch.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n       }\n       \n     -+static const char *get_default_remote_ref(char **full_ref_out)\n     ++static char *get_default_remote_ref(char **full_ref_out)\n      +{\n      +\tint flag;\n      +\tconst char *resolved;\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +static int is_default_remote_branch(const char *name)\n      +{\n      +\tchar *default_full = NULL;\n     -+\tconst char *default_short;\n     ++\tchar *default_short;\n      +\tint result = 0;\n      +\n      +\tdefault_short = get_default_remote_ref(&default_full);\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\n      +\tresult = !strcmp(name, default_short);\n      +\n     ++\tfree(default_short);\n      +\tfree(default_full);\n      +\treturn result;\n      +}\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +{\n      +\tint default_ours = 0, default_theirs = 0;\n      +\tchar *default_full = NULL;\n     -+\tconst char *default_short;\n     ++\tchar *default_short;\n      +\n      +\tdefault_short = get_default_remote_ref(&default_full);\n      +\tif (!default_short)\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\n      +\tif (stat_branch_pair(branch_refname, default_full,\n      +\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n     ++\t\tfree(default_short);\n      +\t\tfree(default_full);\n      +\t\treturn;\n      +\t}\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\t\t\tdefault_short, default_ours + default_theirs);\n      +\t}\n      +\n     ++\tfree(default_short);\n      +\tfree(default_full);\n      +}\n      +\n -:  ---------- > 2:  417f2075fb Simplify default branch comparison logic\n\n-- \ngitgitgadget\n"},{"id":"532666","messageId":"a3800aed18e576e42de273c9dae0e25bbe26ac6a.1766530448.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v2.git.git.1766530448.gitgitgadget@gmail.com","subject":"[PATCH v2 1/2] status: show comparison with upstream default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-23T22:54:07Z","receivedAt":"2025-12-23T22:54:13Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\" (i.e. you have built new history,\nwhile others are not touching it), \"behind\" (i.e. you haven't\nadded any work since you were in-sync, while others have added\ntheir work on the branch), \"diverged\" (i.e. you have commits\nthat you haven't pushed out, while others have added commits).\n\nWhen you fork a branch 'feature' from the 'main' branch of the\nremote, but then create 'feature' branch at the remote and push\nthere, while you still occasionally pull from or rebase onto\ntheir 'main', you'd also want to know how much you have diverged\nfrom 'main', in addition to how your 'feature' and their\n'feature' compares. Currently the comparison with 'main' is not\ngiven, making it hard to know when to start thinking about\nrebasing onto the upstream default branch.\n\nShow two sets of comparison: one with the tracking branch (as\nbefore), and another with the upstream's default branch (what\ntheir HEAD points to, typically 'main' or 'master'). The latter\ncomparison appears on a separate line after the tracking branch\nstatus, using the same format:\n- \"Ahead of 'origin/main' by N commits\" when purely ahead\n- \"Behind 'origin/main' by N commits\" when purely behind\n- \"Diverged from 'origin/main' by N commits\" when diverged\n\nExample output when tracking a feature branch:\n    On branch feature\n    Your branch is ahead of 'origin/feature' by 2 commits.\n      (use \"git push\" to publish your local commits)\n\n    Ahead of 'origin/main' by 5 commits.\n\nThe upstream default branch is determined by checking symbolic refs:\n1. refs/remotes/upstream/HEAD (if upstream remote exists)\n2. refs/remotes/origin/HEAD (fallback)\n\nThis works with any default branch name (main, master, develop,\netc.) as long as the symbolic ref is configured. The comparison\nis also shown when the branch is up-to-date with its tracking\nbranch but differs from the upstream default branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 104 +++++++++++++++++\n t/t6040-tracking-info.sh | 246 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 350 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..b2a1e980b1 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,98 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_default_remote_ref(char **full_ref_out)\n+{\n+\tint flag;\n+\tconst char *resolved;\n+\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n+\tint i;\n+\n+\tfor (i = 0; remotes[i]; i++) {\n+\t\tstruct strbuf head_ref = STRBUF_INIT;\n+\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n+\n+\t\tresolved = refs_resolve_ref_unsafe(\n+\t\t\tget_main_ref_store(the_repository),\n+\t\t\thead_ref.buf,\n+\t\t\tRESOLVE_REF_READING,\n+\t\t\tNULL, &flag);\n+\n+\t\tstrbuf_release(&head_ref);\n+\n+\t\tif (resolved && (flag & REF_ISSYMREF)) {\n+\t\t\tif (full_ref_out)\n+\t\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\t\treturn refs_shorten_unambiguous_ref(\n+\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t\t}\n+\t}\n+\n+\treturn NULL;\n+}\n+\n+static int is_default_remote_branch(const char *name)\n+{\n+\tchar *default_full = NULL;\n+\tchar *default_short;\n+\tint result = 0;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn 0;\n+\n+\tresult = !strcmp(name, default_short);\n+\n+\tfree(default_short);\n+\tfree(default_full);\n+\treturn result;\n+}\n+\n+static void format_default_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint default_ours = 0, default_theirs = 0;\n+\tchar *default_full = NULL;\n+\tchar *default_short;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn;\n+\n+\tif (stat_branch_pair(branch_refname, default_full,\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n+\t\tfree(default_short);\n+\t\tfree(default_full);\n+\t\treturn;\n+\t}\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (default_ours > 0 && default_theirs == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   default_ours),\n+\t\t\tdefault_short, default_ours);\n+\t} else if (default_theirs > 0 && default_ours == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   default_theirs),\n+\t\t\tdefault_short, default_theirs);\n+\t} else if (default_ours > 0 && default_theirs > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   default_ours + default_theirs),\n+\t\t\tdefault_short, default_ours + default_theirs);\n+\t}\n+\n+\tfree(default_short);\n+\tfree(default_full);\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2248,6 +2340,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tint show_default_branch_comparison;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2258,6 +2351,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n+\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2269,6 +2365,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2285,6 +2383,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2296,6 +2396,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2310,6 +2412,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n \tfree(base);\n \treturn 1;\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..e2bd48f858 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,6 +21,7 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -292,4 +293,249 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup for ahead of non-main tracking branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature origin/main &&\n+\t\tadvance feature1 &&\n+\t\tgit push origin feature &&\n+\t\tgit checkout -b work --track origin/feature &&\n+\t\tadvance work1 &&\n+\t\tadvance work2\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main >/dev/null &&\n+\t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+Switched to branch '\\''work'\\''\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 >/dev/null &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n+\ttest_grep ! \"Ahead of\" actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b oldfeature &&\n+\t\tadvance oldfeature1 &&\n+\t\tgit push origin oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_newer &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout -b work2 --track origin/oldfeature &&\n+\t\tadvance work2_commit\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2\n+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for diverged from tracked but behind main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b work2b &&\n+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_extra &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2b >/dev/null &&\n+\t\tgit status --long -b | head -6\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2b\n+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n+and have 1 and 1 different commits each, respectively.\n+  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+Behind '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for behind tracked but ahead of main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b feature3 &&\n+\t\tadvance feature3_1 &&\n+\t\tadvance feature3_2 &&\n+\t\tadvance feature3_3 &&\n+\t\tgit push origin feature3 &&\n+\t\tgit checkout -b work3 --track origin/feature3 &&\n+\t\tgit reset --hard HEAD~2\n+\t)\n+'\n+\n+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work3 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work3\n+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n+  (use \"git pull\" to update your local branch)\n+\n+Ahead of '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote preference' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t)\n+'\n+\n+test_expect_success 'status prefers upstream remote over origin for comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''upstream/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''upstream/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote remove upstream &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead2 &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature2\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature3 --track origin/feature\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"532667","messageId":"417f2075fb876cbf5a00cd9877fb867ceddf7f6d.1766530448.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v2.git.git.1766530448.gitgitgadget@gmail.com","subject":"[PATCH v2 2/2] Simplify default branch comparison logic","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-23T22:54:08Z","receivedAt":"2025-12-23T22:54:15Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nThis maintains the same functionality while reducing ref resolution calls\nfrom multiple to one, and eliminating unnecessary memory allocations.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 59 ++++++++++++++++++--------------------------------------\n 1 file changed, 19 insertions(+), 40 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b2a1e980b1..f3831ef3be 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2267,41 +2267,17 @@ static char *get_default_remote_ref(char **full_ref_out)\n \treturn NULL;\n }\n \n-static int is_default_remote_branch(const char *name)\n-{\n-\tchar *default_full = NULL;\n-\tchar *default_short;\n-\tint result = 0;\n-\n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (!default_short)\n-\t\treturn 0;\n-\n-\tresult = !strcmp(name, default_short);\n-\n-\tfree(default_short);\n-\tfree(default_full);\n-\treturn result;\n-}\n-\n static void format_default_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     const char *default_full,\n+\t\t\t\t\t     const char *default_short,\n \t\t\t\t\t     enum ahead_behind_flags abf)\n {\n \tint default_ours = 0, default_theirs = 0;\n-\tchar *default_full = NULL;\n-\tchar *default_short;\n-\n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (!default_short)\n-\t\treturn;\n \n \tif (stat_branch_pair(branch_refname, default_full,\n-\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n-\t\tfree(default_short);\n-\t\tfree(default_full);\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0)\n \t\treturn;\n-\t}\n \n \tstrbuf_addstr(sb, \"\\n\");\n \n@@ -2324,9 +2300,6 @@ static void format_default_branch_comparison(struct strbuf *sb,\n \t\t\t   default_ours + default_theirs),\n \t\t\tdefault_short, default_ours + default_theirs);\n \t}\n-\n-\tfree(default_short);\n-\tfree(default_full);\n }\n \n /*\n@@ -2340,7 +2313,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n-\tint show_default_branch_comparison;\n+\tchar *default_full = NULL;\n+\tchar *default_short = NULL;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2352,7 +2326,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n-\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (default_short && !strcmp(base, default_short)) {\n+\t\tfree(default_short);\n+\t\tfree(default_full);\n+\t\tdefault_short = NULL;\n+\t\tdefault_full = NULL;\n+\t}\n \n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n@@ -2365,8 +2345,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2383,8 +2361,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2396,8 +2372,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2412,10 +2386,15 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n+\n+\tif (default_short && !upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK)\n+\t\tformat_default_branch_comparison(sb, branch->refname, default_full,\n+\t\t\t\t\t\t default_short, abf);\n+\n \tfree(base);\n+\tfree(default_short);\n+\tfree(default_full);\n \treturn 1;\n }\n \n-- \ngitgitgadget\n"},{"id":"532668","messageId":"CAHTeOx_kSX7RhVvjjffSK849MMQbjNreqrq=ezHazw0GjMO2Ww@mail.gmail.com","threadId":"64668","inReplyTo":"pull.2138.git.git.1766451217075.gitgitgadget@gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Yee Cheng Chin","fromEmail":"ychin.macvim@gmail.com","sentAt":"2025-12-23T23:11:34Z","receivedAt":"2025-12-23T23:12:12Z","isPatch":true,"sender":{"key":"ychin.macvim@gmail.com","avatar":null},"body":"> The default branch is determined dynamically by checking:\n> 1. refs/remotes/upstream/HEAD (if upstream remote exists)\n> 2. refs/remotes/origin/HEAD (fallback)\n\nI feel like this is making a lot of assumptions regarding remotes.\n\"origin\" and \"upstream\" are not inherently special names for remotes.\nI personally have different Git repositories where they could mean\nslightly different things, and I don't use the \"upstream\" wording\nmyself (I sometimes use \"official\" for the upstream branch, and/or\n\"ychin\" for my own fork's remote). Feels like we should not be\nimposing such a hard-coded value when nothing else in Git enforces it.\n\nAlso, when there are multiple remotes, it's not always clear which one\nthe user actually cares about. It's not always clear if they care\nabout the upstream or the downstream remote, of a third one that\nactually matters more.\n\nThis would also work poorly with detached branches (e.g. the popular\n'gh-pages' branches in a lot of repositories), or permanently foked\nbranches like fixed versions (e.g. v2.x legacy branch when the\nsoftware main branch moved to v3.0). Seems like for this to work well\nit would need to be configurable per branch. Even on a repository\nlevel there would likely be lots of edge cases with each branch having\nits unique circumstances.\n"},{"id":"532669","messageId":"20251223235939.45614-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"CAHTeOx_kSX7RhVvjjffSK849MMQbjNreqrq=ezHazw0GjMO2Ww@mail.gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-23T23:59:39Z","receivedAt":"2025-12-23T23:59:42Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"These are very fair points, maybe a reason not to have this feature on by default then.\n\nFor me, I work against clear origin/master, origin/master, origin/develop branches at my dayjob, and when I do open-source there is a default branch to work against. The GitHub 'gh' tool defaults to upstream and origin repos, which is why I chose those. But I agree it might not be right for everyone.\n\nMaybe what I wrote in a previous message about making this configurable via 'repo.settings.defaultBranch' (maybe call it 'repo.settings.statusGoalBranch') would be useful, but having it off by default instead of on by default as I originally suggested.\n\nI feel strongly that it should be able to be set be repo-specific (and globally). Having it only per branch defeats a big part of it. Should be straightforward git config to have a repo-wide rule but still allow disabling it for e.g. 'gh-pages', I hope?\n\n\nHarald\n"},{"id":"532670","messageId":"20251224000040.45727-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v2.git.git.1766530448.gitgitgadget@gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-24T00:00:40Z","receivedAt":"2025-12-24T00:00:43Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"By the way, I'm using my own version of git while working on this, and I'm loving it 😅\n\n\t$ /Users/Harald/git-repos/github.com/git/git/git status\n\tOn branch ahead_of_main_status\n\tYour branch and 'origin/ahead_of_main_status' have diverged,\n\tand have 1 and 1 different commits each, respectively.\n\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n\n\tAhead of 'upstream/master' by 2 commits.\n\n\tnothing to commit, working tree clean\n"},{"id":"532675","messageId":"xmqqy0msogso.fsf@gitster.g","threadId":"64668","inReplyTo":"CAHTeOx_kSX7RhVvjjffSK849MMQbjNreqrq=ezHazw0GjMO2Ww@mail.gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-24T00:38:15Z","receivedAt":"2025-12-24T00:38:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Yee Cheng Chin <ychin.macvim@gmail.com> writes:\n\n>> The default branch is determined dynamically by checking:\n>> 1. refs/remotes/upstream/HEAD (if upstream remote exists)\n>> 2. refs/remotes/origin/HEAD (fallback)\n>\n> I feel like this is making a lot of assumptions regarding remotes.\n> \"origin\" and \"upstream\" are not inherently special names for remotes.\n\nGood point.  There is a mechanism to determine where a branch would\nbe pushed to with \"git push\", and where the new material to update\nthe branch would come from with \"git pull\", and these places need to\nbe considered when doing comparisons.  This series seems to punt on\ndetermining both repository and branch and instead uses a hardcoded\n\"upstream\" (or \"origin\") and \"HEAD\", which is not satisfactory.\n\n> I personally have different Git repositories where they could mean\n> slightly different things, and I don't use the \"upstream\" wording\n> myself (I sometimes use \"official\" for the upstream branch, and/or\n> \"ychin\" for my own fork's remote). Feels like we should not be\n> imposing such a hard-coded value when nothing else in Git enforces it.\n>\n> Also, when there are multiple remotes, it's not always clear which one\n> the user actually cares about. It's not always clear if they care\n> about the upstream or the downstream remote, of a third one that\n> actually matters more.\n>\n> This would also work poorly with detached branches (e.g. the popular\n> 'gh-pages' branches in a lot of repositories), or permanently foked\n> branches like fixed versions (e.g. v2.x legacy branch when the\n> software main branch moved to v3.0). Seems like for this to work well\n> it would need to be configurable per branch. Even on a repository\n> level there would likely be lots of edge cases with each branch having\n> its unique circumstances.\n\nYes, unrelated branches like gh-pages gives us a very good example\nto think about.\n\nThanks.\n"},{"id":"532676","messageId":"CAHTeOx8nrjJVD0U0=BDTUJZNZX0Xs_b4YGbj=WSavWEPEg-9Eg@mail.gmail.com","threadId":"64668","inReplyTo":"xmqqy0msogso.fsf@gitster.g","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Yee Cheng Chin","fromEmail":"ychin.macvim@gmail.com","sentAt":"2025-12-24T00:49:42Z","receivedAt":"2025-12-24T00:50:20Z","isPatch":true,"sender":{"key":"ychin.macvim@gmail.com","avatar":null},"body":"On Tue, Dec 23, 2025 at 4:38 PM Junio C Hamano <gitster@pobox.com> wrote:\n> Good point.  There is a mechanism to determine where a branch would\n> be pushed to with \"git push\", and where the new material to update\n> the branch would come from with \"git pull\", and these places need to\n> be considered when doing comparisons.  This series seems to punt on\n> determining both repository and branch and instead uses a hardcoded\n> \"upstream\" (or \"origin\") and \"HEAD\", which is not satisfactory.\n\nI may be speaking for the author here, but I think the reason why the\nupstream discovery (or alternatively an additional configuration in\nthe repo) feature was suggested is that not everyone uses the\nmechanism you described (which I think is the \"pushRemote\" config).\nIt's true that if you *do* indeed have separate push/pull remotes for\na branch, then both should ideally be shown in `git status`. However,\noftentimes when you set up a feature branch it may not be correct /\ndesirable to set up separate push/pull remotes for those, especially\nif you need to collaborate with people on the feature and therefore do\nneed to pull from the same feature's branch. In this case, I think the\nauthor still wants `git status` to be able to show the diversion from\nthe original upstream master/main.\n\nPersonally I wonder if this feature gets added, people will start\nasking for a list of \"upstream branches\" to be able to be compared to,\nrather than just a fixed upstream/HEAD or origin/HEAD. But maybe I'm\nthinking too far ahead.\n\nBut for example, I maintain MacVim, which is a downstream fork of Vim.\nAs such, \"upstream\" is a fluid concept for me depending on what I'm\ndoing. It could mean deviation between ychin/macvim vs\nmacvim-dev/macvim, or macvim-dev/macvim vs vim/vim. For complicated\nforks, it's feasible someone may ask for the ability to see the branch\ncomparison against multiple branches concurrently.\n"},{"id":"532677","messageId":"CAHTeOx84MS+hWHzXZdG2YTxGc9_5q-t3T3iEf6hVaN3cKvF=Rw@mail.gmail.com","threadId":"64668","inReplyTo":"20251223235939.45614-1-haraldnordgren@gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Yee Cheng Chin","fromEmail":"ychin.macvim@gmail.com","sentAt":"2025-12-24T00:55:31Z","receivedAt":"2025-12-24T00:56:08Z","isPatch":true,"sender":{"key":"ychin.macvim@gmail.com","avatar":null},"body":"> I feel strongly that it should be able to be set be repo-specific (and globally). Having it only per branch defeats a big part of it. Should be straightforward git config to have a repo-wide rule but still allow disabling it for e.g. 'gh-pages', I hope?\n\nI don't have that much of an opinion on per-repo / per-branch\nconfiguration but I just wanted to point out that some branches would\nbenefit from the ability to turn this off. Perhaps it could be a two\nlayered setting that could be both configured per-repo / per-branch\nbut I don't know if this makes it too complicated.\n"},{"id":"532678","messageId":"20251224011207.45987-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqy0msogso.fsf@gitster.g","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-24T01:12:07Z","receivedAt":"2025-12-24T01:12:11Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"I see push and pull remotes mentioned a lot here. And I want to find out how much of the problem that can solve. Let’s say you have two remotes ’upstream’ and ’fork’.\n\nCan push and pull remotes be set up in a way so the user can run these bare commands without and additional args or flags:\n\n    git push # push to fork\n    git pull # pull from upstream\n\nAnd if so, will ’git status’ also show the status against upstream?\n"},{"id":"532679","messageId":"aUtCP5YvE_v9hmgM@fruit.crustytoothpaste.net","threadId":"64668","inReplyTo":"a3800aed18e576e42de273c9dae0e25bbe26ac6a.1766530448.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 1/2] status: show comparison with upstream default branch","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2025-12-24T01:30:39Z","receivedAt":"2025-12-24T01:30:48Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2025-12-23 at 22:54:07, Harald Nordgren via GitGitGadget wrote:\n> +static char *get_default_remote_ref(char **full_ref_out)\n> +{\n> +\tint flag;\n> +\tconst char *resolved;\n> +\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n\nThis should definitely be configurable, not hard-coded.  For instance, I\nhave this in my gitconfig:\n\n    [clone]\n        defaultRemoteName = def\n\nso my default remote name is `def` (short for \"default\", as in the\ndefault place to push) and this code will never work.\n\nAnother reason to have this be configurable is that some projects (like\na company I used to work at) have tooling that demands a specific remote\nnaming convention for tooling to work properly.  In this particular\ncase, `upstream` would have been the right choice, but I also worked at a\ncompany where English was the common language.  Many people may prefer\nto use names suitable to their local language.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"532680","messageId":"xmqqqzskodqe.fsf@gitster.g","threadId":"64668","inReplyTo":"CAHTeOx8nrjJVD0U0=BDTUJZNZX0Xs_b4YGbj=WSavWEPEg-9Eg@mail.gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-24T01:44:25Z","receivedAt":"2025-12-24T01:44:27Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Yee Cheng Chin <ychin.macvim@gmail.com> writes:\n\n> I may be speaking for the author here, but I think the reason why the\n> upstream discovery (or alternatively an additional configuration in\n> the repo) feature was suggested is that not everyone uses the\n> mechanism you described (which I think is the \"pushRemote\" config).\n\nThe mention of pushRemote by Harald is a red-herring.  My point is\nthat branch.<name>.remote tells what remote we interact with (and\npushRemote is used as a fallback), so there is no need to, and it is\nactively wrong to, assume \"upstream\" or \"origin\".  Even if you push\nback to the same repository as you pull from, you may push to a\ndifferent branch there, in which case you do not need pushRemote,\nbut you'd need to learn what configured remote you are using anyway.\n"},{"id":"532681","messageId":"xmqqms38odmx.fsf@gitster.g","threadId":"64668","inReplyTo":"aUtCP5YvE_v9hmgM@fruit.crustytoothpaste.net","subject":"Re: [PATCH v2 1/2] status: show comparison with upstream default branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-24T01:46:30Z","receivedAt":"2025-12-24T01:46:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> On 2025-12-23 at 22:54:07, Harald Nordgren via GitGitGadget wrote:\n>> +static char *get_default_remote_ref(char **full_ref_out)\n>> +{\n>> +\tint flag;\n>> +\tconst char *resolved;\n>> +\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n>\n> This should definitely be configurable, not hard-coded.  For instance, I\n> have this in my gitconfig:\n>\n>     [clone]\n>         defaultRemoteName = def\n>\n> so my default remote name is `def` (short for \"default\", as in the\n> default place to push) and this code will never work.\n\nBut there does not need any new configuration.  \"git pull\" and \"git\npush\" on the branch is equipped to figure out where they pull from\nand push to, so the new feature should be able to figure out these\n(often the same, but not necessarily) repositories the same way.\n"},{"id":"532691","messageId":"pull.2138.v3.git.git.1766568665.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v2.git.git.1766530448.gitgitgadget@gmail.com","subject":"[PATCH v3 0/3] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T09:31:02Z","receivedAt":"2025-12-24T09:31:08Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\n\nHarald Nordgren (3):\n  status: show comparison with upstream default branch\n  Simplify default branch comparison logic\n  Use repo.settings.statusGoalBranch config for status comparison\n\n remote.c                 |  93 ++++++++++++\n t/t6040-tracking-info.sh | 317 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 410 insertions(+)\n\n\nbase-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v3\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v2:\n\n 1:  a3800aed18 = 1:  a3800aed18 status: show comparison with upstream default branch\n 2:  417f2075fb = 2:  417f2075fb Simplify default branch comparison logic\n -:  ---------- > 3:  c9ec5d9610 Use repo.settings.statusGoalBranch config for status comparison\n\n-- \ngitgitgadget\n"},{"id":"532693","messageId":"a3800aed18e576e42de273c9dae0e25bbe26ac6a.1766568665.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v3.git.git.1766568665.gitgitgadget@gmail.com","subject":"[PATCH v3 1/3] status: show comparison with upstream default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T09:31:03Z","receivedAt":"2025-12-24T09:31:09Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\" (i.e. you have built new history,\nwhile others are not touching it), \"behind\" (i.e. you haven't\nadded any work since you were in-sync, while others have added\ntheir work on the branch), \"diverged\" (i.e. you have commits\nthat you haven't pushed out, while others have added commits).\n\nWhen you fork a branch 'feature' from the 'main' branch of the\nremote, but then create 'feature' branch at the remote and push\nthere, while you still occasionally pull from or rebase onto\ntheir 'main', you'd also want to know how much you have diverged\nfrom 'main', in addition to how your 'feature' and their\n'feature' compares. Currently the comparison with 'main' is not\ngiven, making it hard to know when to start thinking about\nrebasing onto the upstream default branch.\n\nShow two sets of comparison: one with the tracking branch (as\nbefore), and another with the upstream's default branch (what\ntheir HEAD points to, typically 'main' or 'master'). The latter\ncomparison appears on a separate line after the tracking branch\nstatus, using the same format:\n- \"Ahead of 'origin/main' by N commits\" when purely ahead\n- \"Behind 'origin/main' by N commits\" when purely behind\n- \"Diverged from 'origin/main' by N commits\" when diverged\n\nExample output when tracking a feature branch:\n    On branch feature\n    Your branch is ahead of 'origin/feature' by 2 commits.\n      (use \"git push\" to publish your local commits)\n\n    Ahead of 'origin/main' by 5 commits.\n\nThe upstream default branch is determined by checking symbolic refs:\n1. refs/remotes/upstream/HEAD (if upstream remote exists)\n2. refs/remotes/origin/HEAD (fallback)\n\nThis works with any default branch name (main, master, develop,\netc.) as long as the symbolic ref is configured. The comparison\nis also shown when the branch is up-to-date with its tracking\nbranch but differs from the upstream default branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 104 +++++++++++++++++\n t/t6040-tracking-info.sh | 246 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 350 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..b2a1e980b1 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,98 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_default_remote_ref(char **full_ref_out)\n+{\n+\tint flag;\n+\tconst char *resolved;\n+\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n+\tint i;\n+\n+\tfor (i = 0; remotes[i]; i++) {\n+\t\tstruct strbuf head_ref = STRBUF_INIT;\n+\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n+\n+\t\tresolved = refs_resolve_ref_unsafe(\n+\t\t\tget_main_ref_store(the_repository),\n+\t\t\thead_ref.buf,\n+\t\t\tRESOLVE_REF_READING,\n+\t\t\tNULL, &flag);\n+\n+\t\tstrbuf_release(&head_ref);\n+\n+\t\tif (resolved && (flag & REF_ISSYMREF)) {\n+\t\t\tif (full_ref_out)\n+\t\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\t\treturn refs_shorten_unambiguous_ref(\n+\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t\t}\n+\t}\n+\n+\treturn NULL;\n+}\n+\n+static int is_default_remote_branch(const char *name)\n+{\n+\tchar *default_full = NULL;\n+\tchar *default_short;\n+\tint result = 0;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn 0;\n+\n+\tresult = !strcmp(name, default_short);\n+\n+\tfree(default_short);\n+\tfree(default_full);\n+\treturn result;\n+}\n+\n+static void format_default_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint default_ours = 0, default_theirs = 0;\n+\tchar *default_full = NULL;\n+\tchar *default_short;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn;\n+\n+\tif (stat_branch_pair(branch_refname, default_full,\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n+\t\tfree(default_short);\n+\t\tfree(default_full);\n+\t\treturn;\n+\t}\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (default_ours > 0 && default_theirs == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   default_ours),\n+\t\t\tdefault_short, default_ours);\n+\t} else if (default_theirs > 0 && default_ours == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   default_theirs),\n+\t\t\tdefault_short, default_theirs);\n+\t} else if (default_ours > 0 && default_theirs > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   default_ours + default_theirs),\n+\t\t\tdefault_short, default_ours + default_theirs);\n+\t}\n+\n+\tfree(default_short);\n+\tfree(default_full);\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2248,6 +2340,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tint show_default_branch_comparison;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2258,6 +2351,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n+\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2269,6 +2365,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2285,6 +2383,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2296,6 +2396,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2310,6 +2412,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n \tfree(base);\n \treturn 1;\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..e2bd48f858 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,6 +21,7 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -292,4 +293,249 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup for ahead of non-main tracking branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature origin/main &&\n+\t\tadvance feature1 &&\n+\t\tgit push origin feature &&\n+\t\tgit checkout -b work --track origin/feature &&\n+\t\tadvance work1 &&\n+\t\tadvance work2\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main >/dev/null &&\n+\t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+Switched to branch '\\''work'\\''\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 >/dev/null &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n+\ttest_grep ! \"Ahead of\" actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b oldfeature &&\n+\t\tadvance oldfeature1 &&\n+\t\tgit push origin oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_newer &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout -b work2 --track origin/oldfeature &&\n+\t\tadvance work2_commit\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2\n+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for diverged from tracked but behind main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b work2b &&\n+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_extra &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2b >/dev/null &&\n+\t\tgit status --long -b | head -6\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2b\n+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n+and have 1 and 1 different commits each, respectively.\n+  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+Behind '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for behind tracked but ahead of main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b feature3 &&\n+\t\tadvance feature3_1 &&\n+\t\tadvance feature3_2 &&\n+\t\tadvance feature3_3 &&\n+\t\tgit push origin feature3 &&\n+\t\tgit checkout -b work3 --track origin/feature3 &&\n+\t\tgit reset --hard HEAD~2\n+\t)\n+'\n+\n+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work3 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work3\n+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n+  (use \"git pull\" to update your local branch)\n+\n+Ahead of '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote preference' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t)\n+'\n+\n+test_expect_success 'status prefers upstream remote over origin for comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''upstream/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''upstream/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote remove upstream &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead2 &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature2\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature3 --track origin/feature\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"532692","messageId":"417f2075fb876cbf5a00cd9877fb867ceddf7f6d.1766568665.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v3.git.git.1766568665.gitgitgadget@gmail.com","subject":"[PATCH v3 2/3] Simplify default branch comparison logic","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T09:31:04Z","receivedAt":"2025-12-24T09:31:11Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nThis maintains the same functionality while reducing ref resolution calls\nfrom multiple to one, and eliminating unnecessary memory allocations.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 59 ++++++++++++++++++--------------------------------------\n 1 file changed, 19 insertions(+), 40 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b2a1e980b1..f3831ef3be 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2267,41 +2267,17 @@ static char *get_default_remote_ref(char **full_ref_out)\n \treturn NULL;\n }\n \n-static int is_default_remote_branch(const char *name)\n-{\n-\tchar *default_full = NULL;\n-\tchar *default_short;\n-\tint result = 0;\n-\n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (!default_short)\n-\t\treturn 0;\n-\n-\tresult = !strcmp(name, default_short);\n-\n-\tfree(default_short);\n-\tfree(default_full);\n-\treturn result;\n-}\n-\n static void format_default_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     const char *default_full,\n+\t\t\t\t\t     const char *default_short,\n \t\t\t\t\t     enum ahead_behind_flags abf)\n {\n \tint default_ours = 0, default_theirs = 0;\n-\tchar *default_full = NULL;\n-\tchar *default_short;\n-\n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (!default_short)\n-\t\treturn;\n \n \tif (stat_branch_pair(branch_refname, default_full,\n-\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n-\t\tfree(default_short);\n-\t\tfree(default_full);\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0)\n \t\treturn;\n-\t}\n \n \tstrbuf_addstr(sb, \"\\n\");\n \n@@ -2324,9 +2300,6 @@ static void format_default_branch_comparison(struct strbuf *sb,\n \t\t\t   default_ours + default_theirs),\n \t\t\tdefault_short, default_ours + default_theirs);\n \t}\n-\n-\tfree(default_short);\n-\tfree(default_full);\n }\n \n /*\n@@ -2340,7 +2313,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n-\tint show_default_branch_comparison;\n+\tchar *default_full = NULL;\n+\tchar *default_short = NULL;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2352,7 +2326,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n-\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (default_short && !strcmp(base, default_short)) {\n+\t\tfree(default_short);\n+\t\tfree(default_full);\n+\t\tdefault_short = NULL;\n+\t\tdefault_full = NULL;\n+\t}\n \n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n@@ -2365,8 +2345,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2383,8 +2361,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2396,8 +2372,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2412,10 +2386,15 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n+\n+\tif (default_short && !upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK)\n+\t\tformat_default_branch_comparison(sb, branch->refname, default_full,\n+\t\t\t\t\t\t default_short, abf);\n+\n \tfree(base);\n+\tfree(default_short);\n+\tfree(default_full);\n \treturn 1;\n }\n \n-- \ngitgitgadget\n\n"},{"id":"532694","messageId":"c9ec5d9610a024f18866683863127564d7d05a83.1766568665.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v3.git.git.1766568665.gitgitgadget@gmail.com","subject":"[PATCH v3 3/3] Use repo.settings.statusGoalBranch config for status comparison","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T09:31:05Z","receivedAt":"2025-12-24T09:31:12Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nReplace hardcoded upstream/origin preference with configurable\nrepo.settings.statusGoalBranch setting. When unset, skip the\ndefault branch comparison entirely.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 48 ++++++++++++++++-----------\n t/t6040-tracking-info.sh | 71 ++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 100 insertions(+), 19 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex f3831ef3be..edb6d374e7 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2239,32 +2239,42 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \n static char *get_default_remote_ref(char **full_ref_out)\n {\n-\tint flag;\n+\tconst char *config_value;\n \tconst char *resolved;\n-\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n-\tint i;\n+\tint flag;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *slash_pos;\n+\tchar *ret = NULL;\n \n-\tfor (i = 0; remotes[i]; i++) {\n-\t\tstruct strbuf head_ref = STRBUF_INIT;\n-\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n+\tif (repo_config_get_value(the_repository, \"repo.settings.statusGoalBranch\", &config_value))\n+\t\treturn NULL;\n \n-\t\tresolved = refs_resolve_ref_unsafe(\n-\t\t\tget_main_ref_store(the_repository),\n-\t\t\thead_ref.buf,\n-\t\t\tRESOLVE_REF_READING,\n-\t\t\tNULL, &flag);\n+\tif (!config_value || !*config_value)\n+\t\treturn NULL;\n \n-\t\tstrbuf_release(&head_ref);\n+\tslash_pos = strchr(config_value, '/');\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1])\n+\t\treturn NULL;\n \n-\t\tif (resolved && (flag & REF_ISSYMREF)) {\n-\t\t\tif (full_ref_out)\n-\t\t\t\t*full_ref_out = xstrdup(resolved);\n-\t\t\treturn refs_shorten_unambiguous_ref(\n-\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n-\t\t}\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n+\t\t    (int)(slash_pos - config_value), config_value,\n+\t\t    slash_pos + 1);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, &flag);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n \t}\n \n-\treturn NULL;\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n }\n \n static void format_default_branch_comparison(struct strbuf *sb,\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex e2bd48f858..00dadc03e7 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -309,6 +309,7 @@ test_expect_success 'status shows ahead of both tracked branch and origin/main'\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -325,6 +326,7 @@ test_expect_success 'checkout shows ahead of both tracked branch and origin/main\n \t(\n \t\tcd test &&\n \t\tgit checkout main >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -364,6 +366,7 @@ test_expect_success 'status shows ahead of tracked and diverged from origin/main\n \t(\n \t\tcd test &&\n \t\tgit checkout work2 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -393,6 +396,7 @@ test_expect_success 'status shows diverged from tracked and behind origin/main'\n \t(\n \t\tcd test &&\n \t\tgit checkout work2b >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -6\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -425,6 +429,7 @@ test_expect_success 'status shows behind tracked and ahead of origin/main' '\n \t(\n \t\tcd test &&\n \t\tgit checkout work3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -450,6 +455,7 @@ test_expect_success 'status prefers upstream remote over origin for comparison'\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch upstream/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -477,6 +483,7 @@ test_expect_success 'status shows up to date with tracked but diverged from defa\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch upstream/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -504,6 +511,7 @@ test_expect_success 'status shows up to date with tracked but diverged from orig\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -527,6 +535,7 @@ test_expect_success 'status shows up to date with tracked but shows default bran\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -538,4 +547,66 @@ EOF\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status with repo.settings.statusGoalBranch unset shows no default comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config --unset repo.settings.statusGoalBranch 2>/dev/null || true &&\n+\t\tgit status --long -b | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set uses configured branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set to different remote/branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/feature &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set to non-existent branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/nonexistent &&\n+\t\tgit status --long -b | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"532695","messageId":"pull.2138.v4.git.git.1766571587.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v3.git.git.1766568665.gitgitgadget@gmail.com","subject":"[PATCH v4 0/4] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:19:43Z","receivedAt":"2025-12-24T10:19:50Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\n\nHarald Nordgren (4):\n  status: show comparison with upstream default branch\n  Simplify default branch comparison logic\n  Use repo.settings.statusGoalBranch config for status comparison\n  Rename default_remote to goal_branch\n\n remote.c                 |  92 ++++++++++++\n t/t6040-tracking-info.sh | 317 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 409 insertions(+)\n\n\nbase-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v4\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v3:\n\n 1:  a3800aed18 = 1:  a3800aed18 status: show comparison with upstream default branch\n 2:  417f2075fb = 2:  417f2075fb Simplify default branch comparison logic\n 3:  c9ec5d9610 = 3:  c9ec5d9610 Use repo.settings.statusGoalBranch config for status comparison\n -:  ---------- > 4:  0e308141da Rename default_remote to goal_branch\n\n-- \ngitgitgadget\n"},{"id":"532699","messageId":"a3800aed18e576e42de273c9dae0e25bbe26ac6a.1766571587.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v4.git.git.1766571587.gitgitgadget@gmail.com","subject":"[PATCH v4 1/4] status: show comparison with upstream default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:19:44Z","receivedAt":"2025-12-24T10:19:51Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\" (i.e. you have built new history,\nwhile others are not touching it), \"behind\" (i.e. you haven't\nadded any work since you were in-sync, while others have added\ntheir work on the branch), \"diverged\" (i.e. you have commits\nthat you haven't pushed out, while others have added commits).\n\nWhen you fork a branch 'feature' from the 'main' branch of the\nremote, but then create 'feature' branch at the remote and push\nthere, while you still occasionally pull from or rebase onto\ntheir 'main', you'd also want to know how much you have diverged\nfrom 'main', in addition to how your 'feature' and their\n'feature' compares. Currently the comparison with 'main' is not\ngiven, making it hard to know when to start thinking about\nrebasing onto the upstream default branch.\n\nShow two sets of comparison: one with the tracking branch (as\nbefore), and another with the upstream's default branch (what\ntheir HEAD points to, typically 'main' or 'master'). The latter\ncomparison appears on a separate line after the tracking branch\nstatus, using the same format:\n- \"Ahead of 'origin/main' by N commits\" when purely ahead\n- \"Behind 'origin/main' by N commits\" when purely behind\n- \"Diverged from 'origin/main' by N commits\" when diverged\n\nExample output when tracking a feature branch:\n    On branch feature\n    Your branch is ahead of 'origin/feature' by 2 commits.\n      (use \"git push\" to publish your local commits)\n\n    Ahead of 'origin/main' by 5 commits.\n\nThe upstream default branch is determined by checking symbolic refs:\n1. refs/remotes/upstream/HEAD (if upstream remote exists)\n2. refs/remotes/origin/HEAD (fallback)\n\nThis works with any default branch name (main, master, develop,\netc.) as long as the symbolic ref is configured. The comparison\nis also shown when the branch is up-to-date with its tracking\nbranch but differs from the upstream default branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 104 +++++++++++++++++\n t/t6040-tracking-info.sh | 246 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 350 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..b2a1e980b1 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,98 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_default_remote_ref(char **full_ref_out)\n+{\n+\tint flag;\n+\tconst char *resolved;\n+\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n+\tint i;\n+\n+\tfor (i = 0; remotes[i]; i++) {\n+\t\tstruct strbuf head_ref = STRBUF_INIT;\n+\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n+\n+\t\tresolved = refs_resolve_ref_unsafe(\n+\t\t\tget_main_ref_store(the_repository),\n+\t\t\thead_ref.buf,\n+\t\t\tRESOLVE_REF_READING,\n+\t\t\tNULL, &flag);\n+\n+\t\tstrbuf_release(&head_ref);\n+\n+\t\tif (resolved && (flag & REF_ISSYMREF)) {\n+\t\t\tif (full_ref_out)\n+\t\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\t\treturn refs_shorten_unambiguous_ref(\n+\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t\t}\n+\t}\n+\n+\treturn NULL;\n+}\n+\n+static int is_default_remote_branch(const char *name)\n+{\n+\tchar *default_full = NULL;\n+\tchar *default_short;\n+\tint result = 0;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn 0;\n+\n+\tresult = !strcmp(name, default_short);\n+\n+\tfree(default_short);\n+\tfree(default_full);\n+\treturn result;\n+}\n+\n+static void format_default_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint default_ours = 0, default_theirs = 0;\n+\tchar *default_full = NULL;\n+\tchar *default_short;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn;\n+\n+\tif (stat_branch_pair(branch_refname, default_full,\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n+\t\tfree(default_short);\n+\t\tfree(default_full);\n+\t\treturn;\n+\t}\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (default_ours > 0 && default_theirs == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   default_ours),\n+\t\t\tdefault_short, default_ours);\n+\t} else if (default_theirs > 0 && default_ours == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   default_theirs),\n+\t\t\tdefault_short, default_theirs);\n+\t} else if (default_ours > 0 && default_theirs > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   default_ours + default_theirs),\n+\t\t\tdefault_short, default_ours + default_theirs);\n+\t}\n+\n+\tfree(default_short);\n+\tfree(default_full);\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2248,6 +2340,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tint show_default_branch_comparison;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2258,6 +2351,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n+\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2269,6 +2365,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2285,6 +2383,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2296,6 +2396,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2310,6 +2412,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n \tfree(base);\n \treturn 1;\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..e2bd48f858 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,6 +21,7 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -292,4 +293,249 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup for ahead of non-main tracking branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature origin/main &&\n+\t\tadvance feature1 &&\n+\t\tgit push origin feature &&\n+\t\tgit checkout -b work --track origin/feature &&\n+\t\tadvance work1 &&\n+\t\tadvance work2\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main >/dev/null &&\n+\t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+Switched to branch '\\''work'\\''\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 >/dev/null &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n+\ttest_grep ! \"Ahead of\" actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b oldfeature &&\n+\t\tadvance oldfeature1 &&\n+\t\tgit push origin oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_newer &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout -b work2 --track origin/oldfeature &&\n+\t\tadvance work2_commit\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2\n+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for diverged from tracked but behind main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b work2b &&\n+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_extra &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2b >/dev/null &&\n+\t\tgit status --long -b | head -6\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2b\n+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n+and have 1 and 1 different commits each, respectively.\n+  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+Behind '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for behind tracked but ahead of main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b feature3 &&\n+\t\tadvance feature3_1 &&\n+\t\tadvance feature3_2 &&\n+\t\tadvance feature3_3 &&\n+\t\tgit push origin feature3 &&\n+\t\tgit checkout -b work3 --track origin/feature3 &&\n+\t\tgit reset --hard HEAD~2\n+\t)\n+'\n+\n+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work3 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work3\n+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n+  (use \"git pull\" to update your local branch)\n+\n+Ahead of '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote preference' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t)\n+'\n+\n+test_expect_success 'status prefers upstream remote over origin for comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''upstream/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''upstream/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote remove upstream &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead2 &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature2\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature3 --track origin/feature\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"532696","messageId":"417f2075fb876cbf5a00cd9877fb867ceddf7f6d.1766571587.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v4.git.git.1766571587.gitgitgadget@gmail.com","subject":"[PATCH v4 2/4] Simplify default branch comparison logic","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:19:45Z","receivedAt":"2025-12-24T10:19:52Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nThis maintains the same functionality while reducing ref resolution calls\nfrom multiple to one, and eliminating unnecessary memory allocations.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 59 ++++++++++++++++++--------------------------------------\n 1 file changed, 19 insertions(+), 40 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b2a1e980b1..f3831ef3be 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2267,41 +2267,17 @@ static char *get_default_remote_ref(char **full_ref_out)\n \treturn NULL;\n }\n \n-static int is_default_remote_branch(const char *name)\n-{\n-\tchar *default_full = NULL;\n-\tchar *default_short;\n-\tint result = 0;\n-\n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (!default_short)\n-\t\treturn 0;\n-\n-\tresult = !strcmp(name, default_short);\n-\n-\tfree(default_short);\n-\tfree(default_full);\n-\treturn result;\n-}\n-\n static void format_default_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     const char *default_full,\n+\t\t\t\t\t     const char *default_short,\n \t\t\t\t\t     enum ahead_behind_flags abf)\n {\n \tint default_ours = 0, default_theirs = 0;\n-\tchar *default_full = NULL;\n-\tchar *default_short;\n-\n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (!default_short)\n-\t\treturn;\n \n \tif (stat_branch_pair(branch_refname, default_full,\n-\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n-\t\tfree(default_short);\n-\t\tfree(default_full);\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0)\n \t\treturn;\n-\t}\n \n \tstrbuf_addstr(sb, \"\\n\");\n \n@@ -2324,9 +2300,6 @@ static void format_default_branch_comparison(struct strbuf *sb,\n \t\t\t   default_ours + default_theirs),\n \t\t\tdefault_short, default_ours + default_theirs);\n \t}\n-\n-\tfree(default_short);\n-\tfree(default_full);\n }\n \n /*\n@@ -2340,7 +2313,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n-\tint show_default_branch_comparison;\n+\tchar *default_full = NULL;\n+\tchar *default_short = NULL;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2352,7 +2326,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n-\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (default_short && !strcmp(base, default_short)) {\n+\t\tfree(default_short);\n+\t\tfree(default_full);\n+\t\tdefault_short = NULL;\n+\t\tdefault_full = NULL;\n+\t}\n \n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n@@ -2365,8 +2345,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2383,8 +2361,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2396,8 +2372,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2412,10 +2386,15 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n+\n+\tif (default_short && !upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK)\n+\t\tformat_default_branch_comparison(sb, branch->refname, default_full,\n+\t\t\t\t\t\t default_short, abf);\n+\n \tfree(base);\n+\tfree(default_short);\n+\tfree(default_full);\n \treturn 1;\n }\n \n-- \ngitgitgadget\n\n"},{"id":"532697","messageId":"c9ec5d9610a024f18866683863127564d7d05a83.1766571587.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v4.git.git.1766571587.gitgitgadget@gmail.com","subject":"[PATCH v4 3/4] Use repo.settings.statusGoalBranch config for status comparison","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:19:46Z","receivedAt":"2025-12-24T10:19:53Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nReplace hardcoded upstream/origin preference with configurable\nrepo.settings.statusGoalBranch setting. When unset, skip the\ndefault branch comparison entirely.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 48 ++++++++++++++++-----------\n t/t6040-tracking-info.sh | 71 ++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 100 insertions(+), 19 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex f3831ef3be..edb6d374e7 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2239,32 +2239,42 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \n static char *get_default_remote_ref(char **full_ref_out)\n {\n-\tint flag;\n+\tconst char *config_value;\n \tconst char *resolved;\n-\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n-\tint i;\n+\tint flag;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *slash_pos;\n+\tchar *ret = NULL;\n \n-\tfor (i = 0; remotes[i]; i++) {\n-\t\tstruct strbuf head_ref = STRBUF_INIT;\n-\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n+\tif (repo_config_get_value(the_repository, \"repo.settings.statusGoalBranch\", &config_value))\n+\t\treturn NULL;\n \n-\t\tresolved = refs_resolve_ref_unsafe(\n-\t\t\tget_main_ref_store(the_repository),\n-\t\t\thead_ref.buf,\n-\t\t\tRESOLVE_REF_READING,\n-\t\t\tNULL, &flag);\n+\tif (!config_value || !*config_value)\n+\t\treturn NULL;\n \n-\t\tstrbuf_release(&head_ref);\n+\tslash_pos = strchr(config_value, '/');\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1])\n+\t\treturn NULL;\n \n-\t\tif (resolved && (flag & REF_ISSYMREF)) {\n-\t\t\tif (full_ref_out)\n-\t\t\t\t*full_ref_out = xstrdup(resolved);\n-\t\t\treturn refs_shorten_unambiguous_ref(\n-\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n-\t\t}\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n+\t\t    (int)(slash_pos - config_value), config_value,\n+\t\t    slash_pos + 1);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, &flag);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n \t}\n \n-\treturn NULL;\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n }\n \n static void format_default_branch_comparison(struct strbuf *sb,\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex e2bd48f858..00dadc03e7 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -309,6 +309,7 @@ test_expect_success 'status shows ahead of both tracked branch and origin/main'\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -325,6 +326,7 @@ test_expect_success 'checkout shows ahead of both tracked branch and origin/main\n \t(\n \t\tcd test &&\n \t\tgit checkout main >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -364,6 +366,7 @@ test_expect_success 'status shows ahead of tracked and diverged from origin/main\n \t(\n \t\tcd test &&\n \t\tgit checkout work2 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -393,6 +396,7 @@ test_expect_success 'status shows diverged from tracked and behind origin/main'\n \t(\n \t\tcd test &&\n \t\tgit checkout work2b >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -6\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -425,6 +429,7 @@ test_expect_success 'status shows behind tracked and ahead of origin/main' '\n \t(\n \t\tcd test &&\n \t\tgit checkout work3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -450,6 +455,7 @@ test_expect_success 'status prefers upstream remote over origin for comparison'\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch upstream/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -477,6 +483,7 @@ test_expect_success 'status shows up to date with tracked but diverged from defa\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch upstream/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -504,6 +511,7 @@ test_expect_success 'status shows up to date with tracked but diverged from orig\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -527,6 +535,7 @@ test_expect_success 'status shows up to date with tracked but shows default bran\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -538,4 +547,66 @@ EOF\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status with repo.settings.statusGoalBranch unset shows no default comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config --unset repo.settings.statusGoalBranch 2>/dev/null || true &&\n+\t\tgit status --long -b | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set uses configured branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set to different remote/branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/feature &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set to non-existent branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/nonexistent &&\n+\t\tgit status --long -b | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"532698","messageId":"0e308141da0b639ba4ba15c640b8e4260689a42a.1766571587.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v4.git.git.1766571587.gitgitgadget@gmail.com","subject":"[PATCH v4 4/4] Rename default_remote to goal_branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:19:47Z","receivedAt":"2025-12-24T10:19:55Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRename variables and functions from 'default' to 'goal' to better\nreflect that they represent the goal branch from config rather than\na default remote. Also move the goal branch lookup to where it's used.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 63 ++++++++++++++++++++++++++++----------------------------\n 1 file changed, 31 insertions(+), 32 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex edb6d374e7..99c0e18df4 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,7 +2237,7 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-static char *get_default_remote_ref(char **full_ref_out)\n+static char *get_goal_branch_ref(char **full_ref_out)\n {\n \tconst char *config_value;\n \tconst char *resolved;\n@@ -2253,8 +2253,11 @@ static char *get_default_remote_ref(char **full_ref_out)\n \t\treturn NULL;\n \n \tslash_pos = strchr(config_value, '/');\n-\tif (!slash_pos || slash_pos == config_value || !slash_pos[1])\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n+\t\twarning(_(\"invalid value for repo.settings.statusGoalBranch: '%s' (expected format: remote/branch)\"),\n+\t\t\tconfig_value);\n \t\treturn NULL;\n+\t}\n \n \tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n \t\t    (int)(slash_pos - config_value), config_value,\n@@ -2277,38 +2280,38 @@ static char *get_default_remote_ref(char **full_ref_out)\n \treturn ret;\n }\n \n-static void format_default_branch_comparison(struct strbuf *sb,\n+static void format_goal_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t     const char *branch_refname,\n-\t\t\t\t\t     const char *default_full,\n-\t\t\t\t\t     const char *default_short,\n+\t\t\t\t\t     const char *goal_full,\n+\t\t\t\t\t     const char *goal_short,\n \t\t\t\t\t     enum ahead_behind_flags abf)\n {\n-\tint default_ours = 0, default_theirs = 0;\n+\tint goal_ahead = 0, goal_behind = 0;\n \n-\tif (stat_branch_pair(branch_refname, default_full,\n-\t\t\t     &default_ours, &default_theirs, abf) <= 0)\n+\tif (stat_branch_pair(branch_refname, goal_full,\n+\t\t\t     &goal_ahead, &goal_behind, abf) <= 0)\n \t\treturn;\n \n \tstrbuf_addstr(sb, \"\\n\");\n \n-\tif (default_ours > 0 && default_theirs == 0) {\n+\tif (goal_ahead > 0 && goal_behind == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n-\t\t\t   default_ours),\n-\t\t\tdefault_short, default_ours);\n-\t} else if (default_theirs > 0 && default_ours == 0) {\n+\t\t\t   goal_ahead),\n+\t\t\tgoal_short, goal_ahead);\n+\t} else if (goal_behind > 0 && goal_ahead == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n \t\t\t   \"Behind '%s' by %d commits.\\n\",\n-\t\t\t   default_theirs),\n-\t\t\tdefault_short, default_theirs);\n-\t} else if (default_ours > 0 && default_theirs > 0) {\n+\t\t\t   goal_behind),\n+\t\t\tgoal_short, goal_behind);\n+\t} else if (goal_ahead > 0 && goal_behind > 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n \t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n-\t\t\t   default_ours + default_theirs),\n-\t\t\tdefault_short, default_ours + default_theirs);\n+\t\t\t   goal_ahead + goal_behind),\n+\t\t\tgoal_short, goal_ahead + goal_behind);\n \t}\n }\n \n@@ -2323,8 +2326,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n-\tchar *default_full = NULL;\n-\tchar *default_short = NULL;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2336,14 +2337,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (default_short && !strcmp(base, default_short)) {\n-\t\tfree(default_short);\n-\t\tfree(default_full);\n-\t\tdefault_short = NULL;\n-\t\tdefault_full = NULL;\n-\t}\n-\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2398,13 +2391,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n \n-\tif (default_short && !upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK)\n-\t\tformat_default_branch_comparison(sb, branch->refname, default_full,\n-\t\t\t\t\t\t default_short, abf);\n+\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tchar *goal_full = NULL;\n+\t\tchar *goal_short = get_goal_branch_ref(&goal_full);\n+\n+\t\tif (goal_short && strcmp(base, goal_short))\n+\t\t\tformat_goal_branch_comparison(sb, branch->refname, goal_full,\n+\t\t\t\t\t\t     goal_short, abf);\n+\n+\t\tfree(goal_short);\n+\t\tfree(goal_full);\n+\t}\n \n \tfree(base);\n-\tfree(default_short);\n-\tfree(default_full);\n \treturn 1;\n }\n \n-- \ngitgitgadget\n"},{"id":"532700","messageId":"20251224102440.66208-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqqzskodqe.fsf@gitster.g","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-24T10:24:40Z","receivedAt":"2025-12-24T10:24:43Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> This series seems to punt on\n> determining both repository and branch and instead uses a hardcoded\n> \"upstream\" (or \"origin\") and \"HEAD\", which is not satisfactory.\n\nI have made a change to the code to not assume \"upstream\" or \"origin\" as remote names. Now there is a config setting 'repo.settings.statusGoalBranch' to enable this the check.\n\nI side-stepped the discussion about pushRemotes since what Yee Cheng Chin brought up seems to kill the possibility of determining comparison branch dynamically. Let me know what you think of the latest code and I can handle it if needed 🤗\n\n> I don't have that much of an opinion on per-repo / per-branch\n> configuration but I just wanted to point out that some branches would\n> benefit from the ability to turn this off. Perhaps it could be a two\n> layered setting that could be both configured per-repo / per-branch\n> but I don't know if this makes it too complicated.\n\nI skipped this for now because I couldn't find a precedent in the sourc code for config settings that are \"on repo-wide, but turned off for some branches\". I could dig into this if we need it. I would like to hear Junio C Hamano's thoughts on this specific thing too 🤗\n\n\nHarald\n"},{"id":"532701","messageId":"pull.2138.v5.git.git.1766572715.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v4.git.git.1766571587.gitgitgadget@gmail.com","subject":"[PATCH v5 0/5] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:38:30Z","receivedAt":"2025-12-24T10:38:38Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\n\nHarald Nordgren (5):\n  status: show comparison with upstream default branch\n  Simplify default branch comparison logic\n  Use repo.settings.statusGoalBranch config for status comparison\n  Rename default_remote to goal_branch\n  Add warning for malformed statusGoalBranch config\n\n remote.c                 |  92 +++++++++++\n t/t6040-tracking-info.sh | 340 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 432 insertions(+)\n\n\nbase-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v5\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v4:\n\n 1:  a3800aed18 = 1:  a3800aed18 status: show comparison with upstream default branch\n 2:  417f2075fb = 2:  417f2075fb Simplify default branch comparison logic\n 3:  c9ec5d9610 = 3:  c9ec5d9610 Use repo.settings.statusGoalBranch config for status comparison\n 4:  0e308141da = 4:  0e308141da Rename default_remote to goal_branch\n -:  ---------- > 5:  441678939f Add warning for malformed statusGoalBranch config\n\n-- \ngitgitgadget\n"},{"id":"532702","messageId":"a3800aed18e576e42de273c9dae0e25bbe26ac6a.1766572715.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v5.git.git.1766572715.gitgitgadget@gmail.com","subject":"[PATCH v5 1/5] status: show comparison with upstream default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:38:31Z","receivedAt":"2025-12-24T10:38:39Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\" (i.e. you have built new history,\nwhile others are not touching it), \"behind\" (i.e. you haven't\nadded any work since you were in-sync, while others have added\ntheir work on the branch), \"diverged\" (i.e. you have commits\nthat you haven't pushed out, while others have added commits).\n\nWhen you fork a branch 'feature' from the 'main' branch of the\nremote, but then create 'feature' branch at the remote and push\nthere, while you still occasionally pull from or rebase onto\ntheir 'main', you'd also want to know how much you have diverged\nfrom 'main', in addition to how your 'feature' and their\n'feature' compares. Currently the comparison with 'main' is not\ngiven, making it hard to know when to start thinking about\nrebasing onto the upstream default branch.\n\nShow two sets of comparison: one with the tracking branch (as\nbefore), and another with the upstream's default branch (what\ntheir HEAD points to, typically 'main' or 'master'). The latter\ncomparison appears on a separate line after the tracking branch\nstatus, using the same format:\n- \"Ahead of 'origin/main' by N commits\" when purely ahead\n- \"Behind 'origin/main' by N commits\" when purely behind\n- \"Diverged from 'origin/main' by N commits\" when diverged\n\nExample output when tracking a feature branch:\n    On branch feature\n    Your branch is ahead of 'origin/feature' by 2 commits.\n      (use \"git push\" to publish your local commits)\n\n    Ahead of 'origin/main' by 5 commits.\n\nThe upstream default branch is determined by checking symbolic refs:\n1. refs/remotes/upstream/HEAD (if upstream remote exists)\n2. refs/remotes/origin/HEAD (fallback)\n\nThis works with any default branch name (main, master, develop,\netc.) as long as the symbolic ref is configured. The comparison\nis also shown when the branch is up-to-date with its tracking\nbranch but differs from the upstream default branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 104 +++++++++++++++++\n t/t6040-tracking-info.sh | 246 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 350 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..b2a1e980b1 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,98 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_default_remote_ref(char **full_ref_out)\n+{\n+\tint flag;\n+\tconst char *resolved;\n+\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n+\tint i;\n+\n+\tfor (i = 0; remotes[i]; i++) {\n+\t\tstruct strbuf head_ref = STRBUF_INIT;\n+\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n+\n+\t\tresolved = refs_resolve_ref_unsafe(\n+\t\t\tget_main_ref_store(the_repository),\n+\t\t\thead_ref.buf,\n+\t\t\tRESOLVE_REF_READING,\n+\t\t\tNULL, &flag);\n+\n+\t\tstrbuf_release(&head_ref);\n+\n+\t\tif (resolved && (flag & REF_ISSYMREF)) {\n+\t\t\tif (full_ref_out)\n+\t\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\t\treturn refs_shorten_unambiguous_ref(\n+\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t\t}\n+\t}\n+\n+\treturn NULL;\n+}\n+\n+static int is_default_remote_branch(const char *name)\n+{\n+\tchar *default_full = NULL;\n+\tchar *default_short;\n+\tint result = 0;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn 0;\n+\n+\tresult = !strcmp(name, default_short);\n+\n+\tfree(default_short);\n+\tfree(default_full);\n+\treturn result;\n+}\n+\n+static void format_default_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint default_ours = 0, default_theirs = 0;\n+\tchar *default_full = NULL;\n+\tchar *default_short;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn;\n+\n+\tif (stat_branch_pair(branch_refname, default_full,\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n+\t\tfree(default_short);\n+\t\tfree(default_full);\n+\t\treturn;\n+\t}\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (default_ours > 0 && default_theirs == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   default_ours),\n+\t\t\tdefault_short, default_ours);\n+\t} else if (default_theirs > 0 && default_ours == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   default_theirs),\n+\t\t\tdefault_short, default_theirs);\n+\t} else if (default_ours > 0 && default_theirs > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   default_ours + default_theirs),\n+\t\t\tdefault_short, default_ours + default_theirs);\n+\t}\n+\n+\tfree(default_short);\n+\tfree(default_full);\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2248,6 +2340,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tint show_default_branch_comparison;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2258,6 +2351,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n+\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2269,6 +2365,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2285,6 +2383,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2296,6 +2396,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2310,6 +2412,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n \tfree(base);\n \treturn 1;\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..e2bd48f858 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,6 +21,7 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -292,4 +293,249 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup for ahead of non-main tracking branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature origin/main &&\n+\t\tadvance feature1 &&\n+\t\tgit push origin feature &&\n+\t\tgit checkout -b work --track origin/feature &&\n+\t\tadvance work1 &&\n+\t\tadvance work2\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main >/dev/null &&\n+\t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+Switched to branch '\\''work'\\''\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 >/dev/null &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n+\ttest_grep ! \"Ahead of\" actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b oldfeature &&\n+\t\tadvance oldfeature1 &&\n+\t\tgit push origin oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_newer &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout -b work2 --track origin/oldfeature &&\n+\t\tadvance work2_commit\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2\n+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for diverged from tracked but behind main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b work2b &&\n+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_extra &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2b >/dev/null &&\n+\t\tgit status --long -b | head -6\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2b\n+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n+and have 1 and 1 different commits each, respectively.\n+  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+Behind '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for behind tracked but ahead of main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b feature3 &&\n+\t\tadvance feature3_1 &&\n+\t\tadvance feature3_2 &&\n+\t\tadvance feature3_3 &&\n+\t\tgit push origin feature3 &&\n+\t\tgit checkout -b work3 --track origin/feature3 &&\n+\t\tgit reset --hard HEAD~2\n+\t)\n+'\n+\n+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work3 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work3\n+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n+  (use \"git pull\" to update your local branch)\n+\n+Ahead of '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote preference' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t)\n+'\n+\n+test_expect_success 'status prefers upstream remote over origin for comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''upstream/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''upstream/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote remove upstream &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead2 &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature2\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature3 --track origin/feature\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"532703","messageId":"417f2075fb876cbf5a00cd9877fb867ceddf7f6d.1766572715.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v5.git.git.1766572715.gitgitgadget@gmail.com","subject":"[PATCH v5 2/5] Simplify default branch comparison logic","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:38:32Z","receivedAt":"2025-12-24T10:38:41Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nThis maintains the same functionality while reducing ref resolution calls\nfrom multiple to one, and eliminating unnecessary memory allocations.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 59 ++++++++++++++++++--------------------------------------\n 1 file changed, 19 insertions(+), 40 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b2a1e980b1..f3831ef3be 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2267,41 +2267,17 @@ static char *get_default_remote_ref(char **full_ref_out)\n \treturn NULL;\n }\n \n-static int is_default_remote_branch(const char *name)\n-{\n-\tchar *default_full = NULL;\n-\tchar *default_short;\n-\tint result = 0;\n-\n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (!default_short)\n-\t\treturn 0;\n-\n-\tresult = !strcmp(name, default_short);\n-\n-\tfree(default_short);\n-\tfree(default_full);\n-\treturn result;\n-}\n-\n static void format_default_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     const char *default_full,\n+\t\t\t\t\t     const char *default_short,\n \t\t\t\t\t     enum ahead_behind_flags abf)\n {\n \tint default_ours = 0, default_theirs = 0;\n-\tchar *default_full = NULL;\n-\tchar *default_short;\n-\n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (!default_short)\n-\t\treturn;\n \n \tif (stat_branch_pair(branch_refname, default_full,\n-\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n-\t\tfree(default_short);\n-\t\tfree(default_full);\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0)\n \t\treturn;\n-\t}\n \n \tstrbuf_addstr(sb, \"\\n\");\n \n@@ -2324,9 +2300,6 @@ static void format_default_branch_comparison(struct strbuf *sb,\n \t\t\t   default_ours + default_theirs),\n \t\t\tdefault_short, default_ours + default_theirs);\n \t}\n-\n-\tfree(default_short);\n-\tfree(default_full);\n }\n \n /*\n@@ -2340,7 +2313,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n-\tint show_default_branch_comparison;\n+\tchar *default_full = NULL;\n+\tchar *default_short = NULL;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2352,7 +2326,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n-\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (default_short && !strcmp(base, default_short)) {\n+\t\tfree(default_short);\n+\t\tfree(default_full);\n+\t\tdefault_short = NULL;\n+\t\tdefault_full = NULL;\n+\t}\n \n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n@@ -2365,8 +2345,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2383,8 +2361,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2396,8 +2372,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2412,10 +2386,15 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n+\n+\tif (default_short && !upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK)\n+\t\tformat_default_branch_comparison(sb, branch->refname, default_full,\n+\t\t\t\t\t\t default_short, abf);\n+\n \tfree(base);\n+\tfree(default_short);\n+\tfree(default_full);\n \treturn 1;\n }\n \n-- \ngitgitgadget\n\n"},{"id":"532704","messageId":"c9ec5d9610a024f18866683863127564d7d05a83.1766572715.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v5.git.git.1766572715.gitgitgadget@gmail.com","subject":"[PATCH v5 3/5] Use repo.settings.statusGoalBranch config for status comparison","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:38:33Z","receivedAt":"2025-12-24T10:38:42Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nReplace hardcoded upstream/origin preference with configurable\nrepo.settings.statusGoalBranch setting. When unset, skip the\ndefault branch comparison entirely.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 48 ++++++++++++++++-----------\n t/t6040-tracking-info.sh | 71 ++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 100 insertions(+), 19 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex f3831ef3be..edb6d374e7 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2239,32 +2239,42 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \n static char *get_default_remote_ref(char **full_ref_out)\n {\n-\tint flag;\n+\tconst char *config_value;\n \tconst char *resolved;\n-\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n-\tint i;\n+\tint flag;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *slash_pos;\n+\tchar *ret = NULL;\n \n-\tfor (i = 0; remotes[i]; i++) {\n-\t\tstruct strbuf head_ref = STRBUF_INIT;\n-\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n+\tif (repo_config_get_value(the_repository, \"repo.settings.statusGoalBranch\", &config_value))\n+\t\treturn NULL;\n \n-\t\tresolved = refs_resolve_ref_unsafe(\n-\t\t\tget_main_ref_store(the_repository),\n-\t\t\thead_ref.buf,\n-\t\t\tRESOLVE_REF_READING,\n-\t\t\tNULL, &flag);\n+\tif (!config_value || !*config_value)\n+\t\treturn NULL;\n \n-\t\tstrbuf_release(&head_ref);\n+\tslash_pos = strchr(config_value, '/');\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1])\n+\t\treturn NULL;\n \n-\t\tif (resolved && (flag & REF_ISSYMREF)) {\n-\t\t\tif (full_ref_out)\n-\t\t\t\t*full_ref_out = xstrdup(resolved);\n-\t\t\treturn refs_shorten_unambiguous_ref(\n-\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n-\t\t}\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n+\t\t    (int)(slash_pos - config_value), config_value,\n+\t\t    slash_pos + 1);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, &flag);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n \t}\n \n-\treturn NULL;\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n }\n \n static void format_default_branch_comparison(struct strbuf *sb,\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex e2bd48f858..00dadc03e7 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -309,6 +309,7 @@ test_expect_success 'status shows ahead of both tracked branch and origin/main'\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -325,6 +326,7 @@ test_expect_success 'checkout shows ahead of both tracked branch and origin/main\n \t(\n \t\tcd test &&\n \t\tgit checkout main >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -364,6 +366,7 @@ test_expect_success 'status shows ahead of tracked and diverged from origin/main\n \t(\n \t\tcd test &&\n \t\tgit checkout work2 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -393,6 +396,7 @@ test_expect_success 'status shows diverged from tracked and behind origin/main'\n \t(\n \t\tcd test &&\n \t\tgit checkout work2b >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -6\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -425,6 +429,7 @@ test_expect_success 'status shows behind tracked and ahead of origin/main' '\n \t(\n \t\tcd test &&\n \t\tgit checkout work3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -450,6 +455,7 @@ test_expect_success 'status prefers upstream remote over origin for comparison'\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch upstream/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -477,6 +483,7 @@ test_expect_success 'status shows up to date with tracked but diverged from defa\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch upstream/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -504,6 +511,7 @@ test_expect_success 'status shows up to date with tracked but diverged from orig\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -527,6 +535,7 @@ test_expect_success 'status shows up to date with tracked but shows default bran\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -538,4 +547,66 @@ EOF\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status with repo.settings.statusGoalBranch unset shows no default comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config --unset repo.settings.statusGoalBranch 2>/dev/null || true &&\n+\t\tgit status --long -b | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set uses configured branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set to different remote/branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/feature &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set to non-existent branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/nonexistent &&\n+\t\tgit status --long -b | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"532705","messageId":"0e308141da0b639ba4ba15c640b8e4260689a42a.1766572715.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v5.git.git.1766572715.gitgitgadget@gmail.com","subject":"[PATCH v5 4/5] Rename default_remote to goal_branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:38:34Z","receivedAt":"2025-12-24T10:38:43Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRename variables and functions from 'default' to 'goal' to better\nreflect that they represent the goal branch from config rather than\na default remote. Also move the goal branch lookup to where it's used.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 63 ++++++++++++++++++++++++++++----------------------------\n 1 file changed, 31 insertions(+), 32 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex edb6d374e7..99c0e18df4 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,7 +2237,7 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-static char *get_default_remote_ref(char **full_ref_out)\n+static char *get_goal_branch_ref(char **full_ref_out)\n {\n \tconst char *config_value;\n \tconst char *resolved;\n@@ -2253,8 +2253,11 @@ static char *get_default_remote_ref(char **full_ref_out)\n \t\treturn NULL;\n \n \tslash_pos = strchr(config_value, '/');\n-\tif (!slash_pos || slash_pos == config_value || !slash_pos[1])\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n+\t\twarning(_(\"invalid value for repo.settings.statusGoalBranch: '%s' (expected format: remote/branch)\"),\n+\t\t\tconfig_value);\n \t\treturn NULL;\n+\t}\n \n \tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n \t\t    (int)(slash_pos - config_value), config_value,\n@@ -2277,38 +2280,38 @@ static char *get_default_remote_ref(char **full_ref_out)\n \treturn ret;\n }\n \n-static void format_default_branch_comparison(struct strbuf *sb,\n+static void format_goal_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t     const char *branch_refname,\n-\t\t\t\t\t     const char *default_full,\n-\t\t\t\t\t     const char *default_short,\n+\t\t\t\t\t     const char *goal_full,\n+\t\t\t\t\t     const char *goal_short,\n \t\t\t\t\t     enum ahead_behind_flags abf)\n {\n-\tint default_ours = 0, default_theirs = 0;\n+\tint goal_ahead = 0, goal_behind = 0;\n \n-\tif (stat_branch_pair(branch_refname, default_full,\n-\t\t\t     &default_ours, &default_theirs, abf) <= 0)\n+\tif (stat_branch_pair(branch_refname, goal_full,\n+\t\t\t     &goal_ahead, &goal_behind, abf) <= 0)\n \t\treturn;\n \n \tstrbuf_addstr(sb, \"\\n\");\n \n-\tif (default_ours > 0 && default_theirs == 0) {\n+\tif (goal_ahead > 0 && goal_behind == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n-\t\t\t   default_ours),\n-\t\t\tdefault_short, default_ours);\n-\t} else if (default_theirs > 0 && default_ours == 0) {\n+\t\t\t   goal_ahead),\n+\t\t\tgoal_short, goal_ahead);\n+\t} else if (goal_behind > 0 && goal_ahead == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n \t\t\t   \"Behind '%s' by %d commits.\\n\",\n-\t\t\t   default_theirs),\n-\t\t\tdefault_short, default_theirs);\n-\t} else if (default_ours > 0 && default_theirs > 0) {\n+\t\t\t   goal_behind),\n+\t\t\tgoal_short, goal_behind);\n+\t} else if (goal_ahead > 0 && goal_behind > 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n \t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n-\t\t\t   default_ours + default_theirs),\n-\t\t\tdefault_short, default_ours + default_theirs);\n+\t\t\t   goal_ahead + goal_behind),\n+\t\t\tgoal_short, goal_ahead + goal_behind);\n \t}\n }\n \n@@ -2323,8 +2326,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n-\tchar *default_full = NULL;\n-\tchar *default_short = NULL;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2336,14 +2337,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (default_short && !strcmp(base, default_short)) {\n-\t\tfree(default_short);\n-\t\tfree(default_full);\n-\t\tdefault_short = NULL;\n-\t\tdefault_full = NULL;\n-\t}\n-\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2398,13 +2391,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n \n-\tif (default_short && !upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK)\n-\t\tformat_default_branch_comparison(sb, branch->refname, default_full,\n-\t\t\t\t\t\t default_short, abf);\n+\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tchar *goal_full = NULL;\n+\t\tchar *goal_short = get_goal_branch_ref(&goal_full);\n+\n+\t\tif (goal_short && strcmp(base, goal_short))\n+\t\t\tformat_goal_branch_comparison(sb, branch->refname, goal_full,\n+\t\t\t\t\t\t     goal_short, abf);\n+\n+\t\tfree(goal_short);\n+\t\tfree(goal_full);\n+\t}\n \n \tfree(base);\n-\tfree(default_short);\n-\tfree(default_full);\n \treturn 1;\n }\n \n-- \ngitgitgadget\n\n"},{"id":"532706","messageId":"441678939f30ba6cc07e7e8d4a41c72d0fd7427f.1766572715.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v5.git.git.1766572715.gitgitgadget@gmail.com","subject":"[PATCH v5 5/5] Add warning for malformed statusGoalBranch config","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T10:38:35Z","receivedAt":"2025-12-24T10:38:44Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nWarn when repo.settings.statusGoalBranch has invalid format\nand skip the goal branch comparison. Also update test to\ncompare full checkout output instead of grepping.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n t/t6040-tracking-info.sh | 49 +++++++++++++++++++++++++++++-----------\n 1 file changed, 36 insertions(+), 13 deletions(-)\n\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 00dadc03e7..598dd89483 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -310,7 +310,7 @@ test_expect_success 'status shows ahead of both tracked branch and origin/main'\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -5\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work\n@@ -318,6 +318,8 @@ Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n   (use \"git push\" to publish your local commits)\n \n Ahead of '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -327,11 +329,13 @@ test_expect_success 'checkout shows ahead of both tracked branch and origin/main\n \t\tcd test &&\n \t\tgit checkout main >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n+\t\tgit checkout work 2>&1\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n Switched to branch '\\''work'\\''\n Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n Ahead of '\\''origin/main'\\'' by 3 commits.\n EOF\n \ttest_cmp expect actual\n@@ -367,7 +371,7 @@ test_expect_success 'status shows ahead of tracked and diverged from origin/main\n \t\tcd test &&\n \t\tgit checkout work2 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -5\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work2\n@@ -375,6 +379,8 @@ Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n   (use \"git push\" to publish your local commits)\n \n Diverged from '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -397,7 +403,7 @@ test_expect_success 'status shows diverged from tracked and behind origin/main'\n \t\tcd test &&\n \t\tgit checkout work2b >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -6\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work2b\n@@ -406,6 +412,8 @@ and have 1 and 1 different commits each, respectively.\n   (use \"git pull\" if you want to integrate the remote branch with yours)\n \n Behind '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -430,7 +438,7 @@ test_expect_success 'status shows behind tracked and ahead of origin/main' '\n \t\tcd test &&\n \t\tgit checkout work3 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -5\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work3\n@@ -438,6 +446,8 @@ Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forw\n   (use \"git pull\" to update your local branch)\n \n Ahead of '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -456,7 +466,7 @@ test_expect_success 'status prefers upstream remote over origin for comparison'\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch upstream/main &&\n-\t\tgit status --long -b | head -5\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work\n@@ -464,6 +474,8 @@ Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n   (use \"git push\" to publish your local commits)\n \n Diverged from '\\''upstream/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -484,13 +496,15 @@ test_expect_success 'status shows up to date with tracked but diverged from defa\n \t\tcd test &&\n \t\tgit checkout synced_feature >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch upstream/main &&\n-\t\tgit status --long -b | head -4\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature\n Your branch is up to date with '\\''origin/feature'\\''.\n \n Diverged from '\\''upstream/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -512,13 +526,15 @@ test_expect_success 'status shows up to date with tracked but diverged from orig\n \t\tcd test &&\n \t\tgit checkout synced_feature2 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -4\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature2\n Your branch is up to date with '\\''origin/feature'\\''.\n \n Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -536,13 +552,15 @@ test_expect_success 'status shows up to date with tracked but shows default bran\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -4\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature3\n Your branch is up to date with '\\''origin/feature'\\''.\n \n Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -552,12 +570,13 @@ test_expect_success 'status with repo.settings.statusGoalBranch unset shows no d\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n \t\tgit config --unset repo.settings.statusGoalBranch 2>/dev/null || true &&\n-\t\tgit status --long -b | head -3\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature3\n Your branch is up to date with '\\''origin/feature'\\''.\n \n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -567,13 +586,15 @@ test_expect_success 'status with repo.settings.statusGoalBranch set uses configu\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -4\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature3\n Your branch is up to date with '\\''origin/feature'\\''.\n \n Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -583,13 +604,14 @@ test_expect_success 'status with repo.settings.statusGoalBranch set to different\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/feature &&\n-\t\tgit status --long -b | head -4\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work\n Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n   (use \"git push\" to publish your local commits)\n \n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -599,12 +621,13 @@ test_expect_success 'status with repo.settings.statusGoalBranch set to non-exist\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/nonexistent &&\n-\t\tgit status --long -b | head -3\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature3\n Your branch is up to date with '\\''origin/feature'\\''.\n \n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n-- \ngitgitgadget\n"},{"id":"532726","messageId":"pull.2138.v6.git.git.1766619672.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v5.git.git.1766572715.gitgitgadget@gmail.com","subject":"[PATCH v6 0/6] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T23:41:06Z","receivedAt":"2025-12-24T23:41:17Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\n\nHarald Nordgren (6):\n  status: show comparison with upstream default branch\n  Simplify default branch comparison logic\n  Use repo.settings.statusGoalBranch config for status comparison\n  Rename default_remote to goal_branch\n  Add warning for malformed statusGoalBranch config\n  Change config key to status.compareBranch\n\n remote.c                 |  92 +++++++++++\n t/t6040-tracking-info.sh | 340 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 432 insertions(+)\n\n\nbase-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v6\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v5:\n\n 1:  a3800aed18 = 1:  a3800aed18 status: show comparison with upstream default branch\n 2:  417f2075fb = 2:  417f2075fb Simplify default branch comparison logic\n 3:  c9ec5d9610 = 3:  c9ec5d9610 Use repo.settings.statusGoalBranch config for status comparison\n 4:  0e308141da = 4:  0e308141da Rename default_remote to goal_branch\n 5:  441678939f = 5:  441678939f Add warning for malformed statusGoalBranch config\n -:  ---------- > 6:  242dbbae44 Change config key to status.compareBranch\n\n-- \ngitgitgadget\n"},{"id":"532727","messageId":"a3800aed18e576e42de273c9dae0e25bbe26ac6a.1766619672.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v6.git.git.1766619672.gitgitgadget@gmail.com","subject":"[PATCH v6 1/6] status: show comparison with upstream default branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T23:41:07Z","receivedAt":"2025-12-24T23:41:19Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\" (i.e. you have built new history,\nwhile others are not touching it), \"behind\" (i.e. you haven't\nadded any work since you were in-sync, while others have added\ntheir work on the branch), \"diverged\" (i.e. you have commits\nthat you haven't pushed out, while others have added commits).\n\nWhen you fork a branch 'feature' from the 'main' branch of the\nremote, but then create 'feature' branch at the remote and push\nthere, while you still occasionally pull from or rebase onto\ntheir 'main', you'd also want to know how much you have diverged\nfrom 'main', in addition to how your 'feature' and their\n'feature' compares. Currently the comparison with 'main' is not\ngiven, making it hard to know when to start thinking about\nrebasing onto the upstream default branch.\n\nShow two sets of comparison: one with the tracking branch (as\nbefore), and another with the upstream's default branch (what\ntheir HEAD points to, typically 'main' or 'master'). The latter\ncomparison appears on a separate line after the tracking branch\nstatus, using the same format:\n- \"Ahead of 'origin/main' by N commits\" when purely ahead\n- \"Behind 'origin/main' by N commits\" when purely behind\n- \"Diverged from 'origin/main' by N commits\" when diverged\n\nExample output when tracking a feature branch:\n    On branch feature\n    Your branch is ahead of 'origin/feature' by 2 commits.\n      (use \"git push\" to publish your local commits)\n\n    Ahead of 'origin/main' by 5 commits.\n\nThe upstream default branch is determined by checking symbolic refs:\n1. refs/remotes/upstream/HEAD (if upstream remote exists)\n2. refs/remotes/origin/HEAD (fallback)\n\nThis works with any default branch name (main, master, develop,\netc.) as long as the symbolic ref is configured. The comparison\nis also shown when the branch is up-to-date with its tracking\nbranch but differs from the upstream default branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 104 +++++++++++++++++\n t/t6040-tracking-info.sh | 246 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 350 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..b2a1e980b1 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,98 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_default_remote_ref(char **full_ref_out)\n+{\n+\tint flag;\n+\tconst char *resolved;\n+\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n+\tint i;\n+\n+\tfor (i = 0; remotes[i]; i++) {\n+\t\tstruct strbuf head_ref = STRBUF_INIT;\n+\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n+\n+\t\tresolved = refs_resolve_ref_unsafe(\n+\t\t\tget_main_ref_store(the_repository),\n+\t\t\thead_ref.buf,\n+\t\t\tRESOLVE_REF_READING,\n+\t\t\tNULL, &flag);\n+\n+\t\tstrbuf_release(&head_ref);\n+\n+\t\tif (resolved && (flag & REF_ISSYMREF)) {\n+\t\t\tif (full_ref_out)\n+\t\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\t\treturn refs_shorten_unambiguous_ref(\n+\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t\t}\n+\t}\n+\n+\treturn NULL;\n+}\n+\n+static int is_default_remote_branch(const char *name)\n+{\n+\tchar *default_full = NULL;\n+\tchar *default_short;\n+\tint result = 0;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn 0;\n+\n+\tresult = !strcmp(name, default_short);\n+\n+\tfree(default_short);\n+\tfree(default_full);\n+\treturn result;\n+}\n+\n+static void format_default_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint default_ours = 0, default_theirs = 0;\n+\tchar *default_full = NULL;\n+\tchar *default_short;\n+\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (!default_short)\n+\t\treturn;\n+\n+\tif (stat_branch_pair(branch_refname, default_full,\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n+\t\tfree(default_short);\n+\t\tfree(default_full);\n+\t\treturn;\n+\t}\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (default_ours > 0 && default_theirs == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   default_ours),\n+\t\t\tdefault_short, default_ours);\n+\t} else if (default_theirs > 0 && default_ours == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   default_theirs),\n+\t\t\tdefault_short, default_theirs);\n+\t} else if (default_ours > 0 && default_theirs > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   default_ours + default_theirs),\n+\t\t\tdefault_short, default_ours + default_theirs);\n+\t}\n+\n+\tfree(default_short);\n+\tfree(default_full);\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2248,6 +2340,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tint show_default_branch_comparison;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2258,6 +2351,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n+\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2269,6 +2365,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2285,6 +2383,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2296,6 +2396,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2310,6 +2412,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n+\t\tif (show_default_branch_comparison)\n+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n \tfree(base);\n \treturn 1;\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..e2bd48f858 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,6 +21,7 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -292,4 +293,249 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup for ahead of non-main tracking branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature origin/main &&\n+\t\tadvance feature1 &&\n+\t\tgit push origin feature &&\n+\t\tgit checkout -b work --track origin/feature &&\n+\t\tadvance work1 &&\n+\t\tadvance work2\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main >/dev/null &&\n+\t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+Switched to branch '\\''work'\\''\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 >/dev/null &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n+\ttest_grep ! \"Ahead of\" actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b oldfeature &&\n+\t\tadvance oldfeature1 &&\n+\t\tgit push origin oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_newer &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout -b work2 --track origin/oldfeature &&\n+\t\tadvance work2_commit\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2\n+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for diverged from tracked but behind main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b work2b &&\n+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_extra &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2b >/dev/null &&\n+\t\tgit status --long -b | head -6\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2b\n+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n+and have 1 and 1 different commits each, respectively.\n+  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+Behind '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for behind tracked but ahead of main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b feature3 &&\n+\t\tadvance feature3_1 &&\n+\t\tadvance feature3_2 &&\n+\t\tadvance feature3_3 &&\n+\t\tgit push origin feature3 &&\n+\t\tgit checkout -b work3 --track origin/feature3 &&\n+\t\tgit reset --hard HEAD~2\n+\t)\n+'\n+\n+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work3 >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work3\n+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n+  (use \"git pull\" to update your local branch)\n+\n+Ahead of '\\''origin/main'\\'' by 1 commit.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote preference' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t)\n+'\n+\n+test_expect_success 'status prefers upstream remote over origin for comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit status --long -b | head -5\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''upstream/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''upstream/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote remove upstream &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead2 &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature2\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature3 --track origin/feature\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"532728","messageId":"417f2075fb876cbf5a00cd9877fb867ceddf7f6d.1766619672.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v6.git.git.1766619672.gitgitgadget@gmail.com","subject":"[PATCH v6 2/6] Simplify default branch comparison logic","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T23:41:08Z","receivedAt":"2025-12-24T23:41:22Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nThis maintains the same functionality while reducing ref resolution calls\nfrom multiple to one, and eliminating unnecessary memory allocations.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 59 ++++++++++++++++++--------------------------------------\n 1 file changed, 19 insertions(+), 40 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b2a1e980b1..f3831ef3be 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2267,41 +2267,17 @@ static char *get_default_remote_ref(char **full_ref_out)\n \treturn NULL;\n }\n \n-static int is_default_remote_branch(const char *name)\n-{\n-\tchar *default_full = NULL;\n-\tchar *default_short;\n-\tint result = 0;\n-\n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (!default_short)\n-\t\treturn 0;\n-\n-\tresult = !strcmp(name, default_short);\n-\n-\tfree(default_short);\n-\tfree(default_full);\n-\treturn result;\n-}\n-\n static void format_default_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     const char *default_full,\n+\t\t\t\t\t     const char *default_short,\n \t\t\t\t\t     enum ahead_behind_flags abf)\n {\n \tint default_ours = 0, default_theirs = 0;\n-\tchar *default_full = NULL;\n-\tchar *default_short;\n-\n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (!default_short)\n-\t\treturn;\n \n \tif (stat_branch_pair(branch_refname, default_full,\n-\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n-\t\tfree(default_short);\n-\t\tfree(default_full);\n+\t\t\t     &default_ours, &default_theirs, abf) <= 0)\n \t\treturn;\n-\t}\n \n \tstrbuf_addstr(sb, \"\\n\");\n \n@@ -2324,9 +2300,6 @@ static void format_default_branch_comparison(struct strbuf *sb,\n \t\t\t   default_ours + default_theirs),\n \t\t\tdefault_short, default_ours + default_theirs);\n \t}\n-\n-\tfree(default_short);\n-\tfree(default_full);\n }\n \n /*\n@@ -2340,7 +2313,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n-\tint show_default_branch_comparison;\n+\tchar *default_full = NULL;\n+\tchar *default_short = NULL;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2352,7 +2326,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n-\tshow_default_branch_comparison = !is_default_remote_branch(base);\n+\tdefault_short = get_default_remote_ref(&default_full);\n+\tif (default_short && !strcmp(base, default_short)) {\n+\t\tfree(default_short);\n+\t\tfree(default_full);\n+\t\tdefault_short = NULL;\n+\t\tdefault_full = NULL;\n+\t}\n \n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n@@ -2365,8 +2345,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n@@ -2383,8 +2361,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else if (!ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n@@ -2396,8 +2372,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t} else {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n@@ -2412,10 +2386,15 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n-\t\tif (show_default_branch_comparison)\n-\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n \t}\n+\n+\tif (default_short && !upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK)\n+\t\tformat_default_branch_comparison(sb, branch->refname, default_full,\n+\t\t\t\t\t\t default_short, abf);\n+\n \tfree(base);\n+\tfree(default_short);\n+\tfree(default_full);\n \treturn 1;\n }\n \n-- \ngitgitgadget\n\n"},{"id":"532729","messageId":"c9ec5d9610a024f18866683863127564d7d05a83.1766619672.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v6.git.git.1766619672.gitgitgadget@gmail.com","subject":"[PATCH v6 3/6] Use repo.settings.statusGoalBranch config for status comparison","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T23:41:09Z","receivedAt":"2025-12-24T23:41:24Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nReplace hardcoded upstream/origin preference with configurable\nrepo.settings.statusGoalBranch setting. When unset, skip the\ndefault branch comparison entirely.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 48 ++++++++++++++++-----------\n t/t6040-tracking-info.sh | 71 ++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 100 insertions(+), 19 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex f3831ef3be..edb6d374e7 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2239,32 +2239,42 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \n static char *get_default_remote_ref(char **full_ref_out)\n {\n-\tint flag;\n+\tconst char *config_value;\n \tconst char *resolved;\n-\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n-\tint i;\n+\tint flag;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *slash_pos;\n+\tchar *ret = NULL;\n \n-\tfor (i = 0; remotes[i]; i++) {\n-\t\tstruct strbuf head_ref = STRBUF_INIT;\n-\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n+\tif (repo_config_get_value(the_repository, \"repo.settings.statusGoalBranch\", &config_value))\n+\t\treturn NULL;\n \n-\t\tresolved = refs_resolve_ref_unsafe(\n-\t\t\tget_main_ref_store(the_repository),\n-\t\t\thead_ref.buf,\n-\t\t\tRESOLVE_REF_READING,\n-\t\t\tNULL, &flag);\n+\tif (!config_value || !*config_value)\n+\t\treturn NULL;\n \n-\t\tstrbuf_release(&head_ref);\n+\tslash_pos = strchr(config_value, '/');\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1])\n+\t\treturn NULL;\n \n-\t\tif (resolved && (flag & REF_ISSYMREF)) {\n-\t\t\tif (full_ref_out)\n-\t\t\t\t*full_ref_out = xstrdup(resolved);\n-\t\t\treturn refs_shorten_unambiguous_ref(\n-\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n-\t\t}\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n+\t\t    (int)(slash_pos - config_value), config_value,\n+\t\t    slash_pos + 1);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, &flag);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n \t}\n \n-\treturn NULL;\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n }\n \n static void format_default_branch_comparison(struct strbuf *sb,\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex e2bd48f858..00dadc03e7 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -309,6 +309,7 @@ test_expect_success 'status shows ahead of both tracked branch and origin/main'\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -325,6 +326,7 @@ test_expect_success 'checkout shows ahead of both tracked branch and origin/main\n \t(\n \t\tcd test &&\n \t\tgit checkout main >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -364,6 +366,7 @@ test_expect_success 'status shows ahead of tracked and diverged from origin/main\n \t(\n \t\tcd test &&\n \t\tgit checkout work2 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -393,6 +396,7 @@ test_expect_success 'status shows diverged from tracked and behind origin/main'\n \t(\n \t\tcd test &&\n \t\tgit checkout work2b >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -6\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -425,6 +429,7 @@ test_expect_success 'status shows behind tracked and ahead of origin/main' '\n \t(\n \t\tcd test &&\n \t\tgit checkout work3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -450,6 +455,7 @@ test_expect_success 'status prefers upstream remote over origin for comparison'\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch upstream/main &&\n \t\tgit status --long -b | head -5\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -477,6 +483,7 @@ test_expect_success 'status shows up to date with tracked but diverged from defa\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch upstream/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -504,6 +511,7 @@ test_expect_success 'status shows up to date with tracked but diverged from orig\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -527,6 +535,7 @@ test_expect_success 'status shows up to date with tracked but shows default bran\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n \t\tgit status --long -b | head -4\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -538,4 +547,66 @@ EOF\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status with repo.settings.statusGoalBranch unset shows no default comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config --unset repo.settings.statusGoalBranch 2>/dev/null || true &&\n+\t\tgit status --long -b | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set uses configured branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set to different remote/branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/feature &&\n+\t\tgit status --long -b | head -4\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with repo.settings.statusGoalBranch set to non-existent branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config repo.settings.statusGoalBranch origin/nonexistent &&\n+\t\tgit status --long -b | head -3\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"532730","messageId":"0e308141da0b639ba4ba15c640b8e4260689a42a.1766619672.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v6.git.git.1766619672.gitgitgadget@gmail.com","subject":"[PATCH v6 4/6] Rename default_remote to goal_branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T23:41:10Z","receivedAt":"2025-12-24T23:41:27Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRename variables and functions from 'default' to 'goal' to better\nreflect that they represent the goal branch from config rather than\na default remote. Also move the goal branch lookup to where it's used.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 63 ++++++++++++++++++++++++++++----------------------------\n 1 file changed, 31 insertions(+), 32 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex edb6d374e7..99c0e18df4 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,7 +2237,7 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-static char *get_default_remote_ref(char **full_ref_out)\n+static char *get_goal_branch_ref(char **full_ref_out)\n {\n \tconst char *config_value;\n \tconst char *resolved;\n@@ -2253,8 +2253,11 @@ static char *get_default_remote_ref(char **full_ref_out)\n \t\treturn NULL;\n \n \tslash_pos = strchr(config_value, '/');\n-\tif (!slash_pos || slash_pos == config_value || !slash_pos[1])\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n+\t\twarning(_(\"invalid value for repo.settings.statusGoalBranch: '%s' (expected format: remote/branch)\"),\n+\t\t\tconfig_value);\n \t\treturn NULL;\n+\t}\n \n \tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n \t\t    (int)(slash_pos - config_value), config_value,\n@@ -2277,38 +2280,38 @@ static char *get_default_remote_ref(char **full_ref_out)\n \treturn ret;\n }\n \n-static void format_default_branch_comparison(struct strbuf *sb,\n+static void format_goal_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t     const char *branch_refname,\n-\t\t\t\t\t     const char *default_full,\n-\t\t\t\t\t     const char *default_short,\n+\t\t\t\t\t     const char *goal_full,\n+\t\t\t\t\t     const char *goal_short,\n \t\t\t\t\t     enum ahead_behind_flags abf)\n {\n-\tint default_ours = 0, default_theirs = 0;\n+\tint goal_ahead = 0, goal_behind = 0;\n \n-\tif (stat_branch_pair(branch_refname, default_full,\n-\t\t\t     &default_ours, &default_theirs, abf) <= 0)\n+\tif (stat_branch_pair(branch_refname, goal_full,\n+\t\t\t     &goal_ahead, &goal_behind, abf) <= 0)\n \t\treturn;\n \n \tstrbuf_addstr(sb, \"\\n\");\n \n-\tif (default_ours > 0 && default_theirs == 0) {\n+\tif (goal_ahead > 0 && goal_behind == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n-\t\t\t   default_ours),\n-\t\t\tdefault_short, default_ours);\n-\t} else if (default_theirs > 0 && default_ours == 0) {\n+\t\t\t   goal_ahead),\n+\t\t\tgoal_short, goal_ahead);\n+\t} else if (goal_behind > 0 && goal_ahead == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n \t\t\t   \"Behind '%s' by %d commits.\\n\",\n-\t\t\t   default_theirs),\n-\t\t\tdefault_short, default_theirs);\n-\t} else if (default_ours > 0 && default_theirs > 0) {\n+\t\t\t   goal_behind),\n+\t\t\tgoal_short, goal_behind);\n+\t} else if (goal_ahead > 0 && goal_behind > 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n \t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n-\t\t\t   default_ours + default_theirs),\n-\t\t\tdefault_short, default_ours + default_theirs);\n+\t\t\t   goal_ahead + goal_behind),\n+\t\t\tgoal_short, goal_ahead + goal_behind);\n \t}\n }\n \n@@ -2323,8 +2326,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n-\tchar *default_full = NULL;\n-\tchar *default_short = NULL;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2336,14 +2337,6 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n-\tdefault_short = get_default_remote_ref(&default_full);\n-\tif (default_short && !strcmp(base, default_short)) {\n-\t\tfree(default_short);\n-\t\tfree(default_full);\n-\t\tdefault_short = NULL;\n-\t\tdefault_full = NULL;\n-\t}\n-\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2398,13 +2391,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n \n-\tif (default_short && !upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK)\n-\t\tformat_default_branch_comparison(sb, branch->refname, default_full,\n-\t\t\t\t\t\t default_short, abf);\n+\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tchar *goal_full = NULL;\n+\t\tchar *goal_short = get_goal_branch_ref(&goal_full);\n+\n+\t\tif (goal_short && strcmp(base, goal_short))\n+\t\t\tformat_goal_branch_comparison(sb, branch->refname, goal_full,\n+\t\t\t\t\t\t     goal_short, abf);\n+\n+\t\tfree(goal_short);\n+\t\tfree(goal_full);\n+\t}\n \n \tfree(base);\n-\tfree(default_short);\n-\tfree(default_full);\n \treturn 1;\n }\n \n-- \ngitgitgadget\n\n"},{"id":"532731","messageId":"441678939f30ba6cc07e7e8d4a41c72d0fd7427f.1766619672.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v6.git.git.1766619672.gitgitgadget@gmail.com","subject":"[PATCH v6 5/6] Add warning for malformed statusGoalBranch config","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T23:41:11Z","receivedAt":"2025-12-24T23:41:29Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nWarn when repo.settings.statusGoalBranch has invalid format\nand skip the goal branch comparison. Also update test to\ncompare full checkout output instead of grepping.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n t/t6040-tracking-info.sh | 49 +++++++++++++++++++++++++++++-----------\n 1 file changed, 36 insertions(+), 13 deletions(-)\n\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 00dadc03e7..598dd89483 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -310,7 +310,7 @@ test_expect_success 'status shows ahead of both tracked branch and origin/main'\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -5\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work\n@@ -318,6 +318,8 @@ Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n   (use \"git push\" to publish your local commits)\n \n Ahead of '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -327,11 +329,13 @@ test_expect_success 'checkout shows ahead of both tracked branch and origin/main\n \t\tcd test &&\n \t\tgit checkout main >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n+\t\tgit checkout work 2>&1\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n Switched to branch '\\''work'\\''\n Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n Ahead of '\\''origin/main'\\'' by 3 commits.\n EOF\n \ttest_cmp expect actual\n@@ -367,7 +371,7 @@ test_expect_success 'status shows ahead of tracked and diverged from origin/main\n \t\tcd test &&\n \t\tgit checkout work2 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -5\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work2\n@@ -375,6 +379,8 @@ Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n   (use \"git push\" to publish your local commits)\n \n Diverged from '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -397,7 +403,7 @@ test_expect_success 'status shows diverged from tracked and behind origin/main'\n \t\tcd test &&\n \t\tgit checkout work2b >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -6\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work2b\n@@ -406,6 +412,8 @@ and have 1 and 1 different commits each, respectively.\n   (use \"git pull\" if you want to integrate the remote branch with yours)\n \n Behind '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -430,7 +438,7 @@ test_expect_success 'status shows behind tracked and ahead of origin/main' '\n \t\tcd test &&\n \t\tgit checkout work3 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -5\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work3\n@@ -438,6 +446,8 @@ Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forw\n   (use \"git pull\" to update your local branch)\n \n Ahead of '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -456,7 +466,7 @@ test_expect_success 'status prefers upstream remote over origin for comparison'\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch upstream/main &&\n-\t\tgit status --long -b | head -5\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work\n@@ -464,6 +474,8 @@ Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n   (use \"git push\" to publish your local commits)\n \n Diverged from '\\''upstream/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -484,13 +496,15 @@ test_expect_success 'status shows up to date with tracked but diverged from defa\n \t\tcd test &&\n \t\tgit checkout synced_feature >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch upstream/main &&\n-\t\tgit status --long -b | head -4\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature\n Your branch is up to date with '\\''origin/feature'\\''.\n \n Diverged from '\\''upstream/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -512,13 +526,15 @@ test_expect_success 'status shows up to date with tracked but diverged from orig\n \t\tcd test &&\n \t\tgit checkout synced_feature2 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -4\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature2\n Your branch is up to date with '\\''origin/feature'\\''.\n \n Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -536,13 +552,15 @@ test_expect_success 'status shows up to date with tracked but shows default bran\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -4\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature3\n Your branch is up to date with '\\''origin/feature'\\''.\n \n Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -552,12 +570,13 @@ test_expect_success 'status with repo.settings.statusGoalBranch unset shows no d\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n \t\tgit config --unset repo.settings.statusGoalBranch 2>/dev/null || true &&\n-\t\tgit status --long -b | head -3\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature3\n Your branch is up to date with '\\''origin/feature'\\''.\n \n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -567,13 +586,15 @@ test_expect_success 'status with repo.settings.statusGoalBranch set uses configu\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/main &&\n-\t\tgit status --long -b | head -4\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature3\n Your branch is up to date with '\\''origin/feature'\\''.\n \n Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -583,13 +604,14 @@ test_expect_success 'status with repo.settings.statusGoalBranch set to different\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/feature &&\n-\t\tgit status --long -b | head -4\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch work\n Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n   (use \"git push\" to publish your local commits)\n \n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n@@ -599,12 +621,13 @@ test_expect_success 'status with repo.settings.statusGoalBranch set to non-exist\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n \t\tgit config repo.settings.statusGoalBranch origin/nonexistent &&\n-\t\tgit status --long -b | head -3\n+\t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n On branch synced_feature3\n Your branch is up to date with '\\''origin/feature'\\''.\n \n+nothing to commit, working tree clean\n EOF\n \ttest_cmp expect actual\n '\n-- \ngitgitgadget\n\n"},{"id":"532732","messageId":"242dbbae449d769dd9e013fad142988a6fc7e414.1766619672.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v6.git.git.1766619672.gitgitgadget@gmail.com","subject":"[PATCH v6 6/6] Change config key to status.compareBranch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T23:41:12Z","receivedAt":"2025-12-24T23:41:31Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nUpdate from repo.settings.statusGoalBranch to follow standard\ngit config pattern status.compareBranch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 |  4 ++--\n t/t6040-tracking-info.sh | 34 +++++++++++++++++-----------------\n 2 files changed, 19 insertions(+), 19 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 99c0e18df4..7e13c027b5 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2246,7 +2246,7 @@ static char *get_goal_branch_ref(char **full_ref_out)\n \tchar *slash_pos;\n \tchar *ret = NULL;\n \n-\tif (repo_config_get_value(the_repository, \"repo.settings.statusGoalBranch\", &config_value))\n+\tif (repo_config_get_value(the_repository, \"status.goalBranch\", &config_value))\n \t\treturn NULL;\n \n \tif (!config_value || !*config_value)\n@@ -2254,7 +2254,7 @@ static char *get_goal_branch_ref(char **full_ref_out)\n \n \tslash_pos = strchr(config_value, '/');\n \tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n-\t\twarning(_(\"invalid value for repo.settings.statusGoalBranch: '%s' (expected format: remote/branch)\"),\n+\t\twarning(_(\"invalid value for status.goalBranch: '%s' (expected format: remote/branch)\"),\n \t\t\tconfig_value);\n \t\treturn NULL;\n \t}\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 598dd89483..fe34ddf0ab 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -309,7 +309,7 @@ test_expect_success 'status shows ahead of both tracked branch and origin/main'\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit config status.goalBranch origin/main &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -328,7 +328,7 @@ test_expect_success 'checkout shows ahead of both tracked branch and origin/main\n \t(\n \t\tcd test &&\n \t\tgit checkout main >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit config status.goalBranch origin/main &&\n \t\tgit checkout work 2>&1\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -370,7 +370,7 @@ test_expect_success 'status shows ahead of tracked and diverged from origin/main\n \t(\n \t\tcd test &&\n \t\tgit checkout work2 >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit config status.goalBranch origin/main &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -402,7 +402,7 @@ test_expect_success 'status shows diverged from tracked and behind origin/main'\n \t(\n \t\tcd test &&\n \t\tgit checkout work2b >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit config status.goalBranch origin/main &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -437,7 +437,7 @@ test_expect_success 'status shows behind tracked and ahead of origin/main' '\n \t(\n \t\tcd test &&\n \t\tgit checkout work3 >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit config status.goalBranch origin/main &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -465,7 +465,7 @@ test_expect_success 'status prefers upstream remote over origin for comparison'\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch upstream/main &&\n+\t\tgit config status.goalBranch upstream/main &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -495,7 +495,7 @@ test_expect_success 'status shows up to date with tracked but diverged from defa\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch upstream/main &&\n+\t\tgit config status.goalBranch upstream/main &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -525,7 +525,7 @@ test_expect_success 'status shows up to date with tracked but diverged from orig\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature2 >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit config status.goalBranch origin/main &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -551,7 +551,7 @@ test_expect_success 'status shows up to date with tracked but shows default bran\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit config status.goalBranch origin/main &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -565,11 +565,11 @@ EOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'status with repo.settings.statusGoalBranch unset shows no default comparison' '\n+test_expect_success 'status with status.goalBranch unset shows no default comparison' '\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n-\t\tgit config --unset repo.settings.statusGoalBranch 2>/dev/null || true &&\n+\t\tgit config --unset status.goalBranch 2>/dev/null || true &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -581,11 +581,11 @@ EOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'status with repo.settings.statusGoalBranch set uses configured branch' '\n+test_expect_success 'status with status.goalBranch set uses configured branch' '\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch origin/main &&\n+\t\tgit config status.goalBranch origin/main &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -599,11 +599,11 @@ EOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'status with repo.settings.statusGoalBranch set to different remote/branch' '\n+test_expect_success 'status with status.goalBranch set to different remote/branch' '\n \t(\n \t\tcd test &&\n \t\tgit checkout work >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch origin/feature &&\n+\t\tgit config status.goalBranch origin/feature &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n@@ -616,11 +616,11 @@ EOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'status with repo.settings.statusGoalBranch set to non-existent branch' '\n+test_expect_success 'status with status.goalBranch set to non-existent branch' '\n \t(\n \t\tcd test &&\n \t\tgit checkout synced_feature3 >/dev/null &&\n-\t\tgit config repo.settings.statusGoalBranch origin/nonexistent &&\n+\t\tgit config status.goalBranch origin/nonexistent &&\n \t\tgit status --long -b\n \t) >actual &&\n \tcat >expect <<-\\EOF &&\n-- \ngitgitgadget\n"},{"id":"532736","messageId":"xmqqms37m1n9.fsf@gitster.g","threadId":"64668","inReplyTo":"pull.2138.v6.git.git.1766619672.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 0/6] status: show default branch comparison when tracking non-default branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-25T08:00:42Z","receivedAt":"2025-12-25T08:00:45Z","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> cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\n> ychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\n>\n> Harald Nordgren (6):\n>   status: show comparison with upstream default branch\n>   Simplify default branch comparison logic\n>   Use repo.settings.statusGoalBranch config for status comparison\n>   Rename default_remote to goal_branch\n>   Add warning for malformed statusGoalBranch config\n>   Change config key to status.compareBranch\n\nIt seems that [v6 6/6] smells like an \"oops, what I did in [v6 3/6]\nwas wrong, and this is an incremental fix on top of it\".\n\nPlease don't.\n\nWhen presenting your topic to the list, rather, after you finish a\nseries and the end result reaches a satisfactory state, please look\nback and polish patches to hide such mistakes in the middle, pretend\nas if you are a perfect developer who wrote a logical progression of\npatches that goes straight to the goal without stumbling around,\ntaking detours, and making mistakes you need to correct in a later\nstep.  Detours may have been taken when you initially wrote the\nseries, and it may show the \"true history\" from your point of view,\nbut to others (and the most importantly, to those who read \"git log\n-p\" later in order to extend your work to suit their needs better),\nthey are merely distracting.\n\nThe titles in [2-6/6] by the way do not seem to follow the\nestablished convention, like [1/6] does, i.e. \"status: show\ncomparison...\", use an area prefix \"<area>:\", followed by a\nshort-summary that is not capitalized.\n\nThanks.\n"},{"id":"532737","messageId":"pull.2138.v7.git.git.1766655947789.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v6.git.git.1766619672.gitgitgadget@gmail.com","subject":"[PATCH v7] status: additional comparison with goal branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-25T09:45:47Z","receivedAt":"2025-12-25T09:45:50Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\" (i.e. you have built new history,\nwhile others are not touching it), \"behind\" (i.e. you haven't\nadded any work since you were in-sync, while others have added\ntheir work on the branch), \"diverged\" (i.e. you have commits\nthat you haven't pushed out, while others have added commits).\n\nWhen you fork a branch 'feature' from the 'main' branch of the\nremote, but then create 'feature' branch at the remote and push\nthere, while you still occasionally pull from or rebase onto\ntheir 'main', you'd also want to know how much you have diverged\nfrom 'main', in addition to how your 'feature' and their\n'feature' compares. Currently the comparison with 'main' is not\ngiven, making it hard to know when to start thinking about\nrebasing onto the upstream default branch.\n\nShow two sets of comparison: one with the tracking branch (as\nbefore), and another with the upstream's default branch (what\ntheir HEAD points to, typically 'main' or 'master'). The latter\ncomparison appears on a separate line after the tracking branch\nstatus, using the same format:\n- \"Ahead of 'origin/main' by N commits\" when purely ahead\n- \"Behind 'origin/main' by N commits\" when purely behind\n- \"Diverged from 'origin/main' by N commits\" when diverged\n\nExample output when tracking a feature branch:\n    On branch feature\n    Your branch is ahead of 'origin/feature' by 2 commits.\n      (use \"git push\" to publish your local commits)\n\n    Ahead of 'origin/main' by 5 commits.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    status: show default branch comparison when tracking non-default branch\n    \n    cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\n    ychin.macvim@gmail.com cc: \"brian m. carlson\"\n    sandals@crustytoothpaste.net\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v7\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v7\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v6:\n\n 1:  a3800aed18 ! 1:  04e2fb76ee status: show comparison with upstream default branch\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    status: show comparison with upstream default branch\n     +    status: additional comparison with goal branch\n      \n          \"git status\" on a branch that follows a remote branch compares\n          commits on the current branch and the remote-tracking branch it\n     @@ Commit message\n      \n              Ahead of 'origin/main' by 5 commits.\n      \n     -    The upstream default branch is determined by checking symbolic refs:\n     -    1. refs/remotes/upstream/HEAD (if upstream remote exists)\n     -    2. refs/remotes/origin/HEAD (fallback)\n     -\n     -    This works with any default branch name (main, master, develop,\n     -    etc.) as long as the symbolic ref is configured. The comparison\n     -    is also shown when the branch is up-to-date with its tracking\n     -    branch but differs from the upstream default branch.\n     -\n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## remote.c ##\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n       }\n       \n     -+static char *get_default_remote_ref(char **full_ref_out)\n     ++static char *get_goal_branch_ref(char **full_ref_out)\n      +{\n     -+\tint flag;\n     ++\tconst char *config_value;\n      +\tconst char *resolved;\n     -+\tstatic const char *remotes[] = { \"upstream\", \"origin\", NULL };\n     -+\tint i;\n     -+\n     -+\tfor (i = 0; remotes[i]; i++) {\n     -+\t\tstruct strbuf head_ref = STRBUF_INIT;\n     -+\t\tstrbuf_addf(&head_ref, \"refs/remotes/%s/HEAD\", remotes[i]);\n     -+\n     -+\t\tresolved = refs_resolve_ref_unsafe(\n     -+\t\t\tget_main_ref_store(the_repository),\n     -+\t\t\thead_ref.buf,\n     -+\t\t\tRESOLVE_REF_READING,\n     -+\t\t\tNULL, &flag);\n     -+\n     -+\t\tstrbuf_release(&head_ref);\n     -+\n     -+\t\tif (resolved && (flag & REF_ISSYMREF)) {\n     -+\t\t\tif (full_ref_out)\n     -+\t\t\t\t*full_ref_out = xstrdup(resolved);\n     -+\t\t\treturn refs_shorten_unambiguous_ref(\n     -+\t\t\t\tget_main_ref_store(the_repository), resolved, 0);\n     -+\t\t}\n     -+\t}\n     ++\tint flag;\n     ++\tstruct strbuf ref_buf = STRBUF_INIT;\n     ++\tchar *slash_pos;\n     ++\tchar *ret = NULL;\n      +\n     -+\treturn NULL;\n     -+}\n     ++\tif (repo_config_get_value(the_repository, \"status.goalBranch\", &config_value))\n     ++\t\treturn NULL;\n      +\n     -+static int is_default_remote_branch(const char *name)\n     -+{\n     -+\tchar *default_full = NULL;\n     -+\tchar *default_short;\n     -+\tint result = 0;\n     ++\tif (!config_value || !*config_value)\n     ++\t\treturn NULL;\n      +\n     -+\tdefault_short = get_default_remote_ref(&default_full);\n     -+\tif (!default_short)\n     -+\t\treturn 0;\n     ++\tslash_pos = strchr(config_value, '/');\n     ++\tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n     ++\t\twarning(_(\"invalid value for status.goalBranch: '%s' (expected format: remote/branch)\"),\n     ++\t\t\tconfig_value);\n     ++\t\treturn NULL;\n     ++\t}\n      +\n     -+\tresult = !strcmp(name, default_short);\n     ++\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n     ++\t\t    (int)(slash_pos - config_value), config_value,\n     ++\t\t    slash_pos + 1);\n     ++\n     ++\tresolved = refs_resolve_ref_unsafe(\n     ++\t\tget_main_ref_store(the_repository),\n     ++\t\tref_buf.buf,\n     ++\t\tRESOLVE_REF_READING,\n     ++\t\tNULL, &flag);\n     ++\n     ++\tif (resolved) {\n     ++\t\tif (full_ref_out)\n     ++\t\t\t*full_ref_out = xstrdup(resolved);\n     ++\t\tret = refs_shorten_unambiguous_ref(\n     ++\t\t\tget_main_ref_store(the_repository), resolved, 0);\n     ++\t}\n      +\n     -+\tfree(default_short);\n     -+\tfree(default_full);\n     -+\treturn result;\n     ++\tstrbuf_release(&ref_buf);\n     ++\treturn ret;\n      +}\n      +\n     -+static void format_default_branch_comparison(struct strbuf *sb,\n     ++static void format_goal_branch_comparison(struct strbuf *sb,\n      +\t\t\t\t\t     const char *branch_refname,\n     ++\t\t\t\t\t     const char *goal_full,\n     ++\t\t\t\t\t     const char *goal_short,\n      +\t\t\t\t\t     enum ahead_behind_flags abf)\n      +{\n     -+\tint default_ours = 0, default_theirs = 0;\n     -+\tchar *default_full = NULL;\n     -+\tchar *default_short;\n     -+\n     -+\tdefault_short = get_default_remote_ref(&default_full);\n     -+\tif (!default_short)\n     -+\t\treturn;\n     ++\tint goal_ahead = 0, goal_behind = 0;\n      +\n     -+\tif (stat_branch_pair(branch_refname, default_full,\n     -+\t\t\t     &default_ours, &default_theirs, abf) <= 0) {\n     -+\t\tfree(default_short);\n     -+\t\tfree(default_full);\n     ++\tif (stat_branch_pair(branch_refname, goal_full,\n     ++\t\t\t     &goal_ahead, &goal_behind, abf) <= 0)\n      +\t\treturn;\n     -+\t}\n      +\n      +\tstrbuf_addstr(sb, \"\\n\");\n      +\n     -+\tif (default_ours > 0 && default_theirs == 0) {\n     ++\tif (goal_ahead > 0 && goal_behind == 0) {\n      +\t\tstrbuf_addf(sb,\n      +\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n      +\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n     -+\t\t\t   default_ours),\n     -+\t\t\tdefault_short, default_ours);\n     -+\t} else if (default_theirs > 0 && default_ours == 0) {\n     ++\t\t\t   goal_ahead),\n     ++\t\t\tgoal_short, goal_ahead);\n     ++\t} else if (goal_behind > 0 && goal_ahead == 0) {\n      +\t\tstrbuf_addf(sb,\n      +\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n      +\t\t\t   \"Behind '%s' by %d commits.\\n\",\n     -+\t\t\t   default_theirs),\n     -+\t\t\tdefault_short, default_theirs);\n     -+\t} else if (default_ours > 0 && default_theirs > 0) {\n     ++\t\t\t   goal_behind),\n     ++\t\t\tgoal_short, goal_behind);\n     ++\t} else if (goal_ahead > 0 && goal_behind > 0) {\n      +\t\tstrbuf_addf(sb,\n      +\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n      +\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n     -+\t\t\t   default_ours + default_theirs),\n     -+\t\t\tdefault_short, default_ours + default_theirs);\n     ++\t\t\t   goal_ahead + goal_behind),\n     ++\t\t\tgoal_short, goal_ahead + goal_behind);\n      +\t}\n     -+\n     -+\tfree(default_short);\n     -+\tfree(default_full);\n      +}\n      +\n       /*\n        * Return true when there is anything to report, otherwise false.\n        */\n      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \tconst char *full_base;\n     - \tchar *base;\n     - \tint upstream_is_gone = 0;\n     -+\tint show_default_branch_comparison;\n     - \n     - \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n     - \tif (sti < 0) {\n     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \n       \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n       \t\t\t\t\t    full_base, 0);\n     -+\n     -+\tshow_default_branch_comparison = !is_default_remote_branch(base);\n      +\n       \tif (upstream_is_gone) {\n       \t\tstrbuf_addf(sb,\n       \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \t\tstrbuf_addf(sb,\n     - \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n     - \t\t\tbase);\n     -+\t\tif (show_default_branch_comparison)\n     -+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n     - \t} else if (abf == AHEAD_BEHIND_QUICK) {\n     - \t\tstrbuf_addf(sb,\n     - \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     - \t\t\tstrbuf_addstr(sb,\n     - \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n     -+\t\tif (show_default_branch_comparison)\n     -+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n     - \t} else if (!ours) {\n     - \t\tstrbuf_addf(sb,\n     - \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     - \t\t\tstrbuf_addstr(sb,\n     - \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n     -+\t\tif (show_default_branch_comparison)\n     -+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n     - \t} else {\n     - \t\tstrbuf_addf(sb,\n     - \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n     -+\t\tif (show_default_branch_comparison)\n     -+\t\t\tformat_default_branch_comparison(sb, branch->refname, abf);\n       \t}\n     ++\n     ++\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n     ++\t\tchar *goal_full = NULL;\n     ++\t\tchar *goal_short = get_goal_branch_ref(&goal_full);\n     ++\n     ++\t\tif (goal_short && strcmp(base, goal_short))\n     ++\t\t\tformat_goal_branch_comparison(sb, branch->refname, goal_full,\n     ++\t\t\t\t\t\t     goal_short, abf);\n     ++\n     ++\t\tfree(goal_short);\n     ++\t\tfree(goal_full);\n     ++\t}\n     ++\n       \tfree(base);\n       \treturn 1;\n     + }\n      \n       ## t/t6040-tracking-info.sh ##\n      @@ t/t6040-tracking-info.sh: test_expect_success setup '\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout work >/dev/null &&\n     -+\t\tgit status --long -b | head -5\n     ++\t\tgit config status.goalBranch origin/main &&\n     ++\t\tgit status --long -b\n      +\t) >actual &&\n      +\tcat >expect <<-\\EOF &&\n      +On branch work\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +  (use \"git push\" to publish your local commits)\n      +\n      +Ahead of '\\''origin/main'\\'' by 3 commits.\n     ++\n     ++nothing to commit, working tree clean\n      +EOF\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout main >/dev/null &&\n     -+\t\tgit checkout work 2>&1 | grep -E \"(Switched|Your branch|Ahead of)\" | head -3\n     ++\t\tgit config status.goalBranch origin/main &&\n     ++\t\tgit checkout work 2>&1\n      +\t) >actual &&\n      +\tcat >expect <<-\\EOF &&\n      +Switched to branch '\\''work'\\''\n      +Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n     ++  (use \"git push\" to publish your local commits)\n     ++\n      +Ahead of '\\''origin/main'\\'' by 3 commits.\n      +EOF\n      +\ttest_cmp expect actual\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout work2 >/dev/null &&\n     -+\t\tgit status --long -b | head -5\n     ++\t\tgit config status.goalBranch origin/main &&\n     ++\t\tgit status --long -b\n      +\t) >actual &&\n      +\tcat >expect <<-\\EOF &&\n      +On branch work2\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +  (use \"git push\" to publish your local commits)\n      +\n      +Diverged from '\\''origin/main'\\'' by 3 commits.\n     ++\n     ++nothing to commit, working tree clean\n      +EOF\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout work2b >/dev/null &&\n     -+\t\tgit status --long -b | head -6\n     ++\t\tgit config status.goalBranch origin/main &&\n     ++\t\tgit status --long -b\n      +\t) >actual &&\n      +\tcat >expect <<-\\EOF &&\n      +On branch work2b\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +  (use \"git pull\" if you want to integrate the remote branch with yours)\n      +\n      +Behind '\\''origin/main'\\'' by 1 commit.\n     ++\n     ++nothing to commit, working tree clean\n      +EOF\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout work3 >/dev/null &&\n     -+\t\tgit status --long -b | head -5\n     ++\t\tgit config status.goalBranch origin/main &&\n     ++\t\tgit status --long -b\n      +\t) >actual &&\n      +\tcat >expect <<-\\EOF &&\n      +On branch work3\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +  (use \"git pull\" to update your local branch)\n      +\n      +Ahead of '\\''origin/main'\\'' by 1 commit.\n     ++\n     ++nothing to commit, working tree clean\n      +EOF\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout work >/dev/null &&\n     -+\t\tgit status --long -b | head -5\n     ++\t\tgit config status.goalBranch upstream/main &&\n     ++\t\tgit status --long -b\n      +\t) >actual &&\n      +\tcat >expect <<-\\EOF &&\n      +On branch work\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +  (use \"git push\" to publish your local commits)\n      +\n      +Diverged from '\\''upstream/main'\\'' by 5 commits.\n     ++\n     ++nothing to commit, working tree clean\n      +EOF\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout synced_feature >/dev/null &&\n     -+\t\tgit status --long -b | head -4\n     ++\t\tgit config status.goalBranch upstream/main &&\n     ++\t\tgit status --long -b\n      +\t) >actual &&\n      +\tcat >expect <<-\\EOF &&\n      +On branch synced_feature\n      +Your branch is up to date with '\\''origin/feature'\\''.\n      +\n      +Diverged from '\\''upstream/main'\\'' by 3 commits.\n     ++\n     ++nothing to commit, working tree clean\n      +EOF\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout synced_feature2 >/dev/null &&\n     -+\t\tgit status --long -b | head -4\n     ++\t\tgit config status.goalBranch origin/main &&\n     ++\t\tgit status --long -b\n      +\t) >actual &&\n      +\tcat >expect <<-\\EOF &&\n      +On branch synced_feature2\n      +Your branch is up to date with '\\''origin/feature'\\''.\n      +\n      +Diverged from '\\''origin/main'\\'' by 5 commits.\n     ++\n     ++nothing to commit, working tree clean\n      +EOF\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout synced_feature3 >/dev/null &&\n     -+\t\tgit status --long -b | head -4\n     ++\t\tgit config status.goalBranch origin/main &&\n     ++\t\tgit status --long -b\n     ++\t) >actual &&\n     ++\tcat >expect <<-\\EOF &&\n     ++On branch synced_feature3\n     ++Your branch is up to date with '\\''origin/feature'\\''.\n     ++\n     ++Diverged from '\\''origin/main'\\'' by 5 commits.\n     ++\n     ++nothing to commit, working tree clean\n     ++EOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'status with status.goalBranch unset shows no default comparison' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout synced_feature3 >/dev/null &&\n     ++\t\tgit config --unset status.goalBranch 2>/dev/null || true &&\n     ++\t\tgit status --long -b\n     ++\t) >actual &&\n     ++\tcat >expect <<-\\EOF &&\n     ++On branch synced_feature3\n     ++Your branch is up to date with '\\''origin/feature'\\''.\n     ++\n     ++nothing to commit, working tree clean\n     ++EOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'status with status.goalBranch set uses configured branch' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout synced_feature3 >/dev/null &&\n     ++\t\tgit config status.goalBranch origin/main &&\n     ++\t\tgit status --long -b\n      +\t) >actual &&\n      +\tcat >expect <<-\\EOF &&\n      +On branch synced_feature3\n      +Your branch is up to date with '\\''origin/feature'\\''.\n      +\n      +Diverged from '\\''origin/main'\\'' by 5 commits.\n     ++\n     ++nothing to commit, working tree clean\n     ++EOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'status with status.goalBranch set to different remote/branch' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout work >/dev/null &&\n     ++\t\tgit config status.goalBranch origin/feature &&\n     ++\t\tgit status --long -b\n     ++\t) >actual &&\n     ++\tcat >expect <<-\\EOF &&\n     ++On branch work\n     ++Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n     ++  (use \"git push\" to publish your local commits)\n     ++\n     ++nothing to commit, working tree clean\n     ++EOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'status with status.goalBranch set to non-existent branch' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout synced_feature3 >/dev/null &&\n     ++\t\tgit config status.goalBranch origin/nonexistent &&\n     ++\t\tgit status --long -b\n     ++\t) >actual &&\n     ++\tcat >expect <<-\\EOF &&\n     ++On branch synced_feature3\n     ++Your branch is up to date with '\\''origin/feature'\\''.\n     ++\n     ++nothing to commit, working tree clean\n      +EOF\n      +\ttest_cmp expect actual\n      +'\n 2:  417f2075fb < -:  ---------- Simplify default branch comparison logic\n 3:  c9ec5d9610 < -:  ---------- Use repo.settings.statusGoalBranch config for status comparison\n 4:  0e308141da < -:  ---------- Rename default_remote to goal_branch\n 5:  441678939f < -:  ---------- Add warning for malformed statusGoalBranch config\n 6:  242dbbae44 < -:  ---------- Change config key to status.compareBranch\n\n\n remote.c                 |  92 +++++++++++\n t/t6040-tracking-info.sh | 340 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 432 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..7e13c027b5 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,84 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_goal_branch_ref(char **full_ref_out)\n+{\n+\tconst char *config_value;\n+\tconst char *resolved;\n+\tint flag;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *slash_pos;\n+\tchar *ret = NULL;\n+\n+\tif (repo_config_get_value(the_repository, \"status.goalBranch\", &config_value))\n+\t\treturn NULL;\n+\n+\tif (!config_value || !*config_value)\n+\t\treturn NULL;\n+\n+\tslash_pos = strchr(config_value, '/');\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n+\t\twarning(_(\"invalid value for status.goalBranch: '%s' (expected format: remote/branch)\"),\n+\t\t\tconfig_value);\n+\t\treturn NULL;\n+\t}\n+\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n+\t\t    (int)(slash_pos - config_value), config_value,\n+\t\t    slash_pos + 1);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, &flag);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t}\n+\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n+}\n+\n+static void format_goal_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     const char *goal_full,\n+\t\t\t\t\t     const char *goal_short,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint goal_ahead = 0, goal_behind = 0;\n+\n+\tif (stat_branch_pair(branch_refname, goal_full,\n+\t\t\t     &goal_ahead, &goal_behind, abf) <= 0)\n+\t\treturn;\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (goal_ahead > 0 && goal_behind == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   goal_ahead),\n+\t\t\tgoal_short, goal_ahead);\n+\t} else if (goal_behind > 0 && goal_ahead == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   goal_behind),\n+\t\t\tgoal_short, goal_behind);\n+\t} else if (goal_ahead > 0 && goal_behind > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   goal_ahead + goal_behind),\n+\t\t\tgoal_short, goal_ahead + goal_behind);\n+\t}\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2258,6 +2336,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2311,6 +2390,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+\n+\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tchar *goal_full = NULL;\n+\t\tchar *goal_short = get_goal_branch_ref(&goal_full);\n+\n+\t\tif (goal_short && strcmp(base, goal_short))\n+\t\t\tformat_goal_branch_comparison(sb, branch->refname, goal_full,\n+\t\t\t\t\t\t     goal_short, abf);\n+\n+\t\tfree(goal_short);\n+\t\tfree(goal_full);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..fe34ddf0ab 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,6 +21,7 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -292,4 +293,343 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup for ahead of non-main tracking branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature origin/main &&\n+\t\tadvance feature1 &&\n+\t\tgit push origin feature &&\n+\t\tgit checkout -b work --track origin/feature &&\n+\t\tadvance work1 &&\n+\t\tadvance work2\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit checkout work 2>&1\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+Switched to branch '\\''work'\\''\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 >/dev/null &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n+\ttest_grep ! \"Ahead of\" actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b oldfeature &&\n+\t\tadvance oldfeature1 &&\n+\t\tgit push origin oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_newer &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout -b work2 --track origin/oldfeature &&\n+\t\tadvance work2_commit\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2\n+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for diverged from tracked but behind main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b work2b &&\n+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_extra &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2b >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2b\n+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n+and have 1 and 1 different commits each, respectively.\n+  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+Behind '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for behind tracked but ahead of main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b feature3 &&\n+\t\tadvance feature3_1 &&\n+\t\tadvance feature3_2 &&\n+\t\tadvance feature3_3 &&\n+\t\tgit push origin feature3 &&\n+\t\tgit checkout -b work3 --track origin/feature3 &&\n+\t\tgit reset --hard HEAD~2\n+\t)\n+'\n+\n+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work3\n+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n+  (use \"git pull\" to update your local branch)\n+\n+Ahead of '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote preference' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t)\n+'\n+\n+test_expect_success 'status prefers upstream remote over origin for comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch upstream/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''upstream/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit config status.goalBranch upstream/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''upstream/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote remove upstream &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead2 &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature2\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature3 --track origin/feature\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch unset shows no default comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config --unset status.goalBranch 2>/dev/null || true &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set uses configured branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set to different remote/branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch origin/feature &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set to non-existent branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/nonexistent &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda\n-- \ngitgitgadget\n"},{"id":"532738","messageId":"20251225094556.5944-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqms37m1n9.fsf@gitster.g","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-25T09:45:56Z","receivedAt":"2025-12-25T09:45:59Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Got it! I'd rather just squash all the commits then, the total diff is\nquite small still.\n\n\nHarald\n"},{"id":"532739","messageId":"pull.2138.v8.git.git.1766666006561.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v7.git.git.1766655947789.gitgitgadget@gmail.com","subject":"[PATCH v8] status: show comparison with configured goal branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-25T12:33:26Z","receivedAt":"2025-12-25T12:33:29Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to a goal branch (e.g.,\nupstream/main), you can configure status.goalBranch to show an\nadditional comparison.\n\nAdd support for status.goalBranch configuration option that specifies\na remote/branch to compare against. When set, git status shows both\nthe comparison with the tracking branch (as before) and an additional\ncomparison with the configured goal branch, if it differs from the\ntracking branch. The goal branch comparison appears on a separate\nline after the tracking branch status, using the same format:\n- \"Ahead of 'upstream/main' by N commits\" when purely ahead\n- \"Behind 'upstream/main' by N commits\" when purely behind\n- \"Diverged from 'upstream/main' by N commits\" when diverged\n\nExample output when tracking a feature branch with status.goalBranch\nset to upstream/main:\n    On branch feature\n    Your branch is ahead of 'origin/feature' by 2 commits.\n      (use \"git push\" to publish your local commits)\n\n    Ahead of 'upstream/main' by 5 commits.\n\nThe comparison is only shown when status.goalBranch is configured\nand the goal branch differs from the tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    status: show comparison with configured goal branch\n    \n    cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\n    ychin.macvim@gmail.com cc: \"brian m. carlson\"\n    sandals@crustytoothpaste.net\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v8\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v8\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v7:\n\n 1:  04e2fb76ee ! 1:  7e2574d5ae status: additional comparison with goal branch\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    status: additional comparison with goal branch\n     +    status: show comparison with configured goal branch\n      \n          \"git status\" on a branch that follows a remote branch compares\n          commits on the current branch and the remote-tracking branch it\n     -    builds upon, to show \"ahead\" (i.e. you have built new history,\n     -    while others are not touching it), \"behind\" (i.e. you haven't\n     -    added any work since you were in-sync, while others have added\n     -    their work on the branch), \"diverged\" (i.e. you have commits\n     -    that you haven't pushed out, while others have added commits).\n     +    builds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n      \n     -    When you fork a branch 'feature' from the 'main' branch of the\n     -    remote, but then create 'feature' branch at the remote and push\n     -    there, while you still occasionally pull from or rebase onto\n     -    their 'main', you'd also want to know how much you have diverged\n     -    from 'main', in addition to how your 'feature' and their\n     -    'feature' compares. Currently the comparison with 'main' is not\n     -    given, making it hard to know when to start thinking about\n     -    rebasing onto the upstream default branch.\n     +    When working on a feature branch that tracks a remote feature branch,\n     +    but you also want to track progress relative to a goal branch (e.g.,\n     +    upstream/main), you can configure status.goalBranch to show an\n     +    additional comparison.\n      \n     -    Show two sets of comparison: one with the tracking branch (as\n     -    before), and another with the upstream's default branch (what\n     -    their HEAD points to, typically 'main' or 'master'). The latter\n     -    comparison appears on a separate line after the tracking branch\n     -    status, using the same format:\n     -    - \"Ahead of 'origin/main' by N commits\" when purely ahead\n     -    - \"Behind 'origin/main' by N commits\" when purely behind\n     -    - \"Diverged from 'origin/main' by N commits\" when diverged\n     +    Add support for status.goalBranch configuration option that specifies\n     +    a remote/branch to compare against. When set, git status shows both\n     +    the comparison with the tracking branch (as before) and an additional\n     +    comparison with the configured goal branch, if it differs from the\n     +    tracking branch. The goal branch comparison appears on a separate\n     +    line after the tracking branch status, using the same format:\n     +    - \"Ahead of 'upstream/main' by N commits\" when purely ahead\n     +    - \"Behind 'upstream/main' by N commits\" when purely behind\n     +    - \"Diverged from 'upstream/main' by N commits\" when diverged\n      \n     -    Example output when tracking a feature branch:\n     +    Example output when tracking a feature branch with status.goalBranch\n     +    set to upstream/main:\n              On branch feature\n              Your branch is ahead of 'origin/feature' by 2 commits.\n                (use \"git push\" to publish your local commits)\n      \n     -        Ahead of 'origin/main' by 5 commits.\n     +        Ahead of 'upstream/main' by 5 commits.\n     +\n     +    The comparison is only shown when status.goalBranch is configured\n     +    and the goal branch differs from the tracking branch.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n\n\n remote.c                 |  92 +++++++++++\n t/t6040-tracking-info.sh | 340 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 432 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..7e13c027b5 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,84 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_goal_branch_ref(char **full_ref_out)\n+{\n+\tconst char *config_value;\n+\tconst char *resolved;\n+\tint flag;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *slash_pos;\n+\tchar *ret = NULL;\n+\n+\tif (repo_config_get_value(the_repository, \"status.goalBranch\", &config_value))\n+\t\treturn NULL;\n+\n+\tif (!config_value || !*config_value)\n+\t\treturn NULL;\n+\n+\tslash_pos = strchr(config_value, '/');\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n+\t\twarning(_(\"invalid value for status.goalBranch: '%s' (expected format: remote/branch)\"),\n+\t\t\tconfig_value);\n+\t\treturn NULL;\n+\t}\n+\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n+\t\t    (int)(slash_pos - config_value), config_value,\n+\t\t    slash_pos + 1);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, &flag);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t}\n+\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n+}\n+\n+static void format_goal_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     const char *goal_full,\n+\t\t\t\t\t     const char *goal_short,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint goal_ahead = 0, goal_behind = 0;\n+\n+\tif (stat_branch_pair(branch_refname, goal_full,\n+\t\t\t     &goal_ahead, &goal_behind, abf) <= 0)\n+\t\treturn;\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (goal_ahead > 0 && goal_behind == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   goal_ahead),\n+\t\t\tgoal_short, goal_ahead);\n+\t} else if (goal_behind > 0 && goal_ahead == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   goal_behind),\n+\t\t\tgoal_short, goal_behind);\n+\t} else if (goal_ahead > 0 && goal_behind > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   goal_ahead + goal_behind),\n+\t\t\tgoal_short, goal_ahead + goal_behind);\n+\t}\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2258,6 +2336,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2311,6 +2390,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+\n+\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tchar *goal_full = NULL;\n+\t\tchar *goal_short = get_goal_branch_ref(&goal_full);\n+\n+\t\tif (goal_short && strcmp(base, goal_short))\n+\t\t\tformat_goal_branch_comparison(sb, branch->refname, goal_full,\n+\t\t\t\t\t\t     goal_short, abf);\n+\n+\t\tfree(goal_short);\n+\t\tfree(goal_full);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..fe34ddf0ab 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,6 +21,7 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -292,4 +293,343 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup for ahead of non-main tracking branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature origin/main &&\n+\t\tadvance feature1 &&\n+\t\tgit push origin feature &&\n+\t\tgit checkout -b work --track origin/feature &&\n+\t\tadvance work1 &&\n+\t\tadvance work2\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit checkout work 2>&1\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+Switched to branch '\\''work'\\''\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 >/dev/null &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n+\ttest_grep ! \"Ahead of\" actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b oldfeature &&\n+\t\tadvance oldfeature1 &&\n+\t\tgit push origin oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_newer &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout -b work2 --track origin/oldfeature &&\n+\t\tadvance work2_commit\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2\n+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for diverged from tracked but behind main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b work2b &&\n+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_extra &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2b >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2b\n+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n+and have 1 and 1 different commits each, respectively.\n+  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+Behind '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for behind tracked but ahead of main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b feature3 &&\n+\t\tadvance feature3_1 &&\n+\t\tadvance feature3_2 &&\n+\t\tadvance feature3_3 &&\n+\t\tgit push origin feature3 &&\n+\t\tgit checkout -b work3 --track origin/feature3 &&\n+\t\tgit reset --hard HEAD~2\n+\t)\n+'\n+\n+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work3\n+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n+  (use \"git pull\" to update your local branch)\n+\n+Ahead of '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote preference' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t)\n+'\n+\n+test_expect_success 'status prefers upstream remote over origin for comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch upstream/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''upstream/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit config status.goalBranch upstream/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''upstream/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote remove upstream &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead2 &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature2\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature3 --track origin/feature\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch unset shows no default comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config --unset status.goalBranch 2>/dev/null || true &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set uses configured branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set to different remote/branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch origin/feature &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set to non-existent branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/nonexistent &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda\n-- \ngitgitgadget\n"},{"id":"532744","messageId":"xmqqikdum29t.fsf@gitster.g","threadId":"64668","inReplyTo":"20251225094556.5944-1-haraldnordgren@gmail.com","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-26T01:59:26Z","receivedAt":"2025-12-26T01:59:29Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> Got it! I'd rather just squash all the commits then, the total diff is\n> quite small still.\n\nWell, that is quite different from what I meant, but let's see what\nothers may say.\n\nHappy holidays.\n"},{"id":"532751","messageId":"20251226105846.75864-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqikdum29t.fsf@gitster.g","subject":"Re: [PATCH] status: show default branch comparison when tracking non-default branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-26T10:58:46Z","receivedAt":"2025-12-26T10:58:50Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Thanks, I appreciate all your help! I feel like a bit of a bull in a china shop here.\n\n\nMerry Christmas!\n"},{"id":"532788","messageId":"20251228091607.14896-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v8.git.git.1766666006561.gitgitgadget@gmail.com","subject":"Code review?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-28T09:16:07Z","receivedAt":"2025-12-28T09:16:10Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi!\n\nCould I get some code review on this? Maybe it's ready to be merged?\n\n\nHarald\n"},{"id":"532790","messageId":"xmqqjyy6kew6.fsf@gitster.g","threadId":"64668","inReplyTo":"pull.2138.v8.git.git.1766666006561.gitgitgadget@gmail.com","subject":"Re: [PATCH v8] status: show comparison with configured goal branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-28T11:46:33Z","receivedAt":"2025-12-28T11:46:36Z","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> +test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout work >/dev/null &&\n\nWhat is this redirecction for?\n\n> +\t\tgit config status.goalBranch origin/main &&\n> +\t\tgit status --long -b\n> +\t) >actual &&\n\nInstead of redirecting the whole thing, if you are grabbing the\noutput from \"git status\", do it more like this, probably:\n\n\t(\n\t\tcd test &&\n\t\t... &&\n\t\tgit status --long --branch >../actual\n\t)\n\n> +\tcat >expect <<-\\EOF &&\n\nLooking up what \"<<-\\EOF\" means, it makes little sense to have these\nlines ...\n\n> +On branch work\n> +Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n> +  (use \"git push\" to publish your local commits)\n> +\n> +Ahead of '\\''origin/main'\\'' by 3 commits.\n> +\n> +nothing to commit, working tree clean\n> +EOF\n\n... abut the left edge of the page.  Unlike <<\\EOF, the dash sign\ntells the shell that it should remove the leading tab from the line\nbefore feeding \"cat\", and the point of using that construct \"<<-\\EOF\"\nto begin with is so that you can indent the here doc to the same\nlevel as the command text.  IOW, you use \"<<-\\EOF\" only because you\nwant to avoid these ugly lines that are sticking to the left, like\nthe above.  Instead you can do this:\n\n\tcat >expect <<-\\EOF &&\n\tOn branch work\n\tYour branch is ...\n\t  (use \"git push\" ...\n\t...\n\tEOF\n\nand the shell strips the leading tabs from these lines.\n\n> +\ttest_cmp expect actual\n> +'\n\n\n> +test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout main >/dev/null &&\n> +\t\tgit config status.goalBranch origin/main &&\n> +\t\tgit checkout work 2>&1\n\nLikewise.\n\n> +\t) >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +Switched to branch '\\''work'\\''\n> +Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n> +  (use \"git push\" to publish your local commits)\n> +\n> +Ahead of '\\''origin/main'\\'' by 3 commits.\n> +EOF\n\nLikewise.\n\nAlso, doesn't $SQ work here, i.e.\n\n\tcat >expect <<-EOF &&\n\tSwitched to branch ${SQ}work${SQ}\n\tYour branch is ahead of ${SQ}...${SQ} by 2 commits.\n\t...\n\tEOF\n\nAs you want interpolation if you go this route, we lose quote from\nthe end of here-doc token and write \"<<-EOF\" here, instead of\n\"<<-\\EOF\".\n\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status tracking origin/main shows only main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout b4 >/dev/null &&\n> +\t\tgit status --long -b\n\nLikewise.\n\n> +\t) >actual &&\n> +\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n> +\ttest_grep ! \"Ahead of\" actual\n> +'\n\nI'll stop here.\n"},{"id":"532792","messageId":"pull.2138.v9.git.git.1766936483.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v8.git.git.1766666006561.gitgitgadget@gmail.com","subject":"[PATCH v9 0/2] status: show comparison with configured goal branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-28T15:41:21Z","receivedAt":"2025-12-28T15:41:27Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\n\nHarald Nordgren (2):\n  status: show comparison with configured goal branch\n  improve tests\n\n remote.c                 |  92 +++++++++++\n t/t6040-tracking-info.sh | 339 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 431 insertions(+)\n\n\nbase-commit: 7c7698a654a7a0031f65b0ab0c1c4e438e95df60\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v9\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v9\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v8:\n\n 1:  7e2574d5ae = 1:  ecfe122585 status: show comparison with configured goal branch\n -:  ---------- > 2:  53bab23737 improve tests\n\n-- \ngitgitgadget\n"},{"id":"532793","messageId":"ecfe122585617c1a1a3862fc787e9d099d69723e.1766936483.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v9.git.git.1766936483.gitgitgadget@gmail.com","subject":"[PATCH v9 1/2] status: show comparison with configured goal branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-28T15:41:22Z","receivedAt":"2025-12-28T15:41:28Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to a goal branch (e.g.,\nupstream/main), you can configure status.goalBranch to show an\nadditional comparison.\n\nAdd support for status.goalBranch configuration option that specifies\na remote/branch to compare against. When set, git status shows both\nthe comparison with the tracking branch (as before) and an additional\ncomparison with the configured goal branch, if it differs from the\ntracking branch. The goal branch comparison appears on a separate\nline after the tracking branch status, using the same format:\n- \"Ahead of 'upstream/main' by N commits\" when purely ahead\n- \"Behind 'upstream/main' by N commits\" when purely behind\n- \"Diverged from 'upstream/main' by N commits\" when diverged\n\nExample output when tracking a feature branch with status.goalBranch\nset to upstream/main:\n    On branch feature\n    Your branch is ahead of 'origin/feature' by 2 commits.\n      (use \"git push\" to publish your local commits)\n\n    Ahead of 'upstream/main' by 5 commits.\n\nThe comparison is only shown when status.goalBranch is configured\nand the goal branch differs from the tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 |  92 +++++++++++\n t/t6040-tracking-info.sh | 340 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 432 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..7e13c027b5 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,84 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_goal_branch_ref(char **full_ref_out)\n+{\n+\tconst char *config_value;\n+\tconst char *resolved;\n+\tint flag;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *slash_pos;\n+\tchar *ret = NULL;\n+\n+\tif (repo_config_get_value(the_repository, \"status.goalBranch\", &config_value))\n+\t\treturn NULL;\n+\n+\tif (!config_value || !*config_value)\n+\t\treturn NULL;\n+\n+\tslash_pos = strchr(config_value, '/');\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n+\t\twarning(_(\"invalid value for status.goalBranch: '%s' (expected format: remote/branch)\"),\n+\t\t\tconfig_value);\n+\t\treturn NULL;\n+\t}\n+\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n+\t\t    (int)(slash_pos - config_value), config_value,\n+\t\t    slash_pos + 1);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, &flag);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t}\n+\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n+}\n+\n+static void format_goal_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     const char *goal_full,\n+\t\t\t\t\t     const char *goal_short,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint goal_ahead = 0, goal_behind = 0;\n+\n+\tif (stat_branch_pair(branch_refname, goal_full,\n+\t\t\t     &goal_ahead, &goal_behind, abf) <= 0)\n+\t\treturn;\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (goal_ahead > 0 && goal_behind == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   goal_ahead),\n+\t\t\tgoal_short, goal_ahead);\n+\t} else if (goal_behind > 0 && goal_ahead == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   goal_behind),\n+\t\t\tgoal_short, goal_behind);\n+\t} else if (goal_ahead > 0 && goal_behind > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   goal_ahead + goal_behind),\n+\t\t\tgoal_short, goal_ahead + goal_behind);\n+\t}\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2258,6 +2336,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2311,6 +2390,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+\n+\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tchar *goal_full = NULL;\n+\t\tchar *goal_short = get_goal_branch_ref(&goal_full);\n+\n+\t\tif (goal_short && strcmp(base, goal_short))\n+\t\t\tformat_goal_branch_comparison(sb, branch->refname, goal_full,\n+\t\t\t\t\t\t     goal_short, abf);\n+\n+\t\tfree(goal_short);\n+\t\tfree(goal_full);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..fe34ddf0ab 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,6 +21,7 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -292,4 +293,343 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup for ahead of non-main tracking branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature origin/main &&\n+\t\tadvance feature1 &&\n+\t\tgit push origin feature &&\n+\t\tgit checkout -b work --track origin/feature &&\n+\t\tadvance work1 &&\n+\t\tadvance work2\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit checkout work 2>&1\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+Switched to branch '\\''work'\\''\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 >/dev/null &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n+\ttest_grep ! \"Ahead of\" actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b oldfeature &&\n+\t\tadvance oldfeature1 &&\n+\t\tgit push origin oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_newer &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout -b work2 --track origin/oldfeature &&\n+\t\tadvance work2_commit\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2\n+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for diverged from tracked but behind main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b work2b &&\n+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_extra &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2b >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2b\n+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n+and have 1 and 1 different commits each, respectively.\n+  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+Behind '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for behind tracked but ahead of main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b feature3 &&\n+\t\tadvance feature3_1 &&\n+\t\tadvance feature3_2 &&\n+\t\tadvance feature3_3 &&\n+\t\tgit push origin feature3 &&\n+\t\tgit checkout -b work3 --track origin/feature3 &&\n+\t\tgit reset --hard HEAD~2\n+\t)\n+'\n+\n+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work3\n+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n+  (use \"git pull\" to update your local branch)\n+\n+Ahead of '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote preference' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t)\n+'\n+\n+test_expect_success 'status prefers upstream remote over origin for comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch upstream/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''upstream/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit config status.goalBranch upstream/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''upstream/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote remove upstream &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead2 &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature2\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature3 --track origin/feature\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch unset shows no default comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config --unset status.goalBranch 2>/dev/null || true &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set uses configured branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set to different remote/branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch origin/feature &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set to non-existent branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/nonexistent &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"532794","messageId":"53bab23737eb0e60e18380f2347a09d613c68a35.1766936483.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v9.git.git.1766936483.gitgitgadget@gmail.com","subject":"[PATCH v9 2/2] improve tests","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-28T15:41:23Z","receivedAt":"2025-12-28T15:41:30Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nSimplify tests based on feedback.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n t/t6040-tracking-info.sh | 249 +++++++++++++++++++--------------------\n 1 file changed, 124 insertions(+), 125 deletions(-)\n\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex fe34ddf0ab..a875b4c73b 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -308,45 +308,44 @@ test_expect_success 'setup for ahead of non-main tracking branch' '\n test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work >/dev/null &&\n+\t\tgit checkout work &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work\n-Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n-  (use \"git push\" to publish your local commits)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work\n+\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n \n-Ahead of '\\''origin/main'\\'' by 3 commits.\n+\tAhead of ${SQ}origin/main${SQ} by 3 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout main >/dev/null &&\n+\t\tgit checkout main &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit checkout work 2>&1\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-Switched to branch '\\''work'\\''\n-Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n-  (use \"git push\" to publish your local commits)\n+\t\tgit checkout work >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n \n-Ahead of '\\''origin/main'\\'' by 3 commits.\n-EOF\n+\tAhead of ${SQ}origin/main${SQ} by 3 commits.\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'status tracking origin/main shows only main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout b4 >/dev/null &&\n-\t\tgit status --long -b\n-\t) >actual &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n \ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n \ttest_grep ! \"Ahead of\" actual\n '\n@@ -369,19 +368,19 @@ test_expect_success 'setup for ahead of tracked but diverged from main' '\n test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work2 >/dev/null &&\n+\t\tgit checkout work2 &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work2\n-Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n-  (use \"git push\" to publish your local commits)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work2\n+\tYour branch is ahead of ${SQ}origin/oldfeature${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n \n-Diverged from '\\''origin/main'\\'' by 3 commits.\n+\tDiverged from ${SQ}origin/main${SQ} by 3 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -401,20 +400,20 @@ test_expect_success 'setup for diverged from tracked but behind main' '\n test_expect_success 'status shows diverged from tracked and behind origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work2b >/dev/null &&\n+\t\tgit checkout work2b &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work2b\n-Your branch and '\\''origin/oldfeature'\\'' have diverged,\n-and have 1 and 1 different commits each, respectively.\n-  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work2b\n+\tYour branch and ${SQ}origin/oldfeature${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n \n-Behind '\\''origin/main'\\'' by 1 commit.\n+\tBehind ${SQ}origin/main${SQ} by 1 commit.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -436,19 +435,19 @@ test_expect_success 'setup for behind tracked but ahead of main' '\n test_expect_success 'status shows behind tracked and ahead of origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work3 >/dev/null &&\n+\t\tgit checkout work3 &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work3\n-Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n-  (use \"git pull\" to update your local branch)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work3\n+\tYour branch is behind ${SQ}origin/feature3${SQ} by 2 commits, and can be fast-forwarded.\n+\t  (use \"git pull\" to update your local branch)\n \n-Ahead of '\\''origin/main'\\'' by 1 commit.\n+\tAhead of ${SQ}origin/main${SQ} by 1 commit.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -464,19 +463,19 @@ test_expect_success 'setup upstream remote preference' '\n test_expect_success 'status prefers upstream remote over origin for comparison' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work >/dev/null &&\n+\t\tgit checkout work &&\n \t\tgit config status.goalBranch upstream/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work\n-Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n-  (use \"git push\" to publish your local commits)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work\n+\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n \n-Diverged from '\\''upstream/main'\\'' by 5 commits.\n+\tDiverged from ${SQ}upstream/main${SQ} by 5 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -494,18 +493,18 @@ test_expect_success 'setup for up to date with tracked but ahead of default' '\n test_expect_success 'status shows up to date with tracked but diverged from default' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit checkout synced_feature &&\n \t\tgit config status.goalBranch upstream/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-Diverged from '\\''upstream/main'\\'' by 3 commits.\n+\tDiverged from ${SQ}upstream/main${SQ} by 3 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -524,18 +523,18 @@ test_expect_success 'setup for up to date with tracked but ahead of origin/main'\n test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit checkout synced_feature2 &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature2\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature2\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-Diverged from '\\''origin/main'\\'' by 5 commits.\n+\tDiverged from ${SQ}origin/main${SQ} by 5 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -550,85 +549,85 @@ test_expect_success 'setup for up to date with tracked but purely ahead of origi\n test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit checkout synced_feature3 &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature3\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature3\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-Diverged from '\\''origin/main'\\'' by 5 commits.\n+\tDiverged from ${SQ}origin/main${SQ} by 5 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'status with status.goalBranch unset shows no default comparison' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature3 >/dev/null &&\n-\t\tgit config --unset status.goalBranch 2>/dev/null || true &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature3\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit checkout synced_feature3 &&\n+\t\tgit config --unset status.goalBranch || true &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature3\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'status with status.goalBranch set uses configured branch' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit checkout synced_feature3 &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature3\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature3\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-Diverged from '\\''origin/main'\\'' by 5 commits.\n+\tDiverged from ${SQ}origin/main${SQ} by 5 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'status with status.goalBranch set to different remote/branch' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work >/dev/null &&\n+\t\tgit checkout work &&\n \t\tgit config status.goalBranch origin/feature &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work\n-Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n-  (use \"git push\" to publish your local commits)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work\n+\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'status with status.goalBranch set to non-existent branch' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit checkout synced_feature3 &&\n \t\tgit config status.goalBranch origin/nonexistent &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature3\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature3\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n-- \ngitgitgadget\n"},{"id":"532795","messageId":"20251228154654.82235-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqjyy6kew6.fsf@gitster.g","subject":"Code review?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-28T15:46:54Z","receivedAt":"2025-12-28T15:46:56Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Thanks a lot, I sent a new patch!\n"},{"id":"532802","messageId":"xmqqbjjijt2u.fsf@gitster.g","threadId":"64668","inReplyTo":"20251228091607.14896-1-haraldnordgren@gmail.com","subject":"Re: Code review?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-28T19:37:45Z","receivedAt":"2025-12-28T19:37:48Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> Could I get some code review on this? Maybe it's ready to be merged?\n\nAt least not from me at the code level, not yet for now.\n\nI still do not quite get why you need a new configuration variable,\nso the problem I had were still at the design level.\n\nA happy new year.\n"},{"id":"532803","messageId":"20251228201613.83476-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqbjjijt2u.fsf@gitster.g","subject":"Code review?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-28T20:16:13Z","receivedAt":"2025-12-28T20:16:17Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi!\n\nThe config variable solves to problem of finding which ”goal branch” to compare to, which I otherwise find unsolvable. I took it from the previous discussion that trying to extract the default branch from the remote is not a good idea.\n\nIs the solution through using the remote/pushRemote?\n\nIf that’s the solution they are set per branch (as I understand it), so each time checking out a new branch this ”goal branch” comparison would be lost and has to be configured again. That ruins the feature.\n\nLooking at the git repositories I have on my machine, none of them have pushRemote set up. And the remote setting is pointing to my fork, never to upstream, I think most people have it like that.\n\nI appreciate all the help so far! Happy new year!\n\n\nHarald\n"},{"id":"532805","messageId":"B70285B3-5197-404C-AA4D-AA3E6FEEE321@gmail.com","threadId":"64668","inReplyTo":"20251228201613.83476-1-haraldnordgren@gmail.com","subject":"Re: Code review?","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-12-28T23:09:45Z","receivedAt":"2025-12-28T23:09:57Z","isPatch":false,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> \n> Le 28 déc. 2025 à 15:16, Harald Nordgren <haraldnordgren@gmail.com> a écrit :\n> \n> ﻿Hi!\n> \n> The config variable solves to problem of finding which ”goal branch” to compare to, which I otherwise find unsolvable. I took it from the previous discussion that trying to extract the default branch from the remote is not a good idea.\n> \n> Is the solution through using the remote/pushRemote?\n> \n> If that’s the solution they are set per branch (as I understand it), so each time checking out a new branch this ”goal branch” comparison would be lost and has to be configured again. That ruins the feature.\n> \n> Looking at the git repositories I have on my machine, none of them have pushRemote set up. And the remote setting is pointing to my fork, never to upstream, I think most people have it like that.\n> \n> I appreciate all the help so far! Happy new year!\n> \n> \n> Harald\n> \n\nWhen I get to it, I intend to send a short email describing how I configure Git for triangular workflows. "},{"id":"532809","messageId":"20251229121731.90086-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"B70285B3-5197-404C-AA4D-AA3E6FEEE321@gmail.com","subject":"Triangular workflows","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-29T12:17:31Z","receivedAt":"2025-12-29T12:17:34Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi Ben!\n\nThat would be very nice, thanks!\n\n\nHarald\n"},{"id":"532844","messageId":"pull.2138.v10.git.git.1767110888.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v9.git.git.1766936483.gitgitgadget@gmail.com","subject":"[PATCH v10 0/3] status: show additional comparison with push branch when different from tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-30T16:08:05Z","receivedAt":"2025-12-30T16:08:10Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com\n\nHarald Nordgren (3):\n  status: show comparison with configured goal branch\n  improve tests\n  use pushRemote and tracking branch\n\n remote.c                 |  89 +++++++++++++++++++++\n t/t6040-tracking-info.sh | 167 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 256 insertions(+)\n\n\nbase-commit: 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v10\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v10\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v9:\n\n 1:  ecfe122585 = 1:  43a75944fb status: show comparison with configured goal branch\n 2:  53bab23737 = 2:  e6d24b8b6a improve tests\n -:  ---------- > 3:  13c2a03b0a use pushRemote and tracking branch\n\n-- \ngitgitgadget\n"},{"id":"532846","messageId":"43a75944fb8082cc4716c007899d2335bb441de8.1767110888.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v10.git.git.1767110888.gitgitgadget@gmail.com","subject":"[PATCH v10 1/3] status: show comparison with configured goal branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-30T16:08:06Z","receivedAt":"2025-12-30T16:08:12Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to a goal branch (e.g.,\nupstream/main), you can configure status.goalBranch to show an\nadditional comparison.\n\nAdd support for status.goalBranch configuration option that specifies\na remote/branch to compare against. When set, git status shows both\nthe comparison with the tracking branch (as before) and an additional\ncomparison with the configured goal branch, if it differs from the\ntracking branch. The goal branch comparison appears on a separate\nline after the tracking branch status, using the same format:\n- \"Ahead of 'upstream/main' by N commits\" when purely ahead\n- \"Behind 'upstream/main' by N commits\" when purely behind\n- \"Diverged from 'upstream/main' by N commits\" when diverged\n\nExample output when tracking a feature branch with status.goalBranch\nset to upstream/main:\n    On branch feature\n    Your branch is ahead of 'origin/feature' by 2 commits.\n      (use \"git push\" to publish your local commits)\n\n    Ahead of 'upstream/main' by 5 commits.\n\nThe comparison is only shown when status.goalBranch is configured\nand the goal branch differs from the tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 |  92 +++++++++++\n t/t6040-tracking-info.sh | 340 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 432 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..7e13c027b5 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,84 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_goal_branch_ref(char **full_ref_out)\n+{\n+\tconst char *config_value;\n+\tconst char *resolved;\n+\tint flag;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *slash_pos;\n+\tchar *ret = NULL;\n+\n+\tif (repo_config_get_value(the_repository, \"status.goalBranch\", &config_value))\n+\t\treturn NULL;\n+\n+\tif (!config_value || !*config_value)\n+\t\treturn NULL;\n+\n+\tslash_pos = strchr(config_value, '/');\n+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n+\t\twarning(_(\"invalid value for status.goalBranch: '%s' (expected format: remote/branch)\"),\n+\t\t\tconfig_value);\n+\t\treturn NULL;\n+\t}\n+\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n+\t\t    (int)(slash_pos - config_value), config_value,\n+\t\t    slash_pos + 1);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, &flag);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t}\n+\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n+}\n+\n+static void format_goal_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     const char *goal_full,\n+\t\t\t\t\t     const char *goal_short,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint goal_ahead = 0, goal_behind = 0;\n+\n+\tif (stat_branch_pair(branch_refname, goal_full,\n+\t\t\t     &goal_ahead, &goal_behind, abf) <= 0)\n+\t\treturn;\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (goal_ahead > 0 && goal_behind == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   goal_ahead),\n+\t\t\tgoal_short, goal_ahead);\n+\t} else if (goal_behind > 0 && goal_ahead == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   goal_behind),\n+\t\t\tgoal_short, goal_behind);\n+\t} else if (goal_ahead > 0 && goal_behind > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   goal_ahead + goal_behind),\n+\t\t\tgoal_short, goal_ahead + goal_behind);\n+\t}\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2258,6 +2336,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2311,6 +2390,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+\n+\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tchar *goal_full = NULL;\n+\t\tchar *goal_short = get_goal_branch_ref(&goal_full);\n+\n+\t\tif (goal_short && strcmp(base, goal_short))\n+\t\t\tformat_goal_branch_comparison(sb, branch->refname, goal_full,\n+\t\t\t\t\t\t     goal_short, abf);\n+\n+\t\tfree(goal_short);\n+\t\tfree(goal_full);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..fe34ddf0ab 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,6 +21,7 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -292,4 +293,343 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup for ahead of non-main tracking branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature origin/main &&\n+\t\tadvance feature1 &&\n+\t\tgit push origin feature &&\n+\t\tgit checkout -b work --track origin/feature &&\n+\t\tadvance work1 &&\n+\t\tadvance work2\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit checkout work 2>&1\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+Switched to branch '\\''work'\\''\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Ahead of '\\''origin/main'\\'' by 3 commits.\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 >/dev/null &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n+\ttest_grep ! \"Ahead of\" actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b oldfeature &&\n+\t\tadvance oldfeature1 &&\n+\t\tgit push origin oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_newer &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout -b work2 --track origin/oldfeature &&\n+\t\tadvance work2_commit\n+\t)\n+'\n+\n+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2\n+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''origin/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for diverged from tracked but behind main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b work2b &&\n+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_extra &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work2b >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work2b\n+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n+and have 1 and 1 different commits each, respectively.\n+  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+Behind '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for behind tracked but ahead of main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit fetch origin &&\n+\t\tgit checkout origin/main &&\n+\t\tgit checkout -b feature3 &&\n+\t\tadvance feature3_1 &&\n+\t\tadvance feature3_2 &&\n+\t\tadvance feature3_3 &&\n+\t\tgit push origin feature3 &&\n+\t\tgit checkout -b work3 --track origin/feature3 &&\n+\t\tgit reset --hard HEAD~2\n+\t)\n+'\n+\n+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work3\n+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n+  (use \"git pull\" to update your local branch)\n+\n+Ahead of '\\''origin/main'\\'' by 1 commit.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote preference' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t)\n+'\n+\n+test_expect_success 'status prefers upstream remote over origin for comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch upstream/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+Diverged from '\\''upstream/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from default' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit config status.goalBranch upstream/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''upstream/main'\\'' by 3 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote remove upstream &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance main_ahead2 &&\n+\t\tgit push origin HEAD:main\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature2\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout origin/feature &&\n+\t\tgit checkout -b synced_feature3 --track origin/feature\n+\t)\n+'\n+\n+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch unset shows no default comparison' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config --unset status.goalBranch 2>/dev/null || true &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set uses configured branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/main &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+Diverged from '\\''origin/main'\\'' by 5 commits.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set to different remote/branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout work >/dev/null &&\n+\t\tgit config status.goalBranch origin/feature &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch work\n+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n+  (use \"git push\" to publish your local commits)\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with status.goalBranch set to non-existent branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit config status.goalBranch origin/nonexistent &&\n+\t\tgit status --long -b\n+\t) >actual &&\n+\tcat >expect <<-\\EOF &&\n+On branch synced_feature3\n+Your branch is up to date with '\\''origin/feature'\\''.\n+\n+nothing to commit, working tree clean\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"532845","messageId":"e6d24b8b6ac7282638591051cbe4cb60ac57fd58.1767110888.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v10.git.git.1767110888.gitgitgadget@gmail.com","subject":"[PATCH v10 2/3] improve tests","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-30T16:08:07Z","receivedAt":"2025-12-30T16:08:13Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nSimplify tests based on feedback.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n t/t6040-tracking-info.sh | 249 +++++++++++++++++++--------------------\n 1 file changed, 124 insertions(+), 125 deletions(-)\n\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex fe34ddf0ab..a875b4c73b 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -308,45 +308,44 @@ test_expect_success 'setup for ahead of non-main tracking branch' '\n test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work >/dev/null &&\n+\t\tgit checkout work &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work\n-Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n-  (use \"git push\" to publish your local commits)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work\n+\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n \n-Ahead of '\\''origin/main'\\'' by 3 commits.\n+\tAhead of ${SQ}origin/main${SQ} by 3 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout main >/dev/null &&\n+\t\tgit checkout main &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit checkout work 2>&1\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-Switched to branch '\\''work'\\''\n-Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n-  (use \"git push\" to publish your local commits)\n+\t\tgit checkout work >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n \n-Ahead of '\\''origin/main'\\'' by 3 commits.\n-EOF\n+\tAhead of ${SQ}origin/main${SQ} by 3 commits.\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'status tracking origin/main shows only main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout b4 >/dev/null &&\n-\t\tgit status --long -b\n-\t) >actual &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n \ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n \ttest_grep ! \"Ahead of\" actual\n '\n@@ -369,19 +368,19 @@ test_expect_success 'setup for ahead of tracked but diverged from main' '\n test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work2 >/dev/null &&\n+\t\tgit checkout work2 &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work2\n-Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n-  (use \"git push\" to publish your local commits)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work2\n+\tYour branch is ahead of ${SQ}origin/oldfeature${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n \n-Diverged from '\\''origin/main'\\'' by 3 commits.\n+\tDiverged from ${SQ}origin/main${SQ} by 3 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -401,20 +400,20 @@ test_expect_success 'setup for diverged from tracked but behind main' '\n test_expect_success 'status shows diverged from tracked and behind origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work2b >/dev/null &&\n+\t\tgit checkout work2b &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work2b\n-Your branch and '\\''origin/oldfeature'\\'' have diverged,\n-and have 1 and 1 different commits each, respectively.\n-  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work2b\n+\tYour branch and ${SQ}origin/oldfeature${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n \n-Behind '\\''origin/main'\\'' by 1 commit.\n+\tBehind ${SQ}origin/main${SQ} by 1 commit.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -436,19 +435,19 @@ test_expect_success 'setup for behind tracked but ahead of main' '\n test_expect_success 'status shows behind tracked and ahead of origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work3 >/dev/null &&\n+\t\tgit checkout work3 &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work3\n-Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n-  (use \"git pull\" to update your local branch)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work3\n+\tYour branch is behind ${SQ}origin/feature3${SQ} by 2 commits, and can be fast-forwarded.\n+\t  (use \"git pull\" to update your local branch)\n \n-Ahead of '\\''origin/main'\\'' by 1 commit.\n+\tAhead of ${SQ}origin/main${SQ} by 1 commit.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -464,19 +463,19 @@ test_expect_success 'setup upstream remote preference' '\n test_expect_success 'status prefers upstream remote over origin for comparison' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work >/dev/null &&\n+\t\tgit checkout work &&\n \t\tgit config status.goalBranch upstream/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work\n-Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n-  (use \"git push\" to publish your local commits)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work\n+\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n \n-Diverged from '\\''upstream/main'\\'' by 5 commits.\n+\tDiverged from ${SQ}upstream/main${SQ} by 5 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -494,18 +493,18 @@ test_expect_success 'setup for up to date with tracked but ahead of default' '\n test_expect_success 'status shows up to date with tracked but diverged from default' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature >/dev/null &&\n+\t\tgit checkout synced_feature &&\n \t\tgit config status.goalBranch upstream/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-Diverged from '\\''upstream/main'\\'' by 3 commits.\n+\tDiverged from ${SQ}upstream/main${SQ} by 3 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -524,18 +523,18 @@ test_expect_success 'setup for up to date with tracked but ahead of origin/main'\n test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature2 >/dev/null &&\n+\t\tgit checkout synced_feature2 &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature2\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature2\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-Diverged from '\\''origin/main'\\'' by 5 commits.\n+\tDiverged from ${SQ}origin/main${SQ} by 5 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n@@ -550,85 +549,85 @@ test_expect_success 'setup for up to date with tracked but purely ahead of origi\n test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit checkout synced_feature3 &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature3\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature3\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-Diverged from '\\''origin/main'\\'' by 5 commits.\n+\tDiverged from ${SQ}origin/main${SQ} by 5 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'status with status.goalBranch unset shows no default comparison' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature3 >/dev/null &&\n-\t\tgit config --unset status.goalBranch 2>/dev/null || true &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature3\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit checkout synced_feature3 &&\n+\t\tgit config --unset status.goalBranch || true &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature3\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'status with status.goalBranch set uses configured branch' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit checkout synced_feature3 &&\n \t\tgit config status.goalBranch origin/main &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature3\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature3\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-Diverged from '\\''origin/main'\\'' by 5 commits.\n+\tDiverged from ${SQ}origin/main${SQ} by 5 commits.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'status with status.goalBranch set to different remote/branch' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work >/dev/null &&\n+\t\tgit checkout work &&\n \t\tgit config status.goalBranch origin/feature &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch work\n-Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n-  (use \"git push\" to publish your local commits)\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch work\n+\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success 'status with status.goalBranch set to non-existent branch' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature3 >/dev/null &&\n+\t\tgit checkout synced_feature3 &&\n \t\tgit config status.goalBranch origin/nonexistent &&\n-\t\tgit status --long -b\n-\t) >actual &&\n-\tcat >expect <<-\\EOF &&\n-On branch synced_feature3\n-Your branch is up to date with '\\''origin/feature'\\''.\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch synced_feature3\n+\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n \n-nothing to commit, working tree clean\n-EOF\n+\tnothing to commit, working tree clean\n+\tEOF\n \ttest_cmp expect actual\n '\n \n-- \ngitgitgadget\n\n"},{"id":"532847","messageId":"13c2a03b0a25054cdaae030a52b1afddebce56a9.1767110888.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v10.git.git.1767110888.gitgitgadget@gmail.com","subject":"[PATCH v10 3/3] use pushRemote and tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-30T16:08:08Z","receivedAt":"2025-12-30T16:08:15Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nUse them for comparisons instead of config variable.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 |  71 +++++-----\n t/t6040-tracking-info.sh | 296 ++++++++-------------------------------\n 2 files changed, 96 insertions(+), 271 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 7e13c027b5..2317725f7d 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,31 +2237,22 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-static char *get_goal_branch_ref(char **full_ref_out)\n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n {\n-\tconst char *config_value;\n+\tconst char *push_remote;\n \tconst char *resolved;\n \tint flag;\n \tstruct strbuf ref_buf = STRBUF_INIT;\n-\tchar *slash_pos;\n \tchar *ret = NULL;\n \n-\tif (repo_config_get_value(the_repository, \"status.goalBranch\", &config_value))\n-\t\treturn NULL;\n-\n-\tif (!config_value || !*config_value)\n+\tif (!branch)\n \t\treturn NULL;\n \n-\tslash_pos = strchr(config_value, '/');\n-\tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n-\t\twarning(_(\"invalid value for status.goalBranch: '%s' (expected format: remote/branch)\"),\n-\t\t\tconfig_value);\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n \t\treturn NULL;\n-\t}\n \n-\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n-\t\t    (int)(slash_pos - config_value), config_value,\n-\t\t    slash_pos + 1);\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%s/%s\", push_remote, branch->name);\n \n \tresolved = refs_resolve_ref_unsafe(\n \t\tget_main_ref_store(the_repository),\n@@ -2280,38 +2271,44 @@ static char *get_goal_branch_ref(char **full_ref_out)\n \treturn ret;\n }\n \n-static void format_goal_branch_comparison(struct strbuf *sb,\n+static void format_push_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t     const char *branch_refname,\n-\t\t\t\t\t     const char *goal_full,\n-\t\t\t\t\t     const char *goal_short,\n+\t\t\t\t\t     const char *push_full,\n+\t\t\t\t\t     const char *push_short,\n \t\t\t\t\t     enum ahead_behind_flags abf)\n {\n-\tint goal_ahead = 0, goal_behind = 0;\n+\tint push_ahead = 0, push_behind = 0;\n+\tint stat_result;\n \n-\tif (stat_branch_pair(branch_refname, goal_full,\n-\t\t\t     &goal_ahead, &goal_behind, abf) <= 0)\n+\tstat_result = stat_branch_pair(branch_refname, push_full,\n+\t\t\t\t       &push_ahead, &push_behind, abf);\n+\tif (stat_result < 0)\n \t\treturn;\n \n \tstrbuf_addstr(sb, \"\\n\");\n \n-\tif (goal_ahead > 0 && goal_behind == 0) {\n+\tif (stat_result == 0 || (push_ahead == 0 && push_behind == 0)) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n+\t\t\tpush_short);\n+\t} else if (push_ahead > 0 && push_behind == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n-\t\t\t   goal_ahead),\n-\t\t\tgoal_short, goal_ahead);\n-\t} else if (goal_behind > 0 && goal_ahead == 0) {\n+\t\t\t   push_ahead),\n+\t\t\tpush_short, push_ahead);\n+\t} else if (push_behind > 0 && push_ahead == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n \t\t\t   \"Behind '%s' by %d commits.\\n\",\n-\t\t\t   goal_behind),\n-\t\t\tgoal_short, goal_behind);\n-\t} else if (goal_ahead > 0 && goal_behind > 0) {\n+\t\t\t   push_behind),\n+\t\t\tpush_short, push_behind);\n+\t} else if (push_ahead > 0 && push_behind > 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n \t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n-\t\t\t   goal_ahead + goal_behind),\n-\t\t\tgoal_short, goal_ahead + goal_behind);\n+\t\t\t   push_ahead + push_behind),\n+\t\t\tpush_short, push_ahead + push_behind);\n \t}\n }\n \n@@ -2392,15 +2389,15 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t}\n \n \tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n-\t\tchar *goal_full = NULL;\n-\t\tchar *goal_short = get_goal_branch_ref(&goal_full);\n+\t\tchar *push_full = NULL;\n+\t\tchar *push_short = get_remote_push_branch(branch, &push_full);\n \n-\t\tif (goal_short && strcmp(base, goal_short))\n-\t\t\tformat_goal_branch_comparison(sb, branch->refname, goal_full,\n-\t\t\t\t\t\t     goal_short, abf);\n+\t\tif (push_short && strcmp(base, push_short))\n+\t\t\tformat_push_branch_comparison(sb, branch->refname, push_full,\n+\t\t\t\t\t\t     push_short, abf);\n \n-\t\tfree(goal_short);\n-\t\tfree(goal_full);\n+\t\tfree(push_short);\n+\t\tfree(push_full);\n \t}\n \n \tfree(base);\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex a875b4c73b..f27ae719ad 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -21,7 +21,6 @@ test_expect_success setup '\n \tgit clone . test &&\n \t(\n \t\tcd test &&\n-\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n \t\tgit checkout -b b1 origin &&\n \t\tgit reset --hard HEAD^ &&\n \t\tadvance d &&\n@@ -293,340 +292,169 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'setup for ahead of non-main tracking branch' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout -b feature origin/main &&\n-\t\tadvance feature1 &&\n-\t\tgit push origin feature &&\n-\t\tgit checkout -b work --track origin/feature &&\n-\t\tadvance work1 &&\n-\t\tadvance work2\n-\t)\n-'\n-\n-test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n+test_expect_success 'status tracking origin/main shows only main' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work &&\n-\t\tgit config status.goalBranch origin/main &&\n+\t\tgit checkout b4 &&\n \t\tgit status >../actual\n \t) &&\n \tcat >expect <<-EOF &&\n-\tOn branch work\n-\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n \t  (use \"git push\" to publish your local commits)\n \n-\tAhead of ${SQ}origin/main${SQ} by 3 commits.\n-\n \tnothing to commit, working tree clean\n \tEOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout main &&\n-\t\tgit config status.goalBranch origin/main &&\n-\t\tgit checkout work >../actual\n-\t) &&\n-\tcat >expect <<-EOF &&\n-\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n-\t  (use \"git push\" to publish your local commits)\n-\n-\tAhead of ${SQ}origin/main${SQ} by 3 commits.\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'status tracking origin/main shows only main' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout b4 &&\n-\t\tgit status >../actual\n-\t) &&\n-\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n-\ttest_grep ! \"Ahead of\" actual\n-'\n-\n-test_expect_success 'setup for ahead of tracked but diverged from main' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout origin/main &&\n-\t\tgit checkout -b oldfeature &&\n-\t\tadvance oldfeature1 &&\n-\t\tgit push origin oldfeature &&\n-\t\tgit checkout origin/main &&\n-\t\tadvance main_newer &&\n-\t\tgit push origin HEAD:main &&\n-\t\tgit checkout -b work2 --track origin/oldfeature &&\n-\t\tadvance work2_commit\n-\t)\n-'\n-\n-test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work2 &&\n-\t\tgit config status.goalBranch origin/main &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n \t\tgit status >../actual\n \t) &&\n \tcat >expect <<-EOF &&\n-\tOn branch work2\n-\tYour branch is ahead of ${SQ}origin/oldfeature${SQ} by 1 commit.\n+\tOn branch feature2\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-\tDiverged from ${SQ}origin/main${SQ} by 3 commits.\n+\tAhead of ${SQ}origin/feature2${SQ} by 1 commit.\n \n \tnothing to commit, working tree clean\n \tEOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'setup for diverged from tracked but behind main' '\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n \t(\n \t\tcd test &&\n-\t\tgit fetch origin &&\n-\t\tgit checkout origin/main &&\n-\t\tgit checkout -b work2b &&\n-\t\tgit branch --set-upstream-to=origin/oldfeature &&\n-\t\tgit checkout origin/main &&\n-\t\tadvance main_extra &&\n-\t\tgit push origin HEAD:main\n-\t)\n-'\n-\n-test_expect_success 'status shows diverged from tracked and behind origin/main' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout work2b &&\n-\t\tgit config status.goalBranch origin/main &&\n-\t\tgit status >../actual\n+\t\tgit checkout feature2 >../actual\n \t) &&\n \tcat >expect <<-EOF &&\n-\tOn branch work2b\n-\tYour branch and ${SQ}origin/oldfeature${SQ} have diverged,\n-\tand have 1 and 1 different commits each, respectively.\n-\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n-\n-\tBehind ${SQ}origin/main${SQ} by 1 commit.\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+\tAhead of ${SQ}origin/feature2${SQ} by 1 commit.\n \tEOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'setup for behind tracked but ahead of main' '\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n \t(\n \t\tcd test &&\n-\t\tgit fetch origin &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n \t\tgit checkout origin/main &&\n-\t\tgit checkout -b feature3 &&\n-\t\tadvance feature3_1 &&\n-\t\tadvance feature3_2 &&\n-\t\tadvance feature3_3 &&\n-\t\tgit push origin feature3 &&\n-\t\tgit checkout -b work3 --track origin/feature3 &&\n-\t\tgit reset --hard HEAD~2\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n \t)\n '\n \n-test_expect_success 'status shows behind tracked and ahead of origin/main' '\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work3 &&\n-\t\tgit config status.goalBranch origin/main &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n \t\tgit status >../actual\n \t) &&\n \tcat >expect <<-EOF &&\n-\tOn branch work3\n-\tYour branch is behind ${SQ}origin/feature3${SQ} by 2 commits, and can be fast-forwarded.\n-\t  (use \"git pull\" to update your local branch)\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n \n-\tAhead of ${SQ}origin/main${SQ} by 1 commit.\n+\tAhead of ${SQ}origin/feature4${SQ} by 1 commit.\n \n \tnothing to commit, working tree clean\n \tEOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'setup upstream remote preference' '\n+test_expect_success 'setup upstream remote' '\n \t(\n \t\tcd test &&\n \t\tgit remote add upstream ../. &&\n \t\tgit fetch upstream &&\n-\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n+\t\tgit config remote.pushDefault origin\n \t)\n '\n \n-test_expect_success 'status prefers upstream remote over origin for comparison' '\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work &&\n-\t\tgit config status.goalBranch upstream/main &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n \t\tgit status >../actual\n \t) &&\n \tcat >expect <<-EOF &&\n-\tOn branch work\n-\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n+\tOn branch feature5\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-\tDiverged from ${SQ}upstream/main${SQ} by 5 commits.\n+\tAhead of ${SQ}origin/feature5${SQ} by 1 commit.\n \n \tnothing to commit, working tree clean\n \tEOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'setup for up to date with tracked but ahead of default' '\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout origin/feature &&\n-\t\tgit checkout -b synced_feature --track origin/feature &&\n-\t\tgit checkout origin/main &&\n-\t\tadvance main_ahead &&\n-\t\tgit push origin HEAD:main\n-\t)\n-'\n-\n-test_expect_success 'status shows up to date with tracked but diverged from default' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout synced_feature &&\n-\t\tgit config status.goalBranch upstream/main &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n \t\tgit status >../actual\n \t) &&\n \tcat >expect <<-EOF &&\n-\tOn branch synced_feature\n-\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n-\n-\tDiverged from ${SQ}upstream/main${SQ} by 3 commits.\n-\n-\tnothing to commit, working tree clean\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n-\t(\n-\t\tcd test &&\n-\t\tgit remote remove upstream &&\n-\t\tgit checkout origin/feature &&\n-\t\tgit checkout -b synced_feature2 --track origin/feature &&\n-\t\tgit checkout origin/main &&\n-\t\tadvance main_ahead2 &&\n-\t\tgit push origin HEAD:main\n-\t)\n-'\n-\n-test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout synced_feature2 &&\n-\t\tgit config status.goalBranch origin/main &&\n-\t\tgit status >../actual\n-\t) &&\n-\tcat >expect <<-EOF &&\n-\tOn branch synced_feature2\n-\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n-\n-\tDiverged from ${SQ}origin/main${SQ} by 5 commits.\n-\n-\tnothing to commit, working tree clean\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout origin/feature &&\n-\t\tgit checkout -b synced_feature3 --track origin/feature\n-\t)\n-'\n-\n-test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout synced_feature3 &&\n-\t\tgit config status.goalBranch origin/main &&\n-\t\tgit status >../actual\n-\t) &&\n-\tcat >expect <<-EOF &&\n-\tOn branch synced_feature3\n-\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n-\n-\tDiverged from ${SQ}origin/main${SQ} by 5 commits.\n-\n-\tnothing to commit, working tree clean\n-\tEOF\n-\ttest_cmp expect actual\n-'\n+\tOn branch feature6\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-test_expect_success 'status with status.goalBranch unset shows no default comparison' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout synced_feature3 &&\n-\t\tgit config --unset status.goalBranch || true &&\n-\t\tgit status >../actual\n-\t) &&\n-\tcat >expect <<-EOF &&\n-\tOn branch synced_feature3\n-\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n+\tDiverged from ${SQ}origin/feature6${SQ} by 2 commits.\n \n \tnothing to commit, working tree clean\n \tEOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'status with status.goalBranch set uses configured branch' '\n+test_expect_success 'status with upstream remote and push branch up to date' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout synced_feature3 &&\n-\t\tgit config status.goalBranch origin/main &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n \t\tgit status >../actual\n \t) &&\n \tcat >expect <<-EOF &&\n-\tOn branch synced_feature3\n-\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n \n-\tDiverged from ${SQ}origin/main${SQ} by 5 commits.\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n \n \tnothing to commit, working tree clean\n \tEOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'status with status.goalBranch set to different remote/branch' '\n+test_expect_success 'checkout shows push branch up to date' '\n \t(\n \t\tcd test &&\n-\t\tgit checkout work &&\n-\t\tgit config status.goalBranch origin/feature &&\n-\t\tgit status >../actual\n-\t) &&\n-\tcat >expect <<-EOF &&\n-\tOn branch work\n-\tYour branch is ahead of ${SQ}origin/feature${SQ} by 2 commits.\n-\t  (use \"git push\" to publish your local commits)\n-\n-\tnothing to commit, working tree clean\n-\tEOF\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'status with status.goalBranch set to non-existent branch' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout synced_feature3 &&\n-\t\tgit config status.goalBranch origin/nonexistent &&\n-\t\tgit status >../actual\n+\t\tgit checkout feature7 >../actual\n \t) &&\n \tcat >expect <<-EOF &&\n-\tOn branch synced_feature3\n-\tYour branch is up to date with ${SQ}origin/feature${SQ}.\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n \n-\tnothing to commit, working tree clean\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n \tEOF\n \ttest_cmp expect actual\n '\n-- \ngitgitgadget\n"},{"id":"532848","messageId":"20251230160854.42862-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqbjjijt2u.fsf@gitster.g","subject":"Re: Code review?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2025-12-30T16:08:54Z","receivedAt":"2025-12-30T16:08:57Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi!\n\nI found a way now to eliminate the config variable, and instead uses the\npush branch when it's different from the tracking branch.\n\nLet me know what you think.\n\n\nHarald\n"},{"id":"532879","messageId":"20260101194954.14649-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20251229121731.90086-1-haraldnordgren@gmail.com","subject":"Re: Code review?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-01T19:49:54Z","receivedAt":"2026-01-01T19:49:58Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi!\n\nI found a way now to eliminate the config variable, and instead uses the\npush branch when it's different from the tracking branch.\n\nLet me know what you think.\n\n\nHarald\n"},{"id":"532880","messageId":"20260101195919.14951-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"CAPx1GvfrQNao78WYfadttM=B8iyXKfxYaxTyX5w_MLZt2_bU4w@mail.gmail.com","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-01T19:59:19Z","receivedAt":"2026-01-01T19:59:21Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi Chris!\n\nCan you take a look at PATCH v10 to see if it achieves that you suggested here?\n\n\nHarald\n"},{"id":"532881","messageId":"20260101200155.15210-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"aUtCP5YvE_v9hmgM@fruit.crustytoothpaste.net","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-01T20:01:55Z","receivedAt":"2026-01-01T20:01:58Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi Brian!\n\nCan you take a look at PATCH v10 to see if it achieves that you suggested here?\n\n\nHarald\n"},{"id":"532882","messageId":"20260101200534.15457-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"CAHTeOx8nrjJVD0U0=BDTUJZNZX0Xs_b4YGbj=WSavWEPEg-9Eg@mail.gmail.com","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-01T20:05:34Z","receivedAt":"2026-01-01T20:05:37Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi Brian!\n\nCan you take a look at PATCH v10 to see if it achieves that you suggested here?\n\n\nHarald\n"},{"id":"532892","messageId":"xmqqh5t5c4lj.fsf@gitster.g","threadId":"64668","inReplyTo":"pull.2138.v10.git.git.1767110888.gitgitgadget@gmail.com","subject":"Re: [PATCH v10 0/3] status: show additional comparison with push branch when different from tracking branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-01T23:09:44Z","receivedAt":"2026-01-01T23:09:46Z","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> cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\n> ychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\n> cc: Ben Knoble ben.knoble@gmail.com\n>\n> Harald Nordgren (3):\n>   status: show comparison with configured goal branch\n>   improve tests\n>   use pushRemote and tracking branch\n\nAgain this seems to do a \"step 1 goes in a direction, step 2 fixes\nits mistake, step 3 changes course\" drunken-man's walk.\n\nThe same advice to restructure them into a logical incremental\nprogression that moves the codebase in one consistent direction to\neventually reach the goal at the end applies.\n\nI see you are now using pushremote_for_branch() that is already used\nby branch_get_push().  If that gives us \"the other thing\" that we\nwould want to compare, instead of adding yet another configuration\nvariable users need to be aware of, that is really good.\n\n"},{"id":"532894","messageId":"20260101233839.17639-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqh5t5c4lj.fsf@gitster.g","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-01T23:38:39Z","receivedAt":"2026-01-01T23:38:43Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Again this seems to do a \"step 1 goes in a direction, step 2 fixes\n> its mistake, step 3 changes course\" drunken-man's walk.\n>\n> The same advice to restructure them into a logical incremental\n> progression that moves the codebase in one consistent direction to\n> eventually reach the goal at the end applies.\n\nIsn't programming always bit of drunken-man's walk?\n\nI'm very hesitant to restructure my history before I am confident I will\nnot need any of the old work later -- I would hate to lose history if I\nmake a mistake.\n\nOne option is to keep my code backed up on a separate branch locally, but\nthis gets problematic as I add more work (endless cherry-picking and\nsquashing) between local branches before submitting new patches. So now you\nknow some of my reasoning. I'm not saying I'm right, but it's a bit\nfear-based.\n\nWith that said, my idea has always been to squash everything into a single\ncommit before merging this. The whole diff is not that big. I can split it\ninto code in one commit and tests in another.\n\nAs a side-note: In my day job we only allow \"squash and merge\" on our\nGitHub. This gives devs the flexibility to treat their branches as a WIP\narea before merging, but still gives a pristine git history after merge.\nThis feels to me like a good trade-offs. But again, happy to take\ninstructions on how to do better.\n\n> I see you are now using pushremote_for_branch() that is already used\n> by branch_get_push().  If that gives us \"the other thing\" that we\n> would want to compare, instead of adding yet another configuration\n> variable users need to be aware of, that is really good.\n\nThanks for the encouragement! I put a lot of work into the tests as well,\nI hope they tell the story of what this code achieves now.\n\n\nHarald\n"},{"id":"532911","messageId":"757d6df5-7834-4ff2-8302-8edd8e990970@app.fastmail.com","threadId":"64668","inReplyTo":"20260101233839.17639-1-haraldnordgren@gmail.com","subject":"Re: Another look?","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-01-02T09:48:46Z","receivedAt":"2026-01-02T09:49:33Z","isPatch":false,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Fri, Jan 2, 2026, at 00:38, Harald Nordgren wrote:\n>> Again this seems to do a \"step 1 goes in a direction, step 2 fixes\n>> its mistake, step 3 changes course\" drunken-man's walk.\n>>\n>> The same advice to restructure them into a logical incremental\n>> progression that moves the codebase in one consistent direction to\n>> eventually reach the goal at the end applies.\n>\n> Isn't programming always bit of drunken-man's walk?\n\nWe don’t have to present the chain of code changes as they “really\nhappened”. An alternative is to present what will eventually be the\nfinal version as if you had both a borderline perfect plan, foresight,\nand execution. And that’s the convention in this project. Because that’s\nthe natural progression of a patch series; each iteration you get help\nto arrive at what looks like the perfect iteration. (Until someone finds\na off-by-one error two years later?)\n\nWhat *really happened* is always fiction in any case.\n\n> I'm very hesitant to restructure my history before I am confident I will\n> not need any of the old work later -- I would hate to lose history if I\n> make a mistake.\n>\n> One option is to keep my code backed up on a separate branch locally, but\n> this gets problematic as I add more work (endless cherry-picking and\n> squashing) between local branches before submitting new patches. So now you\n> know some of my reasoning. I'm not saying I'm right, but it's a bit\n> fear-based.\n\nTo make a snapshot of each version seems inevitable in my book since you\nare encouraged to include a range-diff (and optionally also an\ninterdiff) between each iteration.\n\nNow you of course have the backup of each version because you can\ndownload the patches that you yourself posted. But that’s more work then\njust snapshotting each version, for me at least.\n\n> With that said, my idea has always been to squash everything into a single\n> commit before merging this. The whole diff is not that big. I can split it\n> into code in one commit and tests in another.\n>\n> As a side-note: In my day job we only allow \"squash and merge\" on our\n> GitHub. This gives devs the flexibility to treat their branches as a WIP\n> area before merging, but still gives a pristine git history after merge.\n> This feels to me like a good trade-offs. But again, happy to take\n> instructions on how to do better.\n\nIf that’s a good trade off, what are the variables involved in the trade\noff? So far it seems like:\n\n1. Flexibility to iterate like you want\n2. A final history without any back-and-forth noise (pristine/sober\n   walk)\n\nBut a third variable here is\n\n3. A series of logically separated commits\n\nAnd you don’t get that with that approach. And a good final history is\nvery important in many people’s eyes.\n\nPeople call this mandatory squash strategy some word similar to *clean*\npresumably because there is no back-and-forth noise. That’s the memetic\ncontagion. But there are other adjectives as well:\n\n• Bloated: When the mandated squash strategy forces different concerns\n  (code formatting, whitespace formatting, refactor, bug fix, ...) to be\n  truncated into one commit\n• Lossy: When so many commits get squashed that you cannot, with any\n  amount of analysis, piece together what lines in the commit message\n  correspond to some part of the diff (especially likely to happen if\n  (1) the squash commit message is a bullet list and (2) the commit\n  messages are just things like “WIP” and “fix”)\n\nSomeone might argue that the squash merges will not be large because the\npull requests are not large and the tasks are not large. Or they should\nnot be. But that demands more of both the project/task management and\nthe pull request management:\n\n1. Better project management foresight and planning. Notice that we have\n   traded the rewriting commits strategy of “perfect plan, foresight,\n   and execution” for demanding more foresight from project\n   management. Just because we have mandated no more than one commit per\n   pull request or patch series.\n\n   And “perfect plan, foresight, and execution” is not even a\n   requirement. Just a contrast to the one-commit requirement. A project\n   could allow contributors to both present “perfect plan, foresight,\n   and execution” series as well as messy series, with the latter being\n   squashed at the integrator’s/reviewer’s/mantainer’s discretion.\n2. Divide changes into more pull requests or patch series. At least with\n   vanilla GitHub that causes overhead as you have to manually link all\n   the pull requests. You might also have to manually link the pull\n   requests using URLs if the target branch of the PR does not do it for\n   you.\n\n   On the other hand you might have little or no overhead with some\n   “stacked PR” tool.\n\nThe one-commit rule works just as well if not better[1] than the\nalternatives in theory. The problem is that the theory demands much more\nfrom project management and pull request handling.\n\n† 1: Part of the motivation is often avoiding merge commits. And if\n     merge commits always cause point deductions then a perfectly\n     executed squash merge strategy will always win over some strategy\n     involving true merges.\n\n     Although I would personally choose a rebase-with-trailer strategy\n     over squash merges here; rebase the series/PR and add a trailer to\n     each commit which links to the series/PR.\n\n>\n>>[snip]\n"},{"id":"532916","messageId":"pull.2138.v11.git.git.1767352663477.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v10.git.git.1767110888.gitgitgadget@gmail.com","subject":"[PATCH v11] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-02T11:17:43Z","receivedAt":"2026-01-02T11:17:46Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push remote's\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen a push remote is configured (via branch.<name>.pushRemote or\nremote.pushDefault), git status shows both the comparison with the\nupstream tracking branch (as before) and an additional comparison with\nthe push remote's tracking branch, if it differs from the upstream\ntracking branch. The push branch comparison appears on a separate\nline after the upstream branch status, using the same format:\n- \"Ahead of 'origin/feature' by N commits\" when purely ahead\n- \"Behind 'origin/feature' by N commits\" when purely behind\n- \"Diverged from 'origin/feature' by N commits\" when diverged\n\nExample output when tracking upstream/main with pushRemote set to origin:\n    On branch feature\n    Your branch is ahead of 'upstream/main' by 2 commits.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Ahead of 'origin/feature' by 5 commits.\n\nThe comparison is only shown when a push remote is configured and the\npush remote's tracking branch differs from the upstream tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    status: show comparison with push remote tracking branch\n    \n    cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\n    ychin.macvim@gmail.com cc: \"brian m. carlson\"\n    sandals@crustytoothpaste.net cc: Ben Knoble ben.knoble@gmail.com cc:\n    \"Kristoffer Haugsbakk\" kristofferhaugsbakk@fastmail.com\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v11\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v11\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v10:\n\n 1:  43a75944fb ! 1:  53bb1cb6bb status: show comparison with configured goal branch\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    status: show comparison with configured goal branch\n     +    status: show comparison with push remote tracking branch\n      \n          \"git status\" on a branch that follows a remote branch compares\n          commits on the current branch and the remote-tracking branch it\n          builds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n      \n          When working on a feature branch that tracks a remote feature branch,\n     -    but you also want to track progress relative to a goal branch (e.g.,\n     -    upstream/main), you can configure status.goalBranch to show an\n     -    additional comparison.\n     +    but you also want to track progress relative to the push remote's\n     +    tracking branch (which may differ from the upstream branch), git status\n     +    now shows an additional comparison.\n      \n     -    Add support for status.goalBranch configuration option that specifies\n     -    a remote/branch to compare against. When set, git status shows both\n     -    the comparison with the tracking branch (as before) and an additional\n     -    comparison with the configured goal branch, if it differs from the\n     -    tracking branch. The goal branch comparison appears on a separate\n     -    line after the tracking branch status, using the same format:\n     -    - \"Ahead of 'upstream/main' by N commits\" when purely ahead\n     -    - \"Behind 'upstream/main' by N commits\" when purely behind\n     -    - \"Diverged from 'upstream/main' by N commits\" when diverged\n     +    When a push remote is configured (via branch.<name>.pushRemote or\n     +    remote.pushDefault), git status shows both the comparison with the\n     +    upstream tracking branch (as before) and an additional comparison with\n     +    the push remote's tracking branch, if it differs from the upstream\n     +    tracking branch. The push branch comparison appears on a separate\n     +    line after the upstream branch status, using the same format:\n     +    - \"Ahead of 'origin/feature' by N commits\" when purely ahead\n     +    - \"Behind 'origin/feature' by N commits\" when purely behind\n     +    - \"Diverged from 'origin/feature' by N commits\" when diverged\n      \n     -    Example output when tracking a feature branch with status.goalBranch\n     -    set to upstream/main:\n     +    Example output when tracking upstream/main with pushRemote set to origin:\n              On branch feature\n     -        Your branch is ahead of 'origin/feature' by 2 commits.\n     -          (use \"git push\" to publish your local commits)\n     +        Your branch is ahead of 'upstream/main' by 2 commits.\n     +          (use \"git pull\" if you want to integrate the remote branch with yours)\n      \n     -        Ahead of 'upstream/main' by 5 commits.\n     +        Ahead of 'origin/feature' by 5 commits.\n      \n     -    The comparison is only shown when status.goalBranch is configured\n     -    and the goal branch differs from the tracking branch.\n     +    The comparison is only shown when a push remote is configured and the\n     +    push remote's tracking branch differs from the upstream tracking branch.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n       }\n       \n     -+static char *get_goal_branch_ref(char **full_ref_out)\n     ++static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n      +{\n     -+\tconst char *config_value;\n     ++\tconst char *push_remote;\n      +\tconst char *resolved;\n      +\tint flag;\n      +\tstruct strbuf ref_buf = STRBUF_INIT;\n     -+\tchar *slash_pos;\n      +\tchar *ret = NULL;\n      +\n     -+\tif (repo_config_get_value(the_repository, \"status.goalBranch\", &config_value))\n     ++\tif (!branch)\n      +\t\treturn NULL;\n      +\n     -+\tif (!config_value || !*config_value)\n     ++\tpush_remote = pushremote_for_branch(branch, NULL);\n     ++\tif (!push_remote)\n      +\t\treturn NULL;\n      +\n     -+\tslash_pos = strchr(config_value, '/');\n     -+\tif (!slash_pos || slash_pos == config_value || !slash_pos[1]) {\n     -+\t\twarning(_(\"invalid value for status.goalBranch: '%s' (expected format: remote/branch)\"),\n     -+\t\t\tconfig_value);\n     -+\t\treturn NULL;\n     -+\t}\n     -+\n     -+\tstrbuf_addf(&ref_buf, \"refs/remotes/%.*s/%s\",\n     -+\t\t    (int)(slash_pos - config_value), config_value,\n     -+\t\t    slash_pos + 1);\n     ++\tstrbuf_addf(&ref_buf, \"refs/remotes/%s/%s\", push_remote, branch->name);\n      +\n      +\tresolved = refs_resolve_ref_unsafe(\n      +\t\tget_main_ref_store(the_repository),\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\treturn ret;\n      +}\n      +\n     -+static void format_goal_branch_comparison(struct strbuf *sb,\n     ++static void format_push_branch_comparison(struct strbuf *sb,\n      +\t\t\t\t\t     const char *branch_refname,\n     -+\t\t\t\t\t     const char *goal_full,\n     -+\t\t\t\t\t     const char *goal_short,\n     ++\t\t\t\t\t     const char *push_full,\n     ++\t\t\t\t\t     const char *push_short,\n      +\t\t\t\t\t     enum ahead_behind_flags abf)\n      +{\n     -+\tint goal_ahead = 0, goal_behind = 0;\n     ++\tint push_ahead = 0, push_behind = 0;\n     ++\tint stat_result;\n      +\n     -+\tif (stat_branch_pair(branch_refname, goal_full,\n     -+\t\t\t     &goal_ahead, &goal_behind, abf) <= 0)\n     ++\tstat_result = stat_branch_pair(branch_refname, push_full,\n     ++\t\t\t\t       &push_ahead, &push_behind, abf);\n     ++\tif (stat_result < 0)\n      +\t\treturn;\n      +\n      +\tstrbuf_addstr(sb, \"\\n\");\n      +\n     -+\tif (goal_ahead > 0 && goal_behind == 0) {\n     ++\tif (stat_result == 0 || (push_ahead == 0 && push_behind == 0)) {\n     ++\t\tstrbuf_addf(sb,\n     ++\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n     ++\t\t\tpush_short);\n     ++\t} else if (push_ahead > 0 && push_behind == 0) {\n      +\t\tstrbuf_addf(sb,\n      +\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n      +\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n     -+\t\t\t   goal_ahead),\n     -+\t\t\tgoal_short, goal_ahead);\n     -+\t} else if (goal_behind > 0 && goal_ahead == 0) {\n     ++\t\t\t   push_ahead),\n     ++\t\t\tpush_short, push_ahead);\n     ++\t} else if (push_behind > 0 && push_ahead == 0) {\n      +\t\tstrbuf_addf(sb,\n      +\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n      +\t\t\t   \"Behind '%s' by %d commits.\\n\",\n     -+\t\t\t   goal_behind),\n     -+\t\t\tgoal_short, goal_behind);\n     -+\t} else if (goal_ahead > 0 && goal_behind > 0) {\n     ++\t\t\t   push_behind),\n     ++\t\t\tpush_short, push_behind);\n     ++\t} else if (push_ahead > 0 && push_behind > 0) {\n      +\t\tstrbuf_addf(sb,\n      +\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n      +\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n     -+\t\t\t   goal_ahead + goal_behind),\n     -+\t\t\tgoal_short, goal_ahead + goal_behind);\n     ++\t\t\t   push_ahead + push_behind),\n     ++\t\t\tpush_short, push_ahead + push_behind);\n      +\t}\n      +}\n      +\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t}\n      +\n      +\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n     -+\t\tchar *goal_full = NULL;\n     -+\t\tchar *goal_short = get_goal_branch_ref(&goal_full);\n     ++\t\tchar *push_full = NULL;\n     ++\t\tchar *push_short = get_remote_push_branch(branch, &push_full);\n      +\n     -+\t\tif (goal_short && strcmp(base, goal_short))\n     -+\t\t\tformat_goal_branch_comparison(sb, branch->refname, goal_full,\n     -+\t\t\t\t\t\t     goal_short, abf);\n     ++\t\tif (push_short && strcmp(base, push_short))\n     ++\t\t\tformat_push_branch_comparison(sb, branch->refname, push_full,\n     ++\t\t\t\t\t\t     push_short, abf);\n      +\n     -+\t\tfree(goal_short);\n     -+\t\tfree(goal_full);\n     ++\t\tfree(push_short);\n     ++\t\tfree(push_full);\n      +\t}\n      +\n       \tfree(base);\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       }\n      \n       ## t/t6040-tracking-info.sh ##\n     -@@ t/t6040-tracking-info.sh: test_expect_success setup '\n     - \tgit clone . test &&\n     - \t(\n     - \t\tcd test &&\n     -+\t\tgit symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&\n     - \t\tgit checkout -b b1 origin &&\n     - \t\tgit reset --hard HEAD^ &&\n     - \t\tadvance d &&\n      @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n       \ttest_cmp expect actual\n       '\n       \n     -+test_expect_success 'setup for ahead of non-main tracking branch' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout -b feature origin/main &&\n     -+\t\tadvance feature1 &&\n     -+\t\tgit push origin feature &&\n     -+\t\tgit checkout -b work --track origin/feature &&\n     -+\t\tadvance work1 &&\n     -+\t\tadvance work2\n     -+\t)\n     -+'\n     -+\n     -+test_expect_success 'status shows ahead of both tracked branch and origin/main' '\n     ++test_expect_success 'status tracking origin/main shows only main' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout work >/dev/null &&\n     -+\t\tgit config status.goalBranch origin/main &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch work\n     -+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n     -+  (use \"git push\" to publish your local commits)\n     -+\n     -+Ahead of '\\''origin/main'\\'' by 3 commits.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     ++\t\tgit checkout b4 &&\n     ++\t\tgit status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch b4\n     ++\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n     ++\t  (use \"git push\" to publish your local commits)\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '\n     ++test_expect_success 'status shows ahead of both origin/main and feature branch' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout main >/dev/null &&\n     -+\t\tgit config status.goalBranch origin/main &&\n     -+\t\tgit checkout work 2>&1\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+Switched to branch '\\''work'\\''\n     -+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n     -+  (use \"git push\" to publish your local commits)\n     -+\n     -+Ahead of '\\''origin/main'\\'' by 3 commits.\n     -+EOF\n     ++\t\tgit checkout -b feature2 origin/main &&\n     ++\t\tgit push origin HEAD &&\n     ++\t\tadvance work &&\n     ++\t\tgit status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature2\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     ++\tAhead of ${SQ}origin/feature2${SQ} by 1 commit.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status tracking origin/main shows only main' '\n     ++test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout b4 >/dev/null &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\ttest_grep \"ahead of .origin/main. by 2 commits\" actual &&\n     -+\ttest_grep ! \"Ahead of\" actual\n     ++\t\tgit checkout feature2 >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\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     ++\tAhead of ${SQ}origin/feature2${SQ} by 1 commit.\n     ++\tEOF\n     ++\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'setup for ahead of tracked but diverged from main' '\n      +\t(\n      +\t\tcd test &&\n     ++\t\tgit checkout -b feature4 origin/main &&\n     ++\t\tadvance work1 &&\n      +\t\tgit checkout origin/main &&\n     -+\t\tgit checkout -b oldfeature &&\n     -+\t\tadvance oldfeature1 &&\n     -+\t\tgit push origin oldfeature &&\n     -+\t\tgit checkout origin/main &&\n     -+\t\tadvance main_newer &&\n     ++\t\tadvance work2 &&\n      +\t\tgit push origin HEAD:main &&\n     -+\t\tgit checkout -b work2 --track origin/oldfeature &&\n     -+\t\tadvance work2_commit\n     -+\t)\n     -+'\n     -+\n     -+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout work2 >/dev/null &&\n     -+\t\tgit config status.goalBranch origin/main &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch work2\n     -+Your branch is ahead of '\\''origin/oldfeature'\\'' by 1 commit.\n     -+  (use \"git push\" to publish your local commits)\n     -+\n     -+Diverged from '\\''origin/main'\\'' by 3 commits.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'setup for diverged from tracked but behind main' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit fetch origin &&\n     -+\t\tgit checkout origin/main &&\n     -+\t\tgit checkout -b work2b &&\n     -+\t\tgit branch --set-upstream-to=origin/oldfeature &&\n     -+\t\tgit checkout origin/main &&\n     -+\t\tadvance main_extra &&\n     -+\t\tgit push origin HEAD:main\n     -+\t)\n     -+'\n     -+\n     -+test_expect_success 'status shows diverged from tracked and behind origin/main' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout work2b >/dev/null &&\n     -+\t\tgit config status.goalBranch origin/main &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch work2b\n     -+Your branch and '\\''origin/oldfeature'\\'' have diverged,\n     -+and have 1 and 1 different commits each, respectively.\n     -+  (use \"git pull\" if you want to integrate the remote branch with yours)\n     -+\n     -+Behind '\\''origin/main'\\'' by 1 commit.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'setup for behind tracked but ahead of main' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit fetch origin &&\n     -+\t\tgit checkout origin/main &&\n     -+\t\tgit checkout -b feature3 &&\n     -+\t\tadvance feature3_1 &&\n     -+\t\tadvance feature3_2 &&\n     -+\t\tadvance feature3_3 &&\n     -+\t\tgit push origin feature3 &&\n     -+\t\tgit checkout -b work3 --track origin/feature3 &&\n     -+\t\tgit reset --hard HEAD~2\n     ++\t\tgit checkout feature4 &&\n     ++\t\tadvance work3\n      +\t)\n      +'\n      +\n     -+test_expect_success 'status shows behind tracked and ahead of origin/main' '\n     ++test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout work3 >/dev/null &&\n     -+\t\tgit config status.goalBranch origin/main &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch work3\n     -+Your branch is behind '\\''origin/feature3'\\'' by 2 commits, and can be fast-forwarded.\n     -+  (use \"git pull\" to update your local branch)\n     -+\n     -+Ahead of '\\''origin/main'\\'' by 1 commit.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     ++\t\tgit checkout feature4 &&\n     ++\t\tgit branch --set-upstream-to origin/main &&\n     ++\t\tgit push origin HEAD &&\n     ++\t\tadvance work &&\n     ++\t\tgit status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature4\n     ++\tYour branch and ${SQ}origin/main${SQ} have diverged,\n     ++\tand have 3 and 1 different commits each, respectively.\n     ++\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n     ++\n     ++\tAhead of ${SQ}origin/feature4${SQ} by 1 commit.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'setup upstream remote preference' '\n     ++test_expect_success 'setup upstream remote' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit remote add upstream ../. &&\n      +\t\tgit fetch upstream &&\n     -+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main\n     ++\t\tgit config remote.pushDefault origin\n      +\t)\n      +'\n      +\n     -+test_expect_success 'status prefers upstream remote over origin for comparison' '\n     ++test_expect_success 'status with upstream remote and push.default set to origin' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout work >/dev/null &&\n     -+\t\tgit config status.goalBranch upstream/main &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch work\n     -+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n     -+  (use \"git push\" to publish your local commits)\n     -+\n     -+Diverged from '\\''upstream/main'\\'' by 5 commits.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     ++\t\tgit checkout -b feature5 upstream/main &&\n     ++\t\tgit push origin &&\n     ++\t\tadvance work &&\n     ++\t\tgit status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature5\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     ++\tAhead of ${SQ}origin/feature5${SQ} by 1 commit.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'setup for up to date with tracked but ahead of default' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout origin/feature &&\n     -+\t\tgit checkout -b synced_feature --track origin/feature &&\n     -+\t\tgit checkout origin/main &&\n     -+\t\tadvance main_ahead &&\n     -+\t\tgit push origin HEAD:main\n     -+\t)\n     -+'\n     -+\n     -+test_expect_success 'status shows up to date with tracked but diverged from default' '\n     ++test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout synced_feature >/dev/null &&\n     -+\t\tgit config status.goalBranch upstream/main &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch synced_feature\n     -+Your branch is up to date with '\\''origin/feature'\\''.\n     -+\n     -+Diverged from '\\''upstream/main'\\'' by 3 commits.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     ++\t\tgit checkout -b feature6 upstream/main &&\n     ++\t\tadvance work &&\n     ++\t\tgit push origin &&\n     ++\t\tgit reset --hard upstream/main &&\n     ++\t\tadvance work &&\n     ++\t\tgit status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature6\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     ++\tDiverged from ${SQ}origin/feature6${SQ} by 2 commits.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '\n     ++test_expect_success 'status with upstream remote and push branch up to date' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit remote remove upstream &&\n     -+\t\tgit checkout origin/feature &&\n     -+\t\tgit checkout -b synced_feature2 --track origin/feature &&\n     -+\t\tgit checkout origin/main &&\n     -+\t\tadvance main_ahead2 &&\n     -+\t\tgit push origin HEAD:main\n     -+\t)\n     -+'\n     -+\n     -+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout synced_feature2 >/dev/null &&\n     -+\t\tgit config status.goalBranch origin/main &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch synced_feature2\n     -+Your branch is up to date with '\\''origin/feature'\\''.\n     -+\n     -+Diverged from '\\''origin/main'\\'' by 5 commits.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     ++\t\tgit checkout -b feature7 upstream/main &&\n     ++\t\tgit push origin &&\n     ++\t\tgit status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature7\n     ++\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n     ++\n     ++\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '\n     ++test_expect_success 'checkout shows push branch up to date' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout origin/feature &&\n     -+\t\tgit checkout -b synced_feature3 --track origin/feature\n     -+\t)\n     -+'\n     ++\t\tgit checkout feature7 >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n      +\n     -+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout synced_feature3 >/dev/null &&\n     -+\t\tgit config status.goalBranch origin/main &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch synced_feature3\n     -+Your branch is up to date with '\\''origin/feature'\\''.\n     -+\n     -+Diverged from '\\''origin/main'\\'' by 5 commits.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'status with status.goalBranch unset shows no default comparison' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout synced_feature3 >/dev/null &&\n     -+\t\tgit config --unset status.goalBranch 2>/dev/null || true &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch synced_feature3\n     -+Your branch is up to date with '\\''origin/feature'\\''.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'status with status.goalBranch set uses configured branch' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout synced_feature3 >/dev/null &&\n     -+\t\tgit config status.goalBranch origin/main &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch synced_feature3\n     -+Your branch is up to date with '\\''origin/feature'\\''.\n     -+\n     -+Diverged from '\\''origin/main'\\'' by 5 commits.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'status with status.goalBranch set to different remote/branch' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout work >/dev/null &&\n     -+\t\tgit config status.goalBranch origin/feature &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch work\n     -+Your branch is ahead of '\\''origin/feature'\\'' by 2 commits.\n     -+  (use \"git push\" to publish your local commits)\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'status with status.goalBranch set to non-existent branch' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout synced_feature3 >/dev/null &&\n     -+\t\tgit config status.goalBranch origin/nonexistent &&\n     -+\t\tgit status --long -b\n     -+\t) >actual &&\n     -+\tcat >expect <<-\\EOF &&\n     -+On branch synced_feature3\n     -+Your branch is up to date with '\\''origin/feature'\\''.\n     -+\n     -+nothing to commit, working tree clean\n     -+EOF\n     ++\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n     ++\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n 2:  e6d24b8b6a < -:  ---------- improve tests\n 3:  13c2a03b0a < -:  ---------- use pushRemote and tracking branch\n\n\n remote.c                 |  89 +++++++++++++++++++++\n t/t6040-tracking-info.sh | 167 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 256 insertions(+)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..2317725f7d 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,81 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tconst char *push_remote;\n+\tconst char *resolved;\n+\tint flag;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *ret = NULL;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%s/%s\", push_remote, branch->name);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, &flag);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t}\n+\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n+}\n+\n+static void format_push_branch_comparison(struct strbuf *sb,\n+\t\t\t\t\t     const char *branch_refname,\n+\t\t\t\t\t     const char *push_full,\n+\t\t\t\t\t     const char *push_short,\n+\t\t\t\t\t     enum ahead_behind_flags abf)\n+{\n+\tint push_ahead = 0, push_behind = 0;\n+\tint stat_result;\n+\n+\tstat_result = stat_branch_pair(branch_refname, push_full,\n+\t\t\t\t       &push_ahead, &push_behind, abf);\n+\tif (stat_result < 0)\n+\t\treturn;\n+\n+\tstrbuf_addstr(sb, \"\\n\");\n+\n+\tif (stat_result == 0 || (push_ahead == 0 && push_behind == 0)) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n+\t\t\tpush_short);\n+\t} else if (push_ahead > 0 && push_behind == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n+\t\t\t   push_ahead),\n+\t\t\tpush_short, push_ahead);\n+\t} else if (push_behind > 0 && push_ahead == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n+\t\t\t   push_behind),\n+\t\t\tpush_short, push_behind);\n+\t} else if (push_ahead > 0 && push_behind > 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n+\t\t\t   push_ahead + push_behind),\n+\t\t\tpush_short, push_ahead + push_behind);\n+\t}\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2258,6 +2333,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2311,6 +2387,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+\n+\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tchar *push_full = NULL;\n+\t\tchar *push_short = get_remote_push_branch(branch, &push_full);\n+\n+\t\tif (push_short && strcmp(base, push_short))\n+\t\t\tformat_push_branch_comparison(sb, branch->refname, push_full,\n+\t\t\t\t\t\t     push_short, abf);\n+\n+\t\tfree(push_short);\n+\t\tfree(push_full);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..f27ae719ad 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,171 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\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+\tAhead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\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+\tAhead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tAhead of ${SQ}origin/feature4${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\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+\tAhead of ${SQ}origin/feature5${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\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+\tDiverged from ${SQ}origin/feature6${SQ} by 2 commits.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe\n-- \ngitgitgadget\n"},{"id":"532917","messageId":"20260102112037.35360-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"757d6df5-7834-4ff2-8302-8edd8e990970@app.fastmail.com","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-02T11:20:37Z","receivedAt":"2026-01-02T11:20:40Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Thanks for an interesting discussion, always nice to talk about Git\nstrategy!\n\nDid you have some time to also look at the code? Would be nice to move this\nalong if possible.\n\n\nHarald\n"},{"id":"532921","messageId":"6526c419-c60c-49d2-9c1a-52be7aca82f0@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v11.git.git.1767352663477.gitgitgadget@gmail.com","subject":"Re: [PATCH v11] status: show comparison with push remote tracking branch","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-01-02T15:18:43Z","receivedAt":"2026-01-02T15:18:47Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Harald\n\nOn 02/01/2026 11:17, Harald Nordgren via GitGitGadget wrote:\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n> \n> \"git status\" on a branch that follows a remote branch compares\n> commits on the current branch and the remote-tracking branch it\n> builds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n> \n> When working on a feature branch that tracks a remote feature branch,\n> but you also want to track progress relative to the push remote's\n> tracking branch (which may differ from the upstream branch), git status\n> now shows an additional comparison.\n\nThis is great and it is really good that it is now using the default \npush destination rather than a custom config key.\n\n> When a push remote is configured (via branch.<name>.pushRemote or\n> remote.pushDefault) git status shows both the comparison with the\n> upstream tracking branch (as before) and an additional comparison with\n> the push remote's tracking branch, if it differs from the upstream\n> tracking branch.\n\nLooking at the tests, it seems that the extra information is shown \nwhenever the upstream branch differs from the default push destination \neven if they are on the same remote. I think that is sensible but this \nparagraph should be updated to reflect the fact that one does not need \nto set either of the \"pushDefault\" config settings to benefit from this.\n\n> The push branch comparison appears on a separate\n> line after the upstream branch status, using the same format:\n> - \"Ahead of 'origin/feature' by N commits\" when purely ahead\n> - \"Behind 'origin/feature' by N commits\" when purely behind\n> - \"Diverged from 'origin/feature' by N commits\" when diverged\n\nI'm wondering why we don't reuse the existing messages - I don't see how \nusing a different wording for the push destination compared to the \nupstream branch benefits the user. We should adjust the hints that are \nshown so that we only recommend pulling from the upstream branch and \nonly recommend pushing to the default push destination but the \ncomparisons should be the same. Also I don't find the message \"Diverged \nfrom 'origin/feature' by N commits' very helpful, I'd find it more \nuseful if it gave the ahead/behind count like we do for the upstream branch.\n\n> Example output when tracking upstream/main with pushRemote set to origin:\n>      On branch feature\n>      Your branch is ahead of 'upstream/main' by 2 commits.\n>        (use \"git pull\" if you want to integrate the remote branch with yours)\n\nIs this a typo? - why are we recommending \"git pull\" when our branch is \nahead of the upstream branch?\n\n>      Ahead of 'origin/feature' by 5 commits.\n\nIt would be nice to have a hint suggesting the user runs \"git push\" here.\n\n> The comparison is only shown when a push remote is configured and the\n> push remote's tracking branch differs from the upstream tracking branch.\n\nLooking at the tests, only the second half of that sentence appears to \nbe true.\n\n> diff --git a/remote.c b/remote.c\n> index 59b3715120..2317725f7d 100644\n> --- a/remote.c\n> +++ b/remote.c\n> @@ -2237,6 +2237,81 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n>   \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n>   }\n>   \n> +static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n> +{\n> +\tconst char *push_remote;\n> +\tconst char *resolved;\n> +\tint flag;\n> +\tstruct strbuf ref_buf = STRBUF_INIT;\n> +\tchar *ret = NULL;\n> +\n> +\tif (!branch)\n> +\t\treturn NULL;\n> +\n> +\tpush_remote = pushremote_for_branch(branch, NULL);\n> +\tif (!push_remote)\n> +\t\treturn NULL;\n> +\n> +\tstrbuf_addf(&ref_buf, \"refs/remotes/%s/%s\", push_remote, branch->name);\n\nShouldn't we be taking account of the push and fetch refspecs here? \nThere is no guarantee that $branch maps to refs/remotes/$remote/$branch. \nTo take a silly example, if we have\n\n     remote.$remote.fetch = \nrefs/heads/$branch:refs/remotes/$remote/abc-$branch\n     remote.$remote.pull = refs/heads/$branch:refs/heads/xyz-$branch\n\nThen we should be using \"refs/remotes/$remote/abc-xyz-$branch\" in the \nmessage above as \"$branch\" will be pushed to \"xyz-$branch\" on the remote \nwhich is fetched to \"$remote/abc-xyz-$branch\"\n\n> +\n> +\tresolved = refs_resolve_ref_unsafe(\n> +\t\tget_main_ref_store(the_repository),\n> +\t\tref_buf.buf,\n> +\t\tRESOLVE_REF_READING,\n> +\t\tNULL, &flag);\n\nAs we don't use flag we can pass NULL - see the documentation for this \nfunction in refs.h\n\n> +static void format_push_branch_comparison(struct strbuf *sb,\n> +\t\t\t\t\t     const char *branch_refname,\n> +\t\t\t\t\t     const char *push_full,\n> +\t\t\t\t\t     const char *push_short,\n> +\t\t\t\t\t     enum ahead_behind_flags abf)\n> +{\n> +\tint push_ahead = 0, push_behind = 0;\n> +\tint stat_result;\n> +\n> +\tstat_result = stat_branch_pair(branch_refname, push_full,\n> +\t\t\t\t       &push_ahead, &push_behind, abf);\n> +\tif (stat_result < 0)\n> +\t\treturn;\n> +\n> +\tstrbuf_addstr(sb, \"\\n\");\n\nAs I said above it would be nice if we could use the existing messages \nhere. Can we have a separate preparatory patch that refactors \nfromat_tracking_info() so that we can use the same messages for the \nupstream branch and the default push destination?\n\n> @@ -2311,6 +2387,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>   \t\t\tstrbuf_addstr(sb,\n>   \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n>   \t}\n> +\n> +\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n\nWhy do we handle AHEAD_BEHIND_QUICK differently to the upstream branch? \nSurely it would be useful to tell the user whether the branch is up to \ndate with the default push destination or not?\n\n> diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\n> index 0b719bbae6..f27ae719ad 100755\n> --- a/t/t6040-tracking-info.sh\n> +++ b/t/t6040-tracking-info.sh\n> [...]\n> +test_expect_success 'status shows ahead of both origin/main and feature branch' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature2 origin/main &&\n> +\t\tgit push origin HEAD &&\n> +\t\tadvance work &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature2\n> +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n> +\t  (use \"git push\" to publish your local commits)\n\nWhy are we still suggesting pushing to the upstream branch when we know \nthe user is pushing to a different remote branch?\n\n> +\tAhead of ${SQ}origin/feature2${SQ} by 1 commit.\n\nWhy aren't we suggesting to use \"git push\" here?\n\nThanks for working on this. With a few tweaks it will be a really useful \nimprovement to \"git status\"\n\nPhillip\n\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout feature2 >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\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> +\tAhead of ${SQ}origin/feature2${SQ} by 1 commit.\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'setup for ahead of tracked but diverged from main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature4 origin/main &&\n> +\t\tadvance work1 &&\n> +\t\tgit checkout origin/main &&\n> +\t\tadvance work2 &&\n> +\t\tgit push origin HEAD:main &&\n> +\t\tgit checkout feature4 &&\n> +\t\tadvance work3\n> +\t)\n> +'\n> +\n> +test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout feature4 &&\n> +\t\tgit branch --set-upstream-to origin/main &&\n> +\t\tgit push origin HEAD &&\n> +\t\tadvance work &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature4\n> +\tYour branch and ${SQ}origin/main${SQ} have diverged,\n> +\tand have 3 and 1 different commits each, respectively.\n> +\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n> +\n> +\tAhead of ${SQ}origin/feature4${SQ} by 1 commit.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'setup upstream remote' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit remote add upstream ../. &&\n> +\t\tgit fetch upstream &&\n> +\t\tgit config remote.pushDefault origin\n> +\t)\n> +'\n> +\n> +test_expect_success 'status with upstream remote and push.default set to origin' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature5 upstream/main &&\n> +\t\tgit push origin &&\n> +\t\tadvance work &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature5\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> +\tAhead of ${SQ}origin/feature5${SQ} by 1 commit.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature6 upstream/main &&\n> +\t\tadvance work &&\n> +\t\tgit push origin &&\n> +\t\tgit reset --hard upstream/main &&\n> +\t\tadvance work &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature6\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> +\tDiverged from ${SQ}origin/feature6${SQ} by 2 commits.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status with upstream remote and push branch up to date' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature7 upstream/main &&\n> +\t\tgit push origin &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature7\n> +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n> +\n> +\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'checkout shows push branch up to date' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout feature7 >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n> +\n> +\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n>   test_done\n> \n> base-commit: 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe\n\n"},{"id":"532937","messageId":"20260102202738.82432-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"6526c419-c60c-49d2-9c1a-52be7aca82f0@gmail.com","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-02T20:27:38Z","receivedAt":"2026-01-02T20:27:41Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Looking at the tests, it seems that the extra information is shown\n> whenever the upstream branch differs from the default push destination\n> even if they are on the same remote. I think that is sensible but this\n> paragraph should be updated to reflect the fact that one does not need to\n> set either of the \"pushDefault\" config settings to benefit from this.\n\nGood catch, will be included in the next diff!\n\n> I'm wondering why we don't reuse the existing messages - I don't see\n> how using a different wording for the push destination compared to the\n> upstream branch benefits the user. We should adjust the hints that\n> are shown so that we only recommend pulling from the upstream branch\n> and only recommend pushing to the default push destination but the\n> comparisons should be the same. Also I don't find the message \"Diverged\n> from 'origin/feature' by N commits' very helpful, I'd find it more useful\n> if it gave the ahead/behind count like we do for the upstream branch.\n\nThanks for saying this, I actually did the majority of this work already\nbut was hesitant to include it in the diff because it increases the surface\narea. But I will include it now!\n\n> Shouldn't we be taking account of the push and fetch refspecs here? There\n> is no guarantee that $branch maps to refs/remotes/$remote/$branch. To take\n> a silly example, if we have\n>\n>      remote.$remote.fetch =\n> refs/heads/$branch:refs/remotes/$remote/abc-$branch remote.$remote.pull =\n> refs/heads/$branch:refs/heads/xyz-$branch\n>\n> Then we should be using \"refs/remotes/$remote/abc-xyz-$branch\" in the\n> message above as \"$branch\" will be pushed to \"xyz-$branch\" on the remote\n> which is fetched to \"$remote/abc-xyz-$branch\"\n\nI don't understand this one, can you maybe explain how to code will need to\nchange?\n\n> As we don't use flag we can pass NULL - see the documentation for this\n> function in refs.h\n\nThanks, will be included in the next diff!\n\n> Why are we still suggesting pushing to the upstream branch when we know\nthe user is pushing to a different remote branch?\n\nThanks, will be included in the next diff!\n\n\nHarald\n"},{"id":"532939","messageId":"pull.2138.v12.git.git.1767389649.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v11.git.git.1767352663477.gitgitgadget@gmail.com","subject":"[PATCH v12 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-02T21:34:07Z","receivedAt":"2026-01-02T21:34:11Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\n\nHarald Nordgren (2):\n  refactor: format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 164 +++++++++++++++++++++++++++++---------\n t/t6040-tracking-info.sh | 168 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 295 insertions(+), 37 deletions(-)\n\n\nbase-commit: 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v12\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v12\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v11:\n\n -:  ---------- > 1:  a2c160c53e refactor: format_branch_comparison in preparation\n 1:  53bb1cb6bb ! 2:  a586038d1f status: show comparison with push remote tracking branch\n     @@ Commit message\n          builds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n      \n          When working on a feature branch that tracks a remote feature branch,\n     -    but you also want to track progress relative to the push remote's\n     +    but you also want to track progress relative to the push destination\n          tracking branch (which may differ from the upstream branch), git status\n          now shows an additional comparison.\n      \n     -    When a push remote is configured (via branch.<name>.pushRemote or\n     -    remote.pushDefault), git status shows both the comparison with the\n     -    upstream tracking branch (as before) and an additional comparison with\n     -    the push remote's tracking branch, if it differs from the upstream\n     -    tracking branch. The push branch comparison appears on a separate\n     -    line after the upstream branch status, using the same format:\n     -    - \"Ahead of 'origin/feature' by N commits\" when purely ahead\n     -    - \"Behind 'origin/feature' by N commits\" when purely behind\n     -    - \"Diverged from 'origin/feature' by N commits\" when diverged\n     +    When the upstream tracking branch differs from the push destination\n     +    tracking branch, git status shows both the comparison with the upstream\n     +    tracking branch (as before) and an additional comparison with the push\n     +    destination tracking branch. The push branch comparison appears on a\n     +    separate line after the upstream branch status, using the same format.\n      \n     -    Example output when tracking upstream/main with pushRemote set to origin:\n     +    Example output when tracking origin/main but push destination is\n     +    origin/feature:\n              On branch feature\n     -        Your branch is ahead of 'upstream/main' by 2 commits.\n     +        Your branch and 'origin/main' have diverged,\n     +        and have 3 and 1 different commits each, respectively.\n                (use \"git pull\" if you want to integrate the remote branch with yours)\n      \n     -        Ahead of 'origin/feature' by 5 commits.\n     +        Your branch is ahead of 'origin/feature' by 1 commit.\n     +          (use \"git push\" to publish your local commits)\n      \n     -    The comparison is only shown when a push remote is configured and the\n     -    push remote's tracking branch differs from the upstream tracking branch.\n     +    The comparison is only shown when the push destination tracking branch\n     +    differs from the upstream tracking branch, even if they are on the same\n     +    remote.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +{\n      +\tconst char *push_remote;\n      +\tconst char *resolved;\n     -+\tint flag;\n      +\tstruct strbuf ref_buf = STRBUF_INIT;\n      +\tchar *ret = NULL;\n      +\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\t\tget_main_ref_store(the_repository),\n      +\t\tref_buf.buf,\n      +\t\tRESOLVE_REF_READING,\n     -+\t\tNULL, &flag);\n     ++\t\tNULL, NULL);\n      +\n      +\tif (resolved) {\n      +\t\tif (full_ref_out)\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\treturn ret;\n      +}\n      +\n     -+static void format_push_branch_comparison(struct strbuf *sb,\n     -+\t\t\t\t\t     const char *branch_refname,\n     -+\t\t\t\t\t     const char *push_full,\n     -+\t\t\t\t\t     const char *push_short,\n     -+\t\t\t\t\t     enum ahead_behind_flags abf)\n     -+{\n     -+\tint push_ahead = 0, push_behind = 0;\n     -+\tint stat_result;\n     -+\n     -+\tstat_result = stat_branch_pair(branch_refname, push_full,\n     -+\t\t\t\t       &push_ahead, &push_behind, abf);\n     -+\tif (stat_result < 0)\n     -+\t\treturn;\n     -+\n     -+\tstrbuf_addstr(sb, \"\\n\");\n     -+\n     -+\tif (stat_result == 0 || (push_ahead == 0 && push_behind == 0)) {\n     -+\t\tstrbuf_addf(sb,\n     -+\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n     -+\t\t\tpush_short);\n     -+\t} else if (push_ahead > 0 && push_behind == 0) {\n     -+\t\tstrbuf_addf(sb,\n     -+\t\t\tQ_(\"Ahead of '%s' by %d commit.\\n\",\n     -+\t\t\t   \"Ahead of '%s' by %d commits.\\n\",\n     -+\t\t\t   push_ahead),\n     -+\t\t\tpush_short, push_ahead);\n     -+\t} else if (push_behind > 0 && push_ahead == 0) {\n     -+\t\tstrbuf_addf(sb,\n     -+\t\t\tQ_(\"Behind '%s' by %d commit.\\n\",\n     -+\t\t\t   \"Behind '%s' by %d commits.\\n\",\n     -+\t\t\t   push_behind),\n     -+\t\t\tpush_short, push_behind);\n     -+\t} else if (push_ahead > 0 && push_behind > 0) {\n     -+\t\tstrbuf_addf(sb,\n     -+\t\t\tQ_(\"Diverged from '%s' by %d commit.\\n\",\n     -+\t\t\t   \"Diverged from '%s' by %d commits.\\n\",\n     -+\t\t\t   push_ahead + push_behind),\n     -+\t\t\tpush_short, push_ahead + push_behind);\n     -+\t}\n     -+}\n     -+\n     - /*\n     -  * Return true when there is anything to report, otherwise false.\n     -  */\n     + static void format_branch_comparison(struct strbuf *sb,\n     + \t\t\t\t     int ahead, int behind,\n     + \t\t\t\t     const char *branch_name,\n      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     + \tconst char *full_base;\n     + \tchar *base;\n     + \tint upstream_is_gone = 0;\n     ++\tint push_ours = 0, push_theirs = 0;\n     ++\tint push_stat_result = -1;\n     ++\tint will_show_push_comparison = 0;\n       \n     + \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n     + \tif (sti < 0) {\n     +@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n       \t\t\t\t\t    full_base, 0);\n     + \n     ++\tif (!upstream_is_gone && abf != AHEAD_BEHIND_QUICK) {\n     ++\t\tchar *push_full = NULL;\n     ++\t\tchar *push_short = get_remote_push_branch(branch, &push_full);\n      +\n     - \tif (upstream_is_gone) {\n     - \t\tstrbuf_addf(sb,\n     - \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     ++\t\tif (push_short && strcmp(base, push_short)) {\n     ++\t\t\tpush_stat_result = stat_branch_pair(branch->refname, push_full,\n     ++\t\t\t\t\t\t\t    &push_ours, &push_theirs, abf);\n     ++\t\t\tif (push_stat_result >= 0)\n     ++\t\t\t\twill_show_push_comparison = 1;\n     ++\t\t}\n     ++\n     ++\t\tfree(push_short);\n     ++\t\tfree(push_full);\n     ++\t}\n     ++\n     + \tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n     + \tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n     +-\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n     ++\t\tif (!theirs && !will_show_push_comparison &&\n     ++\t\t    advice_enabled(ADVICE_STATUS_HINTS)) {\n       \t\t\tstrbuf_addstr(sb,\n     - \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n     + \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n     + \t\t} else if (!ours && advice_enabled(ADVICE_STATUS_HINTS)) {\n     +@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     + \t\t}\n       \t}\n     -+\n     -+\tif (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {\n     + \n     ++\tif (will_show_push_comparison) {\n      +\t\tchar *push_full = NULL;\n      +\t\tchar *push_short = get_remote_push_branch(branch, &push_full);\n      +\n     -+\t\tif (push_short && strcmp(base, push_short))\n     -+\t\t\tformat_push_branch_comparison(sb, branch->refname, push_full,\n     -+\t\t\t\t\t\t     push_short, abf);\n     ++\t\tif (push_short && strcmp(base, push_short)) {\n     ++\t\t\tstrbuf_addstr(sb, \"\\n\");\n     ++\t\t\tformat_branch_comparison(sb, push_ours, push_theirs, push_short, 0, abf,\n     ++\t\t\t\t\t\tpush_ours || push_theirs);\n     ++\t\t\tif (push_ours > 0 && push_theirs == 0 &&\n     ++\t\t\t    advice_enabled(ADVICE_STATUS_HINTS)) {\n     ++\t\t\t\tstrbuf_addstr(sb,\n     ++\t\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n     ++\t\t\t}\n     ++\t\t}\n      +\n      +\t\tfree(push_short);\n      +\t\tfree(push_full);\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature2\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     -+\tAhead of ${SQ}origin/feature2${SQ} by 1 commit.\n     ++\tYour branch is ahead of ${SQ}origin/feature2${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     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t) &&\n      +\tcat >expect <<-EOF &&\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     -+\tAhead of ${SQ}origin/feature2${SQ} by 1 commit.\n     ++\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n     ++\t  (use \"git push\" to publish your local commits)\n      +\tEOF\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\tand have 3 and 1 different commits each, respectively.\n      +\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n      +\n     -+\tAhead of ${SQ}origin/feature4${SQ} by 1 commit.\n     ++\tYour branch is ahead of ${SQ}origin/feature4${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     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature5\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     -+\tAhead of ${SQ}origin/feature5${SQ} by 1 commit.\n     ++\tYour branch is ahead of ${SQ}origin/feature5${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     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature6\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     -+\tDiverged from ${SQ}origin/feature6${SQ} by 2 commits.\n     ++\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n     ++\tand have 1 and 1 different commits each, respectively.\n      +\n      +\tnothing to commit, working tree clean\n      +\tEOF\n\n-- \ngitgitgadget\n"},{"id":"532938","messageId":"a2c160c53ee0159a88234c64409f2a216d584cc4.1767389649.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v12.git.git.1767389649.gitgitgadget@gmail.com","subject":"[PATCH v12 1/2] refactor: format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-02T21:34:08Z","receivedAt":"2026-01-02T21:34:12Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 99 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 59 insertions(+), 40 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..58093f64b0 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,66 +2237,50 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     int ahead, int behind,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     int upstream_is_gone,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     int sti)\n {\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else if (!sti) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n-\t} else if (!theirs) {\n+\t} else if (ahead == 0 && behind == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n+\t\t\tbranch_name);\n+\t} else if (ahead > 0 && behind == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n-\t\t\t   ours),\n-\t\t\tbase, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n-\t} else if (!ours) {\n+\t\t\t   ahead),\n+\t\t\tbranch_name, ahead);\n+\t} else if (behind > 0 && ahead == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n-\t\t\t   theirs),\n-\t\t\tbase, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n-\t} else {\n+\t\t\t   behind),\n+\t\t\tbranch_name, behind);\n+\t} else if (ahead > 0 && behind > 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n \t\t\t       \"and have %d and %d different commit each, \"\n@@ -2304,13 +2288,48 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch and '%s' have diverged,\\n\"\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n-\t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\t   ahead + behind),\n+\t\t\tbranch_name, ahead, behind);\n+\t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, sti;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (sti < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n+\tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\t} else if (!ours && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n+\t\t} else if (ours && theirs && show_divergence_advice &&\n+\t\t\t   advice_enabled(ADVICE_STATUS_HINTS)) {\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n+\t\t}\n \t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"532940","messageId":"a586038d1f9daafb0c6428ace7d396f79b036ed4.1767389649.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v12.git.git.1767389649.gitgitgadget@gmail.com","subject":"[PATCH v12 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-02T21:34:09Z","receivedAt":"2026-01-02T21:34:13Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 |  73 ++++++++++++++++-\n t/t6040-tracking-info.sh | 168 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 240 insertions(+), 1 deletion(-)\n\ndiff --git a/remote.c b/remote.c\nindex 58093f64b0..1663bd9236 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,39 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tconst char *push_remote;\n+\tconst char *resolved;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *ret = NULL;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%s/%s\", push_remote, branch->name);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t}\n+\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     int ahead, int behind,\n \t\t\t\t     const char *branch_name,\n@@ -2304,6 +2337,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tint push_ours = 0, push_theirs = 0;\n+\tint push_stat_result = -1;\n+\tint will_show_push_comparison = 0;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2315,9 +2351,25 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tif (!upstream_is_gone && abf != AHEAD_BEHIND_QUICK) {\n+\t\tchar *push_full = NULL;\n+\t\tchar *push_short = get_remote_push_branch(branch, &push_full);\n+\n+\t\tif (push_short && strcmp(base, push_short)) {\n+\t\t\tpush_stat_result = stat_branch_pair(branch->refname, push_full,\n+\t\t\t\t\t\t\t    &push_ours, &push_theirs, abf);\n+\t\t\tif (push_stat_result >= 0)\n+\t\t\t\twill_show_push_comparison = 1;\n+\t\t}\n+\n+\t\tfree(push_short);\n+\t\tfree(push_full);\n+\t}\n+\n \tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n \tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n-\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\tif (!theirs && !will_show_push_comparison &&\n+\t\t    advice_enabled(ADVICE_STATUS_HINTS)) {\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t\t} else if (!ours && advice_enabled(ADVICE_STATUS_HINTS)) {\n@@ -2330,6 +2382,25 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t}\n \t}\n \n+\tif (will_show_push_comparison) {\n+\t\tchar *push_full = NULL;\n+\t\tchar *push_short = get_remote_push_branch(branch, &push_full);\n+\n+\t\tif (push_short && strcmp(base, push_short)) {\n+\t\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\t\tformat_branch_comparison(sb, push_ours, push_theirs, push_short, 0, abf,\n+\t\t\t\t\t\tpush_ours || push_theirs);\n+\t\t\tif (push_ours > 0 && push_theirs == 0 &&\n+\t\t\t    advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\t\t}\n+\t\t}\n+\n+\t\tfree(push_short);\n+\t\tfree(push_full);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..8eb1f3e1f1 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,172 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"532941","messageId":"pull.2138.v13.git.git.1767409701.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v12.git.git.1767389649.gitgitgadget@gmail.com","subject":"[PATCH v13 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-03T03:08:19Z","receivedAt":"2026-01-03T03:08:25Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\n\nHarald Nordgren (2):\n  refactor: format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 152 ++++++++++++++++++++++++++---------\n t/t6040-tracking-info.sh | 168 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 283 insertions(+), 37 deletions(-)\n\n\nbase-commit: 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v13\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v13\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v12:\n\n 1:  a2c160c53e = 1:  a2c160c53e refactor: format_branch_comparison in preparation\n 2:  a586038d1f ! 2:  891239211e status: show comparison with push remote tracking branch\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tconst char *full_base;\n       \tchar *base;\n       \tint upstream_is_gone = 0;\n     -+\tint push_ours = 0, push_theirs = 0;\n     -+\tint push_stat_result = -1;\n     -+\tint will_show_push_comparison = 0;\n     ++\tint push_ours, push_theirs, push_sti;\n     ++\tchar *full_push = NULL;\n     ++\tchar *push = NULL;\n     ++\tint show_push_comparison = 0;\n       \n       \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n       \tif (sti < 0) {\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n       \t\t\t\t\t    full_base, 0);\n       \n     -+\tif (!upstream_is_gone && abf != AHEAD_BEHIND_QUICK) {\n     -+\t\tchar *push_full = NULL;\n     -+\t\tchar *push_short = get_remote_push_branch(branch, &push_full);\n     -+\n     -+\t\tif (push_short && strcmp(base, push_short)) {\n     -+\t\t\tpush_stat_result = stat_branch_pair(branch->refname, push_full,\n     -+\t\t\t\t\t\t\t    &push_ours, &push_theirs, abf);\n     -+\t\t\tif (push_stat_result >= 0)\n     -+\t\t\t\twill_show_push_comparison = 1;\n     -+\t\t}\n     -+\n     -+\t\tfree(push_short);\n     -+\t\tfree(push_full);\n     ++\tpush = get_remote_push_branch(branch, &full_push);\n     ++\tif (push && strcmp(base, push)) {\n     ++\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n     ++\t\t\t\t\t   &push_ours, &push_theirs, abf);\n     ++\t\tif (push_sti >= 0)\n     ++\t\t\tshow_push_comparison = 1;\n      +\t}\n      +\n       \tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n       \tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n      -\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n     -+\t\tif (!theirs && !will_show_push_comparison &&\n     ++\t\tif (!theirs && !show_push_comparison &&\n      +\t\t    advice_enabled(ADVICE_STATUS_HINTS)) {\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t\t}\n       \t}\n       \n     -+\tif (will_show_push_comparison) {\n     -+\t\tchar *push_full = NULL;\n     -+\t\tchar *push_short = get_remote_push_branch(branch, &push_full);\n     -+\n     -+\t\tif (push_short && strcmp(base, push_short)) {\n     -+\t\t\tstrbuf_addstr(sb, \"\\n\");\n     -+\t\t\tformat_branch_comparison(sb, push_ours, push_theirs, push_short, 0, abf,\n     -+\t\t\t\t\t\tpush_ours || push_theirs);\n     -+\t\t\tif (push_ours > 0 && push_theirs == 0 &&\n     -+\t\t\t    advice_enabled(ADVICE_STATUS_HINTS)) {\n     ++\tif (show_push_comparison) {\n     ++\t\tstrbuf_addstr(sb, \"\\n\");\n     ++\t\tformat_branch_comparison(sb, push_ours, push_theirs, push, 0, abf, push_sti);\n     ++\t\tif (push_sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n     ++\t\t\tif (!push_theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n      +\t\t\t\tstrbuf_addstr(sb,\n      +\t\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n      +\t\t\t}\n      +\t\t}\n     -+\n     -+\t\tfree(push_short);\n     -+\t\tfree(push_full);\n      +\t}\n      +\n       \tfree(base);\n     ++\tfree(full_push);\n     ++\tfree(push);\n       \treturn 1;\n       }\n     + \n      \n       ## t/t6040-tracking-info.sh ##\n      @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n\n-- \ngitgitgadget\n"},{"id":"532942","messageId":"a2c160c53ee0159a88234c64409f2a216d584cc4.1767409701.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v13.git.git.1767409701.gitgitgadget@gmail.com","subject":"[PATCH v13 1/2] refactor: format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-03T03:08:20Z","receivedAt":"2026-01-03T03:08:27Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 99 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 59 insertions(+), 40 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..58093f64b0 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,66 +2237,50 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     int ahead, int behind,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     int upstream_is_gone,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     int sti)\n {\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else if (!sti) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n-\t} else if (!theirs) {\n+\t} else if (ahead == 0 && behind == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n+\t\t\tbranch_name);\n+\t} else if (ahead > 0 && behind == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n-\t\t\t   ours),\n-\t\t\tbase, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n-\t} else if (!ours) {\n+\t\t\t   ahead),\n+\t\t\tbranch_name, ahead);\n+\t} else if (behind > 0 && ahead == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n-\t\t\t   theirs),\n-\t\t\tbase, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n-\t} else {\n+\t\t\t   behind),\n+\t\t\tbranch_name, behind);\n+\t} else if (ahead > 0 && behind > 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n \t\t\t       \"and have %d and %d different commit each, \"\n@@ -2304,13 +2288,48 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch and '%s' have diverged,\\n\"\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n-\t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\t   ahead + behind),\n+\t\t\tbranch_name, ahead, behind);\n+\t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, sti;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (sti < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n+\tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\t} else if (!ours && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n+\t\t} else if (ours && theirs && show_divergence_advice &&\n+\t\t\t   advice_enabled(ADVICE_STATUS_HINTS)) {\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n+\t\t}\n \t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"532943","messageId":"891239211e8bd59c46d5ee4b0d024467f02597d4.1767409701.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v13.git.git.1767409701.gitgitgadget@gmail.com","subject":"[PATCH v13 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-03T03:08:21Z","receivedAt":"2026-01-03T03:08:30Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 |  61 +++++++++++++-\n t/t6040-tracking-info.sh | 168 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 228 insertions(+), 1 deletion(-)\n\ndiff --git a/remote.c b/remote.c\nindex 58093f64b0..c90441fe42 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,39 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tconst char *push_remote;\n+\tconst char *resolved;\n+\tstruct strbuf ref_buf = STRBUF_INIT;\n+\tchar *ret = NULL;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tstrbuf_addf(&ref_buf, \"refs/remotes/%s/%s\", push_remote, branch->name);\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tref_buf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (resolved) {\n+\t\tif (full_ref_out)\n+\t\t\t*full_ref_out = xstrdup(resolved);\n+\t\tret = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n+\t}\n+\n+\tstrbuf_release(&ref_buf);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     int ahead, int behind,\n \t\t\t\t     const char *branch_name,\n@@ -2304,6 +2337,10 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tint push_ours, push_theirs, push_sti;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tint show_push_comparison = 0;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2315,9 +2352,18 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_sti >= 0)\n+\t\t\tshow_push_comparison = 1;\n+\t}\n+\n \tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n \tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n-\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\tif (!theirs && !show_push_comparison &&\n+\t\t    advice_enabled(ADVICE_STATUS_HINTS)) {\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t\t} else if (!ours && advice_enabled(ADVICE_STATUS_HINTS)) {\n@@ -2330,7 +2376,20 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t}\n \t}\n \n+\tif (show_push_comparison) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, push_ours, push_theirs, push, 0, abf, push_sti);\n+\t\tif (push_sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\t\tif (!push_theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\t\t}\n+\t\t}\n+\t}\n+\n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..8eb1f3e1f1 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,172 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"532946","messageId":"9d3407a6-7d88-4f30-9961-5b21b059e1b0@gmail.com","threadId":"64668","inReplyTo":"20260102202738.82432-1-haraldnordgren@gmail.com","subject":"Re: Another look?","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-01-03T10:04:01Z","receivedAt":"2026-01-03T10:04:04Z","isPatch":false,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 02/01/2026 20:27, Harald Nordgren wrote:\n> \n>> Shouldn't we be taking account of the push and fetch refspecs here? There\n>> is no guarantee that $branch maps to refs/remotes/$remote/$branch. To take\n>> a silly example, if we have\n>>\n>>       remote.$remote.fetch =\n>> refs/heads/$branch:refs/remotes/$remote/abc-$branch remote.$remote.pull =\n>> refs/heads/$branch:refs/heads/xyz-$branch\n>>\n>> Then we should be using \"refs/remotes/$remote/abc-xyz-$branch\" in the\n>> message above as \"$branch\" will be pushed to \"xyz-$branch\" on the remote\n>> which is fetched to \"$remote/abc-xyz-$branch\"\n> \n> I don't understand this one, can you maybe explain how to code will need to\n> change?\n\nYou want to display the remote tracking ref that tracks ref on the \nremote that we push to. When git pushes $branch to $remote it checks if \nany of the push refspecs remote.$remote.push match $branch. If there is \na match then the branch name is mapped according to the refspec and that \nit the ref that is updated in the remote repository. It then takes that \nref and checks the fetch refspecs remote.$remote.fetch and if it finds a \nmatch it maps the ref we've pushed to a remote tracking ref in the local \nrepository.\n\nIt looks like you can find where we would push a branch to with \nremote_ref_for_branch() and you can map that to a remote tracking ref \nwith tracking_for_push_dest(). We could use branch_get_push() but does \nmore than just map the refs as it checks push.default to see whether \n\"git push\" would actually push the branch.\n\nThanks\n\nPhillip\n\n"},{"id":"532952","messageId":"20260103130005.26640-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"9d3407a6-7d88-4f30-9961-5b21b059e1b0@gmail.com","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-03T13:00:05Z","receivedAt":"2026-01-03T13:00:09Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> You want to display the remote tracking ref that tracks ref on the remote\n> that we push to. When git pushes $branch to $remote it checks if any\n> of the push refspecs remote.$remote.push match $branch. If there is a\n> match then the branch name is mapped according to the refspec and that\n> it the ref that is updated in the remote repository. It then takes that\n> ref and checks the fetch refspecs remote.$remote.fetch and if it finds a\n> match it maps the ref we've pushed to a remote tracking ref in the local\n> repository.\n>\n> It looks like you can find where we would push a branch to with\n> remote_ref_for_branch() and you can map that to a remote tracking ref with\n> tracking_for_push_dest(). We could use branch_get_push() but does more\n> than just map the refs as it checks push.default to see whether \"git push\"\n> would actually push the branch.\n\nThanks! I have updated the code and added a test for remapped refspec.\n\nWith that said, I don't 100% understand what I'm doing with the refspecs, so\nmaybe you can give it another review?\n\n\nHarald\n"},{"id":"532953","messageId":"pull.2138.v14.git.git.1767445236.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v13.git.git.1767409701.gitgitgadget@gmail.com","subject":"[PATCH v14 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-03T13:00:34Z","receivedAt":"2026-01-03T13:00:38Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\n\nHarald Nordgren (2):\n  refactor: format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 171 ++++++++++++++++++++++++-------\n t/t6040-tracking-info.sh | 210 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 344 insertions(+), 37 deletions(-)\n\n\nbase-commit: 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v14\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v14\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v13:\n\n 1:  a2c160c53e = 1:  a2c160c53e refactor: format_branch_comparison in preparation\n 2:  891239211e ! 2:  b9b2f15498 status: show comparison with push remote tracking branch\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \n      +static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n      +{\n     ++\tstruct remote *remote;\n      +\tconst char *push_remote;\n     ++\tchar *push_dst = NULL;\n     ++\tchar *tracking_ref;\n      +\tconst char *resolved;\n     -+\tstruct strbuf ref_buf = STRBUF_INIT;\n     -+\tchar *ret = NULL;\n     ++\tchar *ret;\n      +\n      +\tif (!branch)\n      +\t\treturn NULL;\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\tif (!push_remote)\n      +\t\treturn NULL;\n      +\n     -+\tstrbuf_addf(&ref_buf, \"refs/remotes/%s/%s\", push_remote, branch->name);\n     ++\tremote = remotes_remote_get(the_repository, push_remote);\n     ++\tif (!remote)\n     ++\t\treturn NULL;\n     ++\n     ++\tpush_dst = remote_ref_for_branch(branch, 1);\n     ++\tif (!push_dst) {\n     ++\t\tif (remote->push.nr)\n     ++\t\t\treturn NULL;\n     ++\t\tpush_dst = xstrdup(branch->refname);\n     ++\t}\n     ++\n     ++\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n     ++\tfree(push_dst);\n     ++\n     ++\tif (!tracking_ref)\n     ++\t\treturn NULL;\n      +\n      +\tresolved = refs_resolve_ref_unsafe(\n      +\t\tget_main_ref_store(the_repository),\n     -+\t\tref_buf.buf,\n     ++\t\ttracking_ref,\n      +\t\tRESOLVE_REF_READING,\n      +\t\tNULL, NULL);\n      +\n     -+\tif (resolved) {\n     -+\t\tif (full_ref_out)\n     -+\t\t\t*full_ref_out = xstrdup(resolved);\n     -+\t\tret = refs_shorten_unambiguous_ref(\n     -+\t\t\tget_main_ref_store(the_repository), resolved, 0);\n     ++\tif (!resolved) {\n     ++\t\tfree(tracking_ref);\n     ++\t\treturn NULL;\n      +\t}\n      +\n     -+\tstrbuf_release(&ref_buf);\n     ++\tif (full_ref_out)\n     ++\t\t*full_ref_out = xstrdup(resolved);\n     ++\tret = refs_shorten_unambiguous_ref(\n     ++\t\tget_main_ref_store(the_repository), resolved, 0);\n     ++\tfree(tracking_ref);\n      +\treturn ret;\n      +}\n      +\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\tEOF\n      +\ttest_cmp expect actual\n      +'\n     ++\n     ++test_expect_success 'status shows remapped push refspec' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout -b feature8 origin/main &&\n     ++\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n     ++\t\tgit push &&\n     ++\t\tadvance work &&\n     ++\t\tgit status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature8\n     ++\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n     ++\n     ++\tYour branch is ahead of ${SQ}origin/remapped${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     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'status shows remapped push refspec with upstream remote' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout -b feature9 upstream/main &&\n     ++\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n     ++\t\tgit push origin &&\n     ++\t\tadvance work &&\n     ++\t\tgit status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature9\n     ++\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n     ++\n     ++\tYour branch is ahead of ${SQ}origin/remapped${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     ++\ttest_cmp expect actual\n     ++'\n      +\n       test_done\n\n-- \ngitgitgadget\n"},{"id":"532954","messageId":"a2c160c53ee0159a88234c64409f2a216d584cc4.1767445236.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v14.git.git.1767445236.gitgitgadget@gmail.com","subject":"[PATCH v14 1/2] refactor: format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-03T13:00:35Z","receivedAt":"2026-01-03T13:00:39Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 99 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 59 insertions(+), 40 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..58093f64b0 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,66 +2237,50 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     int ahead, int behind,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     int upstream_is_gone,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     int sti)\n {\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else if (!sti) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n-\t} else if (!theirs) {\n+\t} else if (ahead == 0 && behind == 0) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n+\t\t\tbranch_name);\n+\t} else if (ahead > 0 && behind == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n-\t\t\t   ours),\n-\t\t\tbase, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n-\t} else if (!ours) {\n+\t\t\t   ahead),\n+\t\t\tbranch_name, ahead);\n+\t} else if (behind > 0 && ahead == 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n-\t\t\t   theirs),\n-\t\t\tbase, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n-\t} else {\n+\t\t\t   behind),\n+\t\t\tbranch_name, behind);\n+\t} else if (ahead > 0 && behind > 0) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n \t\t\t       \"and have %d and %d different commit each, \"\n@@ -2304,13 +2288,48 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch and '%s' have diverged,\\n\"\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n-\t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\t   ahead + behind),\n+\t\t\tbranch_name, ahead, behind);\n+\t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, sti;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (sti < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n+\tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\t} else if (!ours && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n+\t\t} else if (ours && theirs && show_divergence_advice &&\n+\t\t\t   advice_enabled(ADVICE_STATUS_HINTS)) {\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n+\t\t}\n \t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"532955","messageId":"b9b2f154982c216a3f19d567be97185196723350.1767445236.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v14.git.git.1767445236.gitgitgadget@gmail.com","subject":"[PATCH v14 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-03T13:00:36Z","receivedAt":"2026-01-03T13:00:41Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 |  80 ++++++++++++++-\n t/t6040-tracking-info.sh | 210 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 289 insertions(+), 1 deletion(-)\n\ndiff --git a/remote.c b/remote.c\nindex 58093f64b0..f5d690d377 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,6 +2237,58 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (full_ref_out)\n+\t\t*full_ref_out = xstrdup(resolved);\n+\tret = refs_shorten_unambiguous_ref(\n+\t\tget_main_ref_store(the_repository), resolved, 0);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     int ahead, int behind,\n \t\t\t\t     const char *branch_name,\n@@ -2304,6 +2356,10 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tint push_ours, push_theirs, push_sti;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tint show_push_comparison = 0;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2315,9 +2371,18 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_sti >= 0)\n+\t\t\tshow_push_comparison = 1;\n+\t}\n+\n \tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n \tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n-\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\tif (!theirs && !show_push_comparison &&\n+\t\t    advice_enabled(ADVICE_STATUS_HINTS)) {\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t\t} else if (!ours && advice_enabled(ADVICE_STATUS_HINTS)) {\n@@ -2330,7 +2395,20 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t}\n \t}\n \n+\tif (show_push_comparison) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, push_ours, push_theirs, push, 0, abf, push_sti);\n+\t\tif (push_sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n+\t\t\tif (!push_theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n+\t\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n+\t\t\t}\n+\t\t}\n+\t}\n+\n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..68f298bf3a 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,214 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"532960","messageId":"xmqq7btyqfxy.fsf@gitster.g","threadId":"64668","inReplyTo":"20260101233839.17639-1-haraldnordgren@gmail.com","subject":"Re: Another look?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-04T02:17:45Z","receivedAt":"2026-01-04T02:17:47Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> Again this seems to do a \"step 1 goes in a direction, step 2 fixes\n>> its mistake, step 3 changes course\" drunken-man's walk.\n>>\n>> The same advice to restructure them into a logical incremental\n>> progression that moves the codebase in one consistent direction to\n>> eventually reach the goal at the end applies.\n>\n> Isn't programming always bit of drunken-man's walk?\n\nYes, but the point is that other people do not have to see you\ntaking roundabout route (or for that matter, they do not have to see\nyou coding in your bathrobe like Linus, even if it may be true you\n;-).\n\nWhen other developers later need to figure out what you really\nwanted to achieve so that they do not break your intention while\nthey update your code to fix bugs in it and/or adding new features\non top of it, it would be far easier for them to work on a clean\nhistory.\n"},{"id":"532966","messageId":"aVnTc8GmqH+5Ui3M@ubby","threadId":"64668","inReplyTo":"xmqq7btyqfxy.fsf@gitster.g","subject":"Re: Another look?","fromName":"Nico Williams","fromEmail":"nico@cryptonector.com","sentAt":"2026-01-04T02:41:55Z","receivedAt":"2026-01-04T02:42:05Z","isPatch":false,"sender":{"key":"nico@cryptonector.com","avatar":null},"body":"On Sun, Jan 04, 2026 at 11:17:45AM +0900, Junio C Hamano wrote:\n> Harald Nordgren <haraldnordgren@gmail.com> writes:\n> > Isn't programming always bit of drunken-man's walk?\n> \n> Yes, but the point is that other people do not have to see you\n> taking roundabout route (or for that matter, they do not have to see\n> you coding in your bathrobe like Linus, even if it may be true you\n> ;-).\n\nSun Microsystems, Inc. used a rebase workflow from 1992 to its end\n(RIP).  Thousands of developers worked with stricly linear history\n(merges were not allowed!).  Large team projects that had to have their\nown alternate repositories and do the Teamware equivalent of `git rebase\n--onto` periodically.  There was no branching, only forking.  Forks were\nkept for backporting and for history archival purposes.\n\nIt worked like a charm.\n\nMerge workflows are -IMO- toxic and obnoxious.  I do NOT want to see any\nprogrammer's \"drunken-man's walk\", unless it's because I'm interviewing\nthem.  There is no value to preserving that.\n\n> When other developers later need to figure out what you really\n> wanted to achieve so that they do not break your intention while\n> they update your code to fix bugs in it and/or adding new features\n> on top of it, it would be far easier for them to work on a clean\n> history.\n\nHear hear!\n\nStrictly linear history makes it much easier to understand what's going\non.  The only merge turds I would allow are fast-forward ones where the\npurpose of the merge commit is to document push boundaries in the commit\nhistory, but even this I don't like.\n\nNico\n-- \n"},{"id":"532971","messageId":"xmqqo6nanh8o.fsf@gitster.g","threadId":"64668","inReplyTo":"aVnTc8GmqH+5Ui3M@ubby","subject":"Re: Another look?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-04T04:17:59Z","receivedAt":"2026-01-04T04:18:02Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Nico Williams <nico@cryptonector.com> writes:\n\n> Sun Microsystems, Inc. used a rebase workflow from 1992 to its end\n> ...\n> It worked like a charm.\n\nThat's good for you.  But I do not necessarily think merge workflows\nare bad, and my advice was certainly *not* about avoiding merges.\n\nA linear logical progression of commits that is about a single theme\nis much nicer than drunken-man's walk that is also a linear sequence\nof commits.  The distinction between them has nothing to do with\nmerges.\n\nAnd a history that bundles together a collection of linear logical\nprogressions with merges of these topics one by one is much easier\nthan rebasing these unrelated topics into a strictly linear single\nstrand of pearls, by making it much clear where each of these topics\nconcludes.\n\n"},{"id":"532972","messageId":"xmqq8qeeng7c.fsf@gitster.g","threadId":"64668","inReplyTo":"a2c160c53ee0159a88234c64409f2a216d584cc4.1767445236.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v14 1/2] refactor: format_branch_comparison in preparation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-04T04:40:23Z","receivedAt":"2026-01-04T04:40:25Z","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> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> Refactor format_branch_comparison function in preparation for showing\n> comparison with push remote tracking branch.\n>\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n>  remote.c | 99 +++++++++++++++++++++++++++++++++-----------------------\n>  1 file changed, 59 insertions(+), 40 deletions(-)\n>\n> diff --git a/remote.c b/remote.c\n> index 59b3715120..58093f64b0 100644\n> --- a/remote.c\n> +++ b/remote.c\n> @@ -2237,66 +2237,50 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n>  \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n>  }\n>  \n> -/*\n> - * Return true when there is anything to report, otherwise false.\n> - */\n> -int format_tracking_info(struct branch *branch, struct strbuf *sb,\n> -\t\t\t enum ahead_behind_flags abf,\n> -\t\t\t int show_divergence_advice)\n> +static void format_branch_comparison(struct strbuf *sb,\n> +\t\t\t\t     int ahead, int behind,\n> +\t\t\t\t     const char *branch_name,\n> +\t\t\t\t     int upstream_is_gone,\n> +\t\t\t\t     enum ahead_behind_flags abf,\n> +\t\t\t\t     int sti)\n>  {\n> -\tint ours, theirs, sti;\n> -\tconst char *full_base;\n> -\tchar *base;\n> -\tint upstream_is_gone = 0;\n> -\n> -\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n> -\tif (sti < 0) {\n> -\t\tif (!full_base)\n> -\t\t\treturn 0;\n> -\t\tupstream_is_gone = 1;\n> -\t}\n> -\n> -\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n> -\t\t\t\t\t    full_base, 0);\n>  \tif (upstream_is_gone) {\n>  \t\tstrbuf_addf(sb,\n>  \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n> -\t\t\tbase);\n> +\t\t\tbranch_name);\n>  \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n>  \t\t\tstrbuf_addstr(sb,\n>  \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n>  \t} else if (!sti) {\n>  \t\tstrbuf_addf(sb,\n>  \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n> -\t\t\tbase);\n> +\t\t\tbranch_name);\n\nSo, we used to say this when stat-tracking-info gave 0, i.e., ours\nand theirs have not diverged.  But now you decided to make the\ncaller responsible for making the call to stat_tracking_info(), it\nseems to me that this if/else-if arm is somewhat redundant given\nthat the caller can and will signal the same thing with both ahead\nand behind being zero.\n\nIOW, it smells to me that leaving \"sti\" as a parameter to this\nfunction is an incomplete refactoring---I say \"smell\" because I\nhaven't seen the other, new, caller that will be added in the future\nstep of this patch series.\n\nAnd if the new calling convention is to let the caller be\nresponsible for calling stat_tracking_info() and figuring out the\nbase branch name, it probably is also better to have the caller\nhandle upstream_is_gone case.  This is especially true if your plan\nis to reuse this \"branch comparison\" helper to compare a branch with\nanother branch that is *NOT* its upstream (e.g., where the result is\npushed to).\n\n>  \t} else if (abf == AHEAD_BEHIND_QUICK) {\n>  \t\tstrbuf_addf(sb,\n>  \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n> -\t\t\t    base);\n> +\t\t\t    branch_name);\n>  \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n>  \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n>  \t\t\t\t    \"git status --ahead-behind\");\n> -\t} else if (!theirs) {\n\nAs the code already handled !sti, which is equivalent to (!theirs &&\n!ours), in the original, !theirs here meant (!theirs && ours), which\nis (ahead && !behind) in the new world order, which we see a few lines\nbelow.\n\n> +\t} else if (ahead == 0 && behind == 0) {\n\nAnd this is what the above (!sti) should have checked for, after the\nhelper function lost the sti parameter.\n\nDo not compare with 0 for equality.  Instead write it like so:\n\n\t} else if (!ahead && !behind) {\n\nI won't repeat for other style violations of the same kind.\n\n> +\t\tstrbuf_addf(sb,\n> +\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n> +\t\t\tbranch_name);\n\n> +\t} else if (ahead > 0 && behind == 0) {\n>  \t\tstrbuf_addf(sb,\n>  \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n>  \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n> -\t\t\t   ours),\n> -\t\t\tbase, ours);\n> -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n> -\t\t\tstrbuf_addstr(sb,\n> -\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n> -\t} else if (!ours) {\n> +\t\t\t   ahead),\n> +\t\t\tbranch_name, ahead);\n> +\t} else if (behind > 0 && ahead == 0) {\n>  \t\tstrbuf_addf(sb,\n>  \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n>  \t\t\t       \"and can be fast-forwarded.\\n\",\n>  \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n>  \t\t\t       \"and can be fast-forwarded.\\n\",\n> -\t\t\t   theirs),\n> -\t\t\tbase, theirs);\n> -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n> -\t\t\tstrbuf_addstr(sb,\n> -\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n> -\t} else {\n> +\t\t\t   behind),\n> +\t\t\tbranch_name, behind);\n> +\t} else if (ahead > 0 && behind > 0) {\n>  \t\tstrbuf_addf(sb,\n>  \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n>  \t\t\t       \"and have %d and %d different commit each, \"\n> @@ -2304,13 +2288,48 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>  \t\t\t   \"Your branch and '%s' have diverged,\\n\"\n>  \t\t\t       \"and have %d and %d different commits each, \"\n>  \t\t\t       \"respectively.\\n\",\n> -\t\t\t   ours + theirs),\n> -\t\t\tbase, ours, theirs);\n> -\t\tif (show_divergence_advice &&\n> -\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n> +\t\t\t   ahead + behind),\n> +\t\t\tbranch_name, ahead, behind);\n> +\t}\n> +}\n> +\n> +/*\n> + * Return true when there is anything to report, otherwise false.\n> + */\n> +int format_tracking_info(struct branch *branch, struct strbuf *sb,\n> +\t\t\t enum ahead_behind_flags abf,\n> +\t\t\t int show_divergence_advice)\n> +{\n> +\tint ours, theirs, sti;\n> +\tconst char *full_base;\n> +\tchar *base;\n> +\tint upstream_is_gone = 0;\n> +\n> +\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n> +\tif (sti < 0) {\n> +\t\tif (!full_base)\n> +\t\t\treturn 0;\n> +\t\tupstream_is_gone = 1;\n> +\t}\n> +\n> +\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n> +\t\t\t\t\t    full_base, 0);\n> +\n> +\tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n> +\tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n> +\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n> +\t\t\tstrbuf_addstr(sb,\n> +\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n> +\t\t} else if (!ours && advice_enabled(ADVICE_STATUS_HINTS)) {\n> +\t\t\tstrbuf_addstr(sb,\n> +\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n> +\t\t} else if (ours && theirs && show_divergence_advice &&\n> +\t\t\t   advice_enabled(ADVICE_STATUS_HINTS)) {\n>  \t\t\tstrbuf_addstr(sb,\n>  \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n> +\t\t}\n>  \t}\n> +\n>  \tfree(base);\n>  \treturn 1;\n>  }\n\nOverall I think this is a reasonable direction to go, even though\npassing the \"sti\" thing is iffy, and the implementation in the\nfunction may have small rooms for improvements.\n\nI didn't look at minute details to ensure that the output for all\ncases are identical to that of the original, though.\n\n"},{"id":"532981","messageId":"20260104102749.30950-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqq8qeeng7c.fsf@gitster.g","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-04T10:27:49Z","receivedAt":"2026-01-04T10:27:53Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> And if the new calling convention is to let the caller be responsible for\n> calling stat_tracking_info() and figuring out the base branch name, it\n> probably is also better to have the caller handle upstream_is_gone case.\n> This is especially true if your plan is to reuse this \"branch comparison\"\n> helper to compare a branch with another branch that is *NOT* its upstream\n> (e.g., where the result is pushed to).\n\nDone, will be in the next patch!\n\n> IOW, it smells to me that leaving \"sti\" as a parameter to this function\n> is an incomplete refactoring---I say \"smell\" because I haven't seen the\n> other, new, caller that will be added in the future step of this patch\n> series.\n>\n> [...]\n>\n> As the code already handled !sti, which is equivalent to (!theirs &&\n> !ours), in the original, !theirs here meant (!theirs && ours), which is\n> (ahead && !behind) in the new world order, which we see a few lines below.\n\nThis sounds reasonable, however, if I replace '!sti' with 'ahead && !behind'\nthen old tests are breaking, one why to avoid them breaking is to switch the\norder of these cases, to this, would that be acceptable?\n\n``` } else if (abf == AHEAD_BEHIND_QUICK) { strbuf_addf(sb, _(\"Your\nbranch and '%s' refer to different commits.\\n\"), branch_name); if\n(advice_enabled(ADVICE_STATUS_HINTS)) strbuf_addf(sb, _(\" (use \\\"%s\\\" for\ndetails)\\n\"), \"git status --ahead-behind\"); } else if (!theirs && !ours) {\nstrbuf_addf(sb, _(\"Your branch is up to date with '%s'.\\n\"), branch_name);\n```\n\n> Do not compare with 0 for equality. Instead write it like so:\n\nGood point! I will upate it!\n\nThanks for you continued attention to this, much appreciated!\n\n\n\nHarald\n"},{"id":"532985","messageId":"20260104104855.31105-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260104102749.30950-1-haraldnordgren@gmail.com","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-04T10:48:55Z","receivedAt":"2026-01-04T10:48:58Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Sorry, here's what the code block will look like:\n\n```\n\t} else if (abf == AHEAD_BEHIND_QUICK) {\n\t\tstrbuf_addf(sb,\n\t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n\t\t\t    branch_name);\n\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n\t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n\t\t\t\t    \"git status --ahead-behind\");\n\t} else if (!theirs && !ours) {\n\t\tstrbuf_addf(sb,\n\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n\t\t\tbranch_name);\n```\n\n\nHarald\n"},{"id":"532988","messageId":"pull.2138.v15.git.git.1767527634.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v14.git.git.1767445236.gitgitgadget@gmail.com","subject":"[PATCH v15 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-04T11:53:52Z","receivedAt":"2026-01-04T11:53:57Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 168 ++++++++++++++++++++++++-------\n t/t6040-tracking-info.sh | 210 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 342 insertions(+), 36 deletions(-)\n\n\nbase-commit: 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v15\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v15\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v14:\n\n 1:  a2c160c53e ! 1:  cf4e9779c5 refactor: format_branch_comparison in preparation\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    refactor: format_branch_comparison in preparation\n     +    refactor format_branch_comparison in preparation\n      \n          Refactor format_branch_comparison function in preparation for showing\n          comparison with push remote tracking branch.\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      -\t\t\t enum ahead_behind_flags abf,\n      -\t\t\t int show_divergence_advice)\n      +static void format_branch_comparison(struct strbuf *sb,\n     -+\t\t\t\t     int ahead, int behind,\n     ++\t\t\t\t     int ours, int theirs,\n      +\t\t\t\t     const char *branch_name,\n     -+\t\t\t\t     int upstream_is_gone,\n      +\t\t\t\t     enum ahead_behind_flags abf,\n     -+\t\t\t\t     int sti)\n     ++\t\t\t\t     int show_divergence_advice)\n       {\n      -\tint ours, theirs, sti;\n      -\tconst char *full_base;\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      -\n      -\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n      -\t\t\t\t\t    full_base, 0);\n     - \tif (upstream_is_gone) {\n     - \t\tstrbuf_addf(sb,\n     - \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n     +-\tif (upstream_is_gone) {\n     +-\t\tstrbuf_addf(sb,\n     +-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n      -\t\t\tbase);\n     -+\t\t\tbranch_name);\n     - \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     - \t\t\tstrbuf_addstr(sb,\n     - \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n     - \t} else if (!sti) {\n     - \t\tstrbuf_addf(sb,\n     - \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n     +-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     +-\t\t\tstrbuf_addstr(sb,\n     +-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n     +-\t} else if (!sti) {\n     +-\t\tstrbuf_addf(sb,\n     +-\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n      -\t\t\tbase);\n     -+\t\t\tbranch_name);\n     - \t} else if (abf == AHEAD_BEHIND_QUICK) {\n     +-\t} else if (abf == AHEAD_BEHIND_QUICK) {\n     ++\tif (abf == AHEAD_BEHIND_QUICK) {\n       \t\tstrbuf_addf(sb,\n       \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n      -\t\t\t    base);\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n       \t\t\t\t    \"git status --ahead-behind\");\n     --\t} else if (!theirs) {\n     -+\t} else if (ahead == 0 && behind == 0) {\n     ++\t} else if (!ours && !theirs) {\n      +\t\tstrbuf_addf(sb,\n      +\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n      +\t\t\tbranch_name);\n     -+\t} else if (ahead > 0 && behind == 0) {\n     + \t} else if (!theirs) {\n       \t\tstrbuf_addf(sb,\n       \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n       \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n     --\t\t\t   ours),\n     + \t\t\t   ours),\n      -\t\t\tbase, ours);\n     --\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     --\t\t\tstrbuf_addstr(sb,\n     --\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n     --\t} else if (!ours) {\n     -+\t\t\t   ahead),\n     -+\t\t\tbranch_name, ahead);\n     -+\t} else if (behind > 0 && ahead == 0) {\n     - \t\tstrbuf_addf(sb,\n     - \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n     - \t\t\t       \"and can be fast-forwarded.\\n\",\n     ++\t\t\tbranch_name, ours);\n     + \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     + \t\t\tstrbuf_addstr(sb,\n     + \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n     +@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n       \t\t\t       \"and can be fast-forwarded.\\n\",\n     --\t\t\t   theirs),\n     + \t\t\t   theirs),\n      -\t\t\tbase, theirs);\n     --\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     --\t\t\tstrbuf_addstr(sb,\n     --\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n     --\t} else {\n     -+\t\t\t   behind),\n     -+\t\t\tbranch_name, behind);\n     -+\t} else if (ahead > 0 && behind > 0) {\n     - \t\tstrbuf_addf(sb,\n     - \t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n     - \t\t\t       \"and have %d and %d different commit each, \"\n     ++\t\t\tbranch_name, theirs);\n     + \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     + \t\t\tstrbuf_addstr(sb,\n     + \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \t\t\t   \"Your branch and '%s' have diverged,\\n\"\n       \t\t\t       \"and have %d and %d different commits each, \"\n       \t\t\t       \"respectively.\\n\",\n     --\t\t\t   ours + theirs),\n     + \t\t\t   ours + theirs),\n      -\t\t\tbase, ours, theirs);\n     --\t\tif (show_divergence_advice &&\n     --\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\t\t   ahead + behind),\n     -+\t\t\tbranch_name, ahead, behind);\n     -+\t}\n     ++\t\t\tbranch_name, ours, theirs);\n     + \t\tif (show_divergence_advice &&\n     + \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     + \t\t\tstrbuf_addstr(sb,\n     + \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n     + \t}\n      +}\n      +\n      +/*\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      +\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n      +\t\t\t\t\t    full_base, 0);\n      +\n     -+\tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n     -+\tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n     -+\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n     -+\t\t\tstrbuf_addstr(sb,\n     -+\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n     -+\t\t} else if (!ours && advice_enabled(ADVICE_STATUS_HINTS)) {\n     ++\tif (upstream_is_gone) {\n     ++\t\tstrbuf_addf(sb,\n     ++\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n     ++\t\t\tbase);\n     ++\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n      +\t\t\tstrbuf_addstr(sb,\n     -+\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n     -+\t\t} else if (ours && theirs && show_divergence_advice &&\n     -+\t\t\t   advice_enabled(ADVICE_STATUS_HINTS)) {\n     - \t\t\tstrbuf_addstr(sb,\n     - \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n     -+\t\t}\n     - \t}\n     ++\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n     ++\t} else {\n     ++\t\tformat_branch_comparison(sb, ours, theirs, base, abf, show_divergence_advice);\n     ++\t}\n      +\n       \tfree(base);\n       \treturn 1;\n 2:  b9b2f15498 ! 2:  a435cf4ce4 status: show comparison with push remote tracking branch\n     @@ Commit message\n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## remote.c ##\n     +@@\n     + \n     + enum map_direction { FROM_SRC, FROM_DST };\n     + \n     ++enum branch_type {\n     ++\tPUSH = 1 << 0,\n     ++\tPULL = 1 << 1\n     ++};\n     ++\n     + struct counted_string {\n     + \tsize_t len;\n     + \tconst char *s;\n      @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n       \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n       }\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +}\n      +\n       static void format_branch_comparison(struct strbuf *sb,\n     - \t\t\t\t     int ahead, int behind,\n     + \t\t\t\t     int ours, int theirs,\n       \t\t\t\t     const char *branch_name,\n     + \t\t\t\t     enum ahead_behind_flags abf,\n     ++\t\t\t\t     enum branch_type bt,\n     + \t\t\t\t     int show_divergence_advice)\n     + {\n     + \tif (abf == AHEAD_BEHIND_QUICK) {\n     +@@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n     + \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n     + \t\t\t   ours),\n     + \t\t\tbranch_name, ours);\n     +-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     ++\t\tif ((bt & PUSH) && advice_enabled(ADVICE_STATUS_HINTS))\n     + \t\t\tstrbuf_addstr(sb,\n     + \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n     + \t} else if (!ours) {\n     +@@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n     + \t\t\t       \"and can be fast-forwarded.\\n\",\n     + \t\t\t   theirs),\n     + \t\t\tbranch_name, theirs);\n     +-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     ++\t\tif ((bt & PULL) && advice_enabled(ADVICE_STATUS_HINTS))\n     + \t\t\tstrbuf_addstr(sb,\n     + \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n     + \t} else {\n     +@@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n     + \t\t\t       \"respectively.\\n\",\n     + \t\t\t   ours + theirs),\n     + \t\t\tbranch_name, ours, theirs);\n     +-\t\tif (show_divergence_advice &&\n     ++\t\tif ((bt & PULL) &&\n     ++\t\t    show_divergence_advice &&\n     + \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     + \t\t\tstrbuf_addstr(sb,\n     + \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tconst char *full_base;\n       \tchar *base;\n       \tint upstream_is_gone = 0;\n     ++\tenum branch_type base_bt = PUSH | PULL;\n      +\tint push_ours, push_theirs, push_sti;\n      +\tchar *full_push = NULL;\n      +\tchar *push = NULL;\n     -+\tint show_push_comparison = 0;\n     ++\tenum branch_type push_bt = 0;\n       \n       \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n       \tif (sti < 0) {\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      +\tif (push && strcmp(base, push)) {\n      +\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n      +\t\t\t\t\t   &push_ours, &push_theirs, abf);\n     -+\t\tif (push_sti >= 0)\n     -+\t\t\tshow_push_comparison = 1;\n     ++\t\tif (push_sti >= 0) {\n     ++\t\t\tbase_bt = PULL;\n     ++\t\t\tpush_bt = PUSH;\n     ++\t\t}\n      +\t}\n      +\n     - \tformat_branch_comparison(sb, ours, theirs, base, upstream_is_gone, abf, sti);\n     - \tif (sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n     --\t\tif (!theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n     -+\t\tif (!theirs && !show_push_comparison &&\n     -+\t\t    advice_enabled(ADVICE_STATUS_HINTS)) {\n     - \t\t\tstrbuf_addstr(sb,\n     - \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n     - \t\t} else if (!ours && advice_enabled(ADVICE_STATUS_HINTS)) {\n     + \tif (upstream_is_gone) {\n     + \t\tstrbuf_addf(sb,\n     + \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \t\t}\n     - \t}\n     - \n     -+\tif (show_push_comparison) {\n     -+\t\tstrbuf_addstr(sb, \"\\n\");\n     -+\t\tformat_branch_comparison(sb, push_ours, push_theirs, push, 0, abf, push_sti);\n     -+\t\tif (push_sti > 0 && abf != AHEAD_BEHIND_QUICK) {\n     -+\t\t\tif (!push_theirs && advice_enabled(ADVICE_STATUS_HINTS)) {\n     -+\t\t\t\tstrbuf_addstr(sb,\n     -+\t\t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n     -+\t\t\t}\n     -+\t\t}\n     + \t\t\tstrbuf_addstr(sb,\n     + \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n     + \t} else {\n     +-\t\tformat_branch_comparison(sb, ours, theirs, base, abf, show_divergence_advice);\n     ++\t\tformat_branch_comparison(sb, ours, theirs, base, abf, base_bt,\n     ++\t\t\t\t\t show_divergence_advice);\n      +\t}\n      +\n     ++\tif (push_bt & PUSH) {\n     ++\t\tstrbuf_addstr(sb, \"\\n\");\n     ++\t\tformat_branch_comparison(sb, push_ours, push_theirs, push, abf,\n     ++\t\t\t\t\t push_bt, 0);\n     + \t}\n     + \n       \tfree(base);\n      +\tfree(full_push);\n      +\tfree(push);\n\n-- \ngitgitgadget\n"},{"id":"532989","messageId":"cf4e9779c5d47b22bd4a04fd9b2b138d5602145a.1767527634.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v15.git.git.1767527634.gitgitgadget@gmail.com","subject":"[PATCH v15 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-04T11:53:53Z","receivedAt":"2026-01-04T11:53:58Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 85 ++++++++++++++++++++++++++++++++------------------------\n 1 file changed, 49 insertions(+), 36 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..b6a9e14376 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,51 +2237,29 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     int show_divergence_advice)\n {\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n-\t} else if (abf == AHEAD_BEHIND_QUICK) {\n+\tif (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n+\t} else if (!ours && !theirs) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n+\t\t\tbranch_name);\n \t} else if (!theirs) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2292,7 +2270,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2305,12 +2283,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, sti;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (sti < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"532990","messageId":"a435cf4ce42e18ef3b1d4b7fc5f9ecef7151f57e.1767527634.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v15.git.git.1767527634.gitgitgadget@gmail.com","subject":"[PATCH v15 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-04T11:53:54Z","receivedAt":"2026-01-04T11:53: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\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 |  91 ++++++++++++++++-\n t/t6040-tracking-info.sh | 210 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 297 insertions(+), 4 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b6a9e14376..3a8706395a 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,11 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum branch_type {\n+\tPUSH = 1 << 0,\n+\tPULL = 1 << 1\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2237,10 +2242,63 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (full_ref_out)\n+\t\t*full_ref_out = xstrdup(resolved);\n+\tret = refs_shorten_unambiguous_ref(\n+\t\tget_main_ref_store(the_repository), resolved, 0);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     enum branch_type bt,\n \t\t\t\t     int show_divergence_advice)\n {\n \tif (abf == AHEAD_BEHIND_QUICK) {\n@@ -2260,7 +2318,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif ((bt & PUSH) && advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2271,7 +2329,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif ((bt & PULL) && advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2284,7 +2342,8 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n+\t\tif ((bt & PULL) &&\n+\t\t    show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n@@ -2302,6 +2361,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tenum branch_type base_bt = PUSH | PULL;\n+\tint push_ours, push_theirs, push_sti;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tenum branch_type push_bt = 0;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2313,6 +2377,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_sti >= 0) {\n+\t\t\tbase_bt = PULL;\n+\t\t\tpush_bt = PUSH;\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2321,10 +2395,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, ours, theirs, base, abf, show_divergence_advice);\n+\t\tformat_branch_comparison(sb, ours, theirs, base, abf, base_bt,\n+\t\t\t\t\t show_divergence_advice);\n+\t}\n+\n+\tif (push_bt & PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_bt, 0);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..68f298bf3a 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,214 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533001","messageId":"pull.2138.v16.git.git.1767568882.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v15.git.git.1767527634.gitgitgadget@gmail.com","subject":"[PATCH v16 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-04T23:21:20Z","receivedAt":"2026-01-04T23:21:25Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 171 ++++++++++++++++++++++++-------\n t/t6040-tracking-info.sh | 210 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 345 insertions(+), 36 deletions(-)\n\n\nbase-commit: 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v16\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v16\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v15:\n\n 1:  cf4e9779c5 = 1:  cf4e9779c5 refactor format_branch_comparison in preparation\n 2:  a435cf4ce4 ! 2:  06cb483f61 status: show comparison with push remote tracking branch\n     @@ remote.c\n       \n       enum map_direction { FROM_SRC, FROM_DST };\n       \n     -+enum branch_type {\n     -+\tPUSH = 1 << 0,\n     -+\tPULL = 1 << 1\n     ++enum branch_mode_flags {\n     ++\tBRANCH_MODE_PULL = (1 << 0),\n     ++\tBRANCH_MODE_PUSH = (1 << 1),\n      +};\n      +\n       struct counted_string {\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\n      +\tif (full_ref_out)\n      +\t\t*full_ref_out = xstrdup(resolved);\n     ++\n      +\tret = refs_shorten_unambiguous_ref(\n      +\t\tget_main_ref_store(the_repository), resolved, 0);\n      +\tfree(tracking_ref);\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \t\t\t\t     int ours, int theirs,\n       \t\t\t\t     const char *branch_name,\n       \t\t\t\t     enum ahead_behind_flags abf,\n     -+\t\t\t\t     enum branch_type bt,\n     ++\t\t\t\t     enum branch_mode_flags advice_flags,\n       \t\t\t\t     int show_divergence_advice)\n       {\n       \tif (abf == AHEAD_BEHIND_QUICK) {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   ours),\n       \t\t\tbranch_name, ours);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif ((bt & PUSH) && advice_enabled(ADVICE_STATUS_HINTS))\n     ++\t\tif ((advice_flags & BRANCH_MODE_PUSH) &&\n     ++\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n       \t} else if (!ours) {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   theirs),\n       \t\t\tbranch_name, theirs);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif ((bt & PULL) && advice_enabled(ADVICE_STATUS_HINTS))\n     ++\t\tif ((advice_flags & BRANCH_MODE_PULL) &&\n     ++\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n       \t} else {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   ours + theirs),\n       \t\t\tbranch_name, ours, theirs);\n      -\t\tif (show_divergence_advice &&\n     -+\t\tif ((bt & PULL) &&\n     ++\t\tif ((advice_flags & BRANCH_MODE_PULL) &&\n      +\t\t    show_divergence_advice &&\n       \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addstr(sb,\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tconst char *full_base;\n       \tchar *base;\n       \tint upstream_is_gone = 0;\n     -+\tenum branch_type base_bt = PUSH | PULL;\n     ++\tenum branch_mode_flags base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n      +\tint push_ours, push_theirs, push_sti;\n      +\tchar *full_push = NULL;\n      +\tchar *push = NULL;\n     -+\tenum branch_type push_bt = 0;\n     ++\tenum branch_mode_flags push_branch_modes = 0;\n       \n       \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n       \tif (sti < 0) {\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      +\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n      +\t\t\t\t\t   &push_ours, &push_theirs, abf);\n      +\t\tif (push_sti >= 0) {\n     -+\t\t\tbase_bt = PULL;\n     -+\t\t\tpush_bt = PUSH;\n     ++\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n     ++\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n      +\t\t}\n      +\t}\n      +\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n       \t} else {\n      -\t\tformat_branch_comparison(sb, ours, theirs, base, abf, show_divergence_advice);\n     -+\t\tformat_branch_comparison(sb, ours, theirs, base, abf, base_bt,\n     -+\t\t\t\t\t show_divergence_advice);\n     ++\t\tformat_branch_comparison(sb, ours, theirs, base, abf,\n     ++\t\t\t\t\t base_branch_modes, show_divergence_advice);\n      +\t}\n      +\n     -+\tif (push_bt & PUSH) {\n     ++\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n      +\t\tstrbuf_addstr(sb, \"\\n\");\n      +\t\tformat_branch_comparison(sb, push_ours, push_theirs, push, abf,\n     -+\t\t\t\t\t push_bt, 0);\n     ++\t\t\t\t\t push_branch_modes, 0);\n       \t}\n       \n       \tfree(base);\n\n-- \ngitgitgadget\n"},{"id":"533002","messageId":"cf4e9779c5d47b22bd4a04fd9b2b138d5602145a.1767568882.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v16.git.git.1767568882.gitgitgadget@gmail.com","subject":"[PATCH v16 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-04T23:21:21Z","receivedAt":"2026-01-04T23:21:26Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 85 ++++++++++++++++++++++++++++++++------------------------\n 1 file changed, 49 insertions(+), 36 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..b6a9e14376 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,51 +2237,29 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     int show_divergence_advice)\n {\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n-\t} else if (abf == AHEAD_BEHIND_QUICK) {\n+\tif (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n+\t} else if (!ours && !theirs) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n+\t\t\tbranch_name);\n \t} else if (!theirs) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2292,7 +2270,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2305,12 +2283,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, sti;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (sti < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"533003","messageId":"06cb483f615220490e75edcdad1606bd12b71868.1767568882.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v16.git.git.1767568882.gitgitgadget@gmail.com","subject":"[PATCH v16 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-04T23:21:22Z","receivedAt":"2026-01-04T23:21:29Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 |  94 +++++++++++++++++-\n t/t6040-tracking-info.sh | 210 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 300 insertions(+), 4 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b6a9e14376..74a394c2a6 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,11 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum branch_mode_flags {\n+\tBRANCH_MODE_PULL = (1 << 0),\n+\tBRANCH_MODE_PUSH = (1 << 1),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2237,10 +2242,64 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (full_ref_out)\n+\t\t*full_ref_out = xstrdup(resolved);\n+\n+\tret = refs_shorten_unambiguous_ref(\n+\t\tget_main_ref_store(the_repository), resolved, 0);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     enum branch_mode_flags advice_flags,\n \t\t\t\t     int show_divergence_advice)\n {\n \tif (abf == AHEAD_BEHIND_QUICK) {\n@@ -2260,7 +2319,8 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif ((advice_flags & BRANCH_MODE_PUSH) &&\n+\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2271,7 +2331,8 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif ((advice_flags & BRANCH_MODE_PULL) &&\n+\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2284,7 +2345,8 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n+\t\tif ((advice_flags & BRANCH_MODE_PULL) &&\n+\t\t    show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n@@ -2302,6 +2364,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tenum branch_mode_flags base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n+\tint push_ours, push_theirs, push_sti;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tenum branch_mode_flags push_branch_modes = 0;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2313,6 +2380,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_sti >= 0) {\n+\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n+\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2321,10 +2398,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, ours, theirs, base, abf, show_divergence_advice);\n+\t\tformat_branch_comparison(sb, ours, theirs, base, abf,\n+\t\t\t\t\t base_branch_modes, show_divergence_advice);\n+\t}\n+\n+\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_branch_modes, 0);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..68f298bf3a 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,214 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533004","messageId":"xmqqqzs4n9jp.fsf@gitster.g","threadId":"64668","inReplyTo":"20260104104855.31105-1-haraldnordgren@gmail.com","subject":"Re: Another look?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-05T01:16:26Z","receivedAt":"2026-01-05T01:16:28Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> Sorry, here's what the code block will look like:\n>\n> ```\n> \t} else if (abf == AHEAD_BEHIND_QUICK) {\n> \t\tstrbuf_addf(sb,\n> \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n> \t\t\t    branch_name);\n> \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n> \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n> \t\t\t\t    \"git status --ahead-behind\");\n> \t} else if (!theirs && !ours) {\n> \t\tstrbuf_addf(sb,\n> \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n> \t\t\tbranch_name);\n> ```\n\nI did not check what comes before or after this block, but the above\nlooks like a reversed ordering.  If you have \"the branches are the\nsame\" check first, it would make more sense, as after ruling out\nthat case, QUICK can short-cut comparison and asy \"they are\ndifferent\", and presumably after these two else/if arms, you'd have\ncases for \"theirs && !ours -> they are ahead of us\", \"!theirs &&\nours -> we are ahead of them\", and \"theirs && ours -> we diverged\"\nto handle.\n"},{"id":"533007","messageId":"xmqqa4ysn7a5.fsf@gitster.g","threadId":"64668","inReplyTo":"cf4e9779c5d47b22bd4a04fd9b2b138d5602145a.1767568882.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v16 1/2] refactor format_branch_comparison in preparation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-05T02:05:22Z","receivedAt":"2026-01-05T02:05:24Z","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> -\t} else if (!sti) {\n> -\t\tstrbuf_addf(sb,\n> -\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n> -\t\t\tbase);\n> -\t} else if (abf == AHEAD_BEHIND_QUICK) {\n> +\tif (abf == AHEAD_BEHIND_QUICK) {\n>  \t\tstrbuf_addf(sb,\n>  \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n> -\t\t\t    base);\n> +\t\t\t    branch_name);\n>  \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n>  \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n>  \t\t\t\t    \"git status --ahead-behind\");\n> +\t} else if (!ours && !theirs) {\n> +\t\tstrbuf_addf(sb,\n> +\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n> +\t\t\tbranch_name);\n\nWe used to check if there is nothing to report (i.e., !sti is a\nsignal from stat_tracking_info() that there are no differences\nbetween the branches) and reported that first, so when abf was\nchecked, we knew that there are some differences.  Now, your patch\nreverses the order so whether there is or is not a change, abf\ncodepath will always report \"you have differences!\".\n\nThis smells iffy.\n"},{"id":"533011","messageId":"20260105091517.86301-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqa4ysn7a5.fsf@gitster.g","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-05T09:15:17Z","receivedAt":"2026-01-05T09:15:21Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> > -     } else if (!sti) {\n> > -             strbuf_addf(sb,\n> > -                     _(\"Your branch is up to date with '%s'.\\n\"),\n> > -                     base);\n> > -     } else if (abf == AHEAD_BEHIND_QUICK) {\n> > +     if (abf == AHEAD_BEHIND_QUICK) {\n> >               strbuf_addf(sb,\n> >                           _(\"Your branch and '%s' refer to different commits.\\n\"),\n> > -                         base);\n> > +                         branch_name);\n> >               if (advice_enabled(ADVICE_STATUS_HINTS))\n> >                       strbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n> >                                   \"git status --ahead-behind\");\n> > +     } else if (!ours && !theirs) {\n> > +             strbuf_addf(sb,\n> > +                     _(\"Your branch is up to date with '%s'.\\n\"),\n> > +                     branch_name);\n>\n> We used to check if there is nothing to report (i.e., !sti is a\n> signal from stat_tracking_info() that there are no differences\n> between the branches) and reported that first, so when abf was\n> checked, we knew that there are some differences.  Now, your patch\n> reverses the order so whether there is or is not a change, abf\n> codepath will always report \"you have differences!\".\n\nAgreed! This change was done to get rid of sti as a parameter. Maybe I\nmisunderstood your previous comment around the sti.\n\nI will bring back sti as a parameter to ’format_branch_comparison’ now,\nI don’t see any other way to solve this.\n\n\nHarald\n"},{"id":"533013","messageId":"20260105094633.9674-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260105091517.86301-1-haraldnordgren@gmail.com","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-05T09:46:33Z","receivedAt":"2026-01-05T09:46:36Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"I'm also adding better test coverage for '--no-ahead-behind', it's not good\nthat that feature can silently break.\n\n\nHarald\n"},{"id":"533016","messageId":"pull.2138.v17.git.git.1767608269.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v16.git.git.1767568882.gitgitgadget@gmail.com","subject":"[PATCH v17 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-05T10:17:47Z","receivedAt":"2026-01-05T10:17:52Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 169 ++++++++++++++++++++-----\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 397 insertions(+), 34 deletions(-)\n\n\nbase-commit: 68cb7f9e92a5d8e9824f5b52ac3d0a9d8f653dbe\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v17\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v17\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v16:\n\n 1:  cf4e9779c5 ! 1:  b62a9feb4d refactor format_branch_comparison in preparation\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      -int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      -\t\t\t enum ahead_behind_flags abf,\n      -\t\t\t int show_divergence_advice)\n     -+static void format_branch_comparison(struct strbuf *sb,\n     -+\t\t\t\t     int ours, int theirs,\n     -+\t\t\t\t     const char *branch_name,\n     -+\t\t\t\t     enum ahead_behind_flags abf,\n     -+\t\t\t\t     int show_divergence_advice)\n     - {\n     +-{\n      -\tint ours, theirs, sti;\n      -\tconst char *full_base;\n      -\tchar *base;\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      -\t\t\tstrbuf_addstr(sb,\n      -\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n      -\t} else if (!sti) {\n     --\t\tstrbuf_addf(sb,\n     --\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n     ++static void format_branch_comparison(struct strbuf *sb,\n     ++\t\t\t\t     int sti,\n     ++\t\t\t\t     int ours, int theirs,\n     ++\t\t\t\t     const char *branch_name,\n     ++\t\t\t\t     enum ahead_behind_flags abf,\n     ++\t\t\t\t     int show_divergence_advice)\n     ++{\n     ++\tif (!sti) {\n     + \t\tstrbuf_addf(sb,\n     + \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n      -\t\t\tbase);\n     --\t} else if (abf == AHEAD_BEHIND_QUICK) {\n     -+\tif (abf == AHEAD_BEHIND_QUICK) {\n     ++\t\t\tbranch_name);\n     + \t} else if (abf == AHEAD_BEHIND_QUICK) {\n       \t\tstrbuf_addf(sb,\n       \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n      -\t\t\t    base);\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n       \t\t\t\t    \"git status --ahead-behind\");\n     -+\t} else if (!ours && !theirs) {\n     -+\t\tstrbuf_addf(sb,\n     -+\t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n     -+\t\t\tbranch_name);\n     - \t} else if (!theirs) {\n     - \t\tstrbuf_addf(sb,\n     +@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n       \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n       \t\t\t   ours),\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      +\t\t\tstrbuf_addstr(sb,\n      +\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n      +\t} else {\n     -+\t\tformat_branch_comparison(sb, ours, theirs, base, abf, show_divergence_advice);\n     ++\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf, show_divergence_advice);\n      +\t}\n      +\n       \tfree(base);\n 2:  06cb483f61 ! 2:  1348542edc status: show comparison with push remote tracking branch\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +}\n      +\n       static void format_branch_comparison(struct strbuf *sb,\n     + \t\t\t\t     int sti,\n       \t\t\t\t     int ours, int theirs,\n       \t\t\t\t     const char *branch_name,\n       \t\t\t\t     enum ahead_behind_flags abf,\n      +\t\t\t\t     enum branch_mode_flags advice_flags,\n       \t\t\t\t     int show_divergence_advice)\n       {\n     - \tif (abf == AHEAD_BEHIND_QUICK) {\n     + \tif (!sti) {\n     +@@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n     + \t\tstrbuf_addf(sb,\n     + \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n     + \t\t\t    branch_name);\n     +-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     ++\t\tif ((advice_flags & BRANCH_MODE_PUSH) &&\n     ++\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     + \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n     + \t\t\t\t    \"git status --ahead-behind\");\n     + \t} else if (!theirs) {\n      @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n       \t\t\t   ours),\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n       \t} else {\n     --\t\tformat_branch_comparison(sb, ours, theirs, base, abf, show_divergence_advice);\n     -+\t\tformat_branch_comparison(sb, ours, theirs, base, abf,\n     +-\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf, show_divergence_advice);\n     ++\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf,\n      +\t\t\t\t\t base_branch_modes, show_divergence_advice);\n      +\t}\n      +\n      +\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n      +\t\tstrbuf_addstr(sb, \"\\n\");\n     -+\t\tformat_branch_comparison(sb, push_ours, push_theirs, push, abf,\n     ++\t\tformat_branch_comparison(sb, push_sti, push_ours, push_theirs, push, abf,\n      +\t\t\t\t\t push_branch_modes, 0);\n       \t}\n       \n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     ++test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout b4 &&\n     ++\t\tgit status --no-ahead-behind >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch b4\n     ++\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n     ++\t  (use \"git status --ahead-behind\" for details)\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n      +test_expect_success 'status shows ahead of both origin/main and feature branch' '\n      +\t(\n      +\t\tcd test &&\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     ++test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout feature4 &&\n     ++\t\tgit status --no-ahead-behind >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature4\n     ++\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n     ++\n     ++\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n     ++\t  (use \"git status --ahead-behind\" for details)\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n      +test_expect_success 'setup upstream remote' '\n      +\t(\n      +\t\tcd test &&\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     ++test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout feature7 &&\n     ++\t\tgit push origin &&\n     ++\t\tgit status --no-ahead-behind >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature7\n     ++\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n     ++\n     ++\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n      +test_expect_success 'checkout shows push branch up to date' '\n      +\t(\n      +\t\tcd test &&\n\n-- \ngitgitgadget\n"},{"id":"533017","messageId":"b62a9feb4ddefb34945a83fd7d76c81c871ea144.1767608269.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v17.git.git.1767608269.gitgitgadget@gmail.com","subject":"[PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-05T10:17:48Z","receivedAt":"2026-01-05T10:17:53Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..7163a8ec28 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,42 +2237,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     int sti,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     int show_divergence_advice)\n+{\n+\tif (!sti) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2281,7 +2260,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2292,7 +2271,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2305,12 +2284,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, sti;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (sti < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"533018","messageId":"1348542edced5d17c1220ee6bd30afd943acdbcf.1767608269.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v17.git.git.1767608269.gitgitgadget@gmail.com","subject":"[PATCH v17 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-05T10:17:49Z","receivedAt":"2026-01-05T10:17:54Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 |  97 ++++++++++++++-\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 354 insertions(+), 5 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 7163a8ec28..1f87b85b22 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,11 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum branch_mode_flags {\n+\tBRANCH_MODE_PULL = (1 << 0),\n+\tBRANCH_MODE_PUSH = (1 << 1),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2237,11 +2242,65 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (full_ref_out)\n+\t\t*full_ref_out = xstrdup(resolved);\n+\n+\tret = refs_shorten_unambiguous_ref(\n+\t\tget_main_ref_store(the_repository), resolved, 0);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     int sti,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     enum branch_mode_flags advice_flags,\n \t\t\t\t     int show_divergence_advice)\n {\n \tif (!sti) {\n@@ -2252,7 +2311,8 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif ((advice_flags & BRANCH_MODE_PUSH) &&\n+\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2261,7 +2321,8 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif ((advice_flags & BRANCH_MODE_PUSH) &&\n+\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2272,7 +2333,8 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif ((advice_flags & BRANCH_MODE_PULL) &&\n+\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2285,7 +2347,8 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n+\t\tif ((advice_flags & BRANCH_MODE_PULL) &&\n+\t\t    show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n@@ -2303,6 +2366,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tenum branch_mode_flags base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n+\tint push_ours, push_theirs, push_sti;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tenum branch_mode_flags push_branch_modes = 0;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2314,6 +2382,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_sti >= 0) {\n+\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n+\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2322,10 +2400,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf, show_divergence_advice);\n+\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf,\n+\t\t\t\t\t base_branch_modes, show_divergence_advice);\n+\t}\n+\n+\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, push_sti, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_branch_modes, 0);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..cf5a926dcd 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,266 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533034","messageId":"xmqqldickzva.fsf@gitster.g","threadId":"64668","inReplyTo":"20260105091517.86301-1-haraldnordgren@gmail.com","subject":"Re: Another look?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-05T12:28:25Z","receivedAt":"2026-01-05T12:28:27Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> > -     } else if (!sti) {\n>> > -             strbuf_addf(sb,\n>> > -                     _(\"Your branch is up to date with '%s'.\\n\"),\n>> > -                     base);\n>> > -     } else if (abf == AHEAD_BEHIND_QUICK) {\n>> > +     if (abf == AHEAD_BEHIND_QUICK) {\n>> >               strbuf_addf(sb,\n>> >                           _(\"Your branch and '%s' refer to different commits.\\n\"),\n>> > -                         base);\n>> > +                         branch_name);\n>> >               if (advice_enabled(ADVICE_STATUS_HINTS))\n>> >                       strbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n>> >                                   \"git status --ahead-behind\");\n>> > +     } else if (!ours && !theirs) {\n>> > +             strbuf_addf(sb,\n>> > +                     _(\"Your branch is up to date with '%s'.\\n\"),\n>> > +                     branch_name);\n>>\n>> We used to check if there is nothing to report (i.e., !sti is a\n>> signal from stat_tracking_info() that there are no differences\n>> between the branches) and reported that first, so when abf was\n>> checked, we knew that there are some differences.  Now, your patch\n>> reverses the order so whether there is or is not a change, abf\n>> codepath will always report \"you have differences!\".\n>\n> Agreed! This change was done to get rid of sti as a parameter. Maybe I\n> misunderstood your previous comment around the sti.\n>\n> I will bring back sti as a parameter to ’format_branch_comparison’ now,\n> I don’t see any other way to solve this.\n\nPlease don't.  Unless my assumption, which is that in the old code\n\"!sti\" and \"!ours && !theirs\" is equivalent, is wrong, all you need\nto do around that part is to first check \"if (!ours && !theirs)\" and\nsay \"your branch is up to date with...\", and then have the check\n\"else if (abf == ABQ)\" next.  That way, when we check abf we know\nthe branches are different.\n\n"},{"id":"533039","messageId":"20260105131654.88348-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqldickzva.fsf@gitster.g","subject":"Another look?","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-05T13:16:54Z","receivedAt":"2026-01-05T13:16:57Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Please don't.  Unless my assumption, which is that in the old code\n> \"!sti\" and \"!ours && !theirs\" is equivalent, is wrong, all you need\n> to do around that part is to first check \"if (!ours && !theirs)\" and\n> say \"your branch is up to date with...\", and then have the check\n> \"else if (abf == ABQ)\" next.  That way, when we check abf we know\n> the branches are different.\n\nIt seems to be an incorrect assumption. This code change breaks several\ntests including old ones:\n\n```\ndiff --git a/remote.c b/remote.c\nindex 1f87b85b22..8db4fcd7b5 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2303,7 +2303,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     enum branch_mode_flags advice_flags,\n \t\t\t\t     int show_divergence_advice)\n {\n-\tif (!sti) {\n+\tif (!ours && !theirs) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbranch_name);\n```\n\n\nHarald\n"},{"id":"533083","messageId":"CALnO6CCw-LiFa6kx7C6xRyZOTr_AkAzWQz4UZasT=kWX=-vSXQ@mail.gmail.com","threadId":"64668","inReplyTo":"20260101233839.17639-1-haraldnordgren@gmail.com","subject":"Re: Another look?","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-05T21:55:26Z","receivedAt":"2026-01-05T21:55:39Z","isPatch":false,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Thu, Jan 1, 2026 at 6:38 PM Harald Nordgren <haraldnordgren@gmail.com> wrote:\n>\n> > Again this seems to do a \"step 1 goes in a direction, step 2 fixes\n> > its mistake, step 3 changes course\" drunken-man's walk.\n> >\n> > The same advice to restructure them into a logical incremental\n> > progression that moves the codebase in one consistent direction to\n> > eventually reach the goal at the end applies.\n>\n> Isn't programming always bit of drunken-man's walk?\n>\n> I'm very hesitant to restructure my history before I am confident I will\n> not need any of the old work later -- I would hate to lose history if I\n> make a mistake.\n>\n> One option is to keep my code backed up on a separate branch locally, but\n\nGit will already do this, more or less! See \"git reflog\". No need to worry :)\n\nI don't fear-driven development to lead to optimal results ;)\n\n> As a side-note: In my day job we only allow \"squash and merge\" on our\n> GitHub. This gives devs the flexibility to treat their branches as a WIP\n> area before merging, but still gives a pristine git history after merge.\n> This feels to me like a good trade-offs. But again, happy to take\n> instructions on how to do better.\n\nI think others have covered this, but you can both \"branch is WIP\" and\n\"clean history\" by iterating within a PR. The GitHub UI does not make\nthis particularly nice [1], but my recipe is essentially\n\n1. Make changes\n2. Post range-diff [2] and force-push\n\n[1]: https://benknoble.github.io/blog/2025/03/17/more-range-diff/\n[2]: https://benknoble.github.io/blog/2024/10/04/copy-range-diff/\n\n-- \nD. Ben Knoble\n"},{"id":"533084","messageId":"xmqq5x9flndb.fsf@gitster.g","threadId":"64668","inReplyTo":"20260105131654.88348-1-haraldnordgren@gmail.com","subject":"Re: Another look?","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-05T22:13:04Z","receivedAt":"2026-01-05T22:13:07Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> Please don't.  Unless my assumption, which is that in the old code\n>> \"!sti\" and \"!ours && !theirs\" is equivalent, is wrong, all you need\n>> to do around that part is to first check \"if (!ours && !theirs)\" and\n>> say \"your branch is up to date with...\", and then have the check\n>> \"else if (abf == ABQ)\" next.  That way, when we check abf we know\n>> the branches are different.\n>\n> It seems to be an incorrect assumption. This code change breaks several\n> tests including old ones:\n\n\n\n>\n> ```\n> diff --git a/remote.c b/remote.c\n> index 1f87b85b22..8db4fcd7b5 100644\n> --- a/remote.c\n> +++ b/remote.c\n> @@ -2303,7 +2303,7 @@ static void format_branch_comparison(struct strbuf *sb,\n>  \t\t\t\t     enum branch_mode_flags advice_flags,\n>  \t\t\t\t     int show_divergence_advice)\n>  {\n> -\tif (!sti) {\n> +\tif (!ours && !theirs) {\n>  \t\tstrbuf_addf(sb,\n>  \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n>  \t\t\tbranch_name);\n> ```\n>\n>\n> Harald\n\nThat is unexpected.\n\nLooking at what stat_branch_pair() does, before returning 0, the\nfunction always clears *num_theirs and *num_ours, so there is\nsomething else going on.\n\nIf your caller is *not* initializing ours and theirs, and if it is\nnot detecting an error return from stat_tracking_info, and the test\ncode is trying to see when stat_branch_info() signals failure by\nreturning -1, then I can understand why the above change makes a\ndifference (i.e., your code above with or without sti -> ours/theirs\nchange is broken), but then, that should be handled at the caller of\nstat_tracking_info() by noticing its return value being negative, I\nwould have to say.\n"},{"id":"533093","messageId":"20260106000814.58893-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqq5x9flndb.fsf@gitster.g","subject":"ABQ","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-06T00:08:14Z","receivedAt":"2026-01-06T00:08:17Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Note that this change also makes tests fail on the 'master' branch, so not\nan issue introdued by my code:\n\n```\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..f84f2747b2 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2265,7 +2265,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+\t} else if (!ours && !theirs) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n \t\t\tbase);\n```\n\nIf you have the time, it might be interesting to run\n'./t6040-tracking-info.sh' and to see what happens for the different cases.\n\nIt's failing for the ABQ cases, maybe because of this:\n\n```\n\tif (abf == AHEAD_BEHIND_QUICK)\n\t\treturn 1;\n```\n\nI would argue this is getting outside of the scope of push branch\ncomparison, so maybe better to do this as a follow-up? What do you say?\n\n\nHarald\n"},{"id":"533278","messageId":"20260108101926.94715-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260106000814.58893-1-haraldnordgren@gmail.com","subject":"ABQ","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-08T10:19:26Z","receivedAt":"2026-01-08T10:19:30Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi again! Could this code be ready be the merged?\n\n\nHarald\n"},{"id":"533378","messageId":"daab4e8f-cd9e-40af-bdfd-c5884d15d852@gmail.com","threadId":"64668","inReplyTo":"b62a9feb4ddefb34945a83fd7d76c81c871ea144.1767608269.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-01-09T14:56:21Z","receivedAt":"2026-01-09T14:56:24Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Harald\n\nOn 05/01/2026 10:17, Harald Nordgren via GitGitGadget wrote:\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n> \n> +static void format_branch_comparison(struct strbuf *sb,\n> +\t\t\t\t     int sti,\n\nI wondered why we needed to pass sti as well as ours and theirs but it \nis because when we're using AHEAD_BEHIND_QUICK our and theirs are always \nzero and so we need to check sti to see if the branch is up to date. \nPerhaps we could make this a boolean called 'up_to_date' ?\n\n> +\t\t\t\t     int ours, int theirs,\n> +\t\t\t\t     const char *branch_name,\n> +\t\t\t\t     enum ahead_behind_flags abf,\n> +\t\t\t\t     int show_divergence_advice)\n\nThis could be 'bool' not 'int'\n\nEverything else looks fine - it is a faithful conversion from the \noriginal and it makes sense to check if the upstream is gone in the caller.\n\nThanks\n\nPhillip\n\n> +{\n> +\tif (!sti) {\n>   \t\tstrbuf_addf(sb,\n>   \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n> -\t\t\tbase);\n> +\t\t\tbranch_name);\n>   \t} else if (abf == AHEAD_BEHIND_QUICK) {\n>   \t\tstrbuf_addf(sb,\n>   \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n> -\t\t\t    base);\n> +\t\t\t    branch_name);\n>   \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n>   \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n>   \t\t\t\t    \"git status --ahead-behind\");\n> @@ -2281,7 +2260,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>   \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n>   \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n>   \t\t\t   ours),\n> -\t\t\tbase, ours);\n> +\t\t\tbranch_name, ours);\n>   \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n>   \t\t\tstrbuf_addstr(sb,\n>   \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n> @@ -2292,7 +2271,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>   \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n>   \t\t\t       \"and can be fast-forwarded.\\n\",\n>   \t\t\t   theirs),\n> -\t\t\tbase, theirs);\n> +\t\t\tbranch_name, theirs);\n>   \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n>   \t\t\tstrbuf_addstr(sb,\n>   \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n> @@ -2305,12 +2284,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>   \t\t\t       \"and have %d and %d different commits each, \"\n>   \t\t\t       \"respectively.\\n\",\n>   \t\t\t   ours + theirs),\n> -\t\t\tbase, ours, theirs);\n> +\t\t\tbranch_name, ours, theirs);\n>   \t\tif (show_divergence_advice &&\n>   \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n>   \t\t\tstrbuf_addstr(sb,\n>   \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n>   \t}\n> +}\n> +\n> +/*\n> + * Return true when there is anything to report, otherwise false.\n> + */\n> +int format_tracking_info(struct branch *branch, struct strbuf *sb,\n> +\t\t\t enum ahead_behind_flags abf,\n> +\t\t\t int show_divergence_advice)\n> +{\n> +\tint ours, theirs, sti;\n> +\tconst char *full_base;\n> +\tchar *base;\n> +\tint upstream_is_gone = 0;\n> +\n> +\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n> +\tif (sti < 0) {\n> +\t\tif (!full_base)\n> +\t\t\treturn 0;\n> +\t\tupstream_is_gone = 1;\n> +\t}\n> +\n> +\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n> +\t\t\t\t\t    full_base, 0);\n> +\n> +\tif (upstream_is_gone) {\n> +\t\tstrbuf_addf(sb,\n> +\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n> +\t\t\tbase);\n> +\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n> +\t\t\tstrbuf_addstr(sb,\n> +\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n> +\t} else {\n> +\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf, show_divergence_advice);\n> +\t}\n> +\n>   \tfree(base);\n>   \treturn 1;\n>   }\n\n"},{"id":"533379","messageId":"ba2b65a5-0e2c-41a5-a480-ec3f5ec2178a@gmail.com","threadId":"64668","inReplyTo":"1348542edced5d17c1220ee6bd30afd943acdbcf.1767608269.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v17 2/2] status: show comparison with push remote tracking branch","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-01-09T14:56:38Z","receivedAt":"2026-01-09T14:56:41Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Harald\n\nOn 05/01/2026 10:17, Harald Nordgren via GitGitGadget wrote:\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n> \n> \"git status\" on a branch that follows a remote branch compares\n> commits on the current branch and the remote-tracking branch it\n> builds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n> \n> When working on a feature branch that tracks a remote feature branch,\n> but you also want to track progress relative to the push destination\n> tracking branch (which may differ from the upstream branch), git status\n> now shows an additional comparison.\n> \n> When the upstream tracking branch differs from the push destination\n> tracking branch, git status shows both the comparison with the upstream\n> tracking branch (as before) and an additional comparison with the push\n> destination tracking branch. The push branch comparison appears on a\n> separate line after the upstream branch status, using the same format.\n> \n> Example output when tracking origin/main but push destination is\n> origin/feature:\n>      On branch feature\n>      Your branch and 'origin/main' have diverged,\n>      and have 3 and 1 different commits each, respectively.\n>        (use \"git pull\" if you want to integrate the remote branch with yours)\n> \n>      Your branch is ahead of 'origin/feature' by 1 commit.\n>        (use \"git push\" to publish your local commits)\n\nThe advice looks good\n\n> The comparison is only shown when the push destination tracking branch\n> differs from the upstream tracking branch, even if they are on the same\n> remote.\n\nSounds sensible\n\n> diff --git a/remote.c b/remote.c\n> index 7163a8ec28..1f87b85b22 100644\n> --- a/remote.c\n> +++ b/remote.c\n> @@ -29,6 +29,11 @@\n>   \n>   enum map_direction { FROM_SRC, FROM_DST };\n>   \n> +enum branch_mode_flags {\n> +\tBRANCH_MODE_PULL = (1 << 0),\n> +\tBRANCH_MODE_PUSH = (1 << 1),\n> +};\n\nUsing an enum for a set of flags is a bit confusing.\n\n> +static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n> +{\n> +\tstruct remote *remote;\n> +\tconst char *push_remote;\n> +\tchar *push_dst = NULL;\n> +\tchar *tracking_ref;\n> +\tconst char *resolved;\n> +\tchar *ret;\n> +\n> +\tif (!branch)\n> +\t\treturn NULL;\n> +\n> +\tpush_remote = pushremote_for_branch(branch, NULL);\n> +\tif (!push_remote)\n> +\t\treturn NULL;\n> +\n> +\tremote = remotes_remote_get(the_repository, push_remote);\n> +\tif (!remote)\n> +\t\treturn NULL;\n> +\n> +\tpush_dst = remote_ref_for_branch(branch, 1);\n> +\tif (!push_dst) {\n> +\t\tif (remote->push.nr)\n> +\t\t\treturn NULL;\n> +\t\tpush_dst = xstrdup(branch->refname);\n> +\t}\n> +\n> +\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n> +\tfree(push_dst);\n\nOn reflection I wonder if we should be calling branch_get_push() instead \nof remote_ref_for_branch() and tracking_for_push_dest() as it respects \n'push.default' and so the branch it returns is the one that \"git push\" \nwithout any arguments would push to.\n\n> +\tif (!tracking_ref)\n> +\t\treturn NULL;\n> +\n> +\tresolved = refs_resolve_ref_unsafe(\n> +\t\tget_main_ref_store(the_repository),\n> +\t\ttracking_ref,\n> +\t\tRESOLVE_REF_READING,\n> +\t\tNULL, NULL);\n> +\n> +\tif (!resolved) {\n> +\t\tfree(tracking_ref);\n> +\t\treturn NULL;\n> +\t}\n> +\n> +\tif (full_ref_out)\n\nI think it would be simpler to just return the full refname and let the \ncaller shorten it.\n\n> +\t\t*full_ref_out = xstrdup(resolved);\n> +\n> +\tret = refs_shorten_unambiguous_ref(\n> +\t\tget_main_ref_store(the_repository), resolved, 0);\n> +\tfree(tracking_ref);\n> +\treturn ret;\n> +}\n> +\n>   static void format_branch_comparison(struct strbuf *sb,\n>   \t\t\t\t     int sti,\n>   \t\t\t\t     int ours, int theirs,\n>   \t\t\t\t     const char *branch_name,\n>   \t\t\t\t     enum ahead_behind_flags abf,\n> +\t\t\t\t     enum branch_mode_flags advice_flags,\n>   \t\t\t\t     int show_divergence_advice)\n>   {\n>   \tif (!sti) {\n> @@ -2252,7 +2311,8 @@ static void format_branch_comparison(struct strbuf *sb,\n>   \t\tstrbuf_addf(sb,\n>   \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n>   \t\t\t    branch_name);\n> -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n> +\t\tif ((advice_flags & BRANCH_MODE_PUSH) &&\n\nWhy are we checking for BRANCH_MODE_PUSH here? Don't we want to show \nthis advice regardless of the mode?\n\n\n > +\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n>   \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n>   \t\t\t\t    \"git status --ahead-behind\");\n>   \t} else if (!theirs) {\n> @@ -2261,7 +2321,8 @@ static void format_branch_comparison(struct strbuf *sb,\n>   \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n>   \t\t\t   ours),\n>   \t\t\tbranch_name, ours);\n> -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n> +\t\tif ((advice_flags & BRANCH_MODE_PUSH) &&\n> +\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n\nHaving to test the flags each time is a bit cumbersome. We could define\na couple of local variables to simplify this\n\n\tbool want_push_advice = (advice_flags & BRANCH_MODE_PUSH) &&\n\t\t\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n\tbool want_pull_advice = advice_flags & BRANCH_MODE_PULL &&\n\t\t\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n\nThen we can simplify the above to\n\n\tif (want_push_advice)\n\n>   \t\t\tstrbuf_addstr(sb,\n>   \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n>   \t} else if (!ours) {\n > [...]\n> @@ -2285,7 +2347,8 @@ static void format_branch_comparison(struct strbuf *sb,\n>   \t\t\t       \"respectively.\\n\",\n>   \t\t\t   ours + theirs),\n>   \t\t\tbranch_name, ours, theirs);\n> -\t\tif (show_divergence_advice &&\n> +\t\tif ((advice_flags & BRANCH_MODE_PULL) &&\n> +\t\t    show_divergence_advice &&\n\nIf we don't want to show this can't we set show_divergance_adivce to \nfalse when we call this function - why is it guarded by BRANCH_MODE_PULL \nas well?\n\n>   \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n>   \t\t\tstrbuf_addstr(sb,\n>   \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n> @@ -2303,6 +2366,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>   \tconst char *full_base;\n>   \tchar *base;\n>   \tint upstream_is_gone = 0;\n> +\tenum branch_mode_flags base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n\nHere we set an enum to a value that is not a member of the enum.\n\n> +\tint push_ours, push_theirs, push_sti;\n> +\tchar *full_push = NULL;\n> +\tchar *push = NULL;\n> +\tenum branch_mode_flags push_branch_modes = 0;\n>   \n>   \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n>   \tif (sti < 0) {\n> @@ -2314,6 +2382,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>   \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n>   \t\t\t\t\t    full_base, 0);\n>   \n> +\tpush = get_remote_push_branch(branch, &full_push);\n> +\tif (push && strcmp(base, push)) {\n\nThis is good - we only show the push branch separately if it differs \nfrom the upstream branch.\n\n> +\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n> +\t\t\t\t\t   &push_ours, &push_theirs, abf);\n> +\t\tif (push_sti >= 0) {\n> +\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n> +\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n> +\t\t}\n\nThis combined with checking \"push_branch_modes & BRANCH_MODE_PUSH\" below \nensures we skip the push branch if push_sti < 0. That's good but it is a \nbit hard to follow.\n\nThanks\n\nPhillip\n\n> +\t}\n> +\n>   \tif (upstream_is_gone) {\n>   \t\tstrbuf_addf(sb,\n>   \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n> @@ -2322,10 +2400,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>   \t\t\tstrbuf_addstr(sb,\n>   \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n>   \t} else {\n> -\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf, show_divergence_advice);\n> +\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf,\n> +\t\t\t\t\t base_branch_modes, show_divergence_advice);\n> +\t}\n> +\n> +\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n> +\t\tstrbuf_addstr(sb, \"\\n\");\n> +\t\tformat_branch_comparison(sb, push_sti, push_ours, push_theirs, push, abf,\n> +\t\t\t\t\t push_branch_modes, 0);\n>   \t}\n>   \n>   \tfree(base);\n> +\tfree(full_push);\n> +\tfree(push);\n>   \treturn 1;\n>   }\n>   \n> diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\n> index 0b719bbae6..cf5a926dcd 100755\n> --- a/t/t6040-tracking-info.sh\n> +++ b/t/t6040-tracking-info.sh\n> @@ -292,4 +292,266 @@ test_expect_success '--set-upstream-to @{-1}' '\n>   \ttest_cmp expect actual\n>   '\n>   \n> +test_expect_success 'status tracking origin/main shows only main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout b4 &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch b4\n> +\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n> +\t  (use \"git push\" to publish your local commits)\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout b4 &&\n> +\t\tgit status --no-ahead-behind >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch b4\n> +\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n> +\t  (use \"git status --ahead-behind\" for details)\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status shows ahead of both origin/main and feature branch' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature2 origin/main &&\n> +\t\tgit push origin HEAD &&\n> +\t\tadvance work &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature2\n> +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n> +\n> +\tYour branch is ahead of ${SQ}origin/feature2${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> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout feature2 >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n> +\n> +\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n> +\t  (use \"git push\" to publish your local commits)\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'setup for ahead of tracked but diverged from main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature4 origin/main &&\n> +\t\tadvance work1 &&\n> +\t\tgit checkout origin/main &&\n> +\t\tadvance work2 &&\n> +\t\tgit push origin HEAD:main &&\n> +\t\tgit checkout feature4 &&\n> +\t\tadvance work3\n> +\t)\n> +'\n> +\n> +test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout feature4 &&\n> +\t\tgit branch --set-upstream-to origin/main &&\n> +\t\tgit push origin HEAD &&\n> +\t\tadvance work &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature4\n> +\tYour branch and ${SQ}origin/main${SQ} have diverged,\n> +\tand have 3 and 1 different commits each, respectively.\n> +\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n> +\n> +\tYour branch is ahead of ${SQ}origin/feature4${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> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout feature4 &&\n> +\t\tgit status --no-ahead-behind >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature4\n> +\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n> +\n> +\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n> +\t  (use \"git status --ahead-behind\" for details)\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'setup upstream remote' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit remote add upstream ../. &&\n> +\t\tgit fetch upstream &&\n> +\t\tgit config remote.pushDefault origin\n> +\t)\n> +'\n> +\n> +test_expect_success 'status with upstream remote and push.default set to origin' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature5 upstream/main &&\n> +\t\tgit push origin &&\n> +\t\tadvance work &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature5\n> +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n> +\n> +\tYour branch is ahead of ${SQ}origin/feature5${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> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature6 upstream/main &&\n> +\t\tadvance work &&\n> +\t\tgit push origin &&\n> +\t\tgit reset --hard upstream/main &&\n> +\t\tadvance work &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature6\n> +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n> +\n> +\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n> +\tand have 1 and 1 different commits each, respectively.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status with upstream remote and push branch up to date' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature7 upstream/main &&\n> +\t\tgit push origin &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature7\n> +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n> +\n> +\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout feature7 &&\n> +\t\tgit push origin &&\n> +\t\tgit status --no-ahead-behind >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature7\n> +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n> +\n> +\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'checkout shows push branch up to date' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout feature7 >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n> +\n> +\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status shows remapped push refspec' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature8 origin/main &&\n> +\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n> +\t\tgit push &&\n> +\t\tadvance work &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature8\n> +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n> +\n> +\tYour branch is ahead of ${SQ}origin/remapped${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> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status shows remapped push refspec with upstream remote' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature9 upstream/main &&\n> +\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n> +\t\tgit push origin &&\n> +\t\tadvance work &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature9\n> +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n> +\n> +\tYour branch is ahead of ${SQ}origin/remapped${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> +\ttest_cmp expect actual\n> +'\n> +\n>   test_done\n\n"},{"id":"533380","messageId":"20260109152339.70643-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"daab4e8f-cd9e-40af-bdfd-c5884d15d852@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-09T15:23:39Z","receivedAt":"2026-01-09T15:23:42Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> I wondered why we needed to pass sti as well as ours and theirs but it \n> is because when we're using AHEAD_BEHIND_QUICK our and theirs are always \n> zero and so we need to check sti to see if the branch is up to date. \n> Perhaps we could make this a boolean called 'up_to_date' ?\n\nYes, this is the reason. And I tried a lot of things to get around it. But\nyes, let's pass this boolean instead.\n\n> This could be 'bool' not 'int'\n> \n> Everything else looks fine - it is a faithful conversion from the \n> original and it makes sense to check if the upstream is gone in the caller.\n\nDone, will be in the next patch.\n\nI didn't update the signature of 'format_tracking_info' to have a bool\nthere too, because it makes the surface area bigger of also updating the .h\nfile there.\n\n\nHarald\n"},{"id":"533384","messageId":"20260109160037.2067-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"ba2b65a5-0e2c-41a5-a480-ec3f5ec2178a@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-09T16:00:37Z","receivedAt":"2026-01-09T16:00:42Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Using an enum for a set of flags is a bit confusing.\n\nThe point of the flag and the bitmasking is to selectively turn off the push\nand pull advice advice from the relevant branch when the push branch\ncomparison is active.\n\nIn an earlier implementation the advice logic was moved to the caller\ninstead 'format_branch_comparison', but it's more faithful to the original\nto have the advice logic inside 'format_branch_comparison'. Maybe I\nmisunderstood your comments around this?\n\nWould happily take a suggestion on a nicer way to handle it.\n\n> On reflection I wonder if we should be calling branch_get_push() instead \n> of remote_ref_for_branch() and tracking_for_push_dest() as it respects \n> 'push.default' and so the branch it returns is the one that \"git push\" \n> without any arguments would push to.\n\nI'm not getting that to work without tests breaking.\n\n> I think it would be simpler to just return the full refname and let the \n> caller shorten it.\n\nWe could to that but what's the benefit? Shouldn't a helper function reduce\nthe work for the caller, not the other way around? 🤗\n\n> Why are we checking for BRANCH_MODE_PUSH here? Don't we want to show \n> this advice regardless of the mode?\n\nYeah, that's a good point. However, this will often lead to the advice\nbeing shown twice, see this test:\n\n```\nstatus --no-ahead-behind shows diverged from origin/main and ahead of feature branch\n```\n\n> Having to test the flags each time is a bit cumbersome. We could define\n> a couple of local variables to simplify this\n> \n> \tbool want_push_advice = (advice_flags & BRANCH_MODE_PUSH) &&\n> \t\t\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n> \tbool want_pull_advice = advice_flags & BRANCH_MODE_PULL &&\n> \t\t\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n> \n> Then we can simplify the above to\n> \n> \tif (want_push_advice)\n\nDone, will be in the next batch!\n\n> If we don't want to show this can't we set show_divergance_adivce to \n> false when we call this function - why is it guarded by BRANCH_MODE_PULL \n> as well?\n\nThis will already have been shown the the pull branch (the upstream\nbranch).\n\nAnd in the cases when it's NOT shown for the pull branch but only the push\nbranch has diverged, then pulling won't solve the problem anyway. (Unless\nyou would try to pull from your push branch.)\n\n> Here we set an enum to a value that is not a member of the enum.\n\nI use bitmasking to enable both modes by default. But see my comments above\nmaybe there is a nicer way to handle all of this.\n\n> This combined with checking \"push_branch_modes & BRANCH_MODE_PUSH\" below \n> ensures we skip the push branch if push_sti < 0. That's good but it is a \n> bit hard to follow.\n\nPossibly this can be done in a nicer way too. But at least I try to\nencapsulate the complexity to only here, to allow the rest of the code to\nbe more straightforward. A good trade-off I think.\n\n\nHarald\n"},{"id":"533387","messageId":"848006C9-FD1B-4E73-935F-D4A338AF2EC9@gmail.com","threadId":"64668","inReplyTo":"20260109160037.2067-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-09T16:22:33Z","receivedAt":"2026-01-09T16:22:44Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 9 janv. 2026 à 11:07, Harald Nordgren <haraldnordgren@gmail.com> a écrit :\n> \n> ﻿\n>> \n>> Using an enum for a set of flags is a bit confusing.\n> \n> The point of the flag and the bitmasking is to selectively turn off the push\n> and pull advice advice from the relevant branch when the push branch\n> comparison is active.\n> \n> In an earlier implementation the advice logic was moved to the caller\n> instead 'format_branch_comparison', but it's more faithful to the original\n> to have the advice logic inside 'format_branch_comparison'. Maybe I\n> misunderstood your comments around this?\n> \n> Would happily take a suggestion on a nicer way to handle it.\n\nI think other uses in Git declare a bunch of integer constants for the bitfields, not an enum. Why? Because or-ing flags together creates a value not in the enum…\n\n>> Here we set an enum to a value that is not a member of the enum.\n> \n> I use bitmasking to enable both modes by default. But see my comments above\n> maybe there is a nicer way to handle all of this.\n\n…which explains this comment.\n\nIOW: set of flags makes sense, but there’s a more usual way to set that up?"},{"id":"533389","messageId":"aWEtp77UB30SbKle@pks.im","threadId":"64668","inReplyTo":"848006C9-FD1B-4E73-935F-D4A338AF2EC9@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T16:32:39Z","receivedAt":"2026-01-09T16:32:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 09, 2026 at 11:22:33AM -0500, Ben Knoble wrote:\n> \n> > Le 9 janv. 2026 à 11:07, Harald Nordgren <haraldnordgren@gmail.com> a écrit :\n> > \n> > ﻿\n> >> \n> >> Using an enum for a set of flags is a bit confusing.\n> > \n> > The point of the flag and the bitmasking is to selectively turn off the push\n> > and pull advice advice from the relevant branch when the push branch\n> > comparison is active.\n> > \n> > In an earlier implementation the advice logic was moved to the caller\n> > instead 'format_branch_comparison', but it's more faithful to the original\n> > to have the advice logic inside 'format_branch_comparison'. Maybe I\n> > misunderstood your comments around this?\n> > \n> > Would happily take a suggestion on a nicer way to handle it.\n> \n> I think other uses in Git declare a bunch of integer constants for the\n> bitfields, not an enum. Why? Because or-ing flags together creates a\n> value not in the enum…\n\nWe nowadays typically declare the flags as enum, but when accepting the\nbitfield we use `unsigned`.\n\nPatrick\n"},{"id":"533391","messageId":"pull.2138.v18.git.git.1767976906.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v17.git.git.1767608269.gitgitgadget@gmail.com","subject":"[PATCH v18 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T16:41:44Z","receivedAt":"2026-01-09T16:41:49Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 174 ++++++++++++++++++++------\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 401 insertions(+), 35 deletions(-)\n\n\nbase-commit: d529f3a197364881746f558e5652f0236131eb86\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v18\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v18\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v17:\n\n 1:  b62a9feb4d ! 1:  451d7a4986 refactor format_branch_comparison in preparation\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      -\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n      -\t} else if (!sti) {\n      +static void format_branch_comparison(struct strbuf *sb,\n     -+\t\t\t\t     int sti,\n     ++\t\t\t\t     bool up_to_date,\n      +\t\t\t\t     int ours, int theirs,\n      +\t\t\t\t     const char *branch_name,\n      +\t\t\t\t     enum ahead_behind_flags abf,\n     -+\t\t\t\t     int show_divergence_advice)\n     ++\t\t\t\t     bool show_divergence_advice)\n      +{\n     -+\tif (!sti) {\n     ++\tif (up_to_date) {\n       \t\tstrbuf_addf(sb,\n       \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n      -\t\t\tbase);\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      +\t\t\tstrbuf_addstr(sb,\n      +\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n      +\t} else {\n     -+\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf, show_divergence_advice);\n     ++\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n      +\t}\n      +\n       \tfree(base);\n 2:  1348542edc ! 2:  02476eb8e6 status: show comparison with push remote tracking branch\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +}\n      +\n       static void format_branch_comparison(struct strbuf *sb,\n     - \t\t\t\t     int sti,\n     + \t\t\t\t     bool up_to_date,\n       \t\t\t\t     int ours, int theirs,\n       \t\t\t\t     const char *branch_name,\n       \t\t\t\t     enum ahead_behind_flags abf,\n      +\t\t\t\t     enum branch_mode_flags advice_flags,\n     - \t\t\t\t     int show_divergence_advice)\n     + \t\t\t\t     bool show_divergence_advice)\n       {\n     - \tif (!sti) {\n     ++\tbool want_push_advice = (advice_flags & BRANCH_MODE_PUSH) &&\n     ++\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     ++\tbool want_pull_advice = (advice_flags & BRANCH_MODE_PULL) &&\n     ++\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     ++\tbool want_divergence_advice = (advice_flags & BRANCH_MODE_PULL) &&\n     ++\t\tshow_divergence_advice &&\n     ++\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     ++\n     + \tif (up_to_date) {\n     + \t\tstrbuf_addf(sb,\n     + \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n      @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\tstrbuf_addf(sb,\n       \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n       \t\t\t    branch_name);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif ((advice_flags & BRANCH_MODE_PUSH) &&\n     -+\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     ++\t\tif (want_push_advice)\n       \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n       \t\t\t\t    \"git status --ahead-behind\");\n       \t} else if (!theirs) {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   ours),\n       \t\t\tbranch_name, ours);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif ((advice_flags & BRANCH_MODE_PUSH) &&\n     -+\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     ++\t\tif (want_push_advice)\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n       \t} else if (!ours) {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   theirs),\n       \t\t\tbranch_name, theirs);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif ((advice_flags & BRANCH_MODE_PULL) &&\n     -+\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     ++\t\tif (want_pull_advice)\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n       \t} else {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   ours + theirs),\n       \t\t\tbranch_name, ours, theirs);\n      -\t\tif (show_divergence_advice &&\n     -+\t\tif ((advice_flags & BRANCH_MODE_PULL) &&\n     -+\t\t    show_divergence_advice &&\n     - \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     +-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     ++\t\tif (want_divergence_advice)\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n     + \t}\n      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tconst char *full_base;\n       \tchar *base;\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n       \t} else {\n     --\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf, show_divergence_advice);\n     -+\t\tformat_branch_comparison(sb, sti, ours, theirs, base, abf,\n     +-\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n     ++\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf,\n      +\t\t\t\t\t base_branch_modes, show_divergence_advice);\n      +\t}\n      +\n      +\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n      +\t\tstrbuf_addstr(sb, \"\\n\");\n     -+\t\tformat_branch_comparison(sb, push_sti, push_ours, push_theirs, push, abf,\n     -+\t\t\t\t\t push_branch_modes, 0);\n     ++\t\tformat_branch_comparison(sb, !push_sti, push_ours, push_theirs, push, abf,\n     ++\t\t\t\t\t push_branch_modes, show_divergence_advice);\n       \t}\n       \n       \tfree(base);\n\n-- \ngitgitgadget\n"},{"id":"533392","messageId":"451d7a498617fbcbeb08ee644cb621cbf6af0bd5.1767976906.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v18.git.git.1767976906.gitgitgadget@gmail.com","subject":"[PATCH v18 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T16:41:45Z","receivedAt":"2026-01-09T16:41:50Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..eba013b6b4 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,42 +2237,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2281,7 +2260,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2292,7 +2271,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2305,12 +2284,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, sti;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (sti < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"533393","messageId":"02476eb8e628b9e6572759a6c7cf5a60e42f3d68.1767976906.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v18.git.git.1767976906.gitgitgadget@gmail.com","subject":"[PATCH v18 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T16:41:46Z","receivedAt":"2026-01-09T16:41:51Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 102 ++++++++++++++-\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 358 insertions(+), 6 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex eba013b6b4..4c3f18c3c1 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,11 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum branch_mode_flags {\n+\tBRANCH_MODE_PULL = (1 << 0),\n+\tBRANCH_MODE_PUSH = (1 << 1),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2237,13 +2242,75 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (full_ref_out)\n+\t\t*full_ref_out = xstrdup(resolved);\n+\n+\tret = refs_shorten_unambiguous_ref(\n+\t\tget_main_ref_store(the_repository), resolved, 0);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     enum branch_mode_flags advice_flags,\n \t\t\t\t     bool show_divergence_advice)\n {\n+\tbool want_push_advice = (advice_flags & BRANCH_MODE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_pull_advice = (advice_flags & BRANCH_MODE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_divergence_advice = (advice_flags & BRANCH_MODE_PULL) &&\n+\t\tshow_divergence_advice &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2252,7 +2319,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2261,7 +2328,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2272,7 +2339,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2285,8 +2352,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2303,6 +2369,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tenum branch_mode_flags base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n+\tint push_ours, push_theirs, push_sti;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tenum branch_mode_flags push_branch_modes = 0;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2314,6 +2385,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_sti >= 0) {\n+\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n+\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2322,10 +2403,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n+\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf,\n+\t\t\t\t\t base_branch_modes, show_divergence_advice);\n+\t}\n+\n+\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, !push_sti, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_branch_modes, show_divergence_advice);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..cf5a926dcd 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,266 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533398","messageId":"20260109180309.14669-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"aWEtp77UB30SbKle@pks.im","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-09T18:03:09Z","receivedAt":"2026-01-09T18:03:12Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> > I think other uses in Git declare a bunch of integer constants for the\n> > bitfields, not an enum. Why? Because or-ing flags together creates a\n> > value not in the enum…\n> \n> We nowadays typically declare the flags as enum, but when accepting the\n> bitfield we use `unsigned`.\n\nGood points! I have updated it to remove the name branch_mode_flags now,\ndefine the enums and pass them around as unsigned now. Will be in the next\npatch.\n\nHarald\n"},{"id":"533401","messageId":"pull.2138.v19.git.git.1767984037.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v18.git.git.1767976906.gitgitgadget@gmail.com","subject":"[PATCH v19 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T18:40:35Z","receivedAt":"2026-01-09T18:40:39Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 174 ++++++++++++++++++++------\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 401 insertions(+), 35 deletions(-)\n\n\nbase-commit: d529f3a197364881746f558e5652f0236131eb86\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v19\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v19\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v18:\n\n 1:  451d7a4986 = 1:  451d7a4986 refactor format_branch_comparison in preparation\n 2:  02476eb8e6 ! 2:  dc8ab23158 status: show comparison with push remote tracking branch\n     @@ remote.c\n       \n       enum map_direction { FROM_SRC, FROM_DST };\n       \n     -+enum branch_mode_flags {\n     ++enum {\n      +\tBRANCH_MODE_PULL = (1 << 0),\n      +\tBRANCH_MODE_PUSH = (1 << 1),\n      +};\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \t\t\t\t     int ours, int theirs,\n       \t\t\t\t     const char *branch_name,\n       \t\t\t\t     enum ahead_behind_flags abf,\n     -+\t\t\t\t     enum branch_mode_flags advice_flags,\n     ++\t\t\t\t     unsigned flags,\n       \t\t\t\t     bool show_divergence_advice)\n       {\n     -+\tbool want_push_advice = (advice_flags & BRANCH_MODE_PUSH) &&\n     ++\tbool want_push_advice = (flags & BRANCH_MODE_PUSH) &&\n      +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     -+\tbool want_pull_advice = (advice_flags & BRANCH_MODE_PULL) &&\n     ++\tbool want_pull_advice = (flags & BRANCH_MODE_PULL) &&\n      +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     -+\tbool want_divergence_advice = (advice_flags & BRANCH_MODE_PULL) &&\n     ++\tbool want_divergence_advice = (flags & BRANCH_MODE_PULL) &&\n      +\t\tshow_divergence_advice &&\n      +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n      +\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tconst char *full_base;\n       \tchar *base;\n       \tint upstream_is_gone = 0;\n     -+\tenum branch_mode_flags base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n     ++\tunsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n      +\tint push_ours, push_theirs, push_sti;\n      +\tchar *full_push = NULL;\n      +\tchar *push = NULL;\n     -+\tenum branch_mode_flags push_branch_modes = 0;\n     ++\tunsigned push_branch_modes = 0;\n       \n       \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n       \tif (sti < 0) {\n\n-- \ngitgitgadget\n"},{"id":"533402","messageId":"451d7a498617fbcbeb08ee644cb621cbf6af0bd5.1767984037.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v19.git.git.1767984037.gitgitgadget@gmail.com","subject":"[PATCH v19 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T18:40:36Z","receivedAt":"2026-01-09T18:40:40Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..eba013b6b4 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,42 +2237,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2281,7 +2260,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2292,7 +2271,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2305,12 +2284,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, sti;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (sti < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"533403","messageId":"dc8ab23158e5b43cf650f71ef5c2b3a094f54129.1767984037.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v19.git.git.1767984037.gitgitgadget@gmail.com","subject":"[PATCH v19 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T18:40:37Z","receivedAt":"2026-01-09T18:40:43Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 102 ++++++++++++++-\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 358 insertions(+), 6 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex eba013b6b4..b8d2d1360a 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,11 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tBRANCH_MODE_PULL = (1 << 0),\n+\tBRANCH_MODE_PUSH = (1 << 1),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2237,13 +2242,75 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (full_ref_out)\n+\t\t*full_ref_out = xstrdup(resolved);\n+\n+\tret = refs_shorten_unambiguous_ref(\n+\t\tget_main_ref_store(the_repository), resolved, 0);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     unsigned flags,\n \t\t\t\t     bool show_divergence_advice)\n {\n+\tbool want_push_advice = (flags & BRANCH_MODE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_pull_advice = (flags & BRANCH_MODE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_divergence_advice = (flags & BRANCH_MODE_PULL) &&\n+\t\tshow_divergence_advice &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2252,7 +2319,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2261,7 +2328,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2272,7 +2339,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2285,8 +2352,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2303,6 +2369,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tunsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n+\tint push_ours, push_theirs, push_sti;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tunsigned push_branch_modes = 0;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2314,6 +2385,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_sti >= 0) {\n+\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n+\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2322,10 +2403,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n+\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf,\n+\t\t\t\t\t base_branch_modes, show_divergence_advice);\n+\t}\n+\n+\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, !push_sti, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_branch_modes, show_divergence_advice);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..cf5a926dcd 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,266 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533465","messageId":"xmqqjyxqtebq.fsf@gitster.g","threadId":"64668","inReplyTo":"451d7a498617fbcbeb08ee644cb621cbf6af0bd5.1767984037.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v19 1/2] refactor format_branch_comparison in preparation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-10T02:02:33Z","receivedAt":"2026-01-10T02:02:35Z","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> +static void format_branch_comparison(struct strbuf *sb,\n> +\t\t\t\t     bool up_to_date,\n> +\t\t\t\t     int ours, int theirs,\n> +\t\t\t\t     const char *branch_name,\n> +\t\t\t\t     enum ahead_behind_flags abf,\n> +\t\t\t\t     bool show_divergence_advice)\n> +{\n> +\tif (up_to_date) {\n>  \t\tstrbuf_addf(sb,\n>  \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n> -\t\t\tbase);\n> +\t\t\tbranch_name);\n>  \t} else if (abf == AHEAD_BEHIND_QUICK) {\n>  \t\tstrbuf_addf(sb,\n>  \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n> -\t\t\t    base);\n> +\t\t\t    branch_name);\n>  \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n>  \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n>  \t\t\t\t    \"git status --ahead-behind\");\n\nOK.  We _could_, just like we moved the \"upstream_gone\" case to the\ncaller, move the \"up to date\" case to the caller as well, but this\nreads well now.  And we do not need the (!ours && !theirs) case in\nthis if/else if/... cascade.  Very good.\n\n> +/*\n> + * Return true when there is anything to report, otherwise false.\n> + */\n> +int format_tracking_info(struct branch *branch, struct strbuf *sb,\n> +\t\t\t enum ahead_behind_flags abf,\n> +\t\t\t int show_divergence_advice)\n> +{\n> +\tint ours, theirs, sti;\n> +\tconst char *full_base;\n> +\tchar *base;\n> +\tint upstream_is_gone = 0;\n> +\n> +\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n> +\tif (sti < 0) {\n> +\t\tif (!full_base)\n> +\t\t\treturn 0;\n> +\t\tupstream_is_gone = 1;\n> +\t}\n> +\n> +\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n> +\t\t\t\t\t    full_base, 0);\n> +\n> +\tif (upstream_is_gone) {\n> +\t\tstrbuf_addf(sb,\n> +\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n> +\t\t\tbase);\n> +\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n> +\t\t\tstrbuf_addstr(sb,\n> +\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n> +\t} else {\n> +\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n> +\t}\n> +\n>  \tfree(base);\n>  \treturn 1;\n>  }\n"},{"id":"533466","messageId":"xmqqbjj2tdu0.fsf@gitster.g","threadId":"64668","inReplyTo":"dc8ab23158e5b43cf650f71ef5c2b3a094f54129.1767984037.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v19 2/2] status: show comparison with push remote tracking branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-10T02:13:11Z","receivedAt":"2026-01-10T02:13:13Z","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> @@ -2285,8 +2352,7 @@ static void format_branch_comparison(struct strbuf *sb,\n>  \t\t\t       \"respectively.\\n\",\n>  \t\t\t   ours + theirs),\n>  \t\t\tbranch_name, ours, theirs);\n> -\t\tif (show_divergence_advice &&\n> -\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n> +\t\tif (want_divergence_advice)\n>  \t\t\tstrbuf_addstr(sb,\n>  \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n>  \t}\n\nThis is not a new issue introduced by this series, but it is curious\nthere is \"ours + theirs\" there #leftoverbits.\n\nIt is part of ngetext() aka Q_() call, used this way:\n\n\t} else {\n\t\tstrbuf_addf(sb,\n\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n\t\t\t       \"and have %d and %d different commit each, \"\n\t\t\t       \"respectively.\\n\",\n\t\t\t   \"Your branch and '%s' have diverged,\\n\"\n\t\t\t       \"and have %d and %d different commits each, \"\n\t\t\t       \"respectively.\\n\",\n\t\t\t   ours + theirs),\n\t\t\tbranch_name, ours, theirs);\n\nBut in this if/else if/... cascade, we have ruled out cases where\neither/both of ours and theirs is 0 already, so ours + theirs has to\nbe at least two (because each has to be at least one).  Q_() based\non a value that is always plural would always use the latter form\n(i.e., \"commits\").\n\n"},{"id":"533467","messageId":"xmqq7btqtdgy.fsf@gitster.g","threadId":"64668","inReplyTo":"dc8ab23158e5b43cf650f71ef5c2b3a094f54129.1767984037.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v19 2/2] status: show comparison with push remote tracking branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-10T02:21:01Z","receivedAt":"2026-01-10T02:21:03Z","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> @@ -2303,6 +2369,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>  \tconst char *full_base;\n>  \tchar *base;\n>  \tint upstream_is_gone = 0;\n> +\tunsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n> +\tint push_ours, push_theirs, push_sti;\n> +\tchar *full_push = NULL;\n> +\tchar *push = NULL;\n> +\tunsigned push_branch_modes = 0;\n>  \n>  \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n>  \tif (sti < 0) {\n> @@ -2314,6 +2385,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>  \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n>  \t\t\t\t\t    full_base, 0);\n>  \n> +\tpush = get_remote_push_branch(branch, &full_push);\n> +\tif (push && strcmp(base, push)) {\n> +\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n> +\t\t\t\t\t   &push_ours, &push_theirs, abf);\n\nWhy is this variable called \"push_sti\"?  Calling the return value of\nstat_tracking_info() \"sti\" was klumsy but understandable.  It would\nhave been much easier to follow the code if the variable were named\nafter what it _means_ in this particular caller's code flow, like\n\"cmp_fetch\" (comparison on the fetching side, by convention negative\nsignals an error, and zero signals 'the same').  Perhaps rename \"sti\"\nand \"push_sti\" at the same time to make them more symmetric?\n\nOther than this minor nit, this step looks nicely done (and the\nprevious one is also good).\n\nThanks.\n"},{"id":"533476","messageId":"20260110110629.18501-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqq7btqtdgy.fsf@gitster.g","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-10T11:06:29Z","receivedAt":"2026-01-10T11:06:33Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"It's following my naming scheme where each push version of a base variable\ntacks on _push at the end. Since the push logic introduces a second version\nof most variables, it's hard to make it fully logical when the base versions\nstill retain their \"unqualified\" names.\n\nI can rename the sti's to 'cmp_fetch' and 'cmp_fetch_push', but does it\nhelp?\n\nProbably best would be to create a struct for the common variables between\nbase and push cases. But that's a large refactoring, so maybe better done\nas a separate follow-up to all of this instead? Here's what is looks like:\n\n```\ndiff --git a/remote.c b/remote.c\nindex eba013b6b4..581a62c266 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,41 +2237,47 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+struct format_branch {\n+\tint ours, theirs;\n+\tint cmp_fetch;\n+\tbool up_to_date;\n+\tconst char *full;\n+\tchar *name;\n+};\n+\n static void format_branch_comparison(struct strbuf *sb,\n-\t\t\t\t     bool up_to_date,\n-\t\t\t\t     int ours, int theirs,\n-\t\t\t\t     const char *branch_name,\n+\t\t\t\t     const struct format_branch *branch,\n \t\t\t\t     enum ahead_behind_flags abf,\n \t\t\t\t     bool show_divergence_advice)\n {\n-\tif (up_to_date) {\n+\tif (branch->up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbranch_name);\n+\t\t\tbranch->name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    branch_name);\n+\t\t\t    branch->name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n-\t} else if (!theirs) {\n+\t} else if (!branch->theirs) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n-\t\t\t   ours),\n-\t\t\tbranch_name, ours);\n+\t\t\t   branch->ours),\n+\t\t\tbranch->name, branch->ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n-\t} else if (!ours) {\n+\t} else if (!branch->ours) {\n \t\tstrbuf_addf(sb,\n \t\t\tQ_(\"Your branch is behind '%s' by %d commit, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n-\t\t\t   theirs),\n-\t\t\tbranch_name, theirs);\n+\t\t\t   branch->theirs),\n+\t\t\tbranch->name, branch->theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2283,8 +2289,8 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch and '%s' have diverged,\\n\"\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n-\t\t\t   ours + theirs),\n-\t\t\tbranch_name, ours, theirs);\n+\t\t\t   branch->ours + branch->theirs),\n+\t\t\tbranch->name, branch->ours, branch->theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n@@ -2299,33 +2305,32 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t enum ahead_behind_flags abf,\n \t\t\t int show_divergence_advice)\n {\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n \tint upstream_is_gone = 0;\n+\tstruct format_branch base;\n \n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n+\tbase.cmp_fetch = stat_tracking_info(branch, &base.ours, &base.theirs, &base.full, 0, abf);\n+\tbase.up_to_date = !base.cmp_fetch;\n+\tif (base.cmp_fetch < 0) {\n+\t\tif (!base.full)\n \t\t\treturn 0;\n \t\tupstream_is_gone = 1;\n \t}\n \n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n+\tbase.name = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    base.full, 0);\n \n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n+\t\t\tbase.name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n+\t\tformat_branch_comparison(sb, &base, abf, show_divergence_advice);\n \t}\n \n-\tfree(base);\n+\tfree(base.name);\n \treturn 1;\n }\n```\n\n\nHarald\n"},{"id":"533478","messageId":"20260110111431.27230-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqbjj2tdu0.fsf@gitster.g","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-10T11:14:31Z","receivedAt":"2026-01-10T11:14:35Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> This is not a new issue introduced by this series, but it is curious\n> there is \"ours + theirs\" there #leftoverbits.\n>\n> It is part of ngetext() aka Q_() call, used this way:\n\nAgreed, I thought about this one too. Will update it!\n\n\nHarald\n"},{"id":"533485","messageId":"pull.2138.v20.git.git.1768051831.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v19.git.git.1767984037.gitgitgadget@gmail.com","subject":"[PATCH v20 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T13:30:29Z","receivedAt":"2026-01-10T13:30:35Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 183 ++++++++++++++++++++-------\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 403 insertions(+), 42 deletions(-)\n\n\nbase-commit: d529f3a197364881746f558e5652f0236131eb86\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v20\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v20\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v19:\n\n 1:  451d7a4986 ! 1:  bb3e00863b refactor format_branch_comparison in preparation\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \t\t\t       \"and have %d and %d different commits each, \"\n     - \t\t\t       \"respectively.\\n\",\n     - \t\t\t   ours + theirs),\n     + \t} else {\n     + \t\tstrbuf_addf(sb,\n     +-\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n     +-\t\t\t       \"and have %d and %d different commit each, \"\n     +-\t\t\t       \"respectively.\\n\",\n     +-\t\t\t   \"Your branch and '%s' have diverged,\\n\"\n     +-\t\t\t       \"and have %d and %d different commits each, \"\n     +-\t\t\t       \"respectively.\\n\",\n     +-\t\t\t   ours + theirs),\n      -\t\t\tbase, ours, theirs);\n     ++\t\t\t\"Your branch and '%s' have diverged,\\n\"\n     ++\t\t\t       \"and have %d and %d different commits each, respectively.\\n\",\n      +\t\t\tbranch_name, ours, theirs);\n       \t\tif (show_divergence_advice &&\n       \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n 2:  dc8ab23158 ! 2:  050197eac3 status: show comparison with push remote tracking branch\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n       \t} else {\n      @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n     - \t\t\t       \"respectively.\\n\",\n     - \t\t\t   ours + theirs),\n     + \t\t\t\"Your branch and '%s' have diverged,\\n\"\n     + \t\t\t       \"and have %d and %d different commits each, respectively.\\n\",\n       \t\t\tbranch_name, ours, theirs);\n      -\t\tif (show_divergence_advice &&\n      -\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n\n-- \ngitgitgadget\n"},{"id":"533486","messageId":"bb3e00863b57ddbd8cebc6f8795629b12ac1a717.1768051831.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v20.git.git.1768051831.gitgitgadget@gmail.com","subject":"[PATCH v20 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T13:30:30Z","receivedAt":"2026-01-10T13:30:37Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 91 +++++++++++++++++++++++++++++++-------------------------\n 1 file changed, 50 insertions(+), 41 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..b053d4e443 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,42 +2237,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2281,7 +2260,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2292,25 +2271,55 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n \t\tstrbuf_addf(sb,\n-\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n-\t\t\t       \"and have %d and %d different commit each, \"\n-\t\t\t       \"respectively.\\n\",\n-\t\t\t   \"Your branch and '%s' have diverged,\\n\"\n-\t\t\t       \"and have %d and %d different commits each, \"\n-\t\t\t       \"respectively.\\n\",\n-\t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\t\"Your branch and '%s' have diverged,\\n\"\n+\t\t\t       \"and have %d and %d different commits each, respectively.\\n\",\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, sti;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (sti < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"533487","messageId":"050197eac32278cefb01ace05e32de70ec36ad1f.1768051831.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v20.git.git.1768051831.gitgitgadget@gmail.com","subject":"[PATCH v20 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T13:30:31Z","receivedAt":"2026-01-10T13:30:40Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 102 ++++++++++++++-\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 358 insertions(+), 6 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b053d4e443..dd8f94fa23 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,11 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tBRANCH_MODE_PULL = (1 << 0),\n+\tBRANCH_MODE_PUSH = (1 << 1),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2237,13 +2242,75 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (full_ref_out)\n+\t\t*full_ref_out = xstrdup(resolved);\n+\n+\tret = refs_shorten_unambiguous_ref(\n+\t\tget_main_ref_store(the_repository), resolved, 0);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     unsigned flags,\n \t\t\t\t     bool show_divergence_advice)\n {\n+\tbool want_push_advice = (flags & BRANCH_MODE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_pull_advice = (flags & BRANCH_MODE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_divergence_advice = (flags & BRANCH_MODE_PULL) &&\n+\t\tshow_divergence_advice &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2252,7 +2319,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2261,7 +2328,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2272,7 +2339,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2280,8 +2347,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\"Your branch and '%s' have diverged,\\n\"\n \t\t\t       \"and have %d and %d different commits each, respectively.\\n\",\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2298,6 +2364,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tunsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n+\tint push_ours, push_theirs, push_sti;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tunsigned push_branch_modes = 0;\n \n \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (sti < 0) {\n@@ -2309,6 +2380,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_sti >= 0) {\n+\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n+\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2317,10 +2398,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n+\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf,\n+\t\t\t\t\t base_branch_modes, show_divergence_advice);\n+\t}\n+\n+\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, !push_sti, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_branch_modes, show_divergence_advice);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..cf5a926dcd 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,266 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533490","messageId":"20260110144403.16387-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260110110629.18501-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-10T14:44:03Z","receivedAt":"2026-01-10T14:44:06Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Why is this variable called \"push_sti\"?  Calling the return value of\n> stat_tracking_info() \"sti\" was klumsy but understandable.  It would\n> have been much easier to follow the code if the variable were named\n> after what it _means_ in this particular caller's code flow, like\n> \"cmp_fetch\" (comparison on the fetching side, by convention negative\n> signals an error, and zero signals 'the same').  Perhaps rename \"sti\"\n> and \"push_sti\" at the same time to make them more symmetric?\n\nI renamed to 'push_cmp_fetch' and 'push_cmp_fetch' now. I think on my first\nreading I misunderstood your comment.\n\n> Other than this minor nit, this step looks nicely done (and the\n> previous one is also good).\n\nThanks a lot!\n\nHarald\n"},{"id":"533491","messageId":"pull.2138.v21.git.git.1768058653.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v20.git.git.1768051831.gitgitgadget@gmail.com","subject":"[PATCH v21 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T15:24:11Z","receivedAt":"2026-01-10T15:24:16Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 183 ++++++++++++++++++++-------\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 403 insertions(+), 42 deletions(-)\n\n\nbase-commit: d529f3a197364881746f558e5652f0236131eb86\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v21\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v21\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v20:\n\n 1:  bb3e00863b ! 1:  ce1f1eebb5 refactor format_branch_comparison in preparation\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      +\t\t\t enum ahead_behind_flags abf,\n      +\t\t\t int show_divergence_advice)\n      +{\n     -+\tint ours, theirs, sti;\n     ++\tint ours, theirs, cmp_fetch;\n      +\tconst char *full_base;\n      +\tchar *base;\n      +\tint upstream_is_gone = 0;\n      +\n     -+\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n     -+\tif (sti < 0) {\n     ++\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n     ++\tif (cmp_fetch < 0) {\n      +\t\tif (!full_base)\n      +\t\t\treturn 0;\n      +\t\tupstream_is_gone = 1;\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      +\t\t\tstrbuf_addstr(sb,\n      +\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n      +\t} else {\n     -+\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n     ++\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n      +\t}\n      +\n       \tfree(base);\n 2:  050197eac3 ! 2:  51d8486fe0 status: show comparison with push remote tracking branch\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tchar *base;\n       \tint upstream_is_gone = 0;\n      +\tunsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n     -+\tint push_ours, push_theirs, push_sti;\n     ++\tint push_ours, push_theirs, push_cmp_fetch;\n      +\tchar *full_push = NULL;\n      +\tchar *push = NULL;\n      +\tunsigned push_branch_modes = 0;\n       \n     - \tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n     - \tif (sti < 0) {\n     + \tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n     + \tif (cmp_fetch < 0) {\n      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n       \t\t\t\t\t    full_base, 0);\n       \n      +\tpush = get_remote_push_branch(branch, &full_push);\n      +\tif (push && strcmp(base, push)) {\n     -+\t\tpush_sti = stat_branch_pair(branch->refname, full_push,\n     ++\t\tpush_cmp_fetch = stat_branch_pair(branch->refname, full_push,\n      +\t\t\t\t\t   &push_ours, &push_theirs, abf);\n     -+\t\tif (push_sti >= 0) {\n     ++\t\tif (push_cmp_fetch >= 0) {\n      +\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n      +\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n      +\t\t}\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n       \t} else {\n     --\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);\n     -+\t\tformat_branch_comparison(sb, !sti, ours, theirs, base, abf,\n     +-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n     ++\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,\n      +\t\t\t\t\t base_branch_modes, show_divergence_advice);\n      +\t}\n      +\n      +\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n      +\t\tstrbuf_addstr(sb, \"\\n\");\n     -+\t\tformat_branch_comparison(sb, !push_sti, push_ours, push_theirs, push, abf,\n     ++\t\tformat_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,\n      +\t\t\t\t\t push_branch_modes, show_divergence_advice);\n       \t}\n       \n\n-- \ngitgitgadget\n"},{"id":"533492","messageId":"ce1f1eebb5a9371baccbdbe5f1f2c3324d5f8d11.1768058653.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v21.git.git.1768058653.gitgitgadget@gmail.com","subject":"[PATCH v21 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T15:24:12Z","receivedAt":"2026-01-10T15:24:17Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 91 +++++++++++++++++++++++++++++++-------------------------\n 1 file changed, 50 insertions(+), 41 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..af078817a4 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,42 +2237,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2281,7 +2260,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2292,25 +2271,55 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n \t\tstrbuf_addf(sb,\n-\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n-\t\t\t       \"and have %d and %d different commit each, \"\n-\t\t\t       \"respectively.\\n\",\n-\t\t\t   \"Your branch and '%s' have diverged,\\n\"\n-\t\t\t       \"and have %d and %d different commits each, \"\n-\t\t\t       \"respectively.\\n\",\n-\t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\t\"Your branch and '%s' have diverged,\\n\"\n+\t\t\t       \"and have %d and %d different commits each, respectively.\\n\",\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"533493","messageId":"51d8486fe01326b38ce93567f8573aa0ea83fb04.1768058653.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v21.git.git.1768058653.gitgitgadget@gmail.com","subject":"[PATCH v21 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T15:24:13Z","receivedAt":"2026-01-10T15:24:19Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 102 ++++++++++++++-\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 358 insertions(+), 6 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex af078817a4..40e4ebb3b7 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,11 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tBRANCH_MODE_PULL = (1 << 0),\n+\tBRANCH_MODE_PUSH = (1 << 1),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2237,13 +2242,75 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (full_ref_out)\n+\t\t*full_ref_out = xstrdup(resolved);\n+\n+\tret = refs_shorten_unambiguous_ref(\n+\t\tget_main_ref_store(the_repository), resolved, 0);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     unsigned flags,\n \t\t\t\t     bool show_divergence_advice)\n {\n+\tbool want_push_advice = (flags & BRANCH_MODE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_pull_advice = (flags & BRANCH_MODE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_divergence_advice = (flags & BRANCH_MODE_PULL) &&\n+\t\tshow_divergence_advice &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2252,7 +2319,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2261,7 +2328,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2272,7 +2339,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2280,8 +2347,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\"Your branch and '%s' have diverged,\\n\"\n \t\t\t       \"and have %d and %d different commits each, respectively.\\n\",\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2298,6 +2364,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tunsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n+\tint push_ours, push_theirs, push_cmp_fetch;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tunsigned push_branch_modes = 0;\n \n \tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (cmp_fetch < 0) {\n@@ -2309,6 +2380,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_cmp_fetch = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_cmp_fetch >= 0) {\n+\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n+\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2317,10 +2398,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,\n+\t\t\t\t\t base_branch_modes, show_divergence_advice);\n+\t}\n+\n+\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_branch_modes, show_divergence_advice);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..cf5a926dcd 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,266 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533498","messageId":"xmqqfr8ds7br.fsf@gitster.g","threadId":"64668","inReplyTo":"20260110110629.18501-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-10T17:31:20Z","receivedAt":"2026-01-10T17:31:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> I can rename the sti's to 'cmp_fetch' and 'cmp_fetch_push', but does it\n> help?\n\nNot at all.  Unless the contrast were \"something_fetch\" vs\n\"something_push\", that is.  And that something being cryptic \"sti\"\n(recall my comment on it, being the name of the function that\nreturns the value, which is less understandable than using words\nthat signals what the variable _means_), would not make much sense\nfor the \"push\" direction, as \"sti\" is not even an abbreviation for\nthe function that gives the value.  \"cmp\" (or \"compare\" for that\nmatter) still has the ambiguity \"compare with what and what for?\",\nbut at least it would be better than \"sti\".\n"},{"id":"533500","messageId":"xmqq7btps75z.fsf@gitster.g","threadId":"64668","inReplyTo":"20260110111431.27230-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-10T17:34:48Z","receivedAt":"2026-01-10T17:34:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> This is not a new issue introduced by this series, but it is curious\n>> there is \"ours + theirs\" there #leftoverbits.\n>>\n>> It is part of ngetext() aka Q_() call, used this way:\n>\n> Agreed, I thought about this one too. Will update it!\n\nPlease do so as a separate patch, not mix it into a patch that does\nsomething else.\n\nI do not think we mind even if it is left outside the current topic\nwe are discussing, if it distracts the main theme of the topic.\n"},{"id":"533501","messageId":"xmqqzf6lqs9w.fsf@gitster.g","threadId":"64668","inReplyTo":"pull.2138.v20.git.git.1768051831.gitgitgadget@gmail.com","subject":"Re: [PATCH v20 0/2] status: show comparison with push remote tracking branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-10T17:41:47Z","receivedAt":"2026-01-10T17:41:49Z","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\nHere is a place for you to explain what helps people to decypher the\ndifferences since the previous iteration in Range-diff (which is not\nvery easy to read).\n\n> cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\n> ychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\n> cc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\n> kristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\n> cc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im\n\nI am not sure what good these lines are doing (to GGG).\n\n> Harald Nordgren (2):\n>   refactor format_branch_comparison in preparation\n>   status: show comparison with push remote tracking branch\n>\n>  remote.c                 | 183 ++++++++++++++++++++-------\n>  t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n>  2 files changed, 403 insertions(+), 42 deletions(-)\n>\n>\n> base-commit: d529f3a197364881746f558e5652f0236131eb86\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v20\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v20\n> Pull-Request: https://github.com/git/git/pull/2138\n>\n> Range-diff vs v19:\n>\n>  1:  451d7a4986 ! 1:  bb3e00863b refactor format_branch_comparison in preparation\n>      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>        \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n>        \t\t\tstrbuf_addstr(sb,\n>        \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n>      -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>      - \t\t\t       \"and have %d and %d different commits each, \"\n>      - \t\t\t       \"respectively.\\n\",\n>      - \t\t\t   ours + theirs),\n>      + \t} else {\n>      + \t\tstrbuf_addf(sb,\n>      +-\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n>      +-\t\t\t       \"and have %d and %d different commit each, \"\n>      +-\t\t\t       \"respectively.\\n\",\n>      +-\t\t\t   \"Your branch and '%s' have diverged,\\n\"\n>      +-\t\t\t       \"and have %d and %d different commits each, \"\n>      +-\t\t\t       \"respectively.\\n\",\n>      +-\t\t\t   ours + theirs),\n>       -\t\t\tbase, ours, theirs);\n>      ++\t\t\t\"Your branch and '%s' have diverged,\\n\"\n>      ++\t\t\t       \"and have %d and %d different commits each, respectively.\\n\",\n>       +\t\t\tbranch_name, ours, theirs);\n>        \t\tif (show_divergence_advice &&\n>        \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n\nCould you not mix the ours+theirs thing into the same step?  Either\nmake it a standalone patch to clean up before or after your main 2\npatches, or leave it totally outside the series and send it after\nthis series settles.\n"},{"id":"533502","messageId":"20260110190657.25872-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqzf6lqs9w.fsf@gitster.g","subject":"Re: [PATCH v20 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-10T19:06:57Z","receivedAt":"2026-01-10T19:07:02Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Here is a place for you to explain what helps people to decypher the\n> differences since the previous iteration in Range-diff (which is not\n> very easy to read).\n\n?\n\n> Could you not mix the ours+theirs thing into the same step?  Either\n> make it a standalone patch to clean up before or after your main 2\n> patches, or leave it totally outside the series and send it after\n> this series settles.\n\nGot it, I'll revert that change.\n\n\nHarald\n"},{"id":"533503","messageId":"pull.2138.v22.git.git.1768074976.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v21.git.git.1768058653.gitgitgadget@gmail.com","subject":"[PATCH v22 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T19:56:14Z","receivedAt":"2026-01-10T19:56:19Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 174 ++++++++++++++++++++------\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 401 insertions(+), 35 deletions(-)\n\n\nbase-commit: d529f3a197364881746f558e5652f0236131eb86\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v22\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v22\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v21:\n\n 1:  ce1f1eebb5 ! 1:  4aa4f1abc8 refactor format_branch_comparison in preparation\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n     - \t} else {\n     - \t\tstrbuf_addf(sb,\n     --\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n     --\t\t\t       \"and have %d and %d different commit each, \"\n     --\t\t\t       \"respectively.\\n\",\n     --\t\t\t   \"Your branch and '%s' have diverged,\\n\"\n     --\t\t\t       \"and have %d and %d different commits each, \"\n     --\t\t\t       \"respectively.\\n\",\n     --\t\t\t   ours + theirs),\n     +@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     + \t\t\t       \"and have %d and %d different commits each, \"\n     + \t\t\t       \"respectively.\\n\",\n     + \t\t\t   ours + theirs),\n      -\t\t\tbase, ours, theirs);\n     -+\t\t\t\"Your branch and '%s' have diverged,\\n\"\n     -+\t\t\t       \"and have %d and %d different commits each, respectively.\\n\",\n      +\t\t\tbranch_name, ours, theirs);\n       \t\tif (show_divergence_advice &&\n       \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n 2:  51d8486fe0 ! 2:  b7e29887d9 status: show comparison with push remote tracking branch\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n       \t} else {\n      @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n     - \t\t\t\"Your branch and '%s' have diverged,\\n\"\n     - \t\t\t       \"and have %d and %d different commits each, respectively.\\n\",\n     + \t\t\t       \"respectively.\\n\",\n     + \t\t\t   ours + theirs),\n       \t\t\tbranch_name, ours, theirs);\n      -\t\tif (show_divergence_advice &&\n      -\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n\n-- \ngitgitgadget\n"},{"id":"533504","messageId":"4aa4f1abc85540773b27b52ee273bd9359500e2f.1768074976.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v22.git.git.1768074976.gitgitgadget@gmail.com","subject":"[PATCH v22 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T19:56:15Z","receivedAt":"2026-01-10T19:56:20Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 59b3715120..d5a6486026 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2237,42 +2237,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2281,7 +2260,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2292,7 +2271,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2305,12 +2284,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"533505","messageId":"b7e29887d99ad1f41f32cc39b65e011c01854e21.1768074976.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v22.git.git.1768074976.gitgitgadget@gmail.com","subject":"[PATCH v22 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-10T19:56:16Z","receivedAt":"2026-01-10T19:56:22Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 102 ++++++++++++++-\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 358 insertions(+), 6 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex d5a6486026..f9aa4c61bc 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,11 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tBRANCH_MODE_PULL = (1 << 0),\n+\tBRANCH_MODE_PUSH = (1 << 1),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2237,13 +2242,75 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (full_ref_out)\n+\t\t*full_ref_out = xstrdup(resolved);\n+\n+\tret = refs_shorten_unambiguous_ref(\n+\t\tget_main_ref_store(the_repository), resolved, 0);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     unsigned flags,\n \t\t\t\t     bool show_divergence_advice)\n {\n+\tbool want_push_advice = (flags & BRANCH_MODE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_pull_advice = (flags & BRANCH_MODE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_divergence_advice = (flags & BRANCH_MODE_PULL) &&\n+\t\tshow_divergence_advice &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2252,7 +2319,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2261,7 +2328,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2272,7 +2339,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2285,8 +2352,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2303,6 +2369,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tunsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n+\tint push_ours, push_theirs, push_cmp_fetch;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tunsigned push_branch_modes = 0;\n \n \tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (cmp_fetch < 0) {\n@@ -2314,6 +2385,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_cmp_fetch = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_cmp_fetch >= 0) {\n+\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n+\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2322,10 +2403,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,\n+\t\t\t\t\t base_branch_modes, show_divergence_advice);\n+\t}\n+\n+\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_branch_modes, show_divergence_advice);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..cf5a926dcd 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,266 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533506","messageId":"20260110200431.53479-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqfr8ds7br.fsf@gitster.g","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-10T20:04:31Z","receivedAt":"2026-01-10T20:04:34Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Not at all.  Unless the contrast were \"something_fetch\" vs\n> \"something_push\", that is.  And that something being cryptic \"sti\"\n> (recall my comment on it, being the name of the function that\n> returns the value, which is less understandable than using words\n> that signals what the variable _means_), would not make much sense\n> for the \"push\" direction, as \"sti\" is not even an abbreviation for\n> the function that gives the value.  \"cmp\" (or \"compare\" for that\n> matter) still has the ambiguity \"compare with what and what for?\",\n> but at least it would be better than \"sti\".\n\nMaybe simplest would be 'upstream_diff' and 'push_diff'? But this begs the\nquestion of why not all the old 'ours', 'theirs', etc variables are missing\nan 'upstream_' prefix.\n\nI feel like this can turn into endless refactoring, latest version now has\n'cmp_fetch' and 'push_cmp_fetch'.\n\nI have on my TODO list to fix the pluralization of the \"diverged\" text\nafter this patch series has been merged. I'm also working on refactoring\nthis by introducing a struct to pass data to 'format_branch_comparison'.\nThis has the very nice benefit on natural namespacing of variables when\nthey become fields in struct variables like 'upstream_branch' and\n'push_branch'.\n\nIn the meantime I think I will leave this patch series for now 🤗\n\n\nHarald\n"},{"id":"533508","messageId":"xmqqtswsrf5t.fsf@gitster.g","threadId":"64668","inReplyTo":"20260110200431.53479-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-11T03:39:42Z","receivedAt":"2026-01-11T03:39:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> In the meantime I think I will leave this patch series for now 🤗\n\nI think we are reaching the point of diminishing returns after\npolishing the series enough at the 22nd iteration.\n\nLet's wait for a few days to see if what others find in the series\nand then mark the topic for 'next'.  Hopefully any minor nits can be\naddressed later as a follow-up, just like the Q_(ours+theirs) thing.\n\nThanks.\n\n"},{"id":"533557","messageId":"2bf70c65-3d8d-414c-b4d0-9fe0a3decd15@app.fastmail.com","threadId":"64668","inReplyTo":"20260102112037.35360-1-haraldnordgren@gmail.com","subject":"Re: Another look?","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-01-12T07:34:21Z","receivedAt":"2026-01-12T07:34:42Z","isPatch":false,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Fri, Jan 2, 2026, at 12:20, Harald Nordgren wrote:\n>[snip]\n> Did you have some time to also look at the code? Would be nice to move this\n> along if possible.\n\nI’m not the right person to review this. ;)\n"},{"id":"533558","messageId":"1d92e252-b395-422d-be6b-ecb7c4dfd999@app.fastmail.com","threadId":"64668","inReplyTo":"20260110190657.25872-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v20 0/2] status: show comparison with push remote tracking branch","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-01-12T07:33:11Z","receivedAt":"2026-01-12T07:34:56Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Sat, Jan 10, 2026, at 20:06, Harald Nordgren wrote:\n>> Here is a place for you to explain what helps people to decypher the\n>> differences since the previous iteration in Range-diff (which is not\n>> very easy to read).\n>\n> ?\n\nIt’s a bit opaque. He’s saying that you should reserve some space in the\ncover letter email (`PATCH 0/2`) for explaining what has changed in this\nversion compared to the previous one.\n\nPeople often write:\n\n    Here is what this patch series does.\n\n    ### Changes in v3\n\n    Fixed the commit message typos.\n\nI don’t know where this should be filled in when using gitgitgadget. ;)\n"},{"id":"533636","messageId":"d2ea8560-1473-43c0-96c1-e1869e22c721@gmail.com","threadId":"64668","inReplyTo":"20260109160037.2067-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-01-12T14:45:16Z","receivedAt":"2026-01-12T14:45:19Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 09/01/2026 16:00, Harald Nordgren wrote:\n>> Using an enum for a set of flags is a bit confusing.\n> \n>> On reflection I wonder if we should be calling branch_get_push() instead\n>> of remote_ref_for_branch() and tracking_for_push_dest() as it respects\n>> 'push.default' and so the branch it returns is the one that \"git push\"\n>> without any arguments would push to.\n> \n> I'm not getting that to work without tests breaking.\n\nIt is hard to discuss this without knowing what actually breaks. Are you \ntalking about the tests added in this series? If so that means we're \nexpecting a different behavior to what \"git push\" actually does. As Ben \nhas pointed out elsewhere in this thread, if you're pushing back to a \ndifferent branch on the same remote as the upstream branch you need to \nset `push.default=current`.\n\nThe advantage would be that it simplifies the code and it means we \nrespect `push.default=upstream`.\n\nThe current implementation returns NULL if there is a push refspec \nexists but it does not match the current branch - in that case we should \nreturn a copy of the branch name instead.\n\n>> I think it would be simpler to just return the full refname and let the\n>> caller shorten it.\n> \n> We could to that but what's the benefit? Shouldn't a helper function reduce\n> the work for the caller, not the other way around? 🤗\n\nThe benefit is that you get a sane interface rather that returning two \ndifferent versions of the same string in two different ways (one from \nthe function's return value and the other from a function parameter). It \nalso matches what we do for the upstream branch.\n\n>> Why are we checking for BRANCH_MODE_PUSH here? Don't we want to show\n>> this advice regardless of the mode?\n> \n> Yeah, that's a good point. However, this will often lead to the advice\n> being shown twice, see this test:\n> \n> ```\n> status --no-ahead-behind shows diverged from origin/main and ahead of feature branch\n> ```\n\nI can't seem to see that test. If we're printing the advice once for the \nupstream branch and once for the default push remote I think that would \nbe ok.\n\n>> If we don't want to show this can't we set show_divergance_adivce to\n>> false when we call this function - why is it guarded by BRANCH_MODE_PULL\n>> as well?\n> \n> This will already have been shown the the pull branch (the upstream\n> branch).\n> \n> And in the cases when it's NOT shown for the pull branch but only the push\n> branch has diverged, then pulling won't solve the problem anyway. (Unless\n> you would try to pull from your push branch.)\n\nBut we set show_divergance_advice to false for the push branch so there \nis no need to check the flag.\n\nThanks\n\nPhillip\n\n"},{"id":"533677","messageId":"20260112194733.23192-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"d2ea8560-1473-43c0-96c1-e1869e22c721@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-12T19:47:33Z","receivedAt":"2026-01-12T19:47:36Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> It is hard to discuss this without knowing what actually breaks. Are you\n> talking about the tests added in this series? If so that means we're\n> expecting a different behavior to what \"git push\" actually does. As Ben\n> has pointed out elsewhere in this thread, if you're pushing back to a\n> different branch on the same remote as the upstream branch you need to\n> set `push.default=current`.\n\nYes, it's my new tests that are breaking. Maybe it's easiest if you check\nout the `seen` branch which now has this logic, play with the code and run\nthe tests to see when it breaks.\n\nI designed the feature around 'push.default=current' which I use.\n\nIf we would design the feature around 'push.default=upstream' then what is\nthe point? 🤗 Why do we need to show status for both an upstream and a push\nbranch if we are already pushing to our upstream branch?\n\n> The benefit is that you get a sane interface rather that returning two\n> different versions of the same string in two different ways (one from\n> the function's return value and the other from a function parameter). It\n> also matches what we do for the upstream branch.\n\nThat's a good point about matching what we do for upstream branch, I'll\ntake a look.\n\n> I can't seem to see that test. If we're printing the advice once for the\n> upstream branch and once for the default push remote I think that would\n> be ok.\n\nThis test is also part of my patch 🤗\n\nI disagree about showing the same advice twice.\n\n> But we set show_divergance_advice to false for the push branch so there\n> is no need to check the flag.\n\nGood point! I'll update it!\n\n\nHarald\n"},{"id":"533681","messageId":"pull.2138.v23.git.git.1768249586.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v22.git.git.1768074976.gitgitgadget@gmail.com","subject":"[PATCH v23 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-12T20:26:24Z","receivedAt":"2026-01-12T20:26:28Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 173 ++++++++++++++++++++------\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 400 insertions(+), 35 deletions(-)\n\n\nbase-commit: 8745eae506f700657882b9e32b2aa00f234a6fb6\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v23\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v23\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v22:\n\n 1:  4aa4f1abc8 = 1:  fd05c7b778 refactor format_branch_comparison in preparation\n 2:  b7e29887d9 ! 2:  f1ad7a1b6f status: show comparison with push remote tracking branch\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n      +\tbool want_pull_advice = (flags & BRANCH_MODE_PULL) &&\n      +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     -+\tbool want_divergence_advice = (flags & BRANCH_MODE_PULL) &&\n     -+\t\tshow_divergence_advice &&\n     ++\tbool want_divergence_advice = show_divergence_advice &&\n      +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n      +\n       \tif (up_to_date) {\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      +\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n      +\t\tstrbuf_addstr(sb, \"\\n\");\n      +\t\tformat_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,\n     -+\t\t\t\t\t push_branch_modes, show_divergence_advice);\n     ++\t\t\t\t\t push_branch_modes, 0);\n       \t}\n       \n       \tfree(base);\n\n-- \ngitgitgadget\n"},{"id":"533682","messageId":"fd05c7b7786892b2a1efe3a3daa74bb57546830d.1768249586.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v23.git.git.1768249586.gitgitgadget@gmail.com","subject":"[PATCH v23 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-12T20:26:25Z","receivedAt":"2026-01-12T20:26:30Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b756ff6f15..fd592ec659 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"533683","messageId":"f1ad7a1b6f7239c5f52a9a74721f340cb35d0b36.1768249586.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v23.git.git.1768249586.gitgitgadget@gmail.com","subject":"[PATCH v23 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-12T20:26:26Z","receivedAt":"2026-01-12T20:26:31Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 101 ++++++++++++++-\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 357 insertions(+), 6 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex fd592ec659..132fc6849f 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,11 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tBRANCH_MODE_PULL = (1 << 0),\n+\tBRANCH_MODE_PUSH = (1 << 1),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2230,13 +2235,74 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (full_ref_out)\n+\t\t*full_ref_out = xstrdup(resolved);\n+\n+\tret = refs_shorten_unambiguous_ref(\n+\t\tget_main_ref_store(the_repository), resolved, 0);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     unsigned flags,\n \t\t\t\t     bool show_divergence_advice)\n {\n+\tbool want_push_advice = (flags & BRANCH_MODE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_pull_advice = (flags & BRANCH_MODE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool want_divergence_advice = show_divergence_advice &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2245,7 +2311,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2254,7 +2320,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2265,7 +2331,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2278,8 +2344,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (want_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2296,6 +2361,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tunsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n+\tint push_ours, push_theirs, push_cmp_fetch;\n+\tchar *full_push = NULL;\n+\tchar *push = NULL;\n+\tunsigned push_branch_modes = 0;\n \n \tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (cmp_fetch < 0) {\n@@ -2307,6 +2377,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tpush = get_remote_push_branch(branch, &full_push);\n+\tif (push && strcmp(base, push)) {\n+\t\tpush_cmp_fetch = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\tif (push_cmp_fetch >= 0) {\n+\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n+\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2315,10 +2395,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,\n+\t\t\t\t\t base_branch_modes, show_divergence_advice);\n+\t}\n+\n+\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_branch_modes, 0);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..cf5a926dcd 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,266 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533684","messageId":"xmqqv7h6lfua.fsf@gitster.g","threadId":"64668","inReplyTo":"f1ad7a1b6f7239c5f52a9a74721f340cb35d0b36.1768249586.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v23 2/2] status: show comparison with push remote tracking branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-12T20:46:05Z","receivedAt":"2026-01-12T20:46:07Z","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> +\tbool want_push_advice = (flags & BRANCH_MODE_PUSH) &&\n> +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n> +\tbool want_pull_advice = (flags & BRANCH_MODE_PULL) &&\n> +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n> +\tbool want_divergence_advice = show_divergence_advice &&\n> +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n\nJust an observation, and not an objection, but it looks curious why\nthe last one is so different from the other two.  IOW, the above\nmakes me wonder if it makes sense to roll the show_divergence_advice\nbit into the base_branch_modes flag word.\n\nOther than that, this round is a pleasant read.  Thanks.\n"},{"id":"533720","messageId":"20260113095447.52482-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqv7h6lfua.fsf@gitster.g","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-13T09:54:47Z","receivedAt":"2026-01-13T09:54:51Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Just an observation, and not an objection, but it looks curious why\n> the last one is so different from the other two.  IOW, the above\n> makes me wonder if it makes sense to roll the show_divergence_advice\n> bit into the base_branch_modes flag word.\n\nGood point. I changed it, it makes it nicer with one fewer parameter.\n\n> Other than that, this round is a pleasant read.  Thanks.\n\nThanks for all the help so far!\n\nHarald\n"},{"id":"533726","messageId":"pull.2138.v24.git.git.1768298118.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v23.git.git.1768249586.gitgitgadget@gmail.com","subject":"[PATCH v24 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-13T09:55:16Z","receivedAt":"2026-01-13T09:55:23Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 175 ++++++++++++++++++++------\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 402 insertions(+), 35 deletions(-)\n\n\nbase-commit: 8745eae506f700657882b9e32b2aa00f234a6fb6\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v24\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v24\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v23:\n\n 1:  fd05c7b778 = 1:  fd05c7b778 refactor format_branch_comparison in preparation\n 2:  f1ad7a1b6f ! 2:  138b79a0b9 status: show comparison with push remote tracking branch\n     @@ remote.c\n       enum map_direction { FROM_SRC, FROM_DST };\n       \n      +enum {\n     -+\tBRANCH_MODE_PULL = (1 << 0),\n     -+\tBRANCH_MODE_PUSH = (1 << 1),\n     ++\tENABLE_ADVICE_PULL       = (1 << 0),\n     ++\tENABLE_ADVICE_PUSH       = (1 << 1),\n     ++\tENABLE_ADVICE_DIVERGENCE = (1 << 2),\n      +};\n      +\n       struct counted_string {\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n       }\n       \n     -+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)\n     ++static char *get_remote_push_branch(struct branch *branch)\n      +{\n      +\tstruct remote *remote;\n      +\tconst char *push_remote;\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\t\treturn NULL;\n      +\t}\n      +\n     -+\tif (full_ref_out)\n     -+\t\t*full_ref_out = xstrdup(resolved);\n     -+\n     -+\tret = refs_shorten_unambiguous_ref(\n     -+\t\tget_main_ref_store(the_repository), resolved, 0);\n     ++\tret = xstrdup(resolved);\n      +\tfree(tracking_ref);\n      +\treturn ret;\n      +}\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \t\t\t\t     int ours, int theirs,\n       \t\t\t\t     const char *branch_name,\n       \t\t\t\t     enum ahead_behind_flags abf,\n     -+\t\t\t\t     unsigned flags,\n     - \t\t\t\t     bool show_divergence_advice)\n     +-\t\t\t\t     bool show_divergence_advice)\n     ++\t\t\t\t     unsigned flags)\n       {\n     -+\tbool want_push_advice = (flags & BRANCH_MODE_PUSH) &&\n     ++\tbool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&\n      +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     -+\tbool want_pull_advice = (flags & BRANCH_MODE_PULL) &&\n     ++\tbool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&\n      +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     -+\tbool want_divergence_advice = show_divergence_advice &&\n     ++\tbool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&\n      +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n      +\n       \tif (up_to_date) {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n       \t\t\t    branch_name);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif (want_push_advice)\n     ++\t\tif (enable_push_advice)\n       \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n       \t\t\t\t    \"git status --ahead-behind\");\n       \t} else if (!theirs) {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   ours),\n       \t\t\tbranch_name, ours);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif (want_push_advice)\n     ++\t\tif (enable_push_advice)\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n       \t} else if (!ours) {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   theirs),\n       \t\t\tbranch_name, theirs);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif (want_pull_advice)\n     ++\t\tif (enable_pull_advice)\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n       \t} else {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\tbranch_name, ours, theirs);\n      -\t\tif (show_divergence_advice &&\n      -\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif (want_divergence_advice)\n     ++\t\tif (enable_divergence_advice)\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n       \t}\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tconst char *full_base;\n       \tchar *base;\n       \tint upstream_is_gone = 0;\n     -+\tunsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;\n     ++\tunsigned base_branch_flags = ENABLE_ADVICE_PULL | ENABLE_ADVICE_PUSH;\n      +\tint push_ours, push_theirs, push_cmp_fetch;\n     -+\tchar *full_push = NULL;\n     ++\tchar *full_push;\n      +\tchar *push = NULL;\n     -+\tunsigned push_branch_modes = 0;\n     ++\tunsigned push_branch_flags = 0;\n       \n       \tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n       \tif (cmp_fetch < 0) {\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n       \t\t\t\t\t    full_base, 0);\n       \n     -+\tpush = get_remote_push_branch(branch, &full_push);\n     -+\tif (push && strcmp(base, push)) {\n     -+\t\tpush_cmp_fetch = stat_branch_pair(branch->refname, full_push,\n     -+\t\t\t\t\t   &push_ours, &push_theirs, abf);\n     -+\t\tif (push_cmp_fetch >= 0) {\n     -+\t\t\tbase_branch_modes = BRANCH_MODE_PULL;\n     -+\t\t\tpush_branch_modes = BRANCH_MODE_PUSH;\n     ++\tfull_push = get_remote_push_branch(branch);\n     ++\tif (full_push) {\n     ++\t\tpush = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n     ++\t\t\t\t\t\t     full_push, 0);\n     ++\t\tif (push && base && strcmp(base, push)) {\n     ++\t\t\tpush_cmp_fetch = stat_branch_pair(branch->refname, full_push,\n     ++\t\t\t\t\t\t   &push_ours, &push_theirs, abf);\n     ++\t\t\tif (push_cmp_fetch >= 0) {\n     ++\t\t\t\tbase_branch_flags = ENABLE_ADVICE_PULL;\n     ++\t\t\t\tpush_branch_flags = ENABLE_ADVICE_PUSH;\n     ++\t\t\t}\n      +\t\t}\n      +\t}\n      +\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n       \t} else {\n      -\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n     ++\t\tif (show_divergence_advice)\n     ++\t\t\tbase_branch_flags |= ENABLE_ADVICE_DIVERGENCE;\n      +\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,\n     -+\t\t\t\t\t base_branch_modes, show_divergence_advice);\n     ++\t\t\t\t\t base_branch_flags);\n      +\t}\n      +\n     -+\tif (push_branch_modes & BRANCH_MODE_PUSH) {\n     ++\tif (push_branch_flags & ENABLE_ADVICE_PUSH) {\n      +\t\tstrbuf_addstr(sb, \"\\n\");\n      +\t\tformat_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,\n     -+\t\t\t\t\t push_branch_modes, 0);\n     ++\t\t\t\t\t push_branch_flags);\n       \t}\n       \n       \tfree(base);\n\n-- \ngitgitgadget\n"},{"id":"533727","messageId":"fd05c7b7786892b2a1efe3a3daa74bb57546830d.1768298118.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v24.git.git.1768298118.gitgitgadget@gmail.com","subject":"[PATCH v24 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-13T09:55:17Z","receivedAt":"2026-01-13T09:55:26Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b756ff6f15..fd592ec659 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"533728","messageId":"138b79a0b9987fceb2da1269fbed88f89221e061.1768298118.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v24.git.git.1768298118.gitgitgadget@gmail.com","subject":"[PATCH v24 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-13T09:55:18Z","receivedAt":"2026-01-13T09:55:28Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 105 ++++++++++++++--\n t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 360 insertions(+), 7 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex fd592ec659..8d64a3bee6 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,12 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tENABLE_ADVICE_PULL       = (1 << 0),\n+\tENABLE_ADVICE_PUSH       = (1 << 1),\n+\tENABLE_ADVICE_DIVERGENCE = (1 << 2),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2230,13 +2236,69 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tret = xstrdup(resolved);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n-\t\t\t\t     bool show_divergence_advice)\n+\t\t\t\t     unsigned flags)\n {\n+\tbool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2245,7 +2307,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2254,7 +2316,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2265,7 +2327,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2278,8 +2340,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2296,6 +2357,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tunsigned base_branch_flags = ENABLE_ADVICE_PULL | ENABLE_ADVICE_PUSH;\n+\tint push_ours, push_theirs, push_cmp_fetch;\n+\tchar *full_push;\n+\tchar *push = NULL;\n+\tunsigned push_branch_flags = 0;\n \n \tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (cmp_fetch < 0) {\n@@ -2307,6 +2373,20 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tfull_push = get_remote_push_branch(branch);\n+\tif (full_push) {\n+\t\tpush = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t\t     full_push, 0);\n+\t\tif (push && base && strcmp(base, push)) {\n+\t\t\tpush_cmp_fetch = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\t\tif (push_cmp_fetch >= 0) {\n+\t\t\t\tbase_branch_flags = ENABLE_ADVICE_PULL;\n+\t\t\t\tpush_branch_flags = ENABLE_ADVICE_PUSH;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2315,10 +2395,21 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t\tif (show_divergence_advice)\n+\t\t\tbase_branch_flags |= ENABLE_ADVICE_DIVERGENCE;\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,\n+\t\t\t\t\t base_branch_flags);\n+\t}\n+\n+\tif (push_branch_flags & ENABLE_ADVICE_PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_branch_flags);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..cf5a926dcd 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,266 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533730","messageId":"d8ecb19d-8a17-4979-b663-a6d928709e4b@gmail.com","threadId":"64668","inReplyTo":"20260112194733.23192-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-01-13T10:41:52Z","receivedAt":"2026-01-13T10:41:55Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 12/01/2026 19:47, Harald Nordgren wrote:\n>> It is hard to discuss this without knowing what actually breaks. Are you\n>> talking about the tests added in this series? If so that means we're\n>> expecting a different behavior to what \"git push\" actually does. As Ben\n>> has pointed out elsewhere in this thread, if you're pushing back to a\n>> different branch on the same remote as the upstream branch you need to\n>> set `push.default=current`.\n> \n> Yes, it's my new tests that are breaking. Maybe it's easiest if you check\n> out the `seen` branch which now has this logic, play with the code and run\n> the tests to see when it breaks.\n> \n> I designed the feature around 'push.default=current' which I use.\n> \n> If we would design the feature around 'push.default=upstream' then what is\n> the point? 🤗 Why do we need to show status for both an upstream and a push\n> branch if we are already pushing to our upstream branch?\n\nI'm not suggesting we design the feature around 'push.default=upstream', \nI'm suggesting that we design it to respect 'push.default' so it gives \nsensible output (i.e. something that resembles what \"git push\" would do) \nwhatever the setting.\n\n>> The benefit is that you get a sane interface rather that returning two\n>> different versions of the same string in two different ways (one from\n>> the function's return value and the other from a function parameter). It\n>> also matches what we do for the upstream branch.\n> \n> That's a good point about matching what we do for upstream branch, I'll\n> take a look.\n\nThanks, I think that would be cleaner\n\n>> I can't seem to see that test. If we're printing the advice once for the\n>> upstream branch and once for the default push remote I think that would\n>> be ok.\n> \n> This test is also part of my patch 🤗\n\nOh, I was looking in junio's \"seen\" branch from the 8th and it wasn't \nthere. I've updated and I can see it now. I can see tests for\n\n  - upstream differs from the local branch, no push branch shown\n  - upstream and push branches differ from the local branch\n  - upstream and push branches match the local branch\n\nI can't see a test for the local branch differing from the upstream \nbranch when the push branch matches the local branch. As far as I can \nsee in that case we don't show the advice when we should do (and we \ncurrently do show it)\n\n> I disagree about showing the same advice twice.\n\nI think it is less bad than not showing it when the upstream branch does \nnot match the local branch.\n\n>> But we set show_divergance_advice to false for the push branch so there\n>> is no need to check the flag.\n> \n> Good point! I'll update it!\n\nThat's great\n\nThanks\n\nPhillip\n\n> \n> Harald\n\n"},{"id":"533732","messageId":"20260113121118.49541-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"d8ecb19d-8a17-4979-b663-a6d928709e4b@gmail.com","subject":"Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-13T12:11:18Z","receivedAt":"2026-01-13T12:11:21Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> I'm not suggesting we design the feature around 'push.default=upstream',\n> I'm suggesting that we design it to respect 'push.default' so it gives\n> sensible output (i.e. something that resembles what \"git push\" would do)\n> whatever the setting.\n\nI guess I'm still not getting it, maybe explain it like I'm more stupid 😅\n\n'push.default' does not change the output of git status, which shows a\ncomparison with the tracking branch. I wrote the code to only report push\nbranch status when it differs from the upstream. If we use\n'push.default=upstream' they will be the same, so no need to report it\ntwice.\n\n\n> Thanks, I think that would be cleaner\n\nIt's in the latest patch now.\n\n> I can see tests for\n> \n>  - upstream differs from the local branch, no push branch shown\n>  - upstream and push branches differ from the local branch\n>  - upstream and push branches match the local branch\n> \n> I can't see a test for the local branch differing from the upstream\n> branch when the push branch matches the local branch. As far as I can see\n> in that case we don't show the advice when we should do (and we currently\n> do show it)\n\nI added a test for this now, which I think proves that it's working\ncorrectly:\n\n```\ntest_expect_success 'status with upstream ahead and push branch up to date' '\n```\n\n\nHarald\n"},{"id":"533733","messageId":"pull.2138.v25.git.git.1768306316.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v24.git.git.1768298118.gitgitgadget@gmail.com","subject":"[PATCH v25 0/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-13T12:11:54Z","receivedAt":"2026-01-13T12:11:59Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: show comparison with push remote tracking branch\n\n remote.c                 | 175 +++++++++++++++++++-----\n t/t6040-tracking-info.sh | 285 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 425 insertions(+), 35 deletions(-)\n\n\nbase-commit: 8745eae506f700657882b9e32b2aa00f234a6fb6\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v25\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v25\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v24:\n\n 1:  fd05c7b778 = 1:  fd05c7b778 refactor format_branch_comparison in preparation\n 2:  138b79a0b9 ! 2:  fa744efc59 status: show comparison with push remote tracking branch\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     ++test_expect_success 'status with upstream ahead and push branch up to date' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout -b ahead upstream/main &&\n     ++\t\tadvance work &&\n     ++\t\tgit push upstream HEAD &&\n     ++\t\tgit checkout -b feature8 upstream/main &&\n     ++\t\tgit push origin &&\n     ++\t\tgit branch --set-upstream-to upstream/ahead &&\n     ++\t\tgit status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature8\n     ++\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n     ++\t  (use \"git pull\" to update your local branch)\n     ++\n     ++\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n      +test_expect_success 'status shows remapped push refspec' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout -b feature8 origin/main &&\n     -+\t\tgit config remote.origin.push refs/heads/feature8:refs/heads/remapped &&\n     ++\t\tgit checkout -b feature9 origin/main &&\n     ++\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n      +\t\tgit push &&\n      +\t\tadvance work &&\n      +\t\tgit status >../actual\n      +\t) &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature8\n     ++\tOn branch feature9\n      +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n      +\n      +\tYour branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +test_expect_success 'status shows remapped push refspec with upstream remote' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout -b feature9 upstream/main &&\n     -+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n     ++\t\tgit checkout -b feature10 upstream/main &&\n     ++\t\tgit config remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n      +\t\tgit push origin &&\n      +\t\tadvance work &&\n      +\t\tgit status >../actual\n      +\t) &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature9\n     ++\tOn branch feature10\n      +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n      +\n      +\tYour branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.\n\n-- \ngitgitgadget\n"},{"id":"533734","messageId":"fd05c7b7786892b2a1efe3a3daa74bb57546830d.1768306316.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v25.git.git.1768306316.gitgitgadget@gmail.com","subject":"[PATCH v25 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-13T12:11:55Z","receivedAt":"2026-01-13T12:12: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\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b756ff6f15..fd592ec659 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"533735","messageId":"fa744efc598cd902e9a3a4a17b24134351ca5803.1768306316.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v25.git.git.1768306316.gitgitgadget@gmail.com","subject":"[PATCH v25 2/2] status: show comparison with push remote tracking branch","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-13T12:11:56Z","receivedAt":"2026-01-13T12:12:02Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\n\"git status\" on a branch that follows a remote branch compares\ncommits on the current branch and the remote-tracking branch it\nbuilds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n\nWhen working on a feature branch that tracks a remote feature branch,\nbut you also want to track progress relative to the push destination\ntracking branch (which may differ from the upstream branch), git status\nnow shows an additional comparison.\n\nWhen the upstream tracking branch differs from the push destination\ntracking branch, git status shows both the comparison with the upstream\ntracking branch (as before) and an additional comparison with the push\ndestination tracking branch. The push branch comparison appears on a\nseparate line after the upstream branch status, using the same format.\n\nExample output when tracking origin/main but push destination is\norigin/feature:\n    On branch feature\n    Your branch and 'origin/main' have diverged,\n    and have 3 and 1 different commits each, respectively.\n      (use \"git pull\" if you want to integrate the remote branch with yours)\n\n    Your branch is ahead of 'origin/feature' by 1 commit.\n      (use \"git push\" to publish your local commits)\n\nThe comparison is only shown when the push destination tracking branch\ndiffers from the upstream tracking branch, even if they are on the same\nremote.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 105 ++++++++++++++-\n t/t6040-tracking-info.sh | 285 +++++++++++++++++++++++++++++++++++++++\n 2 files changed, 383 insertions(+), 7 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex fd592ec659..8d64a3bee6 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,12 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tENABLE_ADVICE_PULL       = (1 << 0),\n+\tENABLE_ADVICE_PUSH       = (1 << 1),\n+\tENABLE_ADVICE_DIVERGENCE = (1 << 2),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2230,13 +2236,69 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *get_remote_push_branch(struct branch *branch)\n+{\n+\tstruct remote *remote;\n+\tconst char *push_remote;\n+\tchar *push_dst = NULL;\n+\tchar *tracking_ref;\n+\tconst char *resolved;\n+\tchar *ret;\n+\n+\tif (!branch)\n+\t\treturn NULL;\n+\n+\tpush_remote = pushremote_for_branch(branch, NULL);\n+\tif (!push_remote)\n+\t\treturn NULL;\n+\n+\tremote = remotes_remote_get(the_repository, push_remote);\n+\tif (!remote)\n+\t\treturn NULL;\n+\n+\tpush_dst = remote_ref_for_branch(branch, 1);\n+\tif (!push_dst) {\n+\t\tif (remote->push.nr)\n+\t\t\treturn NULL;\n+\t\tpush_dst = xstrdup(branch->refname);\n+\t}\n+\n+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n+\tfree(push_dst);\n+\n+\tif (!tracking_ref)\n+\t\treturn NULL;\n+\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\ttracking_ref,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\n+\tif (!resolved) {\n+\t\tfree(tracking_ref);\n+\t\treturn NULL;\n+\t}\n+\n+\tret = xstrdup(resolved);\n+\tfree(tracking_ref);\n+\treturn ret;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n-\t\t\t\t     bool show_divergence_advice)\n+\t\t\t\t     unsigned flags)\n {\n+\tbool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2245,7 +2307,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2254,7 +2316,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2265,7 +2327,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2278,8 +2340,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2296,6 +2357,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tconst char *full_base;\n \tchar *base;\n \tint upstream_is_gone = 0;\n+\tunsigned base_branch_flags = ENABLE_ADVICE_PULL | ENABLE_ADVICE_PUSH;\n+\tint push_ours, push_theirs, push_cmp_fetch;\n+\tchar *full_push;\n+\tchar *push = NULL;\n+\tunsigned push_branch_flags = 0;\n \n \tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n \tif (cmp_fetch < 0) {\n@@ -2307,6 +2373,20 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n \t\t\t\t\t    full_base, 0);\n \n+\tfull_push = get_remote_push_branch(branch);\n+\tif (full_push) {\n+\t\tpush = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t\t     full_push, 0);\n+\t\tif (push && base && strcmp(base, push)) {\n+\t\t\tpush_cmp_fetch = stat_branch_pair(branch->refname, full_push,\n+\t\t\t\t\t\t   &push_ours, &push_theirs, abf);\n+\t\t\tif (push_cmp_fetch >= 0) {\n+\t\t\t\tbase_branch_flags = ENABLE_ADVICE_PULL;\n+\t\t\t\tpush_branch_flags = ENABLE_ADVICE_PUSH;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n \tif (upstream_is_gone) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n@@ -2315,10 +2395,21 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n \t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t\tif (show_divergence_advice)\n+\t\t\tbase_branch_flags |= ENABLE_ADVICE_DIVERGENCE;\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,\n+\t\t\t\t\t base_branch_flags);\n+\t}\n+\n+\tif (push_branch_flags & ENABLE_ADVICE_PUSH) {\n+\t\tstrbuf_addstr(sb, \"\\n\");\n+\t\tformat_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,\n+\t\t\t\t\t push_branch_flags);\n \t}\n \n \tfree(base);\n+\tfree(full_push);\n+\tfree(push);\n \treturn 1;\n }\n \ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..da5696113e 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,289 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout shows push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature7 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status with upstream ahead and push branch up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b ahead upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push upstream HEAD &&\n+\t\tgit checkout -b feature8 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit branch --set-upstream-to upstream/ahead &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n+\t  (use \"git pull\" to update your local branch)\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature9 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status shows remapped push refspec with upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature10 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature10\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"533754","messageId":"20260113170321.GA265671@coredump.intra.peff.net","threadId":"64668","inReplyTo":"fa744efc598cd902e9a3a4a17b24134351ca5803.1768306316.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v25 2/2] status: show comparison with push remote tracking branch","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-13T17:03:21Z","receivedAt":"2026-01-13T17:03:31Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Jan 13, 2026 at 12:11:56PM +0000, Harald Nordgren via GitGitGadget wrote:\n\n> Example output when tracking origin/main but push destination is\n> origin/feature:\n>     On branch feature\n>     Your branch and 'origin/main' have diverged,\n>     and have 3 and 1 different commits each, respectively.\n>       (use \"git pull\" if you want to integrate the remote branch with yours)\n> \n>     Your branch is ahead of 'origin/feature' by 1 commit.\n>       (use \"git push\" to publish your local commits)\n\nCan we make this configurable?\n\nI build my daily driver off of the 'jch' branch, which now includes this\nseries, and I've found that for my triangular workflow the ahead/behind\nfor the push branch is just useless noise. I treat my push destination\nlike a mirror, where I always just push up everything at the end of the\nday.\n\nI know that the output can be disabled with status.aheadbehind, but:\n\n  1. I noticed this first via \"git checkout\", which does not have such a\n     flag (AFAIK).\n\n  2. That flag would also disable the upstream branch ahead/behind\n     output, which I do find useful.\n\n-Peff\n"},{"id":"533765","messageId":"20260113183557.7991-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260113170321.GA265671@coredump.intra.peff.net","subject":"Triangular workflow","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-13T18:35:57Z","receivedAt":"2026-01-13T18:36:00Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> For my triangular workflow the ahead/behind for the push branch is just\n> useless noise. I treat my push destination like a mirror, where I always\n> just push up everything at the end of the day.\n\nThis seems like a sub-optimal workflow 🤗\n\nMay I ask where you normally push (unless you only ever push once at the\nend) of the day? If it's another branch than your mirror, why not set that\nas your push destination then, and push to your push destination with the\nlonger\n\n    git push origin my-mirror\n\nIt makes more sense to me to reserve the shorter and more convenient\n'git push' for something you do many times a day.\n\n\nHarald\n"},{"id":"533775","messageId":"20260113214059.GC288857@coredump.intra.peff.net","threadId":"64668","inReplyTo":"20260113183557.7991-1-haraldnordgren@gmail.com","subject":"Re: Triangular workflow","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-13T21:40:59Z","receivedAt":"2026-01-13T21:41:01Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Jan 13, 2026 at 07:35:57PM +0100, Harald Nordgren wrote:\n\n> > For my triangular workflow the ahead/behind for the push branch is just\n> > useless noise. I treat my push destination like a mirror, where I always\n> > just push up everything at the end of the day.\n> \n> This seems like a sub-optimal workflow 🤗\n> \n> May I ask where you normally push (unless you only ever push once at the\n> end) of the day? If it's another branch than your mirror, why not set that\n> as your push destination then, and push to your push destination with the\n> longer\n> \n>     git push origin my-mirror\n> \n> It makes more sense to me to reserve the shorter and more convenient\n> 'git push' for something you do many times a day.\n\nI couldn't quite parse what you meant by \"another branch than your\nmirror\", but I'll describe my workflow:\n\n  1. My \"origin\" remote is https://github.com/gitster/git.\n\n  2. Most branches have origin/master as their upstream, though\n     occasionally I'll have dependent branches that use \".\" as their\n     remote and the local base branch for the \"branch\" field.\n\n     E.g.\n\n       git checkout -b jk/some-topic origin/master\n       ...\n       git checkout -t -b jk/another-topic jk/some-topic\n\n  3. My mirror is https://github.com/peff/git, which I call \"github\" in\n     the remote (arguably confusing, but back when I started that\n     convention I was pulling Junio's tree from kernel.org ;) ). I point\n     remote.pushdefault to the \"github\" remote.\n\n  4. My push.default setting is \"current\", so if I want to push up a\n     single branch, I can just \"git push\". I hardly ever do that,\n     though.\n\n  5. I usually push once per day or so. After integrating my topics into\n     a shared daily-driver branch, I run something like:\n\n       make test && make install && git push -f github refs/heads/jk/*\n\n     (it's not exactly that; I have a script which picks out the local\n      refs, but it's the moral equivalent of that).\n\nSo I don't ever care about the relationship between a specific jk/foo\nand my mirror's version of it. I don't expect anybody to look at those,\nand they exist mostly for backup and CI purposes (though I don't run CI\non the individual branches, but only the integrated result).\n\nAnd having the extra output from \"git checkout\" is just extra noise for\nme, especially because it is easy to see only the second message (which\nlooks just like the upstream ahead/behind message, of course) and get\nconfused. The first time I saw it I thought I had misconfigured\nsomething with my branch.\n\n-Peff\n"},{"id":"533779","messageId":"20260113230107.16728-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260113214059.GC288857@coredump.intra.peff.net","subject":"Triangular workflow","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-13T23:01:07Z","receivedAt":"2026-01-13T23:01:11Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi Jeff!\n\nI'm very happy that your responded respectfully despite me basically\nsaying that you were using Git wrong. It's nice to see how some of the pros\ndo it!\n\nI'm wondering if since you are scripting this anyway, if you really need a\npush branch at all? Can't you just as easily switch to doing this in the\nscript:\n\n    git config push.default upstream\n    git push github jk/some-topic\n\nAs a note, before I started working on this feature, I don't realize\nthat there was such a thing as a push branch (i.e. something different from\nthe tracking branch). So I had the habit of checking out and pushing like\nthis:\n\n    git branch --set-upstream-to upstream/master\n    git push origin $(git rev-parse --abbrev-ref HEAD)\n\nI worked really well for me. The only issue was missing the status info\nfrom my own branch -- which is why I started writing this feature.\n\n\nHarald\n"},{"id":"533789","messageId":"CALnO6CAnfmDfCFJY55J81TTeRCYqeHq=dVapeanF0d0Z_yg=tw@mail.gmail.com","threadId":"64668","inReplyTo":"20260113230107.16728-1-haraldnordgren@gmail.com","subject":"Re: Triangular workflow","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-14T02:22:27Z","receivedAt":"2026-01-14T02:22:45Z","isPatch":false,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Tue, Jan 13, 2026 at 6:01 PM Harald Nordgren\n<haraldnordgren@gmail.com> wrote:\n>\n> Hi Jeff!\n>\n> I'm very happy that your responded respectfully despite me basically\n> saying that you were using Git wrong. It's nice to see how some of the pros\n> do it!\n>\n> I'm wondering if since you are scripting this anyway, if you really need a\n> push branch at all? Can't you just as easily switch to doing this in the\n> script:\n>\n>     git config push.default upstream\n>     git push github jk/some-topic\n\nThe script is doing the \"moral equivalent of\" \"git push -f github\nrefs/heads/jk/*\", so I'm not sure I follow the suggestion here.\n\n> As a note, before I started working on this feature, I don't realize\n> that there was such a thing as a push branch (i.e. something different from\n> the tracking branch). So I had the habit of checking out and pushing like\n> this:\n>\n>     git branch --set-upstream-to upstream/master\n>     git push origin $(git rev-parse --abbrev-ref HEAD)\n>\n> I worked really well for me. The only issue was missing the status info\n> from my own branch -- which is why I started writing this feature.\n>\n>\n> Harald\n\nMy workflow is different from Peff's, but it is similar along at least\none line: it's really convenient to have \"git push\" with no further\narguments (only possibly flags) to push my branch to a remote mirror.\n\n-- \nD. Ben Knoble\n"},{"id":"533790","messageId":"20260114023408.GA858378@coredump.intra.peff.net","threadId":"64668","inReplyTo":"20260113230107.16728-1-haraldnordgren@gmail.com","subject":"Re: Triangular workflow","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-14T02:34:08Z","receivedAt":"2026-01-14T02:34:09Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Jan 14, 2026 at 12:01:07AM +0100, Harald Nordgren wrote:\n\n> I'm wondering if since you are scripting this anyway, if you really need a\n> push branch at all? Can't you just as easily switch to doing this in the\n> script:\n> \n>     git config push.default upstream\n>     git push github jk/some-topic\n\nI could (and in fact the script names the remote directly already,\nbecause you can't pass refspecs without specifying the remote). But I do\noccasionally push a single branch with a bare \"git push\". Usually this\nis the integration branch, when I am trying to trigger CI manually\n(e.g., when piling hacks on top in order to debug a CI failure ;) ).\n\nSo even if I only do it infrequently, it feels weird that a bare \"git\npush\" would try to push to the upstream remote (which I don't even have\nwrite access to!).\n\n> As a note, before I started working on this feature, I don't realize\n> that there was such a thing as a push branch (i.e. something different from\n> the tracking branch). So I had the habit of checking out and pushing like\n> this:\n> \n>     git branch --set-upstream-to upstream/master\n>     git push origin $(git rev-parse --abbrev-ref HEAD)\n> \n> I worked really well for me. The only issue was missing the status info\n> from my own branch -- which is why I started writing this feature.\n\nYeah, though @{push} is usually not explicitly configured in the same\nway @{upstream} is, but rather a consequence of how push.default and\nremote.pushdefault interact. But it was added for exactly this kind of\ntriangular workflow. I sometimes will do stuff like:\n\n  git range-diff origin @{push} HEAD\n\nto compare two iterations of a branch if I know that I haven't pushed.\nIt is a bit of a cheat, because what I really mean is \"do a range-diff\nsince the last thing I sent to the list\". But if I have just been\nworking on a branch, and I haven't run an integration cycle since then,\nthen I know that the pushed version will match it.\n\nThere is also branch.*.pushRemote, but I have not found that useful (for\nmy triangular flow there is always a single repo to push to, not one per\nbranch).\n\n-Peff\n"},{"id":"533806","messageId":"20260114075309.32911-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260114023408.GA858378@coredump.intra.peff.net","subject":"Triangular workflow","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-14T07:53:09Z","receivedAt":"2026-01-14T07:53:14Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> I could (and in fact the script names the remote directly already,\n> because you can't pass refspecs without specifying the remote). But I do\n> occasionally push a single branch with a bare \"git push\". Usually this\n> is the integration branch, when I am trying to trigger CI manually\n> (e.g., when piling hacks on top in order to debug a CI failure ;) ).\n> \n> So even if I only do it infrequently, it feels weird that a bare \"git\n> push\" would try to push to the upstream remote (which I don't even have\n> write access to!).\n\nMaybe ’push.default=simple’ or ’push.default=nothing’ are better settings\nin your scenario. Then you get explicit pushing because no push branch gets\nset. And thus 'git status' reports not additinal status.\n\n> Yeah, though @{push} is usually not explicitly configured in the same\n> way @{upstream} is, but rather a consequence of how push.default and\n> remote.pushdefault interact. But it was added for exactly this kind of\n> triangular workflow. I sometimes will do stuff like:\n> \n>   git range-diff origin @{push} HEAD\n\nI imagine the same thing could be achieved with\n\n    origin/$(git rev-parse --abbrev-ref HEAD)\n\n\nHarald\n"},{"id":"533807","messageId":"20260114075936.40221-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"CALnO6CAnfmDfCFJY55J81TTeRCYqeHq=dVapeanF0d0Z_yg=tw@mail.gmail.com","subject":"Triangular workflow","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-14T07:59:36Z","receivedAt":"2026-01-14T07:59:42Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> My workflow is different from Peff's, but it is similar along at least\n> one line: it's really convenient to have \"git push\" with no further\n> arguments (only possibly flags) to push my branch to a remote mirror.\n\nWould you also like the status reporting to be off for your push branch?\nI asking because that's what Jeff is arguing for.\n\nI fully agree on the convenience of bare 'git push', I use it everyday\ntogether with these settings and commands for max convenience:\n\n    git config --global remote.pushDefault origin # As opposed to upstream\n    git config --global push.default current\n    git config --global pull.rebase true\n    git config --global rebase.autoStash true\n\n    git checkout $(gr | rg '^(upstream|origin)$' | tail -n1)/HEAD -b new_branch\n    git push\n    git status\n    git pull\n\n\nHarald\n"},{"id":"533825","messageId":"xmqqecnsgu1i.fsf@gitster.g","threadId":"64668","inReplyTo":"20260114023408.GA858378@coredump.intra.peff.net","subject":"Re: Triangular workflow","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-14T14:15:05Z","receivedAt":"2026-01-14T14:15:07Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> So even if I only do it infrequently, it feels weird that a bare \"git\n> push\" would try to push to the upstream remote (which I don't even have\n> write access to!).\n\nYes, exactly.  I was wondering where Harald's suggestion to swap the\ntwo remotes came from.\n\n> Yeah, though @{push} is usually not explicitly configured in the same\n> way @{upstream} is, but rather a consequence of how push.default and\n> remote.pushdefault interact. But it was added for exactly this kind of\n> triangular workflow. I sometimes will do stuff like:\n>\n>   git range-diff origin @{push} HEAD\n>\n> to compare two iterations of a branch if I know that I haven't pushed.\n> It is a bit of a cheat, because what I really mean is \"do a range-diff\n> since the last thing I sent to the list\". But if I have just been\n> working on a branch, and I haven't run an integration cycle since then,\n> then I know that the pushed version will match it.\n\nGreat suggestion.  It is fun to see that comparing notes among\npeople with different workflows brings out these gems ;-)\n\n> There is also branch.*.pushRemote, but I have not found that useful (for\n> my triangular flow there is always a single repo to push to, not one per\n> branch).\n"},{"id":"533839","messageId":"20260114162427.GA885771@coredump.intra.peff.net","threadId":"64668","inReplyTo":"20260114075309.32911-1-haraldnordgren@gmail.com","subject":"Re: Triangular workflow","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-14T16:24:27Z","receivedAt":"2026-01-14T16:31:11Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Jan 14, 2026 at 08:53:09AM +0100, Harald Nordgren wrote:\n\n> > I could (and in fact the script names the remote directly already,\n> > because you can't pass refspecs without specifying the remote). But I do\n> > occasionally push a single branch with a bare \"git push\". Usually this\n> > is the integration branch, when I am trying to trigger CI manually\n> > (e.g., when piling hacks on top in order to debug a CI failure ;) ).\n> > \n> > So even if I only do it infrequently, it feels weird that a bare \"git\n> > push\" would try to push to the upstream remote (which I don't even have\n> > write access to!).\n> \n> Maybe ’push.default=simple’ or ’push.default=nothing’ are better settings\n> in your scenario. Then you get explicit pushing because no push branch gets\n> set. And thus 'git status' reports not additinal status.\n\nIf I did that, then the occasional \"git push\" (without arguments) that I\ndo would fail. Likewise, @{push} would not be usable.\n\n> > Yeah, though @{push} is usually not explicitly configured in the same\n> > way @{upstream} is, but rather a consequence of how push.default and\n> > remote.pushdefault interact. But it was added for exactly this kind of\n> > triangular workflow. I sometimes will do stuff like:\n> > \n> >   git range-diff origin @{push} HEAD\n> \n> I imagine the same thing could be achieved with\n> \n>     origin/$(git rev-parse --abbrev-ref HEAD)\n\nSure, but:\n\n  1. It is a lot shorter to type @{push}. ;)\n\n  2. Using @{push} works everywhere, even on my non-triangular repos,\n     because it takes into account the push configuration. So it's a\n     much nicer muscle-memory to acquire.\n\n-Peff\n"},{"id":"533855","messageId":"20260114174845.61215-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260114162427.GA885771@coredump.intra.peff.net","subject":"Triangular workflow","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-14T17:48:45Z","receivedAt":"2026-01-14T17:48:49Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Sure, but:\n> \n>   1. It is a lot shorter to type @{push}. ;)\n> \n>   2. Using @{push} works everywhere, even on my non-triangular repos,\n>      because it takes into account the push configuration. So it's a\n>      much nicer muscle-memory to acquire.\n\nMakes sense, I’m all out of arguments here 😅 Please don’t take this the\nwrong way, but it reminds me of this XKCD: https://xkcd.com/1172/\n\nAs long as this is turned on by default then I’m mostly happy. Maybe JCH\nand others can weigh in on if this needs to be this configurable or always\non.\n\n\nHarald\n"},{"id":"533862","messageId":"xmqq8qe0f2iq.fsf@gitster.g","threadId":"64668","inReplyTo":"20260113214059.GC288857@coredump.intra.peff.net","subject":"Re: Triangular workflow","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-14T18:54:53Z","receivedAt":"2026-01-14T18:54:56Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> And having the extra output from \"git checkout\" is just extra noise for\n> me, especially because it is easy to see only the second message (which\n> looks just like the upstream ahead/behind message, of course) and get\n> confused. The first time I saw it I thought I had misconfigured\n> something with my branch.\n\nIt now is clear to me that this should be _optional_, so that those\nwho do really want extra output from the command should explicitly\nopt into the feature.  After all, any optional new feature that you\nmust opt into by definition cannot regress end user experience for\nthose who do not ;-)\n\nAt the same time, I suspect that extra comparison on top of what we\nalready give against the @{upstream} may not be limited to what\nHarald implemented (is it essentially the same as specyfing @{push},\nor something else?).  I wonder if we can come up with a flexible and\nextensible notation to specify what branch(es) to compare with, so\nthat we can use it as the value of this opt-in configuration\nvariable?  Something like\n\n\t[status] compareBranches = @{upstream} @{push}\n\nsignals that the current branch is compared against these two\nbranches, and not having the configuration (i.e., traditional\nbehaviour, which is left the default) would be equivalent to have\n\n\t[status] compareBranches = @{upstream}\n\nor something like that, perhaps?\n\nThanks.\n\n"},{"id":"533888","messageId":"20260114210158.GA1008851@coredump.intra.peff.net","threadId":"64668","inReplyTo":"20260114174845.61215-1-haraldnordgren@gmail.com","subject":"Re: Triangular workflow","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-14T21:01:58Z","receivedAt":"2026-01-14T21:02:00Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Jan 14, 2026 at 06:48:45PM +0100, Harald Nordgren wrote:\n\n> > Sure, but:\n> > \n> >   1. It is a lot shorter to type @{push}. ;)\n> > \n> >   2. Using @{push} works everywhere, even on my non-triangular repos,\n> >      because it takes into account the push configuration. So it's a\n> >      much nicer muscle-memory to acquire.\n> \n> Makes sense, I’m all out of arguments here 😅 Please don’t take this the\n> wrong way, but it reminds me of this XKCD: https://xkcd.com/1172/\n\nPerhaps if the person in that comic was the one who implemented @{push}\nin the first place. ;)\n\nThat said, I really don't think my use case is exotic. I've set up \"git\npush\" in the only way that makes sense for my triangular setup. I just\ndon't happen to use it that often, and I don't want to be reminded me\nunnecessarily of the relationship between each branch and its push\ndestination.\n\n> As long as this is turned on by default then I’m mostly happy. Maybe JCH\n> and others can weigh in on if this needs to be this configurable or always\n> on.\n\nIMHO not making it configurable is a show-stopper, because without that\nit leaves people who don't like the new behavior with no escape hatch.\n\nAs to the default for that config option, I don't have a strong opinion.\nI'd lean towards \"off\", as I do not find the feature that compelling\nmyself, and I don't recall ever hearing of it as a pain point for other\nGit users. But I also recognize that's based on vague vibes, and I think\nusers are generally not very savvy about setting up triangular workflows\nin the first place.\n\n-Peff\n"},{"id":"533889","messageId":"20260114211013.GB1008851@coredump.intra.peff.net","threadId":"64668","inReplyTo":"xmqq8qe0f2iq.fsf@gitster.g","subject":"Re: Triangular workflow","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-14T21:10:13Z","receivedAt":"2026-01-14T21:10:15Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Jan 14, 2026 at 10:54:53AM -0800, Junio C Hamano wrote:\n\n> Jeff King <peff@peff.net> writes:\n> \n> > And having the extra output from \"git checkout\" is just extra noise for\n> > me, especially because it is easy to see only the second message (which\n> > looks just like the upstream ahead/behind message, of course) and get\n> > confused. The first time I saw it I thought I had misconfigured\n> > something with my branch.\n> \n> It now is clear to me that this should be _optional_, so that those\n> who do really want extra output from the command should explicitly\n> opt into the feature.  After all, any optional new feature that you\n> must opt into by definition cannot regress end user experience for\n> those who do not ;-)\n\nTrue, but then it also cannot pleasantly surprise people who didn't\nrealize they wanted it.\n\nHaving your user experience regressed and then tweaking a config option\nto fix it is not too bad. The deciding factor to me is whether more\npeople will be pleasantly surprised or annoyed. ;) I don't have a strong\nsense there.\n\nAs a general principle, though, I think a reasonable path forward for\nany behavior change is:\n\n  1. Implement the new behavior, hidden behind a config option.\n\n  2. Wait a while to see how people like the new option, and shake out\n     any bugs.\n\n  3. If people like the option and are puzzled why it isn't the default,\n     then flip the default on.\n\nIn other words, let the utility of the feature be proven in practice by\npeople opting into it. There is a chicken-and-egg problem if they don't\nknow about it, but if it is truly solving a problem people have, then\nhopefully some of them would look for a solution and find it.\n\nEnd philosophical rambling. ;)\n\n> At the same time, I suspect that extra comparison on top of what we\n> already give against the @{upstream} may not be limited to what\n> Harald implemented (is it essentially the same as specyfing @{push},\n> or something else?).\n\nI haven't been following the feature closely, but my understanding is\nthat yes, it's basically \"also compare to @{push} if it is not the same\nas @{upstream}\".\n\n> I wonder if we can come up with a flexible and extensible notation to\n> specify what branch(es) to compare with, so that we can use it as the\n> value of this opt-in configuration variable?  Something like\n> \n> \t[status] compareBranches = @{upstream} @{push}\n> \n> signals that the current branch is compared against these two\n> branches, and not having the configuration (i.e., traditional\n> behaviour, which is left the default) would be equivalent to have\n> \n> \t[status] compareBranches = @{upstream}\n> \n> or something like that, perhaps?\n\nInteresting. That is more flexible, though I'm not sure how useful that\nflexibility is. I guess you could imagine putting in a static branch.\nE.g., if you base your branches off of \"master\", might you want to show\nahead/behind to \"maint\" or \"next\"? I have trouble imagining a workflow\nwhere I would want to do that often enough for git-status (and checkout)\nto do it automatically, though.\n\nBut assuming it suppresses duplicates, then yeah, this feels like a more\nflexible superset of the functionality.\n\n-Peff\n"},{"id":"533898","messageId":"BADF388F-91FB-42FC-8C12-69693E61F36C@gmail.com","threadId":"64668","inReplyTo":"20260114075936.40221-1-haraldnordgren@gmail.com","subject":"Re: Triangular workflow","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-14T21:38:29Z","receivedAt":"2026-01-14T21:38:45Z","isPatch":false,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 14 janv. 2026 à 02:59, Harald Nordgren <haraldnordgren@gmail.com> a écrit :\n> \n> ﻿\n>> My workflow is different from Peff's, but it is similar along at least\n>> one line: it's really convenient to have \"git push\" with no further\n>> arguments (only possibly flags) to push my branch to a remote mirror.\n> \n> Would you also like the status reporting to be off for your push branch?\n> I asking because that's what Jeff is arguing for.\n\nI’m somewhat indifferent (mildly for having it on by default), but I meant that you don’t want to suggest that in order to use your new feature you don’t want folks to have to break their existing workflows ;)\n"},{"id":"533897","messageId":"4F632782-4B1C-452A-A64B-CA223D2C16B0@gmail.com","threadId":"64668","inReplyTo":"20260114162427.GA885771@coredump.intra.peff.net","subject":"Re: Triangular workflow","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-14T21:38:33Z","receivedAt":"2026-01-14T21:38:47Z","isPatch":false,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 14 janv. 2026 à 11:31, Jeff King <peff@peff.net> a écrit :\n> \n>>> Yeah, though @{push} is usually not explicitly configured in the same\n>>> way @{upstream} is, but rather a consequence of how push.default and\n>>> remote.pushdefault interact. But it was added for exactly this kind of\n>>> triangular workflow. I sometimes will do stuff like:\n>>> git range-diff origin @{push} HEAD\n>> I imagine the same thing could be achieved with\n>>  origin/$(git rev-parse --abbrev-ref HEAD)\n> \n> Sure, but:\n> \n> 1. It is a lot shorter to type @{push}. ;)\n> \n> 2. Using @{push} works everywhere, even on my non-triangular repos,\n\nJust so I’m clear, this is only with push.default=current, right? I could never make @{push} work otherwise."},{"id":"533899","messageId":"56D0BADB-43C8-4E93-A871-6E6B11B59DCF@gmail.com","threadId":"64668","inReplyTo":"20260114211013.GB1008851@coredump.intra.peff.net","subject":"Re: Triangular workflow","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-14T21:38:37Z","receivedAt":"2026-01-14T21:38:50Z","isPatch":false,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 14 janv. 2026 à 16:11, Jeff King <peff@peff.net> a écrit :\n> \n> ﻿On Wed, Jan 14, 2026 at 10:54:53AM -0800, Junio C Hamano wrote:\n> \n>> Jeff King <peff@peff.net> writes:\n>>> And having the extra output from \"git checkout\" is just extra noise for\n>>> me, especially because it is easy to see only the second message (which\n>>> looks just like the upstream ahead/behind message, of course) and get\n>>> confused. The first time I saw it I thought I had misconfigured\n>>> something with my branch.\n>> It now is clear to me that this should be _optional_, so that those\n>> who do really want extra output from the command should explicitly\n>> opt into the feature.  After all, any optional new feature that you\n>> must opt into by definition cannot regress end user experience for\n>> those who do not ;-)\n> \n> True, but then it also cannot pleasantly surprise people who didn't\n> realize they wanted it.\n> \n> Having your user experience regressed and then tweaking a config option\n> to fix it is not too bad. The deciding factor to me is whether more\n> people will be pleasantly surprised or annoyed. ;) I don't have a strong\n> sense there.\n> \n> As a general principle, though, I think a reasonable path forward for\n> any behavior change is:\n> \n> 1. Implement the new behavior, hidden behind a config option.\n> \n> 2. Wait a while to see how people like the new option, and shake out\n>   any bugs.\n> \n> 3. If people like the option and are puzzled why it isn't the default,\n>   then flip the default on.\n> \n> In other words, let the utility of the feature be proven in practice by\n> people opting into it. There is a chicken-and-egg problem if they don't\n> know about it, but if it is truly solving a problem people have, then\n> hopefully some of them would look for a solution and find it.\n> \n> End philosophical rambling. ;)\n\nAgreed generally, but the chicken-egg goes 2 layers deep here due to triangular workflows ;)\n\nI favor something similar to what Junio described but also including @{push} by default (and ignoring it if non-existent), so that folks discovering triangular workflows for the first time are easily able to see what is happening.\n\nUs ”already triangular” squares are probably well-versed enough in Git to find and tweak the new feature if desired.\n\nIdk though. I think more folks at work should try triangular flows, so I’m biased :p"},{"id":"533905","messageId":"20260114221716.GA1013308@coredump.intra.peff.net","threadId":"64668","inReplyTo":"4F632782-4B1C-452A-A64B-CA223D2C16B0@gmail.com","subject":"Re: Triangular workflow","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-14T22:17:16Z","receivedAt":"2026-01-14T22:17:20Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Jan 14, 2026 at 04:38:33PM -0500, Ben Knoble wrote:\n\n> > Le 14 janv. 2026 à 11:31, Jeff King <peff@peff.net> a écrit :\n> > \n> >>> Yeah, though @{push} is usually not explicitly configured in the same\n> >>> way @{upstream} is, but rather a consequence of how push.default and\n> >>> remote.pushdefault interact. But it was added for exactly this kind of\n> >>> triangular workflow. I sometimes will do stuff like:\n> >>> git range-diff origin @{push} HEAD\n> >> I imagine the same thing could be achieved with\n> >>  origin/$(git rev-parse --abbrev-ref HEAD)\n> > \n> > Sure, but:\n> > \n> > 1. It is a lot shorter to type @{push}. ;)\n> > \n> > 2. Using @{push} works everywhere, even on my non-triangular repos,\n> \n> Just so I’m clear, this is only with push.default=current, right? I could never make @{push} work otherwise.\n\nI always use push.default=current, though I think @{push} should work\nwith other modes. E.g., with this setup:\n\n  git checkout -b foo\n  git clone . tmp\n  cd tmp\n\n  # for the sake of simplicity, our triangle goes to the same place ;)\n  git remote add triangle ..\n  git fetch triangle\n  git config remote.pushdefault triangle\n\nthen doing:\n\n  git -c push.default=current rev-parse --symbolic-full-name @{push}\n\nand:\n\n  git -c push.default=matching rev-parse --symbolic-full-name @{push}\n\nshould both point to refs/remotes/triangle/foo. Using \"simple\" will not\nwork, because it demands that the upstream and the push destination are\nthe same (so it doesn't really make sense in a triangular flow at all).\n\nBut in a non-triangular flow, it will happily point @{push} to the same\nas @{upstream}. I use a triangular flow for git.git, but most of my\nother repos are just personal projects, and I push/fetch from a single\ncentral source.\n\n-Peff\n"},{"id":"533912","messageId":"20260114230800.56885-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"56D0BADB-43C8-4E93-A871-6E6B11B59DCF@gmail.com","subject":"Triangular workflow","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-14T23:08:00Z","receivedAt":"2026-01-14T23:08:04Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Agreed generally, but the chicken-egg goes 2 layers deep here due to triangular workflows ;)\n> \n> I favor something similar to what Junio described but also including @{push} by default (and ignoring it if non-existent), so that folks discovering > triangular workflows for the first time are easily able to see what is happening.\n> \n> Us ”already triangular” squares are probably well-versed enough in Git to find and tweak the new feature if desired.\n> \n> Idk though. I think more folks at work should try triangular flows, so I’m biased :p\n\nBen has an excellent point about triangular workflows, and Jeff of course,\nyour philosophical instincts seem correct to me, interesting to read!\n\nI would think that most people don't have separate tracking and push\nbranches to begin with. So it's not a lot of people would be bothered by\nhaving this be on by default. The people who know about triangular\nworkflows will also be able to find how to turn this off.\n\n\nHarald\n"},{"id":"533913","messageId":"20260114231202.61271-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqq8qe0f2iq.fsf@gitster.g","subject":"Triangular workflow","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-14T23:12:02Z","receivedAt":"2026-01-14T23:12:05Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> I wonder if we can come up with a flexible and\n> extensible notation to specify what branch(es) to compare with, so\n> that we can use it as the value of this opt-in configuration\n> variable?  Something like\n> \n>         [status] compareBranches = @{upstream} @{push}\n\nIf we go with that, then it becomes trickier code-wise to show push/pull\nadvice for the correct branches. But not impossible since we can check if\nbranch is the same as @{upstream} or @{push}.\n\nPhilosophically, two main git commands are 'git push' and 'git pull'. So it\nmakes perfect sense to me to signal that those two are special, and not\nallow other compareBranches.\n\n\nHarald\n"},{"id":"533914","messageId":"xmqqecnrepox.fsf@gitster.g","threadId":"64668","inReplyTo":"20260114231202.61271-1-haraldnordgren@gmail.com","subject":"Re: Triangular workflow","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-14T23:31:58Z","receivedAt":"2026-01-14T23:32:05Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> I wonder if we can come up with a flexible and\n>> extensible notation to specify what branch(es) to compare with, so\n>> that we can use it as the value of this opt-in configuration\n>> variable?  Something like\n>> \n>>         [status] compareBranches = @{upstream} @{push}\n>\n> If we go with that, then it becomes trickier code-wise to show push/pull\n> advice for the correct branches. But not impossible since we can check if\n> branch is the same as @{upstream} or @{push}.\n>\n> Philosophically, two main git commands are 'git push' and 'git pull'. So it\n> makes perfect sense to me to signal that those two are special, and not\n> allow other compareBranches.\n\nYou are falling into the same trap as what the folks who designed\nthe original ahead/behind comparison did by limiting yourself to\npush and pull.  They said object transfer always interacts with the\n@{upstream}, hence only comparison with that is sufficient and it\nmakes sense not to allow comparing with anything else.\n\nUntil you started wishing that you want to also compare with\n@{push}, that is ;-).\n\nA static point that has nothing to do with pushing and pulling\n(e.g., \"the latest official release tag\") may be something useful to\ncompare with in certain workflows (presumably workflow of the\nmaintainer type), so limiting our sights to object transfer like\npush and pull is likely to be a mistake.\n"},{"id":"533925","messageId":"0037e6b9-e57d-4cd3-b156-c8cbf439347a@gmail.com","threadId":"64668","inReplyTo":"20260113170321.GA265671@coredump.intra.peff.net","subject":"Re: [PATCH v25 2/2] status: show comparison with push remote tracking branch","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-01-15T10:31:41Z","receivedAt":"2026-01-15T10:31:44Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 13/01/2026 17:03, Jeff King wrote:\n> \n> Can we make this configurable?\n> \n> I build my daily driver off of the 'jch' branch, which now includes this\n> series, and I've found that for my triangular workflow the ahead/behind\n> for the push branch is just useless noise. I treat my push destination\n> like a mirror, where I always just push up everything at the end of the\n> day.\n\nThis is a slight tangent but if we're taking about making things \nconfigurable I've never found the actual values of the ahead/behind \nnumbers particularly useful. In a triangular workflow all I'm really \ninterested in is whether (a) my local branch is behind the upstream \nbranch and if so is it going to be a pig to rebase it, and (b) my local \nbranch matches the push branch (i.e. do I need to run \"git push\"). I \ndon't really care exactly how many commits ahead or behind the local \nbranch is, the counts are just distracting and don't really answer the \n\"is it going to be a pig to rebase\" question - the only way to answer \nthat is to try it and see.\n\nSo as well as configuring whether we show the comparison the push branch \nit would be nice to be able to configure a simpler output as well. That \ndoes not need to be part of this series but perhaps we should design the \nconfiguration to be extendable.\n\nThanks\n\nPhillip\n\n\n> I know that the output can be disabled with status.aheadbehind, but:\n> \n>    1. I noticed this first via \"git checkout\", which does not have such a\n>       flag (AFAIK).\n> \n>    2. That flag would also disable the upstream branch ahead/behind\n>       output, which I do find useful.\n> \n> -Peff\n> \n\n"},{"id":"533956","messageId":"xmqq5x93dmhf.fsf@gitster.g","threadId":"64668","inReplyTo":"0037e6b9-e57d-4cd3-b156-c8cbf439347a@gmail.com","subject":"Re: [PATCH v25 2/2] status: show comparison with push remote tracking branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-15T13:38:52Z","receivedAt":"2026-01-15T13:38:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> So as well as configuring whether we show the comparison the push branch \n> it would be nice to be able to configure a simpler output as well. That \n> does not need to be part of this series but perhaps we should design the \n> configuration to be extendable.\n\nSo, there are orthogonal axes that might benefit from\nconfigurabiliity.\n\nWhat other branch to compare (e.g., @{upstream}, @{push}, something\nelse?) and how detailed comparison we want (e.g., only tell me when\nwe are ahead with just a single bit, give me ahead/behind count,\nonly tell me when we are behind with just a single bit, etc.).\n\nThere may be other interesting axes people may be able to come up\nwith in the future.  We do not have to be exhaustive on values on\nall axes on the first day, but it would be nice to have a fairly\ncomplete coverage of what axes matter to users, and it is a great\nsuggestion that the level of details can be one.  Thanks.\n"},{"id":"533968","messageId":"CALnO6CD3Zg_69cPPn+Rh4EKDF-tDQSLJ42YSMJz308=1C8ZQHg@mail.gmail.com","threadId":"64668","inReplyTo":"20260114221716.GA1013308@coredump.intra.peff.net","subject":"Re: Triangular workflow","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-15T16:17:45Z","receivedAt":"2026-01-15T16:17:56Z","isPatch":false,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Wed, Jan 14, 2026 at 5:17 PM Jeff King <peff@peff.net> wrote:\n>\n> On Wed, Jan 14, 2026 at 04:38:33PM -0500, Ben Knoble wrote:\n>\n> > > Le 14 janv. 2026 à 11:31, Jeff King <peff@peff.net> a écrit :\n> > >\n> > >>> Yeah, though @{push} is usually not explicitly configured in the same\n> > >>> way @{upstream} is, but rather a consequence of how push.default and\n> > >>> remote.pushdefault interact. But it was added for exactly this kind of\n> > >>> triangular workflow. I sometimes will do stuff like:\n> > >>> git range-diff origin @{push} HEAD\n> > >> I imagine the same thing could be achieved with\n> > >>  origin/$(git rev-parse --abbrev-ref HEAD)\n> > >\n> > > Sure, but:\n> > >\n> > > 1. It is a lot shorter to type @{push}. ;)\n> > >\n> > > 2. Using @{push} works everywhere, even on my non-triangular repos,\n> >\n> > Just so I’m clear, this is only with push.default=current, right? I could never make @{push} work otherwise.\n>\n> I always use push.default=current, though I think @{push} should work\n> with other modes. E.g., with this setup:\n>\n>   git checkout -b foo\n>   git clone . tmp\n>   cd tmp\n>\n>   # for the sake of simplicity, our triangle goes to the same place ;)\n>   git remote add triangle ..\n>   git fetch triangle\n>   git config remote.pushdefault triangle\n>\n> then doing:\n>\n>   git -c push.default=current rev-parse --symbolic-full-name @{push}\n>\n> and:\n>\n>   git -c push.default=matching rev-parse --symbolic-full-name @{push}\n>\n> should both point to refs/remotes/triangle/foo. Using \"simple\" will not\n> work, because it demands that the upstream and the push destination are\n> the same (so it doesn't really make sense in a triangular flow at all).\n\nGotcha. Yeah, simple (the default) doesn't work. I suppose upstream\nmight, too. (I also have push.default=current globally, was just\nwondering about what the minimum was to enabled triangular workflows;\nsee <https://lore.kernel.org/git/CALnO6CAUSU-Pq_r-WYm3o0to6H8MdqiYOuoKaRfL1PTt30VaoQ@mail.gmail.com/>.)\n\n> But in a non-triangular flow, it will happily point @{push} to the same\n> as @{upstream}. I use a triangular flow for git.git, but most of my\n> other repos are just personal projects, and I push/fetch from a single\n> central source.\n>\n> -Peff\n\nDitto, yeah (_sometimes_ I actually do the triangle origin/main, local\nbranch, origin/branch, so I almost always set upstream as origin/main\nanyways). Cool and thanks!\n\n-- \nD. Ben Knoble\n"},{"id":"533981","messageId":"20260115195335.GA1053259@coredump.intra.peff.net","threadId":"64668","inReplyTo":"0037e6b9-e57d-4cd3-b156-c8cbf439347a@gmail.com","subject":"Re: [PATCH v25 2/2] status: show comparison with push remote tracking branch","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-15T19:53:35Z","receivedAt":"2026-01-15T19:53:42Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Jan 15, 2026 at 10:31:41AM +0000, Phillip Wood wrote:\n\n> On 13/01/2026 17:03, Jeff King wrote:\n> > \n> > Can we make this configurable?\n> > \n> > I build my daily driver off of the 'jch' branch, which now includes this\n> > series, and I've found that for my triangular workflow the ahead/behind\n> > for the push branch is just useless noise. I treat my push destination\n> > like a mirror, where I always just push up everything at the end of the\n> > day.\n> \n> This is a slight tangent but if we're taking about making things\n> configurable I've never found the actual values of the ahead/behind numbers\n> particularly useful. In a triangular workflow all I'm really interested in\n> is whether (a) my local branch is behind the upstream branch and if so is it\n> going to be a pig to rebase it, and (b) my local branch matches the push\n> branch (i.e. do I need to run \"git push\"). I don't really care exactly how\n> many commits ahead or behind the local branch is, the counts are just\n> distracting and don't really answer the \"is it going to be a pig to rebase\"\n> question - the only way to answer that is to try it and see.\n\nI mostly feel the same way. The only time I recall thinking the numbers\nwere important is when they are wildly out of place. You \"git checkout\"\na branch and it is 1500 commits ahead/behind or something, and you know\nyou've screwed up setting the tracking branch somehow. ;)\n\n> So as well as configuring whether we show the comparison the push branch it\n> would be nice to be able to configure a simpler output as well. That does\n> not need to be part of this series but perhaps we should design the\n> configuration to be extendable.\n\nYeah, I agree with all of that.\n\n-Peff\n"},{"id":"534152","messageId":"20260118195845.16908-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqq8qe0f2iq.fsf@gitster.g","subject":"Triangular workflow","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-18T19:58:45Z","receivedAt":"2026-01-18T19:58:49Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> > And having the extra output from \"git checkout\" is just extra noise for\n> > me, especially because it is easy to see only the second message (which\n> > looks just like the upstream ahead/behind message, of course) and get\n> > confused. The first time I saw it I thought I had misconfigured\n> > something with my branch.\n> \n> It now is clear to me that this should be _optional_, so that those\n> who do really want extra output from the command should explicitly\n> opt into the feature.  After all, any optional new feature that you\n> must opt into by definition cannot regress end user experience for\n> those who do not ;-)\n> \n> At the same time, I suspect that extra comparison on top of what we\n> already give against the @{upstream} may not be limited to what\n> Harald implemented (is it essentially the same as specyfing @{push},\n> or something else?).  I wonder if we can come up with a flexible and\n> extensible notation to specify what branch(es) to compare with, so\n> that we can use it as the value of this opt-in configuration\n> variable?  Something like\n> \n> \t[status] compareBranches = @{upstream} @{push}\n> \n> signals that the current branch is compared against these two\n> branches, and not having the configuration (i.e., traditional\n> behaviour, which is left the default) would be equivalent to have\n> \n> \t[status] compareBranches = @{upstream}\n> \n> or something like that, perhaps?\n\nI'm implemented this now. Please take a look at the latest patch!\n\nBut there seems to be a memory leak that I can't figure out after spending\nsome hours running the CI over and over.\n\nHarald\n"},{"id":"534153","messageId":"pull.2138.v26.git.git.1768766353.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v25.git.git.1768306316.gitgitgadget@gmail.com","subject":"[PATCH v26 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-18T19:59:11Z","receivedAt":"2026-01-18T19:59:15Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im cc:\nJeff King peff@peff.net\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: add status.compareBranches config for multiple branch\n    comparisons\n\n Documentation/config/status.adoc |  20 ++\n remote.c                         | 187 ++++++++++++++----\n t/t6040-tracking-info.sh         | 322 +++++++++++++++++++++++++++++++\n 3 files changed, 492 insertions(+), 37 deletions(-)\n\n\nbase-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v26\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v26\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v25:\n\n 1:  fd05c7b778 = 1:  27a46f8d9c refactor format_branch_comparison in preparation\n 2:  fa744efc59 ! 2:  caa761f615 status: show comparison with push remote tracking branch\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    status: show comparison with push remote tracking branch\n     +    status: add status.compareBranches config for multiple branch comparisons\n      \n     -    \"git status\" on a branch that follows a remote branch compares\n     -    commits on the current branch and the remote-tracking branch it\n     -    builds upon, to show \"ahead\", \"behind\", or \"diverged\" status.\n     +    Add a new configuration variable `status.compareBranches` that allows\n     +    users to specify a space-separated list of branches to compare against\n     +    the current branch in `git status` output.\n      \n     -    When working on a feature branch that tracks a remote feature branch,\n     -    but you also want to track progress relative to the push destination\n     -    tracking branch (which may differ from the upstream branch), git status\n     -    now shows an additional comparison.\n     +    Each branch in the list can be:\n     +    - A remote-tracking branch name (e.g., `origin/main`)\n     +    - The special reference `@{upstream}` for the tracking branch\n     +    - The special reference `@{push}` for the push destination\n      \n     -    When the upstream tracking branch differs from the push destination\n     -    tracking branch, git status shows both the comparison with the upstream\n     -    tracking branch (as before) and an additional comparison with the push\n     -    destination tracking branch. The push branch comparison appears on a\n     -    separate line after the upstream branch status, using the same format.\n     +    When not configured, the default behavior is equivalent to setting\n     +    `status.compareBranches = @{upstream}`, preserving backward compatibility.\n      \n     -    Example output when tracking origin/main but push destination is\n     -    origin/feature:\n     -        On branch feature\n     -        Your branch and 'origin/main' have diverged,\n     -        and have 3 and 1 different commits each, respectively.\n     -          (use \"git pull\" if you want to integrate the remote branch with yours)\n     +    The advice messages shown are context-aware:\n     +    - \"git pull\" advice is shown only when comparing against @{upstream}\n     +    - \"git push\" advice is shown only when comparing against @{push}\n     +    - Divergence advice is shown for upstream branch comparisons\n      \n     -        Your branch is ahead of 'origin/feature' by 1 commit.\n     -          (use \"git push\" to publish your local commits)\n     +    This is useful for triangular workflows where the upstream tracking\n     +    branch differs from the push destination, allowing users to see their\n     +    status relative to both branches at once.\n      \n     -    The comparison is only shown when the push destination tracking branch\n     -    differs from the upstream tracking branch, even if they are on the same\n     -    remote.\n     +    Example configuration:\n     +        [status]\n     +            compareBranches = @{upstream} @{push}\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     + ## Documentation/config/status.adoc ##\n     +@@ Documentation/config/status.adoc: status.aheadBehind::\n     + \t`--no-ahead-behind` by default in linkgit:git-status[1] for\n     + \tnon-porcelain status formats.  Defaults to true.\n     + \n     ++status.compareBranches::\n     ++\tA space-separated list of branches to compare the current branch\n     ++\tagainst in linkgit:git-status[1]. Each branch specification can be\n     ++\ta remote-tracking branch name (e.g. `origin/main`), or a special\n     ++\treference like `@{upstream}` or `@{push}`. For each branch in the\n     ++\tlist, git status shows whether the current branch is ahead, behind,\n     ++\tor has diverged from that branch.\n     +++\n     ++If not set, the default behavior is equivalent to `@{upstream}`, which\n     ++compares against the configured upstream tracking branch.\n     +++\n     ++Example:\n     +++\n     ++----\n     ++[status]\n     ++\tcompareBranches = origin/main origin/develop\n     ++----\n     +++\n     ++This would show comparisons against both `origin/main` and `origin/develop`.\n     ++\n     + status.displayCommentPrefix::\n     + \tIf set to true, linkgit:git-status[1] will insert a comment\n     + \tprefix before each output line (starting with\n     +\n       ## remote.c ##\n      @@\n       \n     @@ remote.c\n       struct counted_string {\n       \tsize_t len;\n       \tconst char *s;\n     +@@ remote.c: static void branch_release(struct branch *branch)\n     + \tfree((char *)branch->refname);\n     + \tfree(branch->remote_name);\n     + \tfree(branch->pushremote_name);\n     ++\tfree((char *)branch->push_tracking_ref);\n     + \tmerge_clear(branch);\n     + }\n     + \n      @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n       \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n       }\n       \n     -+static char *get_remote_push_branch(struct branch *branch)\n     ++static char *resolve_compare_branch(struct branch *branch, const char *name)\n      +{\n     -+\tstruct remote *remote;\n     -+\tconst char *push_remote;\n     -+\tchar *push_dst = NULL;\n     -+\tchar *tracking_ref;\n     -+\tconst char *resolved;\n     ++\tstruct strbuf buf = STRBUF_INIT;\n     ++\tconst char *resolved = NULL;\n      +\tchar *ret;\n      +\n     -+\tif (!branch)\n     -+\t\treturn NULL;\n     -+\n     -+\tpush_remote = pushremote_for_branch(branch, NULL);\n     -+\tif (!push_remote)\n     ++\tif (!branch || !name)\n      +\t\treturn NULL;\n      +\n     -+\tremote = remotes_remote_get(the_repository, push_remote);\n     -+\tif (!remote)\n     -+\t\treturn NULL;\n     ++\tif (!strcasecmp(name, \"@{upstream}\") || !strcasecmp(name, \"@{u}\"))\n     ++\t\tresolved = branch_get_upstream(branch, NULL);\n     ++\telse if (!strcasecmp(name, \"@{push}\"))\n     ++\t\tresolved = branch_get_push(branch, NULL);\n      +\n     -+\tpush_dst = remote_ref_for_branch(branch, 1);\n     -+\tif (!push_dst) {\n     -+\t\tif (remote->push.nr)\n     -+\t\t\treturn NULL;\n     -+\t\tpush_dst = xstrdup(branch->refname);\n     -+\t}\n     -+\n     -+\ttracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);\n     -+\tfree(push_dst);\n     -+\n     -+\tif (!tracking_ref)\n     -+\t\treturn NULL;\n     ++\tif (resolved)\n     ++\t\treturn xstrdup(resolved);\n      +\n     ++\tstrbuf_addf(&buf, \"refs/remotes/%s\", name);\n      +\tresolved = refs_resolve_ref_unsafe(\n      +\t\tget_main_ref_store(the_repository),\n     -+\t\ttracking_ref,\n     ++\t\tbuf.buf,\n      +\t\tRESOLVE_REF_READING,\n      +\t\tNULL, NULL);\n     -+\n     -+\tif (!resolved) {\n     -+\t\tfree(tracking_ref);\n     -+\t\treturn NULL;\n     ++\tif (resolved) {\n     ++\t\tret = xstrdup(resolved);\n     ++\t\tstrbuf_release(&buf);\n     ++\t\treturn ret;\n      +\t}\n      +\n     -+\tret = xstrdup(resolved);\n     -+\tfree(tracking_ref);\n     -+\treturn ret;\n     ++\tstrbuf_release(&buf);\n     ++\treturn NULL;\n      +}\n      +\n       static void format_branch_comparison(struct strbuf *sb,\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n       \t}\n      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \tconst char *full_base;\n     - \tchar *base;\n     - \tint upstream_is_gone = 0;\n     -+\tunsigned base_branch_flags = ENABLE_ADVICE_PULL | ENABLE_ADVICE_PUSH;\n     -+\tint push_ours, push_theirs, push_cmp_fetch;\n     -+\tchar *full_push;\n     -+\tchar *push = NULL;\n     -+\tunsigned push_branch_flags = 0;\n     + \t\t\t enum ahead_behind_flags abf,\n     + \t\t\t int show_divergence_advice)\n     + {\n     +-\tint ours, theirs, cmp_fetch;\n     +-\tconst char *full_base;\n     +-\tchar *base;\n     +-\tint upstream_is_gone = 0;\n     +-\n     +-\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n     +-\tif (cmp_fetch < 0) {\n     +-\t\tif (!full_base)\n     +-\t\t\treturn 0;\n     +-\t\tupstream_is_gone = 1;\n     ++\tchar *compare_branches_config = NULL;\n     ++\tstruct string_list compare_branches = STRING_LIST_INIT_DUP;\n     ++\tstruct string_list_item *item;\n     ++\tint reported = 0;\n     ++\tsize_t i;\n     ++\tconst char *upstream_ref;\n     ++\tconst char *push_ref;\n     ++\n     ++\trepo_config_get_string(the_repository, \"status.comparebranches\",\n     ++\t\t\t       &compare_branches_config);\n     ++\n     ++\tif (compare_branches_config) {\n     ++\t\tstring_list_split(&compare_branches, compare_branches_config,\n     ++\t\t\t\t  \" \", -1);\n     ++\t\tstring_list_remove_empty_items(&compare_branches, 0);\n     ++\t} else {\n     ++\t\tstring_list_append(&compare_branches, \"@{upstream}\");\n     + \t}\n       \n     - \tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n     - \tif (cmp_fetch < 0) {\n     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n     - \t\t\t\t\t    full_base, 0);\n     +-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n     +-\t\t\t\t\t    full_base, 0);\n     ++\tupstream_ref = branch_get_upstream(branch, NULL);\n     ++\tpush_ref = branch_get_push(branch, NULL);\n       \n     -+\tfull_push = get_remote_push_branch(branch);\n     -+\tif (full_push) {\n     -+\t\tpush = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n     -+\t\t\t\t\t\t     full_push, 0);\n     -+\t\tif (push && base && strcmp(base, push)) {\n     -+\t\t\tpush_cmp_fetch = stat_branch_pair(branch->refname, full_push,\n     -+\t\t\t\t\t\t   &push_ours, &push_theirs, abf);\n     -+\t\t\tif (push_cmp_fetch >= 0) {\n     -+\t\t\t\tbase_branch_flags = ENABLE_ADVICE_PULL;\n     -+\t\t\t\tpush_branch_flags = ENABLE_ADVICE_PUSH;\n     +-\tif (upstream_is_gone) {\n     +-\t\tstrbuf_addf(sb,\n     +-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n     +-\t\t\tbase);\n     +-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     +-\t\t\tstrbuf_addstr(sb,\n     +-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n     +-\t} else {\n     +-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n     ++\tfor (i = 0; i < compare_branches.nr; i++) {\n     ++\t\tchar *full_ref;\n     ++\t\tchar *short_ref;\n     ++\t\tint ours, theirs, cmp;\n     ++\t\tint is_upstream, is_push;\n     ++\t\tunsigned flags = 0;\n     ++\n     ++\t\titem = &compare_branches.items[i];\n     ++\t\tfull_ref = resolve_compare_branch(branch, item->string);\n     ++\t\tif (!full_ref)\n     ++\t\t\tcontinue;\n     ++\n     ++\t\tshort_ref = refs_shorten_unambiguous_ref(\n     ++\t\t\tget_main_ref_store(the_repository), full_ref, 0);\n     ++\n     ++\t\tis_upstream = upstream_ref && !strcmp(full_ref, upstream_ref);\n     ++\t\tis_push = push_ref && !strcmp(full_ref, push_ref);\n     ++\n     ++\t\tif (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))\n     ++\t\t\tis_push = 1;\n     ++\n     ++\t\tcmp = stat_branch_pair(branch->refname, full_ref,\n     ++\t\t\t\t       &ours, &theirs, abf);\n     ++\n     ++\t\tif (cmp < 0) {\n     ++\t\t\tif (is_upstream) {\n     ++\t\t\t\tstrbuf_addf(sb,\n     ++\t\t\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n     ++\t\t\t\t\tshort_ref);\n     ++\t\t\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     ++\t\t\t\t\tstrbuf_addstr(sb,\n     ++\t\t\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n     ++\t\t\t\treported = 1;\n      +\t\t\t}\n     ++\t\t\tfree(full_ref);\n     ++\t\t\tfree(short_ref);\n     ++\t\t\tcontinue;\n      +\t\t}\n     -+\t}\n      +\n     - \tif (upstream_is_gone) {\n     - \t\tstrbuf_addf(sb,\n     - \t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n     - \t\t\tstrbuf_addstr(sb,\n     - \t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n     - \t} else {\n     --\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n     -+\t\tif (show_divergence_advice)\n     -+\t\t\tbase_branch_flags |= ENABLE_ADVICE_DIVERGENCE;\n     -+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,\n     -+\t\t\t\t\t base_branch_flags);\n     -+\t}\n     -+\n     -+\tif (push_branch_flags & ENABLE_ADVICE_PUSH) {\n     -+\t\tstrbuf_addstr(sb, \"\\n\");\n     -+\t\tformat_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,\n     -+\t\t\t\t\t push_branch_flags);\n     ++\t\tif (reported)\n     ++\t\t\tstrbuf_addstr(sb, \"\\n\");\n     ++\n     ++\t\tif (is_upstream)\n     ++\t\t\tflags |= ENABLE_ADVICE_PULL;\n     ++\t\tif (is_push)\n     ++\t\t\tflags |= ENABLE_ADVICE_PUSH;\n     ++\t\tif (show_divergence_advice && is_upstream)\n     ++\t\t\tflags |= ENABLE_ADVICE_DIVERGENCE;\n     ++\t\tformat_branch_comparison(sb, !cmp, ours, theirs, short_ref,\n     ++\t\t\t\t\t abf, flags);\n     ++\t\treported = 1;\n     ++\n     ++\t\tfree(full_ref);\n     ++\t\tfree(short_ref);\n       \t}\n       \n     - \tfree(base);\n     -+\tfree(full_push);\n     -+\tfree(push);\n     - \treturn 1;\n     +-\tfree(base);\n     +-\treturn 1;\n     ++\tstring_list_clear(&compare_branches, 0);\n     ++\tfree(compare_branches_config);\n     ++\treturn reported;\n       }\n       \n     + static int one_local_ref(const struct reference *ref, void *cb_data)\n      \n       ## t/t6040-tracking-info.sh ##\n      @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status shows ahead of both origin/main and feature branch' '\n     ++test_expect_success 'setup for compareBranches tests' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit config push.default current &&\n     ++\t\tgit config status.compareBranches \"@{upstream} @{push}\"\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout -b feature2 origin/main &&\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '\n     ++test_expect_success 'checkout with status.compareBranches shows both branches' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout feature2 >../actual\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t)\n      +'\n      +\n     -+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '\n     ++test_expect_success 'status.compareBranches shows diverged and ahead' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout feature4 &&\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '\n     ++test_expect_success 'status --no-ahead-behind with status.compareBranches' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout feature4 &&\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t)\n      +'\n      +\n     -+test_expect_success 'status with upstream remote and push.default set to origin' '\n     ++test_expect_success 'status.compareBranches with upstream and origin remotes' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout -b feature5 upstream/main &&\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '\n     ++test_expect_success 'status.compareBranches with upstream and origin remotes multiple compare branches' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout -b feature6 upstream/main &&\n     ++\t\tgit push origin &&\n     ++\t\tadvance work &&\n     ++\t\tgit -c status.compareBranches=\"upstream/main origin/feature6 origin/feature5\" status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature6\n     ++\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n     ++\n     ++\tYour branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.\n     ++\t  (use \"git push\" to publish your local commits)\n     ++\n     ++\tYour branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'status.compareBranches with diverged push branch' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout -b feature7 upstream/main &&\n      +\t\tadvance work &&\n      +\t\tgit push origin &&\n      +\t\tgit reset --hard upstream/main &&\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t\tgit status >../actual\n      +\t) &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature6\n     ++\tOn branch feature7\n      +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n      +\n     -+\tYour branch and ${SQ}origin/feature6${SQ} have diverged,\n     ++\tYour branch and ${SQ}origin/feature7${SQ} have diverged,\n      +\tand have 1 and 1 different commits each, respectively.\n      +\n      +\tnothing to commit, working tree clean\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status with upstream remote and push branch up to date' '\n     ++test_expect_success 'status.compareBranches shows up to date branches' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout -b feature7 upstream/main &&\n     ++\t\tgit checkout -b feature8 upstream/main &&\n      +\t\tgit push origin &&\n      +\t\tgit status >../actual\n      +\t) &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature7\n     ++\tOn branch feature8\n      +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n      +\n     -+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n     ++\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n      +\n      +\tnothing to commit, working tree clean\n      +\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '\n     ++test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout feature7 &&\n     ++\t\tgit checkout feature8 &&\n      +\t\tgit push origin &&\n      +\t\tgit status --no-ahead-behind >../actual\n      +\t) &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature7\n     ++\tOn branch feature8\n      +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n      +\n     -+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n     ++\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n      +\n      +\tnothing to commit, working tree clean\n      +\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'checkout shows push branch up to date' '\n     ++test_expect_success 'checkout with status.compareBranches shows up to date' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout feature7 >../actual\n     ++\t\tgit checkout feature8 >../actual\n      +\t) &&\n      +\tcat >expect <<-EOF &&\n      +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n      +\n     -+\tYour branch is up to date with ${SQ}origin/feature7${SQ}.\n     ++\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n      +\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status with upstream ahead and push branch up to date' '\n     ++test_expect_success 'status.compareBranches with upstream behind and push up to date' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout -b ahead upstream/main &&\n      +\t\tadvance work &&\n      +\t\tgit push upstream HEAD &&\n     -+\t\tgit checkout -b feature8 upstream/main &&\n     ++\t\tgit checkout -b feature9 upstream/main &&\n      +\t\tgit push origin &&\n      +\t\tgit branch --set-upstream-to upstream/ahead &&\n      +\t\tgit status >../actual\n      +\t) &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature8\n     ++\tOn branch feature9\n      +\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n      +\t  (use \"git pull\" to update your local branch)\n      +\n     -+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n     ++\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n      +\n      +\tnothing to commit, working tree clean\n      +\tEOF\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status shows remapped push refspec' '\n     ++test_expect_success 'status.compareBranches with remapped push refspec' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout -b feature9 origin/main &&\n     -+\t\tgit config remote.origin.push refs/heads/feature9:refs/heads/remapped &&\n     ++\t\tgit checkout -b feature10 origin/main &&\n     ++\t\tgit config remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n      +\t\tgit push &&\n      +\t\tadvance work &&\n      +\t\tgit status >../actual\n      +\t) &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature9\n     ++\tOn branch feature10\n      +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n      +\n      +\tYour branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status shows remapped push refspec with upstream remote' '\n     ++test_expect_success 'status.compareBranches with remapped push and upstream remote' '\n      +\t(\n      +\t\tcd test &&\n     -+\t\tgit checkout -b feature10 upstream/main &&\n     -+\t\tgit config remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n     ++\t\tgit checkout -b feature11 upstream/main &&\n     ++\t\tgit config remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n      +\t\tgit push origin &&\n      +\t\tadvance work &&\n      +\t\tgit status >../actual\n      +\t) &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature10\n     ++\tOn branch feature11\n      +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n      +\n      +\tYour branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\tEOF\n      +\ttest_cmp expect actual\n      +'\n     ++\n     ++test_expect_success 'clean up after compareBranches tests' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit config --unset status.compareBranches\n     ++\t)\n     ++'\n      +\n       test_done\n\n-- \ngitgitgadget\n"},{"id":"534154","messageId":"27a46f8d9cf41674ff2eb5b60ce772a39d43ca3b.1768766353.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v26.git.git.1768766353.gitgitgadget@gmail.com","subject":"[PATCH v26 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-18T19:59:12Z","receivedAt":"2026-01-18T19:59:17Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b756ff6f15..fd592ec659 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"534155","messageId":"caa761f6153b5b8992c63fea5b5c0fc9b9d444a5.1768766353.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v26.git.git.1768766353.gitgitgadget@gmail.com","subject":"[PATCH v26 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-18T19:59:13Z","receivedAt":"2026-01-18T19:59:19Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a new configuration variable `status.compareBranches` that allows\nusers to specify a space-separated list of branches to compare against\nthe current branch in `git status` output.\n\nEach branch in the list can be:\n- A remote-tracking branch name (e.g., `origin/main`)\n- The special reference `@{upstream}` for the tracking branch\n- The special reference `@{push}` for the push destination\n\nWhen not configured, the default behavior is equivalent to setting\n`status.compareBranches = @{upstream}`, preserving backward compatibility.\n\nThe advice messages shown are context-aware:\n- \"git pull\" advice is shown only when comparing against @{upstream}\n- \"git push\" advice is shown only when comparing against @{push}\n- Divergence advice is shown for upstream branch comparisons\n\nThis is useful for triangular workflows where the upstream tracking\nbranch differs from the push destination, allowing users to see their\nstatus relative to both branches at once.\n\nExample configuration:\n    [status]\n        compareBranches = @{upstream} @{push}\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/status.adoc |  20 ++\n remote.c                         | 157 ++++++++++++---\n t/t6040-tracking-info.sh         | 322 +++++++++++++++++++++++++++++++\n 3 files changed, 470 insertions(+), 29 deletions(-)\n\ndiff --git a/Documentation/config/status.adoc b/Documentation/config/status.adoc\nindex 8caf90f51c..ab54f25fbc 100644\n--- a/Documentation/config/status.adoc\n+++ b/Documentation/config/status.adoc\n@@ -17,6 +17,26 @@ status.aheadBehind::\n \t`--no-ahead-behind` by default in linkgit:git-status[1] for\n \tnon-porcelain status formats.  Defaults to true.\n \n+status.compareBranches::\n+\tA space-separated list of branches to compare the current branch\n+\tagainst in linkgit:git-status[1]. Each branch specification can be\n+\ta remote-tracking branch name (e.g. `origin/main`), or a special\n+\treference like `@{upstream}` or `@{push}`. For each branch in the\n+\tlist, git status shows whether the current branch is ahead, behind,\n+\tor has diverged from that branch.\n++\n+If not set, the default behavior is equivalent to `@{upstream}`, which\n+compares against the configured upstream tracking branch.\n++\n+Example:\n++\n+----\n+[status]\n+\tcompareBranches = origin/main origin/develop\n+----\n++\n+This would show comparisons against both `origin/main` and `origin/develop`.\n+\n status.displayCommentPrefix::\n \tIf set to true, linkgit:git-status[1] will insert a comment\n \tprefix before each output line (starting with\ndiff --git a/remote.c b/remote.c\nindex fd592ec659..1ea17cbbfa 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,12 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tENABLE_ADVICE_PULL       = (1 << 0),\n+\tENABLE_ADVICE_PUSH       = (1 << 1),\n+\tENABLE_ADVICE_DIVERGENCE = (1 << 2),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -272,6 +278,7 @@ static void branch_release(struct branch *branch)\n \tfree((char *)branch->refname);\n \tfree(branch->remote_name);\n \tfree(branch->pushremote_name);\n+\tfree((char *)branch->push_tracking_ref);\n \tmerge_clear(branch);\n }\n \n@@ -2230,13 +2237,53 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *resolve_compare_branch(struct branch *branch, const char *name)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tconst char *resolved = NULL;\n+\tchar *ret;\n+\n+\tif (!branch || !name)\n+\t\treturn NULL;\n+\n+\tif (!strcasecmp(name, \"@{upstream}\") || !strcasecmp(name, \"@{u}\"))\n+\t\tresolved = branch_get_upstream(branch, NULL);\n+\telse if (!strcasecmp(name, \"@{push}\"))\n+\t\tresolved = branch_get_push(branch, NULL);\n+\n+\tif (resolved)\n+\t\treturn xstrdup(resolved);\n+\n+\tstrbuf_addf(&buf, \"refs/remotes/%s\", name);\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tbuf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\tif (resolved) {\n+\t\tret = xstrdup(resolved);\n+\t\tstrbuf_release(&buf);\n+\t\treturn ret;\n+\t}\n+\n+\tstrbuf_release(&buf);\n+\treturn NULL;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n-\t\t\t\t     bool show_divergence_advice)\n+\t\t\t\t     unsigned flags)\n {\n+\tbool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2245,7 +2292,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2254,7 +2301,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2265,7 +2312,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2278,8 +2325,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2292,34 +2338,87 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t enum ahead_behind_flags abf,\n \t\t\t int show_divergence_advice)\n {\n-\tint ours, theirs, cmp_fetch;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (cmp_fetch < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n+\tchar *compare_branches_config = NULL;\n+\tstruct string_list compare_branches = STRING_LIST_INIT_DUP;\n+\tstruct string_list_item *item;\n+\tint reported = 0;\n+\tsize_t i;\n+\tconst char *upstream_ref;\n+\tconst char *push_ref;\n+\n+\trepo_config_get_string(the_repository, \"status.comparebranches\",\n+\t\t\t       &compare_branches_config);\n+\n+\tif (compare_branches_config) {\n+\t\tstring_list_split(&compare_branches, compare_branches_config,\n+\t\t\t\t  \" \", -1);\n+\t\tstring_list_remove_empty_items(&compare_branches, 0);\n+\t} else {\n+\t\tstring_list_append(&compare_branches, \"@{upstream}\");\n \t}\n \n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n+\tupstream_ref = branch_get_upstream(branch, NULL);\n+\tpush_ref = branch_get_push(branch, NULL);\n \n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\tfor (i = 0; i < compare_branches.nr; i++) {\n+\t\tchar *full_ref;\n+\t\tchar *short_ref;\n+\t\tint ours, theirs, cmp;\n+\t\tint is_upstream, is_push;\n+\t\tunsigned flags = 0;\n+\n+\t\titem = &compare_branches.items[i];\n+\t\tfull_ref = resolve_compare_branch(branch, item->string);\n+\t\tif (!full_ref)\n+\t\t\tcontinue;\n+\n+\t\tshort_ref = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), full_ref, 0);\n+\n+\t\tis_upstream = upstream_ref && !strcmp(full_ref, upstream_ref);\n+\t\tis_push = push_ref && !strcmp(full_ref, push_ref);\n+\n+\t\tif (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))\n+\t\t\tis_push = 1;\n+\n+\t\tcmp = stat_branch_pair(branch->refname, full_ref,\n+\t\t\t\t       &ours, &theirs, abf);\n+\n+\t\tif (cmp < 0) {\n+\t\t\tif (is_upstream) {\n+\t\t\t\tstrbuf_addf(sb,\n+\t\t\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\t\t\tshort_ref);\n+\t\t\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t\t\t\treported = 1;\n+\t\t\t}\n+\t\t\tfree(full_ref);\n+\t\t\tfree(short_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (reported)\n+\t\t\tstrbuf_addstr(sb, \"\\n\");\n+\n+\t\tif (is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_PULL;\n+\t\tif (is_push)\n+\t\t\tflags |= ENABLE_ADVICE_PUSH;\n+\t\tif (show_divergence_advice && is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_DIVERGENCE;\n+\t\tformat_branch_comparison(sb, !cmp, ours, theirs, short_ref,\n+\t\t\t\t\t abf, flags);\n+\t\treported = 1;\n+\n+\t\tfree(full_ref);\n+\t\tfree(short_ref);\n \t}\n \n-\tfree(base);\n-\treturn 1;\n+\tstring_list_clear(&compare_branches, 0);\n+\tfree(compare_branches_config);\n+\treturn reported;\n }\n \n static int one_local_ref(const struct reference *ref, void *cb_data)\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..ea899a5679 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,326 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for compareBranches tests' '\n+\t(\n+\t\tcd test &&\n+\t\tgit config push.default current &&\n+\t\tgit config status.compareBranches \"@{upstream} @{push}\"\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows both branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches shows diverged and ahead' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches with upstream and origin remotes' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with upstream and origin remotes multiple compare branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit -c status.compareBranches=\"upstream/main origin/feature6 origin/feature5\" status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with diverged push branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature7${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows up to date branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature8 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature8 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with upstream behind and push up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b ahead upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push upstream HEAD &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit branch --set-upstream-to upstream/ahead &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n+\t  (use \"git pull\" to update your local branch)\n+\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature10 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature10\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push and upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature11 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature11\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clean up after compareBranches tests' '\n+\t(\n+\t\tcd test &&\n+\t\tgit config --unset status.compareBranches\n+\t)\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"534157","messageId":"20260119051406.GA1115353@coredump.intra.peff.net","threadId":"64668","inReplyTo":"caa761f6153b5b8992c63fea5b5c0fc9b9d444a5.1768766353.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v26 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-19T05:14:06Z","receivedAt":"2026-01-19T05:14:16Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sun, Jan 18, 2026 at 07:59:13PM +0000, Harald Nordgren via GitGitGadget wrote:\n\n> @@ -272,6 +278,7 @@ static void branch_release(struct branch *branch)\n>  \tfree((char *)branch->refname);\n>  \tfree(branch->remote_name);\n>  \tfree(branch->pushremote_name);\n> +\tfree((char *)branch->push_tracking_ref);\n>  \tmerge_clear(branch);\n>  }\n>  \n\nI don't think this line is right. The push_tracking_ref field is const\nbecause it may or may not be a newly allocated string. In particular,\nwith push.default=upstream, it will be a copy of the string returned\nfrom branch_get_upstream(), which is covered by the merge_clear() call.\nSo we'd get a double-free.\n\nThis is due to some memory-management hackery in e291c75a95 (remote.c:\nadd branch_get_push, 2015-05-21). The good news is that not only is it\nnot too hard to untangle this, but it is closely related to leaks you\nare seeing on your topic.\n\nI'll post a series in a moment fixing both.\n\n-Peff\n"},{"id":"534164","messageId":"CAPx1Gvdpyfcioot5V9Q8=Yg-aPSPeVKaE4Z2_Xt3-SZ2WhVMog@mail.gmail.com","threadId":"64668","inReplyTo":"56D0BADB-43C8-4E93-A871-6E6B11B59DCF@gmail.com","subject":"Re: Triangular workflow","fromName":"Chris Torek","fromEmail":"chris.torek@gmail.com","sentAt":"2026-01-19T05:58:58Z","receivedAt":"2026-01-19T05:59:11Z","isPatch":false,"sender":{"key":"chris.torek@gmail.com","avatar":"https://avatars.githubusercontent.com/u/16826774?v=4"},"body":"(I'm somewhat behind all week so this is from Wednesday...)\n\nRe: status.compareBranch (or similar): I like the idea; I'm not sure what sort\nof details might be needed in the end though.\n\nOn Wed, Jan 14, 2026 at 1:42 PM Ben Knoble <ben.knoble@gmail.com> wrote:\n> I favor something similar to what Junio described but also including @{push} by default (and ignoring it if non-existent), so that folks discovering triangular workflows for the first time are easily able to see what is happening.\n\nThis also seems to me likely to be the right default. It's useful for a lot of\nGitHub and similar forges, where you send fixes upstream by first forking\nsome official repository and then cloning your fork (e.g., to a laptop), setting\nup your local clone (on laptop) to have two remotes: the official repository,\nand your fork, both on the same forge.\n\nIt's a little annoying to have to deal with *three* potential\n\"upstream\" repos, if\nyou need to back up your local work to a corporate server or forge *plus*\na push-for-pull-request at the forge as well, of course. But then at least the\n\"compare with all three\" option becomes available. How you wish to spell\nthe default push location is then up to you :-)\n\nChris\n"},{"id":"534214","messageId":"20260120083547.60416-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"CAPx1Gvdpyfcioot5V9Q8=Yg-aPSPeVKaE4Z2_Xt3-SZ2WhVMog@mail.gmail.com","subject":"Triangular workflow","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-20T08:35:47Z","receivedAt":"2026-01-20T08:35:51Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":">> I favor something similar to what Junio described but also including\n>> @{push} by default (and ignoring it if non-existent), so that folks\n>> discovering triangular workflows for the first time are easily able to\n>> see what is happening.\n\n> This also seems to me likely to be the right default. It's useful for a lot of\n> GitHub and similar forges, where you send fixes upstream by first forking\n> some official repository and then cloning your fork (e.g., to a laptop), setting\n> up your local clone (on laptop) to have two remotes: the official repository,\n> and your fork, both on the same forge.\n\nI agree with this too, would like to make '@{upstream} @{push}' the\ndefault.\n\n\nHarald\n"},{"id":"534218","messageId":"20260120094957.28855-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260119051406.GA1115353@coredump.intra.peff.net","subject":"Memory leak","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-20T09:49:57Z","receivedAt":"2026-01-20T09:50:02Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> This is due to some memory-management hackery in e291c75a95 (remote.c:\n> add branch_get_push, 2015-05-21). The good news is that not only is it\n> not too hard to untangle this, but it is closely related to leaks you\n> are seeing on your topic.\n> \n> I'll post a series in a moment fixing both.\n\nThis seems to not have helped my memory leak. I squashed and applied your\ncommits on top of my work as a separate pull request on GitHub, the CI leak\ntests are still failing:\n\nhttps://github.com/git/git/pull/2170\n\n\nHarald\n"},{"id":"534231","messageId":"20260120132202.1655-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260120094957.28855-1-haraldnordgren@gmail.com","subject":"Memory leak","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-20T13:22:02Z","receivedAt":"2026-01-20T13:22:05Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"My bad, it does fix it! I think I applied your patch incorrectly the first\ntime around.\n\n\nHarald\n"},{"id":"534295","messageId":"xmqq7btcx8pc.fsf@gitster.g","threadId":"64668","inReplyTo":"20260120132202.1655-1-haraldnordgren@gmail.com","subject":"Re: Memory leak","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-20T21:42:07Z","receivedAt":"2026-01-20T21:42:11Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> My bad, it does fix it! I think I applied your patch incorrectly the first\n> time around.\n\nI've queued your v26 on top of a merge of Peff's leakfix patches\ninto 'master'.  It hopefully will appear in today's pushout.\n\nThanks, both.\n"},{"id":"534373","messageId":"xmqqh5seu7jz.fsf@gitster.g","threadId":"64668","inReplyTo":"xmqq7btcx8pc.fsf@gitster.g","subject":"Re: Memory leak","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-21T18:47:28Z","receivedAt":"2026-01-21T18:47:31Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Harald Nordgren <haraldnordgren@gmail.com> writes:\n>\n>> My bad, it does fix it! I think I applied your patch incorrectly the first\n>> time around.\n>\n> I've queued your v26 on top of a merge of Peff's leakfix patches\n> into 'master'.  It hopefully will appear in today's pushout.\n>\n> Thanks, both.\n\nSo are we all happy with v26 of the topic?\n"},{"id":"534382","messageId":"20260121204957.GA747866@coredump.intra.peff.net","threadId":"64668","inReplyTo":"xmqqh5seu7jz.fsf@gitster.g","subject":"Re: Memory leak","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-21T20:49:57Z","receivedAt":"2026-01-21T20:49:59Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Jan 21, 2026 at 10:47:28AM -0800, Junio C Hamano wrote:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > Harald Nordgren <haraldnordgren@gmail.com> writes:\n> >\n> >> My bad, it does fix it! I think I applied your patch incorrectly the first\n> >> time around.\n> >\n> > I've queued your v26 on top of a merge of Peff's leakfix patches\n> > into 'master'.  It hopefully will appear in today's pushout.\n> >\n> > Thanks, both.\n> \n> So are we all happy with v26 of the topic?\n\nIt addresses my main concern, which was having a way to turn it off. ;)\n(And I see it is not even changed by default now).\n\nOne thing that puzzled me a bit is that the show_divergence_advice flag\nis only respected for \"use git pull if you want to integrate...\" hint\nand not other advice. But that was true even before these patches. It\nfeels funny to me that it is treated differently than the other advice.\nBut I think we can ignore that for the purposes of this topic. If\nanything it is a defect in b6f3da5132 (wt-status: don't show divergence\nadvice when committing, 2023-07-12), which perhaps should have covered\nmore code paths.\n\nOther than that, I did not see anything wrong from my (admittedly pretty\ncursory) read.\n\n-Peff\n"},{"id":"534455","messageId":"20260122150339.65566-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqh5seu7jz.fsf@gitster.g","subject":"Memory leak","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-22T15:03:39Z","receivedAt":"2026-01-22T15:03:42Z","isPatch":false,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"I realized now that my code would benefit from branch de-duplication. When\nrunning it from the main branch (which has upsteam+push = origin/main) and\nmy git setting is this which I intended to run from now on:\n\n    git config --global status.compareBranches \"@{upstream} @{push}\"\n\nthen it reports status twice. I have a small fix ready and will push it out\nonce the CI passes on GitHub.\n\n\nHarald\n"},{"id":"534463","messageId":"pull.2138.v27.git.git.1769096240.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v26.git.git.1768766353.gitgitgadget@gmail.com","subject":"[PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-22T15:37:18Z","receivedAt":"2026-01-22T15:37:23Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im cc:\nJeff King peff@peff.net\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: add status.compareBranches config for multiple branch\n    comparisons\n\n Documentation/config/status.adoc |  20 ++\n remote.c                         | 192 ++++++++++++++----\n t/t6040-tracking-info.sh         | 337 +++++++++++++++++++++++++++++++\n 3 files changed, 512 insertions(+), 37 deletions(-)\n\n\nbase-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v27\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v27\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v26:\n\n 1:  27a46f8d9c = 1:  27a46f8d9c refactor format_branch_comparison in preparation\n 2:  caa761f615 ! 2:  0993420fc1 status: add status.compareBranches config for multiple branch comparisons\n     @@ remote.c\n       struct counted_string {\n       \tsize_t len;\n       \tconst char *s;\n     -@@ remote.c: static void branch_release(struct branch *branch)\n     - \tfree((char *)branch->refname);\n     - \tfree(branch->remote_name);\n     - \tfree(branch->pushremote_name);\n     -+\tfree((char *)branch->push_tracking_ref);\n     - \tmerge_clear(branch);\n     - }\n     - \n      @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n       \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n       }\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      -\tconst char *full_base;\n      -\tchar *base;\n      -\tint upstream_is_gone = 0;\n     --\n     ++\tchar *compare_branches = NULL;\n     ++\tstruct string_list branches = STRING_LIST_INIT_DUP;\n     ++\tstruct string_list processed_refs = STRING_LIST_INIT_DUP;\n     ++\tint reported = 0;\n     ++\tsize_t i;\n     ++\tconst char *upstream_ref;\n     ++\tconst char *push_ref;\n     + \n      -\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n      -\tif (cmp_fetch < 0) {\n      -\t\tif (!full_base)\n      -\t\t\treturn 0;\n      -\t\tupstream_is_gone = 1;\n     -+\tchar *compare_branches_config = NULL;\n     -+\tstruct string_list compare_branches = STRING_LIST_INIT_DUP;\n     -+\tstruct string_list_item *item;\n     -+\tint reported = 0;\n     -+\tsize_t i;\n     -+\tconst char *upstream_ref;\n     -+\tconst char *push_ref;\n     -+\n      +\trepo_config_get_string(the_repository, \"status.comparebranches\",\n     -+\t\t\t       &compare_branches_config);\n     ++\t\t\t       &compare_branches);\n      +\n     -+\tif (compare_branches_config) {\n     -+\t\tstring_list_split(&compare_branches, compare_branches_config,\n     -+\t\t\t\t  \" \", -1);\n     -+\t\tstring_list_remove_empty_items(&compare_branches, 0);\n     ++\tif (compare_branches) {\n     ++\t\tstring_list_split(&branches, compare_branches, \" \", -1);\n     ++\t\tstring_list_remove_empty_items(&branches, 0);\n      +\t} else {\n     -+\t\tstring_list_append(&compare_branches, \"@{upstream}\");\n     ++\t\tstring_list_append(&branches, \"@{upstream}\");\n       \t}\n       \n      -\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      -\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n      -\t} else {\n      -\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n     -+\tfor (i = 0; i < compare_branches.nr; i++) {\n     ++\tfor (i = 0; i < branches.nr; i++) {\n      +\t\tchar *full_ref;\n      +\t\tchar *short_ref;\n      +\t\tint ours, theirs, cmp;\n      +\t\tint is_upstream, is_push;\n      +\t\tunsigned flags = 0;\n      +\n     -+\t\titem = &compare_branches.items[i];\n     -+\t\tfull_ref = resolve_compare_branch(branch, item->string);\n     ++\t\tfull_ref = resolve_compare_branch(branch,\n     ++\t\t\t\t\t\t  branches.items[i].string);\n      +\t\tif (!full_ref)\n      +\t\t\tcontinue;\n      +\n     ++\t\tif (string_list_has_string(&processed_refs, full_ref)) {\n     ++\t\t\tfree(full_ref);\n     ++\t\t\tcontinue;\n     ++\t\t}\n     ++\t\tstring_list_insert(&processed_refs, full_ref);\n     ++\n      +\t\tshort_ref = refs_shorten_unambiguous_ref(\n      +\t\t\tget_main_ref_store(the_repository), full_ref, 0);\n      +\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n       \n      -\tfree(base);\n      -\treturn 1;\n     -+\tstring_list_clear(&compare_branches, 0);\n     -+\tfree(compare_branches_config);\n     ++\tstring_list_clear(&branches, 0);\n     ++\tstring_list_clear(&processed_refs, 0);\n     ++\tfree(compare_branches);\n      +\treturn reported;\n       }\n       \n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t)\n      +'\n      +\n     ++test_expect_success 'status.compareBranches from upstream has no duplicates' '\n     ++\t(\n     ++\t\tcd test &&\n     ++\t\tgit checkout main &&\n     ++\t\tgit status >../actual\n     ++\t) &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch main\n     ++\tYour branch is up to date with ${SQ}origin/main${SQ}.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n      +test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n      +\t(\n      +\t\tcd test &&\n\n-- \ngitgitgadget\n"},{"id":"534464","messageId":"27a46f8d9cf41674ff2eb5b60ce772a39d43ca3b.1769096240.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v27.git.git.1769096240.gitgitgadget@gmail.com","subject":"[PATCH v27 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-22T15:37:19Z","receivedAt":"2026-01-22T15:37:25Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b756ff6f15..fd592ec659 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"534465","messageId":"0993420fc1185ec4a907a8c3bb52ca965e720c54.1769096240.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v27.git.git.1769096240.gitgitgadget@gmail.com","subject":"[PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-22T15:37:20Z","receivedAt":"2026-01-22T15:37:27Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a new configuration variable `status.compareBranches` that allows\nusers to specify a space-separated list of branches to compare against\nthe current branch in `git status` output.\n\nEach branch in the list can be:\n- A remote-tracking branch name (e.g., `origin/main`)\n- The special reference `@{upstream}` for the tracking branch\n- The special reference `@{push}` for the push destination\n\nWhen not configured, the default behavior is equivalent to setting\n`status.compareBranches = @{upstream}`, preserving backward compatibility.\n\nThe advice messages shown are context-aware:\n- \"git pull\" advice is shown only when comparing against @{upstream}\n- \"git push\" advice is shown only when comparing against @{push}\n- Divergence advice is shown for upstream branch comparisons\n\nThis is useful for triangular workflows where the upstream tracking\nbranch differs from the push destination, allowing users to see their\nstatus relative to both branches at once.\n\nExample configuration:\n    [status]\n        compareBranches = @{upstream} @{push}\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/status.adoc |  20 ++\n remote.c                         | 160 ++++++++++++---\n t/t6040-tracking-info.sh         | 337 +++++++++++++++++++++++++++++++\n 3 files changed, 489 insertions(+), 28 deletions(-)\n\ndiff --git a/Documentation/config/status.adoc b/Documentation/config/status.adoc\nindex 8caf90f51c..ab54f25fbc 100644\n--- a/Documentation/config/status.adoc\n+++ b/Documentation/config/status.adoc\n@@ -17,6 +17,26 @@ status.aheadBehind::\n \t`--no-ahead-behind` by default in linkgit:git-status[1] for\n \tnon-porcelain status formats.  Defaults to true.\n \n+status.compareBranches::\n+\tA space-separated list of branches to compare the current branch\n+\tagainst in linkgit:git-status[1]. Each branch specification can be\n+\ta remote-tracking branch name (e.g. `origin/main`), or a special\n+\treference like `@{upstream}` or `@{push}`. For each branch in the\n+\tlist, git status shows whether the current branch is ahead, behind,\n+\tor has diverged from that branch.\n++\n+If not set, the default behavior is equivalent to `@{upstream}`, which\n+compares against the configured upstream tracking branch.\n++\n+Example:\n++\n+----\n+[status]\n+\tcompareBranches = origin/main origin/develop\n+----\n++\n+This would show comparisons against both `origin/main` and `origin/develop`.\n+\n status.displayCommentPrefix::\n \tIf set to true, linkgit:git-status[1] will insert a comment\n \tprefix before each output line (starting with\ndiff --git a/remote.c b/remote.c\nindex fd592ec659..e256cc9b81 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,12 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tENABLE_ADVICE_PULL       = (1 << 0),\n+\tENABLE_ADVICE_PUSH       = (1 << 1),\n+\tENABLE_ADVICE_DIVERGENCE = (1 << 2),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2230,13 +2236,53 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *resolve_compare_branch(struct branch *branch, const char *name)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tconst char *resolved = NULL;\n+\tchar *ret;\n+\n+\tif (!branch || !name)\n+\t\treturn NULL;\n+\n+\tif (!strcasecmp(name, \"@{upstream}\") || !strcasecmp(name, \"@{u}\"))\n+\t\tresolved = branch_get_upstream(branch, NULL);\n+\telse if (!strcasecmp(name, \"@{push}\"))\n+\t\tresolved = branch_get_push(branch, NULL);\n+\n+\tif (resolved)\n+\t\treturn xstrdup(resolved);\n+\n+\tstrbuf_addf(&buf, \"refs/remotes/%s\", name);\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tbuf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\tif (resolved) {\n+\t\tret = xstrdup(resolved);\n+\t\tstrbuf_release(&buf);\n+\t\treturn ret;\n+\t}\n+\n+\tstrbuf_release(&buf);\n+\treturn NULL;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n-\t\t\t\t     bool show_divergence_advice)\n+\t\t\t\t     unsigned flags)\n {\n+\tbool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2245,7 +2291,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2254,7 +2300,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2265,7 +2311,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2278,8 +2324,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2292,34 +2337,93 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t enum ahead_behind_flags abf,\n \t\t\t int show_divergence_advice)\n {\n-\tint ours, theirs, cmp_fetch;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n+\tchar *compare_branches = NULL;\n+\tstruct string_list branches = STRING_LIST_INIT_DUP;\n+\tstruct string_list processed_refs = STRING_LIST_INIT_DUP;\n+\tint reported = 0;\n+\tsize_t i;\n+\tconst char *upstream_ref;\n+\tconst char *push_ref;\n \n-\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (cmp_fetch < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n+\trepo_config_get_string(the_repository, \"status.comparebranches\",\n+\t\t\t       &compare_branches);\n+\n+\tif (compare_branches) {\n+\t\tstring_list_split(&branches, compare_branches, \" \", -1);\n+\t\tstring_list_remove_empty_items(&branches, 0);\n+\t} else {\n+\t\tstring_list_append(&branches, \"@{upstream}\");\n \t}\n \n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n+\tupstream_ref = branch_get_upstream(branch, NULL);\n+\tpush_ref = branch_get_push(branch, NULL);\n \n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\tfor (i = 0; i < branches.nr; i++) {\n+\t\tchar *full_ref;\n+\t\tchar *short_ref;\n+\t\tint ours, theirs, cmp;\n+\t\tint is_upstream, is_push;\n+\t\tunsigned flags = 0;\n+\n+\t\tfull_ref = resolve_compare_branch(branch,\n+\t\t\t\t\t\t  branches.items[i].string);\n+\t\tif (!full_ref)\n+\t\t\tcontinue;\n+\n+\t\tif (string_list_has_string(&processed_refs, full_ref)) {\n+\t\t\tfree(full_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tstring_list_insert(&processed_refs, full_ref);\n+\n+\t\tshort_ref = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), full_ref, 0);\n+\n+\t\tis_upstream = upstream_ref && !strcmp(full_ref, upstream_ref);\n+\t\tis_push = push_ref && !strcmp(full_ref, push_ref);\n+\n+\t\tif (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))\n+\t\t\tis_push = 1;\n+\n+\t\tcmp = stat_branch_pair(branch->refname, full_ref,\n+\t\t\t\t       &ours, &theirs, abf);\n+\n+\t\tif (cmp < 0) {\n+\t\t\tif (is_upstream) {\n+\t\t\t\tstrbuf_addf(sb,\n+\t\t\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\t\t\tshort_ref);\n+\t\t\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t\t\t\treported = 1;\n+\t\t\t}\n+\t\t\tfree(full_ref);\n+\t\t\tfree(short_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (reported)\n+\t\t\tstrbuf_addstr(sb, \"\\n\");\n+\n+\t\tif (is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_PULL;\n+\t\tif (is_push)\n+\t\t\tflags |= ENABLE_ADVICE_PUSH;\n+\t\tif (show_divergence_advice && is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_DIVERGENCE;\n+\t\tformat_branch_comparison(sb, !cmp, ours, theirs, short_ref,\n+\t\t\t\t\t abf, flags);\n+\t\treported = 1;\n+\n+\t\tfree(full_ref);\n+\t\tfree(short_ref);\n \t}\n \n-\tfree(base);\n-\treturn 1;\n+\tstring_list_clear(&branches, 0);\n+\tstring_list_clear(&processed_refs, 0);\n+\tfree(compare_branches);\n+\treturn reported;\n }\n \n static int one_local_ref(const struct reference *ref, void *cb_data)\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..f7859d2e5a 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,341 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for compareBranches tests' '\n+\t(\n+\t\tcd test &&\n+\t\tgit config push.default current &&\n+\t\tgit config status.compareBranches \"@{upstream} @{push}\"\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches from upstream has no duplicates' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch main\n+\tYour branch is up to date with ${SQ}origin/main${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows both branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches shows diverged and ahead' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches with upstream and origin remotes' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with upstream and origin remotes multiple compare branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit -c status.compareBranches=\"upstream/main origin/feature6 origin/feature5\" status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with diverged push branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature7${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows up to date branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature8 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature8 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with upstream behind and push up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b ahead upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push upstream HEAD &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit branch --set-upstream-to upstream/ahead &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n+\t  (use \"git pull\" to update your local branch)\n+\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature10 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature10\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push and upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature11 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature11\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clean up after compareBranches tests' '\n+\t(\n+\t\tcd test &&\n+\t\tgit config --unset status.compareBranches\n+\t)\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"534479","messageId":"xmqqbjilo6h9.fsf@gitster.g","threadId":"64668","inReplyTo":"20260122150339.65566-1-haraldnordgren@gmail.com","subject":"Re: Memory leak","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T18:19:30Z","receivedAt":"2026-01-22T18:19:35Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> I realized now that my code would benefit from branch\n> de-duplication.\n\n... meaning that ...\n\n> When\n> running it from the main branch (which has upsteam+push = origin/main) and\n> my git setting is this which I intended to run from now on:\n>\n>     git config --global status.compareBranches \"@{upstream} @{push}\"\n\n... when @{upstream} and @{push} ends up being the same branch?\nYes, if that happens, it would be irritating for the users if we\ndo not deduplicate.\n\nThanks.\n"},{"id":"534481","messageId":"xmqq1pjho4wn.fsf@gitster.g","threadId":"64668","inReplyTo":"pull.2138.v27.git.git.1769096240.gitgitgadget@gmail.com","subject":"Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T18:53:28Z","receivedAt":"2026-01-22T18:53:32Z","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\nBefore these list of CC's (which does not seem to have any effect,\nby the way.  The message is going only to the list with your\npersonal address on CC: and to nobody else), ...\n\n> cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\n> ychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\n> cc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\n> kristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\n> cc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im cc:\n> Jeff King peff@peff.net\n\n... please summarize\n\n (1) what this series of patches are about, to help those who\n     encounter these patches for the first time, and\n\n (2) what changed in this iteration (v27) relative to the previous\n     iteration (v26).\n\nI happen to have seen your other message so I can guess this is\nabout deduping when more than one comparison target is listed and\nsome happen to become the same branches, but others may not have\nseen that other message, and it is not helpful to just dump the\nrange-diff and force them to read it to deduce what you did.\n\nThanks.\n\n> Harald Nordgren (2):\n>   refactor format_branch_comparison in preparation\n>   status: add status.compareBranches config for multiple branch\n>     comparisons\n>\n>  Documentation/config/status.adoc |  20 ++\n>  remote.c                         | 192 ++++++++++++++----\n>  t/t6040-tracking-info.sh         | 337 +++++++++++++++++++++++++++++++\n>  3 files changed, 512 insertions(+), 37 deletions(-)\n>\n>\n> base-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v27\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v27\n> Pull-Request: https://github.com/git/git/pull/2138\n>\n> Range-diff vs v26:\n>\n>  1:  27a46f8d9c = 1:  27a46f8d9c refactor format_branch_comparison in preparation\n>  2:  caa761f615 ! 2:  0993420fc1 status: add status.compareBranches config for multiple branch comparisons\n>      @@ remote.c\n>        struct counted_string {\n>        \tsize_t len;\n>        \tconst char *s;\n>      -@@ remote.c: static void branch_release(struct branch *branch)\n>      - \tfree((char *)branch->refname);\n>      - \tfree(branch->remote_name);\n>      - \tfree(branch->pushremote_name);\n>      -+\tfree((char *)branch->push_tracking_ref);\n>      - \tmerge_clear(branch);\n>      - }\n>      - \n>       @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n>        \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n>        }\n>      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>       -\tconst char *full_base;\n>       -\tchar *base;\n>       -\tint upstream_is_gone = 0;\n>      --\n>      ++\tchar *compare_branches = NULL;\n>      ++\tstruct string_list branches = STRING_LIST_INIT_DUP;\n>      ++\tstruct string_list processed_refs = STRING_LIST_INIT_DUP;\n>      ++\tint reported = 0;\n>      ++\tsize_t i;\n>      ++\tconst char *upstream_ref;\n>      ++\tconst char *push_ref;\n>      + \n>       -\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n>       -\tif (cmp_fetch < 0) {\n>       -\t\tif (!full_base)\n>       -\t\t\treturn 0;\n>       -\t\tupstream_is_gone = 1;\n>      -+\tchar *compare_branches_config = NULL;\n>      -+\tstruct string_list compare_branches = STRING_LIST_INIT_DUP;\n>      -+\tstruct string_list_item *item;\n>      -+\tint reported = 0;\n>      -+\tsize_t i;\n>      -+\tconst char *upstream_ref;\n>      -+\tconst char *push_ref;\n>      -+\n>       +\trepo_config_get_string(the_repository, \"status.comparebranches\",\n>      -+\t\t\t       &compare_branches_config);\n>      ++\t\t\t       &compare_branches);\n>       +\n>      -+\tif (compare_branches_config) {\n>      -+\t\tstring_list_split(&compare_branches, compare_branches_config,\n>      -+\t\t\t\t  \" \", -1);\n>      -+\t\tstring_list_remove_empty_items(&compare_branches, 0);\n>      ++\tif (compare_branches) {\n>      ++\t\tstring_list_split(&branches, compare_branches, \" \", -1);\n>      ++\t\tstring_list_remove_empty_items(&branches, 0);\n>       +\t} else {\n>      -+\t\tstring_list_append(&compare_branches, \"@{upstream}\");\n>      ++\t\tstring_list_append(&branches, \"@{upstream}\");\n>        \t}\n>        \n>       -\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n>      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>       -\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n>       -\t} else {\n>       -\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n>      -+\tfor (i = 0; i < compare_branches.nr; i++) {\n>      ++\tfor (i = 0; i < branches.nr; i++) {\n>       +\t\tchar *full_ref;\n>       +\t\tchar *short_ref;\n>       +\t\tint ours, theirs, cmp;\n>       +\t\tint is_upstream, is_push;\n>       +\t\tunsigned flags = 0;\n>       +\n>      -+\t\titem = &compare_branches.items[i];\n>      -+\t\tfull_ref = resolve_compare_branch(branch, item->string);\n>      ++\t\tfull_ref = resolve_compare_branch(branch,\n>      ++\t\t\t\t\t\t  branches.items[i].string);\n>       +\t\tif (!full_ref)\n>       +\t\t\tcontinue;\n>       +\n>      ++\t\tif (string_list_has_string(&processed_refs, full_ref)) {\n>      ++\t\t\tfree(full_ref);\n>      ++\t\t\tcontinue;\n>      ++\t\t}\n>      ++\t\tstring_list_insert(&processed_refs, full_ref);\n>      ++\n>       +\t\tshort_ref = refs_shorten_unambiguous_ref(\n>       +\t\t\tget_main_ref_store(the_repository), full_ref, 0);\n>       +\n>      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n>        \n>       -\tfree(base);\n>       -\treturn 1;\n>      -+\tstring_list_clear(&compare_branches, 0);\n>      -+\tfree(compare_branches_config);\n>      ++\tstring_list_clear(&branches, 0);\n>      ++\tstring_list_clear(&processed_refs, 0);\n>      ++\tfree(compare_branches);\n>       +\treturn reported;\n>        }\n>        \n>      @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n>       +\t)\n>       +'\n>       +\n>      ++test_expect_success 'status.compareBranches from upstream has no duplicates' '\n>      ++\t(\n>      ++\t\tcd test &&\n>      ++\t\tgit checkout main &&\n>      ++\t\tgit status >../actual\n>      ++\t) &&\n>      ++\tcat >expect <<-EOF &&\n>      ++\tOn branch main\n>      ++\tYour branch is up to date with ${SQ}origin/main${SQ}.\n>      ++\n>      ++\tnothing to commit, working tree clean\n>      ++\tEOF\n>      ++\ttest_cmp expect actual\n>      ++'\n>      ++\n>       +test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n>       +\t(\n>       +\t\tcd test &&\n"},{"id":"534483","messageId":"20260122190751.GA2098026@coredump.intra.peff.net","threadId":"64668","inReplyTo":"0993420fc1185ec4a907a8c3bb52ca965e720c54.1769096240.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-22T19:07:51Z","receivedAt":"2026-01-22T19:07:57Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Jan 22, 2026 at 03:37:20PM +0000, Harald Nordgren via GitGitGadget wrote:\n\n> +\t\tif (string_list_has_string(&processed_refs, full_ref)) {\n> +\t\t\tfree(full_ref);\n> +\t\t\tcontinue;\n> +\t\t}\n> +\t\tstring_list_insert(&processed_refs, full_ref);\n\nUsing a string list like this is quadratic. It probably doesn't matter\ntoo much here since we wouldn't expect the list of configured branches\nto be very long, though. But a strset is probably the better tool, like\nthe diff below (note that its \"add\" can be used as a single operation to\ninsert and check).\n\nI don't know if it's worth re-rolling for this or not. I doubt anybody\nwould hit it in practice, but I'd be more concerned about people\nauditing for accidentally-quadratic uses of string_list and stumbling\nupon it.\n\n-Peff\n\ndiff --git a/remote.c b/remote.c\nindex e256cc9b81..1071a23567 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2339,7 +2339,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n {\n \tchar *compare_branches = NULL;\n \tstruct string_list branches = STRING_LIST_INIT_DUP;\n-\tstruct string_list processed_refs = STRING_LIST_INIT_DUP;\n+\tstruct strset processed_refs = STRSET_INIT;\n \tint reported = 0;\n \tsize_t i;\n \tconst char *upstream_ref;\n@@ -2370,11 +2370,10 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (!full_ref)\n \t\t\tcontinue;\n \n-\t\tif (string_list_has_string(&processed_refs, full_ref)) {\n+\t\tif (!strset_add(&processed_refs, full_ref)) {\n \t\t\tfree(full_ref);\n \t\t\tcontinue;\n \t\t}\n-\t\tstring_list_insert(&processed_refs, full_ref);\n \n \t\tshort_ref = refs_shorten_unambiguous_ref(\n \t\t\tget_main_ref_store(the_repository), full_ref, 0);\n@@ -2421,7 +2420,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t}\n \n \tstring_list_clear(&branches, 0);\n-\tstring_list_clear(&processed_refs, 0);\n+\tstrset_clear(&processed_refs);\n \tfree(compare_branches);\n \treturn reported;\n }\n"},{"id":"534485","messageId":"20260122190954.64780-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqq1pjho4wn.fsf@gitster.g","subject":"Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-22T19:09:54Z","receivedAt":"2026-01-22T19:10:00Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Before these list of CC's (which does not seem to have any effect,\n> by the way.  The message is going only to the list with your\n> personal address on CC: and to nobody else), ...\n\nHmm, I always just the 'git send-email' snippet from example here:\nhttps://lore.kernel.org/git/xmqqbjilo6h9.fsf@gitster.g/, which for this\nmessage is:\n\n  git send-email \\\n    --in-reply-to=xmqqbjilo6h9.fsf@gitster.g \\\n    --to=gitster@pobox.com \\\n    --cc=git@vger.kernel.org \\\n    --cc=gitgitgadget@gmail.com \\\n    --cc=haraldnordgren@gmail.com \\\n    --cc=peff@peff.net \\\n    /path/to/YOUR_REPLY\n\n> ... please summarize\n> \n>  (1) what this series of patches are about, to help those who\n>      encounter these patches for the first time, and\n> \n>  (2) what changed in this iteration (v27) relative to the previous\n>      iteration (v26).\n> \n> I happen to have seen your other message so I can guess this is\n> about deduping when more than one comparison target is listed and\n> some happen to become the same branches, but others may not have\n> seen that other message, and it is not helpful to just dump the\n> range-diff and force them to read it to deduce what you did.\n\nFair point, but I always always use GitGitGadget to submit patches to here,\nI'm sure sure if there is a way to do that from there?\n\nI just write '/submit' to create the whole patch:\nhttps://github.com/git/git/pull/2138#issuecomment-3785055635\n\n\nHarald\n"},{"id":"534486","messageId":"xmqqsebxmp3i.fsf@gitster.g","threadId":"64668","inReplyTo":"20260122190954.64780-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T19:20:17Z","receivedAt":"2026-01-22T19:20:22Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> Before these list of CC's (which does not seem to have any effect,\n>> by the way.  The message is going only to the list with your\n>> personal address on CC: and to nobody else), ...\n>\n> Hmm, I always just the 'git send-email' snippet from example here:\n> https://lore.kernel.org/git/xmqqbjilo6h9.fsf@gitster.g/, which for this\n> message is:\n>\n>   git send-email \\\n>     --in-reply-to=xmqqbjilo6h9.fsf@gitster.g \\\n>     --to=gitster@pobox.com \\\n>     --cc=git@vger.kernel.org \\\n>     --cc=gitgitgadget@gmail.com \\\n>     --cc=haraldnordgren@gmail.com \\\n>     --cc=peff@peff.net \\\n>     /path/to/YOUR_REPLY\n\nBut the patch you sent was not using \"git send-email\", was it?  I am\nnot sure where those annoying and unused CC addresses comes from,\nbut it would be from GGG, not from \"git send-email\", I suspect.\n\n>> ... please summarize\n>> \n>>  (1) what this series of patches are about, to help those who\n>>      encounter these patches for the first time, and\n>> \n>>  (2) what changed in this iteration (v27) relative to the previous\n>>      iteration (v26).\n>> \n>> I happen to have seen your other message so I can guess this is\n>> about deduping when more than one comparison target is listed and\n>> some happen to become the same branches, but others may not have\n>> seen that other message, and it is not helpful to just dump the\n>> range-diff and force them to read it to deduce what you did.\n>\n> Fair point, but I always always use GitGitGadget to submit patches to here,\n> I'm sure sure if there is a way to do that from there?\n\nSorry, I do not work on or use GGG; other users of it may be able to\nchime in?\n\nThanks.\n"},{"id":"534487","messageId":"20260122192238.76368-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260122190751.GA2098026@coredump.intra.peff.net","subject":"Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-22T19:22:38Z","receivedAt":"2026-01-22T19:22:42Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Using a string list like this is quadratic. It probably doesn't matter\n> too much here since we wouldn't expect the list of configured branches\n> to be very long, though. But a strset is probably the better tool, like\n> the diff below (note that its \"add\" can be used as a single operation to\n> insert and check).\n> \n> I don't know if it's worth re-rolling for this or not. I doubt anybody\n> would hit it in practice, but I'd be more concerned about people\n> auditing for accidentally-quadratic uses of string_list and stumbling\n> upon it.\n\nSounds like a good change. Very nice to have insert-and-check as a single\noperation!\n\nI'll update it after passing CI on GitHub -- I'm running CI from two\nseparate GitHub pull requests, because one has your memory leak fix on top\nto allow the leak tests to not fail. 🤗\n\n\nHarald\n"},{"id":"534490","messageId":"20260122192430.76552-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqsebxmp3i.fsf@gitster.g","subject":"Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-22T19:24:30Z","receivedAt":"2026-01-22T19:24:35Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> But the patch you sent was not using \"git send-email\", was it?  I am\n> not sure where those annoying and unused CC addresses comes from,\n> but it would be from GGG, not from \"git send-email\", I suspect.\n\nYes you are right, I misunderstood your point about the CC's.\n\nPatches are 100% via GGG.\n\n\nHarald\n"},{"id":"534491","messageId":"pull.2138.v28.git.git.1769112471.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v27.git.git.1769096240.gitgitgadget@gmail.com","subject":"[PATCH v28 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-22T20:07:49Z","receivedAt":"2026-01-22T20:07:57Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im cc:\nJeff King peff@peff.net\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: add status.compareBranches config for multiple branch\n    comparisons\n\n Documentation/config/status.adoc |  20 ++\n remote.c                         | 191 ++++++++++++++----\n t/t6040-tracking-info.sh         | 337 +++++++++++++++++++++++++++++++\n 3 files changed, 511 insertions(+), 37 deletions(-)\n\n\nbase-commit: 83a69f19359e6d9bc980563caca38b2b5729808c\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v28\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v28\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v27:\n\n 1:  27a46f8d9c = 1:  f3c8c782b0 refactor format_branch_comparison in preparation\n 2:  0993420fc1 ! 2:  067978dd09 status: add status.compareBranches config for multiple branch comparisons\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      -\tint upstream_is_gone = 0;\n      +\tchar *compare_branches = NULL;\n      +\tstruct string_list branches = STRING_LIST_INIT_DUP;\n     -+\tstruct string_list processed_refs = STRING_LIST_INIT_DUP;\n     ++\tstruct strset processed_refs = STRSET_INIT;\n      +\tint reported = 0;\n      +\tsize_t i;\n      +\tconst char *upstream_ref;\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      +\t\tif (!full_ref)\n      +\t\t\tcontinue;\n      +\n     -+\t\tif (string_list_has_string(&processed_refs, full_ref)) {\n     ++\t\tif (!strset_add(&processed_refs, full_ref)) {\n      +\t\t\tfree(full_ref);\n      +\t\t\tcontinue;\n      +\t\t}\n     -+\t\tstring_list_insert(&processed_refs, full_ref);\n      +\n      +\t\tshort_ref = refs_shorten_unambiguous_ref(\n      +\t\t\tget_main_ref_store(the_repository), full_ref, 0);\n     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,\n      -\tfree(base);\n      -\treturn 1;\n      +\tstring_list_clear(&branches, 0);\n     -+\tstring_list_clear(&processed_refs, 0);\n     ++\tstrset_clear(&processed_refs);\n      +\tfree(compare_branches);\n      +\treturn reported;\n       }\n\n-- \ngitgitgadget\n"},{"id":"534493","messageId":"f3c8c782b0ae7d20cf023a315e373a38c1fe9511.1769112471.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v28.git.git.1769112471.gitgitgadget@gmail.com","subject":"[PATCH v28 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-22T20:07:50Z","receivedAt":"2026-01-22T20:07: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\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex b756ff6f15..fd592ec659 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"534492","messageId":"067978dd0946732e00e4805d62ea51348fbf336e.1769112471.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v28.git.git.1769112471.gitgitgadget@gmail.com","subject":"[PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-22T20:07:51Z","receivedAt":"2026-01-22T20:08: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\nAdd a new configuration variable `status.compareBranches` that allows\nusers to specify a space-separated list of branches to compare against\nthe current branch in `git status` output.\n\nEach branch in the list can be:\n- A remote-tracking branch name (e.g., `origin/main`)\n- The special reference `@{upstream}` for the tracking branch\n- The special reference `@{push}` for the push destination\n\nWhen not configured, the default behavior is equivalent to setting\n`status.compareBranches = @{upstream}`, preserving backward compatibility.\n\nThe advice messages shown are context-aware:\n- \"git pull\" advice is shown only when comparing against @{upstream}\n- \"git push\" advice is shown only when comparing against @{push}\n- Divergence advice is shown for upstream branch comparisons\n\nThis is useful for triangular workflows where the upstream tracking\nbranch differs from the push destination, allowing users to see their\nstatus relative to both branches at once.\n\nExample configuration:\n    [status]\n        compareBranches = @{upstream} @{push}\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/status.adoc |  20 ++\n remote.c                         | 159 ++++++++++++---\n t/t6040-tracking-info.sh         | 337 +++++++++++++++++++++++++++++++\n 3 files changed, 488 insertions(+), 28 deletions(-)\n\ndiff --git a/Documentation/config/status.adoc b/Documentation/config/status.adoc\nindex 8caf90f51c..ab54f25fbc 100644\n--- a/Documentation/config/status.adoc\n+++ b/Documentation/config/status.adoc\n@@ -17,6 +17,26 @@ status.aheadBehind::\n \t`--no-ahead-behind` by default in linkgit:git-status[1] for\n \tnon-porcelain status formats.  Defaults to true.\n \n+status.compareBranches::\n+\tA space-separated list of branches to compare the current branch\n+\tagainst in linkgit:git-status[1]. Each branch specification can be\n+\ta remote-tracking branch name (e.g. `origin/main`), or a special\n+\treference like `@{upstream}` or `@{push}`. For each branch in the\n+\tlist, git status shows whether the current branch is ahead, behind,\n+\tor has diverged from that branch.\n++\n+If not set, the default behavior is equivalent to `@{upstream}`, which\n+compares against the configured upstream tracking branch.\n++\n+Example:\n++\n+----\n+[status]\n+\tcompareBranches = origin/main origin/develop\n+----\n++\n+This would show comparisons against both `origin/main` and `origin/develop`.\n+\n status.displayCommentPrefix::\n \tIf set to true, linkgit:git-status[1] will insert a comment\n \tprefix before each output line (starting with\ndiff --git a/remote.c b/remote.c\nindex fd592ec659..1071a23567 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,12 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tENABLE_ADVICE_PULL       = (1 << 0),\n+\tENABLE_ADVICE_PUSH       = (1 << 1),\n+\tENABLE_ADVICE_DIVERGENCE = (1 << 2),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2230,13 +2236,53 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *resolve_compare_branch(struct branch *branch, const char *name)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tconst char *resolved = NULL;\n+\tchar *ret;\n+\n+\tif (!branch || !name)\n+\t\treturn NULL;\n+\n+\tif (!strcasecmp(name, \"@{upstream}\") || !strcasecmp(name, \"@{u}\"))\n+\t\tresolved = branch_get_upstream(branch, NULL);\n+\telse if (!strcasecmp(name, \"@{push}\"))\n+\t\tresolved = branch_get_push(branch, NULL);\n+\n+\tif (resolved)\n+\t\treturn xstrdup(resolved);\n+\n+\tstrbuf_addf(&buf, \"refs/remotes/%s\", name);\n+\tresolved = refs_resolve_ref_unsafe(\n+\t\tget_main_ref_store(the_repository),\n+\t\tbuf.buf,\n+\t\tRESOLVE_REF_READING,\n+\t\tNULL, NULL);\n+\tif (resolved) {\n+\t\tret = xstrdup(resolved);\n+\t\tstrbuf_release(&buf);\n+\t\treturn ret;\n+\t}\n+\n+\tstrbuf_release(&buf);\n+\treturn NULL;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n-\t\t\t\t     bool show_divergence_advice)\n+\t\t\t\t     unsigned flags)\n {\n+\tbool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2245,7 +2291,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2254,7 +2300,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2265,7 +2311,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2278,8 +2324,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2292,34 +2337,92 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t enum ahead_behind_flags abf,\n \t\t\t int show_divergence_advice)\n {\n-\tint ours, theirs, cmp_fetch;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n+\tchar *compare_branches = NULL;\n+\tstruct string_list branches = STRING_LIST_INIT_DUP;\n+\tstruct strset processed_refs = STRSET_INIT;\n+\tint reported = 0;\n+\tsize_t i;\n+\tconst char *upstream_ref;\n+\tconst char *push_ref;\n \n-\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (cmp_fetch < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n+\trepo_config_get_string(the_repository, \"status.comparebranches\",\n+\t\t\t       &compare_branches);\n+\n+\tif (compare_branches) {\n+\t\tstring_list_split(&branches, compare_branches, \" \", -1);\n+\t\tstring_list_remove_empty_items(&branches, 0);\n+\t} else {\n+\t\tstring_list_append(&branches, \"@{upstream}\");\n \t}\n \n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n+\tupstream_ref = branch_get_upstream(branch, NULL);\n+\tpush_ref = branch_get_push(branch, NULL);\n \n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\tfor (i = 0; i < branches.nr; i++) {\n+\t\tchar *full_ref;\n+\t\tchar *short_ref;\n+\t\tint ours, theirs, cmp;\n+\t\tint is_upstream, is_push;\n+\t\tunsigned flags = 0;\n+\n+\t\tfull_ref = resolve_compare_branch(branch,\n+\t\t\t\t\t\t  branches.items[i].string);\n+\t\tif (!full_ref)\n+\t\t\tcontinue;\n+\n+\t\tif (!strset_add(&processed_refs, full_ref)) {\n+\t\t\tfree(full_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tshort_ref = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), full_ref, 0);\n+\n+\t\tis_upstream = upstream_ref && !strcmp(full_ref, upstream_ref);\n+\t\tis_push = push_ref && !strcmp(full_ref, push_ref);\n+\n+\t\tif (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))\n+\t\t\tis_push = 1;\n+\n+\t\tcmp = stat_branch_pair(branch->refname, full_ref,\n+\t\t\t\t       &ours, &theirs, abf);\n+\n+\t\tif (cmp < 0) {\n+\t\t\tif (is_upstream) {\n+\t\t\t\tstrbuf_addf(sb,\n+\t\t\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\t\t\tshort_ref);\n+\t\t\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t\t\t\treported = 1;\n+\t\t\t}\n+\t\t\tfree(full_ref);\n+\t\t\tfree(short_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (reported)\n+\t\t\tstrbuf_addstr(sb, \"\\n\");\n+\n+\t\tif (is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_PULL;\n+\t\tif (is_push)\n+\t\t\tflags |= ENABLE_ADVICE_PUSH;\n+\t\tif (show_divergence_advice && is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_DIVERGENCE;\n+\t\tformat_branch_comparison(sb, !cmp, ours, theirs, short_ref,\n+\t\t\t\t\t abf, flags);\n+\t\treported = 1;\n+\n+\t\tfree(full_ref);\n+\t\tfree(short_ref);\n \t}\n \n-\tfree(base);\n-\treturn 1;\n+\tstring_list_clear(&branches, 0);\n+\tstrset_clear(&processed_refs);\n+\tfree(compare_branches);\n+\treturn reported;\n }\n \n static int one_local_ref(const struct reference *ref, void *cb_data)\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..f7859d2e5a 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,341 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for compareBranches tests' '\n+\t(\n+\t\tcd test &&\n+\t\tgit config push.default current &&\n+\t\tgit config status.compareBranches \"@{upstream} @{push}\"\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches from upstream has no duplicates' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch main\n+\tYour branch is up to date with ${SQ}origin/main${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows both branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches shows diverged and ahead' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches with upstream and origin remotes' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with upstream and origin remotes multiple compare branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit -c status.compareBranches=\"upstream/main origin/feature6 origin/feature5\" status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with diverged push branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature7${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows up to date branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature8 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature8 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with upstream behind and push up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b ahead upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push upstream HEAD &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit branch --set-upstream-to upstream/ahead &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n+\t  (use \"git pull\" to update your local branch)\n+\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature10 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature10\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push and upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature11 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature11\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clean up after compareBranches tests' '\n+\t(\n+\t\tcd test &&\n+\t\tgit config --unset status.compareBranches\n+\t)\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"534497","messageId":"xmqqcy31mmex.fsf@gitster.g","threadId":"64668","inReplyTo":"20260122192238.76368-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T20:18:14Z","receivedAt":"2026-01-22T20:18:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> Using a string list like this is quadratic. It probably doesn't matter\n>> too much here since we wouldn't expect the list of configured branches\n>> to be very long, though. But a strset is probably the better tool, like\n>> the diff below (note that its \"add\" can be used as a single operation to\n>> insert and check).\n>> \n>> I don't know if it's worth re-rolling for this or not. I doubt anybody\n>> would hit it in practice, but I'd be more concerned about people\n>> auditing for accidentally-quadratic uses of string_list and stumbling\n>> upon it.\n>\n> Sounds like a good change. Very nice to have insert-and-check as a single\n> operation!\n>\n> I'll update it after passing CI on GitHub -- I'm running CI from two\n> separate GitHub pull requests, because one has your memory leak fix on top\n> to allow the leak tests to not fail. 🤗\n\nThanks.  I think we are getting to the finish line with this topic,\nfinally ;-)\n"},{"id":"534498","messageId":"xmqq5x8tmlj7.fsf@gitster.g","threadId":"64668","inReplyTo":"0993420fc1185ec4a907a8c3bb52ca965e720c54.1769096240.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T20:37:16Z","receivedAt":"2026-01-22T20:37:20Z","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> diff --git a/remote.c b/remote.c\n> index fd592ec659..e256cc9b81 100644\n> --- a/remote.c\n> +++ b/remote.c\n> @@ -29,6 +29,12 @@\n>  \n>  enum map_direction { FROM_SRC, FROM_DST };\n>  \n> +enum {\n> +\tENABLE_ADVICE_PULL       = (1 << 0),\n> +\tENABLE_ADVICE_PUSH       = (1 << 1),\n> +\tENABLE_ADVICE_DIVERGENCE = (1 << 2),\n> +};\n> +\n>  struct counted_string {\n>  \tsize_t len;\n>  \tconst char *s;\n> @@ -2230,13 +2236,53 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n>  \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n>  }\n>  \n> +static char *resolve_compare_branch(struct branch *branch, const char *name)\n> +{\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tconst char *resolved = NULL;\n> +\tchar *ret;\n> +\n> +\tif (!branch || !name)\n> +\t\treturn NULL;\n> +\n> +\tif (!strcasecmp(name, \"@{upstream}\") || !strcasecmp(name, \"@{u}\"))\n> +\t\tresolved = branch_get_upstream(branch, NULL);\n> +\telse if (!strcasecmp(name, \"@{push}\"))\n> +\t\tresolved = branch_get_push(branch, NULL);\n\nOK.  Usually @{upstream} without anything before the at-sign means\nthe upstream of the current branch, but we need to force pretend\nthat branch were the current branch, so we'd need to special case\nlike this, which looks reasonable.\n\n> +\tif (resolved)\n> +\t\treturn xstrdup(resolved);\n> +\n> +\tstrbuf_addf(&buf, \"refs/remotes/%s\", name);\n> +\tresolved = refs_resolve_ref_unsafe(\n> +\t\tget_main_ref_store(the_repository),\n> +\t\tbuf.buf,\n> +\t\tRESOLVE_REF_READING,\n> +\t\tNULL, NULL);\n> +\tif (resolved) {\n> +\t\tret = xstrdup(resolved);\n> +\t\tstrbuf_release(&buf);\n> +\t\treturn ret;\n> +\t}\n\nIt would be handy to be able to say \"origin/master\" (or even just\n\"origin\", which is interpreted as \"origin/HEAD\" via the DWIM\nmachinery) and prepending of \"refs/remotes/\" above does help such\nDWIMmery, but I wonder if it is too limiting?  Would there be\nsituations where you would want to compare with something outside\nrefs/remotes/ hierarchy?  \n\nFor example, writing \"v2.52.0\" there to see how far we came since\nthe last release would become impossible if we always force prepend\n\"refs/remotes/\".  I wonder if we can reuse already existing DWIMmery\nthat uses refs.c::ref_rev_parse_rules[], which should allow such use\ncase, while still allowing you to write \"origin/master\"?\n"},{"id":"534500","messageId":"20260122205632.80273-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqq5x8tmlj7.fsf@gitster.g","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-22T20:56:32Z","receivedAt":"2026-01-22T20:56:36Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> For example, writing \"v2.52.0\" there to see how far we came since\n> the last release would become impossible if we always force prepend\n> \"refs/remotes/\".  I wonder if we can reuse already existing DWIMmery\n> that uses refs.c::ref_rev_parse_rules[], which should allow such use\n> case, while still allowing you to write \"origin/master\"?\n\nSounds like a follow-up rather of doing now, right? 🤗\n\nSince the inteface won't change, just adding more functionality a new\nfeature, we should be able to fix this behind the scenes later.\n\n\nHarald\n"},{"id":"534501","messageId":"xmqqwm19l5du.fsf@gitster.g","threadId":"64668","inReplyTo":"20260122205632.80273-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T21:11:25Z","receivedAt":"2026-01-22T21:11:31Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> For example, writing \"v2.52.0\" there to see how far we came since\n>> the last release would become impossible if we always force prepend\n>> \"refs/remotes/\".  I wonder if we can reuse already existing DWIMmery\n>> that uses refs.c::ref_rev_parse_rules[], which should allow such use\n>> case, while still allowing you to write \"origin/master\"?\n>\n> Sounds like a follow-up rather of doing now, right? 🤗\n>\n> Since the inteface won't change, just adding more functionality a new\n> feature, we should be able to fix this behind the scenes later.\n\nDisambiguation will make it harder or impossible to add such an\nenhancement later, though.\n\nWhen the user says \"git log origin\" or \"git show origin/main\",\nrefs.c::ref_rev_parse_rules[] is applied and turns these into\nrefs/refs/remotes/origin/HEAD or refs/remotes/origin/main, but as\nyou can see in refs.c::ref_rev_parse_rules[], these are at\nrelatively low precedence order.  Your version has refs/remotes/ as\nthe first (and only) choice, so if a user has refs/heads/origin/main\nand refs/remotes/origin/main at the same time, \"git show\norigin/main\" would use the former.  With the implementation of this\nround, \"[status] compareBranches = origin/main\" would pick\nrefs/remotes/origin/main and nothing else first, and then would\nstart behaving differently once such an enhancement to use the\nstandard DWIMmery rules is introduced, which may appear to be an\nunnecessary regression to end-users.\n\nSo, no, I wouldn't recommend it as a follow-up.  We should decide to\ndo so now or declare that we would never do so.\n\n"},{"id":"534503","messageId":"xmqqms25l47f.fsf@gitster.g","threadId":"64668","inReplyTo":"xmqqwm19l5du.fsf@gitster.g","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T21:36:52Z","receivedAt":"2026-01-22T21:36:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Harald Nordgren <haraldnordgren@gmail.com> writes:\n>\n>>> For example, writing \"v2.52.0\" there to see how far we came since\n>>> the last release would become impossible if we always force prepend\n>>> \"refs/remotes/\".  I wonder if we can reuse already existing DWIMmery\n>>> that uses refs.c::ref_rev_parse_rules[], which should allow such use\n>>> case, while still allowing you to write \"origin/master\"?\n>>\n>> Sounds like a follow-up rather of doing now, right? 🤗\n>>\n>> Since the inteface won't change, just adding more functionality a new\n>> feature, we should be able to fix this behind the scenes later.\n> ...\n> So, no, I wouldn't recommend it as a follow-up.  We should decide to\n> do so now or declare that we would never do so.\n\nAlternatively, if we remove the \"a string that is not @{upstream} or\n@{push} gets refs/remotes/ prefixed\" altogether, then \"the interface\nwon't change, just adding more functionality\" would become true.\n\nIt is not my itch, so I can go either way, but I'd prefer not to see\nus take the \"declare we will never use the unified ref DIWMmery\nrules here\" route, if we can.\n"},{"id":"534505","messageId":"20260122220154.GA2107958@coredump.intra.peff.net","threadId":"64668","inReplyTo":"xmqq5x8tmlj7.fsf@gitster.g","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-22T22:01:54Z","receivedAt":"2026-01-22T22:02:03Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Jan 22, 2026 at 12:37:16PM -0800, Junio C Hamano wrote:\n\n> > +static char *resolve_compare_branch(struct branch *branch, const char *name)\n> > +{\n> > +\tstruct strbuf buf = STRBUF_INIT;\n> > +\tconst char *resolved = NULL;\n> > +\tchar *ret;\n> > +\n> > +\tif (!branch || !name)\n> > +\t\treturn NULL;\n> > +\n> > +\tif (!strcasecmp(name, \"@{upstream}\") || !strcasecmp(name, \"@{u}\"))\n> > +\t\tresolved = branch_get_upstream(branch, NULL);\n> > +\telse if (!strcasecmp(name, \"@{push}\"))\n> > +\t\tresolved = branch_get_push(branch, NULL);\n> \n> OK.  Usually @{upstream} without anything before the at-sign means\n> the upstream of the current branch, but we need to force pretend\n> that branch were the current branch, so we'd need to special case\n> like this, which looks reasonable.\n\nYeah, it is unfortunate to have to special-case these names, even though\nthe resolving functions already know about them. If we are looking at\nbranch \"foo\" we could rewrite them as \"foo@{upstream}\", etc, but that\nalso requires special-casing (though maybe slightly less, if we just\naccept the @ sign?).\n\nI can think of two alternatives, though.\n\nOne is that repo_interpret_branch_name() could accept a field in its\noptions struct to set the default branch (rather than \"HEAD\"). Something\nlike this (totally untested):\n\ndiff --git a/object-name.c b/object-name.c\nindex 8b862c124e..925a487d84 100644\n--- a/object-name.c\n+++ b/object-name.c\n@@ -1732,7 +1732,7 @@ static int interpret_branch_mark(struct repository *r,\n \t\tbranch = branch_get(name_str);\n \t\tfree(name_str);\n \t} else\n-\t\tbranch = branch_get(NULL);\n+\t\tbranch = branch_get(options->default_branch);\n \n \tvalue = get_data(branch, &err);\n \tif (!value) {\ndiff --git a/object-name.h b/object-name.h\nindex cda4934cd5..962f99b0f6 100644\n--- a/object-name.h\n+++ b/object-name.h\n@@ -119,6 +119,12 @@ struct interpret_branch_name_options {\n \t * of die()-ing.\n \t */\n \tunsigned nonfatal_dangling_mark : 1;\n+\n+\t/*\n+\t * Pass this to branch_get() when interpreting @-marks without a\n+\t * branch, rather than using HEAD.\n+\t */\n+\tconst char *default_branch;\n };\n int repo_interpret_branch_name(struct repository *r,\n \t\t\t       const char *str, int len,\n\nMost callers would continue to pass NULL in the usual way, but the\nresolution here would pass in the current branch name.\n\n\nThe second is a bit more complicated, but is even more flexible. Part of\nthe point of this status.compareBranches approach is that you can add\nregular refnames to the list. But would a user want to use a name that\nis derived from the comparison branch, rather than just a static name?\nThat is, to compare branch \"foo\" against \"origin/foo\"? Usually that is\nexactly the kind of refspec-application that @{upstream} and @{push} are\ncomputing (after taking into account various config). But if you have a\nthird source of refs, would you want to be able to compare to\n\"origin/%s\", where %s is the shortened branch name?\n\nIn which case these values could become \"%s@{upstream}\" and \"%s@{push}\",\nand they could just be fed straight to the branch-interpret machinery.\n\n> > +\tstrbuf_addf(&buf, \"refs/remotes/%s\", name);\n> > +\tresolved = refs_resolve_ref_unsafe(\n> > +\t\tget_main_ref_store(the_repository),\n> > +\t\tbuf.buf,\n> > +\t\tRESOLVE_REF_READING,\n> > +\t\tNULL, NULL);\n> > +\tif (resolved) {\n> > +\t\tret = xstrdup(resolved);\n> > +\t\tstrbuf_release(&buf);\n> > +\t\treturn ret;\n> > +\t}\n> \n> It would be handy to be able to say \"origin/master\" (or even just\n> \"origin\", which is interpreted as \"origin/HEAD\" via the DWIM\n> machinery) and prepending of \"refs/remotes/\" above does help such\n> DWIMmery, but I wonder if it is too limiting?  Would there be\n> situations where you would want to compare with something outside\n> refs/remotes/ hierarchy?\n> \n> For example, writing \"v2.52.0\" there to see how far we came since\n> the last release would become impossible if we always force prepend\n> \"refs/remotes/\".  I wonder if we can reuse already existing DWIMmery\n> that uses refs.c::ref_rev_parse_rules[], which should allow such use\n> case, while still allowing you to write \"origin/master\"?\n\nYeah, I think tags or even local branches would be plausible candidates.\nBut at any rate, I'd expect these to be resolved in the \"usual\" way that\nwe do elsewhere. If we switch to using repo_dwim_ref() or something that\ninterprets @-marks, then that matches nicely with the suggestions I gave\nabove.\n\n-Peff\n"},{"id":"534510","messageId":"20260122224427.GB2107958@coredump.intra.peff.net","threadId":"64668","inReplyTo":"20260122220154.GA2107958@coredump.intra.peff.net","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-22T22:44:27Z","receivedAt":"2026-01-22T22:44:30Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Jan 22, 2026 at 05:01:54PM -0500, Jeff King wrote:\n\n> The second is a bit more complicated, but is even more flexible. Part of\n> the point of this status.compareBranches approach is that you can add\n> regular refnames to the list. But would a user want to use a name that\n> is derived from the comparison branch, rather than just a static name?\n> That is, to compare branch \"foo\" against \"origin/foo\"? Usually that is\n> exactly the kind of refspec-application that @{upstream} and @{push} are\n> computing (after taking into account various config). But if you have a\n> third source of refs, would you want to be able to compare to\n> \"origin/%s\", where %s is the shortened branch name?\n> \n> In which case these values could become \"%s@{upstream}\" and \"%s@{push}\",\n> and they could just be fed straight to the branch-interpret machinery.\n\nSo here's a sketch to show what that might look like:\n\ndiff --git a/remote.c b/remote.c\nindex 1071a23567..6f9a28970c 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2239,34 +2239,30 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n static char *resolve_compare_branch(struct branch *branch, const char *name)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n-\tconst char *resolved = NULL;\n+\tstruct object_id oid;\n \tchar *ret;\n \n \tif (!branch || !name)\n \t\treturn NULL;\n \n-\tif (!strcasecmp(name, \"@{upstream}\") || !strcasecmp(name, \"@{u}\"))\n-\t\tresolved = branch_get_upstream(branch, NULL);\n-\telse if (!strcasecmp(name, \"@{push}\"))\n-\t\tresolved = branch_get_push(branch, NULL);\n-\n-\tif (resolved)\n-\t\treturn xstrdup(resolved);\n-\n-\tstrbuf_addf(&buf, \"refs/remotes/%s\", name);\n-\tresolved = refs_resolve_ref_unsafe(\n-\t\tget_main_ref_store(the_repository),\n-\t\tbuf.buf,\n-\t\tRESOLVE_REF_READING,\n-\t\tNULL, NULL);\n-\tif (resolved) {\n-\t\tret = xstrdup(resolved);\n-\t\tstrbuf_release(&buf);\n-\t\treturn ret;\n+\twhile (strbuf_expand_step(&buf, &name)) {\n+\t\tif (skip_prefix(name, \"%\", &name))\n+\t\t\tstrbuf_addch(&buf, '%');\n+\t\telse if (skip_prefix(name, \"s\", &name))\n+\t\t\tstrbuf_addstr(&buf, branch->name);\n+\t\telse\n+\t\t\tdie(\"bad compareBranches format: %%%s\", name);\n \t}\n \n+\t/*\n+\t * This will leave \"ret\" as NULL if we don't find anything. If there\n+\t * are multiple ambiguous matches it will pick the first. Should we\n+\t * return an error instead?\n+\t */\n+\trepo_dwim_ref(the_repository, buf.buf, buf.len, &oid, &ret, 1);\n+\n \tstrbuf_release(&buf);\n-\treturn NULL;\n+\treturn ret;\n }\n \n static void format_branch_comparison(struct strbuf *sb,\n@@ -2352,7 +2348,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tstring_list_split(&branches, compare_branches, \" \", -1);\n \t\tstring_list_remove_empty_items(&branches, 0);\n \t} else {\n-\t\tstring_list_append(&branches, \"@{upstream}\");\n+\t\tstring_list_append(&branches, \"%s@{upstream}\");\n \t}\n \n \tupstream_ref = branch_get_upstream(branch, NULL);\n\n\nI ran into two small complications. The first is that there's an\ninteresting race \"git-checkout -b\":\n\n  1. We read branch and remote config via remote.c:read_config(),\n     caching it.\n\n  2. Then we write some new config for the branch we just created.\n\n  3. Then we try to report tracking info. This will read the cached\n     config from step 1, without regard to the changes in 2.\n\nIt works without my patch above because the tracking code currently\noperates directly on a \"struct branch\" (that we just updated), rather\nthan using the stale config from (1).\n\nWe can fix that like this:\n\ndiff --git a/branch.c b/branch.c\nindex 243db7d0fc..cc24e8522d 100644\n--- a/branch.c\n+++ b/branch.c\n@@ -178,6 +178,12 @@ static int install_branch_config_multiple_remotes(int flag, const char *local,\n \t\tstring_list_clear(&friendly_ref_names, 0);\n \t}\n \n+\t/*\n+\t * We installed new config; flush any internal cache of the on-disk\n+\t * config.\n+\t */\n+\tremote_state_clear(the_repository->remote_state);\n+\n \treturn 0;\n \n out_err:\ndiff --git a/remote.c b/remote.c\nindex 6f9a28970c..e6056199f0 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2859,6 +2859,14 @@ void remote_state_clear(struct remote_state *remote_state)\n \t\tfree(b);\n \t}\n \thashmap_clear(&remote_state->branches_hash);\n+\n+\t/*\n+\t * Eek, we just cleared everything out; we should mark ourselves as\n+\t * uninitialized so we can be used again. Nobody seems to have noticed\n+\t * because we only remote_state_clear() when we are about to discard\n+\t * the struct entirely.\n+\t */\n+\tremote_state->initialized = 0;\n }\n \n /*\n\n\nThe second issue concerns the case when an upstream is configured, but\nthe tracking ref for it is missing. So imagine \"foo\" is configured with\n\"refs/remotes/origin/foo\" as its upstream, but that branch is gone.\n\nUsing branch_get_upstream() will return the name, even if it doesn't\nexist. And then the tracking-info code recognizes this and reports it.\nBut repo_dwim_ref() won't return a missing ref at all, even with\nnonfatal_dangling_mark. So I think we'd need to teach it a new option to\ndo so.\n\n\nI don't know what all of it means. The \"%s\" thing was short to\nimplement, but the real source of the extra complications is using\nrepo_dwim_ref() to do the resolution. But I think the overall direction\nis more consistent with how the rest of Git behaves.\n\n-Peff\n"},{"id":"534511","messageId":"20260122230656.GA2125611@coredump.intra.peff.net","threadId":"64668","inReplyTo":"20260122224427.GB2107958@coredump.intra.peff.net","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-22T23:06:56Z","receivedAt":"2026-01-22T23:06:59Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Jan 22, 2026 at 05:44:27PM -0500, Jeff King wrote:\n\n> The second issue concerns the case when an upstream is configured, but\n> the tracking ref for it is missing. So imagine \"foo\" is configured with\n> \"refs/remotes/origin/foo\" as its upstream, but that branch is gone.\n> \n> Using branch_get_upstream() will return the name, even if it doesn't\n> exist. And then the tracking-info code recognizes this and reports it.\n> But repo_dwim_ref() won't return a missing ref at all, even with\n> nonfatal_dangling_mark. So I think we'd need to teach it a new option to\n> do so.\n\nSo I think that nonfatal_dangling_mark could arguably return the name\n(but no oid) for this case, like so:\n\ndiff --git a/refs.c b/refs.c\nindex 627b7f8698..2316d0b8a9 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -813,7 +813,13 @@ int repo_dwim_ref(struct repository *r, const char *str, int len,\n \tchar *last_branch = substitute_branch_name(r, &str, &len,\n \t\t\t\t\t\t   nonfatal_dangling_mark);\n \tint   refs_found  = expand_ref(r, str, len, oid, ref);\n-\tfree(last_branch);\n+\tif (nonfatal_dangling_mark && !refs_found && last_branch) {\n+\t\toidclr(oid, r->hash_algo);\n+\t\t*ref = last_branch;\n+\t\trefs_found = 1;\n+\t} else {\n+\t\tfree(last_branch);\n+\t}\n \treturn refs_found;\n }\n \n\nBut this brings up yet another corner case: substitute_branch_name()\nwill call shorten_unambiguous_ref() on the result when expanding\n@-marks. So last_branch here might be \"origin/foo\" instead of\n\"refs/remotes/origin/foo\". It's usually not a big deal because\nexpand_ref() will then reverse that shortening.  Which is maybe\nwasteful, but not so bad (though if somebody racily creates an ambiguous\nref, it could affect the results).\n\nBut if we return last_branch explicitly, then information is lost: we\ndon't know what the fully-qualified version of \"origin/foo\" was supposed\nto be. And so the tracking-info code gets confused, because it doesn't\nrealize that \"origin/foo\" was supposed to be our @{upstream}.\n\nI think probably the interpret_branch_name() code should have an option\nto avoid that shortening. So if we do this on top:\n\ndiff --git a/object-name.c b/object-name.c\nindex 8b862c124e..0663946f81 100644\n--- a/object-name.c\n+++ b/object-name.c\n@@ -1747,7 +1747,12 @@ static int interpret_branch_mark(struct repository *r,\n \tif (!branch_interpret_allowed(value, options->allowed))\n \t\treturn -1;\n \n-\tset_shortened_ref(r, buf, value);\n+\tif (options->do_not_shorten) {\n+\t\tstrbuf_reset(buf);\n+\t\tstrbuf_addstr(buf, value);\n+\t} else {\n+\t\tset_shortened_ref(r, buf, value);\n+\t}\n \treturn len + at;\n }\n \ndiff --git a/object-name.h b/object-name.h\nindex cda4934cd5..20393cb213 100644\n--- a/object-name.h\n+++ b/object-name.h\n@@ -119,6 +119,8 @@ struct interpret_branch_name_options {\n \t * of die()-ing.\n \t */\n \tunsigned nonfatal_dangling_mark : 1;\n+\n+\tunsigned do_not_shorten : 1;\n };\n int repo_interpret_branch_name(struct repository *r,\n \t\t\t       const char *str, int len,\ndiff --git a/refs.c b/refs.c\nindex 2316d0b8a9..58f945e213 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -746,6 +746,7 @@ static char *substitute_branch_name(struct repository *r,\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct interpret_branch_name_options options = {\n+\t\t.do_not_shorten = 1,\n \t\t.nonfatal_dangling_mark = nonfatal_dangling_mark\n \t};\n \tint ret = repo_interpret_branch_name(r, *string, *len, &buf, &options);\n\nthen all of t6040 passes (curiously without me swapping in \"%s@{push}\"\nas appropriate; I guess we are often on the branch of interest anyway,\nso it accidentally works with just @{push}).\n\n\nSo I don't love how deep the rabbit-hole has gone here. But at the same\ntime, it feels like all of these \"if we just do this on top\" fixes are\nactually smoothing some rough edges in the rest of Git. So maybe it's\nworth it.\n\n-Peff\n"},{"id":"534513","messageId":"xmqqsebxjl3h.fsf@gitster.g","threadId":"64668","inReplyTo":"20260122224427.GB2107958@coredump.intra.peff.net","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T23:14:58Z","receivedAt":"2026-01-22T23:15:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> I don't know what all of it means. The \"%s\" thing was short to\n> implement, but the real source of the extra complications is using\n> repo_dwim_ref() to do the resolution. But I think the overall direction\n> is more consistent with how the rest of Git behaves.\n\nYeah, I tend to agree.  As long as we do not hardcode the\nprefixing of \"refs/remotes/\", we won't paint ourselves into a corner\nwe cannot get out of, I guess.\n\nThanks.\n"},{"id":"534593","messageId":"20260124085034.8730-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260122230656.GA2125611@coredump.intra.peff.net","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-01-24T08:50:34Z","receivedAt":"2026-01-24T08:50:38Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"I can apply the changes from these three messages, but I don't really\nknow the side-effects of it. Should I do it and submit a patch?\n\nI noticed that it still won't work with tags (although it starts working\nwith 'origin' which then defaults to origin/HEAD). I can imagine we need a\nfew more tests on top of that.\n\n\nHarald\n"},{"id":"534615","messageId":"xmqq4io9lhy4.fsf@gitster.g","threadId":"64668","inReplyTo":"20260124085034.8730-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-25T17:29:07Z","receivedAt":"2026-01-25T17:29:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> I can apply the changes from these three messages, but I don't really\n> know the side-effects of it. Should I do it and submit a patch?\n\nPlease do not send code you cannot answer to questions on it,\nwhether it is written by somebody else,  genAI, or your cat rolling\non your keyboard ;-).\n\nI think we are getting close but will need more polish before we can\nallow users to reuse their already acquired knowledge of how refname\nDWIMmery can be used to spell various refs they mean.\n\nIf we support only @{push} and @{upstream} and error out when we see\nanything else (like \"origin\" or \"origin/main\") in the initial\nversion we ship to the end-users, that would probably be a good\nstopping point.  On top of it, we can later add the DWIMmery Peff\nhas shown (with necessary tweaks, as you found out, like supporting\ntags, perhaps), and that will be purely new feature that does not\nchange any behaviour of what used to work for our users in the\ninitial version.\n\nGoing that way is much safer and does not break end-user\nexperiences, like shipping the first version with \"we always prefix\nhardcoded refs/remotes/ unless it is @{something}\", which will have\nto change the behaviour once the proper DWIMmery gets implemented.\n\nIn any case, that will have to happen all after the current cycle is\nover, it is way too late even for \"@{push} and @{upstream} only\"\nversion for this cycle.\n"},{"id":"535475","messageId":"20260208133348.91370-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260122230656.GA2125611@coredump.intra.peff.net","subject":"Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-02-08T13:33:48Z","receivedAt":"2026-02-08T13:33:51Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi Jeff!\n\nDo you think this is ready to be merged? I think you are the holdout here 🤗\n\n\nHarald\n"},{"id":"536576","messageId":"20260221080201.7847-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"067978dd0946732e00e4805d62ea51348fbf336e.1769112471.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-02-21T08:02:01Z","receivedAt":"2026-02-21T08:02:04Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Hi Junio and Jeff!\n\nI see that this topic has now been marked as \"Stale\". Isn't it time to\nmerge this now?\n\nWe went to several rounds of reviews and edits to reach something that I\nthink everyone agrees with 🤗\n\n\nHarald\n"},{"id":"536597","messageId":"xmqqwm06rp6p.fsf@gitster.g","threadId":"64668","inReplyTo":"20260221080201.7847-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-21T17:17:50Z","receivedAt":"2026-02-21T17:17:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> Hi Junio and Jeff!\n>\n> I see that this topic has now been marked as \"Stale\". Isn't it time to\n> merge this now?\n>\n> We went to several rounds of reviews and edits to reach something that I\n> think everyone agrees with 🤗\n\nThe entry has been asking \"What's the status of this topic?\" in the\npast handful of issues of \"What's cooking\" report and we heard\nnothing from anybody (until you responded ;-)), so I moved it\ntogether with others to the [Stalled] section.\n\nThe topic has been paged out of my consciousness for quite a while,\nso it may take some time for me to answer this question myself, but\nlist participants, do you imagine yourselves using this feature in\nyour daily life?\n\nThanks.\n"},{"id":"536643","messageId":"CALnO6CD_dCoHCOVAUbXE-5LmgU3tVOhyYchDTpAGO+acYPaqRw@mail.gmail.com","threadId":"64668","inReplyTo":"xmqqwm06rp6p.fsf@gitster.g","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-02-22T14:50:19Z","receivedAt":"2026-02-22T14:50:31Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Sat, Feb 21, 2026 at 12:18 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Harald Nordgren <haraldnordgren@gmail.com> writes:\n>\n> > Hi Junio and Jeff!\n> >\n> > I see that this topic has now been marked as \"Stale\". Isn't it time to\n> > merge this now?\n> >\n> > We went to several rounds of reviews and edits to reach something that I\n> > think everyone agrees with 🤗\n>\n> The entry has been asking \"What's the status of this topic?\" in the\n> past handful of issues of \"What's cooking\" report and we heard\n> nothing from anybody (until you responded ;-)), so I moved it\n> together with others to the [Stalled] section.\n>\n> The topic has been paged out of my consciousness for quite a while,\n> so it may take some time for me to answer this question myself, but\n> list participants, do you imagine yourselves using this feature in\n> your daily life?\n>\n> Thanks.\n\nI'm trying to run something closer to the tip of \"next\" at home and\nwork, and I can see a few places where I might try this, at least.\n\n-- \nD. Ben Knoble\n"},{"id":"536788","messageId":"20260223133034.GD215671@coredump.intra.peff.net","threadId":"64668","inReplyTo":"xmqqwm06rp6p.fsf@gitster.g","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-02-23T13:30:34Z","receivedAt":"2026-02-23T13:30:35Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sat, Feb 21, 2026 at 09:17:50AM -0800, Junio C Hamano wrote:\n\n> Harald Nordgren <haraldnordgren@gmail.com> writes:\n> \n> > Hi Junio and Jeff!\n> >\n> > I see that this topic has now been marked as \"Stale\". Isn't it time to\n> > merge this now?\n> >\n> > We went to several rounds of reviews and edits to reach something that I\n> > think everyone agrees with 🤗\n> \n> The entry has been asking \"What's the status of this topic?\" in the\n> past handful of issues of \"What's cooking\" report and we heard\n> nothing from anybody (until you responded ;-)), so I moved it\n> together with others to the [Stalled] section.\n> \n> The topic has been paged out of my consciousness for quite a while,\n> so it may take some time for me to answer this question myself, but\n> list participants, do you imagine yourselves using this feature in\n> your daily life?\n\nI do not personally have any interest in using the feature (I only\njoined the thread to complain that it could not be turned off ;) ).\n\nSo in that sense I don't care how far it takes things along the path of\nwhat was discussed in the v27 review. My main concern is that we not\npaint ourselves into a corner and be stuck forever with an interface\nthat is inconsistent with the rest of Git.\n\nSo what about this. We add a config option that takes a list of items to\ncompare against. But it _only_ supports @{upstream} and @{push} for now,\nand interprets them as branch@{upstream} and branch@{push} (but probably\ndone manually, not via dwim_ref). That limitation gets documented.\n\nThen later, if we choose to allow resolving arbitrary refs via\ndwim_ref(), we can add that support on top:\n\n  - rather than manually resolving @{upstream}, quietly rewrite it to\n    branch@{upstream}. This is OK since branchless @{upstream} makes no\n    sense in this context.\n\n  - otherwise, pass what the user gives us to repo_dwim_ref()\n\n  - optionally allow substitution via %s or similar. Technically I think\n    \"%\" is allowed in a ref. I think that's probably OK in practice,\n    though if we want to be really careful, then \"[branch]\" or something\n    that's forbidden in the ref format would be a possible substitution.\n\nBut if we stop short of all of that today, we don't have to worry about\nall of those weird corner cases I dug up in the earlier thread. And if\nnobody ever _really_ cares about sticking arbitrary refs into their\ncompare-list, then we just never go there.\n\n-Peff\n"},{"id":"537012","messageId":"20260224193639.95757-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260223133034.GD215671@coredump.intra.peff.net","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-02-24T19:36:39Z","receivedAt":"2026-02-24T19:36:42Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"I can only speak for myself, but I use this feature every day!\n\n\nHarald\n"},{"id":"537029","messageId":"xmqqms0xeqaq.fsf@gitster.g","threadId":"64668","inReplyTo":"20260223133034.GD215671@coredump.intra.peff.net","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-24T22:21:17Z","receivedAt":"2026-02-24T22:21:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> So what about this. We add a config option that takes a list of items to\n> compare against. But it _only_ supports @{upstream} and @{push} for now,\n> and interprets them as branch@{upstream} and branch@{push} (but probably\n> done manually, not via dwim_ref). That limitation gets documented.\n\nSounds like a good way forward.\n\n> Then later, if we choose to allow resolving arbitrary refs via\n> dwim_ref(), we can add that support on top:\n>\n>   - rather than manually resolving @{upstream}, quietly rewrite it to\n>     branch@{upstream}. This is OK since branchless @{upstream} makes no\n>     sense in this context.\n>\n>   - otherwise, pass what the user gives us to repo_dwim_ref()\n>\n>   - optionally allow substitution via %s or similar. Technically I think\n>     \"%\" is allowed in a ref. I think that's probably OK in practice,\n>     though if we want to be really careful, then \"[branch]\" or something\n>     that's forbidden in the ref format would be a possible substitution.\n>\n> But if we stop short of all of that today, we don't have to worry about\n> all of those weird corner cases I dug up in the earlier thread. And if\n> nobody ever _really_ cares about sticking arbitrary refs into their\n> compare-list, then we just never go there.\n\n;-).\n"},{"id":"537079","messageId":"20260225102228.95152-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqms0xeqaq.fsf@gitster.g","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-02-25T10:22:28Z","receivedAt":"2026-02-25T10:22:31Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":">> So what about this. We add a config option that takes a list of items to\n>> compare against. But it _only_ supports @{upstream} and @{push} for now,\n>> and interprets them as branch@{upstream} and branch@{push} (but probably\n>> done manually, not via dwim_ref). That limitation gets documented.\n>\n> Sounds like a good way forward.\n\nSo if I do these changes, are we ready to merge then?\n\nI worked on this feature for 2 months, and then it got marked as stale\ninstead of being merged. Will this time be different?\n\n\nHarald\n"},{"id":"537093","messageId":"xmqqzf4wbzev.fsf@gitster.g","threadId":"64668","inReplyTo":"20260225102228.95152-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T15:44:56Z","receivedAt":"2026-02-25T15:44:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>>> So what about this. We add a config option that takes a list of items to\n>>> compare against. But it _only_ supports @{upstream} and @{push} for now,\n>>> and interprets them as branch@{upstream} and branch@{push} (but probably\n>>> done manually, not via dwim_ref). That limitation gets documented.\n>>\n>> Sounds like a good way forward.\n>\n> So if I do these changes, are we ready to merge then?\n\nI do not think of other things that needs to be done to the design\nat this moment.  It of course does not give any guarantee that\nothers won't find flaws in what we have discussed so far, though ;-)\n\n> I worked on this feature for 2 months, and then it got marked as stale\n> instead of being merged. Will this time be different?\n\nA topic becoming stalled is something the original author can (and\nhas the primary responsibility to) avoid by keeping the discussion\nthread alive by responding to reviews, pinging the thread with\ncomments similar to \"now I think this one is done, all comments are\naddressed by either updating the code or replying why we would not\nwant to go there (which the reviewer who made the comment hasn't\nresponded yet, so the ball is in their court)\", etc.  Even though I\ntry to help keep the ball rolling by pinging discussion threads that\nsmell about to go stalled from time to time, I cannot guarantee that\nit would not happen again.   But you can help ;-).\n\nThanks for working on this topic.\n\n\n"},{"id":"537097","messageId":"20260225160842.GA2160019@coredump.intra.peff.net","threadId":"64668","inReplyTo":"xmqqzf4wbzev.fsf@gitster.g","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-02-25T16:08:42Z","receivedAt":"2026-02-25T16:08:44Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Feb 25, 2026 at 07:44:56AM -0800, Junio C Hamano wrote:\n\n> > I worked on this feature for 2 months, and then it got marked as stale\n> > instead of being merged. Will this time be different?\n> \n> A topic becoming stalled is something the original author can (and\n> has the primary responsibility to) avoid by keeping the discussion\n> thread alive by responding to reviews, pinging the thread with\n> comments similar to \"now I think this one is done, all comments are\n> addressed by either updating the code or replying why we would not\n> want to go there (which the reviewer who made the comment hasn't\n> responded yet, so the ball is in their court)\", etc.  Even though I\n> try to help keep the ball rolling by pinging discussion threads that\n> smell about to go stalled from time to time, I cannot guarantee that\n> it would not happen again.   But you can help ;-).\n\nTo be fair to Harald, he did ping a few times, but I think was just\nunsure how to proceed after the issues raised in the last round.\n\nI think what would have moved things forward more than just a ping,\nthough, is generating a plan for moving forward in the face of confusing\nreview. That can be quite hard for first-time contributors, though.\n\nThat was why I tried to lay something out in the last email. I _think_\nit would not be too much work to get there from the last iteration. It\nwould mostly be removing code/feature that we're not ready to commit to\nyet.\n\n-Peff\n"},{"id":"537101","messageId":"xmqqo6lcbw9m.fsf@gitster.g","threadId":"64668","inReplyTo":"20260225160842.GA2160019@coredump.intra.peff.net","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T16:52:53Z","receivedAt":"2026-02-25T16:52:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> That was why I tried to lay something out in the last email. I _think_\n> it would not be too much work to get there from the last iteration. It\n> would mostly be removing code/feature that we're not ready to commit to\n> yet.\n\nSounds fair.  Thanks.\n"},{"id":"537103","messageId":"CALnO6CC5WE=K4sX+1NdNc+dV7KJWQvgdChoOfWWJyDQKH5uzJA@mail.gmail.com","threadId":"64668","inReplyTo":"20260225102228.95152-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-02-25T17:04:52Z","receivedAt":"2026-02-25T17:05:05Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Wed, Feb 25, 2026 at 5:24 AM Harald Nordgren\n<haraldnordgren@gmail.com> wrote:\n>\n> >> So what about this. We add a config option that takes a list of items to\n> >> compare against. But it _only_ supports @{upstream} and @{push} for now,\n> >> and interprets them as branch@{upstream} and branch@{push} (but probably\n> >> done manually, not via dwim_ref). That limitation gets documented.\n> >\n> > Sounds like a good way forward.\n>\n> So if I do these changes, are we ready to merge then?\n>\n> I worked on this feature for 2 months, and then it got marked as stale\n> instead of being merged. Will this time be different?\n>\n>\n> Harald\n\nTry not to be discouraged. It is not unusual for a series to undergo\nlengthy work and many revisions. It's part of what keeps Git working\nso well.\n\n-- \nD. Ben Knoble\n"},{"id":"537151","messageId":"pull.2138.v29.git.git.1772056263.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v28.git.git.1769112471.gitgitgadget@gmail.com","subject":"[PATCH v29 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T21:51:01Z","receivedAt":"2026-02-25T21:51:06Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im cc:\nJeff King peff@peff.net\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: add status.compareBranches config for multiple branch\n    comparisons\n\n Documentation/config/status.adoc |  19 ++\n remote.c                         | 180 +++++++++++++----\n t/t6040-tracking-info.sh         | 335 +++++++++++++++++++++++++++++++\n 3 files changed, 497 insertions(+), 37 deletions(-)\n\n\nbase-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v29\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v29\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v28:\n\n 1:  f3c8c782b0 = 1:  48db1f4847 refactor format_branch_comparison in preparation\n 2:  067978dd09 ! 2:  6a88f41fa5 status: add status.compareBranches config for multiple branch comparisons\n     @@ Metadata\n       ## Commit message ##\n          status: add status.compareBranches config for multiple branch comparisons\n      \n     -    Add a new configuration variable `status.compareBranches` that allows\n     -    users to specify a space-separated list of branches to compare against\n     -    the current branch in `git status` output.\n     +    Add a new configuration variable status.compareBranches that allows\n     +    users to specify a space-separated list of branch comparisons in\n     +    git status output.\n      \n     -    Each branch in the list can be:\n     -    - A remote-tracking branch name (e.g., `origin/main`)\n     -    - The special reference `@{upstream}` for the tracking branch\n     -    - The special reference `@{push}` for the push destination\n     +    Supported values:\n     +    - @{upstream} for the current branch's upstream tracking branch\n     +    - @{push} for the current branch's push destination\n     +\n     +    Any other value is ignored and a warning is shown.\n      \n          When not configured, the default behavior is equivalent to setting\n     -    `status.compareBranches = @{upstream}`, preserving backward compatibility.\n     +    `status.compareBranches = @{upstream}`, preserving backward\n     +    compatibility.\n      \n          The advice messages shown are context-aware:\n          - \"git pull\" advice is shown only when comparing against @{upstream}\n     @@ Documentation/config/status.adoc: status.aheadBehind::\n       \tnon-porcelain status formats.  Defaults to true.\n       \n      +status.compareBranches::\n     -+\tA space-separated list of branches to compare the current branch\n     -+\tagainst in linkgit:git-status[1]. Each branch specification can be\n     -+\ta remote-tracking branch name (e.g. `origin/main`), or a special\n     -+\treference like `@{upstream}` or `@{push}`. For each branch in the\n     -+\tlist, git status shows whether the current branch is ahead, behind,\n     -+\tor has diverged from that branch.\n     ++\tA space-separated list of branch comparison specifiers to use in\n     ++\tlinkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}`\n     ++\tare supported. They are interpreted as `branch@{upstream}` and\n     ++\t`branch@{push}` for the current branch.\n      ++\n      +If not set, the default behavior is equivalent to `@{upstream}`, which\n      +compares against the configured upstream tracking branch.\n     @@ Documentation/config/status.adoc: status.aheadBehind::\n      ++\n      +----\n      +[status]\n     -+\tcompareBranches = origin/main origin/develop\n     ++\tcompareBranches = @{upstream} @{push}\n      +----\n      ++\n     -+This would show comparisons against both `origin/main` and `origin/develop`.\n     ++This would show comparisons against both the configured upstream and push\n     ++tracking branches for the current branch.\n      +\n       status.displayCommentPrefix::\n       \tIf set to true, linkgit:git-status[1] will insert a comment\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n       \n      +static char *resolve_compare_branch(struct branch *branch, const char *name)\n      +{\n     -+\tstruct strbuf buf = STRBUF_INIT;\n      +\tconst char *resolved = NULL;\n     -+\tchar *ret;\n      +\n      +\tif (!branch || !name)\n      +\t\treturn NULL;\n      +\n     -+\tif (!strcasecmp(name, \"@{upstream}\") || !strcasecmp(name, \"@{u}\"))\n     ++\tif (!strcasecmp(name, \"@{upstream}\"))\n      +\t\tresolved = branch_get_upstream(branch, NULL);\n      +\telse if (!strcasecmp(name, \"@{push}\"))\n      +\t\tresolved = branch_get_push(branch, NULL);\n     ++\telse {\n     ++\t\twarning(_(\"ignoring value '%s' for status.compareBranches; only @{upstream} and @{push} are supported\"),\n     ++\t\t\tname);\n     ++\t\treturn NULL;\n     ++\t}\n      +\n      +\tif (resolved)\n      +\t\treturn xstrdup(resolved);\n     -+\n     -+\tstrbuf_addf(&buf, \"refs/remotes/%s\", name);\n     -+\tresolved = refs_resolve_ref_unsafe(\n     -+\t\tget_main_ref_store(the_repository),\n     -+\t\tbuf.buf,\n     -+\t\tRESOLVE_REF_READING,\n     -+\t\tNULL, NULL);\n     -+\tif (resolved) {\n     -+\t\tret = xstrdup(resolved);\n     -+\t\tstrbuf_release(&buf);\n     -+\t\treturn ret;\n     -+\t}\n     -+\n     -+\tstrbuf_release(&buf);\n      +\treturn NULL;\n      +}\n      +\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status.compareBranches with upstream and origin remotes multiple compare branches' '\n     ++test_expect_success 'status.compareBranches supports ordered upstream/push entries' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit checkout -b feature6 upstream/main &&\n      +\t\tgit push origin &&\n      +\t\tadvance work &&\n     -+\t\tgit -c status.compareBranches=\"upstream/main origin/feature6 origin/feature5\" status >../actual\n     ++\t\tgit -c status.compareBranches=\"@{push} @{upstream}\" status >../actual\n      +\t) &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature6\n     -+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n     -+\n      +\tYour branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.\n      +\t  (use \"git push\" to publish your local commits)\n      +\n     -+\tYour branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit.\n     ++\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n      +\n      +\tnothing to commit, working tree clean\n      +\tEOF\n\n-- \ngitgitgadget\n"},{"id":"537152","messageId":"48db1f4847dde9e6ac387fc5b7f24c9465ef5528.1772056263.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v29.git.git.1772056263.gitgitgadget@gmail.com","subject":"[PATCH v29 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T21:51:02Z","receivedAt":"2026-02-25T21:51:07Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex f6980dc656..e9e2f56ed6 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2234,42 +2234,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2278,7 +2257,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2289,7 +2268,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2302,12 +2281,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"537153","messageId":"6a88f41fa52e7b24fdb75dda6cac692014cebbf6.1772056263.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v29.git.git.1772056263.gitgitgadget@gmail.com","subject":"[PATCH v29 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-25T21:51:03Z","receivedAt":"2026-02-25T21:51:09Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a new configuration variable status.compareBranches that allows\nusers to specify a space-separated list of branch comparisons in\ngit status output.\n\nSupported values:\n- @{upstream} for the current branch's upstream tracking branch\n- @{push} for the current branch's push destination\n\nAny other value is ignored and a warning is shown.\n\nWhen not configured, the default behavior is equivalent to setting\n`status.compareBranches = @{upstream}`, preserving backward\ncompatibility.\n\nThe advice messages shown are context-aware:\n- \"git pull\" advice is shown only when comparing against @{upstream}\n- \"git push\" advice is shown only when comparing against @{push}\n- Divergence advice is shown for upstream branch comparisons\n\nThis is useful for triangular workflows where the upstream tracking\nbranch differs from the push destination, allowing users to see their\nstatus relative to both branches at once.\n\nExample configuration:\n    [status]\n        compareBranches = @{upstream} @{push}\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/status.adoc |  19 ++\n remote.c                         | 148 +++++++++++---\n t/t6040-tracking-info.sh         | 335 +++++++++++++++++++++++++++++++\n 3 files changed, 474 insertions(+), 28 deletions(-)\n\ndiff --git a/Documentation/config/status.adoc b/Documentation/config/status.adoc\nindex 8caf90f51c..15ccd0116b 100644\n--- a/Documentation/config/status.adoc\n+++ b/Documentation/config/status.adoc\n@@ -17,6 +17,25 @@ status.aheadBehind::\n \t`--no-ahead-behind` by default in linkgit:git-status[1] for\n \tnon-porcelain status formats.  Defaults to true.\n \n+status.compareBranches::\n+\tA space-separated list of branch comparison specifiers to use in\n+\tlinkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}`\n+\tare supported. They are interpreted as `branch@{upstream}` and\n+\t`branch@{push}` for the current branch.\n++\n+If not set, the default behavior is equivalent to `@{upstream}`, which\n+compares against the configured upstream tracking branch.\n++\n+Example:\n++\n+----\n+[status]\n+\tcompareBranches = @{upstream} @{push}\n+----\n++\n+This would show comparisons against both the configured upstream and push\n+tracking branches for the current branch.\n+\n status.displayCommentPrefix::\n \tIf set to true, linkgit:git-status[1] will insert a comment\n \tprefix before each output line (starting with\ndiff --git a/remote.c b/remote.c\nindex e9e2f56ed6..cc6ff29438 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,12 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tENABLE_ADVICE_PULL       = (1 << 0),\n+\tENABLE_ADVICE_PUSH       = (1 << 1),\n+\tENABLE_ADVICE_DIVERGENCE = (1 << 2),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2234,13 +2240,42 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *resolve_compare_branch(struct branch *branch, const char *name)\n+{\n+\tconst char *resolved = NULL;\n+\n+\tif (!branch || !name)\n+\t\treturn NULL;\n+\n+\tif (!strcasecmp(name, \"@{upstream}\"))\n+\t\tresolved = branch_get_upstream(branch, NULL);\n+\telse if (!strcasecmp(name, \"@{push}\"))\n+\t\tresolved = branch_get_push(branch, NULL);\n+\telse {\n+\t\twarning(_(\"ignoring value '%s' for status.compareBranches; only @{upstream} and @{push} are supported\"),\n+\t\t\tname);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (resolved)\n+\t\treturn xstrdup(resolved);\n+\treturn NULL;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n-\t\t\t\t     bool show_divergence_advice)\n+\t\t\t\t     unsigned flags)\n {\n+\tbool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\tbool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&\n+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2249,7 +2284,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2258,7 +2293,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_push_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2269,7 +2304,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_pull_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2282,8 +2317,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (enable_divergence_advice)\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2296,34 +2330,92 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t enum ahead_behind_flags abf,\n \t\t\t int show_divergence_advice)\n {\n-\tint ours, theirs, cmp_fetch;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n+\tchar *compare_branches = NULL;\n+\tstruct string_list branches = STRING_LIST_INIT_DUP;\n+\tstruct strset processed_refs = STRSET_INIT;\n+\tint reported = 0;\n+\tsize_t i;\n+\tconst char *upstream_ref;\n+\tconst char *push_ref;\n \n-\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (cmp_fetch < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n+\trepo_config_get_string(the_repository, \"status.comparebranches\",\n+\t\t\t       &compare_branches);\n+\n+\tif (compare_branches) {\n+\t\tstring_list_split(&branches, compare_branches, \" \", -1);\n+\t\tstring_list_remove_empty_items(&branches, 0);\n+\t} else {\n+\t\tstring_list_append(&branches, \"@{upstream}\");\n \t}\n \n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n+\tupstream_ref = branch_get_upstream(branch, NULL);\n+\tpush_ref = branch_get_push(branch, NULL);\n \n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\tfor (i = 0; i < branches.nr; i++) {\n+\t\tchar *full_ref;\n+\t\tchar *short_ref;\n+\t\tint ours, theirs, cmp;\n+\t\tint is_upstream, is_push;\n+\t\tunsigned flags = 0;\n+\n+\t\tfull_ref = resolve_compare_branch(branch,\n+\t\t\t\t\t\t  branches.items[i].string);\n+\t\tif (!full_ref)\n+\t\t\tcontinue;\n+\n+\t\tif (!strset_add(&processed_refs, full_ref)) {\n+\t\t\tfree(full_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tshort_ref = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), full_ref, 0);\n+\n+\t\tis_upstream = upstream_ref && !strcmp(full_ref, upstream_ref);\n+\t\tis_push = push_ref && !strcmp(full_ref, push_ref);\n+\n+\t\tif (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))\n+\t\t\tis_push = 1;\n+\n+\t\tcmp = stat_branch_pair(branch->refname, full_ref,\n+\t\t\t\t       &ours, &theirs, abf);\n+\n+\t\tif (cmp < 0) {\n+\t\t\tif (is_upstream) {\n+\t\t\t\tstrbuf_addf(sb,\n+\t\t\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\t\t\tshort_ref);\n+\t\t\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t\t\t\treported = 1;\n+\t\t\t}\n+\t\t\tfree(full_ref);\n+\t\t\tfree(short_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (reported)\n+\t\t\tstrbuf_addstr(sb, \"\\n\");\n+\n+\t\tif (is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_PULL;\n+\t\tif (is_push)\n+\t\t\tflags |= ENABLE_ADVICE_PUSH;\n+\t\tif (show_divergence_advice && is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_DIVERGENCE;\n+\t\tformat_branch_comparison(sb, !cmp, ours, theirs, short_ref,\n+\t\t\t\t\t abf, flags);\n+\t\treported = 1;\n+\n+\t\tfree(full_ref);\n+\t\tfree(short_ref);\n \t}\n \n-\tfree(base);\n-\treturn 1;\n+\tstring_list_clear(&branches, 0);\n+\tstrset_clear(&processed_refs);\n+\tfree(compare_branches);\n+\treturn reported;\n }\n \n static int one_local_ref(const struct reference *ref, void *cb_data)\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..aa9456bb61 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,339 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for compareBranches tests' '\n+\t(\n+\t\tcd test &&\n+\t\tgit config push.default current &&\n+\t\tgit config status.compareBranches \"@{upstream} @{push}\"\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches from upstream has no duplicates' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout main &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch main\n+\tYour branch is up to date with ${SQ}origin/main${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature2 origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows both branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature2 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches shows diverged and ahead' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit branch --set-upstream-to origin/main &&\n+\t\tgit push origin HEAD &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream &&\n+\t\tgit config remote.pushDefault origin\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches with upstream and origin remotes' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature5 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches supports ordered upstream/push entries' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature6 upstream/main &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit -c status.compareBranches=\"@{push} @{upstream}\" status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with diverged push branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature7 upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push origin &&\n+\t\tgit reset --hard upstream/main &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature7${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows up to date branches' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature8 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature8 &&\n+\t\tgit push origin &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout feature8 >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with upstream behind and push up to date' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b ahead upstream/main &&\n+\t\tadvance work &&\n+\t\tgit push upstream HEAD &&\n+\t\tgit checkout -b feature9 upstream/main &&\n+\t\tgit push origin &&\n+\t\tgit branch --set-upstream-to upstream/ahead &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n+\t  (use \"git pull\" to update your local branch)\n+\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push refspec' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature10 origin/main &&\n+\t\tgit config remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n+\t\tgit push &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature10\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push and upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature11 upstream/main &&\n+\t\tgit config remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n+\t\tgit push origin &&\n+\t\tadvance work &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature11\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'clean up after compareBranches tests' '\n+\t(\n+\t\tcd test &&\n+\t\tgit config --unset status.compareBranches\n+\t)\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"537160","messageId":"xmqq1pi876p3.fsf@gitster.g","threadId":"64668","inReplyTo":"6a88f41fa52e7b24fdb75dda6cac692014cebbf6.1772056263.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v29 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-25T23:18:48Z","receivedAt":"2026-02-25T23:18:50Z","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> +static char *resolve_compare_branch(struct branch *branch, const char *name)\n> +{\n> +\tconst char *resolved = NULL;\n> +\n> +\tif (!branch || !name)\n> +\t\treturn NULL;\n> +\n> +\tif (!strcasecmp(name, \"@{upstream}\"))\n> +\t\tresolved = branch_get_upstream(branch, NULL);\n> +\telse if (!strcasecmp(name, \"@{push}\"))\n> +\t\tresolved = branch_get_push(branch, NULL);\n\nAs one of the if/else if/ cascade needs {} around a multi-statement\nblock, everybody else needs {}.\n\nIf the current branch does not have @{upstream}, we will silently\nreceive NULL in resolved, and return NULL from here, which is\nexactly what we want.  The same stroy for missing @{push}.\n\nSounds very sensible.\n\n> +\telse {\n> +\t\twarning(_(\"ignoring value '%s' for status.compareBranches; only @{upstream} and @{push} are supported\"),\n> +\t\t\tname);\n> +\t\treturn NULL;\n> +\t}\n\nI don't know if which one between the above \"warning\" and\ndie(\"unknown/unsupported\") is a better design.  In any case, the\nwarning() line is overly long and needs to be wrapped (my litmus\ntest to complain about \"overly long lines\" is after losing three\ncolumns to the left for \"> +\" in e-mail quote like the above the\nright edge of the line does not fit on my 92-column terminal, which\nwill never happen if you stick to the official \"fit 80-column after\nquoted for a few times in e-mail\" guideline).\n\n>  static void format_branch_comparison(struct strbuf *sb,\n>  \t\t\t\t     bool up_to_date,\n>  \t\t\t\t     int ours, int theirs,\n>  \t\t\t\t     const char *branch_name,\n>  \t\t\t\t     enum ahead_behind_flags abf,\n> -\t\t\t\t     bool show_divergence_advice)\n> +\t\t\t\t     unsigned flags)\n>  {\n> +\tbool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&\n> +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n> +\tbool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&\n> +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n> +\tbool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&\n> +\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n\n\nYou do use enable_push_advice twice so that reduces repetition a\nlittle bit, but other than that, I am not sure if the above makes it\neasier to follow the code.\n\nEspecially, the roundabout way these three variables are computed,\ntogether with the fact that ENABLE_ADVICE_* are not really about\n\"enabling\", but the caller decides that it is an applicable advice\n(e.g., ENABLE_ADVICE_PULL is passed when the iteration over\nbranches.items[] the caller is making is on the upstream branch and\n\"you should pull\" is the appropriate situation.  It is more like\n\"advice-pull is applicable in this situation\", unlike what the\nadvice_enabled() returns, which is \"the user wants this kind of\nadvice messages\"), I find the resulting code below somehow harder to\nfollow than without these intermediate variables.  If they were\nnamed \"use_*_advice\" (instead of \"enable_\"), perhaps I may find it\nmore palatable.\n\nI find it a lot more disturbing that what the caller specifies in\nflags and advice_enabled() is pre-combined in these variables.\nPerhaps splitting them into two separate concerns, like this ...\n\n\tbool use_push_advice = (flags & USE_ADVICE_PUSH);\n\tbool use_pull_advice = (flags & USE_ADVICE_PULL);\n\tbool use_divergence_advice = (flags & USE_ADVICE_DIVERGENCE);\n\n\t...\n\tif (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS))\n\t\t... you should pull first ...\n\n... may make it easier to understand and follow?  I dunno.\n\n\n> -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n> +\t\tif (enable_push_advice)\n>  \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n>  \t\t\t\t    \"git status --ahead-behind\");\n\n\n> diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\n> index 0b719bbae6..aa9456bb61 100755\n> --- a/t/t6040-tracking-info.sh\n> +++ b/t/t6040-tracking-info.sh\n> @@ -292,4 +292,339 @@ test_expect_success '--set-upstream-to @{-1}' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> +test_expect_success 'status tracking origin/main shows only main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout b4 &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch b4\n> +\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n> +\t  (use \"git push\" to publish your local commits)\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout b4 &&\n> +\t\tgit status --no-ahead-behind >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch b4\n> +\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n> +\t  (use \"git status --ahead-behind\" for details)\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'setup for compareBranches tests' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit config push.default current &&\n> +\t\tgit config status.compareBranches \"@{upstream} @{push}\"\n> +\t)\n> +'\n\nShoudln't this be part of the next test, with its own teardown?\nI.e.,\n\n\ttest_expect_success 'check both @{upstream} and @{push}' '\n        \ttest_config -C test push.default current &&\n        \ttest_config -C test status.compareBranches \"...\" &&\n\n\t\tgit -C test checkout main &&\n\t\tgit -C status >actual &&\n\t\tcat >expect <<-EOF &&\n\t\t...\n\t\tEOF\n\t\ttest_cmp expect actual\n\t'\n\n> +test_expect_success 'status.compareBranches from upstream has no duplicates' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout main &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch main\n> +\tYour branch is up to date with ${SQ}origin/main${SQ}.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n\nThat way, the tests can be kept more independent from each other,\nand will clean after themselves, which means that we do not have to\nassume that ...\n\n> ...\n\n> +test_expect_success 'clean up after compareBranches tests' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit config --unset status.compareBranches\n> +\t)\n> +'\n\n... this step will never be skipped or fail, which would result in\ndisturbing the tests that come after this step.\n\nIn any case, the design of the main part of the patch is looking\nmuch nicer than the open-ended one we saw previously.\n\nThanks.\n\n"},{"id":"537178","messageId":"pull.2138.v30.git.git.1772102022.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v29.git.git.1772056263.gitgitgadget@gmail.com","subject":"[PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T10:33:40Z","receivedAt":"2026-02-26T10:33:45Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im cc:\nJeff King peff@peff.net\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: add status.compareBranches config for multiple branch\n    comparisons\n\n Documentation/config/status.adoc |  19 ++\n remote.c                         | 178 ++++++++++++++----\n t/t6040-tracking-info.sh         | 310 +++++++++++++++++++++++++++++++\n 3 files changed, 470 insertions(+), 37 deletions(-)\n\n\nbase-commit: 7b2bccb0d58d4f24705bf985de1f4612e4cf06e5\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v30\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v30\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v29:\n\n 1:  48db1f4847 = 1:  7f517b8c7f refactor format_branch_comparison in preparation\n 2:  6a88f41fa5 ! 2:  501bd40294 status: add status.compareBranches config for multiple branch comparisons\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      +\tif (!branch || !name)\n      +\t\treturn NULL;\n      +\n     -+\tif (!strcasecmp(name, \"@{upstream}\"))\n     ++\tif (!strcasecmp(name, \"@{upstream}\")) {\n      +\t\tresolved = branch_get_upstream(branch, NULL);\n     -+\telse if (!strcasecmp(name, \"@{push}\"))\n     ++\t} else if (!strcasecmp(name, \"@{push}\")) {\n      +\t\tresolved = branch_get_push(branch, NULL);\n     -+\telse {\n     -+\t\twarning(_(\"ignoring value '%s' for status.compareBranches; only @{upstream} and @{push} are supported\"),\n     ++\t} else {\n     ++\t\twarning(_(\"ignoring value '%s' for status.compareBranches, \"\n     ++\t\t\t  \"only @{upstream} and @{push} are supported\"),\n      +\t\t\tname);\n      +\t\treturn NULL;\n      +\t}\n     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_\n      -\t\t\t\t     bool show_divergence_advice)\n      +\t\t\t\t     unsigned flags)\n       {\n     -+\tbool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&\n     -+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     -+\tbool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&\n     -+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     -+\tbool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&\n     -+\t\tadvice_enabled(ADVICE_STATUS_HINTS);\n     ++\tbool use_push_advice = (flags & ENABLE_ADVICE_PUSH);\n     ++\tbool use_pull_advice = (flags & ENABLE_ADVICE_PULL);\n     ++\tbool use_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE);\n      +\n       \tif (up_to_date) {\n       \t\tstrbuf_addf(sb,\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n       \t\t\t    branch_name);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif (enable_push_advice)\n     ++\t\tif (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n       \t\t\t\t    \"git status --ahead-behind\");\n       \t} else if (!theirs) {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   ours),\n       \t\t\tbranch_name, ours);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif (enable_push_advice)\n     ++\t\tif (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n       \t} else if (!ours) {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\t   theirs),\n       \t\t\tbranch_name, theirs);\n      -\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif (enable_pull_advice)\n     ++\t\tif (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n       \t} else {\n     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,\n       \t\t\tbranch_name, ours, theirs);\n      -\t\tif (show_divergence_advice &&\n      -\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n     -+\t\tif (enable_divergence_advice)\n     ++\t\tif (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS))\n       \t\t\tstrbuf_addstr(sb,\n       \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n       \t}\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'setup for compareBranches tests' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit config push.default current &&\n     -+\t\tgit config status.compareBranches \"@{upstream} @{push}\"\n     -+\t)\n     -+'\n     -+\n      +test_expect_success 'status.compareBranches from upstream has no duplicates' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout main &&\n     -+\t\tgit status >../actual\n     -+\t) &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout main &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch main\n      +\tYour branch is up to date with ${SQ}origin/main${SQ}.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout -b feature2 origin/main &&\n     -+\t\tgit push origin HEAD &&\n     -+\t\tadvance work &&\n     -+\t\tgit status >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout -b feature2 origin/main &&\n     ++\tgit -C test push origin HEAD &&\n     ++\t(cd test && advance work) &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature2\n      +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'checkout with status.compareBranches shows both branches' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout feature2 >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout feature2 >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n      +\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status.compareBranches shows diverged and ahead' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout feature4 &&\n     -+\t\tgit branch --set-upstream-to origin/main &&\n     -+\t\tgit push origin HEAD &&\n     -+\t\tadvance work &&\n     -+\t\tgit status >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout feature4 &&\n     ++\tgit -C test branch --set-upstream-to origin/main &&\n     ++\tgit -C test push origin HEAD &&\n     ++\t(cd test && advance work) &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature4\n      +\tYour branch and ${SQ}origin/main${SQ} have diverged,\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status --no-ahead-behind with status.compareBranches' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout feature4 &&\n     -+\t\tgit status --no-ahead-behind >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout feature4 &&\n     ++\tgit -C test status --no-ahead-behind >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature4\n      +\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\t(\n      +\t\tcd test &&\n      +\t\tgit remote add upstream ../. &&\n     -+\t\tgit fetch upstream &&\n     -+\t\tgit config remote.pushDefault origin\n     ++\t\tgit fetch upstream\n      +\t)\n      +'\n      +\n      +test_expect_success 'status.compareBranches with upstream and origin remotes' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout -b feature5 upstream/main &&\n     -+\t\tgit push origin &&\n     -+\t\tadvance work &&\n     -+\t\tgit status >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test remote.pushDefault origin &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout -b feature5 upstream/main &&\n     ++\tgit -C test push origin &&\n     ++\t(cd test && advance work) &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature5\n      +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status.compareBranches supports ordered upstream/push entries' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout -b feature6 upstream/main &&\n     -+\t\tgit push origin &&\n     -+\t\tadvance work &&\n     -+\t\tgit -c status.compareBranches=\"@{push} @{upstream}\" status >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test remote.pushDefault origin &&\n     ++\ttest_config -C test status.compareBranches \"@{push} @{upstream}\" &&\n     ++\tgit -C test checkout -b feature6 upstream/main &&\n     ++\tgit -C test push origin &&\n     ++\t(cd test && advance work) &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature6\n      +\tYour branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status.compareBranches with diverged push branch' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout -b feature7 upstream/main &&\n     -+\t\tadvance work &&\n     -+\t\tgit push origin &&\n     -+\t\tgit reset --hard upstream/main &&\n     -+\t\tadvance work &&\n     -+\t\tgit status >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test remote.pushDefault origin &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout -b feature7 upstream/main &&\n     ++\t(cd test && advance work71) &&\n     ++\tgit -C test push origin &&\n     ++\tgit -C test reset --hard upstream/main &&\n     ++\t(cd test && advance work72) &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature7\n      +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status.compareBranches shows up to date branches' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout -b feature8 upstream/main &&\n     -+\t\tgit push origin &&\n     -+\t\tgit status >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test remote.pushDefault origin &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout -b feature8 upstream/main &&\n     ++\tgit -C test push origin &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature8\n      +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout feature8 &&\n     -+\t\tgit push origin &&\n     -+\t\tgit status --no-ahead-behind >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test remote.pushDefault origin &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout feature8 >actual &&\n     ++\tgit -C test push origin &&\n     ++\tgit -C test status --no-ahead-behind >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature8\n      +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'checkout with status.compareBranches shows up to date' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout feature8 >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test remote.pushDefault origin &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout feature8 >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n      +\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status.compareBranches with upstream behind and push up to date' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout -b ahead upstream/main &&\n     -+\t\tadvance work &&\n     -+\t\tgit push upstream HEAD &&\n     -+\t\tgit checkout -b feature9 upstream/main &&\n     -+\t\tgit push origin &&\n     -+\t\tgit branch --set-upstream-to upstream/ahead &&\n     -+\t\tgit status >../actual\n     -+\t) &&\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test remote.pushDefault origin &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout -b ahead upstream/main &&\n     ++\t(cd test && advance work) &&\n     ++\tgit -C test push upstream HEAD &&\n     ++\tgit -C test checkout -b feature9 upstream/main &&\n     ++\tgit -C test push origin &&\n     ++\tgit -C test branch --set-upstream-to upstream/ahead &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature9\n      +\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status.compareBranches with remapped push refspec' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout -b feature10 origin/main &&\n     -+\t\tgit config remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n     -+\t\tgit push &&\n     -+\t\tadvance work &&\n     -+\t\tgit status >../actual\n     -+\t) &&\n     ++\ttest_config -C test remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout -b feature10 origin/main &&\n     ++\tgit -C test push &&\n     ++\t(cd test && advance work) &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature10\n      +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status.compareBranches with remapped push and upstream remote' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout -b feature11 upstream/main &&\n     -+\t\tgit config remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n     -+\t\tgit push origin &&\n     -+\t\tadvance work &&\n     -+\t\tgit status >../actual\n     -+\t) &&\n     ++\ttest_config -C test remote.pushDefault origin &&\n     ++\ttest_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout -b feature11 upstream/main &&\n     ++\tgit -C test push origin &&\n     ++\t(cd test && advance work) &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature11\n      +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\tEOF\n      +\ttest_cmp expect actual\n      +'\n     -+\n     -+test_expect_success 'clean up after compareBranches tests' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit config --unset status.compareBranches\n     -+\t)\n     -+'\n      +\n       test_done\n\n-- \ngitgitgadget\n"},{"id":"537179","messageId":"7f517b8c7f866c99a7e298bdb29011865e98b6a2.1772102022.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v30.git.git.1772102022.gitgitgadget@gmail.com","subject":"[PATCH v30 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T10:33:41Z","receivedAt":"2026-02-26T10:33:46Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex f6980dc656..e9e2f56ed6 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2234,42 +2234,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2278,7 +2257,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2289,7 +2268,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2302,12 +2281,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"537180","messageId":"501bd402944146aa02fb5515783f6745eede28d3.1772102022.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v30.git.git.1772102022.gitgitgadget@gmail.com","subject":"[PATCH v30 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-26T10:33:42Z","receivedAt":"2026-02-26T10:33:48Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a new configuration variable status.compareBranches that allows\nusers to specify a space-separated list of branch comparisons in\ngit status output.\n\nSupported values:\n- @{upstream} for the current branch's upstream tracking branch\n- @{push} for the current branch's push destination\n\nAny other value is ignored and a warning is shown.\n\nWhen not configured, the default behavior is equivalent to setting\n`status.compareBranches = @{upstream}`, preserving backward\ncompatibility.\n\nThe advice messages shown are context-aware:\n- \"git pull\" advice is shown only when comparing against @{upstream}\n- \"git push\" advice is shown only when comparing against @{push}\n- Divergence advice is shown for upstream branch comparisons\n\nThis is useful for triangular workflows where the upstream tracking\nbranch differs from the push destination, allowing users to see their\nstatus relative to both branches at once.\n\nExample configuration:\n    [status]\n        compareBranches = @{upstream} @{push}\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/status.adoc |  19 ++\n remote.c                         | 146 ++++++++++++---\n t/t6040-tracking-info.sh         | 310 +++++++++++++++++++++++++++++++\n 3 files changed, 447 insertions(+), 28 deletions(-)\n\ndiff --git a/Documentation/config/status.adoc b/Documentation/config/status.adoc\nindex 8caf90f51c..15ccd0116b 100644\n--- a/Documentation/config/status.adoc\n+++ b/Documentation/config/status.adoc\n@@ -17,6 +17,25 @@ status.aheadBehind::\n \t`--no-ahead-behind` by default in linkgit:git-status[1] for\n \tnon-porcelain status formats.  Defaults to true.\n \n+status.compareBranches::\n+\tA space-separated list of branch comparison specifiers to use in\n+\tlinkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}`\n+\tare supported. They are interpreted as `branch@{upstream}` and\n+\t`branch@{push}` for the current branch.\n++\n+If not set, the default behavior is equivalent to `@{upstream}`, which\n+compares against the configured upstream tracking branch.\n++\n+Example:\n++\n+----\n+[status]\n+\tcompareBranches = @{upstream} @{push}\n+----\n++\n+This would show comparisons against both the configured upstream and push\n+tracking branches for the current branch.\n+\n status.displayCommentPrefix::\n \tIf set to true, linkgit:git-status[1] will insert a comment\n \tprefix before each output line (starting with\ndiff --git a/remote.c b/remote.c\nindex e9e2f56ed6..7ca2a6501b 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,12 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tENABLE_ADVICE_PULL       = (1 << 0),\n+\tENABLE_ADVICE_PUSH       = (1 << 1),\n+\tENABLE_ADVICE_DIVERGENCE = (1 << 2),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2234,13 +2240,40 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *resolve_compare_branch(struct branch *branch, const char *name)\n+{\n+\tconst char *resolved = NULL;\n+\n+\tif (!branch || !name)\n+\t\treturn NULL;\n+\n+\tif (!strcasecmp(name, \"@{upstream}\")) {\n+\t\tresolved = branch_get_upstream(branch, NULL);\n+\t} else if (!strcasecmp(name, \"@{push}\")) {\n+\t\tresolved = branch_get_push(branch, NULL);\n+\t} else {\n+\t\twarning(_(\"ignoring value '%s' for status.compareBranches, \"\n+\t\t\t  \"only @{upstream} and @{push} are supported\"),\n+\t\t\tname);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (resolved)\n+\t\treturn xstrdup(resolved);\n+\treturn NULL;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n-\t\t\t\t     bool show_divergence_advice)\n+\t\t\t\t     unsigned flags)\n {\n+\tbool use_push_advice = (flags & ENABLE_ADVICE_PUSH);\n+\tbool use_pull_advice = (flags & ENABLE_ADVICE_PULL);\n+\tbool use_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2249,7 +2282,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2258,7 +2291,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2269,7 +2302,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2282,8 +2315,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2296,34 +2328,92 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t enum ahead_behind_flags abf,\n \t\t\t int show_divergence_advice)\n {\n-\tint ours, theirs, cmp_fetch;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n+\tchar *compare_branches = NULL;\n+\tstruct string_list branches = STRING_LIST_INIT_DUP;\n+\tstruct strset processed_refs = STRSET_INIT;\n+\tint reported = 0;\n+\tsize_t i;\n+\tconst char *upstream_ref;\n+\tconst char *push_ref;\n \n-\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (cmp_fetch < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n+\trepo_config_get_string(the_repository, \"status.comparebranches\",\n+\t\t\t       &compare_branches);\n+\n+\tif (compare_branches) {\n+\t\tstring_list_split(&branches, compare_branches, \" \", -1);\n+\t\tstring_list_remove_empty_items(&branches, 0);\n+\t} else {\n+\t\tstring_list_append(&branches, \"@{upstream}\");\n \t}\n \n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n+\tupstream_ref = branch_get_upstream(branch, NULL);\n+\tpush_ref = branch_get_push(branch, NULL);\n \n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\tfor (i = 0; i < branches.nr; i++) {\n+\t\tchar *full_ref;\n+\t\tchar *short_ref;\n+\t\tint ours, theirs, cmp;\n+\t\tint is_upstream, is_push;\n+\t\tunsigned flags = 0;\n+\n+\t\tfull_ref = resolve_compare_branch(branch,\n+\t\t\t\t\t\t  branches.items[i].string);\n+\t\tif (!full_ref)\n+\t\t\tcontinue;\n+\n+\t\tif (!strset_add(&processed_refs, full_ref)) {\n+\t\t\tfree(full_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tshort_ref = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), full_ref, 0);\n+\n+\t\tis_upstream = upstream_ref && !strcmp(full_ref, upstream_ref);\n+\t\tis_push = push_ref && !strcmp(full_ref, push_ref);\n+\n+\t\tif (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))\n+\t\t\tis_push = 1;\n+\n+\t\tcmp = stat_branch_pair(branch->refname, full_ref,\n+\t\t\t\t       &ours, &theirs, abf);\n+\n+\t\tif (cmp < 0) {\n+\t\t\tif (is_upstream) {\n+\t\t\t\tstrbuf_addf(sb,\n+\t\t\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\t\t\tshort_ref);\n+\t\t\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t\t\t\treported = 1;\n+\t\t\t}\n+\t\t\tfree(full_ref);\n+\t\t\tfree(short_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (reported)\n+\t\t\tstrbuf_addstr(sb, \"\\n\");\n+\n+\t\tif (is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_PULL;\n+\t\tif (is_push)\n+\t\t\tflags |= ENABLE_ADVICE_PUSH;\n+\t\tif (show_divergence_advice && is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_DIVERGENCE;\n+\t\tformat_branch_comparison(sb, !cmp, ours, theirs, short_ref,\n+\t\t\t\t\t abf, flags);\n+\t\treported = 1;\n+\n+\t\tfree(full_ref);\n+\t\tfree(short_ref);\n \t}\n \n-\tfree(base);\n-\treturn 1;\n+\tstring_list_clear(&branches, 0);\n+\tstrset_clear(&processed_refs);\n+\tfree(compare_branches);\n+\treturn reported;\n }\n \n static int one_local_ref(const struct reference *ref, void *cb_data)\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..c24f545036 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,314 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout b4 &&\n+\t\tgit status --no-ahead-behind >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches from upstream has no duplicates' '\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout main &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch main\n+\tYour branch is up to date with ${SQ}origin/main${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature2 origin/main &&\n+\tgit -C test push origin HEAD &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows both branches' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout feature2 >actual &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches shows diverged and ahead' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout feature4 &&\n+\tgit -C test branch --set-upstream-to origin/main &&\n+\tgit -C test push origin HEAD &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout feature4 &&\n+\tgit -C test status --no-ahead-behind >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches with upstream and origin remotes' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature5 upstream/main &&\n+\tgit -C test push origin &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches supports ordered upstream/push entries' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{push} @{upstream}\" &&\n+\tgit -C test checkout -b feature6 upstream/main &&\n+\tgit -C test push origin &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with diverged push branch' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature7 upstream/main &&\n+\t(cd test && advance work71) &&\n+\tgit -C test push origin &&\n+\tgit -C test reset --hard upstream/main &&\n+\t(cd test && advance work72) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature7${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows up to date branches' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature8 upstream/main &&\n+\tgit -C test push origin &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout feature8 >actual &&\n+\tgit -C test push origin &&\n+\tgit -C test status --no-ahead-behind >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows up to date' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout feature8 >actual &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with upstream behind and push up to date' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b ahead upstream/main &&\n+\t(cd test && advance work) &&\n+\tgit -C test push upstream HEAD &&\n+\tgit -C test checkout -b feature9 upstream/main &&\n+\tgit -C test push origin &&\n+\tgit -C test branch --set-upstream-to upstream/ahead &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n+\t  (use \"git pull\" to update your local branch)\n+\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push refspec' '\n+\ttest_config -C test remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature10 origin/main &&\n+\tgit -C test push &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature10\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push and upstream remote' '\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature11 upstream/main &&\n+\tgit -C test push origin &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature11\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"537187","messageId":"20260226134728.3725-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqq1pi876p3.fsf@gitster.g","subject":"Re: [PATCH v29 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-02-26T13:47:28Z","receivedAt":"2026-02-26T13:47:31Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> You do use enable_push_advice twice so that reduces repetition a\n> little bit, but other than that, I am not sure if the above makes it\n> easier to follow the code.\n\nThis was orginally abstracted because of feedback from someone else in this\npatch. But I update it now, togethe with the rest of your feedback, thanks\nfor the help!\n\n\nHarald\n"},{"id":"537188","messageId":"20260226135008.6863-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"20260225160842.GA2160019@coredump.intra.peff.net","subject":"Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-02-26T13:50:08Z","receivedAt":"2026-02-26T13:50:11Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> I think what would have moved things forward more than just a ping,\n> though, is generating a plan for moving forward in the face of confusing\n> review.\n\nVery fair point!\n\n> That can be quite hard for first-time contributors, though.\n\nI'm not actually a first time contributor 🤗 I worked on this feature\nbetween around 2016-18. So I know it can take a while:\n\nhttps://git.kernel.org/pub/scm/git/git.git/commit/?h=seen&id=1fb20dfd8ed70b4459312918a71444bc79ea6f0b \n\n\n\nHarald\n"},{"id":"537198","messageId":"xmqqa4wv5xsb.fsf@gitster.g","threadId":"64668","inReplyTo":"pull.2138.v30.git.git.1772102022.gitgitgadget@gmail.com","subject":"Re: [PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-26T15:28:52Z","receivedAt":"2026-02-26T15:28:54Z","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> cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\n> ychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\n> cc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\n> kristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\n> cc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im cc:\n> Jeff King peff@peff.net\n>\n> Harald Nordgren (2):\n>   refactor format_branch_comparison in preparation\n>   status: add status.compareBranches config for multiple branch\n>     comparisons\n>\n>  Documentation/config/status.adoc |  19 ++\n>  remote.c                         | 178 ++++++++++++++----\n>  t/t6040-tracking-info.sh         | 310 +++++++++++++++++++++++++++++++\n>  3 files changed, 470 insertions(+), 37 deletions(-)\n\nWill replace.  Let's wait to see if we hear further comments on the\ntopic for a day or two and then mark the topic for 'next'.  I didn't\nspot anything unexpected in this round, but haven't had enough time\nto spend on the test part, which I want to read a bit more carefully\nbefore merging.\n\nThanks.\n"},{"id":"537469","messageId":"20260301190647.41248-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqqa4wv5xsb.fsf@gitster.g","subject":"Re: [PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-03-01T19:06:47Z","receivedAt":"2026-03-01T19:06:50Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Will replace.\n\nWhat does this mean? 🤗\n\n> Let's wait to see if we hear further comments on the\n> topic for a day or two and then mark the topic for 'next'.  I didn't\n> spot anything unexpected in this round, but haven't had enough time\n> to spend on the test part, which I want to read a bit more carefully\n> before merging.\n\nDid you have a chance to look at the tests?\n\n\nHarald\n"},{"id":"537472","messageId":"0a861662-3a6f-49bd-b2c9-d90736c1f674@app.fastmail.com","threadId":"64668","inReplyTo":"20260301190647.41248-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-03-01T19:25:05Z","receivedAt":"2026-03-01T19:28:03Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Sun, Mar 1, 2026, at 20:06, Harald Nordgren wrote:\n>> Will replace.\n>\n> What does this mean? 🤗\n\nThat he will replace the branch he has for this topic\n(hn/status-compare-with-push) with this new round.\n\n>[snip]\n"},{"id":"537553","messageId":"xmqqpl5m2mxk.fsf@gitster.g","threadId":"64668","inReplyTo":"20260301190647.41248-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-02T16:52:55Z","receivedAt":"2026-03-02T16:52:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> Let's wait to see if we hear further comments on the\n>> topic for a day or two and then mark the topic for 'next'.  I didn't\n>> spot anything unexpected in this round, but haven't had enough time\n>> to spend on the test part, which I want to read a bit more carefully\n>> before merging.\n>\n> Did you have a chance to look at the tests?\n\nNot me, it was a bit too messy for reading in a single sitting.\n"},{"id":"537629","messageId":"xmqq1pi1wzzh.fsf@gitster.g","threadId":"64668","inReplyTo":"501bd402944146aa02fb5515783f6745eede28d3.1772102022.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v30 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-02T23:52:50Z","receivedAt":"2026-03-02T23:52:52Z","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>  t/t6040-tracking-info.sh         | 310 +++++++++++++++++++++++++++++++\n>  3 files changed, 447 insertions(+), 28 deletions(-)\n>\n> diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\n> index 0b719bbae6..c24f545036 100755\n> --- a/t/t6040-tracking-info.sh\n> +++ b/t/t6040-tracking-info.sh\n> @@ -292,4 +292,314 @@ test_expect_success '--set-upstream-to @{-1}' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> +test_expect_success 'status tracking origin/main shows only main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout b4 &&\n> +\t\tgit status >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch b4\n> +\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n> +\t  (use \"git push\" to publish your local commits)\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout b4 &&\n> +\t\tgit status --no-ahead-behind >../actual\n> +\t) &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch b4\n> +\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n> +\t  (use \"git status --ahead-behind\" for details)\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n\nThe above two uses ( cd test && git $command ) to do what it can do\nwith \"git -C test $command\", but the rest of the script seems to\nmostly stick to the latter.  Want to be a bit more consistent?\n\n> +test_expect_success 'status.compareBranches from upstream has no duplicates' '\n> +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n> +\tgit -C test checkout main &&\n> +\tgit -C test status >actual &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch main\n> +\tYour branch is up to date with ${SQ}origin/main${SQ}.\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n\nWe see only one because... @{upstream} and @{push} are both\norigin/main and we can dedupe and there is no extra advice needed?\nIf @{push} were missing and only @{upstream} existed, we would also\nsee just one, so this test feels a bit under-explained.\n\n> +test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n> +\ttest_config -C test push.default current &&\n> +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n> +\tgit -C test checkout -b feature2 origin/main &&\n> +\tgit -C test push origin HEAD &&\n> +\t(cd test && advance work) &&\n\nOne thing the \"advance\" function does is to call \"test_tick\" to\nincrement the mock timestamp, but the incremented mock timestamp\nwould not survive beyond the end of a subshell.  Not that it matters\ntoo much to have commits with the same timestamp in these tests, as\nlong as things are made more reproducible by use of the \"advance\"\nfunction.\n\n> +\tgit -C test status >actual &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature2\n> +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n> +\n> +\tYour branch is ahead of ${SQ}origin/feature2${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> +\ttest_cmp expect actual\n> +'\n\nVery straight-forward  If we ask for @{upstream} and @{push} that\nresolve to different things, we would see two different reports.\n\nI am curious what should [jc: not asking what the code happens to\ndo; wondering what the sensible behaviour to help users is] happen\nwhen the user configures the variable to \"@{push} @{upstream}\n@{push}\" in this case.  Should we see just two entries (one for\n@{push}, the other for @{upstream})?\n\nI also am curious what should [jc: ditto] happen when @{upstream}\nand @{push} point at the same origin/main and our current branch is\nahead by 1 commit.  The pull side would say \"you are ahead of\norigin/main\" and stop wile the push side would say the same thing\nwith advice to push it out for publishing?  Or should they get\ndeduped?\n\n> +test_expect_success 'checkout with status.compareBranches shows both branches' '\n> +\ttest_config -C test push.default current &&\n> +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n> +\tgit -C test checkout feature2 >actual &&\n> +\tcat >expect <<-EOF &&\n> +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n> +\n> +\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n> +\t  (use \"git push\" to publish your local commits)\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n\nIf the previous step succeeded, we would be on feature2 branch in\nthe \"test\" repository, so this checkout is a no-op-only-for-status\ncheckout and we expect to see the same thing as the previous step.\n\nGood.\n\nIf the previous step failed, this step may break, but that is what\n\"cd t && sh t6040*.sh -i\" is for ;-).\n\n> +test_expect_success 'setup for ahead of tracked but diverged from main' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit checkout -b feature4 origin/main &&\n> +\t\tadvance work1 &&\n> +\t\tgit checkout origin/main &&\n> +\t\tadvance work2 &&\n> +\t\tgit push origin HEAD:main &&\n> +\t\tgit checkout feature4 &&\n> +\t\tadvance work3\n> +\t)\n> +'\n> +\n> +test_expect_success 'status.compareBranches shows diverged and ahead' '\n> +\ttest_config -C test push.default current &&\n> +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n\n\n> +\tgit -C test checkout feature4 &&\n> +\tgit -C test branch --set-upstream-to origin/main &&\n> +\tgit -C test push origin HEAD &&\n> +\t(cd test && advance work) &&\n> +\tgit -C test status >actual &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature4\n> +\tYour branch and ${SQ}origin/main${SQ} have diverged,\n> +\tand have 3 and 1 different commits each, respectively.\n> +\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n> +\n> +\tYour branch is ahead of ${SQ}origin/feature4${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> +\ttest_cmp expect actual\n> +'\n\nVery understandable.\n\n> +test_expect_success 'status --no-ahead-behind with status.compareBranches' '\n> +\ttest_config -C test push.default current &&\n> +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n> +\tgit -C test checkout feature4 &&\n> +\tgit -C test status --no-ahead-behind >actual &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature4\n> +\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n> +\n> +\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n> +\t  (use \"git status --ahead-behind\" for details)\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n\nOK.\n\n> +test_expect_success 'setup upstream remote' '\n> +\t(\n> +\t\tcd test &&\n> +\t\tgit remote add upstream ../. &&\n> +\t\tgit fetch upstream\n> +\t)\n> +'\n> +\n> +test_expect_success 'status.compareBranches with upstream and origin remotes' '\n> +\ttest_config -C test push.default current &&\n> +\ttest_config -C test remote.pushDefault origin &&\n> +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n> +\tgit -C test checkout -b feature5 upstream/main &&\n> +\tgit -C test push origin &&\n> +\t(cd test && advance work) &&\n> +\tgit -C test status >actual &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature5\n> +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n> +\n> +\tYour branch is ahead of ${SQ}origin/feature5${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> +\ttest_cmp expect actual\n> +'\n\nOK.  There aren't much essential difference from what we have\nalready seen so far, which dealt with remote tracking branch(es)\nfrom a single remote repository.  Instead of origin/{main,$feature},\nwe are now dealing with {upstream,origin}/{main,$feature} but we can\nonly pick one for @{upstream} and one for @{push}, we do not have to\nworry about all 4 possible permutations.\n\n> +test_expect_success 'status.compareBranches supports ordered upstream/push entries' '\n> +\ttest_config -C test push.default current &&\n> +\ttest_config -C test remote.pushDefault origin &&\n> +\ttest_config -C test status.compareBranches \"@{push} @{upstream}\" &&\n> +\tgit -C test checkout -b feature6 upstream/main &&\n> +\tgit -C test push origin &&\n> +\t(cd test && advance work) &&\n> +\tgit -C test status >actual &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature6\n> +\tYour branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.\n> +\t  (use \"git push\" to publish your local commits)\n> +\n> +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n\nAh, I guess this example hints the answer to my earlier \"@{push}\n@{upstream} @{push}\" question?  The user can control the order of\nentries shown by tweaking the order they list things on the\nconfiguration variable.  So we would see the one for @{push} and\nthen another for @{upstream}.  We can show the one for @{push} again\nafter these two and claim that it is working as the user told us to,\nor we can stop showing after showing @{push} and then @{upstream}\nand claim that we are deduping.  Either is fine, but we need to be\nconsistent and we need to have it documented.\n\n> +test_expect_success 'status.compareBranches with diverged push branch' '\n> +\ttest_config -C test push.default current &&\n> +\ttest_config -C test remote.pushDefault origin &&\n> +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n> +\tgit -C test checkout -b feature7 upstream/main &&\n> +\t(cd test && advance work71) &&\n> +\tgit -C test push origin &&\n> +\tgit -C test reset --hard upstream/main &&\n> +\t(cd test && advance work72) &&\n> +\tgit -C test status >actual &&\n> +\tcat >expect <<-EOF &&\n> +\tOn branch feature7\n> +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n> +\n> +\tYour branch and ${SQ}origin/feature7${SQ} have diverged,\n> +\tand have 1 and 1 different commits each, respectively.\n> +\n> +\tnothing to commit, working tree clean\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n\nThe same as before, I guess.  Nothing unexpected to see here, or in\nthe rest of the patch.\n\nSo td;lr is that the tests in this step mostly look great.  It is\njust the semantics of \"dedup\" is a bit unclear, without a test that\nmakes it clear (like the \"@{push} @{upstream} @{push}\" one), and\nperhaps a bit more documentation how having multiple tokens on the\nstatus.compareBranches variable affects the output.  A (n easier)\nhalf of that additional documentation could be as small as the\nfollowing, but I didn't attempt to deal with the \"dedup\" part.\n\n\n\n Documentation/config/status.adoc | 3 ++-\n 1 file changed, 2 insertions(+), 1 deletion(-)\n\ndiff --git c/Documentation/config/status.adoc w/Documentation/config/status.adoc\nindex 15ccd0116b..e2c4a014a4 100644\n--- c/Documentation/config/status.adoc\n+++ w/Documentation/config/status.adoc\n@@ -34,7 +34,8 @@ Example:\n ----\n +\n This would show comparisons against both the configured upstream and push\n-tracking branches for the current branch.\n+tracking branches for the current branch in this order (as that is how\n+upstream and push appear in the value of the variable).\n \n status.displayCommentPrefix::\n \tIf set to true, linkgit:git-status[1] will insert a comment\n\n\n"},{"id":"537768","messageId":"20260304103029.8273-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqq1pi1wzzh.fsf@gitster.g","subject":"Re: [PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-03-04T10:30:29Z","receivedAt":"2026-03-04T10:30:32Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> The above two uses ( cd test && git $command ) to do what it can do\n> with \"git -C test $command\", but the rest of the script seems to\n> mostly stick to the latter.  Want to be a bit more consistent?\n\nGood point, will fix!\n\n> We see only one because... @{upstream} and @{push} are both\n> origin/main and we can dedupe and there is no extra advice needed?\n> If @{push} were missing and only @{upstream} existed, we would also\n> see just one, so this test feels a bit under-explained.\n\nRenaming this this now to clarify that it tests deduplicating. Also added\ntwo new tests, for when reach of @{push} and @{upstream} are used alone.\n\n> One thing the \"advance\" function does is to call \"test_tick\" to\n> increment the mock timestamp, but the incremented mock timestamp\n> would not survive beyond the end of a subshell.  Not that it matters\n> too much to have commits with the same timestamp in these tests, as\n> long as things are made more reproducible by use of the \"advance\"\n> function.\n\nYeah, it's not great when used with subshells, I ended up differentiting\ncommits only by name like 'advance work2' because as you say, otherwise\nthey will get the same commit hash.\n\n> I also am curious what should [jc: ditto] happen when @{upstream}\n> and @{push} point at the same origin/main and our current branch is\n> ahead by 1 commit.  The pull side would say \"you are ahead of\n> origin/main\" and stop wile the push side would say the same thing\n> with advice to push it out for publishing?  Or should they get\n> deduped?\n\nJumping in altough the question was not directed to me:\n\nWhen behind and ahead of the same branch we show the \"diverged\" message,\nand since only one line is shown when deduped I think it works in a logical\nway.\n\n> So td;lr is that the tests in this step mostly look great.  It is\n> just the semantics of \"dedup\" is a bit unclear, without a test that\n> makes it clear (like the \"@{push} @{upstream} @{push}\" one), and\n> perhaps a bit more documentation how having multiple tokens on the\n> status.compareBranches variable affects the output.  A (n easier)\n> half of that additional documentation could be as small as the\n> following, but I didn't attempt to deal with the \"dedup\" part.\n\nWill add documentation for the depuping and a new test case.\n\n\nHarald\n"},{"id":"537771","messageId":"pull.2138.v31.git.git.1772627131.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v30.git.git.1772102022.gitgitgadget@gmail.com","subject":"[PATCH v31 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T12:25:29Z","receivedAt":"2026-03-04T12:25:34Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin\nychin.macvim@gmail.com cc: \"brian m. carlson\" sandals@crustytoothpaste.net\ncc: Ben Knoble ben.knoble@gmail.com cc: \"Kristoffer Haugsbakk\"\nkristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com\ncc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im cc:\nJeff King peff@peff.net\n\nHarald Nordgren (2):\n  refactor format_branch_comparison in preparation\n  status: add status.compareBranches config for multiple branch\n    comparisons\n\n Documentation/config/status.adoc |  25 +++\n remote.c                         | 178 ++++++++++++----\n t/t6040-tracking-info.sh         | 354 +++++++++++++++++++++++++++++++\n 3 files changed, 520 insertions(+), 37 deletions(-)\n\n\nbase-commit: 50d063e335afd5828fbb9de2f2b2fb44fd884d2b\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v31\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v31\nPull-Request: https://github.com/git/git/pull/2138\n\nRange-diff vs v30:\n\n 1:  7f517b8c7f = 1:  320c1ce55a refactor format_branch_comparison in preparation\n 2:  501bd40294 ! 2:  f07ccb278c status: add status.compareBranches config for multiple branch comparisons\n     @@ Documentation/config/status.adoc: status.aheadBehind::\n      +If not set, the default behavior is equivalent to `@{upstream}`, which\n      +compares against the configured upstream tracking branch.\n      ++\n     ++The entries are shown in the order they appear in the configuration.\n     ++Duplicate entries that resolve to the same ref are suppressed after\n     ++their first occurrence, so `@{push} @{upstream} @{push}` shows at\n     ++most two comparisons.  When `@{upstream}` and `@{push}` resolve to\n     ++the same remote-tracking branch, only one comparison is shown.\n     +++\n      +Example:\n      ++\n      +----\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n       '\n       \n      +test_expect_success 'status tracking origin/main shows only main' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout b4 &&\n     -+\t\tgit status >../actual\n     -+\t) &&\n     ++\tgit -C test checkout b4 &&\n     ++\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch b4\n      +\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n     -+\t(\n     -+\t\tcd test &&\n     -+\t\tgit checkout b4 &&\n     -+\t\tgit status --no-ahead-behind >../actual\n     -+\t) &&\n     ++\tgit -C test checkout b4 &&\n     ++\tgit -C test status --no-ahead-behind >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch b4\n      +\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status.compareBranches from upstream has no duplicates' '\n     ++test_expect_success 'status.compareBranches deduplicates when upstream and push are the same' '\n      +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n      +\tgit -C test checkout main &&\n      +\tgit -C test status >actual &&\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n     ++test_expect_success 'status.compareBranches with only upstream shows only upstream' '\n     ++\ttest_config -C test status.compareBranches \"@{upstream}\" &&\n     ++\tgit -C test checkout main &&\n     ++\tgit -C test status >actual &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch main\n     ++\tYour branch is up to date with ${SQ}origin/main${SQ}.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'status.compareBranches with only push shows only push' '\n      +\ttest_config -C test push.default current &&\n     -+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\ttest_config -C test status.compareBranches \"@{push}\" &&\n      +\tgit -C test checkout -b feature2 origin/main &&\n      +\tgit -C test push origin HEAD &&\n      +\t(cd test && advance work) &&\n      +\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tOn branch feature2\n     ++\tYour branch is ahead of ${SQ}origin/feature2${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     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     ++\tgit -C test checkout -b feature3 origin/main &&\n     ++\tgit -C test push origin HEAD &&\n     ++\t(cd test && advance work) &&\n     ++\tgit -C test status >actual &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature3\n      +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n      +\n     -+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n     ++\tYour branch is ahead of ${SQ}origin/feature3${SQ} by 1 commit.\n      +\t  (use \"git push\" to publish your local commits)\n      +\n      +\tnothing to commit, working tree clean\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +test_expect_success 'checkout with status.compareBranches shows both branches' '\n      +\ttest_config -C test push.default current &&\n      +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     -+\tgit -C test checkout feature2 >actual &&\n     ++\tgit -C test checkout feature3 >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n      +\n     -+\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n     ++\tYour branch is ahead of ${SQ}origin/feature3${SQ} by 1 commit.\n      +\t  (use \"git push\" to publish your local commits)\n      +\tEOF\n      +\ttest_cmp expect actual\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_cmp expect actual\n      +'\n      +\n     ++test_expect_success 'status.compareBranches deduplicates repeated specifiers' '\n     ++\ttest_config -C test push.default current &&\n     ++\ttest_config -C test remote.pushDefault origin &&\n     ++\ttest_config -C test status.compareBranches \"@{push} @{upstream} @{push}\" &&\n     ++\tgit -C test checkout -b feature7 upstream/main &&\n     ++\tgit -C test push origin &&\n     ++\t(cd test && advance work) &&\n     ++\tgit -C test status >actual &&\n     ++\tcat >expect <<-EOF &&\n     ++\tOn branch feature7\n     ++\tYour branch is ahead of ${SQ}origin/feature7${SQ} by 1 commit.\n     ++\t  (use \"git push\" to publish your local commits)\n     ++\n     ++\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n     ++\n     ++\tnothing to commit, working tree clean\n     ++\tEOF\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n      +test_expect_success 'status.compareBranches with diverged push branch' '\n      +\ttest_config -C test push.default current &&\n      +\ttest_config -C test remote.pushDefault origin &&\n      +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     -+\tgit -C test checkout -b feature7 upstream/main &&\n     -+\t(cd test && advance work71) &&\n     ++\tgit -C test checkout -b feature8 upstream/main &&\n     ++\t(cd test && advance work81) &&\n      +\tgit -C test push origin &&\n      +\tgit -C test reset --hard upstream/main &&\n     -+\t(cd test && advance work72) &&\n     ++\t(cd test && advance work82) &&\n      +\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature7\n     ++\tOn branch feature8\n      +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n      +\n     -+\tYour branch and ${SQ}origin/feature7${SQ} have diverged,\n     ++\tYour branch and ${SQ}origin/feature8${SQ} have diverged,\n      +\tand have 1 and 1 different commits each, respectively.\n      +\n      +\tnothing to commit, working tree clean\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_config -C test push.default current &&\n      +\ttest_config -C test remote.pushDefault origin &&\n      +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     -+\tgit -C test checkout -b feature8 upstream/main &&\n     ++\tgit -C test checkout -b feature9 upstream/main &&\n      +\tgit -C test push origin &&\n      +\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature8\n     ++\tOn branch feature9\n      +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n      +\n     -+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n     ++\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n      +\n      +\tnothing to commit, working tree clean\n      +\tEOF\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_config -C test push.default current &&\n      +\ttest_config -C test remote.pushDefault origin &&\n      +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     -+\tgit -C test checkout feature8 >actual &&\n     ++\tgit -C test checkout feature9 >actual &&\n      +\tgit -C test push origin &&\n      +\tgit -C test status --no-ahead-behind >actual &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature8\n     ++\tOn branch feature9\n      +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n      +\n     -+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n     ++\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n      +\n      +\tnothing to commit, working tree clean\n      +\tEOF\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\ttest_config -C test push.default current &&\n      +\ttest_config -C test remote.pushDefault origin &&\n      +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     -+\tgit -C test checkout feature8 >actual &&\n     ++\tgit -C test checkout feature9 >actual &&\n      +\tcat >expect <<-EOF &&\n      +\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n      +\n     -+\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n     ++\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n      +\tEOF\n      +\ttest_cmp expect actual\n      +'\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\tgit -C test checkout -b ahead upstream/main &&\n      +\t(cd test && advance work) &&\n      +\tgit -C test push upstream HEAD &&\n     -+\tgit -C test checkout -b feature9 upstream/main &&\n     ++\tgit -C test checkout -b feature10 upstream/main &&\n      +\tgit -C test push origin &&\n      +\tgit -C test branch --set-upstream-to upstream/ahead &&\n      +\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature9\n     ++\tOn branch feature10\n      +\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n      +\t  (use \"git pull\" to update your local branch)\n      +\n     -+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n     ++\tYour branch is up to date with ${SQ}origin/feature10${SQ}.\n      +\n      +\tnothing to commit, working tree clean\n      +\tEOF\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +'\n      +\n      +test_expect_success 'status.compareBranches with remapped push refspec' '\n     -+\ttest_config -C test remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n     ++\ttest_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n      +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     -+\tgit -C test checkout -b feature10 origin/main &&\n     ++\tgit -C test checkout -b feature11 origin/main &&\n      +\tgit -C test push &&\n      +\t(cd test && advance work) &&\n      +\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature10\n     ++\tOn branch feature11\n      +\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n      +\n      +\tYour branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '\n      +\n      +test_expect_success 'status.compareBranches with remapped push and upstream remote' '\n      +\ttest_config -C test remote.pushDefault origin &&\n     -+\ttest_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n     ++\ttest_config -C test remote.origin.push refs/heads/feature12:refs/heads/remapped &&\n      +\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n     -+\tgit -C test checkout -b feature11 upstream/main &&\n     ++\tgit -C test checkout -b feature12 upstream/main &&\n      +\tgit -C test push origin &&\n      +\t(cd test && advance work) &&\n      +\tgit -C test status >actual &&\n      +\tcat >expect <<-EOF &&\n     -+\tOn branch feature11\n     ++\tOn branch feature12\n      +\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n      +\n      +\tYour branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.\n\n-- \ngitgitgadget\n"},{"id":"537772","messageId":"320c1ce55a428ea87fde4a8f67a61acdb60466f3.1772627131.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v31.git.git.1772627131.gitgitgadget@gmail.com","subject":"[PATCH v31 1/2] refactor format_branch_comparison in preparation","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T12:25:30Z","receivedAt":"2026-03-04T12:25:35Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nRefactor format_branch_comparison function in preparation for showing\ncomparison with push remote tracking branch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 48 insertions(+), 34 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex f6980dc656..e9e2f56ed6 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2234,42 +2234,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n-/*\n- * Return true when there is anything to report, otherwise false.\n- */\n-int format_tracking_info(struct branch *branch, struct strbuf *sb,\n-\t\t\t enum ahead_behind_flags abf,\n-\t\t\t int show_divergence_advice)\n-{\n-\tint ours, theirs, sti;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n-\n-\tsti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (sti < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n-\t}\n-\n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else if (!sti) {\n+static void format_branch_comparison(struct strbuf *sb,\n+\t\t\t\t     bool up_to_date,\n+\t\t\t\t     int ours, int theirs,\n+\t\t\t\t     const char *branch_name,\n+\t\t\t\t     enum ahead_behind_flags abf,\n+\t\t\t\t     bool show_divergence_advice)\n+{\n+\tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n-\t\t\tbase);\n+\t\t\tbranch_name);\n \t} else if (abf == AHEAD_BEHIND_QUICK) {\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n-\t\t\t    base);\n+\t\t\t    branch_name);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n@@ -2278,7 +2257,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tQ_(\"Your branch is ahead of '%s' by %d commit.\\n\",\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n-\t\t\tbase, ours);\n+\t\t\tbranch_name, ours);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n@@ -2289,7 +2268,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t   \"Your branch is behind '%s' by %d commits, \"\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n-\t\t\tbase, theirs);\n+\t\t\tbranch_name, theirs);\n \t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n@@ -2302,12 +2281,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t       \"and have %d and %d different commits each, \"\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n-\t\t\tbase, ours, theirs);\n+\t\t\tbranch_name, ours, theirs);\n \t\tif (show_divergence_advice &&\n \t\t    advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n+}\n+\n+/*\n+ * Return true when there is anything to report, otherwise false.\n+ */\n+int format_tracking_info(struct branch *branch, struct strbuf *sb,\n+\t\t\t enum ahead_behind_flags abf,\n+\t\t\t int show_divergence_advice)\n+{\n+\tint ours, theirs, cmp_fetch;\n+\tconst char *full_base;\n+\tchar *base;\n+\tint upstream_is_gone = 0;\n+\n+\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n+\tif (cmp_fetch < 0) {\n+\t\tif (!full_base)\n+\t\t\treturn 0;\n+\t\tupstream_is_gone = 1;\n+\t}\n+\n+\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n+\t\t\t\t\t    full_base, 0);\n+\n+\tif (upstream_is_gone) {\n+\t\tstrbuf_addf(sb,\n+\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\tbase);\n+\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t} else {\n+\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\t}\n+\n \tfree(base);\n \treturn 1;\n }\n-- \ngitgitgadget\n\n"},{"id":"537773","messageId":"f07ccb278c171e7350ade1501a8cf7c8ec79046a.1772627131.git.gitgitgadget@gmail.com","threadId":"64668","inReplyTo":"pull.2138.v31.git.git.1772627131.gitgitgadget@gmail.com","subject":"[PATCH v31 2/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-03-04T12:25:31Z","receivedAt":"2026-03-04T12:25:36Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a new configuration variable status.compareBranches that allows\nusers to specify a space-separated list of branch comparisons in\ngit status output.\n\nSupported values:\n- @{upstream} for the current branch's upstream tracking branch\n- @{push} for the current branch's push destination\n\nAny other value is ignored and a warning is shown.\n\nWhen not configured, the default behavior is equivalent to setting\n`status.compareBranches = @{upstream}`, preserving backward\ncompatibility.\n\nThe advice messages shown are context-aware:\n- \"git pull\" advice is shown only when comparing against @{upstream}\n- \"git push\" advice is shown only when comparing against @{push}\n- Divergence advice is shown for upstream branch comparisons\n\nThis is useful for triangular workflows where the upstream tracking\nbranch differs from the push destination, allowing users to see their\nstatus relative to both branches at once.\n\nExample configuration:\n    [status]\n        compareBranches = @{upstream} @{push}\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/status.adoc |  25 +++\n remote.c                         | 146 ++++++++++---\n t/t6040-tracking-info.sh         | 354 +++++++++++++++++++++++++++++++\n 3 files changed, 497 insertions(+), 28 deletions(-)\n\ndiff --git a/Documentation/config/status.adoc b/Documentation/config/status.adoc\nindex 8caf90f51c..b5dd85b761 100644\n--- a/Documentation/config/status.adoc\n+++ b/Documentation/config/status.adoc\n@@ -17,6 +17,31 @@ status.aheadBehind::\n \t`--no-ahead-behind` by default in linkgit:git-status[1] for\n \tnon-porcelain status formats.  Defaults to true.\n \n+status.compareBranches::\n+\tA space-separated list of branch comparison specifiers to use in\n+\tlinkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}`\n+\tare supported. They are interpreted as `branch@{upstream}` and\n+\t`branch@{push}` for the current branch.\n++\n+If not set, the default behavior is equivalent to `@{upstream}`, which\n+compares against the configured upstream tracking branch.\n++\n+The entries are shown in the order they appear in the configuration.\n+Duplicate entries that resolve to the same ref are suppressed after\n+their first occurrence, so `@{push} @{upstream} @{push}` shows at\n+most two comparisons.  When `@{upstream}` and `@{push}` resolve to\n+the same remote-tracking branch, only one comparison is shown.\n++\n+Example:\n++\n+----\n+[status]\n+\tcompareBranches = @{upstream} @{push}\n+----\n++\n+This would show comparisons against both the configured upstream and push\n+tracking branches for the current branch.\n+\n status.displayCommentPrefix::\n \tIf set to true, linkgit:git-status[1] will insert a comment\n \tprefix before each output line (starting with\ndiff --git a/remote.c b/remote.c\nindex e9e2f56ed6..7ca2a6501b 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -29,6 +29,12 @@\n \n enum map_direction { FROM_SRC, FROM_DST };\n \n+enum {\n+\tENABLE_ADVICE_PULL       = (1 << 0),\n+\tENABLE_ADVICE_PUSH       = (1 << 1),\n+\tENABLE_ADVICE_DIVERGENCE = (1 << 2),\n+};\n+\n struct counted_string {\n \tsize_t len;\n \tconst char *s;\n@@ -2234,13 +2240,40 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n }\n \n+static char *resolve_compare_branch(struct branch *branch, const char *name)\n+{\n+\tconst char *resolved = NULL;\n+\n+\tif (!branch || !name)\n+\t\treturn NULL;\n+\n+\tif (!strcasecmp(name, \"@{upstream}\")) {\n+\t\tresolved = branch_get_upstream(branch, NULL);\n+\t} else if (!strcasecmp(name, \"@{push}\")) {\n+\t\tresolved = branch_get_push(branch, NULL);\n+\t} else {\n+\t\twarning(_(\"ignoring value '%s' for status.compareBranches, \"\n+\t\t\t  \"only @{upstream} and @{push} are supported\"),\n+\t\t\tname);\n+\t\treturn NULL;\n+\t}\n+\n+\tif (resolved)\n+\t\treturn xstrdup(resolved);\n+\treturn NULL;\n+}\n+\n static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     bool up_to_date,\n \t\t\t\t     int ours, int theirs,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     enum ahead_behind_flags abf,\n-\t\t\t\t     bool show_divergence_advice)\n+\t\t\t\t     unsigned flags)\n {\n+\tbool use_push_advice = (flags & ENABLE_ADVICE_PUSH);\n+\tbool use_pull_advice = (flags & ENABLE_ADVICE_PULL);\n+\tbool use_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE);\n+\n \tif (up_to_date) {\n \t\tstrbuf_addf(sb,\n \t\t\t_(\"Your branch is up to date with '%s'.\\n\"),\n@@ -2249,7 +2282,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\tstrbuf_addf(sb,\n \t\t\t    _(\"Your branch and '%s' refer to different commits.\\n\"),\n \t\t\t    branch_name);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addf(sb, _(\"  (use \\\"%s\\\" for details)\\n\"),\n \t\t\t\t    \"git status --ahead-behind\");\n \t} else if (!theirs) {\n@@ -2258,7 +2291,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t   \"Your branch is ahead of '%s' by %d commits.\\n\",\n \t\t\t   ours),\n \t\t\tbranch_name, ours);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git push\\\" to publish your local commits)\\n\"));\n \t} else if (!ours) {\n@@ -2269,7 +2302,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"and can be fast-forwarded.\\n\",\n \t\t\t   theirs),\n \t\t\tbranch_name, theirs);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t} else {\n@@ -2282,8 +2315,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t       \"respectively.\\n\",\n \t\t\t   ours + theirs),\n \t\t\tbranch_name, ours, theirs);\n-\t\tif (show_divergence_advice &&\n-\t\t    advice_enabled(ADVICE_STATUS_HINTS))\n+\t\tif (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS))\n \t\t\tstrbuf_addstr(sb,\n \t\t\t\t_(\"  (use \\\"git pull\\\" if you want to integrate the remote branch with yours)\\n\"));\n \t}\n@@ -2296,34 +2328,92 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t enum ahead_behind_flags abf,\n \t\t\t int show_divergence_advice)\n {\n-\tint ours, theirs, cmp_fetch;\n-\tconst char *full_base;\n-\tchar *base;\n-\tint upstream_is_gone = 0;\n+\tchar *compare_branches = NULL;\n+\tstruct string_list branches = STRING_LIST_INIT_DUP;\n+\tstruct strset processed_refs = STRSET_INIT;\n+\tint reported = 0;\n+\tsize_t i;\n+\tconst char *upstream_ref;\n+\tconst char *push_ref;\n \n-\tcmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);\n-\tif (cmp_fetch < 0) {\n-\t\tif (!full_base)\n-\t\t\treturn 0;\n-\t\tupstream_is_gone = 1;\n+\trepo_config_get_string(the_repository, \"status.comparebranches\",\n+\t\t\t       &compare_branches);\n+\n+\tif (compare_branches) {\n+\t\tstring_list_split(&branches, compare_branches, \" \", -1);\n+\t\tstring_list_remove_empty_items(&branches, 0);\n+\t} else {\n+\t\tstring_list_append(&branches, \"@{upstream}\");\n \t}\n \n-\tbase = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),\n-\t\t\t\t\t    full_base, 0);\n+\tupstream_ref = branch_get_upstream(branch, NULL);\n+\tpush_ref = branch_get_push(branch, NULL);\n \n-\tif (upstream_is_gone) {\n-\t\tstrbuf_addf(sb,\n-\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\tbase);\n-\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n-\t} else {\n-\t\tformat_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);\n+\tfor (i = 0; i < branches.nr; i++) {\n+\t\tchar *full_ref;\n+\t\tchar *short_ref;\n+\t\tint ours, theirs, cmp;\n+\t\tint is_upstream, is_push;\n+\t\tunsigned flags = 0;\n+\n+\t\tfull_ref = resolve_compare_branch(branch,\n+\t\t\t\t\t\t  branches.items[i].string);\n+\t\tif (!full_ref)\n+\t\t\tcontinue;\n+\n+\t\tif (!strset_add(&processed_refs, full_ref)) {\n+\t\t\tfree(full_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tshort_ref = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), full_ref, 0);\n+\n+\t\tis_upstream = upstream_ref && !strcmp(full_ref, upstream_ref);\n+\t\tis_push = push_ref && !strcmp(full_ref, push_ref);\n+\n+\t\tif (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))\n+\t\t\tis_push = 1;\n+\n+\t\tcmp = stat_branch_pair(branch->refname, full_ref,\n+\t\t\t\t       &ours, &theirs, abf);\n+\n+\t\tif (cmp < 0) {\n+\t\t\tif (is_upstream) {\n+\t\t\t\tstrbuf_addf(sb,\n+\t\t\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n+\t\t\t\t\tshort_ref);\n+\t\t\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t\t\t\treported = 1;\n+\t\t\t}\n+\t\t\tfree(full_ref);\n+\t\t\tfree(short_ref);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (reported)\n+\t\t\tstrbuf_addstr(sb, \"\\n\");\n+\n+\t\tif (is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_PULL;\n+\t\tif (is_push)\n+\t\t\tflags |= ENABLE_ADVICE_PUSH;\n+\t\tif (show_divergence_advice && is_upstream)\n+\t\t\tflags |= ENABLE_ADVICE_DIVERGENCE;\n+\t\tformat_branch_comparison(sb, !cmp, ours, theirs, short_ref,\n+\t\t\t\t\t abf, flags);\n+\t\treported = 1;\n+\n+\t\tfree(full_ref);\n+\t\tfree(short_ref);\n \t}\n \n-\tfree(base);\n-\treturn 1;\n+\tstring_list_clear(&branches, 0);\n+\tstrset_clear(&processed_refs);\n+\tfree(compare_branches);\n+\treturn reported;\n }\n \n static int one_local_ref(const struct reference *ref, void *cb_data)\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 0b719bbae6..0242b5bf7a 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -292,4 +292,358 @@ test_expect_success '--set-upstream-to @{-1}' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status tracking origin/main shows only main' '\n+\tgit -C test checkout b4 &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n+\tgit -C test checkout b4 &&\n+\tgit -C test status --no-ahead-behind >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch b4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches deduplicates when upstream and push are the same' '\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout main &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch main\n+\tYour branch is up to date with ${SQ}origin/main${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with only upstream shows only upstream' '\n+\ttest_config -C test status.compareBranches \"@{upstream}\" &&\n+\tgit -C test checkout main &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch main\n+\tYour branch is up to date with ${SQ}origin/main${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with only push shows only push' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{push}\" &&\n+\tgit -C test checkout -b feature2 origin/main &&\n+\tgit -C test push origin HEAD &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature3 origin/main &&\n+\tgit -C test push origin HEAD &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature3\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature3${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows both branches' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout feature3 >actual &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature3${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup for ahead of tracked but diverged from main' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature4 origin/main &&\n+\t\tadvance work1 &&\n+\t\tgit checkout origin/main &&\n+\t\tadvance work2 &&\n+\t\tgit push origin HEAD:main &&\n+\t\tgit checkout feature4 &&\n+\t\tadvance work3\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches shows diverged and ahead' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout feature4 &&\n+\tgit -C test branch --set-upstream-to origin/main &&\n+\tgit -C test push origin HEAD &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} have diverged,\n+\tand have 3 and 1 different commits each, respectively.\n+\t  (use \"git pull\" if you want to integrate the remote branch with yours)\n+\n+\tYour branch is ahead of ${SQ}origin/feature4${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout feature4 &&\n+\tgit -C test status --no-ahead-behind >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature4\n+\tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n+\n+\tYour branch and ${SQ}origin/feature4${SQ} refer to different commits.\n+\t  (use \"git status --ahead-behind\" for details)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream remote' '\n+\t(\n+\t\tcd test &&\n+\t\tgit remote add upstream ../. &&\n+\t\tgit fetch upstream\n+\t)\n+'\n+\n+test_expect_success 'status.compareBranches with upstream and origin remotes' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature5 upstream/main &&\n+\tgit -C test push origin &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature5\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/feature5${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches supports ordered upstream/push entries' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{push} @{upstream}\" &&\n+\tgit -C test checkout -b feature6 upstream/main &&\n+\tgit -C test push origin &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature6\n+\tYour branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches deduplicates repeated specifiers' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{push} @{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature7 upstream/main &&\n+\tgit -C test push origin &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is ahead of ${SQ}origin/feature7${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with diverged push branch' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature8 upstream/main &&\n+\t(cd test && advance work81) &&\n+\tgit -C test push origin &&\n+\tgit -C test reset --hard upstream/main &&\n+\t(cd test && advance work82) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature8\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature8${SQ} have diverged,\n+\tand have 1 and 1 different commits each, respectively.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows up to date branches' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature9 upstream/main &&\n+\tgit -C test push origin &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout feature9 >actual &&\n+\tgit -C test push origin &&\n+\tgit -C test status --no-ahead-behind >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature9\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout with status.compareBranches shows up to date' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout feature9 >actual &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is up to date with ${SQ}upstream/main${SQ}.\n+\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with upstream behind and push up to date' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b ahead upstream/main &&\n+\t(cd test && advance work) &&\n+\tgit -C test push upstream HEAD &&\n+\tgit -C test checkout -b feature10 upstream/main &&\n+\tgit -C test push origin &&\n+\tgit -C test branch --set-upstream-to upstream/ahead &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature10\n+\tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n+\t  (use \"git pull\" to update your local branch)\n+\n+\tYour branch is up to date with ${SQ}origin/feature10${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push refspec' '\n+\ttest_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature11 origin/main &&\n+\tgit -C test push &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature11\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with remapped push and upstream remote' '\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test remote.origin.push refs/heads/feature12:refs/heads/remapped &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature12 upstream/main &&\n+\tgit -C test push origin &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature12\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tYour branch is ahead of ${SQ}origin/remapped${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+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n"},{"id":"537792","messageId":"xmqq5x7ba5jf.fsf@gitster.g","threadId":"64668","inReplyTo":"pull.2138.v31.git.git.1772627131.gitgitgadget@gmail.com","subject":"Re: [PATCH v31 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-04T17:05:56Z","receivedAt":"2026-03-04T17:05:59Z","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> Harald Nordgren (2):\n>   refactor format_branch_comparison in preparation\n>   status: add status.compareBranches config for multiple branch\n>     comparisons\n>\n>  Documentation/config/status.adoc |  25 +++\n>  remote.c                         | 178 ++++++++++++----\n>  t/t6040-tracking-info.sh         | 354 +++++++++++++++++++++++++++++++\n>  3 files changed, 520 insertions(+), 37 deletions(-)\n>\n>\n> base-commit: 50d063e335afd5828fbb9de2f2b2fb44fd884d2b\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v31\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v31\n> Pull-Request: https://github.com/git/git/pull/2138\n> ...\n> Range-diff vs v30:\n>\n>  1:  7f517b8c7f = 1:  320c1ce55a refactor format_branch_comparison in preparation\n>  2:  501bd40294 ! 2:  f07ccb278c status: add status.compareBranches config for multiple branch comparisons\n>      @@ Documentation/config/status.adoc: status.aheadBehind::\n>       +If not set, the default behavior is equivalent to `@{upstream}`, which\n>       +compares against the configured upstream tracking branch.\n>       ++\n>      ++The entries are shown in the order they appear in the configuration.\n>      ++Duplicate entries that resolve to the same ref are suppressed after\n>      ++their first occurrence, so `@{push} @{upstream} @{push}` shows at\n>      ++most two comparisons.  When `@{upstream}` and `@{push}` resolve to\n>      ++the same remote-tracking branch, only one comparison is shown.\n\n\nAs the previous iteration of this series is already in 'next', we'd\nneed to turn this into an incremental.  Luckily, as the difference\nis fairly clear and only in [2/2], let me try to do so myself to\npotentially save one roundtrip.\n\nThanks.\n\n----- >8 -----\nFrom: Harald Nordgren <haraldnordgren@gmail.com>\nDate: Wed, 04 Mar 2026 12:25:31 +0000\nSubject: [PATCH 3/2] status: clarify how status.compareBranches deduplicates\n\nThe order of output when multiple branches are specified on the\nconfiguration variable was not clearly spelled out in the\ndocumentation.\n\nAdd a paragraph to describe the order and also how the branches are\ndeduplicated.  Update t6040 with additional tests to illustrate how\nmultiple branches are shown and deduplicated.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n[jc: made a whole replacement into incremental; wrote log message.]\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/config/status.adoc |   6 ++\n t/t6040-tracking-info.sh         | 120 ++++++++++++++++++++++++++-------------\n 2 files changed, 88 insertions(+), 38 deletions(-)\n\ndiff --git c/Documentation/config/status.adoc w/Documentation/config/status.adoc\nindex 15ccd0116b..b5dd85b761 100644\n--- c/Documentation/config/status.adoc\n+++ w/Documentation/config/status.adoc\n@@ -26,6 +26,12 @@ status.compareBranches::\n If not set, the default behavior is equivalent to `@{upstream}`, which\n compares against the configured upstream tracking branch.\n +\n+The entries are shown in the order they appear in the configuration.\n+Duplicate entries that resolve to the same ref are suppressed after\n+their first occurrence, so `@{push} @{upstream} @{push}` shows at\n+most two comparisons.  When `@{upstream}` and `@{push}` resolve to\n+the same remote-tracking branch, only one comparison is shown.\n++\n Example:\n +\n ----\ndiff --git c/t/t6040-tracking-info.sh w/t/t6040-tracking-info.sh\nindex c24f545036..0242b5bf7a 100755\n--- c/t/t6040-tracking-info.sh\n+++ w/t/t6040-tracking-info.sh\n@@ -293,11 +293,8 @@ test_expect_success '--set-upstream-to @{-1}' '\n '\n \n test_expect_success 'status tracking origin/main shows only main' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout b4 &&\n-\t\tgit status >../actual\n-\t) &&\n+\tgit -C test checkout b4 &&\n+\tgit -C test status >actual &&\n \tcat >expect <<-EOF &&\n \tOn branch b4\n \tYour branch is ahead of ${SQ}origin/main${SQ} by 2 commits.\n@@ -309,11 +306,8 @@ test_expect_success 'status tracking origin/main shows only main' '\n '\n \n test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '\n-\t(\n-\t\tcd test &&\n-\t\tgit checkout b4 &&\n-\t\tgit status --no-ahead-behind >../actual\n-\t) &&\n+\tgit -C test checkout b4 &&\n+\tgit -C test status --no-ahead-behind >actual &&\n \tcat >expect <<-EOF &&\n \tOn branch b4\n \tYour branch and ${SQ}origin/main${SQ} refer to different commits.\n@@ -324,7 +318,7 @@ test_expect_success 'status --no-ahead-behind tracking origin/main shows only ma\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'status.compareBranches from upstream has no duplicates' '\n+test_expect_success 'status.compareBranches deduplicates when upstream and push are the same' '\n \ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n \tgit -C test checkout main &&\n \tgit -C test status >actual &&\n@@ -337,18 +331,48 @@ test_expect_success 'status.compareBranches from upstream has no duplicates' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n+test_expect_success 'status.compareBranches with only upstream shows only upstream' '\n+\ttest_config -C test status.compareBranches \"@{upstream}\" &&\n+\tgit -C test checkout main &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch main\n+\tYour branch is up to date with ${SQ}origin/main${SQ}.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches with only push shows only push' '\n \ttest_config -C test push.default current &&\n-\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\ttest_config -C test status.compareBranches \"@{push}\" &&\n \tgit -C test checkout -b feature2 origin/main &&\n \tgit -C test push origin HEAD &&\n \t(cd test && advance work) &&\n \tgit -C test status >actual &&\n \tcat >expect <<-EOF &&\n \tOn branch feature2\n+\tYour branch is ahead of ${SQ}origin/feature2${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+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature3 origin/main &&\n+\tgit -C test push origin HEAD &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature3\n \tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n \n-\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\tYour branch is ahead of ${SQ}origin/feature3${SQ} by 1 commit.\n \t  (use \"git push\" to publish your local commits)\n \n \tnothing to commit, working tree clean\n@@ -359,11 +383,11 @@ test_expect_success 'status.compareBranches shows ahead of both upstream and pus\n test_expect_success 'checkout with status.compareBranches shows both branches' '\n \ttest_config -C test push.default current &&\n \ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n-\tgit -C test checkout feature2 >actual &&\n+\tgit -C test checkout feature3 >actual &&\n \tcat >expect <<-EOF &&\n \tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n \n-\tYour branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.\n+\tYour branch is ahead of ${SQ}origin/feature3${SQ} by 1 commit.\n \t  (use \"git push\" to publish your local commits)\n \tEOF\n \ttest_cmp expect actual\n@@ -469,21 +493,41 @@ test_expect_success 'status.compareBranches supports ordered upstream/push entri\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status.compareBranches deduplicates repeated specifiers' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test remote.pushDefault origin &&\n+\ttest_config -C test status.compareBranches \"@{push} @{upstream} @{push}\" &&\n+\tgit -C test checkout -b feature7 upstream/main &&\n+\tgit -C test push origin &&\n+\t(cd test && advance work) &&\n+\tgit -C test status >actual &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature7\n+\tYour branch is ahead of ${SQ}origin/feature7${SQ} by 1 commit.\n+\t  (use \"git push\" to publish your local commits)\n+\n+\tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'status.compareBranches with diverged push branch' '\n \ttest_config -C test push.default current &&\n \ttest_config -C test remote.pushDefault origin &&\n \ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n-\tgit -C test checkout -b feature7 upstream/main &&\n-\t(cd test && advance work71) &&\n+\tgit -C test checkout -b feature8 upstream/main &&\n+\t(cd test && advance work81) &&\n \tgit -C test push origin &&\n \tgit -C test reset --hard upstream/main &&\n-\t(cd test && advance work72) &&\n+\t(cd test && advance work82) &&\n \tgit -C test status >actual &&\n \tcat >expect <<-EOF &&\n-\tOn branch feature7\n+\tOn branch feature8\n \tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n \n-\tYour branch and ${SQ}origin/feature7${SQ} have diverged,\n+\tYour branch and ${SQ}origin/feature8${SQ} have diverged,\n \tand have 1 and 1 different commits each, respectively.\n \n \tnothing to commit, working tree clean\n@@ -495,14 +539,14 @@ test_expect_success 'status.compareBranches shows up to date branches' '\n \ttest_config -C test push.default current &&\n \ttest_config -C test remote.pushDefault origin &&\n \ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n-\tgit -C test checkout -b feature8 upstream/main &&\n+\tgit -C test checkout -b feature9 upstream/main &&\n \tgit -C test push origin &&\n \tgit -C test status >actual &&\n \tcat >expect <<-EOF &&\n-\tOn branch feature8\n+\tOn branch feature9\n \tYour branch is up to date with ${SQ}upstream/main${SQ}.\n \n-\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n \n \tnothing to commit, working tree clean\n \tEOF\n@@ -513,14 +557,14 @@ test_expect_success 'status --no-ahead-behind with status.compareBranches up to\n \ttest_config -C test push.default current &&\n \ttest_config -C test remote.pushDefault origin &&\n \ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n-\tgit -C test checkout feature8 >actual &&\n+\tgit -C test checkout feature9 >actual &&\n \tgit -C test push origin &&\n \tgit -C test status --no-ahead-behind >actual &&\n \tcat >expect <<-EOF &&\n-\tOn branch feature8\n+\tOn branch feature9\n \tYour branch is up to date with ${SQ}upstream/main${SQ}.\n \n-\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n \n \tnothing to commit, working tree clean\n \tEOF\n@@ -531,11 +575,11 @@ test_expect_success 'checkout with status.compareBranches shows up to date' '\n \ttest_config -C test push.default current &&\n \ttest_config -C test remote.pushDefault origin &&\n \ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n-\tgit -C test checkout feature8 >actual &&\n+\tgit -C test checkout feature9 >actual &&\n \tcat >expect <<-EOF &&\n \tYour branch is up to date with ${SQ}upstream/main${SQ}.\n \n-\tYour branch is up to date with ${SQ}origin/feature8${SQ}.\n+\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n \tEOF\n \ttest_cmp expect actual\n '\n@@ -547,16 +591,16 @@ test_expect_success 'status.compareBranches with upstream behind and push up to\n \tgit -C test checkout -b ahead upstream/main &&\n \t(cd test && advance work) &&\n \tgit -C test push upstream HEAD &&\n-\tgit -C test checkout -b feature9 upstream/main &&\n+\tgit -C test checkout -b feature10 upstream/main &&\n \tgit -C test push origin &&\n \tgit -C test branch --set-upstream-to upstream/ahead &&\n \tgit -C test status >actual &&\n \tcat >expect <<-EOF &&\n-\tOn branch feature9\n+\tOn branch feature10\n \tYour branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.\n \t  (use \"git pull\" to update your local branch)\n \n-\tYour branch is up to date with ${SQ}origin/feature9${SQ}.\n+\tYour branch is up to date with ${SQ}origin/feature10${SQ}.\n \n \tnothing to commit, working tree clean\n \tEOF\n@@ -564,14 +608,14 @@ test_expect_success 'status.compareBranches with upstream behind and push up to\n '\n \n test_expect_success 'status.compareBranches with remapped push refspec' '\n-\ttest_config -C test remote.origin.push refs/heads/feature10:refs/heads/remapped &&\n+\ttest_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n \ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n-\tgit -C test checkout -b feature10 origin/main &&\n+\tgit -C test checkout -b feature11 origin/main &&\n \tgit -C test push &&\n \t(cd test && advance work) &&\n \tgit -C test status >actual &&\n \tcat >expect <<-EOF &&\n-\tOn branch feature10\n+\tOn branch feature11\n \tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n \n \tYour branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.\n@@ -584,14 +628,14 @@ test_expect_success 'status.compareBranches with remapped push refspec' '\n \n test_expect_success 'status.compareBranches with remapped push and upstream remote' '\n \ttest_config -C test remote.pushDefault origin &&\n-\ttest_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&\n+\ttest_config -C test remote.origin.push refs/heads/feature12:refs/heads/remapped &&\n \ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n-\tgit -C test checkout -b feature11 upstream/main &&\n+\tgit -C test checkout -b feature12 upstream/main &&\n \tgit -C test push origin &&\n \t(cd test && advance work) &&\n \tgit -C test status >actual &&\n \tcat >expect <<-EOF &&\n-\tOn branch feature11\n+\tOn branch feature12\n \tYour branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.\n \n \tYour branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.\n\n\n"},{"id":"538254","messageId":"20260309092052.79051-1-haraldnordgren@gmail.com","threadId":"64668","inReplyTo":"xmqq5x7ba5jf.fsf@gitster.g","subject":"Re: [PATCH v31 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-03-09T09:20:52Z","receivedAt":"2026-03-09T09:20:55Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"Thanks for all your help here! Is there anything you need from me now?\n\n\nHarald\n"},{"id":"538286","messageId":"xmqqwlzl598f.fsf@gitster.g","threadId":"64668","inReplyTo":"20260309092052.79051-1-haraldnordgren@gmail.com","subject":"Re: [PATCH v31 0/2] status: add status.compareBranches config for multiple branch comparisons","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-09T15:10:56Z","receivedAt":"2026-03-09T15:10:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> Thanks for all your help here! Is there anything you need from me now?\n\nThanks for all the work.  If you haven't heard any breakages from\nothers, and if you haven't found any breakages yourself, there is\nnothing we need from you right at this moment.\n\nThe patch series is cooking in 'next' and unless people find\nsomething glaringly wrong, will be merged down to 'master' later\n(after which, people might still find something wrong in it, of\ncourse---any breakages and regressions need to be handled no matter\nanyway ;-).\n\n"}]}