Thread (7 messages) 7 messages, 1 author, 6h ago

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

DORMANTno replies

From: Harald Nordgren via GitGitGadget <hidden>
Date: 2026-10-06 07:08:46
Subsystem: the rest · Maintainer: Linus Torvalds

From: Harald Nordgren <redacted>

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 <redacted>
---
 builtin/push.c           | 20 +++++++++++++++++---
 remote.c                 | 17 +++++++++++++++++
 remote.h                 |  6 ++++++
 t/t6040-tracking-info.sh | 25 +++++++++++++++++++++++++
 4 files changed, 65 insertions(+), 3 deletions(-)
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
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help