push: suggest a force push after a clean rebase

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>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
seen
Harald Nordgren 2026-10-06 07:08:24 +00:00 committed by Junio C Hamano
parent ae66f77bf9
commit 84bbe3b61c
4 changed files with 65 additions and 3 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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);

/*

View File

@ -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