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