From: Harald Nordgren via GitGitGadget Date: Sun, 18 Jan 2026 19:59:11 GMT Subject: [PATCH v26 0/2] status: add status.compareBranches config for multiple branch comparisons Message-ID: In-Reply-To: cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin ychin.macvim@gmail.com cc: "brian m. carlson" sandals@crustytoothpaste.net cc: Ben Knoble ben.knoble@gmail.com cc: "Kristoffer Haugsbakk" kristofferhaugsbakk@fastmail.com cc: Phillip Wood phillip.wood123@gmail.com cc: Nico Williams nico@cryptonector.com cc: Patrick Steinhardt ps@pks.im cc: Jeff King peff@peff.net Harald Nordgren (2): refactor format_branch_comparison in preparation status: add status.compareBranches config for multiple branch comparisons Documentation/config/status.adoc | 20 ++ remote.c | 187 ++++++++++++++---- t/t6040-tracking-info.sh | 322 +++++++++++++++++++++++++++++++ 3 files changed, 492 insertions(+), 37 deletions(-) base-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v26 Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v26 Pull-Request: https://github.com/git/git/pull/2138 Range-diff vs v25: 1: fd05c7b778 = 1: 27a46f8d9c refactor format_branch_comparison in preparation 2: fa744efc59 ! 2: caa761f615 status: show comparison with push remote tracking branch @@ Metadata Author: Harald Nordgren ## Commit message ## - status: show comparison with push remote tracking branch + status: add status.compareBranches config for multiple branch comparisons - "git status" on a branch that follows a remote branch compares - commits on the current branch and the remote-tracking branch it - builds upon, to show "ahead", "behind", or "diverged" status. + Add a new configuration variable `status.compareBranches` that allows + users to specify a space-separated list of branches to compare against + the current branch in `git status` output. - When working on a feature branch that tracks a remote feature branch, - but you also want to track progress relative to the push destination - tracking branch (which may differ from the upstream branch), git status - now shows an additional comparison. + Each branch in the list can be: + - A remote-tracking branch name (e.g., `origin/main`) + - The special reference `@{upstream}` for the tracking branch + - The special reference `@{push}` for the push destination - When the upstream tracking branch differs from the push destination - tracking branch, git status shows both the comparison with the upstream - tracking branch (as before) and an additional comparison with the push - destination tracking branch. The push branch comparison appears on a - separate line after the upstream branch status, using the same format. + When not configured, the default behavior is equivalent to setting + `status.compareBranches = @{upstream}`, preserving backward compatibility. - Example output when tracking origin/main but push destination is - origin/feature: - On branch feature - Your branch and 'origin/main' have diverged, - and have 3 and 1 different commits each, respectively. - (use "git pull" if you want to integrate the remote branch with yours) + The advice messages shown are context-aware: + - "git pull" advice is shown only when comparing against @{upstream} + - "git push" advice is shown only when comparing against @{push} + - Divergence advice is shown for upstream branch comparisons - Your branch is ahead of 'origin/feature' by 1 commit. - (use "git push" to publish your local commits) + This is useful for triangular workflows where the upstream tracking + branch differs from the push destination, allowing users to see their + status relative to both branches at once. - The comparison is only shown when the push destination tracking branch - differs from the upstream tracking branch, even if they are on the same - remote. + Example configuration: + [status] + compareBranches = @{upstream} @{push} Signed-off-by: Harald Nordgren + ## Documentation/config/status.adoc ## +@@ Documentation/config/status.adoc: status.aheadBehind:: + `--no-ahead-behind` by default in linkgit:git-status[1] for + non-porcelain status formats. Defaults to true. + ++status.compareBranches:: ++ A space-separated list of branches to compare the current branch ++ against in linkgit:git-status[1]. Each branch specification can be ++ a remote-tracking branch name (e.g. `origin/main`), or a special ++ reference like `@{upstream}` or `@{push}`. For each branch in the ++ list, git status shows whether the current branch is ahead, behind, ++ or has diverged from that branch. +++ ++If not set, the default behavior is equivalent to `@{upstream}`, which ++compares against the configured upstream tracking branch. +++ ++Example: +++ ++---- ++[status] ++ compareBranches = origin/main origin/develop ++---- +++ ++This would show comparisons against both `origin/main` and `origin/develop`. ++ + status.displayCommentPrefix:: + If set to true, linkgit:git-status[1] will insert a comment + prefix before each output line (starting with + ## remote.c ## @@ @@ remote.c struct counted_string { size_t len; const char *s; +@@ remote.c: static void branch_release(struct branch *branch) + free((char *)branch->refname); + free(branch->remote_name); + free(branch->pushremote_name); ++ free((char *)branch->push_tracking_ref); + merge_clear(branch); + } + @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs, return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf); } -+static char *get_remote_push_branch(struct branch *branch) ++static char *resolve_compare_branch(struct branch *branch, const char *name) +{ -+ struct remote *remote; -+ const char *push_remote; -+ char *push_dst = NULL; -+ char *tracking_ref; -+ const char *resolved; ++ struct strbuf buf = STRBUF_INIT; ++ const char *resolved = NULL; + char *ret; + -+ if (!branch) -+ return NULL; -+ -+ push_remote = pushremote_for_branch(branch, NULL); -+ if (!push_remote) ++ if (!branch || !name) + return NULL; + -+ remote = remotes_remote_get(the_repository, push_remote); -+ if (!remote) -+ return NULL; ++ if (!strcasecmp(name, "@{upstream}") || !strcasecmp(name, "@{u}")) ++ resolved = branch_get_upstream(branch, NULL); ++ else if (!strcasecmp(name, "@{push}")) ++ resolved = branch_get_push(branch, NULL); + -+ push_dst = remote_ref_for_branch(branch, 1); -+ if (!push_dst) { -+ if (remote->push.nr) -+ return NULL; -+ push_dst = xstrdup(branch->refname); -+ } -+ -+ tracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL); -+ free(push_dst); -+ -+ if (!tracking_ref) -+ return NULL; ++ if (resolved) ++ return xstrdup(resolved); + ++ strbuf_addf(&buf, "refs/remotes/%s", name); + resolved = refs_resolve_ref_unsafe( + get_main_ref_store(the_repository), -+ tracking_ref, ++ buf.buf, + RESOLVE_REF_READING, + NULL, NULL); -+ -+ if (!resolved) { -+ free(tracking_ref); -+ return NULL; ++ if (resolved) { ++ ret = xstrdup(resolved); ++ strbuf_release(&buf); ++ return ret; + } + -+ ret = xstrdup(resolved); -+ free(tracking_ref); -+ return ret; ++ strbuf_release(&buf); ++ return NULL; +} + static void format_branch_comparison(struct strbuf *sb, @@ remote.c: static void format_branch_comparison(struct strbuf *sb, _(" (use \"git pull\" if you want to integrate the remote branch with yours)\n")); } @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb, - const char *full_base; - char *base; - int upstream_is_gone = 0; -+ unsigned base_branch_flags = ENABLE_ADVICE_PULL | ENABLE_ADVICE_PUSH; -+ int push_ours, push_theirs, push_cmp_fetch; -+ char *full_push; -+ char *push = NULL; -+ unsigned push_branch_flags = 0; + enum ahead_behind_flags abf, + int show_divergence_advice) + { +- int ours, theirs, cmp_fetch; +- const char *full_base; +- char *base; +- int upstream_is_gone = 0; +- +- cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf); +- if (cmp_fetch < 0) { +- if (!full_base) +- return 0; +- upstream_is_gone = 1; ++ char *compare_branches_config = NULL; ++ struct string_list compare_branches = STRING_LIST_INIT_DUP; ++ struct string_list_item *item; ++ int reported = 0; ++ size_t i; ++ const char *upstream_ref; ++ const char *push_ref; ++ ++ repo_config_get_string(the_repository, "status.comparebranches", ++ &compare_branches_config); ++ ++ if (compare_branches_config) { ++ string_list_split(&compare_branches, compare_branches_config, ++ " ", -1); ++ string_list_remove_empty_items(&compare_branches, 0); ++ } else { ++ string_list_append(&compare_branches, "@{upstream}"); + } - cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf); - if (cmp_fetch < 0) { -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb, - base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository), - full_base, 0); +- base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository), +- full_base, 0); ++ upstream_ref = branch_get_upstream(branch, NULL); ++ push_ref = branch_get_push(branch, NULL); -+ full_push = get_remote_push_branch(branch); -+ if (full_push) { -+ push = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository), -+ full_push, 0); -+ if (push && base && strcmp(base, push)) { -+ push_cmp_fetch = stat_branch_pair(branch->refname, full_push, -+ &push_ours, &push_theirs, abf); -+ if (push_cmp_fetch >= 0) { -+ base_branch_flags = ENABLE_ADVICE_PULL; -+ push_branch_flags = ENABLE_ADVICE_PUSH; +- if (upstream_is_gone) { +- strbuf_addf(sb, +- _("Your branch is based on '%s', but the upstream is gone.\n"), +- base); +- if (advice_enabled(ADVICE_STATUS_HINTS)) +- strbuf_addstr(sb, +- _(" (use \"git branch --unset-upstream\" to fixup)\n")); +- } else { +- format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice); ++ for (i = 0; i < compare_branches.nr; i++) { ++ char *full_ref; ++ char *short_ref; ++ int ours, theirs, cmp; ++ int is_upstream, is_push; ++ unsigned flags = 0; ++ ++ item = &compare_branches.items[i]; ++ full_ref = resolve_compare_branch(branch, item->string); ++ if (!full_ref) ++ continue; ++ ++ short_ref = refs_shorten_unambiguous_ref( ++ get_main_ref_store(the_repository), full_ref, 0); ++ ++ is_upstream = upstream_ref && !strcmp(full_ref, upstream_ref); ++ is_push = push_ref && !strcmp(full_ref, push_ref); ++ ++ if (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref))) ++ is_push = 1; ++ ++ cmp = stat_branch_pair(branch->refname, full_ref, ++ &ours, &theirs, abf); ++ ++ if (cmp < 0) { ++ if (is_upstream) { ++ strbuf_addf(sb, ++ _("Your branch is based on '%s', but the upstream is gone.\n"), ++ short_ref); ++ if (advice_enabled(ADVICE_STATUS_HINTS)) ++ strbuf_addstr(sb, ++ _(" (use \"git branch --unset-upstream\" to fixup)\n")); ++ reported = 1; + } ++ free(full_ref); ++ free(short_ref); ++ continue; + } -+ } + - if (upstream_is_gone) { - strbuf_addf(sb, - _("Your branch is based on '%s', but the upstream is gone.\n"), -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb, - strbuf_addstr(sb, - _(" (use \"git branch --unset-upstream\" to fixup)\n")); - } else { -- format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice); -+ if (show_divergence_advice) -+ base_branch_flags |= ENABLE_ADVICE_DIVERGENCE; -+ format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, -+ base_branch_flags); -+ } -+ -+ if (push_branch_flags & ENABLE_ADVICE_PUSH) { -+ strbuf_addstr(sb, "\n"); -+ format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf, -+ push_branch_flags); ++ if (reported) ++ strbuf_addstr(sb, "\n"); ++ ++ if (is_upstream) ++ flags |= ENABLE_ADVICE_PULL; ++ if (is_push) ++ flags |= ENABLE_ADVICE_PUSH; ++ if (show_divergence_advice && is_upstream) ++ flags |= ENABLE_ADVICE_DIVERGENCE; ++ format_branch_comparison(sb, !cmp, ours, theirs, short_ref, ++ abf, flags); ++ reported = 1; ++ ++ free(full_ref); ++ free(short_ref); } - free(base); -+ free(full_push); -+ free(push); - return 1; +- free(base); +- return 1; ++ string_list_clear(&compare_branches, 0); ++ free(compare_branches_config); ++ return reported; } + static int one_local_ref(const struct reference *ref, void *cb_data) ## t/t6040-tracking-info.sh ## @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' + test_cmp expect actual +' + -+test_expect_success 'status shows ahead of both origin/main and feature branch' ' ++test_expect_success 'setup for compareBranches tests' ' ++ ( ++ cd test && ++ git config push.default current && ++ git config status.compareBranches "@{upstream} @{push}" ++ ) ++' ++ ++test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' ' + ( + cd test && + git checkout -b feature2 origin/main && @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' + test_cmp expect actual +' + -+test_expect_success 'checkout shows ahead of both origin/main and feature branch' ' ++test_expect_success 'checkout with status.compareBranches shows both branches' ' + ( + cd test && + git checkout feature2 >../actual @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' + ) +' + -+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' ' ++test_expect_success 'status.compareBranches shows diverged and ahead' ' + ( + cd test && + git checkout feature4 && @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' + test_cmp expect actual +' + -+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' ' ++test_expect_success 'status --no-ahead-behind with status.compareBranches' ' + ( + cd test && + git checkout feature4 && @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' + ) +' + -+test_expect_success 'status with upstream remote and push.default set to origin' ' ++test_expect_success 'status.compareBranches with upstream and origin remotes' ' + ( + cd test && + git checkout -b feature5 upstream/main && @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' + test_cmp expect actual +' + -+test_expect_success 'status with upstream remote and push.default set to origin and diverged' ' ++test_expect_success 'status.compareBranches with upstream and origin remotes multiple compare branches' ' + ( + cd test && + git checkout -b feature6 upstream/main && ++ git push origin && ++ advance work && ++ git -c status.compareBranches="upstream/main origin/feature6 origin/feature5" status >../actual ++ ) && ++ cat >expect <<-EOF && ++ On branch feature6 ++ Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. ++ ++ Your branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit. ++ (use "git push" to publish your local commits) ++ ++ Your branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit. ++ ++ nothing to commit, working tree clean ++ EOF ++ test_cmp expect actual ++' ++ ++test_expect_success 'status.compareBranches with diverged push branch' ' ++ ( ++ cd test && ++ git checkout -b feature7 upstream/main && + advance work && + git push origin && + git reset --hard upstream/main && @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' + git status >../actual + ) && + cat >expect <<-EOF && -+ On branch feature6 ++ On branch feature7 + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + -+ Your branch and ${SQ}origin/feature6${SQ} have diverged, ++ Your branch and ${SQ}origin/feature7${SQ} have diverged, + and have 1 and 1 different commits each, respectively. + + nothing to commit, working tree clean @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' + test_cmp expect actual +' + -+test_expect_success 'status with upstream remote and push branch up to date' ' ++test_expect_success 'status.compareBranches shows up to date branches' ' + ( + cd test && -+ git checkout -b feature7 upstream/main && ++ git checkout -b feature8 upstream/main && + git push origin && + git status >../actual + ) && + cat >expect <<-EOF && -+ On branch feature7 ++ On branch feature8 + Your branch is up to date with ${SQ}upstream/main${SQ}. + -+ Your branch is up to date with ${SQ}origin/feature7${SQ}. ++ Your branch is up to date with ${SQ}origin/feature8${SQ}. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + -+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' ' ++test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' ' + ( + cd test && -+ git checkout feature7 && ++ git checkout feature8 && + git push origin && + git status --no-ahead-behind >../actual + ) && + cat >expect <<-EOF && -+ On branch feature7 ++ On branch feature8 + Your branch is up to date with ${SQ}upstream/main${SQ}. + -+ Your branch is up to date with ${SQ}origin/feature7${SQ}. ++ Your branch is up to date with ${SQ}origin/feature8${SQ}. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + -+test_expect_success 'checkout shows push branch up to date' ' ++test_expect_success 'checkout with status.compareBranches shows up to date' ' + ( + cd test && -+ git checkout feature7 >../actual ++ git checkout feature8 >../actual + ) && + cat >expect <<-EOF && + Your branch is up to date with ${SQ}upstream/main${SQ}. + -+ Your branch is up to date with ${SQ}origin/feature7${SQ}. ++ Your branch is up to date with ${SQ}origin/feature8${SQ}. + EOF + test_cmp expect actual +' + -+test_expect_success 'status with upstream ahead and push branch up to date' ' ++test_expect_success 'status.compareBranches with upstream behind and push up to date' ' + ( + cd test && + git checkout -b ahead upstream/main && + advance work && + git push upstream HEAD && -+ git checkout -b feature8 upstream/main && ++ git checkout -b feature9 upstream/main && + git push origin && + git branch --set-upstream-to upstream/ahead && + git status >../actual + ) && + cat >expect <<-EOF && -+ On branch feature8 ++ On branch feature9 + Your branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull" to update your local branch) + -+ Your branch is up to date with ${SQ}origin/feature8${SQ}. ++ Your branch is up to date with ${SQ}origin/feature9${SQ}. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + -+test_expect_success 'status shows remapped push refspec' ' ++test_expect_success 'status.compareBranches with remapped push refspec' ' + ( + cd test && -+ git checkout -b feature9 origin/main && -+ git config remote.origin.push refs/heads/feature9:refs/heads/remapped && ++ git checkout -b feature10 origin/main && ++ git config remote.origin.push refs/heads/feature10:refs/heads/remapped && + git push && + advance work && + git status >../actual + ) && + cat >expect <<-EOF && -+ On branch feature9 ++ On branch feature10 + Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit. + + Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit. @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' + test_cmp expect actual +' + -+test_expect_success 'status shows remapped push refspec with upstream remote' ' ++test_expect_success 'status.compareBranches with remapped push and upstream remote' ' + ( + cd test && -+ git checkout -b feature10 upstream/main && -+ git config remote.origin.push refs/heads/feature10:refs/heads/remapped && ++ git checkout -b feature11 upstream/main && ++ git config remote.origin.push refs/heads/feature11:refs/heads/remapped && + git push origin && + advance work && + git status >../actual + ) && + cat >expect <<-EOF && -+ On branch feature10 ++ On branch feature11 + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + + Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit. @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' ' + EOF + test_cmp expect actual +' ++ ++test_expect_success 'clean up after compareBranches tests' ' ++ ( ++ cd test && ++ git config --unset status.compareBranches ++ ) ++' + test_done -- gitgitgadget