[PATCH v26 0/2] status: add status.compareBranches config for multiple branch comparisons
From: Harald Nordgren via GitGitGadget <hidden>
Date: 2026-01-18 19:59:15
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 [off-list ref]
## 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 [off-list ref]
+ ## 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