Skip to contentSkip to conversation

status, push: handle a push branch rebased onto a newer upstream

PATCH New Harald Nordgren via GitGitGadget 1 participant 7 messages Oct 6

lore
Share & tools
lore
#0 lore raw

cover status, push: handle a push branch rebased onto a newer upstream

git status now tells when the push branch only differs because your work was rebased onto a newer upstream.

With status.compareBranches including @{push}, when your branch and the push branch have diverged:

* commits from @{upstream} are counted apart: (1 and 1 not in 'upstream/main')
* if the rest carry the same changes on both sides: (rebased cleanly on 'upstream/main'), with a hint to git push --force-with-lease
* if nothing came from @{upstream}, the message is unchanged

When git push is rejected because the branch it updates has diverged:

* if someone else updated your upstream branch, the hint still says git pull
* if you amended or rebased commits you already pushed, the hint offers git pull and git push --force-with-lease
* if they were rebased cleanly, the hint offers git push --force-with-lease

Harald Nordgren (6):
status: count push divergence outside the upstream
status: say when the push branch was rebased cleanly
status: suggest a force push after a clean rebase
push: name the branch to pull from when it is not the upstream
push: offer a force push after rewriting pushed commits
push: suggest a force push after a clean rebase

 builtin/push.c           |  62 ++++++++++++++-
 remote.c                 | 132 +++++++++++++++++++++++++++----
 remote.h                 |   6 ++
 t/t6040-tracking-info.sh | 163 +++++++++++++++++++++++++++++++++++++++
 transport.c              |  42 +++++++++-
 transport.h              |  13 ++--
 6 files changed, 393 insertions(+), 25 deletions(-)

base-commit: 8103b446517e0c44e67561b9d0ccce56efa60a71
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2437%2FHaraldNordgren%2Fstatus-push-not-in-upstream-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2437/HaraldNordgren/status-push-not-in-upstream-v1
Pull-Request: https://github.com/git/git/pull/2437

Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/6 status: count push divergence outside the upstream

From: Harald Nordgren <[email protected]>

After rebasing onto a newer upstream, "git status" counts every commit the upstream gained since the last push as a difference from the push branch when status.compareBranches includes "@{push}". That can be many commits, even when only a few of your own differ.

Keep the full counts, and when they include commits from the upstream, add how many of them are not in it:

Your branch and 'origin/topic' have diverged,
and have 51 and 1 different commits each (1 and 1 not in 'upstream/main').

---
 remote.c                 | 81 +++++++++++++++++++++++++++++++++-------
 t/t6040-tracking-info.sh | 32 ++++++++++++++++
 2 files changed, 100 insertions(+), 13 deletions(-)
remote.c +68 -13
index fe62068463..cbe79275f7 100644
@@ -2247,11 +2247,12 @@ int resolve_remote_symref(struct ref *ref, struct ref *list)
  */
 
 static int stat_branch_pair(const char *branch_name, const char *base,
+			     const char *exclude,
 			     int *num_ours, int *num_theirs,
 			     enum ahead_behind_flags abf)
 {
 	struct object_id oid;
-	struct commit *ours, *theirs;
+	struct commit *ours, *theirs, *excluded = NULL;
 	struct rev_info revs;
 	struct strvec argv = STRVEC_INIT;
 
@@ -2268,6 +2269,14 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 	if (!ours)
 		return -1;
 
+	if (exclude) {
+		if (refs_read_ref(get_main_ref_store(the_repository), exclude, &oid))
+			return -1;
+		excluded = lookup_commit_reference(the_repository, &oid);
+		if (!excluded)
+			return -1;
+	}
+
 	*num_theirs = *num_ours = 0;
 
 	/* are we the same? */
@@ -2284,6 +2293,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 	strvec_pushf(&argv, "%s...%s",
 		     oid_to_hex(&ours->object.oid),
 		     oid_to_hex(&theirs->object.oid));
+	if (excluded)
+		strvec_pushf(&argv, "^%s", oid_to_hex(&excluded->object.oid));
 	strvec_push(&argv, "--");
 
 	repo_init_revisions(the_repository, &revs, NULL);
@@ -2305,6 +2316,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 	/* clear object flags smudged by the above traversal */
 	clear_commit_marks(ours, ALL_REV_FLAGS);
 	clear_commit_marks(theirs, ALL_REV_FLAGS);
+	if (excluded)
+		clear_commit_marks(excluded, ALL_REV_FLAGS);
 
 	strvec_clear(&argv);
 	release_revisions(&revs);
@@ -2344,7 +2357,23 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	if (!base)
 		return -1;
 
-	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
+	return stat_branch_pair(branch->refname, base, NULL,
+				num_ours, num_theirs, abf);
+}
+
+/*
+ * Count the commits that differ between branch_name and base but are not
+ * in upstream. Return false when they cannot be counted or upstream
+ * accounts for none of the ours and theirs commits.
+ */
+static bool stat_outside_upstream(const char *branch_name, const char *base,
+				  const char *upstream, int ours, int theirs,
+				  int *ours_unmerged, int *theirs_unmerged)
+{
+	if (stat_branch_pair(branch_name, base, upstream, ours_unmerged,
+			     theirs_unmerged, AHEAD_BEHIND_FULL) < 0)
+		return false;
+	return *ours_unmerged != ours || *theirs_unmerged != theirs;
 }
 
 static char *resolve_compare_branch(struct branch *branch, const char *name)
@@ -2376,6 +2405,8 @@ static void format_branch_comparison(struct strbuf *sb,
 				     const char *branch_name,
 				     const char *push_remote_name,
 				     const char *push_branch_name,
+				     const char *upstream_name,
+				     int ours_unmerged, int theirs_unmerged,
 				     enum ahead_behind_flags abf,
 				     unsigned flags)
 {
@@ -2421,15 +2452,27 @@ static void format_branch_comparison(struct strbuf *sb,
 					_("  (use \"git pull\" to update your local branch)\n"));
 		}
 	} else {
-		strbuf_addf(sb,
-			Q_("Your branch and '%s' have diverged,\n"
-			       "and have %d and %d different commit each, "
-			       "respectively.\n",
-			   "Your branch and '%s' have diverged,\n"
-			       "and have %d and %d different commits each, "
-			       "respectively.\n",
-			   ours + theirs),
-			branch_name, ours, theirs);
+		if (upstream_name)
+			strbuf_addf(sb,
+				Q_("Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commit each "
+				       "(%d and %d not in '%s').\n",
+				   "Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commits each "
+				       "(%d and %d not in '%s').\n",
+				   ours + theirs),
+				branch_name, ours, theirs,
+				ours_unmerged, theirs_unmerged, upstream_name);
+		else
+			strbuf_addf(sb,
+				Q_("Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commit each, "
+				       "respectively.\n",
+				   "Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commits each, "
+				       "respectively.\n",
+				   ours + theirs),
+				branch_name, ours, theirs);
 		if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS)) {
 			if (push_remote_name && push_branch_name)
 				strbuf_addf(sb,
@@ -2473,7 +2516,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	for (i = 0; i < branches.nr; i++) {
 		char *full_ref;
 		char *short_ref;
+		char *upstream_name = NULL;
 		int ours, theirs, cmp;
+		int ours_unmerged = 0, theirs_unmerged = 0;
 		int is_upstream, is_push;
 		unsigned flags = 0;
 		const char *push_remote_name = NULL;
@@ -2498,9 +2543,17 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		if (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))
 			is_push = 1;
 
-		cmp = stat_branch_pair(branch->refname, full_ref,
+		cmp = stat_branch_pair(branch->refname, full_ref, NULL,
 				       &ours, &theirs, abf);
 
+		if (cmp > 0 && ours && theirs && upstream_ref && !is_upstream &&
+		    stat_outside_upstream(branch->refname, full_ref,
+					  upstream_ref, ours, theirs,
+					  &ours_unmerged, &theirs_unmerged))
+			upstream_name = refs_shorten_unambiguous_ref(
+				get_main_ref_store(the_repository),
+				upstream_ref, 0);
+
 		if (cmp < 0) {
 			if (is_upstream) {
 				strbuf_addf(sb,
@@ -2542,11 +2595,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		}
 		format_branch_comparison(sb, !cmp, ours, theirs, short_ref,
 					 push_remote_name, push_branch_name,
-					 abf, flags);
+					 upstream_name, ours_unmerged,
+					 theirs_unmerged, abf, flags);
 		reported = 1;
 
 		free(full_ref);
 		free(short_ref);
+		free(upstream_name);
 	}
 
 	string_list_clear(&branches, 0);
t/t6040-tracking-info.sh +32 -0
index e95d420972..f8df16a5de 100755
@@ -746,4 +746,36 @@ test_expect_success 'status.compareBranches suppresses advice when push tracking
 	test_cmp expect actual
 '
 
+test_expect_success 'status.compareBranches counts push divergence outside upstream' '
+	test_config -C test push.default current &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	(
+		cd test &&
+		git checkout -b feature18 origin/main &&
+		advance work18 &&
+		git push
+	) &&
+	git checkout main &&
+	advance main18a &&
+	advance main18b &&
+	git checkout - &&
+	(
+		cd test &&
+		echo amended >work18 &&
+		git commit -a --amend --no-edit &&
+		git pull --rebase &&
+		git status >../actual
+	) &&
+	cat >expect <<-EOF &&
+	On branch feature18
+	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+
+	Your branch and ${SQ}origin/feature18${SQ} have diverged,
+	and have 3 and 1 different commits each (1 and 1 not in ${SQ}origin/main${SQ}).
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#2 lore raw

PATCH 2/6 status: say when the push branch was rebased cleanly

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

After rebasing pushed work onto a newer upstream without changing it, "git status" reports the commits outside the upstream on both sides, as if the branch and its push branch held different work.

When those commits carry the same changes on both sides, say that the branch was rebased cleanly:

Your branch and 'origin/topic' have diverged,
and have 51 and 1 different commits each (rebased cleanly on 'upstream/main').

---
 remote.c                 | 49 ++++++++++++++++++++++++++++++++--------
 t/t6040-tracking-info.sh | 30 ++++++++++++++++++++++++
 2 files changed, 69 insertions(+), 10 deletions(-)
remote.c +39 -10
index cbe79275f7..89d142cc7e 100644
@@ -2248,7 +2248,7 @@ int resolve_remote_symref(struct ref *ref, struct ref *list)
 
 static int stat_branch_pair(const char *branch_name, const char *base,
 			     const char *exclude,
-			     int *num_ours, int *num_theirs,
+			     int *num_ours, int *num_theirs, int *num_same,
 			     enum ahead_behind_flags abf)
 {
 	struct object_id oid;
@@ -2278,6 +2278,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 	}
 
 	*num_theirs = *num_ours = 0;
+	if (num_same)
+		*num_same = 0;
 
 	/* are we the same? */
 	if (theirs == ours)
@@ -2290,6 +2292,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 	/* Run "rev-list --left-right ours...theirs" internally... */
 	strvec_push(&argv, ""); /* ignored */
 	strvec_push(&argv, "--left-right");
+	if (num_same)
+		strvec_push(&argv, "--cherry-mark");
 	strvec_pushf(&argv, "%s...%s",
 		     oid_to_hex(&ours->object.oid),
 		     oid_to_hex(&theirs->object.oid));
@@ -2311,6 +2315,8 @@ static int stat_branch_pair(const char *branch_name, const char *base,
 			(*num_ours)++;
 		else
 			(*num_theirs)++;
+		if (num_same && (c->object.flags & PATCHSAME))
+			(*num_same)++;
 	}
 
 	/* clear object flags smudged by the above traversal */
@@ -2358,22 +2364,31 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 		return -1;
 
 	return stat_branch_pair(branch->refname, base, NULL,
-				num_ours, num_theirs, abf);
+				num_ours, num_theirs, NULL, abf);
 }
 
 /*
  * Count the commits that differ between branch_name and base but are not
  * in upstream. Return false when they cannot be counted or upstream
- * accounts for none of the ours and theirs commits.
+ * accounts for none of the ours and theirs commits. Otherwise set
+ * *same_changes when the remaining commits carry the same changes on
+ * both sides.
  */
 static bool stat_outside_upstream(const char *branch_name, const char *base,
 				  const char *upstream, int ours, int theirs,
-				  int *ours_unmerged, int *theirs_unmerged)
+				  int *ours_unmerged, int *theirs_unmerged,
+				  bool *same_changes)
 {
+	int same;
+
 	if (stat_branch_pair(branch_name, base, upstream, ours_unmerged,
-			     theirs_unmerged, AHEAD_BEHIND_FULL) < 0)
+			     theirs_unmerged, &same, AHEAD_BEHIND_FULL) < 0)
+		return false;
+	if (*ours_unmerged == ours && *theirs_unmerged == theirs)
 		return false;
-	return *ours_unmerged != ours || *theirs_unmerged != theirs;
+	*same_changes = *ours_unmerged &&
+		same == *ours_unmerged + *theirs_unmerged;
+	return true;
 }
 
 static char *resolve_compare_branch(struct branch *branch, const char *name)
@@ -2407,6 +2422,7 @@ static void format_branch_comparison(struct strbuf *sb,
 				     const char *push_branch_name,
 				     const char *upstream_name,
 				     int ours_unmerged, int theirs_unmerged,
+				     bool same_changes,
 				     enum ahead_behind_flags abf,
 				     unsigned flags)
 {
@@ -2452,7 +2468,17 @@ static void format_branch_comparison(struct strbuf *sb,
 					_("  (use \"git pull\" to update your local branch)\n"));
 		}
 	} else {
-		if (upstream_name)
+		if (same_changes)
+			strbuf_addf(sb,
+				Q_("Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commit each "
+				       "(rebased cleanly on '%s').\n",
+				   "Your branch and '%s' have diverged,\n"
+				       "and have %d and %d different commits each "
+				       "(rebased cleanly on '%s').\n",
+				   ours + theirs),
+				branch_name, ours, theirs, upstream_name);
+		else if (upstream_name)
 			strbuf_addf(sb,
 				Q_("Your branch and '%s' have diverged,\n"
 				       "and have %d and %d different commit each "
@@ -2519,6 +2545,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		char *upstream_name = NULL;
 		int ours, theirs, cmp;
 		int ours_unmerged = 0, theirs_unmerged = 0;
+		bool same_changes = false;
 		int is_upstream, is_push;
 		unsigned flags = 0;
 		const char *push_remote_name = NULL;
@@ -2544,12 +2571,13 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			is_push = 1;
 
 		cmp = stat_branch_pair(branch->refname, full_ref, NULL,
-				       &ours, &theirs, abf);
+				       &ours, &theirs, NULL, abf);
 
 		if (cmp > 0 && ours && theirs && upstream_ref && !is_upstream &&
 		    stat_outside_upstream(branch->refname, full_ref,
 					  upstream_ref, ours, theirs,
-					  &ours_unmerged, &theirs_unmerged))
+					  &ours_unmerged, &theirs_unmerged,
+					  &same_changes))
 			upstream_name = refs_shorten_unambiguous_ref(
 				get_main_ref_store(the_repository),
 				upstream_ref, 0);
@@ -2596,7 +2624,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		format_branch_comparison(sb, !cmp, ours, theirs, short_ref,
 					 push_remote_name, push_branch_name,
 					 upstream_name, ours_unmerged,
-					 theirs_unmerged, abf, flags);
+					 theirs_unmerged, same_changes,
+					 abf, flags);
 		reported = 1;
 
 		free(full_ref);
t/t6040-tracking-info.sh +30 -0
index f8df16a5de..2ebc3573da 100755
@@ -778,4 +778,34 @@ test_expect_success 'status.compareBranches counts push divergence outside upstr
 	test_cmp expect actual
 '
 
+test_expect_success 'status.compareBranches after a clean rebase of the push branch' '
+	test_config -C test push.default current &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	(
+		cd test &&
+		git checkout -b feature19 origin/main &&
+		advance work19 &&
+		git push
+	) &&
+	git checkout main &&
+	advance main19a &&
+	advance main19b &&
+	git checkout - &&
+	(
+		cd test &&
+		git pull --rebase &&
+		git status >../actual
+	) &&
+	cat >expect <<-EOF &&
+	On branch feature19
+	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+
+	Your branch and ${SQ}origin/feature19${SQ} have diverged,
+	and have 3 and 1 different commits each (rebased cleanly on ${SQ}origin/main${SQ}).
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#3 lore raw

PATCH 3/6 status: suggest a force push after a clean rebase

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

After rebasing pushed work onto a newer upstream without changing it, "git status" says the branch was rebased cleanly but gives no hint on how to publish it. A plain "git push" is rejected, so suggest a force push:

Your branch and 'origin/topic' have diverged,
and have 51 and 1 different commits each (rebased cleanly on 'upstream/main').
(use "git push --force-with-lease" to publish your local commits)

The lease still stops the push if someone else updated the push branch since it was last fetched.

---
 remote.c                 | 7 +++++--
 t/t6040-tracking-info.sh | 1 +
 2 files changed, 6 insertions(+), 2 deletions(-)
remote.c +5 -2
index 89d142cc7e..af8d026073 100644
@@ -2468,7 +2468,7 @@ static void format_branch_comparison(struct strbuf *sb,
 					_("  (use \"git pull\" to update your local branch)\n"));
 		}
 	} else {
-		if (same_changes)
+		if (same_changes) {
 			strbuf_addf(sb,
 				Q_("Your branch and '%s' have diverged,\n"
 				       "and have %d and %d different commit each "
@@ -2478,7 +2478,10 @@ static void format_branch_comparison(struct strbuf *sb,
 				       "(rebased cleanly on '%s').\n",
 				   ours + theirs),
 				branch_name, ours, theirs, upstream_name);
-		else if (upstream_name)
+			if (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))
+				strbuf_addstr(sb,
+					_("  (use \"git push --force-with-lease\" to publish your local commits)\n"));
+		} else if (upstream_name)
 			strbuf_addf(sb,
 				Q_("Your branch and '%s' have diverged,\n"
 				       "and have %d and %d different commit each "
t/t6040-tracking-info.sh +1 -0
index 2ebc3573da..4074c6663a 100755
@@ -802,6 +802,7 @@ test_expect_success 'status.compareBranches after a clean rebase of the push bra
 
 	Your branch and ${SQ}origin/feature19${SQ} have diverged,
 	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
Signature · 1 line
gitgitgadget
#4 lore raw

PATCH 4/6 push: name the branch to pull from when it is not the upstream

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

When someone else pushed to the branch you push to, such as a topic branch on your fork, "git push" is rejected with a hint to run "git pull" first. If that branch is not your upstream, the pull merges the upstream and leaves the push branch diverged, so the push is rejected again.

Name the branch that diverged and suggest pulling from it:

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.

---
 builtin/push.c           | 37 +++++++++++++++++++++++---
 t/t6040-tracking-info.sh | 57 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 91 insertions(+), 3 deletions(-)
builtin/push.c +34 -3
index 2377b5af55..82435beab5 100644
@@ -12,6 +12,7 @@
 #include "environment.h"
 #include "gettext.h"
 #include "hex.h"
+#include "refs.h"
 #include "refspec.h"
 #include "run-command.h"
 #include "remote.h"
@@ -295,6 +296,11 @@ static const char message_advice_pull_before_push[] =
 	   "use 'git pull' before pushing again.\n"
 	   "See the 'Note about fast-forwards' in 'git push --help' for details.");
 
+static const char message_advice_pull_from_branch_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.");
+
 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"
@@ -322,11 +328,35 @@ 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(void)
+static void advise_pull_before_push(struct remote *push_remote)
 {
+	struct branch *branch = branch_get(NULL);
+	struct remote *remote = NULL;
+	const char *upstream = NULL;
+	char *tracking = NULL;
+	char *tracking_name = NULL;
+
 	if (!advice_enabled(ADVICE_PUSH_NON_FF_CURRENT) || !advice_enabled(ADVICE_PUSH_UPDATE_REJECTED))
 		return;
-	advise(_(message_advice_pull_before_push));
+
+	if (branch) {
+		remote = repo_remote_for_push_tracking(the_repository,
+						       push_remote);
+		tracking = apply_refspecs(&remote->fetch, branch->refname);
+		upstream = branch_get_upstream(branch, NULL);
+	}
+	if (tracking)
+		tracking_name = refs_shorten_unambiguous_ref(
+			get_main_ref_store(the_repository), tracking, 0);
+
+	if (tracking && (!upstream || strcmp(tracking, upstream)))
+		advise(_(message_advice_pull_from_branch_before_push),
+		       tracking_name, remote->name, branch->name);
+	else
+		advise(_(message_advice_pull_before_push));
+
+	free(tracking_name);
+	free(tracking);
 }
 
 static void advise_checkout_pull_push(void)
@@ -370,6 +400,7 @@ static int push_with_options(struct transport *transport, struct refspec *rs,
 	int err;
 	unsigned int reject_reasons;
 	char *anon_url = transport_anonymize_url(transport->url);
+	struct remote *remote = transport->remote;
 
 	transport_set_verbosity(transport, verbosity, progress);
 	transport->family = family;
@@ -404,7 +435,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();
+		advise_pull_before_push(remote);
 	} else if (reject_reasons & REJECT_NON_FF_OTHER) {
 		advise_checkout_pull_push();
 	} else if (reject_reasons & REJECT_ALREADY_EXISTS) {
t/t6040-tracking-info.sh +57 -0

61 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#5 lore raw

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

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

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'.

---
 builtin/push.c           | 17 +++++++++++++---
 t/t6040-tracking-info.sh | 18 +++++++++++++++++
 transport.c              | 42 ++++++++++++++++++++++++++++++++++++++--
 transport.h              | 13 +++++++------
 4 files changed, 79 insertions(+), 11 deletions(-)
builtin/push.c +14 -3
index 82435beab5..d918723d43 100644
@@ -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) {
t/t6040-tracking-info.sh +18 -0
index 9eb810e158..b53034ba36 100755
@@ -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
 '
 
transport.c +40 -2

55 lines not on this page · raw message

transport.h +7 -6
index 39193d0077..87944fcd92 100644
@@ -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,
Signature · 1 line
gitgitgadget
#6 lore raw

PATCH 6/6 push: suggest a force push after a clean rebase

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

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.

---
 builtin/push.c           | 20 +++++++++++++++++---
 remote.c                 | 17 +++++++++++++++++
 remote.h                 |  6 ++++++
 t/t6040-tracking-info.sh | 25 +++++++++++++++++++++++++
 4 files changed, 65 insertions(+), 3 deletions(-)
builtin/push.c +17 -3
index d918723d43..bf6e5386a0 100644
@@ -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);
remote.c +17 -0
index af8d026073..623ff98a01 100644
@@ -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;
remote.h +6 -0
index cca02033b9..dd8ef6c443 100644
@@ -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);
 
 /*
t/t6040-tracking-info.sh +25 -0
index b53034ba36..b5339e1452 100755
@@ -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
Signature · 1 line
gitgitgadget