Git development
 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 1/6] status: count push divergence outside the upstream
Date: Tue, 06 Oct 2026 07:08:19 +0000	[thread overview]
Message-ID: <e1651568b1923ab075542e2c5027917bae76c619.1791270504.git.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2437.git.git.1791270504.gitgitgadget@gmail.com>

From: Harald Nordgren <haraldnordgren@gmail.com>

After rebasing onto a newer upstream, "git status" counts every commit
the upstream gained since the last push as a difference from the push
branch when status.compareBranches includes "@{push}". That can be many
commits, even when only a few of your own differ.

Keep the full counts, and when they include commits from the upstream,
add how many of them are not in it:

  Your branch and 'origin/topic' have diverged,
  and have 51 and 1 different commits each (1 and 1 not in 'upstream/main').

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 remote.c                 | 81 +++++++++++++++++++++++++++++++++-------
 t/t6040-tracking-info.sh | 32 ++++++++++++++++
 2 files changed, 100 insertions(+), 13 deletions(-)

diff --git a/remote.c b/remote.c
index fe62068463..cbe79275f7 100644
--- a/remote.c
+++ b/remote.c
@@ -2247,11 +2247,12 @@ int resolve_remote_symref(struct ref *ref, struct ref *list)
  */
 
 static int stat_branch_pair(const char *branch_name, const char *base,
+			     const char *exclude,
 			     int *num_ours, int *num_theirs,
 			     enum ahead_behind_flags abf)
 {
 	struct object_id oid;
-	struct commit *ours, *theirs;
+	struct commit *ours, *theirs, *excluded = NULL;
 	struct rev_info revs;
 	struct strvec argv = STRVEC_INIT;
 
@@ -2268,6 +2269,14 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 	if (!ours)
 		return -1;
 
+	if (exclude) {
+		if (refs_read_ref(get_main_ref_store(the_repository), exclude, &oid))
+			return -1;
+		excluded = lookup_commit_reference(the_repository, &oid);
+		if (!excluded)
+			return -1;
+	}
+
 	*num_theirs = *num_ours = 0;
 
 	/* are we the same? */
@@ -2284,6 +2293,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 	strvec_pushf(&argv, "%s...%s",
 		     oid_to_hex(&ours->object.oid),
 		     oid_to_hex(&theirs->object.oid));
+	if (excluded)
+		strvec_pushf(&argv, "^%s", oid_to_hex(&excluded->object.oid));
 	strvec_push(&argv, "--");
 
 	repo_init_revisions(the_repository, &revs, NULL);
@@ -2305,6 +2316,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 	/* clear object flags smudged by the above traversal */
 	clear_commit_marks(ours, ALL_REV_FLAGS);
 	clear_commit_marks(theirs, ALL_REV_FLAGS);
+	if (excluded)
+		clear_commit_marks(excluded, ALL_REV_FLAGS);
 
 	strvec_clear(&argv);
 	release_revisions(&revs);
@@ -2344,7 +2357,23 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	if (!base)
 		return -1;
 
-	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
+	return stat_branch_pair(branch->refname, base, NULL,
+				num_ours, num_theirs, abf);
+}
+
+/*
+ * Count the commits that differ between branch_name and base but are not
+ * in upstream. Return false when they cannot be counted or upstream
+ * accounts for none of the ours and theirs commits.
+ */
+static bool stat_outside_upstream(const char *branch_name, const char *base,
+				  const char *upstream, int ours, int theirs,
+				  int *ours_unmerged, int *theirs_unmerged)
+{
+	if (stat_branch_pair(branch_name, base, upstream, ours_unmerged,
+			     theirs_unmerged, AHEAD_BEHIND_FULL) < 0)
+		return false;
+	return *ours_unmerged != ours || *theirs_unmerged != theirs;
 }
 
 static char *resolve_compare_branch(struct branch *branch, const char *name)
@@ -2376,6 +2405,8 @@ static void format_branch_comparison(struct strbuf *sb,
 				     const char *branch_name,
 				     const char *push_remote_name,
 				     const char *push_branch_name,
+				     const char *upstream_name,
+				     int ours_unmerged, int theirs_unmerged,
 				     enum ahead_behind_flags abf,
 				     unsigned flags)
 {
@@ -2421,15 +2452,27 @@ static void format_branch_comparison(struct strbuf *sb,
 					_("  (use \"git pull\" to update your local branch)\n"));
 		}
 	} else {
-		strbuf_addf(sb,
-			Q_("Your branch and '%s' have diverged,\n"
-			       "and have %d and %d different commit each, "
-			       "respectively.\n",
-			   "Your branch and '%s' have diverged,\n"
-			       "and have %d and %d different commits each, "
-			       "respectively.\n",
-			   ours + theirs),
-			branch_name, ours, theirs);
+		if (upstream_name)
+			strbuf_addf(sb,
+				Q_("Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commit each "
+				       "(%d and %d not in '%s').\n",
+				   "Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commits each "
+				       "(%d and %d not in '%s').\n",
+				   ours + theirs),
+				branch_name, ours, theirs,
+				ours_unmerged, theirs_unmerged, upstream_name);
+		else
+			strbuf_addf(sb,
+				Q_("Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commit each, "
+				       "respectively.\n",
+				   "Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commits each, "
+				       "respectively.\n",
+				   ours + theirs),
+				branch_name, ours, theirs);
 		if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS)) {
 			if (push_remote_name && push_branch_name)
 				strbuf_addf(sb,
@@ -2473,7 +2516,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	for (i = 0; i < branches.nr; i++) {
 		char *full_ref;
 		char *short_ref;
+		char *upstream_name = NULL;
 		int ours, theirs, cmp;
+		int ours_unmerged = 0, theirs_unmerged = 0;
 		int is_upstream, is_push;
 		unsigned flags = 0;
 		const char *push_remote_name = NULL;
@@ -2498,9 +2543,17 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		if (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))
 			is_push = 1;
 
-		cmp = stat_branch_pair(branch->refname, full_ref,
+		cmp = stat_branch_pair(branch->refname, full_ref, NULL,
 				       &ours, &theirs, abf);
 
+		if (cmp > 0 && ours && theirs && upstream_ref && !is_upstream &&
+		    stat_outside_upstream(branch->refname, full_ref,
+					  upstream_ref, ours, theirs,
+					  &ours_unmerged, &theirs_unmerged))
+			upstream_name = refs_shorten_unambiguous_ref(
+				get_main_ref_store(the_repository),
+				upstream_ref, 0);
+
 		if (cmp < 0) {
 			if (is_upstream) {
 				strbuf_addf(sb,
@@ -2542,11 +2595,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		}
 		format_branch_comparison(sb, !cmp, ours, theirs, short_ref,
 					 push_remote_name, push_branch_name,
-					 abf, flags);
+					 upstream_name, ours_unmerged,
+					 theirs_unmerged, abf, flags);
 		reported = 1;
 
 		free(full_ref);
 		free(short_ref);
+		free(upstream_name);
 	}
 
 	string_list_clear(&branches, 0);
diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
index e95d420972..f8df16a5de 100755
--- a/t/t6040-tracking-info.sh
+++ b/t/t6040-tracking-info.sh
@@ -746,4 +746,36 @@ test_expect_success 'status.compareBranches suppresses advice when push tracking
 	test_cmp expect actual
 '
 
+test_expect_success 'status.compareBranches counts push divergence outside upstream' '
+	test_config -C test push.default current &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	(
+		cd test &&
+		git checkout -b feature18 origin/main &&
+		advance work18 &&
+		git push
+	) &&
+	git checkout main &&
+	advance main18a &&
+	advance main18b &&
+	git checkout - &&
+	(
+		cd test &&
+		echo amended >work18 &&
+		git commit -a --amend --no-edit &&
+		git pull --rebase &&
+		git status >../actual
+	) &&
+	cat >expect <<-EOF &&
+	On branch feature18
+	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+
+	Your branch and ${SQ}origin/feature18${SQ} have diverged,
+	and have 3 and 1 different commits each (1 and 1 not in ${SQ}origin/main${SQ}).
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
gitgitgadget


  reply	other threads:[~2026-10-06  7:08 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-06  7:08 [PATCH 0/6] status, push: handle a push branch rebased onto a newer upstream Harald Nordgren via GitGitGadget
2026-10-06  7:08 ` Harald Nordgren via GitGitGadget [this message]
2026-10-06  7:08 ` [PATCH 2/6] status: say when the push branch was rebased cleanly Harald Nordgren via GitGitGadget
2026-10-06  7:08 ` [PATCH 3/6] status: suggest a force push after a clean rebase Harald Nordgren via GitGitGadget
2026-10-06  7:08 ` [PATCH 4/6] push: name the branch to pull from when it is not the upstream Harald Nordgren via GitGitGadget
2026-10-06  7:08 ` [PATCH 5/6] push: offer a force push after rewriting pushed commits Harald Nordgren via GitGitGadget
2026-10-06  7:08 ` [PATCH 6/6] push: suggest a force push after a clean rebase Harald Nordgren via GitGitGadget

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=e1651568b1923ab075542e2c5027917bae76c619.1791270504.git.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