Volume XXII, number 279Tuesday, October 6, 2026Latest message 21 minutes ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patch, 6 partsstatus, push: handle a push branch rebased onto a newer upstream

7 messages between Oct 6, 2026 and Oct 6, 2026, from Harald Nordgren via GitGitGadget.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Harald Nordgren via GitGitGadgetOct 6, 2026, 07:08 UTC on lore

git status now tells when the push branch only differs because your work was rebased onto a newer upstream.

With status.compareBranches including @{push}, when your branch and the push branch have diverged:

 * commits from @{upstream} are counted apart: (1 and 1 not in
   'upstream/main')
 * if the rest carry the same changes on both sides: (rebased cleanly on
   'upstream/main'), with a hint to git push --force-with-lease
 * if nothing came from @{upstream}, the message is unchanged
When git push is rejected because the branch it updates has diverged:
 * if someone else updated your upstream branch, the hint still says git
   pull
 * if you amended or rebased commits you already pushed, the hint offers git
   pull and git push --force-with-lease
 * if they were rebased cleanly, the hint offers git push --force-with-lease
Harald Nordgren (6):
  status: count push divergence outside the upstream
  status: say when the push branch was rebased cleanly
  status: suggest a force push after a clean rebase
  push: name the branch to pull from when it is not the upstream
  push: offer a force push after rewriting pushed commits
  push: suggest a force push after a clean rebase
 builtin/push.c           |  62 ++++++++++++++-
 remote.c                 | 132 +++++++++++++++++++++++++++----
 remote.h                 |   6 ++
 t/t6040-tracking-info.sh | 163 +++++++++++++++++++++++++++++++++++++++
 transport.c              |  42 +++++++++-
 transport.h              |  13 ++--
 6 files changed, 393 insertions(+), 25 deletions(-)
base-commit: 8103b446517e0c44e67561b9d0ccce56efa60a71
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2437%2FHaraldNordgren%2Fstatus-push-not-in-upstream-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2437/HaraldNordgren/status-push-not-in-upstream-v1
Pull-Request: https://github.com/git/git/pull/2437
-- 
gitgitgadget
Harald Nordgren via GitGitGadgetOct 6, 2026, 07:08 UTC in reply to Harald Nordgren via GitGitGadget on lore

[PATCH 1/6] status: count push divergence outside the upstream

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(-)
Show changes to 2 files +100 −13

remote.c, t/t6040-tracking-info.sh

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
Harald Nordgren via GitGitGadgetOct 6, 2026, 07:08 UTC in reply to Harald Nordgren via GitGitGadget on lore

[PATCH 2/6] status: say when the push branch was rebased cleanly

From: Harald Nordgren <haraldnordgren@gmail.com>

After rebasing pushed work onto a newer upstream without changing it, "git status" reports the commits outside the upstream on both sides, as if the branch and its push branch held different work.

When those commits carry the same changes on both sides, say that the branch was rebased cleanly:

  Your branch and 'origin/topic' have diverged,
  and have 51 and 1 different commits each (rebased cleanly on 'upstream/main').
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 remote.c                 | 49 ++++++++++++++++++++++++++++++++--------
 t/t6040-tracking-info.sh | 30 ++++++++++++++++++++++++
 2 files changed, 69 insertions(+), 10 deletions(-)
Show changes to 2 files +69 −10

remote.c, t/t6040-tracking-info.sh

diff --git a/remote.c b/remote.c
index cbe79275f7..89d142cc7e 100644
--- a/remote.c
+++ b/remote.c
@@ -2248,7 +2248,7 @@ 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,
+			     int *num_ours, int *num_theirs, int *num_same,
 			     enum ahead_behind_flags abf)
 {
 	struct object_id oid;
@@ -2278,6 +2278,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 	}
 
 	*num_theirs = *num_ours = 0;
+	if (num_same)
+		*num_same = 0;
 
 	/* are we the same? */
 	if (theirs == ours)
@@ -2290,6 +2292,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 	/* Run "rev-list --left-right ours...theirs" internally... */
 	strvec_push(&argv, ""); /* ignored */
 	strvec_push(&argv, "--left-right");
+	if (num_same)
+		strvec_push(&argv, "--cherry-mark");
 	strvec_pushf(&argv, "%s...%s",
 		     oid_to_hex(&ours->object.oid),
 		     oid_to_hex(&theirs->object.oid));
@@ -2311,6 +2315,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 			(*num_ours)++;
 		else
 			(*num_theirs)++;
+		if (num_same && (c->object.flags & PATCHSAME))
+			(*num_same)++;
 	}
 
 	/* clear object flags smudged by the above traversal */
@@ -2358,22 +2364,31 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 		return -1;
 
 	return stat_branch_pair(branch->refname, base, NULL,
-				num_ours, num_theirs, abf);
+				num_ours, num_theirs, NULL, 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.
+ * accounts for none of the ours and theirs commits. Otherwise set
+ * *same_changes when the remaining commits carry the same changes on
+ * both sides.
  */
 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)
+				  int *ours_unmerged, int *theirs_unmerged,
+				  bool *same_changes)
 {
+	int same;
+
 	if (stat_branch_pair(branch_name, base, upstream, ours_unmerged,
-			     theirs_unmerged, AHEAD_BEHIND_FULL) < 0)
+			     theirs_unmerged, &same, AHEAD_BEHIND_FULL) < 0)
+		return false;
+	if (*ours_unmerged == ours && *theirs_unmerged == theirs)
 		return false;
-	return *ours_unmerged != ours || *theirs_unmerged != theirs;
+	*same_changes = *ours_unmerged &&
+		same == *ours_unmerged + *theirs_unmerged;
+	return true;
 }
 
 static char *resolve_compare_branch(struct branch *branch, const char *name)
@@ -2407,6 +2422,7 @@ static void format_branch_comparison(struct strbuf *sb,
 				     const char *push_branch_name,
 				     const char *upstream_name,
 				     int ours_unmerged, int theirs_unmerged,
+				     bool same_changes,
 				     enum ahead_behind_flags abf,
 				     unsigned flags)
 {
@@ -2452,7 +2468,17 @@ static void format_branch_comparison(struct strbuf *sb,
 					_("  (use \"git pull\" to update your local branch)\n"));
 		}
 	} else {
-		if (upstream_name)
+		if (same_changes)
+			strbuf_addf(sb,
+				Q_("Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commit each "
+				       "(rebased cleanly on '%s').\n",
+				   "Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commits each "
+				       "(rebased cleanly on '%s').\n",
+				   ours + theirs),
+				branch_name, ours, theirs, upstream_name);
+		else if (upstream_name)
 			strbuf_addf(sb,
 				Q_("Your branch and '%s' have diverged,\n"
 				       "and have %d and %d different commit each "
@@ -2519,6 +2545,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		char *upstream_name = NULL;
 		int ours, theirs, cmp;
 		int ours_unmerged = 0, theirs_unmerged = 0;
+		bool same_changes = false;
 		int is_upstream, is_push;
 		unsigned flags = 0;
 		const char *push_remote_name = NULL;
@@ -2544,12 +2571,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			is_push = 1;
 
 		cmp = stat_branch_pair(branch->refname, full_ref, NULL,
-				       &ours, &theirs, abf);
+				       &ours, &theirs, NULL, 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))
+					  &ours_unmerged, &theirs_unmerged,
+					  &same_changes))
 			upstream_name = refs_shorten_unambiguous_ref(
 				get_main_ref_store(the_repository),
 				upstream_ref, 0);
@@ -2596,7 +2624,8 @@ 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,
 					 upstream_name, ours_unmerged,
-					 theirs_unmerged, abf, flags);
+					 theirs_unmerged, same_changes,
+					 abf, flags);
 		reported = 1;
 
 		free(full_ref);
diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
index f8df16a5de..2ebc3573da 100755
--- a/t/t6040-tracking-info.sh
+++ b/t/t6040-tracking-info.sh
@@ -778,4 +778,34 @@ test_expect_success 'status.compareBranches counts push divergence outside upstr
 	test_cmp expect actual
 '
 
+test_expect_success 'status.compareBranches after a clean rebase of the push branch' '
+	test_config -C test push.default current &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	(
+		cd test &&
+		git checkout -b feature19 origin/main &&
+		advance work19 &&
+		git push
+	) &&
+	git checkout main &&
+	advance main19a &&
+	advance main19b &&
+	git checkout - &&
+	(
+		cd test &&
+		git pull --rebase &&
+		git status >../actual
+	) &&
+	cat >expect <<-EOF &&
+	On branch feature19
+	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+
+	Your branch and ${SQ}origin/feature19${SQ} have diverged,
+	and have 3 and 1 different commits each (rebased cleanly on ${SQ}origin/main${SQ}).
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
gitgitgadget
Harald Nordgren via GitGitGadgetOct 6, 2026, 07:08 UTC in reply to Harald Nordgren via GitGitGadget on lore

[PATCH 3/6] status: suggest a force push after a clean rebase

From: Harald Nordgren <haraldnordgren@gmail.com>

After rebasing pushed work onto a newer upstream without changing it, "git status" says the branch was rebased cleanly but gives no hint on how to publish it. A plain "git push" is rejected, so suggest a force push:

  Your branch and 'origin/topic' have diverged,
  and have 51 and 1 different commits each (rebased cleanly on 'upstream/main').
    (use "git push --force-with-lease" to publish your local commits)

The lease still stops the push if someone else updated the push branch since it was last fetched.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 remote.c                 | 7 +++++--
 t/t6040-tracking-info.sh | 1 +
 2 files changed, 6 insertions(+), 2 deletions(-)
Show changes to 2 files +6 −2

remote.c, t/t6040-tracking-info.sh

diff --git a/remote.c b/remote.c
index 89d142cc7e..af8d026073 100644
--- a/remote.c
+++ b/remote.c
@@ -2468,7 +2468,7 @@ static void format_branch_comparison(struct strbuf *sb,
 					_("  (use \"git pull\" to update your local branch)\n"));
 		}
 	} else {
-		if (same_changes)
+		if (same_changes) {
 			strbuf_addf(sb,
 				Q_("Your branch and '%s' have diverged,\n"
 				       "and have %d and %d different commit each "
@@ -2478,7 +2478,10 @@ static void format_branch_comparison(struct strbuf *sb,
 				       "(rebased cleanly on '%s').\n",
 				   ours + theirs),
 				branch_name, ours, theirs, upstream_name);
-		else if (upstream_name)
+			if (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))
+				strbuf_addstr(sb,
+					_("  (use \"git push --force-with-lease\" to publish your local commits)\n"));
+		} else if (upstream_name)
 			strbuf_addf(sb,
 				Q_("Your branch and '%s' have diverged,\n"
 				       "and have %d and %d different commit each "
diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
index 2ebc3573da..4074c6663a 100755
--- a/t/t6040-tracking-info.sh
+++ b/t/t6040-tracking-info.sh
@@ -802,6 +802,7 @@ test_expect_success 'status.compareBranches after a clean rebase of the push bra
 
 	Your branch and ${SQ}origin/feature19${SQ} have diverged,
 	and have 3 and 1 different commits each (rebased cleanly on ${SQ}origin/main${SQ}).
+	  (use "git push --force-with-lease" to publish your local commits)
 
 	nothing to commit, working tree clean
 	EOF
-- 
gitgitgadget
Harald Nordgren via GitGitGadgetOct 6, 2026, 07:08 UTC in reply to Harald Nordgren via GitGitGadget on lore

[PATCH 4/6] push: name the branch to pull from when it is not the upstream

From: Harald Nordgren <haraldnordgren@gmail.com>

When someone else pushed to the branch you push to, such as a topic branch on your fork, "git push" is rejected with a hint to run "git pull" first. If that branch is not your upstream, the pull merges the upstream and leaves the push branch diverged, so the push is rejected again.

Name the branch that diverged and suggest pulling from it:
  hint: Updates were rejected because 'origin/topic' has diverged
  hint: from your current branch. Use 'git pull origin topic'
  hint: to integrate the remote changes.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 builtin/push.c           | 37 +++++++++++++++++++++++---
 t/t6040-tracking-info.sh | 57 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 91 insertions(+), 3 deletions(-)
Show changes to 2 files +91 −3

builtin/push.c, t/t6040-tracking-info.sh

diff --git a/builtin/push.c b/builtin/push.c
index 2377b5af55..82435beab5 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -12,6 +12,7 @@
 #include "environment.h"
 #include "gettext.h"
 #include "hex.h"
+#include "refs.h"
 #include "refspec.h"
 #include "run-command.h"
 #include "remote.h"
@@ -295,6 +296,11 @@ static const char message_advice_pull_before_push[] =
 	   "use 'git pull' before pushing again.\n"
 	   "See the 'Note about fast-forwards' in 'git push --help' for details.");
 
+static const char message_advice_pull_from_branch_before_push[] =
+	N_("Updates were rejected because '%s' has diverged\n"
+	   "from your current branch. Use 'git pull %s %s'\n"
+	   "to integrate the remote changes.");
+
 static const char message_advice_checkout_pull_push[] =
 	N_("Updates were rejected because a pushed branch tip is behind its remote\n"
 	   "counterpart. If you want to integrate the remote changes, use 'git pull'\n"
@@ -322,11 +328,35 @@ static const char message_advice_ref_needs_update[] =
 	   "remote changes, use 'git pull' before pushing again.\n"
 	   "See the 'Note about fast-forwards' in 'git push --help' for details.");
 
-static void advise_pull_before_push(void)
+static void advise_pull_before_push(struct remote *push_remote)
 {
+	struct branch *branch = branch_get(NULL);
+	struct remote *remote = NULL;
+	const char *upstream = NULL;
+	char *tracking = NULL;
+	char *tracking_name = NULL;
+
 	if (!advice_enabled(ADVICE_PUSH_NON_FF_CURRENT) || !advice_enabled(ADVICE_PUSH_UPDATE_REJECTED))
 		return;
-	advise(_(message_advice_pull_before_push));
+
+	if (branch) {
+		remote = repo_remote_for_push_tracking(the_repository,
+						       push_remote);
+		tracking = apply_refspecs(&remote->fetch, branch->refname);
+		upstream = branch_get_upstream(branch, NULL);
+	}
+	if (tracking)
+		tracking_name = refs_shorten_unambiguous_ref(
+			get_main_ref_store(the_repository), tracking, 0);
+
+	if (tracking && (!upstream || strcmp(tracking, upstream)))
+		advise(_(message_advice_pull_from_branch_before_push),
+		       tracking_name, remote->name, branch->name);
+	else
+		advise(_(message_advice_pull_before_push));
+
+	free(tracking_name);
+	free(tracking);
 }
 
 static void advise_checkout_pull_push(void)
@@ -370,6 +400,7 @@ static int push_with_options(struct transport *transport, struct refspec *rs,
 	int err;
 	unsigned int reject_reasons;
 	char *anon_url = transport_anonymize_url(transport->url);
+	struct remote *remote = transport->remote;
 
 	transport_set_verbosity(transport, verbosity, progress);
 	transport->family = family;
@@ -404,7 +435,7 @@ static int push_with_options(struct transport *transport, struct refspec *rs,
 		return 0;
 
 	if (reject_reasons & REJECT_NON_FF_HEAD) {
-		advise_pull_before_push();
+		advise_pull_before_push(remote);
 	} else if (reject_reasons & REJECT_NON_FF_OTHER) {
 		advise_checkout_pull_push();
 	} else if (reject_reasons & REJECT_ALREADY_EXISTS) {
diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
index 4074c6663a..9eb810e158 100755
--- a/t/t6040-tracking-info.sh
+++ b/t/t6040-tracking-info.sh
@@ -809,4 +809,61 @@ test_expect_success 'status.compareBranches after a clean rebase of the push bra
 	test_cmp expect actual
 '
 
+test_expect_success 'push to a push branch someone else updated suggests pulling from it' '
+	(
+		cd test &&
+		git checkout -b feature20 origin/main &&
+		advance work20 &&
+		git push origin feature20
+	) &&
+	git checkout feature20 &&
+	advance other20 &&
+	git checkout - &&
+	(
+		cd test &&
+		advance mine20 &&
+		git fetch &&
+		test_must_fail git push origin feature20 2>../actual
+	) &&
+	url=$(git -C test config remote.origin.url) &&
+	cat >expect <<-EOF &&
+	To $url
+	 ! [rejected]        feature20 -> feature20 (non-fast-forward)
+	error: failed to push some refs to ${SQ}$url${SQ}
+	hint: Updates were rejected because ${SQ}origin/feature20${SQ} has diverged
+	hint: from your current branch. Use ${SQ}git pull origin feature20${SQ}
+	hint: to integrate the remote changes.
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'push to the upstream branch' '
+	(
+		cd test &&
+		git checkout -b feature21 origin/main &&
+		advance work21 &&
+		git push -u origin feature21
+	) &&
+	git checkout feature21 &&
+	advance other21 &&
+	git checkout - &&
+	(
+		cd test &&
+		advance mine21 &&
+		git fetch &&
+		test_must_fail git push 2>../actual
+	) &&
+	url=$(git -C test config remote.origin.url) &&
+	cat >expect <<-EOF &&
+	To $url
+	 ! [rejected]        feature21 -> feature21 (non-fast-forward)
+	error: failed to push some refs to ${SQ}$url${SQ}
+	hint: Updates were rejected because the tip of your current branch is behind
+	hint: its remote counterpart. If you want to integrate the remote changes,
+	hint: use ${SQ}git pull${SQ} before pushing again.
+	hint: See the ${SQ}Note about fast-forwards${SQ} in ${SQ}git push --help${SQ} for details.
+	EOF
+	test_cmp expect actual
+'
+
 test_done
-- 
gitgitgadget
Harald Nordgren via GitGitGadgetOct 6, 2026, 07:08 UTC in reply to Harald Nordgren via GitGitGadget on lore

[PATCH 5/6] push: offer a force push after rewriting pushed commits

From: Harald Nordgren <haraldnordgren@gmail.com>

After amending or rebasing commits you already pushed, "git push" is rejected with a hint to run "git pull" first. Pulling merges the old copies of the same work back in.

When the rejected remote tip is a commit your branch pointed to before, according to its reflog, the remote carries no work you have not seen. Name the branch and offer a force push next to the pull:

  hint: Updates were rejected because 'origin/topic' has diverged
  hint: from your current branch. Use 'git pull origin topic'
  hint: to integrate the remote changes, or replace them with
  hint: 'git push --force-with-lease origin topic'.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 builtin/push.c           | 17 +++++++++++++---
 t/t6040-tracking-info.sh | 18 +++++++++++++++++
 transport.c              | 42 ++++++++++++++++++++++++++++++++++++++--
 transport.h              | 13 +++++++------
 4 files changed, 79 insertions(+), 11 deletions(-)
Show changes to 4 files +79 −11

builtin/push.c, t/t6040-tracking-info.sh, transport.c, transport.h

diff --git a/builtin/push.c b/builtin/push.c
index 82435beab5..d918723d43 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -301,6 +301,12 @@ static const char message_advice_pull_from_branch_before_push[] =
 	   "from your current branch. Use 'git pull %s %s'\n"
 	   "to integrate the remote changes.");
 
+static const char message_advice_pull_or_force_before_push[] =
+	N_("Updates were rejected because '%s' has diverged\n"
+	   "from your current branch. Use 'git pull %s %s'\n"
+	   "to integrate the remote changes, or replace them with\n"
+	   "'git push --force-with-lease %s %s'.");
+
 static const char message_advice_checkout_pull_push[] =
 	N_("Updates were rejected because a pushed branch tip is behind its remote\n"
 	   "counterpart. If you want to integrate the remote changes, use 'git pull'\n"
@@ -328,7 +334,8 @@ static const char message_advice_ref_needs_update[] =
 	   "remote changes, use 'git pull' before pushing again.\n"
 	   "See the 'Note about fast-forwards' in 'git push --help' for details.");
 
-static void advise_pull_before_push(struct remote *push_remote)
+static void advise_pull_before_push(struct remote *push_remote,
+				    unsigned int reject_reasons)
 {
 	struct branch *branch = branch_get(NULL);
 	struct remote *remote = NULL;
@@ -349,7 +356,11 @@ static void advise_pull_before_push(struct remote *push_remote)
 		tracking_name = refs_shorten_unambiguous_ref(
 			get_main_ref_store(the_repository), tracking, 0);
 
-	if (tracking && (!upstream || strcmp(tracking, upstream)))
+	if (tracking && (reject_reasons & REJECT_NON_FF_HEAD_REWRITE))
+		advise(_(message_advice_pull_or_force_before_push),
+		       tracking_name, remote->name, branch->name,
+		       remote->name, branch->name);
+	else if (tracking && (!upstream || strcmp(tracking, upstream)))
 		advise(_(message_advice_pull_from_branch_before_push),
 		       tracking_name, remote->name, branch->name);
 	else
@@ -435,7 +446,7 @@ static int push_with_options(struct transport *transport, struct refspec *rs,
 		return 0;
 
 	if (reject_reasons & REJECT_NON_FF_HEAD) {
-		advise_pull_before_push(remote);
+		advise_pull_before_push(remote, reject_reasons);
 	} else if (reject_reasons & REJECT_NON_FF_OTHER) {
 		advise_checkout_pull_push();
 	} else if (reject_reasons & REJECT_ALREADY_EXISTS) {
diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
index 9eb810e158..b53034ba36 100755
--- a/t/t6040-tracking-info.sh
+++ b/t/t6040-tracking-info.sh
@@ -863,6 +863,24 @@ test_expect_success 'push to the upstream branch' '
 	hint: use ${SQ}git pull${SQ} before pushing again.
 	hint: See the ${SQ}Note about fast-forwards${SQ} in ${SQ}git push --help${SQ} for details.
 	EOF
+	test_cmp expect actual &&
+	(
+		cd test &&
+		git pull --rebase &&
+		git push &&
+		echo amended >mine21 &&
+		git commit -a --amend --no-edit &&
+		test_must_fail git push 2>../actual
+	) &&
+	cat >expect <<-EOF &&
+	To $url
+	 ! [rejected]        feature21 -> feature21 (non-fast-forward)
+	error: failed to push some refs to ${SQ}$url${SQ}
+	hint: Updates were rejected because ${SQ}origin/feature21${SQ} has diverged
+	hint: from your current branch. Use ${SQ}git pull origin feature21${SQ}
+	hint: to integrate the remote changes, or replace them with
+	hint: ${SQ}git push --force-with-lease origin feature21${SQ}.
+	EOF
 	test_cmp expect actual
 '
 
diff --git a/transport.c b/transport.c
index 25e2c14a7b..630dd699db 100644
--- a/transport.c
+++ b/transport.c
@@ -891,6 +891,42 @@ int transport_summary_width(const struct ref *refs)
 	return (2 * maxw + 3);
 }
 
+struct reflog_has_tip_cb_data {
+	const struct object_id *target;
+	int found;
+};
+
+static int reflog_has_tip(const char *refname UNUSED,
+			   struct object_id *old_oid UNUSED,
+			   struct object_id *new_oid,
+			   const char *committer UNUSED,
+			   timestamp_t timestamp UNUSED,
+			   int tz UNUSED, const char *msg UNUSED,
+			   void *cb_data)
+{
+	struct reflog_has_tip_cb_data *cb = cb_data;
+
+	if (!oideq(new_oid, cb->target))
+		return 0;
+	cb->found = 1;
+	return 1;
+}
+
+/*
+ * Was "refname" ever at "oid" according to its reflog? Then a remote
+ * sitting at "oid" carries no work we have not seen, only commits we
+ * have since rewritten, for example with 'commit --amend' or 'rebase'.
+ */
+static int local_ref_used_to_be_at(const char *refname,
+				    const struct object_id *oid)
+{
+	struct reflog_has_tip_cb_data cb = { .target = oid };
+
+	refs_for_each_reflog_ent_reverse(get_main_ref_store(the_repository),
+					 refname, reflog_has_tip, &cb);
+	return cb.found;
+}
+
 void transport_print_push_status(const char *dest, struct ref *refs,
 				  int verbose, int porcelain, unsigned int *reject_reasons)
 {
@@ -925,9 +961,11 @@ void transport_print_push_status(const char *dest, struct ref *refs,
 			n += print_one_push_status(ref, dest, n,
 						   porcelain, summary_width);
 		if (ref->status == REF_STATUS_REJECT_NONFASTFORWARD) {
-			if (head != NULL && !strcmp(head, ref->name))
+			if (head != NULL && !strcmp(head, ref->name)) {
 				*reject_reasons |= REJECT_NON_FF_HEAD;
-			else
+				if (local_ref_used_to_be_at(head, &ref->old_oid))
+					*reject_reasons |= REJECT_NON_FF_HEAD_REWRITE;
+			} else
 				*reject_reasons |= REJECT_NON_FF_OTHER;
 		} else if (ref->status == REF_STATUS_REJECT_ALREADY_EXISTS) {
 			*reject_reasons |= REJECT_ALREADY_EXISTS;
diff --git a/transport.h b/transport.h
index 39193d0077..87944fcd92 100644
--- a/transport.h
+++ b/transport.h
@@ -252,12 +252,13 @@ int transport_set_option(struct transport *transport, const char *name,
 void transport_set_verbosity(struct transport *transport, int verbosity,
 	int force_progress);
 
-#define REJECT_NON_FF_HEAD      0x01
-#define REJECT_NON_FF_OTHER     0x02
-#define REJECT_ALREADY_EXISTS   0x04
-#define REJECT_FETCH_FIRST      0x08
-#define REJECT_NEEDS_FORCE      0x10
-#define REJECT_REF_NEEDS_UPDATE 0x20
+#define REJECT_NON_FF_HEAD         0x01
+#define REJECT_NON_FF_OTHER        0x02
+#define REJECT_ALREADY_EXISTS      0x04
+#define REJECT_FETCH_FIRST         0x08
+#define REJECT_NEEDS_FORCE         0x10
+#define REJECT_REF_NEEDS_UPDATE    0x20
+#define REJECT_NON_FF_HEAD_REWRITE 0x40
 
 int transport_push(struct repository *repo,
 		   struct transport *connection,
-- 
gitgitgadget
Harald Nordgren via GitGitGadgetOct 6, 2026, 07:08 UTC in reply to Harald Nordgren via GitGitGadget on lore

[PATCH 6/6] push: suggest a force push after a clean rebase

From: Harald Nordgren <haraldnordgren@gmail.com>

After rebasing pushed work onto a newer upstream without changing it, "git status" says the branch was rebased cleanly, but a rejected "git push" still suggests pulling from the push branch, which would only bring back old copies of the same commits.

When the commits outside the upstream carry the same changes on both sides, say what "git status" says and suggest only the force push:

  hint: Updates were rejected because 'origin/topic' has diverged
  hint: from your current branch, which was rebased cleanly on 'upstream/main'.
  hint: Use 'git push --force-with-lease origin topic' to replace it.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 builtin/push.c           | 20 +++++++++++++++++---
 remote.c                 | 17 +++++++++++++++++
 remote.h                 |  6 ++++++
 t/t6040-tracking-info.sh | 25 +++++++++++++++++++++++++
 4 files changed, 65 insertions(+), 3 deletions(-)
Show changes to 4 files +65 −3

builtin/push.c, remote.c, remote.h, t/t6040-tracking-info.sh

diff --git a/builtin/push.c b/builtin/push.c
index d918723d43..bf6e5386a0 100644
--- a/builtin/push.c
+++ b/builtin/push.c
@@ -301,6 +301,11 @@ static const char message_advice_pull_from_branch_before_push[] =
 	   "from your current branch. Use 'git pull %s %s'\n"
 	   "to integrate the remote changes.");
 
+static const char message_advice_force_after_clean_rebase[] =
+	N_("Updates were rejected because '%s' has diverged\n"
+	   "from your current branch, which was rebased cleanly on '%s'.\n"
+	   "Use 'git push --force-with-lease %s %s' to replace it.");
+
 static const char message_advice_pull_or_force_before_push[] =
 	N_("Updates were rejected because '%s' has diverged\n"
 	   "from your current branch. Use 'git pull %s %s'\n"
@@ -356,15 +361,24 @@ static void advise_pull_before_push(struct remote *push_remote,
 		tracking_name = refs_shorten_unambiguous_ref(
 			get_main_ref_store(the_repository), tracking, 0);
 
-	if (tracking && (reject_reasons & REJECT_NON_FF_HEAD_REWRITE))
+	if (tracking && branch_rebased_cleanly(branch, tracking)) {
+		char *upstream_name = refs_shorten_unambiguous_ref(
+			get_main_ref_store(the_repository), upstream, 0);
+
+		advise(_(message_advice_force_after_clean_rebase),
+		       tracking_name, upstream_name,
+		       remote->name, branch->name);
+		free(upstream_name);
+	} else if (tracking && (reject_reasons & REJECT_NON_FF_HEAD_REWRITE)) {
 		advise(_(message_advice_pull_or_force_before_push),
 		       tracking_name, remote->name, branch->name,
 		       remote->name, branch->name);
-	else if (tracking && (!upstream || strcmp(tracking, upstream)))
+	} else if (tracking && (!upstream || strcmp(tracking, upstream))) {
 		advise(_(message_advice_pull_from_branch_before_push),
 		       tracking_name, remote->name, branch->name);
-	else
+	} else {
 		advise(_(message_advice_pull_before_push));
+	}
 
 	free(tracking_name);
 	free(tracking);
diff --git a/remote.c b/remote.c
index af8d026073..623ff98a01 100644
--- a/remote.c
+++ b/remote.c
@@ -2391,6 +2391,23 @@ static bool stat_outside_upstream(const char *branch_name, const char *base,
 	return true;
 }
 
+bool branch_rebased_cleanly(struct branch *branch, const char *base)
+{
+	const char *upstream = branch_get_upstream(branch, NULL);
+	int ours, theirs, ours_unmerged, theirs_unmerged;
+	bool same_changes = false;
+
+	if (!upstream || !strcmp(upstream, base))
+		return false;
+	if (stat_branch_pair(branch->refname, base, NULL, &ours, &theirs,
+			     NULL, AHEAD_BEHIND_FULL) <= 0 || !ours || !theirs)
+		return false;
+	return stat_outside_upstream(branch->refname, base, upstream,
+				     ours, theirs, &ours_unmerged,
+				     &theirs_unmerged, &same_changes) &&
+	       same_changes;
+}
+
 static char *resolve_compare_branch(struct branch *branch, const char *name)
 {
 	const char *resolved = NULL;
diff --git a/remote.h b/remote.h
index cca02033b9..dd8ef6c443 100644
--- a/remote.h
+++ b/remote.h
@@ -408,6 +408,12 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			 enum ahead_behind_flags abf,
 			 int show_divergence_advice);
 
+/*
+ * Return true when the branch has diverged from base only because the
+ * work on base was rebased cleanly on the upstream of the branch.
+ */
+bool branch_rebased_cleanly(struct branch *branch, const char *base);
+
 struct ref *get_local_heads(void);
 
 /*
diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
index b53034ba36..b5339e1452 100755
--- a/t/t6040-tracking-info.sh
+++ b/t/t6040-tracking-info.sh
@@ -804,6 +804,31 @@ test_expect_success 'status.compareBranches after a clean rebase of the push bra
 	and have 3 and 1 different commits each (rebased cleanly on ${SQ}origin/main${SQ}).
 	  (use "git push --force-with-lease" to publish your local commits)
 
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual &&
+	(
+		cd test &&
+		test_must_fail git push 2>../push.err &&
+		git push --force-with-lease origin feature19 &&
+		git status >../actual
+	) &&
+	url=$(git -C test config remote.origin.url) &&
+	cat >expect <<-EOF &&
+	To $url
+	 ! [rejected]        feature19 -> feature19 (non-fast-forward)
+	error: failed to push some refs to ${SQ}$url${SQ}
+	hint: Updates were rejected because ${SQ}origin/feature19${SQ} has diverged
+	hint: from your current branch, which was rebased cleanly on ${SQ}origin/main${SQ}.
+	hint: Use ${SQ}git push --force-with-lease origin feature19${SQ} to replace it.
+	EOF
+	test_cmp expect push.err &&
+	cat >expect <<-EOF &&
+	On branch feature19
+	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+
+	Your branch is up to date with ${SQ}origin/feature19${SQ}.
+
 	nothing to commit, working tree clean
 	EOF
 	test_cmp expect actual
-- 
gitgitgadget

Back to recent threads

[PATCH 0/6] status, push: handle a push branch rebased onto a newer upstream | The Git List