git.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Harald Nordgren <haraldnordgren@gmail.com>,
	Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v7] status: additional comparison with goal branch
Date: Thu, 25 Dec 2025 09:45:47 +0000	[thread overview]
Message-ID: <pull.2138.v7.git.git.1766655947789.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2138.v6.git.git.1766619672.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

"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" (i.e. you have built new history,
while others are not touching it), "behind" (i.e. you haven't
added any work since you were in-sync, while others have added
their work on the branch), "diverged" (i.e. you have commits
that you haven't pushed out, while others have added commits).

When you fork a branch 'feature' from the 'main' branch of the
remote, but then create 'feature' branch at the remote and push
there, while you still occasionally pull from or rebase onto
their 'main', you'd also want to know how much you have diverged
from 'main', in addition to how your 'feature' and their
'feature' compares. Currently the comparison with 'main' is not
given, making it hard to know when to start thinking about
rebasing onto the upstream default branch.

Show two sets of comparison: one with the tracking branch (as
before), and another with the upstream's default branch (what
their HEAD points to, typically 'main' or 'master'). The latter
comparison appears on a separate line after the tracking branch
status, using the same format:
- "Ahead of 'origin/main' by N commits" when purely ahead
- "Behind 'origin/main' by N commits" when purely behind
- "Diverged from 'origin/main' by N commits" when diverged

Example output when tracking a feature branch:
    On branch feature
    Your branch is ahead of 'origin/feature' by 2 commits.
      (use "git push" to publish your local commits)

    Ahead of 'origin/main' by 5 commits.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
    status: show default branch comparison when tracking non-default branch
    
    cc: Chris Torek chris.torek@gmail.com cc: Yee Cheng Chin
    ychin.macvim@gmail.com cc: "brian m. carlson"
    sandals@crustytoothpaste.net

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v7
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v7
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v6:

 1:  a3800aed18 ! 1:  04e2fb76ee status: show comparison with upstream default branch
     @@ Metadata
      Author: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## Commit message ##
     -    status: show comparison with upstream default branch
     +    status: additional comparison with goal branch
      
          "git status" on a branch that follows a remote branch compares
          commits on the current branch and the remote-tracking branch it
     @@ Commit message
      
              Ahead of 'origin/main' by 5 commits.
      
     -    The upstream default branch is determined by checking symbolic refs:
     -    1. refs/remotes/upstream/HEAD (if upstream remote exists)
     -    2. refs/remotes/origin/HEAD (fallback)
     -
     -    This works with any default branch name (main, master, develop,
     -    etc.) as long as the symbolic ref is configured. The comparison
     -    is also shown when the branch is up-to-date with its tracking
     -    branch but differs from the upstream default branch.
     -
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## remote.c ##
     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_
       	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
       }
       
     -+static char *get_default_remote_ref(char **full_ref_out)
     ++static char *get_goal_branch_ref(char **full_ref_out)
      +{
     -+	int flag;
     ++	const char *config_value;
      +	const char *resolved;
     -+	static const char *remotes[] = { "upstream", "origin", NULL };
     -+	int i;
     -+
     -+	for (i = 0; remotes[i]; i++) {
     -+		struct strbuf head_ref = STRBUF_INIT;
     -+		strbuf_addf(&head_ref, "refs/remotes/%s/HEAD", remotes[i]);
     -+
     -+		resolved = refs_resolve_ref_unsafe(
     -+			get_main_ref_store(the_repository),
     -+			head_ref.buf,
     -+			RESOLVE_REF_READING,
     -+			NULL, &flag);
     -+
     -+		strbuf_release(&head_ref);
     -+
     -+		if (resolved && (flag & REF_ISSYMREF)) {
     -+			if (full_ref_out)
     -+				*full_ref_out = xstrdup(resolved);
     -+			return refs_shorten_unambiguous_ref(
     -+				get_main_ref_store(the_repository), resolved, 0);
     -+		}
     -+	}
     ++	int flag;
     ++	struct strbuf ref_buf = STRBUF_INIT;
     ++	char *slash_pos;
     ++	char *ret = NULL;
      +
     -+	return NULL;
     -+}
     ++	if (repo_config_get_value(the_repository, "status.goalBranch", &config_value))
     ++		return NULL;
      +
     -+static int is_default_remote_branch(const char *name)
     -+{
     -+	char *default_full = NULL;
     -+	char *default_short;
     -+	int result = 0;
     ++	if (!config_value || !*config_value)
     ++		return NULL;
      +
     -+	default_short = get_default_remote_ref(&default_full);
     -+	if (!default_short)
     -+		return 0;
     ++	slash_pos = strchr(config_value, '/');
     ++	if (!slash_pos || slash_pos == config_value || !slash_pos[1]) {
     ++		warning(_("invalid value for status.goalBranch: '%s' (expected format: remote/branch)"),
     ++			config_value);
     ++		return NULL;
     ++	}
      +
     -+	result = !strcmp(name, default_short);
     ++	strbuf_addf(&ref_buf, "refs/remotes/%.*s/%s",
     ++		    (int)(slash_pos - config_value), config_value,
     ++		    slash_pos + 1);
     ++
     ++	resolved = refs_resolve_ref_unsafe(
     ++		get_main_ref_store(the_repository),
     ++		ref_buf.buf,
     ++		RESOLVE_REF_READING,
     ++		NULL, &flag);
     ++
     ++	if (resolved) {
     ++		if (full_ref_out)
     ++			*full_ref_out = xstrdup(resolved);
     ++		ret = refs_shorten_unambiguous_ref(
     ++			get_main_ref_store(the_repository), resolved, 0);
     ++	}
      +
     -+	free(default_short);
     -+	free(default_full);
     -+	return result;
     ++	strbuf_release(&ref_buf);
     ++	return ret;
      +}
      +
     -+static void format_default_branch_comparison(struct strbuf *sb,
     ++static void format_goal_branch_comparison(struct strbuf *sb,
      +					     const char *branch_refname,
     ++					     const char *goal_full,
     ++					     const char *goal_short,
      +					     enum ahead_behind_flags abf)
      +{
     -+	int default_ours = 0, default_theirs = 0;
     -+	char *default_full = NULL;
     -+	char *default_short;
     -+
     -+	default_short = get_default_remote_ref(&default_full);
     -+	if (!default_short)
     -+		return;
     ++	int goal_ahead = 0, goal_behind = 0;
      +
     -+	if (stat_branch_pair(branch_refname, default_full,
     -+			     &default_ours, &default_theirs, abf) <= 0) {
     -+		free(default_short);
     -+		free(default_full);
     ++	if (stat_branch_pair(branch_refname, goal_full,
     ++			     &goal_ahead, &goal_behind, abf) <= 0)
      +		return;
     -+	}
      +
      +	strbuf_addstr(sb, "\n");
      +
     -+	if (default_ours > 0 && default_theirs == 0) {
     ++	if (goal_ahead > 0 && goal_behind == 0) {
      +		strbuf_addf(sb,
      +			Q_("Ahead of '%s' by %d commit.\n",
      +			   "Ahead of '%s' by %d commits.\n",
     -+			   default_ours),
     -+			default_short, default_ours);
     -+	} else if (default_theirs > 0 && default_ours == 0) {
     ++			   goal_ahead),
     ++			goal_short, goal_ahead);
     ++	} else if (goal_behind > 0 && goal_ahead == 0) {
      +		strbuf_addf(sb,
      +			Q_("Behind '%s' by %d commit.\n",
      +			   "Behind '%s' by %d commits.\n",
     -+			   default_theirs),
     -+			default_short, default_theirs);
     -+	} else if (default_ours > 0 && default_theirs > 0) {
     ++			   goal_behind),
     ++			goal_short, goal_behind);
     ++	} else if (goal_ahead > 0 && goal_behind > 0) {
      +		strbuf_addf(sb,
      +			Q_("Diverged from '%s' by %d commit.\n",
      +			   "Diverged from '%s' by %d commits.\n",
     -+			   default_ours + default_theirs),
     -+			default_short, default_ours + default_theirs);
     ++			   goal_ahead + goal_behind),
     ++			goal_short, goal_ahead + goal_behind);
      +	}
     -+
     -+	free(default_short);
     -+	free(default_full);
      +}
      +
       /*
        * Return true when there is anything to report, otherwise false.
        */
      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
     - 	const char *full_base;
     - 	char *base;
     - 	int upstream_is_gone = 0;
     -+	int show_default_branch_comparison;
     - 
     - 	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
     - 	if (sti < 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);
     -+
     -+	show_default_branch_comparison = !is_default_remote_branch(base);
      +
       	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_addf(sb,
     - 			_("Your branch is up to date with '%s'.\n"),
     - 			base);
     -+		if (show_default_branch_comparison)
     -+			format_default_branch_comparison(sb, branch->refname, abf);
     - 	} else if (abf == AHEAD_BEHIND_QUICK) {
     - 		strbuf_addf(sb,
     - 			    _("Your branch and '%s' refer to different commits.\n"),
     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
     - 		if (advice_enabled(ADVICE_STATUS_HINTS))
     - 			strbuf_addstr(sb,
     - 				_("  (use \"git push\" to publish your local commits)\n"));
     -+		if (show_default_branch_comparison)
     -+			format_default_branch_comparison(sb, branch->refname, abf);
     - 	} else if (!ours) {
     - 		strbuf_addf(sb,
     - 			Q_("Your branch is behind '%s' by %d commit, "
     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
     - 		if (advice_enabled(ADVICE_STATUS_HINTS))
     - 			strbuf_addstr(sb,
     - 				_("  (use \"git pull\" to update your local branch)\n"));
     -+		if (show_default_branch_comparison)
     -+			format_default_branch_comparison(sb, branch->refname, abf);
     - 	} else {
     - 		strbuf_addf(sb,
     - 			Q_("Your branch and '%s' have diverged,\n"
     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
     - 		    advice_enabled(ADVICE_STATUS_HINTS))
       			strbuf_addstr(sb,
       				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
     -+		if (show_default_branch_comparison)
     -+			format_default_branch_comparison(sb, branch->refname, abf);
       	}
     ++
     ++	if (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {
     ++		char *goal_full = NULL;
     ++		char *goal_short = get_goal_branch_ref(&goal_full);
     ++
     ++		if (goal_short && strcmp(base, goal_short))
     ++			format_goal_branch_comparison(sb, branch->refname, goal_full,
     ++						     goal_short, abf);
     ++
     ++		free(goal_short);
     ++		free(goal_full);
     ++	}
     ++
       	free(base);
       	return 1;
     + }
      
       ## t/t6040-tracking-info.sh ##
      @@ t/t6040-tracking-info.sh: test_expect_success setup '
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	(
      +		cd test &&
      +		git checkout work >/dev/null &&
     -+		git status --long -b | head -5
     ++		git config status.goalBranch origin/main &&
     ++		git status --long -b
      +	) >actual &&
      +	cat >expect <<-\EOF &&
      +On branch work
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +  (use "git push" to publish your local commits)
      +
      +Ahead of '\''origin/main'\'' by 3 commits.
     ++
     ++nothing to commit, working tree clean
      +EOF
      +	test_cmp expect actual
      +'
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	(
      +		cd test &&
      +		git checkout main >/dev/null &&
     -+		git checkout work 2>&1 | grep -E "(Switched|Your branch|Ahead of)" | head -3
     ++		git config status.goalBranch origin/main &&
     ++		git checkout work 2>&1
      +	) >actual &&
      +	cat >expect <<-\EOF &&
      +Switched to branch '\''work'\''
      +Your branch is ahead of '\''origin/feature'\'' by 2 commits.
     ++  (use "git push" to publish your local commits)
     ++
      +Ahead of '\''origin/main'\'' by 3 commits.
      +EOF
      +	test_cmp expect actual
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	(
      +		cd test &&
      +		git checkout work2 >/dev/null &&
     -+		git status --long -b | head -5
     ++		git config status.goalBranch origin/main &&
     ++		git status --long -b
      +	) >actual &&
      +	cat >expect <<-\EOF &&
      +On branch work2
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +  (use "git push" to publish your local commits)
      +
      +Diverged from '\''origin/main'\'' by 3 commits.
     ++
     ++nothing to commit, working tree clean
      +EOF
      +	test_cmp expect actual
      +'
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	(
      +		cd test &&
      +		git checkout work2b >/dev/null &&
     -+		git status --long -b | head -6
     ++		git config status.goalBranch origin/main &&
     ++		git status --long -b
      +	) >actual &&
      +	cat >expect <<-\EOF &&
      +On branch work2b
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +  (use "git pull" if you want to integrate the remote branch with yours)
      +
      +Behind '\''origin/main'\'' by 1 commit.
     ++
     ++nothing to commit, working tree clean
      +EOF
      +	test_cmp expect actual
      +'
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	(
      +		cd test &&
      +		git checkout work3 >/dev/null &&
     -+		git status --long -b | head -5
     ++		git config status.goalBranch origin/main &&
     ++		git status --long -b
      +	) >actual &&
      +	cat >expect <<-\EOF &&
      +On branch work3
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +  (use "git pull" to update your local branch)
      +
      +Ahead of '\''origin/main'\'' by 1 commit.
     ++
     ++nothing to commit, working tree clean
      +EOF
      +	test_cmp expect actual
      +'
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	(
      +		cd test &&
      +		git checkout work >/dev/null &&
     -+		git status --long -b | head -5
     ++		git config status.goalBranch upstream/main &&
     ++		git status --long -b
      +	) >actual &&
      +	cat >expect <<-\EOF &&
      +On branch work
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +  (use "git push" to publish your local commits)
      +
      +Diverged from '\''upstream/main'\'' by 5 commits.
     ++
     ++nothing to commit, working tree clean
      +EOF
      +	test_cmp expect actual
      +'
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	(
      +		cd test &&
      +		git checkout synced_feature >/dev/null &&
     -+		git status --long -b | head -4
     ++		git config status.goalBranch upstream/main &&
     ++		git status --long -b
      +	) >actual &&
      +	cat >expect <<-\EOF &&
      +On branch synced_feature
      +Your branch is up to date with '\''origin/feature'\''.
      +
      +Diverged from '\''upstream/main'\'' by 3 commits.
     ++
     ++nothing to commit, working tree clean
      +EOF
      +	test_cmp expect actual
      +'
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	(
      +		cd test &&
      +		git checkout synced_feature2 >/dev/null &&
     -+		git status --long -b | head -4
     ++		git config status.goalBranch origin/main &&
     ++		git status --long -b
      +	) >actual &&
      +	cat >expect <<-\EOF &&
      +On branch synced_feature2
      +Your branch is up to date with '\''origin/feature'\''.
      +
      +Diverged from '\''origin/main'\'' by 5 commits.
     ++
     ++nothing to commit, working tree clean
      +EOF
      +	test_cmp expect actual
      +'
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	(
      +		cd test &&
      +		git checkout synced_feature3 >/dev/null &&
     -+		git status --long -b | head -4
     ++		git config status.goalBranch origin/main &&
     ++		git status --long -b
     ++	) >actual &&
     ++	cat >expect <<-\EOF &&
     ++On branch synced_feature3
     ++Your branch is up to date with '\''origin/feature'\''.
     ++
     ++Diverged from '\''origin/main'\'' by 5 commits.
     ++
     ++nothing to commit, working tree clean
     ++EOF
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'status with status.goalBranch unset shows no default comparison' '
     ++	(
     ++		cd test &&
     ++		git checkout synced_feature3 >/dev/null &&
     ++		git config --unset status.goalBranch 2>/dev/null || true &&
     ++		git status --long -b
     ++	) >actual &&
     ++	cat >expect <<-\EOF &&
     ++On branch synced_feature3
     ++Your branch is up to date with '\''origin/feature'\''.
     ++
     ++nothing to commit, working tree clean
     ++EOF
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'status with status.goalBranch set uses configured branch' '
     ++	(
     ++		cd test &&
     ++		git checkout synced_feature3 >/dev/null &&
     ++		git config status.goalBranch origin/main &&
     ++		git status --long -b
      +	) >actual &&
      +	cat >expect <<-\EOF &&
      +On branch synced_feature3
      +Your branch is up to date with '\''origin/feature'\''.
      +
      +Diverged from '\''origin/main'\'' by 5 commits.
     ++
     ++nothing to commit, working tree clean
     ++EOF
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'status with status.goalBranch set to different remote/branch' '
     ++	(
     ++		cd test &&
     ++		git checkout work >/dev/null &&
     ++		git config status.goalBranch origin/feature &&
     ++		git status --long -b
     ++	) >actual &&
     ++	cat >expect <<-\EOF &&
     ++On branch work
     ++Your branch is ahead of '\''origin/feature'\'' 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 with status.goalBranch set to non-existent branch' '
     ++	(
     ++		cd test &&
     ++		git checkout synced_feature3 >/dev/null &&
     ++		git config status.goalBranch origin/nonexistent &&
     ++		git status --long -b
     ++	) >actual &&
     ++	cat >expect <<-\EOF &&
     ++On branch synced_feature3
     ++Your branch is up to date with '\''origin/feature'\''.
     ++
     ++nothing to commit, working tree clean
      +EOF
      +	test_cmp expect actual
      +'
 2:  417f2075fb < -:  ---------- Simplify default branch comparison logic
 3:  c9ec5d9610 < -:  ---------- Use repo.settings.statusGoalBranch config for status comparison
 4:  0e308141da < -:  ---------- Rename default_remote to goal_branch
 5:  441678939f < -:  ---------- Add warning for malformed statusGoalBranch config
 6:  242dbbae44 < -:  ---------- Change config key to status.compareBranch


 remote.c                 |  92 +++++++++++
 t/t6040-tracking-info.sh | 340 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 432 insertions(+)

diff --git a/remote.c b/remote.c
index 59b3715120..7e13c027b5 100644
--- a/remote.c
+++ b/remote.c
@@ -2237,6 +2237,84 @@ 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_goal_branch_ref(char **full_ref_out)
+{
+	const char *config_value;
+	const char *resolved;
+	int flag;
+	struct strbuf ref_buf = STRBUF_INIT;
+	char *slash_pos;
+	char *ret = NULL;
+
+	if (repo_config_get_value(the_repository, "status.goalBranch", &config_value))
+		return NULL;
+
+	if (!config_value || !*config_value)
+		return NULL;
+
+	slash_pos = strchr(config_value, '/');
+	if (!slash_pos || slash_pos == config_value || !slash_pos[1]) {
+		warning(_("invalid value for status.goalBranch: '%s' (expected format: remote/branch)"),
+			config_value);
+		return NULL;
+	}
+
+	strbuf_addf(&ref_buf, "refs/remotes/%.*s/%s",
+		    (int)(slash_pos - config_value), config_value,
+		    slash_pos + 1);
+
+	resolved = refs_resolve_ref_unsafe(
+		get_main_ref_store(the_repository),
+		ref_buf.buf,
+		RESOLVE_REF_READING,
+		NULL, &flag);
+
+	if (resolved) {
+		if (full_ref_out)
+			*full_ref_out = xstrdup(resolved);
+		ret = refs_shorten_unambiguous_ref(
+			get_main_ref_store(the_repository), resolved, 0);
+	}
+
+	strbuf_release(&ref_buf);
+	return ret;
+}
+
+static void format_goal_branch_comparison(struct strbuf *sb,
+					     const char *branch_refname,
+					     const char *goal_full,
+					     const char *goal_short,
+					     enum ahead_behind_flags abf)
+{
+	int goal_ahead = 0, goal_behind = 0;
+
+	if (stat_branch_pair(branch_refname, goal_full,
+			     &goal_ahead, &goal_behind, abf) <= 0)
+		return;
+
+	strbuf_addstr(sb, "\n");
+
+	if (goal_ahead > 0 && goal_behind == 0) {
+		strbuf_addf(sb,
+			Q_("Ahead of '%s' by %d commit.\n",
+			   "Ahead of '%s' by %d commits.\n",
+			   goal_ahead),
+			goal_short, goal_ahead);
+	} else if (goal_behind > 0 && goal_ahead == 0) {
+		strbuf_addf(sb,
+			Q_("Behind '%s' by %d commit.\n",
+			   "Behind '%s' by %d commits.\n",
+			   goal_behind),
+			goal_short, goal_behind);
+	} else if (goal_ahead > 0 && goal_behind > 0) {
+		strbuf_addf(sb,
+			Q_("Diverged from '%s' by %d commit.\n",
+			   "Diverged from '%s' by %d commits.\n",
+			   goal_ahead + goal_behind),
+			goal_short, goal_ahead + goal_behind);
+	}
+}
+
 /*
  * Return true when there is anything to report, otherwise false.
  */
@@ -2258,6 +2336,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 
 	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"),
@@ -2311,6 +2390,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+
+	if (!upstream_is_gone && sti >= 0 && abf != AHEAD_BEHIND_QUICK) {
+		char *goal_full = NULL;
+		char *goal_short = get_goal_branch_ref(&goal_full);
+
+		if (goal_short && strcmp(base, goal_short))
+			format_goal_branch_comparison(sb, branch->refname, goal_full,
+						     goal_short, abf);
+
+		free(goal_short);
+		free(goal_full);
+	}
+
 	free(base);
 	return 1;
 }
diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
index 0b719bbae6..fe34ddf0ab 100755
--- a/t/t6040-tracking-info.sh
+++ b/t/t6040-tracking-info.sh
@@ -21,6 +21,7 @@ test_expect_success setup '
 	git clone . test &&
 	(
 		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main &&
 		git checkout -b b1 origin &&
 		git reset --hard HEAD^ &&
 		advance d &&
@@ -292,4 +293,343 @@ test_expect_success '--set-upstream-to @{-1}' '
 	test_cmp expect actual
 '
 
+test_expect_success 'setup for ahead of non-main tracking branch' '
+	(
+		cd test &&
+		git checkout -b feature origin/main &&
+		advance feature1 &&
+		git push origin feature &&
+		git checkout -b work --track origin/feature &&
+		advance work1 &&
+		advance work2
+	)
+'
+
+test_expect_success 'status shows ahead of both tracked branch and origin/main' '
+	(
+		cd test &&
+		git checkout work >/dev/null &&
+		git config status.goalBranch origin/main &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch work
+Your branch is ahead of '\''origin/feature'\'' by 2 commits.
+  (use "git push" to publish your local commits)
+
+Ahead of '\''origin/main'\'' by 3 commits.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'checkout shows ahead of both tracked branch and origin/main' '
+	(
+		cd test &&
+		git checkout main >/dev/null &&
+		git config status.goalBranch origin/main &&
+		git checkout work 2>&1
+	) >actual &&
+	cat >expect <<-\EOF &&
+Switched to branch '\''work'\''
+Your branch is ahead of '\''origin/feature'\'' by 2 commits.
+  (use "git push" to publish your local commits)
+
+Ahead of '\''origin/main'\'' by 3 commits.
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'status tracking origin/main shows only main' '
+	(
+		cd test &&
+		git checkout b4 >/dev/null &&
+		git status --long -b
+	) >actual &&
+	test_grep "ahead of .origin/main. by 2 commits" actual &&
+	test_grep ! "Ahead of" actual
+'
+
+test_expect_success 'setup for ahead of tracked but diverged from main' '
+	(
+		cd test &&
+		git checkout origin/main &&
+		git checkout -b oldfeature &&
+		advance oldfeature1 &&
+		git push origin oldfeature &&
+		git checkout origin/main &&
+		advance main_newer &&
+		git push origin HEAD:main &&
+		git checkout -b work2 --track origin/oldfeature &&
+		advance work2_commit
+	)
+'
+
+test_expect_success 'status shows ahead of tracked and diverged from origin/main' '
+	(
+		cd test &&
+		git checkout work2 >/dev/null &&
+		git config status.goalBranch origin/main &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch work2
+Your branch is ahead of '\''origin/oldfeature'\'' by 1 commit.
+  (use "git push" to publish your local commits)
+
+Diverged from '\''origin/main'\'' by 3 commits.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'setup for diverged from tracked but behind main' '
+	(
+		cd test &&
+		git fetch origin &&
+		git checkout origin/main &&
+		git checkout -b work2b &&
+		git branch --set-upstream-to=origin/oldfeature &&
+		git checkout origin/main &&
+		advance main_extra &&
+		git push origin HEAD:main
+	)
+'
+
+test_expect_success 'status shows diverged from tracked and behind origin/main' '
+	(
+		cd test &&
+		git checkout work2b >/dev/null &&
+		git config status.goalBranch origin/main &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch work2b
+Your branch and '\''origin/oldfeature'\'' have diverged,
+and have 1 and 1 different commits each, respectively.
+  (use "git pull" if you want to integrate the remote branch with yours)
+
+Behind '\''origin/main'\'' by 1 commit.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'setup for behind tracked but ahead of main' '
+	(
+		cd test &&
+		git fetch origin &&
+		git checkout origin/main &&
+		git checkout -b feature3 &&
+		advance feature3_1 &&
+		advance feature3_2 &&
+		advance feature3_3 &&
+		git push origin feature3 &&
+		git checkout -b work3 --track origin/feature3 &&
+		git reset --hard HEAD~2
+	)
+'
+
+test_expect_success 'status shows behind tracked and ahead of origin/main' '
+	(
+		cd test &&
+		git checkout work3 >/dev/null &&
+		git config status.goalBranch origin/main &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch work3
+Your branch is behind '\''origin/feature3'\'' by 2 commits, and can be fast-forwarded.
+  (use "git pull" to update your local branch)
+
+Ahead of '\''origin/main'\'' by 1 commit.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'setup upstream remote preference' '
+	(
+		cd test &&
+		git remote add upstream ../. &&
+		git fetch upstream &&
+		git symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/main
+	)
+'
+
+test_expect_success 'status prefers upstream remote over origin for comparison' '
+	(
+		cd test &&
+		git checkout work >/dev/null &&
+		git config status.goalBranch upstream/main &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch work
+Your branch is ahead of '\''origin/feature'\'' by 2 commits.
+  (use "git push" to publish your local commits)
+
+Diverged from '\''upstream/main'\'' by 5 commits.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'setup for up to date with tracked but ahead of default' '
+	(
+		cd test &&
+		git checkout origin/feature &&
+		git checkout -b synced_feature --track origin/feature &&
+		git checkout origin/main &&
+		advance main_ahead &&
+		git push origin HEAD:main
+	)
+'
+
+test_expect_success 'status shows up to date with tracked but diverged from default' '
+	(
+		cd test &&
+		git checkout synced_feature >/dev/null &&
+		git config status.goalBranch upstream/main &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch synced_feature
+Your branch is up to date with '\''origin/feature'\''.
+
+Diverged from '\''upstream/main'\'' by 3 commits.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'setup for up to date with tracked but ahead of origin/main' '
+	(
+		cd test &&
+		git remote remove upstream &&
+		git checkout origin/feature &&
+		git checkout -b synced_feature2 --track origin/feature &&
+		git checkout origin/main &&
+		advance main_ahead2 &&
+		git push origin HEAD:main
+	)
+'
+
+test_expect_success 'status shows up to date with tracked but diverged from origin/main' '
+	(
+		cd test &&
+		git checkout synced_feature2 >/dev/null &&
+		git config status.goalBranch origin/main &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch synced_feature2
+Your branch is up to date with '\''origin/feature'\''.
+
+Diverged from '\''origin/main'\'' by 5 commits.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'setup for up to date with tracked but purely ahead of origin/main' '
+	(
+		cd test &&
+		git checkout origin/feature &&
+		git checkout -b synced_feature3 --track origin/feature
+	)
+'
+
+test_expect_success 'status shows up to date with tracked but shows default branch comparison' '
+	(
+		cd test &&
+		git checkout synced_feature3 >/dev/null &&
+		git config status.goalBranch origin/main &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch synced_feature3
+Your branch is up to date with '\''origin/feature'\''.
+
+Diverged from '\''origin/main'\'' by 5 commits.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'status with status.goalBranch unset shows no default comparison' '
+	(
+		cd test &&
+		git checkout synced_feature3 >/dev/null &&
+		git config --unset status.goalBranch 2>/dev/null || true &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch synced_feature3
+Your branch is up to date with '\''origin/feature'\''.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'status with status.goalBranch set uses configured branch' '
+	(
+		cd test &&
+		git checkout synced_feature3 >/dev/null &&
+		git config status.goalBranch origin/main &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch synced_feature3
+Your branch is up to date with '\''origin/feature'\''.
+
+Diverged from '\''origin/main'\'' by 5 commits.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'status with status.goalBranch set to different remote/branch' '
+	(
+		cd test &&
+		git checkout work >/dev/null &&
+		git config status.goalBranch origin/feature &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch work
+Your branch is ahead of '\''origin/feature'\'' 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 with status.goalBranch set to non-existent branch' '
+	(
+		cd test &&
+		git checkout synced_feature3 >/dev/null &&
+		git config status.goalBranch origin/nonexistent &&
+		git status --long -b
+	) >actual &&
+	cat >expect <<-\EOF &&
+On branch synced_feature3
+Your branch is up to date with '\''origin/feature'\''.
+
+nothing to commit, working tree clean
+EOF
+	test_cmp expect actual
+'
+
 test_done

base-commit: c4a0c8845e2426375ad257b6c221a3a7d92ecfda
-- 
gitgitgadget

  parent reply	other threads:[~2025-12-25  9:45 UTC|newest]

Thread overview: 66+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-12-23  0:53 [PATCH] status: show default branch comparison when tracking non-default branch Harald Nordgren via GitGitGadget
2025-12-23  5:32 ` Junio C Hamano
2025-12-23 10:24   ` Harald Nordgren
2025-12-23 11:36     ` Harald Nordgren
2025-12-23 12:23       ` Chris Torek
2025-12-23 14:18         ` Harald Nordgren
2025-12-23 14:22           ` Chris Torek
2025-12-23 13:32     ` Junio C Hamano
2025-12-23 14:09       ` Harald Nordgren
2025-12-23 22:54 ` [PATCH v2 0/2] " Harald Nordgren via GitGitGadget
2025-12-23 22:54   ` [PATCH v2 1/2] status: show comparison with upstream default branch Harald Nordgren via GitGitGadget
2025-12-24  1:30     ` brian m. carlson
2025-12-24  1:46       ` Junio C Hamano
2025-12-23 22:54   ` [PATCH v2 2/2] Simplify default branch comparison logic Harald Nordgren via GitGitGadget
2025-12-24  0:00   ` [PATCH] status: show default branch comparison when tracking non-default branch Harald Nordgren
2025-12-24  9:31   ` [PATCH v3 0/3] " Harald Nordgren via GitGitGadget
2025-12-24  9:31     ` [PATCH v3 1/3] status: show comparison with upstream default branch Harald Nordgren via GitGitGadget
2025-12-24  9:31     ` [PATCH v3 2/3] Simplify default branch comparison logic Harald Nordgren via GitGitGadget
2025-12-24  9:31     ` [PATCH v3 3/3] Use repo.settings.statusGoalBranch config for status comparison Harald Nordgren via GitGitGadget
2025-12-24 10:19     ` [PATCH v4 0/4] status: show default branch comparison when tracking non-default branch Harald Nordgren via GitGitGadget
2025-12-24 10:19       ` [PATCH v4 1/4] status: show comparison with upstream default branch Harald Nordgren via GitGitGadget
2025-12-24 10:19       ` [PATCH v4 2/4] Simplify default branch comparison logic Harald Nordgren via GitGitGadget
2025-12-24 10:19       ` [PATCH v4 3/4] Use repo.settings.statusGoalBranch config for status comparison Harald Nordgren via GitGitGadget
2025-12-24 10:19       ` [PATCH v4 4/4] Rename default_remote to goal_branch Harald Nordgren via GitGitGadget
2025-12-24 10:38       ` [PATCH v5 0/5] status: show default branch comparison when tracking non-default branch Harald Nordgren via GitGitGadget
2025-12-24 10:38         ` [PATCH v5 1/5] status: show comparison with upstream default branch Harald Nordgren via GitGitGadget
2025-12-24 10:38         ` [PATCH v5 2/5] Simplify default branch comparison logic Harald Nordgren via GitGitGadget
2025-12-24 10:38         ` [PATCH v5 3/5] Use repo.settings.statusGoalBranch config for status comparison Harald Nordgren via GitGitGadget
2025-12-24 10:38         ` [PATCH v5 4/5] Rename default_remote to goal_branch Harald Nordgren via GitGitGadget
2025-12-24 10:38         ` [PATCH v5 5/5] Add warning for malformed statusGoalBranch config Harald Nordgren via GitGitGadget
2025-12-24 23:41         ` [PATCH v6 0/6] status: show default branch comparison when tracking non-default branch Harald Nordgren via GitGitGadget
2025-12-24 23:41           ` [PATCH v6 1/6] status: show comparison with upstream default branch Harald Nordgren via GitGitGadget
2025-12-24 23:41           ` [PATCH v6 2/6] Simplify default branch comparison logic Harald Nordgren via GitGitGadget
2025-12-24 23:41           ` [PATCH v6 3/6] Use repo.settings.statusGoalBranch config for status comparison Harald Nordgren via GitGitGadget
2025-12-24 23:41           ` [PATCH v6 4/6] Rename default_remote to goal_branch Harald Nordgren via GitGitGadget
2025-12-24 23:41           ` [PATCH v6 5/6] Add warning for malformed statusGoalBranch config Harald Nordgren via GitGitGadget
2025-12-24 23:41           ` [PATCH v6 6/6] Change config key to status.compareBranch Harald Nordgren via GitGitGadget
2025-12-25  8:00           ` [PATCH v6 0/6] status: show default branch comparison when tracking non-default branch Junio C Hamano
2025-12-25  9:45             ` [PATCH] " Harald Nordgren
2025-12-26  1:59               ` Junio C Hamano
2025-12-26 10:58                 ` Harald Nordgren
2025-12-25  9:45           ` Harald Nordgren via GitGitGadget [this message]
2025-12-25 12:33             ` [PATCH v8] status: show comparison with configured goal branch Harald Nordgren via GitGitGadget
2025-12-28  9:16               ` Code review? Harald Nordgren
2025-12-28 19:37                 ` Junio C Hamano
2025-12-28 20:16                   ` Harald Nordgren
2025-12-28 23:09                     ` Ben Knoble
2025-12-29 12:17                       ` Triangular workflows Harald Nordgren
2025-12-30 16:08                   ` Code review? Harald Nordgren
2025-12-28 11:46               ` [PATCH v8] status: show comparison with configured goal branch Junio C Hamano
2025-12-28 15:46                 ` Code review? Harald Nordgren
2025-12-28 15:41               ` [PATCH v9 0/2] status: show comparison with configured goal branch Harald Nordgren via GitGitGadget
2025-12-28 15:41                 ` [PATCH v9 1/2] " Harald Nordgren via GitGitGadget
2025-12-28 15:41                 ` [PATCH v9 2/2] improve tests Harald Nordgren via GitGitGadget
2025-12-30 16:08                 ` [PATCH v10 0/3] status: show additional comparison with push branch when different from tracking branch Harald Nordgren via GitGitGadget
2025-12-30 16:08                   ` [PATCH v10 1/3] status: show comparison with configured goal branch Harald Nordgren via GitGitGadget
2025-12-30 16:08                   ` [PATCH v10 2/3] improve tests Harald Nordgren via GitGitGadget
2025-12-30 16:08                   ` [PATCH v10 3/3] use pushRemote and tracking branch Harald Nordgren via GitGitGadget
2025-12-23 23:11 ` [PATCH] status: show default branch comparison when tracking non-default branch Yee Cheng Chin
2025-12-23 23:59   ` Harald Nordgren
2025-12-24  0:55     ` Yee Cheng Chin
2025-12-24  0:38   ` Junio C Hamano
2025-12-24  0:49     ` Yee Cheng Chin
2025-12-24  1:44       ` Junio C Hamano
2025-12-24 10:24         ` Harald Nordgren
2025-12-24  1:12     ` Harald Nordgren

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=pull.2138.v7.git.git.1766655947789.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=haraldnordgren@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).