git/list[1] front-page[2] threads[3] people[4] search[5] about
 

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

From
Harald Nordgren via GitGitGadget <gitgitgadget@gmail.com>
Date
Oct 6, 2026, 07:08 UTC
Message-ID
<740bf17e1360f698402203654643e4ca585754a9.1791270504.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.2437.git.git.1791270504.gitgitgadget@gmail.com>
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(-)
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
Previous: Harald Nordgren via GitGitGadgetNext: Harald Nordgren via GitGitGadget
Message 6 of 7 in “status, push: handle a push branch rebased onto a newer upstream”
  1. 0/6 status, push: handle a push branch rebased onto a newer upstreamHarald Nordgren via GitGitGadget, Oct 6, 2026
  2. 1/6 status: count push divergence outside the upstreamHarald Nordgren via GitGitGadget, Oct 6, 2026
  3. 2/6 status: say when the push branch was rebased cleanlyHarald Nordgren via GitGitGadget, Oct 6, 2026
  4. 3/6 status: suggest a force push after a clean rebaseHarald Nordgren via GitGitGadget, Oct 6, 2026
  5. 4/6 push: name the branch to pull from when it is not the upstreamHarald Nordgren via GitGitGadget, Oct 6, 2026
  6. 5/6 push: offer a force push after rewriting pushed commitsHarald Nordgren via GitGitGadget, Oct 6, 2026
  7. 6/6 push: suggest a force push after a clean rebaseHarald Nordgren via GitGitGadget, Oct 6, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.