{"thread":{"id":"66470","subject":"[PATCH 0/6] status, push: handle a push branch rebased onto a newer upstream","startedAt":"2026-10-06T07:08:18Z","lastAt":"2026-10-06T07:08:24Z","messageCount":7,"participants":["Harald Nordgren via GitGitGadget"],"isPatch":true,"patchVersion":1,"patchTotal":6},"messages":[{"id":"554241","messageId":"pull.2437.git.git.1791270504.gitgitgadget@gmail.com","threadId":"66470","inReplyTo":null,"subject":"[PATCH 0/6] status, push: handle a push branch rebased onto a newer upstream","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-06T07:08:18Z","receivedAt":"2026-10-06T07:08:18Z","isPatch":true,"body":"git status now tells when the push branch only differs because your work was\nrebased onto a newer upstream.\n\nWith status.compareBranches including @{push}, when your branch and the push\nbranch have diverged:\n\n * commits from @{upstream} are counted apart: (1 and 1 not in\n   'upstream/main')\n * if the rest carry the same changes on both sides: (rebased cleanly on\n   'upstream/main'), with a hint to git push --force-with-lease\n * if nothing came from @{upstream}, the message is unchanged\n\nWhen git push is rejected because the branch it updates has diverged:\n\n * if someone else updated your upstream branch, the hint still says git\n   pull\n * if you amended or rebased commits you already pushed, the hint offers git\n   pull and git push --force-with-lease\n * if they were rebased cleanly, the hint offers git push --force-with-lease\n\nHarald Nordgren (6):\n  status: count push divergence outside the upstream\n  status: say when the push branch was rebased cleanly\n  status: suggest a force push after a clean rebase\n  push: name the branch to pull from when it is not the upstream\n  push: offer a force push after rewriting pushed commits\n  push: suggest a force push after a clean rebase\n\n builtin/push.c           |  62 ++++++++++++++-\n remote.c                 | 132 +++++++++++++++++++++++++++----\n remote.h                 |   6 ++\n t/t6040-tracking-info.sh | 163 +++++++++++++++++++++++++++++++++++++++\n transport.c              |  42 +++++++++-\n transport.h              |  13 ++--\n 6 files changed, 393 insertions(+), 25 deletions(-)\n\n\nbase-commit: 8103b446517e0c44e67561b9d0ccce56efa60a71\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2437%2FHaraldNordgren%2Fstatus-push-not-in-upstream-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2437/HaraldNordgren/status-push-not-in-upstream-v1\nPull-Request: https://github.com/git/git/pull/2437\n-- \ngitgitgadget\n\n"},{"id":"554242","messageId":"e1651568b1923ab075542e2c5027917bae76c619.1791270504.git.gitgitgadget@gmail.com","threadId":"66470","inReplyTo":"pull.2437.git.git.1791270504.gitgitgadget@gmail.com","subject":"[PATCH 1/6] status: count push divergence outside the upstream","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-06T07:08:19Z","receivedAt":"2026-10-06T07:08:19Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAfter rebasing onto a newer upstream, \"git status\" counts every commit\nthe upstream gained since the last push as a difference from the push\nbranch when status.compareBranches includes \"@{push}\". That can be many\ncommits, even when only a few of your own differ.\n\nKeep the full counts, and when they include commits from the upstream,\nadd how many of them are not in it:\n\n  Your branch and 'origin/topic' have diverged,\n  and have 51 and 1 different commits each (1 and 1 not in 'upstream/main').\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 81 +++++++++++++++++++++++++++++++++-------\n t/t6040-tracking-info.sh | 32 ++++++++++++++++\n 2 files changed, 100 insertions(+), 13 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex fe62068463..cbe79275f7 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2247,11 +2247,12 @@ int resolve_remote_symref(struct ref *ref, struct ref *list)\n  */\n \n static int stat_branch_pair(const char *branch_name, const char *base,\n+\t\t\t     const char *exclude,\n \t\t\t     int *num_ours, int *num_theirs,\n \t\t\t     enum ahead_behind_flags abf)\n {\n \tstruct object_id oid;\n-\tstruct commit *ours, *theirs;\n+\tstruct commit *ours, *theirs, *excluded = NULL;\n \tstruct rev_info revs;\n \tstruct strvec argv = STRVEC_INIT;\n \n@@ -2268,6 +2269,14 @@ static int stat_branch_pair(const char *branch_name, const char *base,\n \tif (!ours)\n \t\treturn -1;\n \n+\tif (exclude) {\n+\t\tif (refs_read_ref(get_main_ref_store(the_repository), exclude, &oid))\n+\t\t\treturn -1;\n+\t\texcluded = lookup_commit_reference(the_repository, &oid);\n+\t\tif (!excluded)\n+\t\t\treturn -1;\n+\t}\n+\n \t*num_theirs = *num_ours = 0;\n \n \t/* are we the same? */\n@@ -2284,6 +2293,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,\n \tstrvec_pushf(&argv, \"%s...%s\",\n \t\t     oid_to_hex(&ours->object.oid),\n \t\t     oid_to_hex(&theirs->object.oid));\n+\tif (excluded)\n+\t\tstrvec_pushf(&argv, \"^%s\", oid_to_hex(&excluded->object.oid));\n \tstrvec_push(&argv, \"--\");\n \n \trepo_init_revisions(the_repository, &revs, NULL);\n@@ -2305,6 +2316,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,\n \t/* clear object flags smudged by the above traversal */\n \tclear_commit_marks(ours, ALL_REV_FLAGS);\n \tclear_commit_marks(theirs, ALL_REV_FLAGS);\n+\tif (excluded)\n+\t\tclear_commit_marks(excluded, ALL_REV_FLAGS);\n \n \tstrvec_clear(&argv);\n \trelease_revisions(&revs);\n@@ -2344,7 +2357,23 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \tif (!base)\n \t\treturn -1;\n \n-\treturn stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);\n+\treturn stat_branch_pair(branch->refname, base, NULL,\n+\t\t\t\tnum_ours, num_theirs, abf);\n+}\n+\n+/*\n+ * Count the commits that differ between branch_name and base but are not\n+ * in upstream. Return false when they cannot be counted or upstream\n+ * accounts for none of the ours and theirs commits.\n+ */\n+static bool stat_outside_upstream(const char *branch_name, const char *base,\n+\t\t\t\t  const char *upstream, int ours, int theirs,\n+\t\t\t\t  int *ours_unmerged, int *theirs_unmerged)\n+{\n+\tif (stat_branch_pair(branch_name, base, upstream, ours_unmerged,\n+\t\t\t     theirs_unmerged, AHEAD_BEHIND_FULL) < 0)\n+\t\treturn false;\n+\treturn *ours_unmerged != ours || *theirs_unmerged != theirs;\n }\n \n static char *resolve_compare_branch(struct branch *branch, const char *name)\n@@ -2376,6 +2405,8 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     const char *branch_name,\n \t\t\t\t     const char *push_remote_name,\n \t\t\t\t     const char *push_branch_name,\n+\t\t\t\t     const char *upstream_name,\n+\t\t\t\t     int ours_unmerged, int theirs_unmerged,\n \t\t\t\t     enum ahead_behind_flags abf,\n \t\t\t\t     unsigned flags)\n {\n@@ -2421,15 +2452,27 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t\t}\n \t} else {\n-\t\tstrbuf_addf(sb,\n-\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n-\t\t\t       \"and have %d and %d different commit each, \"\n-\t\t\t       \"respectively.\\n\",\n-\t\t\t   \"Your branch and '%s' have diverged,\\n\"\n-\t\t\t       \"and have %d and %d different commits each, \"\n-\t\t\t       \"respectively.\\n\",\n-\t\t\t   ours + theirs),\n-\t\t\tbranch_name, ours, theirs);\n+\t\tif (upstream_name)\n+\t\t\tstrbuf_addf(sb,\n+\t\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n+\t\t\t\t       \"and have %d and %d different commit each \"\n+\t\t\t\t       \"(%d and %d not in '%s').\\n\",\n+\t\t\t\t   \"Your branch and '%s' have diverged,\\n\"\n+\t\t\t\t       \"and have %d and %d different commits each \"\n+\t\t\t\t       \"(%d and %d not in '%s').\\n\",\n+\t\t\t\t   ours + theirs),\n+\t\t\t\tbranch_name, ours, theirs,\n+\t\t\t\tours_unmerged, theirs_unmerged, upstream_name);\n+\t\telse\n+\t\t\tstrbuf_addf(sb,\n+\t\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n+\t\t\t\t       \"and have %d and %d different commit each, \"\n+\t\t\t\t       \"respectively.\\n\",\n+\t\t\t\t   \"Your branch and '%s' have diverged,\\n\"\n+\t\t\t\t       \"and have %d and %d different commits each, \"\n+\t\t\t\t       \"respectively.\\n\",\n+\t\t\t\t   ours + theirs),\n+\t\t\t\tbranch_name, ours, theirs);\n \t\tif (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS)) {\n \t\t\tif (push_remote_name && push_branch_name)\n \t\t\t\tstrbuf_addf(sb,\n@@ -2473,7 +2516,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \tfor (i = 0; i < branches.nr; i++) {\n \t\tchar *full_ref;\n \t\tchar *short_ref;\n+\t\tchar *upstream_name = NULL;\n \t\tint ours, theirs, cmp;\n+\t\tint ours_unmerged = 0, theirs_unmerged = 0;\n \t\tint is_upstream, is_push;\n \t\tunsigned flags = 0;\n \t\tconst char *push_remote_name = NULL;\n@@ -2498,9 +2543,17 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tif (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))\n \t\t\tis_push = 1;\n \n-\t\tcmp = stat_branch_pair(branch->refname, full_ref,\n+\t\tcmp = stat_branch_pair(branch->refname, full_ref, NULL,\n \t\t\t\t       &ours, &theirs, abf);\n \n+\t\tif (cmp > 0 && ours && theirs && upstream_ref && !is_upstream &&\n+\t\t    stat_outside_upstream(branch->refname, full_ref,\n+\t\t\t\t\t  upstream_ref, ours, theirs,\n+\t\t\t\t\t  &ours_unmerged, &theirs_unmerged))\n+\t\t\tupstream_name = refs_shorten_unambiguous_ref(\n+\t\t\t\tget_main_ref_store(the_repository),\n+\t\t\t\tupstream_ref, 0);\n+\n \t\tif (cmp < 0) {\n \t\t\tif (is_upstream) {\n \t\t\t\tstrbuf_addf(sb,\n@@ -2542,11 +2595,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t}\n \t\tformat_branch_comparison(sb, !cmp, ours, theirs, short_ref,\n \t\t\t\t\t push_remote_name, push_branch_name,\n-\t\t\t\t\t abf, flags);\n+\t\t\t\t\t upstream_name, ours_unmerged,\n+\t\t\t\t\t theirs_unmerged, abf, flags);\n \t\treported = 1;\n \n \t\tfree(full_ref);\n \t\tfree(short_ref);\n+\t\tfree(upstream_name);\n \t}\n \n \tstring_list_clear(&branches, 0);\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex e95d420972..f8df16a5de 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -746,4 +746,36 @@ test_expect_success 'status.compareBranches suppresses advice when push tracking\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status.compareBranches counts push divergence outside upstream' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature18 origin/main &&\n+\t\tadvance work18 &&\n+\t\tgit push\n+\t) &&\n+\tgit checkout main &&\n+\tadvance main18a &&\n+\tadvance main18b &&\n+\tgit checkout - &&\n+\t(\n+\t\tcd test &&\n+\t\techo amended >work18 &&\n+\t\tgit commit -a --amend --no-edit &&\n+\t\tgit pull --rebase &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature18\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature18${SQ} have diverged,\n+\tand have 3 and 1 different commits each (1 and 1 not in ${SQ}origin/main${SQ}).\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n\n"},{"id":"554243","messageId":"dc0efefa0ef02a04f211cf7e24a8b6855f36e12f.1791270504.git.gitgitgadget@gmail.com","threadId":"66470","inReplyTo":"pull.2437.git.git.1791270504.gitgitgadget@gmail.com","subject":"[PATCH 2/6] status: say when the push branch was rebased cleanly","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-06T07:08:20Z","receivedAt":"2026-10-06T07:08:20Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAfter rebasing pushed work onto a newer upstream without changing it,\n\"git status\" reports the commits outside the upstream on both sides,\nas if the branch and its push branch held different work.\n\nWhen those commits carry the same changes on both sides, say that the\nbranch was rebased cleanly:\n\n  Your branch and 'origin/topic' have diverged,\n  and have 51 and 1 different commits each (rebased cleanly on 'upstream/main').\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 49 ++++++++++++++++++++++++++++++++--------\n t/t6040-tracking-info.sh | 30 ++++++++++++++++++++++++\n 2 files changed, 69 insertions(+), 10 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex cbe79275f7..89d142cc7e 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2248,7 +2248,7 @@ int resolve_remote_symref(struct ref *ref, struct ref *list)\n \n static int stat_branch_pair(const char *branch_name, const char *base,\n \t\t\t     const char *exclude,\n-\t\t\t     int *num_ours, int *num_theirs,\n+\t\t\t     int *num_ours, int *num_theirs, int *num_same,\n \t\t\t     enum ahead_behind_flags abf)\n {\n \tstruct object_id oid;\n@@ -2278,6 +2278,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,\n \t}\n \n \t*num_theirs = *num_ours = 0;\n+\tif (num_same)\n+\t\t*num_same = 0;\n \n \t/* are we the same? */\n \tif (theirs == ours)\n@@ -2290,6 +2292,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,\n \t/* Run \"rev-list --left-right ours...theirs\" internally... */\n \tstrvec_push(&argv, \"\"); /* ignored */\n \tstrvec_push(&argv, \"--left-right\");\n+\tif (num_same)\n+\t\tstrvec_push(&argv, \"--cherry-mark\");\n \tstrvec_pushf(&argv, \"%s...%s\",\n \t\t     oid_to_hex(&ours->object.oid),\n \t\t     oid_to_hex(&theirs->object.oid));\n@@ -2311,6 +2315,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,\n \t\t\t(*num_ours)++;\n \t\telse\n \t\t\t(*num_theirs)++;\n+\t\tif (num_same && (c->object.flags & PATCHSAME))\n+\t\t\t(*num_same)++;\n \t}\n \n \t/* clear object flags smudged by the above traversal */\n@@ -2358,22 +2364,31 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,\n \t\treturn -1;\n \n \treturn stat_branch_pair(branch->refname, base, NULL,\n-\t\t\t\tnum_ours, num_theirs, abf);\n+\t\t\t\tnum_ours, num_theirs, NULL, abf);\n }\n \n /*\n  * Count the commits that differ between branch_name and base but are not\n  * in upstream. Return false when they cannot be counted or upstream\n- * accounts for none of the ours and theirs commits.\n+ * accounts for none of the ours and theirs commits. Otherwise set\n+ * *same_changes when the remaining commits carry the same changes on\n+ * both sides.\n  */\n static bool stat_outside_upstream(const char *branch_name, const char *base,\n \t\t\t\t  const char *upstream, int ours, int theirs,\n-\t\t\t\t  int *ours_unmerged, int *theirs_unmerged)\n+\t\t\t\t  int *ours_unmerged, int *theirs_unmerged,\n+\t\t\t\t  bool *same_changes)\n {\n+\tint same;\n+\n \tif (stat_branch_pair(branch_name, base, upstream, ours_unmerged,\n-\t\t\t     theirs_unmerged, AHEAD_BEHIND_FULL) < 0)\n+\t\t\t     theirs_unmerged, &same, AHEAD_BEHIND_FULL) < 0)\n+\t\treturn false;\n+\tif (*ours_unmerged == ours && *theirs_unmerged == theirs)\n \t\treturn false;\n-\treturn *ours_unmerged != ours || *theirs_unmerged != theirs;\n+\t*same_changes = *ours_unmerged &&\n+\t\tsame == *ours_unmerged + *theirs_unmerged;\n+\treturn true;\n }\n \n static char *resolve_compare_branch(struct branch *branch, const char *name)\n@@ -2407,6 +2422,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t     const char *push_branch_name,\n \t\t\t\t     const char *upstream_name,\n \t\t\t\t     int ours_unmerged, int theirs_unmerged,\n+\t\t\t\t     bool same_changes,\n \t\t\t\t     enum ahead_behind_flags abf,\n \t\t\t\t     unsigned flags)\n {\n@@ -2452,7 +2468,17 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t\t}\n \t} else {\n-\t\tif (upstream_name)\n+\t\tif (same_changes)\n+\t\t\tstrbuf_addf(sb,\n+\t\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n+\t\t\t\t       \"and have %d and %d different commit each \"\n+\t\t\t\t       \"(rebased cleanly on '%s').\\n\",\n+\t\t\t\t   \"Your branch and '%s' have diverged,\\n\"\n+\t\t\t\t       \"and have %d and %d different commits each \"\n+\t\t\t\t       \"(rebased cleanly on '%s').\\n\",\n+\t\t\t\t   ours + theirs),\n+\t\t\t\tbranch_name, ours, theirs, upstream_name);\n+\t\telse if (upstream_name)\n \t\t\tstrbuf_addf(sb,\n \t\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n \t\t\t\t       \"and have %d and %d different commit each \"\n@@ -2519,6 +2545,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tchar *upstream_name = NULL;\n \t\tint ours, theirs, cmp;\n \t\tint ours_unmerged = 0, theirs_unmerged = 0;\n+\t\tbool same_changes = false;\n \t\tint is_upstream, is_push;\n \t\tunsigned flags = 0;\n \t\tconst char *push_remote_name = NULL;\n@@ -2544,12 +2571,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\tis_push = 1;\n \n \t\tcmp = stat_branch_pair(branch->refname, full_ref, NULL,\n-\t\t\t\t       &ours, &theirs, abf);\n+\t\t\t\t       &ours, &theirs, NULL, abf);\n \n \t\tif (cmp > 0 && ours && theirs && upstream_ref && !is_upstream &&\n \t\t    stat_outside_upstream(branch->refname, full_ref,\n \t\t\t\t\t  upstream_ref, ours, theirs,\n-\t\t\t\t\t  &ours_unmerged, &theirs_unmerged))\n+\t\t\t\t\t  &ours_unmerged, &theirs_unmerged,\n+\t\t\t\t\t  &same_changes))\n \t\t\tupstream_name = refs_shorten_unambiguous_ref(\n \t\t\t\tget_main_ref_store(the_repository),\n \t\t\t\tupstream_ref, 0);\n@@ -2596,7 +2624,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\tformat_branch_comparison(sb, !cmp, ours, theirs, short_ref,\n \t\t\t\t\t push_remote_name, push_branch_name,\n \t\t\t\t\t upstream_name, ours_unmerged,\n-\t\t\t\t\t theirs_unmerged, abf, flags);\n+\t\t\t\t\t theirs_unmerged, same_changes,\n+\t\t\t\t\t abf, flags);\n \t\treported = 1;\n \n \t\tfree(full_ref);\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex f8df16a5de..2ebc3573da 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -778,4 +778,34 @@ test_expect_success 'status.compareBranches counts push divergence outside upstr\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'status.compareBranches after a clean rebase of the push branch' '\n+\ttest_config -C test push.default current &&\n+\ttest_config -C test status.compareBranches \"@{upstream} @{push}\" &&\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature19 origin/main &&\n+\t\tadvance work19 &&\n+\t\tgit push\n+\t) &&\n+\tgit checkout main &&\n+\tadvance main19a &&\n+\tadvance main19b &&\n+\tgit checkout - &&\n+\t(\n+\t\tcd test &&\n+\t\tgit pull --rebase &&\n+\t\tgit status >../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature19\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch and ${SQ}origin/feature19${SQ} have diverged,\n+\tand have 3 and 1 different commits each (rebased cleanly on ${SQ}origin/main${SQ}).\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n\n"},{"id":"554244","messageId":"46f98796e7b07d93899f2c43c2699a82834e9778.1791270504.git.gitgitgadget@gmail.com","threadId":"66470","inReplyTo":"pull.2437.git.git.1791270504.gitgitgadget@gmail.com","subject":"[PATCH 3/6] status: suggest a force push after a clean rebase","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-06T07:08:21Z","receivedAt":"2026-10-06T07:08:21Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAfter rebasing pushed work onto a newer upstream without changing it,\n\"git status\" says the branch was rebased cleanly but gives no hint on\nhow to publish it. A plain \"git push\" is rejected, so suggest a force\npush:\n\n  Your branch and 'origin/topic' have diverged,\n  and have 51 and 1 different commits each (rebased cleanly on 'upstream/main').\n    (use \"git push --force-with-lease\" to publish your local commits)\n\nThe lease still stops the push if someone else updated the push branch\nsince it was last fetched.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n remote.c                 | 7 +++++--\n t/t6040-tracking-info.sh | 1 +\n 2 files changed, 6 insertions(+), 2 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 89d142cc7e..af8d026073 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2468,7 +2468,7 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t\t_(\"  (use \\\"git pull\\\" to update your local branch)\\n\"));\n \t\t}\n \t} else {\n-\t\tif (same_changes)\n+\t\tif (same_changes) {\n \t\t\tstrbuf_addf(sb,\n \t\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n \t\t\t\t       \"and have %d and %d different commit each \"\n@@ -2478,7 +2478,10 @@ static void format_branch_comparison(struct strbuf *sb,\n \t\t\t\t       \"(rebased cleanly on '%s').\\n\",\n \t\t\t\t   ours + theirs),\n \t\t\t\tbranch_name, ours, theirs, upstream_name);\n-\t\telse if (upstream_name)\n+\t\t\tif (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))\n+\t\t\t\tstrbuf_addstr(sb,\n+\t\t\t\t\t_(\"  (use \\\"git push --force-with-lease\\\" to publish your local commits)\\n\"));\n+\t\t} else if (upstream_name)\n \t\t\tstrbuf_addf(sb,\n \t\t\t\tQ_(\"Your branch and '%s' have diverged,\\n\"\n \t\t\t\t       \"and have %d and %d different commit each \"\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 2ebc3573da..4074c6663a 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -802,6 +802,7 @@ test_expect_success 'status.compareBranches after a clean rebase of the push bra\n \n \tYour branch and ${SQ}origin/feature19${SQ} have diverged,\n \tand have 3 and 1 different commits each (rebased cleanly on ${SQ}origin/main${SQ}).\n+\t  (use \"git push --force-with-lease\" to publish your local commits)\n \n \tnothing to commit, working tree clean\n \tEOF\n-- \ngitgitgadget\n\n\n"},{"id":"554245","messageId":"b44f4cdeaff64a5099adfc9dd325033e8c38f7b4.1791270504.git.gitgitgadget@gmail.com","threadId":"66470","inReplyTo":"pull.2437.git.git.1791270504.gitgitgadget@gmail.com","subject":"[PATCH 4/6] push: name the branch to pull from when it is not the upstream","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-06T07:08:22Z","receivedAt":"2026-10-06T07:08:22Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nWhen someone else pushed to the branch you push to, such as a topic\nbranch on your fork, \"git push\" is rejected with a hint to run\n\"git pull\" first. If that branch is not your upstream, the pull merges\nthe upstream and leaves the push branch diverged, so the push is\nrejected again.\n\nName the branch that diverged and suggest pulling from it:\n\n  hint: Updates were rejected because 'origin/topic' has diverged\n  hint: from your current branch. Use 'git pull origin topic'\n  hint: to integrate the remote changes.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/push.c           | 37 +++++++++++++++++++++++---\n t/t6040-tracking-info.sh | 57 ++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 91 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/push.c b/builtin/push.c\nindex 2377b5af55..82435beab5 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -12,6 +12,7 @@\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.h\"\n+#include \"refs.h\"\n #include \"refspec.h\"\n #include \"run-command.h\"\n #include \"remote.h\"\n@@ -295,6 +296,11 @@ static const char message_advice_pull_before_push[] =\n \t   \"use 'git pull' before pushing again.\\n\"\n \t   \"See the 'Note about fast-forwards' in 'git push --help' for details.\");\n \n+static const char message_advice_pull_from_branch_before_push[] =\n+\tN_(\"Updates were rejected because '%s' has diverged\\n\"\n+\t   \"from your current branch. Use 'git pull %s %s'\\n\"\n+\t   \"to integrate the remote changes.\");\n+\n static const char message_advice_checkout_pull_push[] =\n \tN_(\"Updates were rejected because a pushed branch tip is behind its remote\\n\"\n \t   \"counterpart. If you want to integrate the remote changes, use 'git pull'\\n\"\n@@ -322,11 +328,35 @@ static const char message_advice_ref_needs_update[] =\n \t   \"remote changes, use 'git pull' before pushing again.\\n\"\n \t   \"See the 'Note about fast-forwards' in 'git push --help' for details.\");\n \n-static void advise_pull_before_push(void)\n+static void advise_pull_before_push(struct remote *push_remote)\n {\n+\tstruct branch *branch = branch_get(NULL);\n+\tstruct remote *remote = NULL;\n+\tconst char *upstream = NULL;\n+\tchar *tracking = NULL;\n+\tchar *tracking_name = NULL;\n+\n \tif (!advice_enabled(ADVICE_PUSH_NON_FF_CURRENT) || !advice_enabled(ADVICE_PUSH_UPDATE_REJECTED))\n \t\treturn;\n-\tadvise(_(message_advice_pull_before_push));\n+\n+\tif (branch) {\n+\t\tremote = repo_remote_for_push_tracking(the_repository,\n+\t\t\t\t\t\t       push_remote);\n+\t\ttracking = apply_refspecs(&remote->fetch, branch->refname);\n+\t\tupstream = branch_get_upstream(branch, NULL);\n+\t}\n+\tif (tracking)\n+\t\ttracking_name = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), tracking, 0);\n+\n+\tif (tracking && (!upstream || strcmp(tracking, upstream)))\n+\t\tadvise(_(message_advice_pull_from_branch_before_push),\n+\t\t       tracking_name, remote->name, branch->name);\n+\telse\n+\t\tadvise(_(message_advice_pull_before_push));\n+\n+\tfree(tracking_name);\n+\tfree(tracking);\n }\n \n static void advise_checkout_pull_push(void)\n@@ -370,6 +400,7 @@ static int push_with_options(struct transport *transport, struct refspec *rs,\n \tint err;\n \tunsigned int reject_reasons;\n \tchar *anon_url = transport_anonymize_url(transport->url);\n+\tstruct remote *remote = transport->remote;\n \n \ttransport_set_verbosity(transport, verbosity, progress);\n \ttransport->family = family;\n@@ -404,7 +435,7 @@ static int push_with_options(struct transport *transport, struct refspec *rs,\n \t\treturn 0;\n \n \tif (reject_reasons & REJECT_NON_FF_HEAD) {\n-\t\tadvise_pull_before_push();\n+\t\tadvise_pull_before_push(remote);\n \t} else if (reject_reasons & REJECT_NON_FF_OTHER) {\n \t\tadvise_checkout_pull_push();\n \t} else if (reject_reasons & REJECT_ALREADY_EXISTS) {\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 4074c6663a..9eb810e158 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -809,4 +809,61 @@ test_expect_success 'status.compareBranches after a clean rebase of the push bra\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'push to a push branch someone else updated suggests pulling from it' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature20 origin/main &&\n+\t\tadvance work20 &&\n+\t\tgit push origin feature20\n+\t) &&\n+\tgit checkout feature20 &&\n+\tadvance other20 &&\n+\tgit checkout - &&\n+\t(\n+\t\tcd test &&\n+\t\tadvance mine20 &&\n+\t\tgit fetch &&\n+\t\ttest_must_fail git push origin feature20 2>../actual\n+\t) &&\n+\turl=$(git -C test config remote.origin.url) &&\n+\tcat >expect <<-EOF &&\n+\tTo $url\n+\t ! [rejected]        feature20 -> feature20 (non-fast-forward)\n+\terror: failed to push some refs to ${SQ}$url${SQ}\n+\thint: Updates were rejected because ${SQ}origin/feature20${SQ} has diverged\n+\thint: from your current branch. Use ${SQ}git pull origin feature20${SQ}\n+\thint: to integrate the remote changes.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'push to the upstream branch' '\n+\t(\n+\t\tcd test &&\n+\t\tgit checkout -b feature21 origin/main &&\n+\t\tadvance work21 &&\n+\t\tgit push -u origin feature21\n+\t) &&\n+\tgit checkout feature21 &&\n+\tadvance other21 &&\n+\tgit checkout - &&\n+\t(\n+\t\tcd test &&\n+\t\tadvance mine21 &&\n+\t\tgit fetch &&\n+\t\ttest_must_fail git push 2>../actual\n+\t) &&\n+\turl=$(git -C test config remote.origin.url) &&\n+\tcat >expect <<-EOF &&\n+\tTo $url\n+\t ! [rejected]        feature21 -> feature21 (non-fast-forward)\n+\terror: failed to push some refs to ${SQ}$url${SQ}\n+\thint: Updates were rejected because the tip of your current branch is behind\n+\thint: its remote counterpart. If you want to integrate the remote changes,\n+\thint: use ${SQ}git pull${SQ} before pushing again.\n+\thint: See the ${SQ}Note about fast-forwards${SQ} in ${SQ}git push --help${SQ} for details.\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \ngitgitgadget\n\n\n"},{"id":"554246","messageId":"740bf17e1360f698402203654643e4ca585754a9.1791270504.git.gitgitgadget@gmail.com","threadId":"66470","inReplyTo":"pull.2437.git.git.1791270504.gitgitgadget@gmail.com","subject":"[PATCH 5/6] push: offer a force push after rewriting pushed commits","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-06T07:08:23Z","receivedAt":"2026-10-06T07:08:23Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAfter amending or rebasing commits you already pushed, \"git push\" is\nrejected with a hint to run \"git pull\" first. Pulling merges the old\ncopies of the same work back in.\n\nWhen the rejected remote tip is a commit your branch pointed to before,\naccording to its reflog, the remote carries no work you have not seen.\nName the branch and offer a force push next to the pull:\n\n  hint: Updates were rejected because 'origin/topic' has diverged\n  hint: from your current branch. Use 'git pull origin topic'\n  hint: to integrate the remote changes, or replace them with\n  hint: 'git push --force-with-lease origin topic'.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/push.c           | 17 +++++++++++++---\n t/t6040-tracking-info.sh | 18 +++++++++++++++++\n transport.c              | 42 ++++++++++++++++++++++++++++++++++++++--\n transport.h              | 13 +++++++------\n 4 files changed, 79 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/push.c b/builtin/push.c\nindex 82435beab5..d918723d43 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -301,6 +301,12 @@ static const char message_advice_pull_from_branch_before_push[] =\n \t   \"from your current branch. Use 'git pull %s %s'\\n\"\n \t   \"to integrate the remote changes.\");\n \n+static const char message_advice_pull_or_force_before_push[] =\n+\tN_(\"Updates were rejected because '%s' has diverged\\n\"\n+\t   \"from your current branch. Use 'git pull %s %s'\\n\"\n+\t   \"to integrate the remote changes, or replace them with\\n\"\n+\t   \"'git push --force-with-lease %s %s'.\");\n+\n static const char message_advice_checkout_pull_push[] =\n \tN_(\"Updates were rejected because a pushed branch tip is behind its remote\\n\"\n \t   \"counterpart. If you want to integrate the remote changes, use 'git pull'\\n\"\n@@ -328,7 +334,8 @@ static const char message_advice_ref_needs_update[] =\n \t   \"remote changes, use 'git pull' before pushing again.\\n\"\n \t   \"See the 'Note about fast-forwards' in 'git push --help' for details.\");\n \n-static void advise_pull_before_push(struct remote *push_remote)\n+static void advise_pull_before_push(struct remote *push_remote,\n+\t\t\t\t    unsigned int reject_reasons)\n {\n \tstruct branch *branch = branch_get(NULL);\n \tstruct remote *remote = NULL;\n@@ -349,7 +356,11 @@ static void advise_pull_before_push(struct remote *push_remote)\n \t\ttracking_name = refs_shorten_unambiguous_ref(\n \t\t\tget_main_ref_store(the_repository), tracking, 0);\n \n-\tif (tracking && (!upstream || strcmp(tracking, upstream)))\n+\tif (tracking && (reject_reasons & REJECT_NON_FF_HEAD_REWRITE))\n+\t\tadvise(_(message_advice_pull_or_force_before_push),\n+\t\t       tracking_name, remote->name, branch->name,\n+\t\t       remote->name, branch->name);\n+\telse if (tracking && (!upstream || strcmp(tracking, upstream)))\n \t\tadvise(_(message_advice_pull_from_branch_before_push),\n \t\t       tracking_name, remote->name, branch->name);\n \telse\n@@ -435,7 +446,7 @@ static int push_with_options(struct transport *transport, struct refspec *rs,\n \t\treturn 0;\n \n \tif (reject_reasons & REJECT_NON_FF_HEAD) {\n-\t\tadvise_pull_before_push(remote);\n+\t\tadvise_pull_before_push(remote, reject_reasons);\n \t} else if (reject_reasons & REJECT_NON_FF_OTHER) {\n \t\tadvise_checkout_pull_push();\n \t} else if (reject_reasons & REJECT_ALREADY_EXISTS) {\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex 9eb810e158..b53034ba36 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -863,6 +863,24 @@ test_expect_success 'push to the upstream branch' '\n \thint: use ${SQ}git pull${SQ} before pushing again.\n \thint: See the ${SQ}Note about fast-forwards${SQ} in ${SQ}git push --help${SQ} for details.\n \tEOF\n+\ttest_cmp expect actual &&\n+\t(\n+\t\tcd test &&\n+\t\tgit pull --rebase &&\n+\t\tgit push &&\n+\t\techo amended >mine21 &&\n+\t\tgit commit -a --amend --no-edit &&\n+\t\ttest_must_fail git push 2>../actual\n+\t) &&\n+\tcat >expect <<-EOF &&\n+\tTo $url\n+\t ! [rejected]        feature21 -> feature21 (non-fast-forward)\n+\terror: failed to push some refs to ${SQ}$url${SQ}\n+\thint: Updates were rejected because ${SQ}origin/feature21${SQ} has diverged\n+\thint: from your current branch. Use ${SQ}git pull origin feature21${SQ}\n+\thint: to integrate the remote changes, or replace them with\n+\thint: ${SQ}git push --force-with-lease origin feature21${SQ}.\n+\tEOF\n \ttest_cmp expect actual\n '\n \ndiff --git a/transport.c b/transport.c\nindex 25e2c14a7b..630dd699db 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -891,6 +891,42 @@ int transport_summary_width(const struct ref *refs)\n \treturn (2 * maxw + 3);\n }\n \n+struct reflog_has_tip_cb_data {\n+\tconst struct object_id *target;\n+\tint found;\n+};\n+\n+static int reflog_has_tip(const char *refname UNUSED,\n+\t\t\t   struct object_id *old_oid UNUSED,\n+\t\t\t   struct object_id *new_oid,\n+\t\t\t   const char *committer UNUSED,\n+\t\t\t   timestamp_t timestamp UNUSED,\n+\t\t\t   int tz UNUSED, const char *msg UNUSED,\n+\t\t\t   void *cb_data)\n+{\n+\tstruct reflog_has_tip_cb_data *cb = cb_data;\n+\n+\tif (!oideq(new_oid, cb->target))\n+\t\treturn 0;\n+\tcb->found = 1;\n+\treturn 1;\n+}\n+\n+/*\n+ * Was \"refname\" ever at \"oid\" according to its reflog? Then a remote\n+ * sitting at \"oid\" carries no work we have not seen, only commits we\n+ * have since rewritten, for example with 'commit --amend' or 'rebase'.\n+ */\n+static int local_ref_used_to_be_at(const char *refname,\n+\t\t\t\t    const struct object_id *oid)\n+{\n+\tstruct reflog_has_tip_cb_data cb = { .target = oid };\n+\n+\trefs_for_each_reflog_ent_reverse(get_main_ref_store(the_repository),\n+\t\t\t\t\t refname, reflog_has_tip, &cb);\n+\treturn cb.found;\n+}\n+\n void transport_print_push_status(const char *dest, struct ref *refs,\n \t\t\t\t  int verbose, int porcelain, unsigned int *reject_reasons)\n {\n@@ -925,9 +961,11 @@ void transport_print_push_status(const char *dest, struct ref *refs,\n \t\t\tn += print_one_push_status(ref, dest, n,\n \t\t\t\t\t\t   porcelain, summary_width);\n \t\tif (ref->status == REF_STATUS_REJECT_NONFASTFORWARD) {\n-\t\t\tif (head != NULL && !strcmp(head, ref->name))\n+\t\t\tif (head != NULL && !strcmp(head, ref->name)) {\n \t\t\t\t*reject_reasons |= REJECT_NON_FF_HEAD;\n-\t\t\telse\n+\t\t\t\tif (local_ref_used_to_be_at(head, &ref->old_oid))\n+\t\t\t\t\t*reject_reasons |= REJECT_NON_FF_HEAD_REWRITE;\n+\t\t\t} else\n \t\t\t\t*reject_reasons |= REJECT_NON_FF_OTHER;\n \t\t} else if (ref->status == REF_STATUS_REJECT_ALREADY_EXISTS) {\n \t\t\t*reject_reasons |= REJECT_ALREADY_EXISTS;\ndiff --git a/transport.h b/transport.h\nindex 39193d0077..87944fcd92 100644\n--- a/transport.h\n+++ b/transport.h\n@@ -252,12 +252,13 @@ int transport_set_option(struct transport *transport, const char *name,\n void transport_set_verbosity(struct transport *transport, int verbosity,\n \tint force_progress);\n \n-#define REJECT_NON_FF_HEAD      0x01\n-#define REJECT_NON_FF_OTHER     0x02\n-#define REJECT_ALREADY_EXISTS   0x04\n-#define REJECT_FETCH_FIRST      0x08\n-#define REJECT_NEEDS_FORCE      0x10\n-#define REJECT_REF_NEEDS_UPDATE 0x20\n+#define REJECT_NON_FF_HEAD         0x01\n+#define REJECT_NON_FF_OTHER        0x02\n+#define REJECT_ALREADY_EXISTS      0x04\n+#define REJECT_FETCH_FIRST         0x08\n+#define REJECT_NEEDS_FORCE         0x10\n+#define REJECT_REF_NEEDS_UPDATE    0x20\n+#define REJECT_NON_FF_HEAD_REWRITE 0x40\n \n int transport_push(struct repository *repo,\n \t\t   struct transport *connection,\n-- \ngitgitgadget\n\n\n"},{"id":"554247","messageId":"78571ebf5fb578852f8bae35fbf7cb3eb3a0bd46.1791270504.git.gitgitgadget@gmail.com","threadId":"66470","inReplyTo":"pull.2437.git.git.1791270504.gitgitgadget@gmail.com","subject":"[PATCH 6/6] push: suggest a force push after a clean rebase","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-06T07:08:24Z","receivedAt":"2026-10-06T07:08:24Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAfter rebasing pushed work onto a newer upstream without changing it,\n\"git status\" says the branch was rebased cleanly, but a rejected\n\"git push\" still suggests pulling from the push branch, which would\nonly bring back old copies of the same commits.\n\nWhen the commits outside the upstream carry the same changes on both\nsides, say what \"git status\" says and suggest only the force push:\n\n  hint: Updates were rejected because 'origin/topic' has diverged\n  hint: from your current branch, which was rebased cleanly on 'upstream/main'.\n  hint: Use 'git push --force-with-lease origin topic' to replace it.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n builtin/push.c           | 20 +++++++++++++++++---\n remote.c                 | 17 +++++++++++++++++\n remote.h                 |  6 ++++++\n t/t6040-tracking-info.sh | 25 +++++++++++++++++++++++++\n 4 files changed, 65 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/push.c b/builtin/push.c\nindex d918723d43..bf6e5386a0 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -301,6 +301,11 @@ static const char message_advice_pull_from_branch_before_push[] =\n \t   \"from your current branch. Use 'git pull %s %s'\\n\"\n \t   \"to integrate the remote changes.\");\n \n+static const char message_advice_force_after_clean_rebase[] =\n+\tN_(\"Updates were rejected because '%s' has diverged\\n\"\n+\t   \"from your current branch, which was rebased cleanly on '%s'.\\n\"\n+\t   \"Use 'git push --force-with-lease %s %s' to replace it.\");\n+\n static const char message_advice_pull_or_force_before_push[] =\n \tN_(\"Updates were rejected because '%s' has diverged\\n\"\n \t   \"from your current branch. Use 'git pull %s %s'\\n\"\n@@ -356,15 +361,24 @@ static void advise_pull_before_push(struct remote *push_remote,\n \t\ttracking_name = refs_shorten_unambiguous_ref(\n \t\t\tget_main_ref_store(the_repository), tracking, 0);\n \n-\tif (tracking && (reject_reasons & REJECT_NON_FF_HEAD_REWRITE))\n+\tif (tracking && branch_rebased_cleanly(branch, tracking)) {\n+\t\tchar *upstream_name = refs_shorten_unambiguous_ref(\n+\t\t\tget_main_ref_store(the_repository), upstream, 0);\n+\n+\t\tadvise(_(message_advice_force_after_clean_rebase),\n+\t\t       tracking_name, upstream_name,\n+\t\t       remote->name, branch->name);\n+\t\tfree(upstream_name);\n+\t} else if (tracking && (reject_reasons & REJECT_NON_FF_HEAD_REWRITE)) {\n \t\tadvise(_(message_advice_pull_or_force_before_push),\n \t\t       tracking_name, remote->name, branch->name,\n \t\t       remote->name, branch->name);\n-\telse if (tracking && (!upstream || strcmp(tracking, upstream)))\n+\t} else if (tracking && (!upstream || strcmp(tracking, upstream))) {\n \t\tadvise(_(message_advice_pull_from_branch_before_push),\n \t\t       tracking_name, remote->name, branch->name);\n-\telse\n+\t} else {\n \t\tadvise(_(message_advice_pull_before_push));\n+\t}\n \n \tfree(tracking_name);\n \tfree(tracking);\ndiff --git a/remote.c b/remote.c\nindex af8d026073..623ff98a01 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2391,6 +2391,23 @@ static bool stat_outside_upstream(const char *branch_name, const char *base,\n \treturn true;\n }\n \n+bool branch_rebased_cleanly(struct branch *branch, const char *base)\n+{\n+\tconst char *upstream = branch_get_upstream(branch, NULL);\n+\tint ours, theirs, ours_unmerged, theirs_unmerged;\n+\tbool same_changes = false;\n+\n+\tif (!upstream || !strcmp(upstream, base))\n+\t\treturn false;\n+\tif (stat_branch_pair(branch->refname, base, NULL, &ours, &theirs,\n+\t\t\t     NULL, AHEAD_BEHIND_FULL) <= 0 || !ours || !theirs)\n+\t\treturn false;\n+\treturn stat_outside_upstream(branch->refname, base, upstream,\n+\t\t\t\t     ours, theirs, &ours_unmerged,\n+\t\t\t\t     &theirs_unmerged, &same_changes) &&\n+\t       same_changes;\n+}\n+\n static char *resolve_compare_branch(struct branch *branch, const char *name)\n {\n \tconst char *resolved = NULL;\ndiff --git a/remote.h b/remote.h\nindex cca02033b9..dd8ef6c443 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -408,6 +408,12 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \t\t\t enum ahead_behind_flags abf,\n \t\t\t int show_divergence_advice);\n \n+/*\n+ * Return true when the branch has diverged from base only because the\n+ * work on base was rebased cleanly on the upstream of the branch.\n+ */\n+bool branch_rebased_cleanly(struct branch *branch, const char *base);\n+\n struct ref *get_local_heads(void);\n \n /*\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex b53034ba36..b5339e1452 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -804,6 +804,31 @@ test_expect_success 'status.compareBranches after a clean rebase of the push bra\n \tand have 3 and 1 different commits each (rebased cleanly on ${SQ}origin/main${SQ}).\n \t  (use \"git push --force-with-lease\" to publish your local commits)\n \n+\tnothing to commit, working tree clean\n+\tEOF\n+\ttest_cmp expect actual &&\n+\t(\n+\t\tcd test &&\n+\t\ttest_must_fail git push 2>../push.err &&\n+\t\tgit push --force-with-lease origin feature19 &&\n+\t\tgit status >../actual\n+\t) &&\n+\turl=$(git -C test config remote.origin.url) &&\n+\tcat >expect <<-EOF &&\n+\tTo $url\n+\t ! [rejected]        feature19 -> feature19 (non-fast-forward)\n+\terror: failed to push some refs to ${SQ}$url${SQ}\n+\thint: Updates were rejected because ${SQ}origin/feature19${SQ} has diverged\n+\thint: from your current branch, which was rebased cleanly on ${SQ}origin/main${SQ}.\n+\thint: Use ${SQ}git push --force-with-lease origin feature19${SQ} to replace it.\n+\tEOF\n+\ttest_cmp expect push.err &&\n+\tcat >expect <<-EOF &&\n+\tOn branch feature19\n+\tYour branch is ahead of ${SQ}origin/main${SQ} by 1 commit.\n+\n+\tYour branch is up to date with ${SQ}origin/feature19${SQ}.\n+\n \tnothing to commit, working tree clean\n \tEOF\n \ttest_cmp expect actual\n-- \ngitgitgadget\n\n"}]}