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 5/6] push: offer a force push after rewriting pushed commits
Date: Tue, 06 Oct 2026 07:08:23 +0000 [thread overview]
Message-ID: <740bf17e1360f698402203654643e4ca585754a9.1791270504.git.gitgitgadget@gmail.com> (raw)
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
next prev parent 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 ` [PATCH 1/6] status: count push divergence outside the upstream Harald Nordgren via GitGitGadget
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 ` Harald Nordgren via GitGitGadget [this message]
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=740bf17e1360f698402203654643e4ca585754a9.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