aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2026-03-12 14:09:06 -0700
committerJunio C Hamano <gitster@pobox.com>2026-03-12 14:09:06 -0700
commitd0413b31ddcce6ae6ffaff0a30a67ffbd1a7c648 (patch)
tree1caab056d17c7d62d2727398c34259fe3cc315f5
parent03161747b412fe739d8a7ef631769b3d8f60d56f (diff)
parent68791d7506aa39da7c98de4143cac09cc93fc404 (diff)
downloadgit-d0413b31ddcce6ae6ffaff0a30a67ffbd1a7c648.tar.xz
Merge branch 'hn/status-compare-with-push'
"git status" learned to show comparison between the current branch and various other branches listed on status.compareBranches configuration. * hn/status-compare-with-push: status: clarify how status.compareBranches deduplicates status: add status.compareBranches config for multiple branch comparisons refactor format_branch_comparison in preparation
-rw-r--r--Documentation/config/status.adoc25
-rw-r--r--remote.c178
-rwxr-xr-xt/t6040-tracking-info.sh354
3 files changed, 520 insertions, 37 deletions
diff --git a/Documentation/config/status.adoc b/Documentation/config/status.adoc
index 8caf90f51c..b5dd85b761 100644
--- a/Documentation/config/status.adoc
+++ b/Documentation/config/status.adoc
@@ -17,6 +17,31 @@ 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 branch comparison specifiers to use in
+ linkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}`
+ are supported. They are interpreted as `branch@{upstream}` and
+ `branch@{push}` for the current branch.
++
+If not set, the default behavior is equivalent to `@{upstream}`, which
+compares against the configured upstream tracking branch.
++
+The entries are shown in the order they appear in the configuration.
+Duplicate entries that resolve to the same ref are suppressed after
+their first occurrence, so `@{push} @{upstream} @{push}` shows at
+most two comparisons. When `@{upstream}` and `@{push}` resolve to
+the same remote-tracking branch, only one comparison is shown.
++
+Example:
++
+----
+[status]
+ compareBranches = @{upstream} @{push}
+----
++
+This would show comparisons against both the configured upstream and push
+tracking branches for the current branch.
+
status.displayCommentPrefix::
If set to true, linkgit:git-status[1] will insert a comment
prefix before each output line (starting with
diff --git a/remote.c b/remote.c
index f6980dc656..7ca2a6501b 100644
--- a/remote.c
+++ b/remote.c
@@ -29,6 +29,12 @@
enum map_direction { FROM_SRC, FROM_DST };
+enum {
+ ENABLE_ADVICE_PULL = (1 << 0),
+ ENABLE_ADVICE_PUSH = (1 << 1),
+ ENABLE_ADVICE_DIVERGENCE = (1 << 2),
+};
+
struct counted_string {
size_t len;
const char *s;
@@ -2234,43 +2240,49 @@ 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);
}
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
- enum ahead_behind_flags abf,
- int show_divergence_advice)
+static char *resolve_compare_branch(struct branch *branch, const char *name)
{
- int ours, theirs, sti;
- const char *full_base;
- char *base;
- int upstream_is_gone = 0;
+ const char *resolved = NULL;
- sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
- if (sti < 0) {
- if (!full_base)
- return 0;
- upstream_is_gone = 1;
+ if (!branch || !name)
+ return NULL;
+
+ if (!strcasecmp(name, "@{upstream}")) {
+ resolved = branch_get_upstream(branch, NULL);
+ } else if (!strcasecmp(name, "@{push}")) {
+ resolved = branch_get_push(branch, NULL);
+ } else {
+ warning(_("ignoring value '%s' for status.compareBranches, "
+ "only @{upstream} and @{push} are supported"),
+ name);
+ return NULL;
}
- base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
- full_base, 0);
- 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 if (!sti) {
+ if (resolved)
+ return xstrdup(resolved);
+ return NULL;
+}
+
+static void format_branch_comparison(struct strbuf *sb,
+ bool up_to_date,
+ int ours, int theirs,
+ const char *branch_name,
+ enum ahead_behind_flags abf,
+ unsigned flags)
+{
+ bool use_push_advice = (flags & ENABLE_ADVICE_PUSH);
+ bool use_pull_advice = (flags & ENABLE_ADVICE_PULL);
+ bool use_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE);
+
+ if (up_to_date) {
strbuf_addf(sb,
_("Your branch is up to date with '%s'.\n"),
- base);
+ branch_name);
} else if (abf == AHEAD_BEHIND_QUICK) {
strbuf_addf(sb,
_("Your branch and '%s' refer to different commits.\n"),
- base);
- if (advice_enabled(ADVICE_STATUS_HINTS))
+ branch_name);
+ if (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))
strbuf_addf(sb, _(" (use \"%s\" for details)\n"),
"git status --ahead-behind");
} else if (!theirs) {
@@ -2278,8 +2290,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
Q_("Your branch is ahead of '%s' by %d commit.\n",
"Your branch is ahead of '%s' by %d commits.\n",
ours),
- base, ours);
- if (advice_enabled(ADVICE_STATUS_HINTS))
+ branch_name, ours);
+ if (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))
strbuf_addstr(sb,
_(" (use \"git push\" to publish your local commits)\n"));
} else if (!ours) {
@@ -2289,8 +2301,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
"Your branch is behind '%s' by %d commits, "
"and can be fast-forwarded.\n",
theirs),
- base, theirs);
- if (advice_enabled(ADVICE_STATUS_HINTS))
+ branch_name, theirs);
+ if (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS))
strbuf_addstr(sb,
_(" (use \"git pull\" to update your local branch)\n"));
} else {
@@ -2302,14 +2314,106 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
"and have %d and %d different commits each, "
"respectively.\n",
ours + theirs),
- base, ours, theirs);
- if (show_divergence_advice &&
- advice_enabled(ADVICE_STATUS_HINTS))
+ branch_name, ours, theirs);
+ if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS))
strbuf_addstr(sb,
_(" (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
}
- free(base);
- return 1;
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+ enum ahead_behind_flags abf,
+ int show_divergence_advice)
+{
+ char *compare_branches = NULL;
+ struct string_list branches = STRING_LIST_INIT_DUP;
+ struct strset processed_refs = STRSET_INIT;
+ int reported = 0;
+ size_t i;
+ const char *upstream_ref;
+ const char *push_ref;
+
+ repo_config_get_string(the_repository, "status.comparebranches",
+ &compare_branches);
+
+ if (compare_branches) {
+ string_list_split(&branches, compare_branches, " ", -1);
+ string_list_remove_empty_items(&branches, 0);
+ } else {
+ string_list_append(&branches, "@{upstream}");
+ }
+
+ upstream_ref = branch_get_upstream(branch, NULL);
+ push_ref = branch_get_push(branch, NULL);
+
+ for (i = 0; i < branches.nr; i++) {
+ char *full_ref;
+ char *short_ref;
+ int ours, theirs, cmp;
+ int is_upstream, is_push;
+ unsigned flags = 0;
+
+ full_ref = resolve_compare_branch(branch,
+ branches.items[i].string);
+ if (!full_ref)
+ continue;
+
+ if (!strset_add(&processed_refs, full_ref)) {
+ free(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 (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);
+ }
+
+ string_list_clear(&branches, 0);
+ strset_clear(&processed_refs);
+ free(compare_branches);
+ return reported;
}
static int one_local_ref(const struct reference *ref, void *cb_data)
diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
index 0b719bbae6..0242b5bf7a 100755
--- a/t/t6040-tracking-info.sh
+++ b/t/t6040-tracking-info.sh
@@ -292,4 +292,358 @@ test_expect_success '--set-upstream-to @{-1}' '
test_cmp expect actual
'
+test_expect_success 'status tracking origin/main shows only main' '
+ git -C test checkout b4 &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch b4
+ Your branch is ahead of ${SQ}origin/main${SQ} by 2 commits.
+ (use "git push" to publish your local commits)
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '
+ git -C test checkout b4 &&
+ git -C test status --no-ahead-behind >actual &&
+ cat >expect <<-EOF &&
+ On branch b4
+ Your branch and ${SQ}origin/main${SQ} refer to different commits.
+ (use "git status --ahead-behind" for details)
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches deduplicates when upstream and push are the same' '
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout main &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch main
+ Your branch is up to date with ${SQ}origin/main${SQ}.
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches with only upstream shows only upstream' '
+ test_config -C test status.compareBranches "@{upstream}" &&
+ git -C test checkout main &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch main
+ Your branch is up to date with ${SQ}origin/main${SQ}.
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches with only push shows only push' '
+ test_config -C test push.default current &&
+ test_config -C test status.compareBranches "@{push}" &&
+ git -C test checkout -b feature2 origin/main &&
+ git -C test push origin HEAD &&
+ (cd test && advance work) &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature2
+ Your branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '
+ test_config -C test push.default current &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout -b feature3 origin/main &&
+ git -C test push origin HEAD &&
+ (cd test && advance work) &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature3
+ Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+
+ Your branch is ahead of ${SQ}origin/feature3${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'checkout with status.compareBranches shows both branches' '
+ test_config -C test push.default current &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout feature3 >actual &&
+ cat >expect <<-EOF &&
+ Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+
+ Your branch is ahead of ${SQ}origin/feature3${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'setup for ahead of tracked but diverged from main' '
+ (
+ cd test &&
+ git checkout -b feature4 origin/main &&
+ advance work1 &&
+ git checkout origin/main &&
+ advance work2 &&
+ git push origin HEAD:main &&
+ git checkout feature4 &&
+ advance work3
+ )
+'
+
+test_expect_success 'status.compareBranches shows diverged and ahead' '
+ test_config -C test push.default current &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout feature4 &&
+ git -C test branch --set-upstream-to origin/main &&
+ git -C test push origin HEAD &&
+ (cd test && advance work) &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature4
+ Your branch and ${SQ}origin/main${SQ} have diverged,
+ and have 3 and 1 different commits each, respectively.
+ (use "git pull" if you want to integrate the remote branch with yours)
+
+ Your branch is ahead of ${SQ}origin/feature4${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status --no-ahead-behind with status.compareBranches' '
+ test_config -C test push.default current &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout feature4 &&
+ git -C test status --no-ahead-behind >actual &&
+ cat >expect <<-EOF &&
+ On branch feature4
+ Your branch and ${SQ}origin/main${SQ} refer to different commits.
+
+ Your branch and ${SQ}origin/feature4${SQ} refer to different commits.
+ (use "git status --ahead-behind" for details)
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'setup upstream remote' '
+ (
+ cd test &&
+ git remote add upstream ../. &&
+ git fetch upstream
+ )
+'
+
+test_expect_success 'status.compareBranches with upstream and origin remotes' '
+ test_config -C test push.default current &&
+ test_config -C test remote.pushDefault origin &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout -b feature5 upstream/main &&
+ git -C test push origin &&
+ (cd test && advance work) &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature5
+ Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+ Your branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches supports ordered upstream/push entries' '
+ test_config -C test push.default current &&
+ test_config -C test remote.pushDefault origin &&
+ test_config -C test status.compareBranches "@{push} @{upstream}" &&
+ git -C test checkout -b feature6 upstream/main &&
+ git -C test push origin &&
+ (cd test && advance work) &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature6
+ 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}upstream/main${SQ} by 1 commit.
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches deduplicates repeated specifiers' '
+ test_config -C test push.default current &&
+ test_config -C test remote.pushDefault origin &&
+ test_config -C test status.compareBranches "@{push} @{upstream} @{push}" &&
+ git -C test checkout -b feature7 upstream/main &&
+ git -C test push origin &&
+ (cd test && advance work) &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature7
+ Your branch is ahead of ${SQ}origin/feature7${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+
+ Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches with diverged push branch' '
+ test_config -C test push.default current &&
+ test_config -C test remote.pushDefault origin &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout -b feature8 upstream/main &&
+ (cd test && advance work81) &&
+ git -C test push origin &&
+ git -C test reset --hard upstream/main &&
+ (cd test && advance work82) &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature8
+ Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+ Your branch and ${SQ}origin/feature8${SQ} have diverged,
+ and have 1 and 1 different commits each, respectively.
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches shows up to date branches' '
+ test_config -C test push.default current &&
+ test_config -C test remote.pushDefault origin &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout -b feature9 upstream/main &&
+ git -C test push origin &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature9
+ Your branch is up to date with ${SQ}upstream/main${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 --no-ahead-behind with status.compareBranches up to date' '
+ test_config -C test push.default current &&
+ test_config -C test remote.pushDefault origin &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout feature9 >actual &&
+ git -C test push origin &&
+ git -C test status --no-ahead-behind >actual &&
+ cat >expect <<-EOF &&
+ On branch feature9
+ Your branch is up to date with ${SQ}upstream/main${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 'checkout with status.compareBranches shows up to date' '
+ test_config -C test push.default current &&
+ test_config -C test remote.pushDefault origin &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout feature9 >actual &&
+ cat >expect <<-EOF &&
+ Your branch is up to date with ${SQ}upstream/main${SQ}.
+
+ Your branch is up to date with ${SQ}origin/feature9${SQ}.
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches with upstream behind and push up to date' '
+ test_config -C test push.default current &&
+ test_config -C test remote.pushDefault origin &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout -b ahead upstream/main &&
+ (cd test && advance work) &&
+ git -C test push upstream HEAD &&
+ git -C test checkout -b feature10 upstream/main &&
+ git -C test push origin &&
+ git -C test branch --set-upstream-to upstream/ahead &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature10
+ 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/feature10${SQ}.
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches with remapped push refspec' '
+ test_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout -b feature11 origin/main &&
+ git -C test push &&
+ (cd test && advance work) &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature11
+ Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+
+ Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'status.compareBranches with remapped push and upstream remote' '
+ test_config -C test remote.pushDefault origin &&
+ test_config -C test remote.origin.push refs/heads/feature12:refs/heads/remapped &&
+ test_config -C test status.compareBranches "@{upstream} @{push}" &&
+ git -C test checkout -b feature12 upstream/main &&
+ git -C test push origin &&
+ (cd test && advance work) &&
+ git -C test status >actual &&
+ cat >expect <<-EOF &&
+ On branch feature12
+ Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+ Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.
+ (use "git push" to publish your local commits)
+
+ nothing to commit, working tree clean
+ EOF
+ test_cmp expect actual
+'
+
test_done