Skip to contentSkip to conversation

status: show default branch comparison when tracking non-default branch

PATCH Superseded Harald Nordgren via GitGitGadget 12 participants 259 messages Dec 23, 2025 – Mar 9 Documentation Page 2 of 2

lore
Share & tools
lore
#134 lore raw

PATCH v20 2/2 status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

"git status" on a branch that follows a remote branch compares commits on the current branch and the remote-tracking branch it builds upon, to show "ahead", "behind", or "diverged" status.

When working on a feature branch that tracks a remote feature branch, but you also want to track progress relative to the push destination tracking branch (which may differ from the upstream branch), git status now shows an additional comparison.

When the upstream tracking branch differs from the push destination tracking branch, git status shows both the comparison with the upstream tracking branch (as before) and an additional comparison with the push destination tracking branch. The push branch comparison appears on a separate line after the upstream branch status, using the same format.

Example output when tracking origin/main but push destination is origin/feature:
On branch feature
Your branch and 'origin/main' have diverged,
and have 3 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)

Your branch is ahead of 'origin/feature' by 1 commit.
(use "git push" to publish your local commits)

The comparison is only shown when the push destination tracking branch differs from the upstream tracking branch, even if they are on the same remote.

---
 remote.c                 | 102 ++++++++++++++-
 t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 358 insertions(+), 6 deletions(-)
remote.c +96 -6
index b053d4e443..dd8f94fa23 100644
@@ -29,6 +29,11 @@
 
 enum map_direction { FROM_SRC, FROM_DST };
 
+enum {
+	BRANCH_MODE_PULL = (1 << 0),
+	BRANCH_MODE_PUSH = (1 << 1),
+};
+
 struct counted_string {
 	size_t len;
 	const char *s;
@@ -2237,13 +2242,75 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)
+{
+	struct remote *remote;
+	const char *push_remote;
+	char *push_dst = NULL;
+	char *tracking_ref;
+	const char *resolved;
+	char *ret;
+
+	if (!branch)
+		return NULL;
+
+	push_remote = pushremote_for_branch(branch, NULL);
+	if (!push_remote)
+		return NULL;
+
+	remote = remotes_remote_get(the_repository, push_remote);
+	if (!remote)
+		return NULL;
+
+	push_dst = remote_ref_for_branch(branch, 1);
+	if (!push_dst) {
+		if (remote->push.nr)
+			return NULL;
+		push_dst = xstrdup(branch->refname);
+	}
+
+	tracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);
+	free(push_dst);
+
+	if (!tracking_ref)
+		return NULL;
+
+	resolved = refs_resolve_ref_unsafe(
+		get_main_ref_store(the_repository),
+		tracking_ref,
+		RESOLVE_REF_READING,
+		NULL, NULL);
+
+	if (!resolved) {
+		free(tracking_ref);
+		return NULL;
+	}
+
+	if (full_ref_out)
+		*full_ref_out = xstrdup(resolved);
+
+	ret = refs_shorten_unambiguous_ref(
+		get_main_ref_store(the_repository), resolved, 0);
+	free(tracking_ref);
+	return ret;
+}
+
 static void format_branch_comparison(struct strbuf *sb,
 				     bool up_to_date,
 				     int ours, int theirs,
 				     const char *branch_name,
 				     enum ahead_behind_flags abf,
+				     unsigned flags,
 				     bool show_divergence_advice)
 {
+	bool want_push_advice = (flags & BRANCH_MODE_PUSH) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool want_pull_advice = (flags & BRANCH_MODE_PULL) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool want_divergence_advice = (flags & BRANCH_MODE_PULL) &&
+		show_divergence_advice &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+
 	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
@@ -2252,7 +2319,7 @@ static void format_branch_comparison(struct strbuf *sb,
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
 			    branch_name);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_push_advice)
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
 	} else if (!theirs) {
@@ -2261,7 +2328,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
 			branch_name, ours);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_push_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
 	} else if (!ours) {
@@ -2272,7 +2339,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "and can be fast-forwarded.\n",
 			   theirs),
 			branch_name, theirs);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_pull_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
 	} else {
@@ -2280,8 +2347,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			"Your branch and '%s' have diverged,\n"
 			       "and have %d and %d different commits each, respectively.\n",
 			branch_name, ours, theirs);
-		if (show_divergence_advice &&
-		    advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_divergence_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
@@ -2298,6 +2364,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	const char *full_base;
 	char *base;
 	int upstream_is_gone = 0;
+	unsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;
+	int push_ours, push_theirs, push_sti;
+	char *full_push = NULL;
+	char *push = NULL;
+	unsigned push_branch_modes = 0;
 
 	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
 	if (sti < 0) {
@@ -2309,6 +2380,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
 					    full_base, 0);
 
+	push = get_remote_push_branch(branch, &full_push);
+	if (push && strcmp(base, push)) {
+		push_sti = stat_branch_pair(branch->refname, full_push,
+					   &push_ours, &push_theirs, abf);
+		if (push_sti >= 0) {
+			base_branch_modes = BRANCH_MODE_PULL;
+			push_branch_modes = BRANCH_MODE_PUSH;
+		}
+	}
+
 	if (upstream_is_gone) {
 		strbuf_addf(sb,
 			_("Your branch is based on '%s', but the upstream is gone.\n"),
@@ -2317,10 +2398,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			strbuf_addstr(sb,
 				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
 	} else {
-		format_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);
+		format_branch_comparison(sb, !sti, ours, theirs, base, abf,
+					 base_branch_modes, show_divergence_advice);
+	}
+
+	if (push_branch_modes & BRANCH_MODE_PUSH) {
+		strbuf_addstr(sb, "\n");
+		format_branch_comparison(sb, !push_sti, push_ours, push_theirs, push, abf,
+					 push_branch_modes, show_divergence_advice);
 	}
 
 	free(base);
+	free(full_push);
+	free(push);
 	return 1;
 }
 
t/t6040-tracking-info.sh +262 -0

266 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#135 lore raw

v21 cover status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: show comparison with push remote tracking branch

 remote.c                 | 183 ++++++++++++++++++++-------
 t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 403 insertions(+), 42 deletions(-)

base-commit: d529f3a197364881746f558e5652f0236131eb86
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v21
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v21
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v20:

1: bb3e00863b ! 1: ce1f1eebb5 refactor format_branch_comparison in preparation

     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
      +			 enum ahead_behind_flags abf,
      +			 int show_divergence_advice)
      +{
     -+	int ours, theirs, sti;
     ++	int ours, theirs, cmp_fetch;
      +	const char *full_base;
      +	char *base;
      +	int upstream_is_gone = 0;
      +
     -+	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
     -+	if (sti < 0) {
     ++	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
     ++	if (cmp_fetch < 0) {
      +		if (!full_base)
      +			return 0;
      +		upstream_is_gone = 1;
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
      +			strbuf_addstr(sb,
      +				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
      +	} else {
     -+		format_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);
     ++		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
      +	}
      +
       	free(base);
 2:  050197eac3 ! 2:  51d8486fe0 status: show comparison with push remote tracking branch
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
       	char *base;
       	int upstream_is_gone = 0;
      +	unsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;
     -+	int push_ours, push_theirs, push_sti;
     ++	int push_ours, push_theirs, push_cmp_fetch;
      +	char *full_push = NULL;
      +	char *push = NULL;
      +	unsigned push_branch_modes = 0;
       
     - 	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
     - 	if (sti < 0) {
     + 	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
     + 	if (cmp_fetch < 0) {
      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
       	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
       					    full_base, 0);
       
      +	push = get_remote_push_branch(branch, &full_push);
      +	if (push && strcmp(base, push)) {
     -+		push_sti = stat_branch_pair(branch->refname, full_push,
     ++		push_cmp_fetch = stat_branch_pair(branch->refname, full_push,
      +					   &push_ours, &push_theirs, abf);
     -+		if (push_sti >= 0) {
     ++		if (push_cmp_fetch >= 0) {
      +			base_branch_modes = BRANCH_MODE_PULL;
      +			push_branch_modes = BRANCH_MODE_PUSH;
      +		}
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
       			strbuf_addstr(sb,
       				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
       	} else {
     --		format_branch_comparison(sb, !sti, ours, theirs, base, abf, show_divergence_advice);
     -+		format_branch_comparison(sb, !sti, ours, theirs, base, abf,
     +-		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
     ++		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,
      +					 base_branch_modes, show_divergence_advice);
      +	}
      +
      +	if (push_branch_modes & BRANCH_MODE_PUSH) {
      +		strbuf_addstr(sb, "\n");
     -+		format_branch_comparison(sb, !push_sti, push_ours, push_theirs, push, abf,
     ++		format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,
      +					 push_branch_modes, show_divergence_advice);
       	}
Signature · 1 line
gitgitgadget
#136 lore raw

PATCH v21 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 91 +++++++++++++++++++++++++++++++-------------------------
 1 file changed, 50 insertions(+), 41 deletions(-)
remote.c +50 -41
index 59b3715120..af078817a4 100644
@@ -2237,42 +2237,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2281,7 +2260,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2292,25 +2271,55 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(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),
-			base, ours, theirs);
+			"Your branch and '%s' have diverged,\n"
+			       "and have %d and %d different commits each, respectively.\n",
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#137 lore raw

PATCH v21 2/2 status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

"git status" on a branch that follows a remote branch compares commits on the current branch and the remote-tracking branch it builds upon, to show "ahead", "behind", or "diverged" status.

When working on a feature branch that tracks a remote feature branch, but you also want to track progress relative to the push destination tracking branch (which may differ from the upstream branch), git status now shows an additional comparison.

When the upstream tracking branch differs from the push destination tracking branch, git status shows both the comparison with the upstream tracking branch (as before) and an additional comparison with the push destination tracking branch. The push branch comparison appears on a separate line after the upstream branch status, using the same format.

Example output when tracking origin/main but push destination is origin/feature:
On branch feature
Your branch and 'origin/main' have diverged,
and have 3 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)

Your branch is ahead of 'origin/feature' by 1 commit.
(use "git push" to publish your local commits)

The comparison is only shown when the push destination tracking branch differs from the upstream tracking branch, even if they are on the same remote.

---
 remote.c                 | 102 ++++++++++++++-
 t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 358 insertions(+), 6 deletions(-)
remote.c +96 -6
index af078817a4..40e4ebb3b7 100644
@@ -29,6 +29,11 @@
 
 enum map_direction { FROM_SRC, FROM_DST };
 
+enum {
+	BRANCH_MODE_PULL = (1 << 0),
+	BRANCH_MODE_PUSH = (1 << 1),
+};
+
 struct counted_string {
 	size_t len;
 	const char *s;
@@ -2237,13 +2242,75 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)
+{
+	struct remote *remote;
+	const char *push_remote;
+	char *push_dst = NULL;
+	char *tracking_ref;
+	const char *resolved;
+	char *ret;
+
+	if (!branch)
+		return NULL;
+
+	push_remote = pushremote_for_branch(branch, NULL);
+	if (!push_remote)
+		return NULL;
+
+	remote = remotes_remote_get(the_repository, push_remote);
+	if (!remote)
+		return NULL;
+
+	push_dst = remote_ref_for_branch(branch, 1);
+	if (!push_dst) {
+		if (remote->push.nr)
+			return NULL;
+		push_dst = xstrdup(branch->refname);
+	}
+
+	tracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);
+	free(push_dst);
+
+	if (!tracking_ref)
+		return NULL;
+
+	resolved = refs_resolve_ref_unsafe(
+		get_main_ref_store(the_repository),
+		tracking_ref,
+		RESOLVE_REF_READING,
+		NULL, NULL);
+
+	if (!resolved) {
+		free(tracking_ref);
+		return NULL;
+	}
+
+	if (full_ref_out)
+		*full_ref_out = xstrdup(resolved);
+
+	ret = refs_shorten_unambiguous_ref(
+		get_main_ref_store(the_repository), resolved, 0);
+	free(tracking_ref);
+	return ret;
+}
+
 static void format_branch_comparison(struct strbuf *sb,
 				     bool up_to_date,
 				     int ours, int theirs,
 				     const char *branch_name,
 				     enum ahead_behind_flags abf,
+				     unsigned flags,
 				     bool show_divergence_advice)
 {
+	bool want_push_advice = (flags & BRANCH_MODE_PUSH) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool want_pull_advice = (flags & BRANCH_MODE_PULL) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool want_divergence_advice = (flags & BRANCH_MODE_PULL) &&
+		show_divergence_advice &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+
 	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
@@ -2252,7 +2319,7 @@ static void format_branch_comparison(struct strbuf *sb,
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
 			    branch_name);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_push_advice)
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
 	} else if (!theirs) {
@@ -2261,7 +2328,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
 			branch_name, ours);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_push_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
 	} else if (!ours) {
@@ -2272,7 +2339,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "and can be fast-forwarded.\n",
 			   theirs),
 			branch_name, theirs);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_pull_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
 	} else {
@@ -2280,8 +2347,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			"Your branch and '%s' have diverged,\n"
 			       "and have %d and %d different commits each, respectively.\n",
 			branch_name, ours, theirs);
-		if (show_divergence_advice &&
-		    advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_divergence_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
@@ -2298,6 +2364,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	const char *full_base;
 	char *base;
 	int upstream_is_gone = 0;
+	unsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;
+	int push_ours, push_theirs, push_cmp_fetch;
+	char *full_push = NULL;
+	char *push = NULL;
+	unsigned push_branch_modes = 0;
 
 	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
 	if (cmp_fetch < 0) {
@@ -2309,6 +2380,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
 					    full_base, 0);
 
+	push = get_remote_push_branch(branch, &full_push);
+	if (push && strcmp(base, push)) {
+		push_cmp_fetch = stat_branch_pair(branch->refname, full_push,
+					   &push_ours, &push_theirs, abf);
+		if (push_cmp_fetch >= 0) {
+			base_branch_modes = BRANCH_MODE_PULL;
+			push_branch_modes = BRANCH_MODE_PUSH;
+		}
+	}
+
 	if (upstream_is_gone) {
 		strbuf_addf(sb,
 			_("Your branch is based on '%s', but the upstream is gone.\n"),
@@ -2317,10 +2398,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			strbuf_addstr(sb,
 				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
 	} else {
-		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,
+					 base_branch_modes, show_divergence_advice);
+	}
+
+	if (push_branch_modes & BRANCH_MODE_PUSH) {
+		strbuf_addstr(sb, "\n");
+		format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,
+					 push_branch_modes, show_divergence_advice);
 	}
 
 	free(base);
+	free(full_push);
+	free(push);
 	return 1;
 }
 
t/t6040-tracking-info.sh +262 -0

266 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#138 lore raw

v22 cover status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: show comparison with push remote tracking branch

 remote.c                 | 174 ++++++++++++++++++++------
 t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 401 insertions(+), 35 deletions(-)

base-commit: d529f3a197364881746f558e5652f0236131eb86
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v22
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v22
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v21:

1: ce1f1eebb5 ! 1: 4aa4f1abc8 refactor format_branch_comparison in preparation
@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
if (advice_enabled(ADVICE_STATUS_HINTS))
strbuf_addstr(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),
+@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
+ "and have %d and %d different commits each, "
+ "respectively.\n",
+ ours + theirs),
- base, ours, theirs);
-+ "Your branch and '%s' have diverged,\n"
-+ "and have %d and %d different commits each, respectively.\n",
+ branch_name, ours, theirs);
if (show_divergence_advice &&
advice_enabled(ADVICE_STATUS_HINTS))
2: 51d8486fe0 ! 2: b7e29887d9 status: show comparison with push remote tracking branch
@@ remote.c: static void format_branch_comparison(struct strbuf *sb,
_(" (use \"git pull\" to update your local branch)\n"));
} else {
@@ remote.c: static void format_branch_comparison(struct strbuf *sb,
- "Your branch and '%s' have diverged,\n"
- "and have %d and %d different commits each, respectively.\n",
+ "respectively.\n",
+ ours + theirs),
branch_name, ours, theirs);
- if (show_divergence_advice &&
- advice_enabled(ADVICE_STATUS_HINTS))

Signature · 1 line
gitgitgadget
#139 lore raw

PATCH v22 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 48 insertions(+), 34 deletions(-)
remote.c +48 -34
index 59b3715120..d5a6486026 100644
@@ -2237,42 +2237,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2281,7 +2260,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2292,7 +2271,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
@@ -2305,12 +2284,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			       "and have %d and %d different commits each, "
 			       "respectively.\n",
 			   ours + theirs),
-			base, ours, theirs);
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#140 lore raw

PATCH v22 2/2 status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

"git status" on a branch that follows a remote branch compares commits on the current branch and the remote-tracking branch it builds upon, to show "ahead", "behind", or "diverged" status.

When working on a feature branch that tracks a remote feature branch, but you also want to track progress relative to the push destination tracking branch (which may differ from the upstream branch), git status now shows an additional comparison.

When the upstream tracking branch differs from the push destination tracking branch, git status shows both the comparison with the upstream tracking branch (as before) and an additional comparison with the push destination tracking branch. The push branch comparison appears on a separate line after the upstream branch status, using the same format.

Example output when tracking origin/main but push destination is origin/feature:
On branch feature
Your branch and 'origin/main' have diverged,
and have 3 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)

Your branch is ahead of 'origin/feature' by 1 commit.
(use "git push" to publish your local commits)

The comparison is only shown when the push destination tracking branch differs from the upstream tracking branch, even if they are on the same remote.

---
 remote.c                 | 102 ++++++++++++++-
 t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 358 insertions(+), 6 deletions(-)
remote.c +96 -6
index d5a6486026..f9aa4c61bc 100644
@@ -29,6 +29,11 @@
 
 enum map_direction { FROM_SRC, FROM_DST };
 
+enum {
+	BRANCH_MODE_PULL = (1 << 0),
+	BRANCH_MODE_PUSH = (1 << 1),
+};
+
 struct counted_string {
 	size_t len;
 	const char *s;
@@ -2237,13 +2242,75 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)
+{
+	struct remote *remote;
+	const char *push_remote;
+	char *push_dst = NULL;
+	char *tracking_ref;
+	const char *resolved;
+	char *ret;
+
+	if (!branch)
+		return NULL;
+
+	push_remote = pushremote_for_branch(branch, NULL);
+	if (!push_remote)
+		return NULL;
+
+	remote = remotes_remote_get(the_repository, push_remote);
+	if (!remote)
+		return NULL;
+
+	push_dst = remote_ref_for_branch(branch, 1);
+	if (!push_dst) {
+		if (remote->push.nr)
+			return NULL;
+		push_dst = xstrdup(branch->refname);
+	}
+
+	tracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);
+	free(push_dst);
+
+	if (!tracking_ref)
+		return NULL;
+
+	resolved = refs_resolve_ref_unsafe(
+		get_main_ref_store(the_repository),
+		tracking_ref,
+		RESOLVE_REF_READING,
+		NULL, NULL);
+
+	if (!resolved) {
+		free(tracking_ref);
+		return NULL;
+	}
+
+	if (full_ref_out)
+		*full_ref_out = xstrdup(resolved);
+
+	ret = refs_shorten_unambiguous_ref(
+		get_main_ref_store(the_repository), resolved, 0);
+	free(tracking_ref);
+	return ret;
+}
+
 static void format_branch_comparison(struct strbuf *sb,
 				     bool up_to_date,
 				     int ours, int theirs,
 				     const char *branch_name,
 				     enum ahead_behind_flags abf,
+				     unsigned flags,
 				     bool show_divergence_advice)
 {
+	bool want_push_advice = (flags & BRANCH_MODE_PUSH) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool want_pull_advice = (flags & BRANCH_MODE_PULL) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool want_divergence_advice = (flags & BRANCH_MODE_PULL) &&
+		show_divergence_advice &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+
 	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
@@ -2252,7 +2319,7 @@ static void format_branch_comparison(struct strbuf *sb,
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
 			    branch_name);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_push_advice)
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
 	} else if (!theirs) {
@@ -2261,7 +2328,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
 			branch_name, ours);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_push_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
 	} else if (!ours) {
@@ -2272,7 +2339,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "and can be fast-forwarded.\n",
 			   theirs),
 			branch_name, theirs);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_pull_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
 	} else {
@@ -2285,8 +2352,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "respectively.\n",
 			   ours + theirs),
 			branch_name, ours, theirs);
-		if (show_divergence_advice &&
-		    advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_divergence_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
@@ -2303,6 +2369,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	const char *full_base;
 	char *base;
 	int upstream_is_gone = 0;
+	unsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;
+	int push_ours, push_theirs, push_cmp_fetch;
+	char *full_push = NULL;
+	char *push = NULL;
+	unsigned push_branch_modes = 0;
 
 	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
 	if (cmp_fetch < 0) {
@@ -2314,6 +2385,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
 					    full_base, 0);
 
+	push = get_remote_push_branch(branch, &full_push);
+	if (push && strcmp(base, push)) {
+		push_cmp_fetch = stat_branch_pair(branch->refname, full_push,
+					   &push_ours, &push_theirs, abf);
+		if (push_cmp_fetch >= 0) {
+			base_branch_modes = BRANCH_MODE_PULL;
+			push_branch_modes = BRANCH_MODE_PUSH;
+		}
+	}
+
 	if (upstream_is_gone) {
 		strbuf_addf(sb,
 			_("Your branch is based on '%s', but the upstream is gone.\n"),
@@ -2322,10 +2403,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			strbuf_addstr(sb,
 				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
 	} else {
-		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,
+					 base_branch_modes, show_divergence_advice);
+	}
+
+	if (push_branch_modes & BRANCH_MODE_PUSH) {
+		strbuf_addstr(sb, "\n");
+		format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,
+					 push_branch_modes, show_divergence_advice);
 	}
 
 	free(base);
+	free(full_push);
+	free(push);
 	return 1;
 }
 
t/t6040-tracking-info.sh +262 -0

266 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#141 lore raw

v23 cover status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: show comparison with push remote tracking branch

 remote.c                 | 173 ++++++++++++++++++++------
 t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 400 insertions(+), 35 deletions(-)

base-commit: 8745eae506f700657882b9e32b2aa00f234a6fb6
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v23
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v23
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v22:

 1:  4aa4f1abc8 = 1:  fd05c7b778 refactor format_branch_comparison in preparation
 2:  b7e29887d9 ! 2:  f1ad7a1b6f status: show comparison with push remote tracking branch
     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_
      +		advice_enabled(ADVICE_STATUS_HINTS);
      +	bool want_pull_advice = (flags & BRANCH_MODE_PULL) &&
      +		advice_enabled(ADVICE_STATUS_HINTS);
     -+	bool want_divergence_advice = (flags & BRANCH_MODE_PULL) &&
     -+		show_divergence_advice &&
     ++	bool want_divergence_advice = show_divergence_advice &&
      +		advice_enabled(ADVICE_STATUS_HINTS);
      +
       	if (up_to_date) {
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
      +	if (push_branch_modes & BRANCH_MODE_PUSH) {
      +		strbuf_addstr(sb, "\n");
      +		format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,
     -+					 push_branch_modes, show_divergence_advice);
     ++					 push_branch_modes, 0);
       	}
       
       	free(base);
Signature · 1 line
gitgitgadget
#142 lore raw

PATCH v23 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 48 insertions(+), 34 deletions(-)
remote.c +48 -34
index b756ff6f15..fd592ec659 100644
@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			       "and have %d and %d different commits each, "
 			       "respectively.\n",
 			   ours + theirs),
-			base, ours, theirs);
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#143 lore raw

PATCH v23 2/2 status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

"git status" on a branch that follows a remote branch compares commits on the current branch and the remote-tracking branch it builds upon, to show "ahead", "behind", or "diverged" status.

When working on a feature branch that tracks a remote feature branch, but you also want to track progress relative to the push destination tracking branch (which may differ from the upstream branch), git status now shows an additional comparison.

When the upstream tracking branch differs from the push destination tracking branch, git status shows both the comparison with the upstream tracking branch (as before) and an additional comparison with the push destination tracking branch. The push branch comparison appears on a separate line after the upstream branch status, using the same format.

Example output when tracking origin/main but push destination is origin/feature:
On branch feature
Your branch and 'origin/main' have diverged,
and have 3 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)

Your branch is ahead of 'origin/feature' by 1 commit.
(use "git push" to publish your local commits)

The comparison is only shown when the push destination tracking branch differs from the upstream tracking branch, even if they are on the same remote.

---
 remote.c                 | 101 ++++++++++++++-
 t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 357 insertions(+), 6 deletions(-)
remote.c +95 -6
index fd592ec659..132fc6849f 100644
@@ -29,6 +29,11 @@
 
 enum map_direction { FROM_SRC, FROM_DST };
 
+enum {
+	BRANCH_MODE_PULL = (1 << 0),
+	BRANCH_MODE_PUSH = (1 << 1),
+};
+
 struct counted_string {
 	size_t len;
 	const char *s;
@@ -2230,13 +2235,74 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)
+{
+	struct remote *remote;
+	const char *push_remote;
+	char *push_dst = NULL;
+	char *tracking_ref;
+	const char *resolved;
+	char *ret;
+
+	if (!branch)
+		return NULL;
+
+	push_remote = pushremote_for_branch(branch, NULL);
+	if (!push_remote)
+		return NULL;
+
+	remote = remotes_remote_get(the_repository, push_remote);
+	if (!remote)
+		return NULL;
+
+	push_dst = remote_ref_for_branch(branch, 1);
+	if (!push_dst) {
+		if (remote->push.nr)
+			return NULL;
+		push_dst = xstrdup(branch->refname);
+	}
+
+	tracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);
+	free(push_dst);
+
+	if (!tracking_ref)
+		return NULL;
+
+	resolved = refs_resolve_ref_unsafe(
+		get_main_ref_store(the_repository),
+		tracking_ref,
+		RESOLVE_REF_READING,
+		NULL, NULL);
+
+	if (!resolved) {
+		free(tracking_ref);
+		return NULL;
+	}
+
+	if (full_ref_out)
+		*full_ref_out = xstrdup(resolved);
+
+	ret = refs_shorten_unambiguous_ref(
+		get_main_ref_store(the_repository), resolved, 0);
+	free(tracking_ref);
+	return ret;
+}
+
 static void format_branch_comparison(struct strbuf *sb,
 				     bool up_to_date,
 				     int ours, int theirs,
 				     const char *branch_name,
 				     enum ahead_behind_flags abf,
+				     unsigned flags,
 				     bool show_divergence_advice)
 {
+	bool want_push_advice = (flags & BRANCH_MODE_PUSH) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool want_pull_advice = (flags & BRANCH_MODE_PULL) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool want_divergence_advice = show_divergence_advice &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+
 	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
@@ -2245,7 +2311,7 @@ static void format_branch_comparison(struct strbuf *sb,
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
 			    branch_name);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_push_advice)
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
 	} else if (!theirs) {
@@ -2254,7 +2320,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
 			branch_name, ours);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_push_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
 	} else if (!ours) {
@@ -2265,7 +2331,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "and can be fast-forwarded.\n",
 			   theirs),
 			branch_name, theirs);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_pull_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
 	} else {
@@ -2278,8 +2344,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "respectively.\n",
 			   ours + theirs),
 			branch_name, ours, theirs);
-		if (show_divergence_advice &&
-		    advice_enabled(ADVICE_STATUS_HINTS))
+		if (want_divergence_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
@@ -2296,6 +2361,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	const char *full_base;
 	char *base;
 	int upstream_is_gone = 0;
+	unsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;
+	int push_ours, push_theirs, push_cmp_fetch;
+	char *full_push = NULL;
+	char *push = NULL;
+	unsigned push_branch_modes = 0;
 
 	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
 	if (cmp_fetch < 0) {
@@ -2307,6 +2377,16 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
 					    full_base, 0);
 
+	push = get_remote_push_branch(branch, &full_push);
+	if (push && strcmp(base, push)) {
+		push_cmp_fetch = stat_branch_pair(branch->refname, full_push,
+					   &push_ours, &push_theirs, abf);
+		if (push_cmp_fetch >= 0) {
+			base_branch_modes = BRANCH_MODE_PULL;
+			push_branch_modes = BRANCH_MODE_PUSH;
+		}
+	}
+
 	if (upstream_is_gone) {
 		strbuf_addf(sb,
 			_("Your branch is based on '%s', but the upstream is gone.\n"),
@@ -2315,10 +2395,19 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			strbuf_addstr(sb,
 				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
 	} else {
-		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,
+					 base_branch_modes, show_divergence_advice);
+	}
+
+	if (push_branch_modes & BRANCH_MODE_PUSH) {
+		strbuf_addstr(sb, "\n");
+		format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,
+					 push_branch_modes, 0);
 	}
 
 	free(base);
+	free(full_push);
+	free(push);
 	return 1;
 }
 
t/t6040-tracking-info.sh +262 -0

266 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#144 lore raw

Re: [PATCH v23 2/2] status: show comparison with push remote tracking branch

remote.c int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs, · line 2304

Quoted 6 lines · Harald Nordgren via GitGitGadget
remote.c +6 -0
+	bool want_push_advice = (flags & BRANCH_MODE_PUSH) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool want_pull_advice = (flags & BRANCH_MODE_PULL) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool want_divergence_advice = show_divergence_advice &&
+		advice_enabled(ADVICE_STATUS_HINTS);

Just an observation, and not an objection, but it looks curious why the last one is so different from the other two. IOW, the above makes me wonder if it makes sense to roll the show_divergence_advice bit into the base_branch_modes flag word.

Other than that, this round is a pleasant read. Thanks.

#145 lore raw

Re: [PATCH v17 1/2] refactor format_branch_comparison in preparation

Quoted 4 lines

Just an observation, and not an objection, but it looks curious why the last one is so different from the other two. IOW, the above makes me wonder if it makes sense to roll the show_divergence_advice bit into the base_branch_modes flag word.

Good point. I changed it, it makes it nicer with one fewer parameter.

Quoted 1 line

Other than that, this round is a pleasant read. Thanks.

Thanks for all the help so far!

Harald

#146 lore raw

v24 cover status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: show comparison with push remote tracking branch

 remote.c                 | 175 ++++++++++++++++++++------
 t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 402 insertions(+), 35 deletions(-)

base-commit: 8745eae506f700657882b9e32b2aa00f234a6fb6
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v24
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v24
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v23:

 1:  fd05c7b778 = 1:  fd05c7b778 refactor format_branch_comparison in preparation
 2:  f1ad7a1b6f ! 2:  138b79a0b9 status: show comparison with push remote tracking branch
     @@ remote.c
       enum map_direction { FROM_SRC, FROM_DST };
       
      +enum {
     -+	BRANCH_MODE_PULL = (1 << 0),
     -+	BRANCH_MODE_PUSH = (1 << 1),
     ++	ENABLE_ADVICE_PULL       = (1 << 0),
     ++	ENABLE_ADVICE_PUSH       = (1 << 1),
     ++	ENABLE_ADVICE_DIVERGENCE = (1 << 2),
      +};
      +
       struct counted_string {
     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_
       	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
       }
       
     -+static char *get_remote_push_branch(struct branch *branch, char **full_ref_out)
     ++static char *get_remote_push_branch(struct branch *branch)
      +{
      +	struct remote *remote;
      +	const char *push_remote;
     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_
      +		return NULL;
      +	}
      +
     -+	if (full_ref_out)
     -+		*full_ref_out = xstrdup(resolved);
     -+
     -+	ret = refs_shorten_unambiguous_ref(
     -+		get_main_ref_store(the_repository), resolved, 0);
     ++	ret = xstrdup(resolved);
      +	free(tracking_ref);
      +	return ret;
      +}
     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_
       				     int ours, int theirs,
       				     const char *branch_name,
       				     enum ahead_behind_flags abf,
     -+				     unsigned flags,
     - 				     bool show_divergence_advice)
     +-				     bool show_divergence_advice)
     ++				     unsigned flags)
       {
     -+	bool want_push_advice = (flags & BRANCH_MODE_PUSH) &&
     ++	bool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&
      +		advice_enabled(ADVICE_STATUS_HINTS);
     -+	bool want_pull_advice = (flags & BRANCH_MODE_PULL) &&
     ++	bool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&
      +		advice_enabled(ADVICE_STATUS_HINTS);
     -+	bool want_divergence_advice = show_divergence_advice &&
     ++	bool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&
      +		advice_enabled(ADVICE_STATUS_HINTS);
      +
       	if (up_to_date) {
     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,
       			    _("Your branch and '%s' refer to different commits.\n"),
       			    branch_name);
      -		if (advice_enabled(ADVICE_STATUS_HINTS))
     -+		if (want_push_advice)
     ++		if (enable_push_advice)
       			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
       				    "git status --ahead-behind");
       	} else if (!theirs) {
     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,
       			   ours),
       			branch_name, ours);
      -		if (advice_enabled(ADVICE_STATUS_HINTS))
     -+		if (want_push_advice)
     ++		if (enable_push_advice)
       			strbuf_addstr(sb,
       				_("  (use \"git push\" to publish your local commits)\n"));
       	} else if (!ours) {
     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,
       			   theirs),
       			branch_name, theirs);
      -		if (advice_enabled(ADVICE_STATUS_HINTS))
     -+		if (want_pull_advice)
     ++		if (enable_pull_advice)
       			strbuf_addstr(sb,
       				_("  (use \"git pull\" to update your local branch)\n"));
       	} else {
     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,
       			branch_name, ours, theirs);
      -		if (show_divergence_advice &&
      -		    advice_enabled(ADVICE_STATUS_HINTS))
     -+		if (want_divergence_advice)
     ++		if (enable_divergence_advice)
       			strbuf_addstr(sb,
       				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
       	}
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
       	const char *full_base;
       	char *base;
       	int upstream_is_gone = 0;
     -+	unsigned base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;
     ++	unsigned base_branch_flags = ENABLE_ADVICE_PULL | ENABLE_ADVICE_PUSH;
      +	int push_ours, push_theirs, push_cmp_fetch;
     -+	char *full_push = NULL;
     ++	char *full_push;
      +	char *push = NULL;
     -+	unsigned push_branch_modes = 0;
     ++	unsigned push_branch_flags = 0;
       
       	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
       	if (cmp_fetch < 0) {
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
       	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
       					    full_base, 0);
       
     -+	push = get_remote_push_branch(branch, &full_push);
     -+	if (push && strcmp(base, push)) {
     -+		push_cmp_fetch = stat_branch_pair(branch->refname, full_push,
     -+					   &push_ours, &push_theirs, abf);
     -+		if (push_cmp_fetch >= 0) {
     -+			base_branch_modes = BRANCH_MODE_PULL;
     -+			push_branch_modes = BRANCH_MODE_PUSH;
     ++	full_push = get_remote_push_branch(branch);
     ++	if (full_push) {
     ++		push = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
     ++						     full_push, 0);
     ++		if (push && base && strcmp(base, push)) {
     ++			push_cmp_fetch = stat_branch_pair(branch->refname, full_push,
     ++						   &push_ours, &push_theirs, abf);
     ++			if (push_cmp_fetch >= 0) {
     ++				base_branch_flags = ENABLE_ADVICE_PULL;
     ++				push_branch_flags = ENABLE_ADVICE_PUSH;
     ++			}
      +		}
      +	}
      +
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
       				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
       	} else {
      -		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
     ++		if (show_divergence_advice)
     ++			base_branch_flags |= ENABLE_ADVICE_DIVERGENCE;
      +		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,
     -+					 base_branch_modes, show_divergence_advice);
     ++					 base_branch_flags);
      +	}
      +
     -+	if (push_branch_modes & BRANCH_MODE_PUSH) {
     ++	if (push_branch_flags & ENABLE_ADVICE_PUSH) {
      +		strbuf_addstr(sb, "\n");
      +		format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,
     -+					 push_branch_modes, 0);
     ++					 push_branch_flags);
       	}
       
       	free(base);
Signature · 1 line
gitgitgadget
#147 lore raw

PATCH v24 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 48 insertions(+), 34 deletions(-)
remote.c +48 -34
index b756ff6f15..fd592ec659 100644
@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			       "and have %d and %d different commits each, "
 			       "respectively.\n",
 			   ours + theirs),
-			base, ours, theirs);
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#148 lore raw

PATCH v24 2/2 status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

"git status" on a branch that follows a remote branch compares commits on the current branch and the remote-tracking branch it builds upon, to show "ahead", "behind", or "diverged" status.

When working on a feature branch that tracks a remote feature branch, but you also want to track progress relative to the push destination tracking branch (which may differ from the upstream branch), git status now shows an additional comparison.

When the upstream tracking branch differs from the push destination tracking branch, git status shows both the comparison with the upstream tracking branch (as before) and an additional comparison with the push destination tracking branch. The push branch comparison appears on a separate line after the upstream branch status, using the same format.

Example output when tracking origin/main but push destination is origin/feature:
On branch feature
Your branch and 'origin/main' have diverged,
and have 3 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)

Your branch is ahead of 'origin/feature' by 1 commit.
(use "git push" to publish your local commits)

The comparison is only shown when the push destination tracking branch differs from the upstream tracking branch, even if they are on the same remote.

---
 remote.c                 | 105 ++++++++++++++--
 t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 360 insertions(+), 7 deletions(-)
remote.c +98 -7
index fd592ec659..8d64a3bee6 100644
@@ -29,6 +29,12 @@
 
 enum map_direction { FROM_SRC, FROM_DST };
 
+enum {
+	ENABLE_ADVICE_PULL       = (1 << 0),
+	ENABLE_ADVICE_PUSH       = (1 << 1),
+	ENABLE_ADVICE_DIVERGENCE = (1 << 2),
+};
+
 struct counted_string {
 	size_t len;
 	const char *s;
@@ -2230,13 +2236,69 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
+static char *get_remote_push_branch(struct branch *branch)
+{
+	struct remote *remote;
+	const char *push_remote;
+	char *push_dst = NULL;
+	char *tracking_ref;
+	const char *resolved;
+	char *ret;
+
+	if (!branch)
+		return NULL;
+
+	push_remote = pushremote_for_branch(branch, NULL);
+	if (!push_remote)
+		return NULL;
+
+	remote = remotes_remote_get(the_repository, push_remote);
+	if (!remote)
+		return NULL;
+
+	push_dst = remote_ref_for_branch(branch, 1);
+	if (!push_dst) {
+		if (remote->push.nr)
+			return NULL;
+		push_dst = xstrdup(branch->refname);
+	}
+
+	tracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);
+	free(push_dst);
+
+	if (!tracking_ref)
+		return NULL;
+
+	resolved = refs_resolve_ref_unsafe(
+		get_main_ref_store(the_repository),
+		tracking_ref,
+		RESOLVE_REF_READING,
+		NULL, NULL);
+
+	if (!resolved) {
+		free(tracking_ref);
+		return NULL;
+	}
+
+	ret = xstrdup(resolved);
+	free(tracking_ref);
+	return ret;
+}
+
 static void format_branch_comparison(struct strbuf *sb,
 				     bool up_to_date,
 				     int ours, int theirs,
 				     const char *branch_name,
 				     enum ahead_behind_flags abf,
-				     bool show_divergence_advice)
+				     unsigned flags)
 {
+	bool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+
 	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
@@ -2245,7 +2307,7 @@ static void format_branch_comparison(struct strbuf *sb,
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
 			    branch_name);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (enable_push_advice)
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
 	} else if (!theirs) {
@@ -2254,7 +2316,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
 			branch_name, ours);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (enable_push_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
 	} else if (!ours) {
@@ -2265,7 +2327,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "and can be fast-forwarded.\n",
 			   theirs),
 			branch_name, theirs);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (enable_pull_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
 	} else {
@@ -2278,8 +2340,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "respectively.\n",
 			   ours + theirs),
 			branch_name, ours, theirs);
-		if (show_divergence_advice &&
-		    advice_enabled(ADVICE_STATUS_HINTS))
+		if (enable_divergence_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
@@ -2296,6 +2357,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	const char *full_base;
 	char *base;
 	int upstream_is_gone = 0;
+	unsigned base_branch_flags = ENABLE_ADVICE_PULL | ENABLE_ADVICE_PUSH;
+	int push_ours, push_theirs, push_cmp_fetch;
+	char *full_push;
+	char *push = NULL;
+	unsigned push_branch_flags = 0;
 
 	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
 	if (cmp_fetch < 0) {
@@ -2307,6 +2373,20 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
 					    full_base, 0);
 
+	full_push = get_remote_push_branch(branch);
+	if (full_push) {
+		push = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+						     full_push, 0);
+		if (push && base && strcmp(base, push)) {
+			push_cmp_fetch = stat_branch_pair(branch->refname, full_push,
+						   &push_ours, &push_theirs, abf);
+			if (push_cmp_fetch >= 0) {
+				base_branch_flags = ENABLE_ADVICE_PULL;
+				push_branch_flags = ENABLE_ADVICE_PUSH;
+			}
+		}
+	}
+
 	if (upstream_is_gone) {
 		strbuf_addf(sb,
 			_("Your branch is based on '%s', but the upstream is gone.\n"),
@@ -2315,10 +2395,21 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			strbuf_addstr(sb,
 				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
 	} else {
-		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+		if (show_divergence_advice)
+			base_branch_flags |= ENABLE_ADVICE_DIVERGENCE;
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,
+					 base_branch_flags);
+	}
+
+	if (push_branch_flags & ENABLE_ADVICE_PUSH) {
+		strbuf_addstr(sb, "\n");
+		format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,
+					 push_branch_flags);
 	}
 
 	free(base);
+	free(full_push);
+	free(push);
 	return 1;
 }
 
t/t6040-tracking-info.sh +262 -0

266 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#149 lore raw

v25 cover status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: show comparison with push remote tracking branch

 remote.c                 | 175 +++++++++++++++++++-----
 t/t6040-tracking-info.sh | 285 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 425 insertions(+), 35 deletions(-)

base-commit: 8745eae506f700657882b9e32b2aa00f234a6fb6
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v25
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v25
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v24:

 1:  fd05c7b778 = 1:  fd05c7b778 refactor format_branch_comparison in preparation
 2:  138b79a0b9 ! 2:  fa744efc59 status: show comparison with push remote tracking branch
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     ++test_expect_success 'status with upstream ahead and push branch up to date' '
     ++	(
     ++		cd test &&
     ++		git checkout -b ahead upstream/main &&
     ++		advance work &&
     ++		git push upstream HEAD &&
     ++		git checkout -b feature8 upstream/main &&
     ++		git push origin &&
     ++		git branch --set-upstream-to upstream/ahead &&
     ++		git status >../actual
     ++	) &&
     ++	cat >expect <<-EOF &&
     ++	On branch feature8
     ++	Your branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.
     ++	  (use "git pull" to update your local branch)
     ++
     ++	Your branch is up to date with ${SQ}origin/feature8${SQ}.
     ++
     ++	nothing to commit, working tree clean
     ++	EOF
     ++	test_cmp expect actual
     ++'
     ++
      +test_expect_success 'status shows remapped push refspec' '
      +	(
      +		cd test &&
     -+		git checkout -b feature8 origin/main &&
     -+		git config remote.origin.push refs/heads/feature8:refs/heads/remapped &&
     ++		git checkout -b feature9 origin/main &&
     ++		git config remote.origin.push refs/heads/feature9:refs/heads/remapped &&
      +		git push &&
      +		advance work &&
      +		git status >../actual
      +	) &&
      +	cat >expect <<-EOF &&
     -+	On branch feature8
     ++	On branch feature9
      +	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
      +
      +	Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +test_expect_success 'status shows remapped push refspec with upstream remote' '
      +	(
      +		cd test &&
     -+		git checkout -b feature9 upstream/main &&
     -+		git config remote.origin.push refs/heads/feature9:refs/heads/remapped &&
     ++		git checkout -b feature10 upstream/main &&
     ++		git config remote.origin.push refs/heads/feature10:refs/heads/remapped &&
      +		git push origin &&
      +		advance work &&
      +		git status >../actual
      +	) &&
      +	cat >expect <<-EOF &&
     -+	On branch feature9
     ++	On branch feature10
      +	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
      +
      +	Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.
Signature · 1 line
gitgitgadget
#150 lore raw

PATCH v25 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 48 insertions(+), 34 deletions(-)
remote.c +48 -34
index b756ff6f15..fd592ec659 100644
@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			       "and have %d and %d different commits each, "
 			       "respectively.\n",
 			   ours + theirs),
-			base, ours, theirs);
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#151 lore raw

PATCH v25 2/2 status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

"git status" on a branch that follows a remote branch compares commits on the current branch and the remote-tracking branch it builds upon, to show "ahead", "behind", or "diverged" status.

When working on a feature branch that tracks a remote feature branch, but you also want to track progress relative to the push destination tracking branch (which may differ from the upstream branch), git status now shows an additional comparison.

When the upstream tracking branch differs from the push destination tracking branch, git status shows both the comparison with the upstream tracking branch (as before) and an additional comparison with the push destination tracking branch. The push branch comparison appears on a separate line after the upstream branch status, using the same format.

Example output when tracking origin/main but push destination is origin/feature:
On branch feature
Your branch and 'origin/main' have diverged,
and have 3 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)

Your branch is ahead of 'origin/feature' by 1 commit.
(use "git push" to publish your local commits)

The comparison is only shown when the push destination tracking branch differs from the upstream tracking branch, even if they are on the same remote.

---
 remote.c                 | 105 ++++++++++++++-
 t/t6040-tracking-info.sh | 285 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 383 insertions(+), 7 deletions(-)
remote.c +98 -7
index fd592ec659..8d64a3bee6 100644
@@ -29,6 +29,12 @@
 
 enum map_direction { FROM_SRC, FROM_DST };
 
+enum {
+	ENABLE_ADVICE_PULL       = (1 << 0),
+	ENABLE_ADVICE_PUSH       = (1 << 1),
+	ENABLE_ADVICE_DIVERGENCE = (1 << 2),
+};
+
 struct counted_string {
 	size_t len;
 	const char *s;
@@ -2230,13 +2236,69 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
+static char *get_remote_push_branch(struct branch *branch)
+{
+	struct remote *remote;
+	const char *push_remote;
+	char *push_dst = NULL;
+	char *tracking_ref;
+	const char *resolved;
+	char *ret;
+
+	if (!branch)
+		return NULL;
+
+	push_remote = pushremote_for_branch(branch, NULL);
+	if (!push_remote)
+		return NULL;
+
+	remote = remotes_remote_get(the_repository, push_remote);
+	if (!remote)
+		return NULL;
+
+	push_dst = remote_ref_for_branch(branch, 1);
+	if (!push_dst) {
+		if (remote->push.nr)
+			return NULL;
+		push_dst = xstrdup(branch->refname);
+	}
+
+	tracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);
+	free(push_dst);
+
+	if (!tracking_ref)
+		return NULL;
+
+	resolved = refs_resolve_ref_unsafe(
+		get_main_ref_store(the_repository),
+		tracking_ref,
+		RESOLVE_REF_READING,
+		NULL, NULL);
+
+	if (!resolved) {
+		free(tracking_ref);
+		return NULL;
+	}
+
+	ret = xstrdup(resolved);
+	free(tracking_ref);
+	return ret;
+}
+
 static void format_branch_comparison(struct strbuf *sb,
 				     bool up_to_date,
 				     int ours, int theirs,
 				     const char *branch_name,
 				     enum ahead_behind_flags abf,
-				     bool show_divergence_advice)
+				     unsigned flags)
 {
+	bool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+
 	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
@@ -2245,7 +2307,7 @@ static void format_branch_comparison(struct strbuf *sb,
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
 			    branch_name);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (enable_push_advice)
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
 	} else if (!theirs) {
@@ -2254,7 +2316,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
 			branch_name, ours);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (enable_push_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
 	} else if (!ours) {
@@ -2265,7 +2327,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "and can be fast-forwarded.\n",
 			   theirs),
 			branch_name, theirs);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (enable_pull_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
 	} else {
@@ -2278,8 +2340,7 @@ static void format_branch_comparison(struct strbuf *sb,
 			       "respectively.\n",
 			   ours + theirs),
 			branch_name, ours, theirs);
-		if (show_divergence_advice &&
-		    advice_enabled(ADVICE_STATUS_HINTS))
+		if (enable_divergence_advice)
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
@@ -2296,6 +2357,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	const char *full_base;
 	char *base;
 	int upstream_is_gone = 0;
+	unsigned base_branch_flags = ENABLE_ADVICE_PULL | ENABLE_ADVICE_PUSH;
+	int push_ours, push_theirs, push_cmp_fetch;
+	char *full_push;
+	char *push = NULL;
+	unsigned push_branch_flags = 0;
 
 	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
 	if (cmp_fetch < 0) {
@@ -2307,6 +2373,20 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
 					    full_base, 0);
 
+	full_push = get_remote_push_branch(branch);
+	if (full_push) {
+		push = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+						     full_push, 0);
+		if (push && base && strcmp(base, push)) {
+			push_cmp_fetch = stat_branch_pair(branch->refname, full_push,
+						   &push_ours, &push_theirs, abf);
+			if (push_cmp_fetch >= 0) {
+				base_branch_flags = ENABLE_ADVICE_PULL;
+				push_branch_flags = ENABLE_ADVICE_PUSH;
+			}
+		}
+	}
+
 	if (upstream_is_gone) {
 		strbuf_addf(sb,
 			_("Your branch is based on '%s', but the upstream is gone.\n"),
@@ -2315,10 +2395,21 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			strbuf_addstr(sb,
 				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
 	} else {
-		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+		if (show_divergence_advice)
+			base_branch_flags |= ENABLE_ADVICE_DIVERGENCE;
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,
+					 base_branch_flags);
+	}
+
+	if (push_branch_flags & ENABLE_ADVICE_PUSH) {
+		strbuf_addstr(sb, "\n");
+		format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,
+					 push_branch_flags);
 	}
 
 	free(base);
+	free(full_push);
+	free(push);
 	return 1;
 }
 
t/t6040-tracking-info.sh +285 -0

289 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#152 lore raw

Re: [PATCH v25 2/2] status: show comparison with push remote tracking branch

Quoted 9 lines · Harald Nordgren via GitGitGadget

Example output when tracking origin/main but push destination is origin/feature:
On branch feature
Your branch and 'origin/main' have diverged,
and have 3 and 1 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)

Your branch is ahead of 'origin/feature' by 1 commit.
(use "git push" to publish your local commits)

Can we make this configurable?

I build my daily driver off of the 'jch' branch, which now includes this series, and I've found that for my triangular workflow the ahead/behind for the push branch is just useless noise. I treat my push destination like a mirror, where I always just push up everything at the end of the day.

I know that the output can be disabled with status.aheadbehind, but:

1. I noticed this first via "git checkout", which does not have such a flag (AFAIK).

2. That flag would also disable the upstream branch ahead/behind output, which I do find useful.

-Peff

#153 lore raw

Triangular workflow

Quoted 3 lines

For my triangular workflow the ahead/behind for the push branch is just useless noise. I treat my push destination like a mirror, where I always just push up everything at the end of the day.

This seems like a sub-optimal workflow 🤗

May I ask where you normally push (unless you only ever push once at the end) of the day? If it's another branch than your mirror, why not set that as your push destination then, and push to your push destination with the longer

git push origin my-mirror

It makes more sense to me to reserve the shorter and more convenient 'git push' for something you do many times a day.

Harald

#154 lore raw

Re: Triangular workflow

Quoted 11 lines · Harald Nordgren
Quoted 3 lines

For my triangular workflow the ahead/behind for the push branch is just useless noise. I treat my push destination like a mirror, where I always just push up everything at the end of the day.

This seems like a sub-optimal workflow 🤗

May I ask where you normally push (unless you only ever push once at the end) of the day? If it's another branch than your mirror, why not set that as your push destination then, and push to your push destination with the longer

Quoted 4 lines

git push origin my-mirror

It makes more sense to me to reserve the shorter and more convenient 'git push' for something you do many times a day.

I couldn't quite parse what you meant by "another branch than your mirror", but I'll describe my workflow:

1. My "origin" remote is https://github.com/gitster/git.

2. Most branches have origin/master as their upstream, though occasionally I'll have dependent branches that use "." as their remote and the local base branch for the "branch" field.

E.g.

git checkout -b jk/some-topic origin/master
...
git checkout -t -b jk/another-topic jk/some-topic

3. My mirror is https://github.com/peff/git, which I call "github" in the remote (arguably confusing, but back when I started that convention I was pulling Junio's tree from kernel.org ;) ). I point remote.pushdefault to the "github" remote.

4. My push.default setting is "current", so if I want to push up a single branch, I can just "git push". I hardly ever do that, though.

5. I usually push once per day or so. After integrating my topics into a shared daily-driver branch, I run something like:

make test && make install && git push -f github refs/heads/jk/*

(it's not exactly that; I have a script which picks out the local
refs, but it's the moral equivalent of that).

So I don't ever care about the relationship between a specific jk/foo and my mirror's version of it. I don't expect anybody to look at those, and they exist mostly for backup and CI purposes (though I don't run CI on the individual branches, but only the integrated result).

And having the extra output from "git checkout" is just extra noise for me, especially because it is easy to see only the second message (which looks just like the upstream ahead/behind message, of course) and get confused. The first time I saw it I thought I had misconfigured something with my branch.

-Peff

#155 lore raw

Triangular workflow

Hi Jeff!

I'm very happy that your responded respectfully despite me basically saying that you were using Git wrong. It's nice to see how some of the pros do it!

I'm wondering if since you are scripting this anyway, if you really need a push branch at all? Can't you just as easily switch to doing this in the script:

git config push.default upstream
git push github jk/some-topic

As a note, before I started working on this feature, I don't realize that there was such a thing as a push branch (i.e. something different from the tracking branch). So I had the habit of checking out and pushing like this:

    git branch --set-upstream-to upstream/master
    git push origin $(git rev-parse --abbrev-ref HEAD)

I worked really well for me. The only issue was missing the status info from my own branch -- which is why I started writing this feature.

Harald

#156 lore raw

Re: Triangular workflow

Quoted 7 lines · Harald Nordgren

Hi Jeff!

I'm very happy that your responded respectfully despite me basically saying that you were using Git wrong. It's nice to see how some of the pros do it!

Quoted 6 lines

I'm wondering if since you are scripting this anyway, if you really need a push branch at all? Can't you just as easily switch to doing this in the script:

git config push.default upstream
git push github jk/some-topic

The script is doing the "moral equivalent of" "git push -f github refs/heads/jk/*", so I'm not sure I follow the suggestion here.

Quoted 5 lines

As a note, before I started working on this feature, I don't realize that there was such a thing as a push branch (i.e. something different from the tracking branch). So I had the habit of checking out and pushing like this:

Quoted 8 lines
    git branch --set-upstream-to upstream/master
    git push origin $(git rev-parse --abbrev-ref HEAD)

I worked really well for me. The only issue was missing the status info from my own branch -- which is why I started writing this feature.

Harald

My workflow is different from Peff's, but it is similar along at least one line: it's really convenient to have "git push" with no further arguments (only possibly flags) to push my branch to a remote mirror.

Signature · 1 line
D. Ben Knoble
#157 lore raw

Triangular workflow

Quoted 3 lines

My workflow is different from Peff's, but it is similar along at least one line: it's really convenient to have "git push" with no further arguments (only possibly flags) to push my branch to a remote mirror.

Would you also like the status reporting to be off for your push branch? I asking because that's what Jeff is arguing for.

I fully agree on the convenience of bare 'git push', I use it everyday together with these settings and commands for max convenience:

    git config --global remote.pushDefault origin # As opposed to upstream
    git config --global push.default current
    git config --global pull.rebase true
    git config --global rebase.autoStash true

    git checkout $(gr | rg '^(upstream|origin)$' | tail -n1)/HEAD -b new_branch
    git push
    git status
    git pull

Harald

#158 lore raw

Re: Triangular workflow

Quoted 10 lines · Harald Nordgren



Quoted 3 lines

My workflow is different from Peff's, but it is similar along at least one line: it's really convenient to have "git push" with no further arguments (only possibly flags) to push my branch to a remote mirror.

Would you also like the status reporting to be off for your push branch? I asking because that's what Jeff is arguing for.

I’m somewhat indifferent (mildly for having it on by default), but I meant that you don’t want to suggest that in order to use your new feature you don’t want folks to have to break their existing workflows ;)

#159 lore raw

Re: Triangular workflow

In reply to Harald Nordgren
Quoted 6 lines · Harald Nordgren

I'm wondering if since you are scripting this anyway, if you really need a push branch at all? Can't you just as easily switch to doing this in the script:

git config push.default upstream
git push github jk/some-topic

I could (and in fact the script names the remote directly already, because you can't pass refspecs without specifying the remote). But I do occasionally push a single branch with a bare "git push". Usually this is the integration branch, when I am trying to trigger CI manually (e.g., when piling hacks on top in order to debug a CI failure ;) ).

So even if I only do it infrequently, it feels weird that a bare "git push" would try to push to the upstream remote (which I don't even have write access to!).

Quoted 10 lines

As a note, before I started working on this feature, I don't realize that there was such a thing as a push branch (i.e. something different from the tracking branch). So I had the habit of checking out and pushing like this:

    git branch --set-upstream-to upstream/master
    git push origin $(git rev-parse --abbrev-ref HEAD)

I worked really well for me. The only issue was missing the status info from my own branch -- which is why I started writing this feature.

Yeah, though @{push} is usually not explicitly configured in the same way @{upstream} is, but rather a consequence of how push.default and remote.pushdefault interact. But it was added for exactly this kind of triangular workflow. I sometimes will do stuff like:

git range-diff origin @{push} HEAD

to compare two iterations of a branch if I know that I haven't pushed. It is a bit of a cheat, because what I really mean is "do a range-diff since the last thing I sent to the list". But if I have just been working on a branch, and I haven't run an integration cycle since then, then I know that the pushed version will match it.

There is also branch.*.pushRemote, but I have not found that useful (for my triangular flow there is always a single repo to push to, not one per branch).

-Peff

#160 lore raw

Triangular workflow

Quoted 9 lines

I could (and in fact the script names the remote directly already, because you can't pass refspecs without specifying the remote). But I do occasionally push a single branch with a bare "git push". Usually this is the integration branch, when I am trying to trigger CI manually (e.g., when piling hacks on top in order to debug a CI failure ;) ).

So even if I only do it infrequently, it feels weird that a bare "git push" would try to push to the upstream remote (which I don't even have write access to!).

Maybe ’push.default=simple’ or ’push.default=nothing’ are better settings in your scenario. Then you get explicit pushing because no push branch gets set. And thus 'git status' reports not additinal status.

Quoted 6 lines

Yeah, though @{push} is usually not explicitly configured in the same way @{upstream} is, but rather a consequence of how push.default and remote.pushdefault interact. But it was added for exactly this kind of triangular workflow. I sometimes will do stuff like:

git range-diff origin @{push} HEAD

I imagine the same thing could be achieved with

    origin/$(git rev-parse --abbrev-ref HEAD)

Harald

#161 lore raw

Re: Triangular workflow

Quoted 6 lines · Harald Nordgren
Quoted 6 lines

I could (and in fact the script names the remote directly already, because you can't pass refspecs without specifying the remote). But I do occasionally push a single branch with a bare "git push". Usually this is the integration branch, when I am trying to trigger CI manually (e.g., when piling hacks on top in order to debug a CI failure ;) ).

Quoted 7 lines
Quoted 3 lines

So even if I only do it infrequently, it feels weird that a bare "git push" would try to push to the upstream remote (which I don't even have write access to!).

Maybe ’push.default=simple’ or ’push.default=nothing’ are better settings in your scenario. Then you get explicit pushing because no push branch gets set. And thus 'git status' reports not additinal status.

If I did that, then the occasional "git push" (without arguments) that I do would fail. Likewise, @{push} would not be usable.

Quoted 10 lines
Quoted 6 lines

Yeah, though @{push} is usually not explicitly configured in the same way @{upstream} is, but rather a consequence of how push.default and remote.pushdefault interact. But it was added for exactly this kind of triangular workflow. I sometimes will do stuff like:

git range-diff origin @{push} HEAD

I imagine the same thing could be achieved with

    origin/$(git rev-parse --abbrev-ref HEAD)

Sure, but:

1. It is a lot shorter to type @{push}. ;)

2. Using @{push} works everywhere, even on my non-triangular repos, because it takes into account the push configuration. So it's a much nicer muscle-memory to acquire.

-Peff

#162 lore raw

Triangular workflow

Quoted 7 lines

Sure, but:

1. It is a lot shorter to type @{push}. ;)

2. Using @{push} works everywhere, even on my non-triangular repos, because it takes into account the push configuration. So it's a much nicer muscle-memory to acquire.

Makes sense, I’m all out of arguments here 😅 Please don’t take this the wrong way, but it reminds me of this XKCD: https://xkcd.com/1172/

As long as this is turned on by default then I’m mostly happy. Maybe JCH and others can weigh in on if this needs to be this configurable or always on.

Harald

#163 lore raw

Re: Triangular workflow

Quoted 10 lines · Harald Nordgren
Quoted 7 lines

Sure, but:

1. It is a lot shorter to type @{push}. ;)

2. Using @{push} works everywhere, even on my non-triangular repos, because it takes into account the push configuration. So it's a much nicer muscle-memory to acquire.

Makes sense, I’m all out of arguments here 😅 Please don’t take this the wrong way, but it reminds me of this XKCD: https://xkcd.com/1172/

Perhaps if the person in that comic was the one who implemented @{push} in the first place. ;)

That said, I really don't think my use case is exotic. I've set up "git push" in the only way that makes sense for my triangular setup. I just don't happen to use it that often, and I don't want to be reminded me unnecessarily of the relationship between each branch and its push destination.

Quoted 3 lines

As long as this is turned on by default then I’m mostly happy. Maybe JCH and others can weigh in on if this needs to be this configurable or always on.

IMHO not making it configurable is a show-stopper, because without that it leaves people who don't like the new behavior with no escape hatch.

As to the default for that config option, I don't have a strong opinion. I'd lean towards "off", as I do not find the feature that compelling myself, and I don't recall ever hearing of it as a pain point for other Git users. But I also recognize that's based on vague vibes, and I think users are generally not very savvy about setting up triangular workflows in the first place.

-Peff

#164 lore raw

Re: Triangular workflow

In reply to Jeff King
Quoted 8 lines · Jeff King
Quoted 6 lines
Quoted 6 lines

Yeah, though @{push} is usually not explicitly configured in the same way @{upstream} is, but rather a consequence of how push.default and remote.pushdefault interact. But it was added for exactly this kind of triangular workflow. I sometimes will do stuff like:
git range-diff origin @{push} HEAD

Quoted 8 lines
Quoted 2 lines

I imagine the same thing could be achieved with
origin/$(git rev-parse --abbrev-ref HEAD)

Sure, but:

1. It is a lot shorter to type @{push}. ;)

2. Using @{push} works everywhere, even on my non-triangular repos,

Just so I’m clear, this is only with push.default=current, right? I could never make @{push} work otherwise.

#165 lore raw

Re: Triangular workflow

Quoted 10 lines · Ben Knoble
Quoted 10 lines · Jeff King
Quoted 8 lines
Quoted 6 lines

Yeah, though @{push} is usually not explicitly configured in the same way @{upstream} is, but rather a consequence of how push.default and remote.pushdefault interact. But it was added for exactly this kind of triangular workflow. I sometimes will do stuff like:
git range-diff origin @{push} HEAD

I imagine the same thing could be achieved with
origin/$(git rev-parse --abbrev-ref HEAD)

Quoted 7 lines
Quoted 5 lines

Sure, but:

1. It is a lot shorter to type @{push}. ;)

2. Using @{push} works everywhere, even on my non-triangular repos,

Just so I’m clear, this is only with push.default=current, right? I could never make @{push} work otherwise.

I always use push.default=current, though I think @{push} should work with other modes. E.g., with this setup:

git checkout -b foo
git clone . tmp
cd tmp

  # for the sake of simplicity, our triangle goes to the same place ;)
  git remote add triangle ..
  git fetch triangle
  git config remote.pushdefault triangle

then doing:

  git -c push.default=current rev-parse --symbolic-full-name @{push}

and:

  git -c push.default=matching rev-parse --symbolic-full-name @{push}

should both point to refs/remotes/triangle/foo. Using "simple" will not work, because it demands that the upstream and the push destination are the same (so it doesn't really make sense in a triangular flow at all).

But in a non-triangular flow, it will happily point @{push} to the same as @{upstream}. I use a triangular flow for git.git, but most of my other repos are just personal projects, and I push/fetch from a single central source.

-Peff

#166 lore raw

Re: Triangular workflow

Quoted 37 lines · Jeff King
Quoted 10 lines · Ben Knoble
Quoted 10 lines · Jeff King
Quoted 8 lines
Quoted 6 lines

Yeah, though @{push} is usually not explicitly configured in the same way @{upstream} is, but rather a consequence of how push.default and remote.pushdefault interact. But it was added for exactly this kind of triangular workflow. I sometimes will do stuff like:
git range-diff origin @{push} HEAD

I imagine the same thing could be achieved with
origin/$(git rev-parse --abbrev-ref HEAD)

Quoted 7 lines
Quoted 5 lines

Sure, but:

1. It is a lot shorter to type @{push}. ;)

2. Using @{push} works everywhere, even on my non-triangular repos,

Just so I’m clear, this is only with push.default=current, right? I could never make @{push} work otherwise.

I always use push.default=current, though I think @{push} should work with other modes. E.g., with this setup:

git checkout -b foo
git clone . tmp
cd tmp

  # for the sake of simplicity, our triangle goes to the same place ;)
  git remote add triangle ..
  git fetch triangle
  git config remote.pushdefault triangle

then doing:

  git -c push.default=current rev-parse --symbolic-full-name @{push}
Quoted 7 lines

and:

  git -c push.default=matching rev-parse --symbolic-full-name @{push}

should both point to refs/remotes/triangle/foo. Using "simple" will not work, because it demands that the upstream and the push destination are the same (so it doesn't really make sense in a triangular flow at all).

Gotcha. Yeah, simple (the default) doesn't work. I suppose upstream
might, too. (I also have push.default=current globally, was just
wondering about what the minimum was to enabled triangular workflows;
see <https://lore.kernel.org/git/CALnO6CAUSU-Pq_r-WYm3o0to6H8MdqiYOuoKaRfL1PTt30VaoQ@mail.gmail.com/>.)

Quoted 6 lines

But in a non-triangular flow, it will happily point @{push} to the same as @{upstream}. I use a triangular flow for git.git, but most of my other repos are just personal projects, and I push/fetch from a single central source.

-Peff

Ditto, yeah (_sometimes_ I actually do the triangle origin/main, local branch, origin/branch, so I almost always set upstream as origin/main anyways). Cool and thanks!

Signature · 1 line
D. Ben Knoble
#167 lore raw

Re: Triangular workflow

In reply to Jeff King
Quoted 3 lines · Jeff King

So even if I only do it infrequently, it feels weird that a bare "git push" would try to push to the upstream remote (which I don't even have write access to!).

Yes, exactly. I was wondering where Harald's suggestion to swap the two remotes came from.

Quoted 5 lines

Yeah, though @{push} is usually not explicitly configured in the same way @{upstream} is, but rather a consequence of how push.default and remote.pushdefault interact. But it was added for exactly this kind of triangular workflow. I sometimes will do stuff like:

Quoted 7 lines

git range-diff origin @{push} HEAD

to compare two iterations of a branch if I know that I haven't pushed. It is a bit of a cheat, because what I really mean is "do a range-diff since the last thing I sent to the list". But if I have just been working on a branch, and I haven't run an integration cycle since then, then I know that the pushed version will match it.

Great suggestion. It is fun to see that comparing notes among people with different workflows brings out these gems ;-)

Quoted 3 lines

There is also branch.*.pushRemote, but I have not found that useful (for my triangular flow there is always a single repo to push to, not one per branch).

#168 lore raw

Re: Triangular workflow

In reply to Jeff King
Quoted 5 lines · Jeff King

And having the extra output from "git checkout" is just extra noise for me, especially because it is easy to see only the second message (which looks just like the upstream ahead/behind message, of course) and get confused. The first time I saw it I thought I had misconfigured something with my branch.

It now is clear to me that this should be _optional_, so that those who do really want extra output from the command should explicitly opt into the feature. After all, any optional new feature that you must opt into by definition cannot regress end user experience for those who do not ;-)

At the same time, I suspect that extra comparison on top of what we already give against the @{upstream} may not be limited to what Harald implemented (is it essentially the same as specyfing @{push}, or something else?). I wonder if we can come up with a flexible and extensible notation to specify what branch(es) to compare with, so that we can use it as the value of this opt-in configuration variable? Something like

	[status] compareBranches = @{upstream} @{push}

signals that the current branch is compared against these two branches, and not having the configuration (i.e., traditional behaviour, which is left the default) would be equivalent to have

	[status] compareBranches = @{upstream}

or something like that, perhaps?

Thanks.

#169 lore raw

Re: Triangular workflow

Quoted 8 lines · Junio C Hamano
Quoted 5 lines · Jeff King

And having the extra output from "git checkout" is just extra noise for me, especially because it is easy to see only the second message (which looks just like the upstream ahead/behind message, of course) and get confused. The first time I saw it I thought I had misconfigured something with my branch.

Quoted 5 lines

It now is clear to me that this should be _optional_, so that those who do really want extra output from the command should explicitly opt into the feature. After all, any optional new feature that you must opt into by definition cannot regress end user experience for those who do not ;-)

True, but then it also cannot pleasantly surprise people who didn't realize they wanted it.

Having your user experience regressed and then tweaking a config option to fix it is not too bad. The deciding factor to me is whether more people will be pleasantly surprised or annoyed. ;) I don't have a strong sense there.

As a general principle, though, I think a reasonable path forward for any behavior change is:

1. Implement the new behavior, hidden behind a config option.

2. Wait a while to see how people like the new option, and shake out any bugs.

3. If people like the option and are puzzled why it isn't the default, then flip the default on.

In other words, let the utility of the feature be proven in practice by people opting into it. There is a chicken-and-egg problem if they don't know about it, but if it is truly solving a problem people have, then hopefully some of them would look for a solution and find it.

End philosophical rambling. ;)

Quoted 4 lines

At the same time, I suspect that extra comparison on top of what we already give against the @{upstream} may not be limited to what Harald implemented (is it essentially the same as specyfing @{push}, or something else?).

I haven't been following the feature closely, but my understanding is that yes, it's basically "also compare to @{push} if it is not the same as @{upstream}".

Quoted 6 lines

I wonder if we can come up with a flexible and extensible notation to specify what branch(es) to compare with, so that we can use it as the value of this opt-in configuration variable? Something like

	[status] compareBranches = @{upstream} @{push}
Quoted 7 lines

signals that the current branch is compared against these two branches, and not having the configuration (i.e., traditional behaviour, which is left the default) would be equivalent to have

	[status] compareBranches = @{upstream}

or something like that, perhaps?

Interesting. That is more flexible, though I'm not sure how useful that flexibility is. I guess you could imagine putting in a static branch. E.g., if you base your branches off of "master", might you want to show ahead/behind to "maint" or "next"? I have trouble imagining a workflow where I would want to do that often enough for git-status (and checkout) to do it automatically, though.

But assuming it suppresses duplicates, then yeah, this feels like a more flexible superset of the functionality.

-Peff

#170 lore raw

Re: Triangular workflow

Quoted 36 lines · Jeff King
Quoted 11 lines · Junio C Hamano
Quoted 5 lines · Jeff King

And having the extra output from "git checkout" is just extra noise for me, especially because it is easy to see only the second message (which looks just like the upstream ahead/behind message, of course) and get confused. The first time I saw it I thought I had misconfigured something with my branch.

It now is clear to me that this should be _optional_, so that those who do really want extra output from the command should explicitly opt into the feature. After all, any optional new feature that you must opt into by definition cannot regress end user experience for those who do not ;-)

True, but then it also cannot pleasantly surprise people who didn't realize they wanted it.

Having your user experience regressed and then tweaking a config option to fix it is not too bad. The deciding factor to me is whether more people will be pleasantly surprised or annoyed. ;) I don't have a strong sense there.

As a general principle, though, I think a reasonable path forward for any behavior change is:

1. Implement the new behavior, hidden behind a config option.

2. Wait a while to see how people like the new option, and shake out any bugs.

3. If people like the option and are puzzled why it isn't the default, then flip the default on.

Quoted 6 lines

In other words, let the utility of the feature be proven in practice by people opting into it. There is a chicken-and-egg problem if they don't know about it, but if it is truly solving a problem people have, then hopefully some of them would look for a solution and find it.

End philosophical rambling. ;)

Agreed generally, but the chicken-egg goes 2 layers deep here due to triangular workflows ;)

I favor something similar to what Junio described but also including @{push} by default (and ignoring it if non-existent), so that folks discovering triangular workflows for the first time are easily able to see what is happening.

Us ”already triangular” squares are probably well-versed enough in Git to find and tweak the new feature if desired.

Idk though. I think more folks at work should try triangular flows, so I’m biased :p

#171 lore raw

Triangular workflow

Quoted 7 lines

Agreed generally, but the chicken-egg goes 2 layers deep here due to triangular workflows ;)

I favor something similar to what Junio described but also including @{push} by default (and ignoring it if non-existent), so that folks discovering > triangular workflows for the first time are easily able to see what is happening.

Us ”already triangular” squares are probably well-versed enough in Git to find and tweak the new feature if desired.

Idk though. I think more folks at work should try triangular flows, so I’m biased :p

Ben has an excellent point about triangular workflows, and Jeff of course, your philosophical instincts seem correct to me, interesting to read!

I would think that most people don't have separate tracking and push branches to begin with. So it's not a lot of people would be bothered by having this be on by default. The people who know about triangular workflows will also be able to find how to turn this off.

Harald

#172 lore raw

Re: Triangular workflow

In reply to Ben Knoble

(I'm somewhat behind all week so this is from Wednesday...)

Re: status.compareBranch (or similar): I like the idea; I'm not sure what sort of details might be needed in the end though.

Quoted 1 line · Ben Knoble

I favor something similar to what Junio described but also including @{push} by default (and ignoring it if non-existent), so that folks discovering triangular workflows for the first time are easily able to see what is happening.

This also seems to me likely to be the right default. It's useful for a lot of GitHub and similar forges, where you send fixes upstream by first forking some official repository and then cloning your fork (e.g., to a laptop), setting up your local clone (on laptop) to have two remotes: the official repository, and your fork, both on the same forge.

It's a little annoying to have to deal with *three* potential "upstream" repos, if
you need to back up your local work to a corporate server or forge *plus* a push-for-pull-request at the forge as well, of course. But then at least the "compare with all three" option becomes available. How you wish to spell the default push location is then up to you :-)

Chris

#173 lore raw

Triangular workflow

Quoted 10 lines
Quoted 4 lines

I favor something similar to what Junio described but also including @{push} by default (and ignoring it if non-existent), so that folks discovering triangular workflows for the first time are easily able to see what is happening.

This also seems to me likely to be the right default. It's useful for a lot of GitHub and similar forges, where you send fixes upstream by first forking some official repository and then cloning your fork (e.g., to a laptop), setting up your local clone (on laptop) to have two remotes: the official repository, and your fork, both on the same forge.

I agree with this too, would like to make '@{upstream} @{push}' the default.

Harald

#174 lore raw

Triangular workflow

In reply to Junio C Hamano
Quoted 6 lines

I wonder if we can come up with a flexible and extensible notation to specify what branch(es) to compare with, so that we can use it as the value of this opt-in configuration variable? Something like

        [status] compareBranches = @{upstream} @{push}

If we go with that, then it becomes trickier code-wise to show push/pull advice for the correct branches. But not impossible since we can check if branch is the same as @{upstream} or @{push}.

Philosophically, two main git commands are 'git push' and 'git pull'. So it makes perfect sense to me to signal that those two are special, and not allow other compareBranches.

Harald

#175 lore raw

Re: Triangular workflow

Quoted 7 lines · Harald Nordgren
Quoted 6 lines

I wonder if we can come up with a flexible and extensible notation to specify what branch(es) to compare with, so that we can use it as the value of this opt-in configuration variable? Something like

        [status] compareBranches = @{upstream} @{push}
Quoted 7 lines

If we go with that, then it becomes trickier code-wise to show push/pull advice for the correct branches. But not impossible since we can check if branch is the same as @{upstream} or @{push}.

Philosophically, two main git commands are 'git push' and 'git pull'. So it makes perfect sense to me to signal that those two are special, and not allow other compareBranches.

You are falling into the same trap as what the folks who designed the original ahead/behind comparison did by limiting yourself to push and pull. They said object transfer always interacts with the @{upstream}, hence only comparison with that is sufficient and it makes sense not to allow comparing with anything else.

Until you started wishing that you want to also compare with @{push}, that is ;-).

A static point that has nothing to do with pushing and pulling (e.g., "the latest official release tag") may be something useful to compare with in certain workflows (presumably workflow of the maintainer type), so limiting our sights to object transfer like push and pull is likely to be a mistake.

#176 lore raw

Triangular workflow

In reply to Junio C Hamano
Quoted 22 lines
Quoted 5 lines

And having the extra output from "git checkout" is just extra noise for me, especially because it is easy to see only the second message (which looks just like the upstream ahead/behind message, of course) and get confused. The first time I saw it I thought I had misconfigured something with my branch.

It now is clear to me that this should be _optional_, so that those who do really want extra output from the command should explicitly opt into the feature. After all, any optional new feature that you must opt into by definition cannot regress end user experience for those who do not ;-)

At the same time, I suspect that extra comparison on top of what we already give against the @{upstream} may not be limited to what Harald implemented (is it essentially the same as specyfing @{push}, or something else?). I wonder if we can come up with a flexible and extensible notation to specify what branch(es) to compare with, so that we can use it as the value of this opt-in configuration variable? Something like

	[status] compareBranches = @{upstream} @{push}
Quoted 7 lines

signals that the current branch is compared against these two branches, and not having the configuration (i.e., traditional behaviour, which is left the default) would be equivalent to have

	[status] compareBranches = @{upstream}

or something like that, perhaps?

I'm implemented this now. Please take a look at the latest patch!

But there seems to be a memory leak that I can't figure out after spending some hours running the CI over and over.

Harald

#177 lore raw

Re: [PATCH v25 2/2] status: show comparison with push remote tracking branch

In reply to Jeff King
Quoted 8 lines · Jeff King

Can we make this configurable?

I build my daily driver off of the 'jch' branch, which now includes this series, and I've found that for my triangular workflow the ahead/behind for the push branch is just useless noise. I treat my push destination like a mirror, where I always just push up everything at the end of the day.

This is a slight tangent but if we're taking about making things configurable I've never found the actual values of the ahead/behind numbers particularly useful. In a triangular workflow all I'm really interested in is whether (a) my local branch is behind the upstream branch and if so is it going to be a pig to rebase it, and (b) my local branch matches the push branch (i.e. do I need to run "git push"). I don't really care exactly how many commits ahead or behind the local branch is, the counts are just distracting and don't really answer the "is it going to be a pig to rebase" question - the only way to answer that is to try it and see.

So as well as configuring whether we show the comparison the push branch it would be nice to be able to configure a simpler output as well. That does not need to be part of this series but perhaps we should design the configuration to be extendable.

Thanks

Phillip

Quoted 10 lines

I know that the output can be disabled with status.aheadbehind, but:

1. I noticed this first via "git checkout", which does not have such a flag (AFAIK).

2. That flag would also disable the upstream branch ahead/behind output, which I do find useful.

-Peff

#178 lore raw

Re: [PATCH v25 2/2] status: show comparison with push remote tracking branch

Quoted 4 lines · Phillip Wood

So as well as configuring whether we show the comparison the push branch it would be nice to be able to configure a simpler output as well. That does not need to be part of this series but perhaps we should design the configuration to be extendable.

So, there are orthogonal axes that might benefit from configurabiliity.

What other branch to compare (e.g., @{upstream}, @{push}, something else?) and how detailed comparison we want (e.g., only tell me when we are ahead with just a single bit, give me ahead/behind count, only tell me when we are behind with just a single bit, etc.).

There may be other interesting axes people may be able to come up with in the future. We do not have to be exhaustive on values on all axes on the first day, but it would be nice to have a fairly complete coverage of what axes matter to users, and it is a great suggestion that the level of details can be one. Thanks.

#179 lore raw

Re: [PATCH v25 2/2] status: show comparison with push remote tracking branch

In reply to Phillip Wood
Quoted 10 lines · Phillip Wood
Quoted 8 lines · Jeff King

Can we make this configurable?

I build my daily driver off of the 'jch' branch, which now includes this series, and I've found that for my triangular workflow the ahead/behind for the push branch is just useless noise. I treat my push destination like a mirror, where I always just push up everything at the end of the day.

Quoted 9 lines

This is a slight tangent but if we're taking about making things configurable I've never found the actual values of the ahead/behind numbers particularly useful. In a triangular workflow all I'm really interested in is whether (a) my local branch is behind the upstream branch and if so is it going to be a pig to rebase it, and (b) my local branch matches the push branch (i.e. do I need to run "git push"). I don't really care exactly how many commits ahead or behind the local branch is, the counts are just distracting and don't really answer the "is it going to be a pig to rebase" question - the only way to answer that is to try it and see.

I mostly feel the same way. The only time I recall thinking the numbers were important is when they are wildly out of place. You "git checkout" a branch and it is 1500 commits ahead/behind or something, and you know you've screwed up setting the tracking branch somehow. ;)

Quoted 4 lines

So as well as configuring whether we show the comparison the push branch it would be nice to be able to configure a simpler output as well. That does not need to be part of this series but perhaps we should design the configuration to be extendable.

Yeah, I agree with all of that.

-Peff

#180 lore raw

v26 cover status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected] cc:
Jeff King [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: add status.compareBranches config for multiple branch
comparisons

 Documentation/config/status.adoc |  20 ++
 remote.c                         | 187 ++++++++++++++----
 t/t6040-tracking-info.sh         | 322 +++++++++++++++++++++++++++++++
 3 files changed, 492 insertions(+), 37 deletions(-)

base-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v26
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v26
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v25:

 1:  fd05c7b778 = 1:  27a46f8d9c refactor format_branch_comparison in preparation
 2:  fa744efc59 ! 2:  caa761f615 status: show comparison with push remote tracking branch
     @@ Metadata
      Author: Harald Nordgren <[email protected]>
      
       ## Commit message ##
     -    status: show comparison with push remote tracking branch
     +    status: add status.compareBranches config for multiple branch comparisons
      
     -    "git status" on a branch that follows a remote branch compares
     -    commits on the current branch and the remote-tracking branch it
     -    builds upon, to show "ahead", "behind", or "diverged" status.
     +    Add a new configuration variable `status.compareBranches` that allows
     +    users to specify a space-separated list of branches to compare against
     +    the current branch in `git status` output.
      
     -    When working on a feature branch that tracks a remote feature branch,
     -    but you also want to track progress relative to the push destination
     -    tracking branch (which may differ from the upstream branch), git status
     -    now shows an additional comparison.
     +    Each branch in the list can be:
     +    - A remote-tracking branch name (e.g., `origin/main`)
     +    - The special reference `@{upstream}` for the tracking branch
     +    - The special reference `@{push}` for the push destination
      
     -    When the upstream tracking branch differs from the push destination
     -    tracking branch, git status shows both the comparison with the upstream
     -    tracking branch (as before) and an additional comparison with the push
     -    destination tracking branch. The push branch comparison appears on a
     -    separate line after the upstream branch status, using the same format.
     +    When not configured, the default behavior is equivalent to setting
     +    `status.compareBranches = @{upstream}`, preserving backward compatibility.
      
     -    Example output when tracking origin/main but push destination is
     -    origin/feature:
     -        On branch feature
     -        Your branch and 'origin/main' have diverged,
     -        and have 3 and 1 different commits each, respectively.
     -          (use "git pull" if you want to integrate the remote branch with yours)
     +    The advice messages shown are context-aware:
     +    - "git pull" advice is shown only when comparing against @{upstream}
     +    - "git push" advice is shown only when comparing against @{push}
     +    - Divergence advice is shown for upstream branch comparisons
      
     -        Your branch is ahead of 'origin/feature' by 1 commit.
     -          (use "git push" to publish your local commits)
     +    This is useful for triangular workflows where the upstream tracking
     +    branch differs from the push destination, allowing users to see their
     +    status relative to both branches at once.
      
     -    The comparison is only shown when the push destination tracking branch
     -    differs from the upstream tracking branch, even if they are on the same
     -    remote.
     +    Example configuration:
     +        [status]
     +            compareBranches = @{upstream} @{push}
      
          Signed-off-by: Harald Nordgren <[email protected]>
      
     + ## Documentation/config/status.adoc ##
     +@@ Documentation/config/status.adoc: status.aheadBehind::
     + 	`--no-ahead-behind` by default in linkgit:git-status[1] for
     + 	non-porcelain status formats.  Defaults to true.
     + 
     ++status.compareBranches::
     ++	A space-separated list of branches to compare the current branch
     ++	against in linkgit:git-status[1]. Each branch specification can be
     ++	a remote-tracking branch name (e.g. `origin/main`), or a special
     ++	reference like `@{upstream}` or `@{push}`. For each branch in the
     ++	list, git status shows whether the current branch is ahead, behind,
     ++	or has diverged from that branch.
     +++
     ++If not set, the default behavior is equivalent to `@{upstream}`, which
     ++compares against the configured upstream tracking branch.
     +++
     ++Example:
     +++
     ++----
     ++[status]
     ++	compareBranches = origin/main origin/develop
     ++----
     +++
     ++This would show comparisons against both `origin/main` and `origin/develop`.
     ++
     + status.displayCommentPrefix::
     + 	If set to true, linkgit:git-status[1] will insert a comment
     + 	prefix before each output line (starting with
     +
       ## remote.c ##
      @@
       
     @@ remote.c
       struct counted_string {
       	size_t len;
       	const char *s;
     +@@ remote.c: static void branch_release(struct branch *branch)
     + 	free((char *)branch->refname);
     + 	free(branch->remote_name);
     + 	free(branch->pushremote_name);
     ++	free((char *)branch->push_tracking_ref);
     + 	merge_clear(branch);
     + }
     + 
      @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
       	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
       }
       
     -+static char *get_remote_push_branch(struct branch *branch)
     ++static char *resolve_compare_branch(struct branch *branch, const char *name)
      +{
     -+	struct remote *remote;
     -+	const char *push_remote;
     -+	char *push_dst = NULL;
     -+	char *tracking_ref;
     -+	const char *resolved;
     ++	struct strbuf buf = STRBUF_INIT;
     ++	const char *resolved = NULL;
      +	char *ret;
      +
     -+	if (!branch)
     -+		return NULL;
     -+
     -+	push_remote = pushremote_for_branch(branch, NULL);
     -+	if (!push_remote)
     ++	if (!branch || !name)
      +		return NULL;
      +
     -+	remote = remotes_remote_get(the_repository, push_remote);
     -+	if (!remote)
     -+		return NULL;
     ++	if (!strcasecmp(name, "@{upstream}") || !strcasecmp(name, "@{u}"))
     ++		resolved = branch_get_upstream(branch, NULL);
     ++	else if (!strcasecmp(name, "@{push}"))
     ++		resolved = branch_get_push(branch, NULL);
      +
     -+	push_dst = remote_ref_for_branch(branch, 1);
     -+	if (!push_dst) {
     -+		if (remote->push.nr)
     -+			return NULL;
     -+		push_dst = xstrdup(branch->refname);
     -+	}
     -+
     -+	tracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL);
     -+	free(push_dst);
     -+
     -+	if (!tracking_ref)
     -+		return NULL;
     ++	if (resolved)
     ++		return xstrdup(resolved);
      +
     ++	strbuf_addf(&buf, "refs/remotes/%s", name);
      +	resolved = refs_resolve_ref_unsafe(
      +		get_main_ref_store(the_repository),
     -+		tracking_ref,
     ++		buf.buf,
      +		RESOLVE_REF_READING,
      +		NULL, NULL);
     -+
     -+	if (!resolved) {
     -+		free(tracking_ref);
     -+		return NULL;
     ++	if (resolved) {
     ++		ret = xstrdup(resolved);
     ++		strbuf_release(&buf);
     ++		return ret;
      +	}
      +
     -+	ret = xstrdup(resolved);
     -+	free(tracking_ref);
     -+	return ret;
     ++	strbuf_release(&buf);
     ++	return NULL;
      +}
      +
       static void format_branch_comparison(struct strbuf *sb,
     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,
       				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
       	}
      @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
     - 	const char *full_base;
     - 	char *base;
     - 	int upstream_is_gone = 0;
     -+	unsigned base_branch_flags = ENABLE_ADVICE_PULL | ENABLE_ADVICE_PUSH;
     -+	int push_ours, push_theirs, push_cmp_fetch;
     -+	char *full_push;
     -+	char *push = NULL;
     -+	unsigned push_branch_flags = 0;
     + 			 enum ahead_behind_flags abf,
     + 			 int show_divergence_advice)
     + {
     +-	int ours, theirs, cmp_fetch;
     +-	const char *full_base;
     +-	char *base;
     +-	int upstream_is_gone = 0;
     +-
     +-	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
     +-	if (cmp_fetch < 0) {
     +-		if (!full_base)
     +-			return 0;
     +-		upstream_is_gone = 1;
     ++	char *compare_branches_config = NULL;
     ++	struct string_list compare_branches = STRING_LIST_INIT_DUP;
     ++	struct string_list_item *item;
     ++	int reported = 0;
     ++	size_t i;
     ++	const char *upstream_ref;
     ++	const char *push_ref;
     ++
     ++	repo_config_get_string(the_repository, "status.comparebranches",
     ++			       &compare_branches_config);
     ++
     ++	if (compare_branches_config) {
     ++		string_list_split(&compare_branches, compare_branches_config,
     ++				  " ", -1);
     ++		string_list_remove_empty_items(&compare_branches, 0);
     ++	} else {
     ++		string_list_append(&compare_branches, "@{upstream}");
     + 	}
       
     - 	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
     - 	if (cmp_fetch < 0) {
     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
     - 	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
     - 					    full_base, 0);
     +-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
     +-					    full_base, 0);
     ++	upstream_ref = branch_get_upstream(branch, NULL);
     ++	push_ref = branch_get_push(branch, NULL);
       
     -+	full_push = get_remote_push_branch(branch);
     -+	if (full_push) {
     -+		push = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
     -+						     full_push, 0);
     -+		if (push && base && strcmp(base, push)) {
     -+			push_cmp_fetch = stat_branch_pair(branch->refname, full_push,
     -+						   &push_ours, &push_theirs, abf);
     -+			if (push_cmp_fetch >= 0) {
     -+				base_branch_flags = ENABLE_ADVICE_PULL;
     -+				push_branch_flags = ENABLE_ADVICE_PUSH;
     +-	if (upstream_is_gone) {
     +-		strbuf_addf(sb,
     +-			_("Your branch is based on '%s', but the upstream is gone.\n"),
     +-			base);
     +-		if (advice_enabled(ADVICE_STATUS_HINTS))
     +-			strbuf_addstr(sb,
     +-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
     +-	} else {
     +-		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
     ++	for (i = 0; i < compare_branches.nr; i++) {
     ++		char *full_ref;
     ++		char *short_ref;
     ++		int ours, theirs, cmp;
     ++		int is_upstream, is_push;
     ++		unsigned flags = 0;
     ++
     ++		item = &compare_branches.items[i];
     ++		full_ref = resolve_compare_branch(branch, item->string);
     ++		if (!full_ref)
     ++			continue;
     ++
     ++		short_ref = refs_shorten_unambiguous_ref(
     ++			get_main_ref_store(the_repository), full_ref, 0);
     ++
     ++		is_upstream = upstream_ref && !strcmp(full_ref, upstream_ref);
     ++		is_push = push_ref && !strcmp(full_ref, push_ref);
     ++
     ++		if (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))
     ++			is_push = 1;
     ++
     ++		cmp = stat_branch_pair(branch->refname, full_ref,
     ++				       &ours, &theirs, abf);
     ++
     ++		if (cmp < 0) {
     ++			if (is_upstream) {
     ++				strbuf_addf(sb,
     ++					_("Your branch is based on '%s', but the upstream is gone.\n"),
     ++					short_ref);
     ++				if (advice_enabled(ADVICE_STATUS_HINTS))
     ++					strbuf_addstr(sb,
     ++						_("  (use \"git branch --unset-upstream\" to fixup)\n"));
     ++				reported = 1;
      +			}
     ++			free(full_ref);
     ++			free(short_ref);
     ++			continue;
      +		}
     -+	}
      +
     - 	if (upstream_is_gone) {
     - 		strbuf_addf(sb,
     - 			_("Your branch is based on '%s', but the upstream is gone.\n"),
     -@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
     - 			strbuf_addstr(sb,
     - 				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
     - 	} else {
     --		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
     -+		if (show_divergence_advice)
     -+			base_branch_flags |= ENABLE_ADVICE_DIVERGENCE;
     -+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf,
     -+					 base_branch_flags);
     -+	}
     -+
     -+	if (push_branch_flags & ENABLE_ADVICE_PUSH) {
     -+		strbuf_addstr(sb, "\n");
     -+		format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf,
     -+					 push_branch_flags);
     ++		if (reported)
     ++			strbuf_addstr(sb, "\n");
     ++
     ++		if (is_upstream)
     ++			flags |= ENABLE_ADVICE_PULL;
     ++		if (is_push)
     ++			flags |= ENABLE_ADVICE_PUSH;
     ++		if (show_divergence_advice && is_upstream)
     ++			flags |= ENABLE_ADVICE_DIVERGENCE;
     ++		format_branch_comparison(sb, !cmp, ours, theirs, short_ref,
     ++					 abf, flags);
     ++		reported = 1;
     ++
     ++		free(full_ref);
     ++		free(short_ref);
       	}
       
     - 	free(base);
     -+	free(full_push);
     -+	free(push);
     - 	return 1;
     +-	free(base);
     +-	return 1;
     ++	string_list_clear(&compare_branches, 0);
     ++	free(compare_branches_config);
     ++	return reported;
       }
       
     + static int one_local_ref(const struct reference *ref, void *cb_data)
      
       ## t/t6040-tracking-info.sh ##
      @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status shows ahead of both origin/main and feature branch' '
     ++test_expect_success 'setup for compareBranches tests' '
     ++	(
     ++		cd test &&
     ++		git config push.default current &&
     ++		git config status.compareBranches "@{upstream} @{push}"
     ++	)
     ++'
     ++
     ++test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '
      +	(
      +		cd test &&
      +		git checkout -b feature2 origin/main &&
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'checkout shows ahead of both origin/main and feature branch' '
     ++test_expect_success 'checkout with status.compareBranches shows both branches' '
      +	(
      +		cd test &&
      +		git checkout feature2 >../actual
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	)
      +'
      +
     -+test_expect_success 'status shows diverged from origin/main and ahead of feature branch' '
     ++test_expect_success 'status.compareBranches shows diverged and ahead' '
      +	(
      +		cd test &&
      +		git checkout feature4 &&
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '
     ++test_expect_success 'status --no-ahead-behind with status.compareBranches' '
      +	(
      +		cd test &&
      +		git checkout feature4 &&
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	)
      +'
      +
     -+test_expect_success 'status with upstream remote and push.default set to origin' '
     ++test_expect_success 'status.compareBranches with upstream and origin remotes' '
      +	(
      +		cd test &&
      +		git checkout -b feature5 upstream/main &&
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status with upstream remote and push.default set to origin and diverged' '
     ++test_expect_success 'status.compareBranches with upstream and origin remotes multiple compare branches' '
      +	(
      +		cd test &&
      +		git checkout -b feature6 upstream/main &&
     ++		git push origin &&
     ++		advance work &&
     ++		git -c status.compareBranches="upstream/main origin/feature6 origin/feature5" status >../actual
     ++	) &&
     ++	cat >expect <<-EOF &&
     ++	On branch feature6
     ++	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
     ++
     ++	Your branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.
     ++	  (use "git push" to publish your local commits)
     ++
     ++	Your branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit.
     ++
     ++	nothing to commit, working tree clean
     ++	EOF
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'status.compareBranches with diverged push branch' '
     ++	(
     ++		cd test &&
     ++		git checkout -b feature7 upstream/main &&
      +		advance work &&
      +		git push origin &&
      +		git reset --hard upstream/main &&
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +		git status >../actual
      +	) &&
      +	cat >expect <<-EOF &&
     -+	On branch feature6
     ++	On branch feature7
      +	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
      +
     -+	Your branch and ${SQ}origin/feature6${SQ} have diverged,
     ++	Your branch and ${SQ}origin/feature7${SQ} have diverged,
      +	and have 1 and 1 different commits each, respectively.
      +
      +	nothing to commit, working tree clean
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status with upstream remote and push branch up to date' '
     ++test_expect_success 'status.compareBranches shows up to date branches' '
      +	(
      +		cd test &&
     -+		git checkout -b feature7 upstream/main &&
     ++		git checkout -b feature8 upstream/main &&
      +		git push origin &&
      +		git status >../actual
      +	) &&
      +	cat >expect <<-EOF &&
     -+	On branch feature7
     ++	On branch feature8
      +	Your branch is up to date with ${SQ}upstream/main${SQ}.
      +
     -+	Your branch is up to date with ${SQ}origin/feature7${SQ}.
     ++	Your branch is up to date with ${SQ}origin/feature8${SQ}.
      +
      +	nothing to commit, working tree clean
      +	EOF
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status --no-ahead-behind with upstream remote and push branch up to date' '
     ++test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' '
      +	(
      +		cd test &&
     -+		git checkout feature7 &&
     ++		git checkout feature8 &&
      +		git push origin &&
      +		git status --no-ahead-behind >../actual
      +	) &&
      +	cat >expect <<-EOF &&
     -+	On branch feature7
     ++	On branch feature8
      +	Your branch is up to date with ${SQ}upstream/main${SQ}.
      +
     -+	Your branch is up to date with ${SQ}origin/feature7${SQ}.
     ++	Your branch is up to date with ${SQ}origin/feature8${SQ}.
      +
      +	nothing to commit, working tree clean
      +	EOF
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'checkout shows push branch up to date' '
     ++test_expect_success 'checkout with status.compareBranches shows up to date' '
      +	(
      +		cd test &&
     -+		git checkout feature7 >../actual
     ++		git checkout feature8 >../actual
      +	) &&
      +	cat >expect <<-EOF &&
      +	Your branch is up to date with ${SQ}upstream/main${SQ}.
      +
     -+	Your branch is up to date with ${SQ}origin/feature7${SQ}.
     ++	Your branch is up to date with ${SQ}origin/feature8${SQ}.
      +	EOF
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status with upstream ahead and push branch up to date' '
     ++test_expect_success 'status.compareBranches with upstream behind and push up to date' '
      +	(
      +		cd test &&
      +		git checkout -b ahead upstream/main &&
      +		advance work &&
      +		git push upstream HEAD &&
     -+		git checkout -b feature8 upstream/main &&
     ++		git checkout -b feature9 upstream/main &&
      +		git push origin &&
      +		git branch --set-upstream-to upstream/ahead &&
      +		git status >../actual
      +	) &&
      +	cat >expect <<-EOF &&
     -+	On branch feature8
     ++	On branch feature9
      +	Your branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.
      +	  (use "git pull" to update your local branch)
      +
     -+	Your branch is up to date with ${SQ}origin/feature8${SQ}.
     ++	Your branch is up to date with ${SQ}origin/feature9${SQ}.
      +
      +	nothing to commit, working tree clean
      +	EOF
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status shows remapped push refspec' '
     ++test_expect_success 'status.compareBranches with remapped push refspec' '
      +	(
      +		cd test &&
     -+		git checkout -b feature9 origin/main &&
     -+		git config remote.origin.push refs/heads/feature9:refs/heads/remapped &&
     ++		git checkout -b feature10 origin/main &&
     ++		git config remote.origin.push refs/heads/feature10:refs/heads/remapped &&
      +		git push &&
      +		advance work &&
      +		git status >../actual
      +	) &&
      +	cat >expect <<-EOF &&
     -+	On branch feature9
     ++	On branch feature10
      +	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
      +
      +	Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status shows remapped push refspec with upstream remote' '
     ++test_expect_success 'status.compareBranches with remapped push and upstream remote' '
      +	(
      +		cd test &&
     -+		git checkout -b feature10 upstream/main &&
     -+		git config remote.origin.push refs/heads/feature10:refs/heads/remapped &&
     ++		git checkout -b feature11 upstream/main &&
     ++		git config remote.origin.push refs/heads/feature11:refs/heads/remapped &&
      +		git push origin &&
      +		advance work &&
      +		git status >../actual
      +	) &&
      +	cat >expect <<-EOF &&
     -+	On branch feature10
     ++	On branch feature11
      +	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
      +
      +	Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	EOF
      +	test_cmp expect actual
      +'
     ++
     ++test_expect_success 'clean up after compareBranches tests' '
     ++	(
     ++		cd test &&
     ++		git config --unset status.compareBranches
     ++	)
     ++'
      +
       test_done
Signature · 1 line
gitgitgadget
#181 lore raw

PATCH v26 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 48 insertions(+), 34 deletions(-)
remote.c +48 -34
index b756ff6f15..fd592ec659 100644
@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			       "and have %d and %d different commits each, "
 			       "respectively.\n",
 			   ours + theirs),
-			base, ours, theirs);
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#182 lore raw

PATCH v26 2/2 status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Add a new configuration variable status.compareBranches that allows users to specify a space-separated list of branches to compare against the current branch in git status output.

Each branch in the list can be:
- A remote-tracking branch name (e.g., origin/main)
- The special reference @{upstream} for the tracking branch
- The special reference @{push} for the push destination

When not configured, the default behavior is equivalent to setting status.compareBranches = @{upstream}, preserving backward compatibility.

The advice messages shown are context-aware:
- "git pull" advice is shown only when comparing against @{upstream}
- "git push" advice is shown only when comparing against @{push}
- Divergence advice is shown for upstream branch comparisons

This is useful for triangular workflows where the upstream tracking branch differs from the push destination, allowing users to see their status relative to both branches at once.

Example configuration:

    [status]
        compareBranches = @{upstream} @{push}
---
 Documentation/config/status.adoc |  20 ++
 remote.c                         | 157 ++++++++++++---
 t/t6040-tracking-info.sh         | 322 +++++++++++++++++++++++++++++++
 3 files changed, 470 insertions(+), 29 deletions(-)
Documentation/config/status.adoc +20 -0
index 8caf90f51c..ab54f25fbc 100644
@@ -17,6 +17,26 @@ status.aheadBehind::
 	`--no-ahead-behind` by default in linkgit:git-status[1] for
 	non-porcelain status formats.  Defaults to true.
 
+status.compareBranches::
+	A space-separated list of branches to compare the current branch
+	against in linkgit:git-status[1]. Each branch specification can be
+	a remote-tracking branch name (e.g. `origin/main`), or a special
+	reference like `@{upstream}` or `@{push}`. For each branch in the
+	list, git status shows whether the current branch is ahead, behind,
+	or has diverged from that branch.
++
+If not set, the default behavior is equivalent to `@{upstream}`, which
+compares against the configured upstream tracking branch.
++
+Example:
++
+----
+[status]
+	compareBranches = origin/main origin/develop
+----
++
+This would show comparisons against both `origin/main` and `origin/develop`.
+
 status.displayCommentPrefix::
 	If set to true, linkgit:git-status[1] will insert a comment
 	prefix before each output line (starting with
remote.c +128 -29

216 lines not on this page · raw message

t/t6040-tracking-info.sh +322 -0

326 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#183 lore raw

Re: [PATCH v26 2/2] status: add status.compareBranches config for multiple branch comparisons

remote.c static void branch_release(struct branch *branch) · line 284

Quoted 8 lines · Harald Nordgren via GitGitGadget
remote.c +1 -0
@@ -272,6 +278,7 @@ static void branch_release(struct branch *branch)
 	free((char *)branch->refname);
 	free(branch->remote_name);
 	free(branch->pushremote_name);
+	free((char *)branch->push_tracking_ref);
 	merge_clear(branch);
 }
 

I don't think this line is right. The push_tracking_ref field is const because it may or may not be a newly allocated string. In particular, with push.default=upstream, it will be a copy of the string returned from branch_get_upstream(), which is covered by the merge_clear() call. So we'd get a double-free.

This is due to some memory-management hackery in e291c75a95 (remote.c:
add branch_get_push, 2015-05-21). The good news is that not only is it not too hard to untangle this, but it is closely related to leaks you are seeing on your topic.

I'll post a series in a moment fixing both.

-Peff

#184 lore raw

Memory leak

Quoted 6 lines

This is due to some memory-management hackery in e291c75a95 (remote.c:
add branch_get_push, 2015-05-21). The good news is that not only is it not too hard to untangle this, but it is closely related to leaks you are seeing on your topic.

I'll post a series in a moment fixing both.

This seems to not have helped my memory leak. I squashed and applied your commits on top of my work as a separate pull request on GitHub, the CI leak tests are still failing:

https://github.com/git/git/pull/2170

Harald

#185 lore raw

Memory leak

My bad, it does fix it! I think I applied your patch incorrectly the first time around.

Harald

#186 lore raw

Re: Memory leak

Quoted 2 lines · Harald Nordgren

My bad, it does fix it! I think I applied your patch incorrectly the first time around.

I've queued your v26 on top of a merge of Peff's leakfix patches into 'master'. It hopefully will appear in today's pushout.

Thanks, both.

#187 lore raw

Re: Memory leak

Quoted 9 lines · Junio C Hamano
Quoted 2 lines · Harald Nordgren

My bad, it does fix it! I think I applied your patch incorrectly the first time around.

I've queued your v26 on top of a merge of Peff's leakfix patches into 'master'. It hopefully will appear in today's pushout.

Thanks, both.

So are we all happy with v26 of the topic?

#188 lore raw

Re: Memory leak

Quoted 7 lines · Junio C Hamano
Quoted 5 lines · Junio C Hamano
Quoted 2 lines · Harald Nordgren

My bad, it does fix it! I think I applied your patch incorrectly the first time around.

Quoted 6 lines
Quoted 4 lines

I've queued your v26 on top of a merge of Peff's leakfix patches into 'master'. It hopefully will appear in today's pushout.

Thanks, both.

So are we all happy with v26 of the topic?

It addresses my main concern, which was having a way to turn it off. ;) (And I see it is not even changed by default now).

One thing that puzzled me a bit is that the show_divergence_advice flag is only respected for "use git pull if you want to integrate..." hint and not other advice. But that was true even before these patches. It feels funny to me that it is treated differently than the other advice. But I think we can ignore that for the purposes of this topic. If anything it is a defect in b6f3da5132 (wt-status: don't show divergence advice when committing, 2023-07-12), which perhaps should have covered more code paths.

Other than that, I did not see anything wrong from my (admittedly pretty cursory) read.

-Peff

#189 lore raw

Memory leak

In reply to Junio C Hamano

I realized now that my code would benefit from branch de-duplication. When running it from the main branch (which has upsteam+push = origin/main) and my git setting is this which I intended to run from now on:

    git config --global status.compareBranches "@{upstream} @{push}"

then it reports status twice. I have a small fix ready and will push it out once the CI passes on GitHub.

Harald

#190 lore raw

Re: Memory leak

Quoted 2 lines · Harald Nordgren

I realized now that my code would benefit from branch de-duplication.

... meaning that ...

Quoted 5 lines

When
running it from the main branch (which has upsteam+push = origin/main) and my git setting is this which I intended to run from now on:

    git config --global status.compareBranches "@{upstream} @{push}"

... when @{upstream} and @{push} ends up being the same branch? Yes, if that happens, it would be irritating for the users if we do not deduplicate.

Thanks.

#191 lore raw

v27 cover status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected] cc:
Jeff King [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: add status.compareBranches config for multiple branch
comparisons

 Documentation/config/status.adoc |  20 ++
 remote.c                         | 192 ++++++++++++++----
 t/t6040-tracking-info.sh         | 337 +++++++++++++++++++++++++++++++
 3 files changed, 512 insertions(+), 37 deletions(-)

base-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v27
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v27
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v26:

 1:  27a46f8d9c = 1:  27a46f8d9c refactor format_branch_comparison in preparation
 2:  caa761f615 ! 2:  0993420fc1 status: add status.compareBranches config for multiple branch comparisons
     @@ remote.c
       struct counted_string {
       	size_t len;
       	const char *s;
     -@@ remote.c: static void branch_release(struct branch *branch)
     - 	free((char *)branch->refname);
     - 	free(branch->remote_name);
     - 	free(branch->pushremote_name);
     -+	free((char *)branch->push_tracking_ref);
     - 	merge_clear(branch);
     - }
     - 
      @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
       	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
       }
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
      -	const char *full_base;
      -	char *base;
      -	int upstream_is_gone = 0;
     --
     ++	char *compare_branches = NULL;
     ++	struct string_list branches = STRING_LIST_INIT_DUP;
     ++	struct string_list processed_refs = STRING_LIST_INIT_DUP;
     ++	int reported = 0;
     ++	size_t i;
     ++	const char *upstream_ref;
     ++	const char *push_ref;
     + 
      -	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
      -	if (cmp_fetch < 0) {
      -		if (!full_base)
      -			return 0;
      -		upstream_is_gone = 1;
     -+	char *compare_branches_config = NULL;
     -+	struct string_list compare_branches = STRING_LIST_INIT_DUP;
     -+	struct string_list_item *item;
     -+	int reported = 0;
     -+	size_t i;
     -+	const char *upstream_ref;
     -+	const char *push_ref;
     -+
      +	repo_config_get_string(the_repository, "status.comparebranches",
     -+			       &compare_branches_config);
     ++			       &compare_branches);
      +
     -+	if (compare_branches_config) {
     -+		string_list_split(&compare_branches, compare_branches_config,
     -+				  " ", -1);
     -+		string_list_remove_empty_items(&compare_branches, 0);
     ++	if (compare_branches) {
     ++		string_list_split(&branches, compare_branches, " ", -1);
     ++		string_list_remove_empty_items(&branches, 0);
      +	} else {
     -+		string_list_append(&compare_branches, "@{upstream}");
     ++		string_list_append(&branches, "@{upstream}");
       	}
       
      -	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
      -				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
      -	} else {
      -		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
     -+	for (i = 0; i < compare_branches.nr; i++) {
     ++	for (i = 0; i < branches.nr; i++) {
      +		char *full_ref;
      +		char *short_ref;
      +		int ours, theirs, cmp;
      +		int is_upstream, is_push;
      +		unsigned flags = 0;
      +
     -+		item = &compare_branches.items[i];
     -+		full_ref = resolve_compare_branch(branch, item->string);
     ++		full_ref = resolve_compare_branch(branch,
     ++						  branches.items[i].string);
      +		if (!full_ref)
      +			continue;
      +
     ++		if (string_list_has_string(&processed_refs, full_ref)) {
     ++			free(full_ref);
     ++			continue;
     ++		}
     ++		string_list_insert(&processed_refs, full_ref);
     ++
      +		short_ref = refs_shorten_unambiguous_ref(
      +			get_main_ref_store(the_repository), full_ref, 0);
      +
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
       
      -	free(base);
      -	return 1;
     -+	string_list_clear(&compare_branches, 0);
     -+	free(compare_branches_config);
     ++	string_list_clear(&branches, 0);
     ++	string_list_clear(&processed_refs, 0);
     ++	free(compare_branches);
      +	return reported;
       }
       
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	)
      +'
      +
     ++test_expect_success 'status.compareBranches from upstream has no duplicates' '
     ++	(
     ++		cd test &&
     ++		git checkout main &&
     ++		git status >../actual
     ++	) &&
     ++	cat >expect <<-EOF &&
     ++	On branch main
     ++	Your branch is up to date with ${SQ}origin/main${SQ}.
     ++
     ++	nothing to commit, working tree clean
     ++	EOF
     ++	test_cmp expect actual
     ++'
     ++
      +test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '
      +	(
      +		cd test &&
Signature · 1 line
gitgitgadget
#192 lore raw

PATCH v27 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 48 insertions(+), 34 deletions(-)
remote.c +48 -34
index b756ff6f15..fd592ec659 100644
@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			       "and have %d and %d different commits each, "
 			       "respectively.\n",
 			   ours + theirs),
-			base, ours, theirs);
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#193 lore raw

PATCH v27 2/2 status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Add a new configuration variable status.compareBranches that allows users to specify a space-separated list of branches to compare against the current branch in git status output.

Each branch in the list can be:
- A remote-tracking branch name (e.g., origin/main)
- The special reference @{upstream} for the tracking branch
- The special reference @{push} for the push destination

When not configured, the default behavior is equivalent to setting status.compareBranches = @{upstream}, preserving backward compatibility.

The advice messages shown are context-aware:
- "git pull" advice is shown only when comparing against @{upstream}
- "git push" advice is shown only when comparing against @{push}
- Divergence advice is shown for upstream branch comparisons

This is useful for triangular workflows where the upstream tracking branch differs from the push destination, allowing users to see their status relative to both branches at once.

Example configuration:

    [status]
        compareBranches = @{upstream} @{push}
---
 Documentation/config/status.adoc |  20 ++
 remote.c                         | 160 ++++++++++++---
 t/t6040-tracking-info.sh         | 337 +++++++++++++++++++++++++++++++
 3 files changed, 489 insertions(+), 28 deletions(-)
Documentation/config/status.adoc +20 -0
index 8caf90f51c..ab54f25fbc 100644
@@ -17,6 +17,26 @@ status.aheadBehind::
 	`--no-ahead-behind` by default in linkgit:git-status[1] for
 	non-porcelain status formats.  Defaults to true.
 
+status.compareBranches::
+	A space-separated list of branches to compare the current branch
+	against in linkgit:git-status[1]. Each branch specification can be
+	a remote-tracking branch name (e.g. `origin/main`), or a special
+	reference like `@{upstream}` or `@{push}`. For each branch in the
+	list, git status shows whether the current branch is ahead, behind,
+	or has diverged from that branch.
++
+If not set, the default behavior is equivalent to `@{upstream}`, which
+compares against the configured upstream tracking branch.
++
+Example:
++
+----
+[status]
+	compareBranches = origin/main origin/develop
+----
++
+This would show comparisons against both `origin/main` and `origin/develop`.
+
 status.displayCommentPrefix::
 	If set to true, linkgit:git-status[1] will insert a comment
 	prefix before each output line (starting with
remote.c +132 -28

214 lines not on this page · raw message

t/t6040-tracking-info.sh +337 -0

341 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#194 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

Review

Quoted 5 lines · Harald Nordgren via GitGitGadget
(quoted diff) +5 -0
+		if (string_list_has_string(&processed_refs, full_ref)) {
+			free(full_ref);
+			continue;
+		}
+		string_list_insert(&processed_refs, full_ref);

Using a string list like this is quadratic. It probably doesn't matter too much here since we wouldn't expect the list of configured branches to be very long, though. But a strset is probably the better tool, like the diff below (note that its "add" can be used as a single operation to insert and check).

I don't know if it's worth re-rolling for this or not. I doubt anybody would hit it in practice, but I'd be more concerned about people auditing for accidentally-quadratic uses of string_list and stumbling upon it.

-Peff

remote.c +3 -4
index e256cc9b81..1071a23567 100644
@@ -2339,7 +2339,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 {
 	char *compare_branches = NULL;
 	struct string_list branches = STRING_LIST_INIT_DUP;
-	struct string_list processed_refs = STRING_LIST_INIT_DUP;
+	struct strset processed_refs = STRSET_INIT;
 	int reported = 0;
 	size_t i;
 	const char *upstream_ref;
@@ -2370,11 +2370,10 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		if (!full_ref)
 			continue;
 
-		if (string_list_has_string(&processed_refs, full_ref)) {
+		if (!strset_add(&processed_refs, full_ref)) {
 			free(full_ref);
 			continue;
 		}
-		string_list_insert(&processed_refs, full_ref);
 
 		short_ref = refs_shorten_unambiguous_ref(
 			get_main_ref_store(the_repository), full_ref, 0);
@@ -2421,7 +2420,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 	}
 
 	string_list_clear(&branches, 0);
-	string_list_clear(&processed_refs, 0);
+	strset_clear(&processed_refs);
 	free(compare_branches);
 	return reported;
 }
#195 lore raw

Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 10 lines

Using a string list like this is quadratic. It probably doesn't matter too much here since we wouldn't expect the list of configured branches to be very long, though. But a strset is probably the better tool, like the diff below (note that its "add" can be used as a single operation to insert and check).

I don't know if it's worth re-rolling for this or not. I doubt anybody would hit it in practice, but I'd be more concerned about people auditing for accidentally-quadratic uses of string_list and stumbling upon it.

Sounds like a good change. Very nice to have insert-and-check as a single operation!

I'll update it after passing CI on GitHub -- I'm running CI from two separate GitHub pull requests, because one has your memory leak fix on top to allow the leak tests to not fail. 🤗

Harald

#196 lore raw

Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 11 lines · Harald Nordgren
Quoted 10 lines

Using a string list like this is quadratic. It probably doesn't matter too much here since we wouldn't expect the list of configured branches to be very long, though. But a strset is probably the better tool, like the diff below (note that its "add" can be used as a single operation to insert and check).

I don't know if it's worth re-rolling for this or not. I doubt anybody would hit it in practice, but I'd be more concerned about people auditing for accidentally-quadratic uses of string_list and stumbling upon it.

Quoted 6 lines

Sounds like a good change. Very nice to have insert-and-check as a single operation!

I'll update it after passing CI on GitHub -- I'm running CI from two separate GitHub pull requests, because one has your memory leak fix on top to allow the leak tests to not fail. 🤗

Thanks. I think we are getting to the finish line with this topic, finally ;-)

#197 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget
Quoted 24 lines · Harald Nordgren via GitGitGadget
remote.c +9 -0
index fd592ec659..e256cc9b81 100644
@@ -29,6 +29,12 @@
 
 enum map_direction { FROM_SRC, FROM_DST };
 
+enum {
+	ENABLE_ADVICE_PULL       = (1 << 0),
+	ENABLE_ADVICE_PUSH       = (1 << 1),
+	ENABLE_ADVICE_DIVERGENCE = (1 << 2),
+};
+
 struct counted_string {
 	size_t len;
 	const char *s;
@@ -2230,13 +2236,53 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
+static char *resolve_compare_branch(struct branch *branch, const char *name)
+{
+	struct strbuf buf = STRBUF_INIT;

remote.c int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs, · line 2251

Quoted 10 lines
remote.c +10 -0
+	const char *resolved = NULL;
+	char *ret;
+
+	if (!branch || !name)
+		return NULL;
+
+	if (!strcasecmp(name, "@{upstream}") || !strcasecmp(name, "@{u}"))
+		resolved = branch_get_upstream(branch, NULL);
+	else if (!strcasecmp(name, "@{push}"))
+		resolved = branch_get_push(branch, NULL);

OK. Usually @{upstream} without anything before the at-sign means the upstream of the current branch, but we need to force pretend that branch were the current branch, so we'd need to special case like this, which looks reasonable.

Quoted 4 lines
remote.c +4 -0
+	if (resolved)
+		return xstrdup(resolved);
+
+	strbuf_addf(&buf, "refs/remotes/%s", name);

remote.c int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs, · line 2266

Quoted 10 lines
remote.c +10 -0
+	resolved = refs_resolve_ref_unsafe(
+		get_main_ref_store(the_repository),
+		buf.buf,
+		RESOLVE_REF_READING,
+		NULL, NULL);
+	if (resolved) {
+		ret = xstrdup(resolved);
+		strbuf_release(&buf);
+		return ret;
+	}

It would be handy to be able to say "origin/master" (or even just "origin", which is interpreted as "origin/HEAD" via the DWIM machinery) and prepending of "refs/remotes/" above does help such DWIMmery, but I wonder if it is too limiting? Would there be situations where you would want to compare with something outside refs/remotes/ hierarchy?

For example, writing "v2.52.0" there to see how far we came since the last release would become impossible if we always force prepend "refs/remotes/". I wonder if we can reuse already existing DWIMmery that uses refs.c::ref_rev_parse_rules[], which should allow such use case, while still allowing you to write "origin/master"?

#198 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 5 lines

For example, writing "v2.52.0" there to see how far we came since the last release would become impossible if we always force prepend "refs/remotes/". I wonder if we can reuse already existing DWIMmery that uses refs.c::ref_rev_parse_rules[], which should allow such use case, while still allowing you to write "origin/master"?

Sounds like a follow-up rather of doing now, right? 🤗

Since the inteface won't change, just adding more functionality a new feature, we should be able to fix this behind the scenes later.

Harald

#199 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 10 lines · Harald Nordgren
Quoted 5 lines

For example, writing "v2.52.0" there to see how far we came since the last release would become impossible if we always force prepend "refs/remotes/". I wonder if we can reuse already existing DWIMmery that uses refs.c::ref_rev_parse_rules[], which should allow such use case, while still allowing you to write "origin/master"?

Sounds like a follow-up rather of doing now, right? 🤗

Since the inteface won't change, just adding more functionality a new feature, we should be able to fix this behind the scenes later.

Disambiguation will make it harder or impossible to add such an enhancement later, though.

When the user says "git log origin" or "git show origin/main", refs.c::ref_rev_parse_rules[] is applied and turns these into refs/refs/remotes/origin/HEAD or refs/remotes/origin/main, but as you can see in refs.c::ref_rev_parse_rules[], these are at relatively low precedence order. Your version has refs/remotes/ as the first (and only) choice, so if a user has refs/heads/origin/main and refs/remotes/origin/main at the same time, "git show origin/main" would use the former. With the implementation of this round, "[status] compareBranches = origin/main" would pick refs/remotes/origin/main and nothing else first, and then would start behaving differently once such an enhancement to use the standard DWIMmery rules is introduced, which may appear to be an unnecessary regression to end-users.

So, no, I wouldn't recommend it as a follow-up. We should decide to do so now or declare that we would never do so.

#200 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 8 lines · Junio C Hamano
Quoted 6 lines · Harald Nordgren
Quoted 5 lines

For example, writing "v2.52.0" there to see how far we came since the last release would become impossible if we always force prepend "refs/remotes/". I wonder if we can reuse already existing DWIMmery that uses refs.c::ref_rev_parse_rules[], which should allow such use case, while still allowing you to write "origin/master"?

Quoted 7 lines
Quoted 4 lines

Sounds like a follow-up rather of doing now, right? 🤗

Since the inteface won't change, just adding more functionality a new feature, we should be able to fix this behind the scenes later.

...
So, no, I wouldn't recommend it as a follow-up. We should decide to do so now or declare that we would never do so.

Alternatively, if we remove the "a string that is not @{upstream} or @{push} gets refs/remotes/ prefixed" altogether, then "the interface won't change, just adding more functionality" would become true.

It is not my itch, so I can go either way, but I'd prefer not to see us take the "declare we will never use the unified ref DIWMmery rules here" route, if we can.

#201 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Junio C Hamano
Quoted 10 lines · Junio C Hamano
Quoted 10 lines
(quoted diff) +10 -0
+static char *resolve_compare_branch(struct branch *branch, const char *name)
+{
+	struct strbuf buf = STRBUF_INIT;
+	const char *resolved = NULL;
+	char *ret;
+
+	if (!branch || !name)
+		return NULL;
+
+	if (!strcasecmp(name, "@{upstream}") || !strcasecmp(name, "@{u}"))
Quoted 8 lines

Review

Quoted 3 lines
(quoted diff) +3 -0
+		resolved = branch_get_upstream(branch, NULL);
+	else if (!strcasecmp(name, "@{push}"))
+		resolved = branch_get_push(branch, NULL);

OK. Usually @{upstream} without anything before the at-sign means the upstream of the current branch, but we need to force pretend that branch were the current branch, so we'd need to special case like this, which looks reasonable.

Yeah, it is unfortunate to have to special-case these names, even though the resolving functions already know about them. If we are looking at branch "foo" we could rewrite them as "foo@{upstream}", etc, but that also requires special-casing (though maybe slightly less, if we just accept the @ sign?).

I can think of two alternatives, though.

One is that repo_interpret_branch_name() could accept a field in its options struct to set the default branch (rather than "HEAD"). Something like this (totally untested):

object-name.c +1 -1
index 8b862c124e..925a487d84 100644
@@ -1732,7 +1732,7 @@ static int interpret_branch_mark(struct repository *r,
 		branch = branch_get(name_str);
 		free(name_str);
 	} else
-		branch = branch_get(NULL);
+		branch = branch_get(options->default_branch);
 
 	value = get_data(branch, &err);
 	if (!value) {
object-name.h +6 -0
index cda4934cd5..962f99b0f6 100644
@@ -119,6 +119,12 @@ struct interpret_branch_name_options {
 	 * of die()-ing.
 	 */
 	unsigned nonfatal_dangling_mark : 1;
+
+	/*
+	 * Pass this to branch_get() when interpreting @-marks without a
+	 * branch, rather than using HEAD.
+	 */
+	const char *default_branch;
 };
 int repo_interpret_branch_name(struct repository *r,
 			       const char *str, int len,

Most callers would continue to pass NULL in the usual way, but the resolution here would pass in the current branch name.

The second is a bit more complicated, but is even more flexible. Part of the point of this status.compareBranches approach is that you can add regular refnames to the list. But would a user want to use a name that is derived from the comparison branch, rather than just a static name? That is, to compare branch "foo" against "origin/foo"? Usually that is exactly the kind of refspec-application that @{upstream} and @{push} are computing (after taking into account various config). But if you have a third source of refs, would you want to be able to compare to "origin/%s", where %s is the shortened branch name?

In which case these values could become "%s@{upstream}" and "%s@{push}", and they could just be fed straight to the branch-interpret machinery.

Quoted 19 lines

Review

Quoted 11 lines
(quoted diff) +11 -0
+	strbuf_addf(&buf, "refs/remotes/%s", name);
+	resolved = refs_resolve_ref_unsafe(
+		get_main_ref_store(the_repository),
+		buf.buf,
+		RESOLVE_REF_READING,
+		NULL, NULL);
+	if (resolved) {
+		ret = xstrdup(resolved);
+		strbuf_release(&buf);
+		return ret;
+	}

It would be handy to be able to say "origin/master" (or even just "origin", which is interpreted as "origin/HEAD" via the DWIM machinery) and prepending of "refs/remotes/" above does help such DWIMmery, but I wonder if it is too limiting? Would there be situations where you would want to compare with something outside refs/remotes/ hierarchy?

Quoted 5 lines

For example, writing "v2.52.0" there to see how far we came since the last release would become impossible if we always force prepend "refs/remotes/". I wonder if we can reuse already existing DWIMmery that uses refs.c::ref_rev_parse_rules[], which should allow such use case, while still allowing you to write "origin/master"?

Yeah, I think tags or even local branches would be plausible candidates. But at any rate, I'd expect these to be resolved in the "usual" way that we do elsewhere. If we switch to using repo_dwim_ref() or something that interprets @-marks, then that matches nicely with the suggestions I gave above.

-Peff

#202 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 10 lines · Jeff King

The second is a bit more complicated, but is even more flexible. Part of the point of this status.compareBranches approach is that you can add regular refnames to the list. But would a user want to use a name that is derived from the comparison branch, rather than just a static name? That is, to compare branch "foo" against "origin/foo"? Usually that is exactly the kind of refspec-application that @{upstream} and @{push} are computing (after taking into account various config). But if you have a third source of refs, would you want to be able to compare to "origin/%s", where %s is the shortened branch name?

Quoted 2 lines

In which case these values could become "%s@{upstream}" and "%s@{push}", and they could just be fed straight to the branch-interpret machinery.

So here's a sketch to show what that might look like:

remote.c +17 -21
index 1071a23567..6f9a28970c 100644
@@ -2239,34 +2239,30 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 static char *resolve_compare_branch(struct branch *branch, const char *name)
 {
 	struct strbuf buf = STRBUF_INIT;
-	const char *resolved = NULL;
+	struct object_id oid;
 	char *ret;
 
 	if (!branch || !name)
 		return NULL;
 
-	if (!strcasecmp(name, "@{upstream}") || !strcasecmp(name, "@{u}"))
-		resolved = branch_get_upstream(branch, NULL);
-	else if (!strcasecmp(name, "@{push}"))
-		resolved = branch_get_push(branch, NULL);
-
-	if (resolved)
-		return xstrdup(resolved);
-
-	strbuf_addf(&buf, "refs/remotes/%s", name);
-	resolved = refs_resolve_ref_unsafe(
-		get_main_ref_store(the_repository),
-		buf.buf,
-		RESOLVE_REF_READING,
-		NULL, NULL);
-	if (resolved) {
-		ret = xstrdup(resolved);
-		strbuf_release(&buf);
-		return ret;
+	while (strbuf_expand_step(&buf, &name)) {
+		if (skip_prefix(name, "%", &name))
+			strbuf_addch(&buf, '%');
+		else if (skip_prefix(name, "s", &name))
+			strbuf_addstr(&buf, branch->name);
+		else
+			die("bad compareBranches format: %%%s", name);
 	}
 
+	/*
+	 * This will leave "ret" as NULL if we don't find anything. If there
+	 * are multiple ambiguous matches it will pick the first. Should we
+	 * return an error instead?
+	 */
+	repo_dwim_ref(the_repository, buf.buf, buf.len, &oid, &ret, 1);
+
 	strbuf_release(&buf);
-	return NULL;
+	return ret;
 }
 
 static void format_branch_comparison(struct strbuf *sb,
@@ -2352,7 +2348,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 		string_list_split(&branches, compare_branches, " ", -1);
 		string_list_remove_empty_items(&branches, 0);
 	} else {
-		string_list_append(&branches, "@{upstream}");
+		string_list_append(&branches, "%s@{upstream}");
 	}
 
 	upstream_ref = branch_get_upstream(branch, NULL);

I ran into two small complications. The first is that there's an interesting race "git-checkout -b":

1. We read branch and remote config via remote.c:read_config(), caching it.

2. Then we write some new config for the branch we just created.

3. Then we try to report tracking info. This will read the cached config from step 1, without regard to the changes in 2.

It works without my patch above because the tracking code currently operates directly on a "struct branch" (that we just updated), rather than using the stale config from (1).

We can fix that like this:

branch.c +6 -0
index 243db7d0fc..cc24e8522d 100644
@@ -178,6 +178,12 @@ static int install_branch_config_multiple_remotes(int flag, const char *local,
 		string_list_clear(&friendly_ref_names, 0);
 	}
 
+	/*
+	 * We installed new config; flush any internal cache of the on-disk
+	 * config.
+	 */
+	remote_state_clear(the_repository->remote_state);
+
 	return 0;
 
 out_err:
remote.c +8 -0
index 6f9a28970c..e6056199f0 100644
@@ -2859,6 +2859,14 @@ void remote_state_clear(struct remote_state *remote_state)
 		free(b);
 	}
 	hashmap_clear(&remote_state->branches_hash);
+
+	/*
+	 * Eek, we just cleared everything out; we should mark ourselves as
+	 * uninitialized so we can be used again. Nobody seems to have noticed
+	 * because we only remote_state_clear() when we are about to discard
+	 * the struct entirely.
+	 */
+	remote_state->initialized = 0;
 }
 
 /*

The second issue concerns the case when an upstream is configured, but the tracking ref for it is missing. So imagine "foo" is configured with "refs/remotes/origin/foo" as its upstream, but that branch is gone.

Using branch_get_upstream() will return the name, even if it doesn't exist. And then the tracking-info code recognizes this and reports it. But repo_dwim_ref() won't return a missing ref at all, even with nonfatal_dangling_mark. So I think we'd need to teach it a new option to do so.

I don't know what all of it means. The "%s" thing was short to implement, but the real source of the extra complications is using repo_dwim_ref() to do the resolution. But I think the overall direction is more consistent with how the rest of Git behaves.

-Peff

#203 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 9 lines · Jeff King

The second issue concerns the case when an upstream is configured, but the tracking ref for it is missing. So imagine "foo" is configured with "refs/remotes/origin/foo" as its upstream, but that branch is gone.

Using branch_get_upstream() will return the name, even if it doesn't exist. And then the tracking-info code recognizes this and reports it. But repo_dwim_ref() won't return a missing ref at all, even with nonfatal_dangling_mark. So I think we'd need to teach it a new option to do so.

So I think that nonfatal_dangling_mark could arguably return the name (but no oid) for this case, like so:

refs.c +7 -1
index 627b7f8698..2316d0b8a9 100644
@@ -813,7 +813,13 @@ int repo_dwim_ref(struct repository *r, const char *str, int len,
 	char *last_branch = substitute_branch_name(r, &str, &len,
 						   nonfatal_dangling_mark);
 	int   refs_found  = expand_ref(r, str, len, oid, ref);
-	free(last_branch);
+	if (nonfatal_dangling_mark && !refs_found && last_branch) {
+		oidclr(oid, r->hash_algo);
+		*ref = last_branch;
+		refs_found = 1;
+	} else {
+		free(last_branch);
+	}
 	return refs_found;
 }
 

But this brings up yet another corner case: substitute_branch_name() will call shorten_unambiguous_ref() on the result when expanding @-marks. So last_branch here might be "origin/foo" instead of "refs/remotes/origin/foo". It's usually not a big deal because expand_ref() will then reverse that shortening. Which is maybe wasteful, but not so bad (though if somebody racily creates an ambiguous ref, it could affect the results).

But if we return last_branch explicitly, then information is lost: we don't know what the fully-qualified version of "origin/foo" was supposed to be. And so the tracking-info code gets confused, because it doesn't realize that "origin/foo" was supposed to be our @{upstream}.

I think probably the interpret_branch_name() code should have an option to avoid that shortening. So if we do this on top:

object-name.c +6 -1
index 8b862c124e..0663946f81 100644
@@ -1747,7 +1747,12 @@ static int interpret_branch_mark(struct repository *r,
 	if (!branch_interpret_allowed(value, options->allowed))
 		return -1;
 
-	set_shortened_ref(r, buf, value);
+	if (options->do_not_shorten) {
+		strbuf_reset(buf);
+		strbuf_addstr(buf, value);
+	} else {
+		set_shortened_ref(r, buf, value);
+	}
 	return len + at;
 }
 
object-name.h +2 -0
index cda4934cd5..20393cb213 100644
@@ -119,6 +119,8 @@ struct interpret_branch_name_options {
 	 * of die()-ing.
 	 */
 	unsigned nonfatal_dangling_mark : 1;
+
+	unsigned do_not_shorten : 1;
 };
 int repo_interpret_branch_name(struct repository *r,
 			       const char *str, int len,
refs.c +1 -0
index 2316d0b8a9..58f945e213 100644
@@ -746,6 +746,7 @@ static char *substitute_branch_name(struct repository *r,
 {
 	struct strbuf buf = STRBUF_INIT;
 	struct interpret_branch_name_options options = {
+		.do_not_shorten = 1,
 		.nonfatal_dangling_mark = nonfatal_dangling_mark
 	};
 	int ret = repo_interpret_branch_name(r, *string, *len, &buf, &options);

then all of t6040 passes (curiously without me swapping in "%s@{push}" as appropriate; I guess we are often on the branch of interest anyway, so it accidentally works with just @{push}).

So I don't love how deep the rabbit-hole has gone here. But at the same time, it feels like all of these "if we just do this on top" fixes are actually smoothing some rough edges in the rest of Git. So maybe it's worth it.

-Peff

#204 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

I can apply the changes from these three messages, but I don't really know the side-effects of it. Should I do it and submit a patch?

I noticed that it still won't work with tags (although it starts working with 'origin' which then defaults to origin/HEAD). I can imagine we need a few more tests on top of that.

Harald

#205 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 2 lines · Harald Nordgren

I can apply the changes from these three messages, but I don't really know the side-effects of it. Should I do it and submit a patch?

Please do not send code you cannot answer to questions on it, whether it is written by somebody else, genAI, or your cat rolling on your keyboard ;-).

I think we are getting close but will need more polish before we can allow users to reuse their already acquired knowledge of how refname DWIMmery can be used to spell various refs they mean.

If we support only @{push} and @{upstream} and error out when we see anything else (like "origin" or "origin/main") in the initial version we ship to the end-users, that would probably be a good stopping point. On top of it, we can later add the DWIMmery Peff has shown (with necessary tweaks, as you found out, like supporting tags, perhaps), and that will be purely new feature that does not change any behaviour of what used to work for our users in the initial version.

Going that way is much safer and does not break end-user experiences, like shipping the first version with "we always prefix hardcoded refs/remotes/ unless it is @{something}", which will have to change the behaviour once the proper DWIMmery gets implemented.

In any case, that will have to happen all after the current cycle is over, it is way too late even for "@{push} and @{upstream} only" version for this cycle.

#206 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Jeff King

Hi Jeff!

Do you think this is ready to be merged? I think you are the holdout here 🤗

Harald

#207 lore raw

Re: [PATCH v27 2/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Jeff King
Quoted 4 lines · Jeff King

I don't know what all of it means. The "%s" thing was short to implement, but the real source of the extra complications is using repo_dwim_ref() to do the resolution. But I think the overall direction is more consistent with how the rest of Git behaves.

Yeah, I tend to agree. As long as we do not hardcode the prefixing of "refs/remotes/", we won't paint ourselves into a corner we cannot get out of, I guess.

Thanks.

#208 lore raw

Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

"Harald Nordgren via GitGitGadget" <[email protected]> writes:

Before these list of CC's (which does not seem to have any effect, by the way. The message is going only to the list with your personal address on CC: and to nobody else), ...

Quoted 6 lines

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected] cc:
Jeff King [email protected]

... please summarize

(1) what this series of patches are about, to help those who encounter these patches for the first time, and

(2) what changed in this iteration (v27) relative to the previous

     iteration (v26).

I happen to have seen your other message so I can guess this is about deduping when more than one comparison target is listed and some happen to become the same branches, but others may not have seen that other message, and it is not helpful to just dump the range-diff and force them to read it to deduce what you did.

Thanks.

Quoted 140 lines

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: add status.compareBranches config for multiple branch
comparisons

 Documentation/config/status.adoc |  20 ++
 remote.c                         | 192 ++++++++++++++----
 t/t6040-tracking-info.sh         | 337 +++++++++++++++++++++++++++++++
 3 files changed, 512 insertions(+), 37 deletions(-)

base-commit: b5c409c40f1595e3e590760c6f14a16b6683e22c
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v27
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v27
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v26:

 1:  27a46f8d9c = 1:  27a46f8d9c refactor format_branch_comparison in preparation
 2:  caa761f615 ! 2:  0993420fc1 status: add status.compareBranches config for multiple branch comparisons
     @@ remote.c
       struct counted_string {
       	size_t len;
       	const char *s;
     -@@ remote.c: static void branch_release(struct branch *branch)
     - 	free((char *)branch->refname);
     - 	free(branch->remote_name);
     - 	free(branch->pushremote_name);
     -+	free((char *)branch->push_tracking_ref);
     - 	merge_clear(branch);
     - }
     - 
      @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
       	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
       }
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
      -	const char *full_base;
      -	char *base;
      -	int upstream_is_gone = 0;
     --
     ++	char *compare_branches = NULL;
     ++	struct string_list branches = STRING_LIST_INIT_DUP;
     ++	struct string_list processed_refs = STRING_LIST_INIT_DUP;
     ++	int reported = 0;
     ++	size_t i;
     ++	const char *upstream_ref;
     ++	const char *push_ref;
     + 
      -	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
      -	if (cmp_fetch < 0) {
      -		if (!full_base)
      -			return 0;
      -		upstream_is_gone = 1;
     -+	char *compare_branches_config = NULL;
     -+	struct string_list compare_branches = STRING_LIST_INIT_DUP;
     -+	struct string_list_item *item;
     -+	int reported = 0;
     -+	size_t i;
     -+	const char *upstream_ref;
     -+	const char *push_ref;
     -+
      +	repo_config_get_string(the_repository, "status.comparebranches",
     -+			       &compare_branches_config);
     ++			       &compare_branches);
      +
     -+	if (compare_branches_config) {
     -+		string_list_split(&compare_branches, compare_branches_config,
     -+				  " ", -1);
     -+		string_list_remove_empty_items(&compare_branches, 0);
     ++	if (compare_branches) {
     ++		string_list_split(&branches, compare_branches, " ", -1);
     ++		string_list_remove_empty_items(&branches, 0);
      +	} else {
     -+		string_list_append(&compare_branches, "@{upstream}");
     ++		string_list_append(&branches, "@{upstream}");
       	}
       
      -	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
      -				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
      -	} else {
      -		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
     -+	for (i = 0; i < compare_branches.nr; i++) {
     ++	for (i = 0; i < branches.nr; i++) {
      +		char *full_ref;
      +		char *short_ref;
      +		int ours, theirs, cmp;
      +		int is_upstream, is_push;
      +		unsigned flags = 0;
      +
     -+		item = &compare_branches.items[i];
     -+		full_ref = resolve_compare_branch(branch, item->string);
     ++		full_ref = resolve_compare_branch(branch,
     ++						  branches.items[i].string);
      +		if (!full_ref)
      +			continue;
      +
     ++		if (string_list_has_string(&processed_refs, full_ref)) {
     ++			free(full_ref);
     ++			continue;
     ++		}
     ++		string_list_insert(&processed_refs, full_ref);
     ++
      +		short_ref = refs_shorten_unambiguous_ref(
      +			get_main_ref_store(the_repository), full_ref, 0);
      +
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
       
      -	free(base);
      -	return 1;
     -+	string_list_clear(&compare_branches, 0);
     -+	free(compare_branches_config);
     ++	string_list_clear(&branches, 0);
     ++	string_list_clear(&processed_refs, 0);
     ++	free(compare_branches);
      +	return reported;
       }
       
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	)
      +'
      +
     ++test_expect_success 'status.compareBranches from upstream has no duplicates' '
     ++	(
     ++		cd test &&
     ++		git checkout main &&
     ++		git status >../actual
     ++	) &&
     ++	cat >expect <<-EOF &&
     ++	On branch main
     ++	Your branch is up to date with ${SQ}origin/main${SQ}.
     ++
     ++	nothing to commit, working tree clean
     ++	EOF
     ++	test_cmp expect actual
     ++'
     ++
      +test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '
      +	(
      +		cd test &&
#209 lore raw

Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 3 lines

Before these list of CC's (which does not seem to have any effect, by the way. The message is going only to the list with your personal address on CC: and to nobody else), ...

Hmm, I always just the 'git send-email' snippet from example here:
https://lore.kernel.org/git/[email protected]/, which for this message is:

  git send-email \
    [email protected] \
    [email protected] \
    [email protected] \
    [email protected] \
    [email protected] \
    [email protected] \
    /path/to/YOUR_REPLY
Quoted 5 lines

... please summarize

(1) what this series of patches are about, to help those who encounter these patches for the first time, and

Quoted 8 lines

(2) what changed in this iteration (v27) relative to the previous

     iteration (v26).

I happen to have seen your other message so I can guess this is about deduping when more than one comparison target is listed and some happen to become the same branches, but others may not have seen that other message, and it is not helpful to just dump the range-diff and force them to read it to deduce what you did.

Fair point, but I always always use GitGitGadget to submit patches to here, I'm sure sure if there is a way to do that from there?

I just write '/submit' to create the whole patch:
https://github.com/git/git/pull/2138#issuecomment-3785055635

Harald

#210 lore raw

Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 8 lines · Harald Nordgren
Quoted 3 lines

Before these list of CC's (which does not seem to have any effect, by the way. The message is going only to the list with your personal address on CC: and to nobody else), ...

Hmm, I always just the 'git send-email' snippet from example here:
https://lore.kernel.org/git/[email protected]/, which for this message is:

Quoted 8 lines
  git send-email \
    [email protected] \
    [email protected] \
    [email protected] \
    [email protected] \
    [email protected] \
    [email protected] \
    /path/to/YOUR_REPLY

But the patch you sent was not using "git send-email", was it? I am not sure where those annoying and unused CC addresses comes from, but it would be from GGG, not from "git send-email", I suspect.

Quoted 8 lines
Quoted 8 lines

... please summarize

(1) what this series of patches are about, to help those who encounter these patches for the first time, and

(2) what changed in this iteration (v27) relative to the previous

     iteration (v26).
Quoted 8 lines
Quoted 5 lines

I happen to have seen your other message so I can guess this is about deduping when more than one comparison target is listed and some happen to become the same branches, but others may not have seen that other message, and it is not helpful to just dump the range-diff and force them to read it to deduce what you did.

Fair point, but I always always use GitGitGadget to submit patches to here, I'm sure sure if there is a way to do that from there?

Sorry, I do not work on or use GGG; other users of it may be able to chime in?

Thanks.

#211 lore raw

Re: [PATCH v27 0/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 3 lines

But the patch you sent was not using "git send-email", was it? I am not sure where those annoying and unused CC addresses comes from, but it would be from GGG, not from "git send-email", I suspect.

Yes you are right, I misunderstood your point about the CC's.

Patches are 100% via GGG.

Harald

#212 lore raw

v28 cover status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected] cc:
Jeff King [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: add status.compareBranches config for multiple branch
comparisons

 Documentation/config/status.adoc |  20 ++
 remote.c                         | 191 ++++++++++++++----
 t/t6040-tracking-info.sh         | 337 +++++++++++++++++++++++++++++++
 3 files changed, 511 insertions(+), 37 deletions(-)

base-commit: 83a69f19359e6d9bc980563caca38b2b5729808c
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v28
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v28
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v27:

 1:  27a46f8d9c = 1:  f3c8c782b0 refactor format_branch_comparison in preparation
 2:  0993420fc1 ! 2:  067978dd09 status: add status.compareBranches config for multiple branch comparisons
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
      -	int upstream_is_gone = 0;
      +	char *compare_branches = NULL;
      +	struct string_list branches = STRING_LIST_INIT_DUP;
     -+	struct string_list processed_refs = STRING_LIST_INIT_DUP;
     ++	struct strset processed_refs = STRSET_INIT;
      +	int reported = 0;
      +	size_t i;
      +	const char *upstream_ref;
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
      +		if (!full_ref)
      +			continue;
      +
     -+		if (string_list_has_string(&processed_refs, full_ref)) {
     ++		if (!strset_add(&processed_refs, full_ref)) {
      +			free(full_ref);
      +			continue;
      +		}
     -+		string_list_insert(&processed_refs, full_ref);
      +
      +		short_ref = refs_shorten_unambiguous_ref(
      +			get_main_ref_store(the_repository), full_ref, 0);
     @@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
      -	free(base);
      -	return 1;
      +	string_list_clear(&branches, 0);
     -+	string_list_clear(&processed_refs, 0);
     ++	strset_clear(&processed_refs);
      +	free(compare_branches);
      +	return reported;
       }
Signature · 1 line
gitgitgadget
#213 lore raw

PATCH v28 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 48 insertions(+), 34 deletions(-)
remote.c +48 -34
index b756ff6f15..fd592ec659 100644
@@ -2230,42 +2230,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2274,7 +2253,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2285,7 +2264,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
@@ -2298,12 +2277,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			       "and have %d and %d different commits each, "
 			       "respectively.\n",
 			   ours + theirs),
-			base, ours, theirs);
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#214 lore raw

PATCH v28 2/2 status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Add a new configuration variable status.compareBranches that allows users to specify a space-separated list of branches to compare against the current branch in git status output.

Each branch in the list can be:
- A remote-tracking branch name (e.g., origin/main)
- The special reference @{upstream} for the tracking branch
- The special reference @{push} for the push destination

When not configured, the default behavior is equivalent to setting status.compareBranches = @{upstream}, preserving backward compatibility.

The advice messages shown are context-aware:
- "git pull" advice is shown only when comparing against @{upstream}
- "git push" advice is shown only when comparing against @{push}
- Divergence advice is shown for upstream branch comparisons

This is useful for triangular workflows where the upstream tracking branch differs from the push destination, allowing users to see their status relative to both branches at once.

Example configuration:

    [status]
        compareBranches = @{upstream} @{push}
---
 Documentation/config/status.adoc |  20 ++
 remote.c                         | 159 ++++++++++++---
 t/t6040-tracking-info.sh         | 337 +++++++++++++++++++++++++++++++
 3 files changed, 488 insertions(+), 28 deletions(-)
Documentation/config/status.adoc +20 -0
index 8caf90f51c..ab54f25fbc 100644
@@ -17,6 +17,26 @@ status.aheadBehind::
 	`--no-ahead-behind` by default in linkgit:git-status[1] for
 	non-porcelain status formats.  Defaults to true.
 
+status.compareBranches::
+	A space-separated list of branches to compare the current branch
+	against in linkgit:git-status[1]. Each branch specification can be
+	a remote-tracking branch name (e.g. `origin/main`), or a special
+	reference like `@{upstream}` or `@{push}`. For each branch in the
+	list, git status shows whether the current branch is ahead, behind,
+	or has diverged from that branch.
++
+If not set, the default behavior is equivalent to `@{upstream}`, which
+compares against the configured upstream tracking branch.
++
+Example:
++
+----
+[status]
+	compareBranches = origin/main origin/develop
+----
++
+This would show comparisons against both `origin/main` and `origin/develop`.
+
 status.displayCommentPrefix::
 	If set to true, linkgit:git-status[1] will insert a comment
 	prefix before each output line (starting with
remote.c +131 -28

213 lines not on this page · raw message

t/t6040-tracking-info.sh +337 -0

341 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#215 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

Hi Junio and Jeff!

I see that this topic has now been marked as "Stale". Isn't it time to merge this now?

We went to several rounds of reviews and edits to reach something that I think everyone agrees with 🤗

Harald

#216 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 7 lines · Harald Nordgren

Hi Junio and Jeff!

I see that this topic has now been marked as "Stale". Isn't it time to merge this now?

We went to several rounds of reviews and edits to reach something that I think everyone agrees with 🤗

The entry has been asking "What's the status of this topic?" in the past handful of issues of "What's cooking" report and we heard nothing from anybody (until you responded ;-)), so I moved it together with others to the [Stalled] section.

The topic has been paged out of my consciousness for quite a while, so it may take some time for me to answer this question myself, but list participants, do you imagine yourselves using this feature in your daily life?

Thanks.

#217 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 16 lines · Junio C Hamano
Quoted 7 lines · Harald Nordgren

Hi Junio and Jeff!

I see that this topic has now been marked as "Stale". Isn't it time to merge this now?

We went to several rounds of reviews and edits to reach something that I think everyone agrees with 🤗

The entry has been asking "What's the status of this topic?" in the past handful of issues of "What's cooking" report and we heard nothing from anybody (until you responded ;-)), so I moved it together with others to the [Stalled] section.

Quoted 6 lines

The topic has been paged out of my consciousness for quite a while, so it may take some time for me to answer this question myself, but list participants, do you imagine yourselves using this feature in your daily life?

Thanks.

I'm trying to run something closer to the tip of "next" at home and work, and I can see a few places where I might try this, at least.

Signature · 1 line
D. Ben Knoble
#218 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Junio C Hamano
Quoted 15 lines · Junio C Hamano
Quoted 7 lines · Harald Nordgren

Hi Junio and Jeff!

I see that this topic has now been marked as "Stale". Isn't it time to merge this now?

We went to several rounds of reviews and edits to reach something that I think everyone agrees with 🤗

The entry has been asking "What's the status of this topic?" in the past handful of issues of "What's cooking" report and we heard nothing from anybody (until you responded ;-)), so I moved it together with others to the [Stalled] section.

Quoted 4 lines

The topic has been paged out of my consciousness for quite a while, so it may take some time for me to answer this question myself, but list participants, do you imagine yourselves using this feature in your daily life?

I do not personally have any interest in using the feature (I only joined the thread to complain that it could not be turned off ;) ).

So in that sense I don't care how far it takes things along the path of what was discussed in the v27 review. My main concern is that we not paint ourselves into a corner and be stuck forever with an interface that is inconsistent with the rest of Git.

So what about this. We add a config option that takes a list of items to compare against. But it _only_ supports @{upstream} and @{push} for now, and interprets them as branch@{upstream} and branch@{push} (but probably done manually, not via dwim_ref). That limitation gets documented.

Then later, if we choose to allow resolving arbitrary refs via dwim_ref(), we can add that support on top:

- rather than manually resolving @{upstream}, quietly rewrite it to branch@{upstream}. This is OK since branchless @{upstream} makes no sense in this context.

- otherwise, pass what the user gives us to repo_dwim_ref()

- optionally allow substitution via %s or similar. Technically I think "%" is allowed in a ref. I think that's probably OK in practice, though if we want to be really careful, then "[branch]" or something that's forbidden in the ref format would be a possible substitution.

But if we stop short of all of that today, we don't have to worry about all of those weird corner cases I dug up in the earlier thread. And if nobody ever _really_ cares about sticking arbitrary refs into their compare-list, then we just never go there.

-Peff

#219 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

I can only speak for myself, but I use this feature every day!

Harald

#220 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Jeff King
Quoted 4 lines · Jeff King

So what about this. We add a config option that takes a list of items to compare against. But it _only_ supports @{upstream} and @{push} for now, and interprets them as branch@{upstream} and branch@{push} (but probably done manually, not via dwim_ref). That limitation gets documented.

Sounds like a good way forward.

Quoted 14 lines

Then later, if we choose to allow resolving arbitrary refs via dwim_ref(), we can add that support on top:

- rather than manually resolving @{upstream}, quietly rewrite it to branch@{upstream}. This is OK since branchless @{upstream} makes no sense in this context.

- otherwise, pass what the user gives us to repo_dwim_ref()

- optionally allow substitution via %s or similar. Technically I think "%" is allowed in a ref. I think that's probably OK in practice, though if we want to be really careful, then "[branch]" or something that's forbidden in the ref format would be a possible substitution.

Quoted 4 lines

But if we stop short of all of that today, we don't have to worry about all of those weird corner cases I dug up in the earlier thread. And if nobody ever _really_ cares about sticking arbitrary refs into their compare-list, then we just never go there.

;-).

#221 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 6 lines
Quoted 4 lines

So what about this. We add a config option that takes a list of items to compare against. But it _only_ supports @{upstream} and @{push} for now, and interprets them as branch@{upstream} and branch@{push} (but probably done manually, not via dwim_ref). That limitation gets documented.

Sounds like a good way forward.

So if I do these changes, are we ready to merge then?

I worked on this feature for 2 months, and then it got marked as stale instead of being merged. Will this time be different?

Harald

#222 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 8 lines · Harald Nordgren
Quoted 6 lines
Quoted 4 lines

So what about this. We add a config option that takes a list of items to compare against. But it _only_ supports @{upstream} and @{push} for now, and interprets them as branch@{upstream} and branch@{push} (but probably done manually, not via dwim_ref). That limitation gets documented.

Sounds like a good way forward.

So if I do these changes, are we ready to merge then?

I do not think of other things that needs to be done to the design at this moment. It of course does not give any guarantee that others won't find flaws in what we have discussed so far, though ;-)

Quoted 2 lines

I worked on this feature for 2 months, and then it got marked as stale instead of being merged. Will this time be different?

A topic becoming stalled is something the original author can (and has the primary responsibility to) avoid by keeping the discussion thread alive by responding to reviews, pinging the thread with comments similar to "now I think this one is done, all comments are addressed by either updating the code or replying why we would not want to go there (which the reviewer who made the comment hasn't responded yet, so the ball is in their court)", etc. Even though I try to help keep the ball rolling by pinging discussion threads that smell about to go stalled from time to time, I cannot guarantee that
it would not happen again. But you can help ;-).

Thanks for working on this topic.

#223 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 13 lines · Junio C Hamano
Quoted 2 lines

I worked on this feature for 2 months, and then it got marked as stale instead of being merged. Will this time be different?

A topic becoming stalled is something the original author can (and has the primary responsibility to) avoid by keeping the discussion thread alive by responding to reviews, pinging the thread with comments similar to "now I think this one is done, all comments are addressed by either updating the code or replying why we would not want to go there (which the reviewer who made the comment hasn't responded yet, so the ball is in their court)", etc. Even though I try to help keep the ball rolling by pinging discussion threads that smell about to go stalled from time to time, I cannot guarantee that
it would not happen again. But you can help ;-).

To be fair to Harald, he did ping a few times, but I think was just unsure how to proceed after the issues raised in the last round.

I think what would have moved things forward more than just a ping, though, is generating a plan for moving forward in the face of confusing review. That can be quite hard for first-time contributors, though.

That was why I tried to lay something out in the last email. I _think_ it would not be too much work to get there from the last iteration. It would mostly be removing code/feature that we're not ready to commit to yet.

-Peff

#224 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 4 lines · Jeff King

That was why I tried to lay something out in the last email. I _think_ it would not be too much work to get there from the last iteration. It would mostly be removing code/feature that we're not ready to commit to yet.

Sounds fair. Thanks.

#225 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Jeff King
Quoted 3 lines

I think what would have moved things forward more than just a ping, though, is generating a plan for moving forward in the face of confusing review.

Very fair point!

Quoted 1 line

That can be quite hard for first-time contributors, though.

I'm not actually a first time contributor 🤗 I worked on this feature between around 2016-18. So I know it can take a while:

https://git.kernel.org/pub/scm/git/git.git/commit/?h=seen&id=1fb20dfd8ed70b4459312918a71444bc79ea6f0b

Harald

#226 lore raw

Re: [PATCH v28 2/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren
Quoted 8 lines · Harald Nordgren
Quoted 7 lines
Quoted 5 lines

So what about this. We add a config option that takes a list of items to compare against. But it _only_ supports @{upstream} and @{push} for now, and interprets them as branch@{upstream} and branch@{push} (but probably done manually, not via dwim_ref). That limitation gets documented.

Sounds like a good way forward.

Quoted 7 lines

So if I do these changes, are we ready to merge then?

I worked on this feature for 2 months, and then it got marked as stale instead of being merged. Will this time be different?

Harald

Try not to be discouraged. It is not unusual for a series to undergo lengthy work and many revisions. It's part of what keeps Git working so well.

Signature · 1 line
D. Ben Knoble
#227 lore raw

v29 cover status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected] cc:
Jeff King [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: add status.compareBranches config for multiple branch
comparisons

 Documentation/config/status.adoc |  19 ++
 remote.c                         | 180 +++++++++++++----
 t/t6040-tracking-info.sh         | 335 +++++++++++++++++++++++++++++++
 3 files changed, 497 insertions(+), 37 deletions(-)

base-commit: 7c02d39fc2ed2702223c7674f73150d9a7e61ba4
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v29
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v29
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v28:

 1:  f3c8c782b0 = 1:  48db1f4847 refactor format_branch_comparison in preparation
 2:  067978dd09 ! 2:  6a88f41fa5 status: add status.compareBranches config for multiple branch comparisons
     @@ Metadata
       ## Commit message ##
          status: add status.compareBranches config for multiple branch comparisons
      
     -    Add a new configuration variable `status.compareBranches` that allows
     -    users to specify a space-separated list of branches to compare against
     -    the current branch in `git status` output.
     +    Add a new configuration variable status.compareBranches that allows
     +    users to specify a space-separated list of branch comparisons in
     +    git status output.
      
     -    Each branch in the list can be:
     -    - A remote-tracking branch name (e.g., `origin/main`)
     -    - The special reference `@{upstream}` for the tracking branch
     -    - The special reference `@{push}` for the push destination
     +    Supported values:
     +    - @{upstream} for the current branch's upstream tracking branch
     +    - @{push} for the current branch's push destination
     +
     +    Any other value is ignored and a warning is shown.
      
          When not configured, the default behavior is equivalent to setting
     -    `status.compareBranches = @{upstream}`, preserving backward compatibility.
     +    `status.compareBranches = @{upstream}`, preserving backward
     +    compatibility.
      
          The advice messages shown are context-aware:
          - "git pull" advice is shown only when comparing against @{upstream}
     @@ Documentation/config/status.adoc: status.aheadBehind::
       	non-porcelain status formats.  Defaults to true.
       
      +status.compareBranches::
     -+	A space-separated list of branches to compare the current branch
     -+	against in linkgit:git-status[1]. Each branch specification can be
     -+	a remote-tracking branch name (e.g. `origin/main`), or a special
     -+	reference like `@{upstream}` or `@{push}`. For each branch in the
     -+	list, git status shows whether the current branch is ahead, behind,
     -+	or has diverged from that branch.
     ++	A space-separated list of branch comparison specifiers to use in
     ++	linkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}`
     ++	are supported. They are interpreted as `branch@{upstream}` and
     ++	`branch@{push}` for the current branch.
      ++
      +If not set, the default behavior is equivalent to `@{upstream}`, which
      +compares against the configured upstream tracking branch.
     @@ Documentation/config/status.adoc: status.aheadBehind::
      ++
      +----
      +[status]
     -+	compareBranches = origin/main origin/develop
     ++	compareBranches = @{upstream} @{push}
      +----
      ++
     -+This would show comparisons against both `origin/main` and `origin/develop`.
     ++This would show comparisons against both the configured upstream and push
     ++tracking branches for the current branch.
      +
       status.displayCommentPrefix::
       	If set to true, linkgit:git-status[1] will insert a comment
     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_
       
      +static char *resolve_compare_branch(struct branch *branch, const char *name)
      +{
     -+	struct strbuf buf = STRBUF_INIT;
      +	const char *resolved = NULL;
     -+	char *ret;
      +
      +	if (!branch || !name)
      +		return NULL;
      +
     -+	if (!strcasecmp(name, "@{upstream}") || !strcasecmp(name, "@{u}"))
     ++	if (!strcasecmp(name, "@{upstream}"))
      +		resolved = branch_get_upstream(branch, NULL);
      +	else if (!strcasecmp(name, "@{push}"))
      +		resolved = branch_get_push(branch, NULL);
     ++	else {
     ++		warning(_("ignoring value '%s' for status.compareBranches; only @{upstream} and @{push} are supported"),
     ++			name);
     ++		return NULL;
     ++	}
      +
      +	if (resolved)
      +		return xstrdup(resolved);
     -+
     -+	strbuf_addf(&buf, "refs/remotes/%s", name);
     -+	resolved = refs_resolve_ref_unsafe(
     -+		get_main_ref_store(the_repository),
     -+		buf.buf,
     -+		RESOLVE_REF_READING,
     -+		NULL, NULL);
     -+	if (resolved) {
     -+		ret = xstrdup(resolved);
     -+		strbuf_release(&buf);
     -+		return ret;
     -+	}
     -+
     -+	strbuf_release(&buf);
      +	return NULL;
      +}
      +
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status.compareBranches with upstream and origin remotes multiple compare branches' '
     ++test_expect_success 'status.compareBranches supports ordered upstream/push entries' '
      +	(
      +		cd test &&
      +		git checkout -b feature6 upstream/main &&
      +		git push origin &&
      +		advance work &&
     -+		git -c status.compareBranches="upstream/main origin/feature6 origin/feature5" status >../actual
     ++		git -c status.compareBranches="@{push} @{upstream}" status >../actual
      +	) &&
      +	cat >expect <<-EOF &&
      +	On branch feature6
     -+	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
     -+
      +	Your branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.
      +	  (use "git push" to publish your local commits)
      +
     -+	Your branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit.
     ++	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
      +
      +	nothing to commit, working tree clean
      +	EOF
Signature · 1 line
gitgitgadget
#228 lore raw

PATCH v29 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 48 insertions(+), 34 deletions(-)
remote.c +48 -34
index f6980dc656..e9e2f56ed6 100644
@@ -2234,42 +2234,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2278,7 +2257,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2289,7 +2268,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
@@ -2302,12 +2281,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			       "and have %d and %d different commits each, "
 			       "respectively.\n",
 			   ours + theirs),
-			base, ours, theirs);
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#229 lore raw

PATCH v29 2/2 status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Add a new configuration variable status.compareBranches that allows users to specify a space-separated list of branch comparisons in git status output.

Supported values:
- @{upstream} for the current branch's upstream tracking branch
- @{push} for the current branch's push destination

Any other value is ignored and a warning is shown.

When not configured, the default behavior is equivalent to setting status.compareBranches = @{upstream}, preserving backward compatibility.

The advice messages shown are context-aware:
- "git pull" advice is shown only when comparing against @{upstream}
- "git push" advice is shown only when comparing against @{push}
- Divergence advice is shown for upstream branch comparisons

This is useful for triangular workflows where the upstream tracking branch differs from the push destination, allowing users to see their status relative to both branches at once.

Example configuration:

    [status]
        compareBranches = @{upstream} @{push}
---
 Documentation/config/status.adoc |  19 ++
 remote.c                         | 148 +++++++++++---
 t/t6040-tracking-info.sh         | 335 +++++++++++++++++++++++++++++++
 3 files changed, 474 insertions(+), 28 deletions(-)
Documentation/config/status.adoc +19 -0
index 8caf90f51c..15ccd0116b 100644
@@ -17,6 +17,25 @@ status.aheadBehind::
 	`--no-ahead-behind` by default in linkgit:git-status[1] for
 	non-porcelain status formats.  Defaults to true.
 
+status.compareBranches::
+	A space-separated list of branch comparison specifiers to use in
+	linkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}`
+	are supported. They are interpreted as `branch@{upstream}` and
+	`branch@{push}` for the current branch.
++
+If not set, the default behavior is equivalent to `@{upstream}`, which
+compares against the configured upstream tracking branch.
++
+Example:
++
+----
+[status]
+	compareBranches = @{upstream} @{push}
+----
++
+This would show comparisons against both the configured upstream and push
+tracking branches for the current branch.
+
 status.displayCommentPrefix::
 	If set to true, linkgit:git-status[1] will insert a comment
 	prefix before each output line (starting with
remote.c +120 -28

202 lines not on this page · raw message

t/t6040-tracking-info.sh +335 -0

339 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#230 lore raw

Re: [PATCH v29 2/2] status: add status.compareBranches config for multiple branch comparisons

remote.c int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs, · line 2253

Quoted 11 lines · Harald Nordgren via GitGitGadget
remote.c +11 -0
+static char *resolve_compare_branch(struct branch *branch, const char *name)
+{
+	const char *resolved = NULL;
+
+	if (!branch || !name)
+		return NULL;
+
+	if (!strcasecmp(name, "@{upstream}"))
+		resolved = branch_get_upstream(branch, NULL);
+	else if (!strcasecmp(name, "@{push}"))
+		resolved = branch_get_push(branch, NULL);

As one of the if/else if/ cascade needs {} around a multi-statement block, everybody else needs {}.

If the current branch does not have @{upstream}, we will silently receive NULL in resolved, and return NULL from here, which is exactly what we want. The same stroy for missing @{push}.

Sounds very sensible.

remote.c int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs, · line 2258

Quoted 5 lines
remote.c +5 -0
+	else {
+		warning(_("ignoring value '%s' for status.compareBranches; only @{upstream} and @{push} are supported"),
+			name);
+		return NULL;
+	}

I don't know if which one between the above "warning" and die("unknown/unsupported") is a better design. In any case, the warning() line is overly long and needs to be wrapped (my litmus test to complain about "overly long lines" is after losing three columns to the left for "> +" in e-mail quote like the above the right edge of the line does not fit on my 92-column terminal, which will never happen if you stick to the official "fit 80-column after quoted for a few times in e-mail" guideline).

Quoted 4 lines

static void format_branch_comparison(struct strbuf *sb,

remote.c +0 -0
 				     bool up_to_date,
 				     int ours, int theirs,
 				     const char *branch_name,

remote.c int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs, · line 2277

Quoted 10 lines
remote.c +7 -1
 				     enum ahead_behind_flags abf,
-				     bool show_divergence_advice)
+				     unsigned flags)
 {
+	bool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&
+		advice_enabled(ADVICE_STATUS_HINTS);
+	bool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&
+		advice_enabled(ADVICE_STATUS_HINTS);

You do use enable_push_advice twice so that reduces repetition a little bit, but other than that, I am not sure if the above makes it easier to follow the code.

Especially, the roundabout way these three variables are computed, together with the fact that ENABLE_ADVICE_* are not really about "enabling", but the caller decides that it is an applicable advice (e.g., ENABLE_ADVICE_PULL is passed when the iteration over branches.items[] the caller is making is on the upstream branch and "you should pull" is the appropriate situation. It is more like "advice-pull is applicable in this situation", unlike what the advice_enabled() returns, which is "the user wants this kind of advice messages"), I find the resulting code below somehow harder to follow than without these intermediate variables. If they were named "use_*_advice" (instead of "enable_"), perhaps I may find it more palatable.

I find it a lot more disturbing that what the caller specifies in flags and advice_enabled() is pre-combined in these variables. Perhaps splitting them into two separate concerns, like this ...

	bool use_push_advice = (flags & USE_ADVICE_PUSH);
	bool use_pull_advice = (flags & USE_ADVICE_PULL);
	bool use_divergence_advice = (flags & USE_ADVICE_DIVERGENCE);

	...
	if (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS))
		... you should pull first ...

... may make it easier to understand and follow? I dunno.

Quoted 43 lines
remote.c +1 -1
-		if (advice_enabled(ADVICE_STATUS_HINTS))
+		if (enable_push_advice)
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
t/t6040-tracking-info.sh +29 -0
index 0b719bbae6..aa9456bb61 100755
@@ -292,4 +292,339 @@ test_expect_success '--set-upstream-to @{-1}' '
 	test_cmp expect actual
 '
 
+test_expect_success 'status tracking origin/main shows only main' '
+	(
+		cd test &&
+		git checkout b4 &&
+		git status >../actual
+	) &&
+	cat >expect <<-EOF &&
+	On branch b4
+	Your branch is ahead of ${SQ}origin/main${SQ} by 2 commits.
+	  (use "git push" to publish your local commits)
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '
+	(
+		cd test &&
+		git checkout b4 &&
+		git status --no-ahead-behind >../actual
+	) &&
+	cat >expect <<-EOF &&
+	On branch b4
+	Your branch and ${SQ}origin/main${SQ} refer to different commits.
+	  (use "git status --ahead-behind" for details)
+
+	nothing to commit, working tree clean
+	EOF

t/t6040-tracking-info.sh test_expect_success '--set-upstream-to @{-1}' ' · line 333

Quoted 10 lines
t/t6040-tracking-info.sh +10 -0
+	test_cmp expect actual
+'
+
+test_expect_success 'setup for compareBranches tests' '
+	(
+		cd test &&
+		git config push.default current &&
+		git config status.compareBranches "@{upstream} @{push}"
+	)
+'

Shoudln't this be part of the next test, with its own teardown? I.e.,

	test_expect_success 'check both @{upstream} and @{push}' '
        	test_config -C test push.default current &&
        	test_config -C test status.compareBranches "..." &&

		git -C test checkout main &&
		git -C status >actual &&
		cat >expect <<-EOF &&
		...
		EOF
		test_cmp expect actual
	'
Quoted 4 lines
t/t6040-tracking-info.sh +4 -0
+test_expect_success 'status.compareBranches from upstream has no duplicates' '
+	(
+		cd test &&
+		git checkout main &&

t/t6040-tracking-info.sh test_expect_success '--set-upstream-to @{-1}' ' · line 348

Quoted 10 lines
t/t6040-tracking-info.sh +10 -0
+		git status >../actual
+	) &&
+	cat >expect <<-EOF &&
+	On branch main
+	Your branch is up to date with ${SQ}origin/main${SQ}.
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'

That way, the tests can be kept more independent from each other, and will clean after themselves, which means that we do not have to assume that ...

t/t6040-tracking-info.sh test_expect_success '--set-upstream-to @{-1}' ' · line 628

Quoted 8 lines

...

t/t6040-tracking-info.sh +6 -0
+test_expect_success 'clean up after compareBranches tests' '
+	(
+		cd test &&
+		git config --unset status.compareBranches
+	)
+'

... this step will never be skipped or fail, which would result in disturbing the tests that come after this step.

In any case, the design of the main part of the patch is looking much nicer than the open-ended one we saw previously.

Thanks.

#231 lore raw

Re: [PATCH v29 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 3 lines

You do use enable_push_advice twice so that reduces repetition a little bit, but other than that, I am not sure if the above makes it easier to follow the code.

This was orginally abstracted because of feedback from someone else in this patch. But I update it now, togethe with the rest of your feedback, thanks for the help!

Harald

#232 lore raw

v30 cover status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected] cc:
Jeff King [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: add status.compareBranches config for multiple branch
comparisons

 Documentation/config/status.adoc |  19 ++
 remote.c                         | 178 ++++++++++++++----
 t/t6040-tracking-info.sh         | 310 +++++++++++++++++++++++++++++++
 3 files changed, 470 insertions(+), 37 deletions(-)

base-commit: 7b2bccb0d58d4f24705bf985de1f4612e4cf06e5
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v30
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v30
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v29:

 1:  48db1f4847 = 1:  7f517b8c7f refactor format_branch_comparison in preparation
 2:  6a88f41fa5 ! 2:  501bd40294 status: add status.compareBranches config for multiple branch comparisons
     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_
      +	if (!branch || !name)
      +		return NULL;
      +
     -+	if (!strcasecmp(name, "@{upstream}"))
     ++	if (!strcasecmp(name, "@{upstream}")) {
      +		resolved = branch_get_upstream(branch, NULL);
     -+	else if (!strcasecmp(name, "@{push}"))
     ++	} else if (!strcasecmp(name, "@{push}")) {
      +		resolved = branch_get_push(branch, NULL);
     -+	else {
     -+		warning(_("ignoring value '%s' for status.compareBranches; only @{upstream} and @{push} are supported"),
     ++	} else {
     ++		warning(_("ignoring value '%s' for status.compareBranches, "
     ++			  "only @{upstream} and @{push} are supported"),
      +			name);
      +		return NULL;
      +	}
     @@ remote.c: int stat_tracking_info(struct branch *branch, int *num_ours, int *num_
      -				     bool show_divergence_advice)
      +				     unsigned flags)
       {
     -+	bool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) &&
     -+		advice_enabled(ADVICE_STATUS_HINTS);
     -+	bool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) &&
     -+		advice_enabled(ADVICE_STATUS_HINTS);
     -+	bool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) &&
     -+		advice_enabled(ADVICE_STATUS_HINTS);
     ++	bool use_push_advice = (flags & ENABLE_ADVICE_PUSH);
     ++	bool use_pull_advice = (flags & ENABLE_ADVICE_PULL);
     ++	bool use_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE);
      +
       	if (up_to_date) {
       		strbuf_addf(sb,
     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,
       			    _("Your branch and '%s' refer to different commits.\n"),
       			    branch_name);
      -		if (advice_enabled(ADVICE_STATUS_HINTS))
     -+		if (enable_push_advice)
     ++		if (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))
       			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
       				    "git status --ahead-behind");
       	} else if (!theirs) {
     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,
       			   ours),
       			branch_name, ours);
      -		if (advice_enabled(ADVICE_STATUS_HINTS))
     -+		if (enable_push_advice)
     ++		if (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))
       			strbuf_addstr(sb,
       				_("  (use \"git push\" to publish your local commits)\n"));
       	} else if (!ours) {
     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,
       			   theirs),
       			branch_name, theirs);
      -		if (advice_enabled(ADVICE_STATUS_HINTS))
     -+		if (enable_pull_advice)
     ++		if (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS))
       			strbuf_addstr(sb,
       				_("  (use \"git pull\" to update your local branch)\n"));
       	} else {
     @@ remote.c: static void format_branch_comparison(struct strbuf *sb,
       			branch_name, ours, theirs);
      -		if (show_divergence_advice &&
      -		    advice_enabled(ADVICE_STATUS_HINTS))
     -+		if (enable_divergence_advice)
     ++		if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS))
       			strbuf_addstr(sb,
       				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
       	}
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'setup for compareBranches tests' '
     -+	(
     -+		cd test &&
     -+		git config push.default current &&
     -+		git config status.compareBranches "@{upstream} @{push}"
     -+	)
     -+'
     -+
      +test_expect_success 'status.compareBranches from upstream has no duplicates' '
     -+	(
     -+		cd test &&
     -+		git checkout main &&
     -+		git status >../actual
     -+	) &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout main &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch main
      +	Your branch is up to date with ${SQ}origin/main${SQ}.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '
     -+	(
     -+		cd test &&
     -+		git checkout -b feature2 origin/main &&
     -+		git push origin HEAD &&
     -+		advance work &&
     -+		git status >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout -b feature2 origin/main &&
     ++	git -C test push origin HEAD &&
     ++	(cd test && advance work) &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature2
      +	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'checkout with status.compareBranches shows both branches' '
     -+	(
     -+		cd test &&
     -+		git checkout feature2 >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout feature2 >actual &&
      +	cat >expect <<-EOF &&
      +	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
      +
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status.compareBranches shows diverged and ahead' '
     -+	(
     -+		cd test &&
     -+		git checkout feature4 &&
     -+		git branch --set-upstream-to origin/main &&
     -+		git push origin HEAD &&
     -+		advance work &&
     -+		git status >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout feature4 &&
     ++	git -C test branch --set-upstream-to origin/main &&
     ++	git -C test push origin HEAD &&
     ++	(cd test && advance work) &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature4
      +	Your branch and ${SQ}origin/main${SQ} have diverged,
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status --no-ahead-behind with status.compareBranches' '
     -+	(
     -+		cd test &&
     -+		git checkout feature4 &&
     -+		git status --no-ahead-behind >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout feature4 &&
     ++	git -C test status --no-ahead-behind >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature4
      +	Your branch and ${SQ}origin/main${SQ} refer to different commits.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	(
      +		cd test &&
      +		git remote add upstream ../. &&
     -+		git fetch upstream &&
     -+		git config remote.pushDefault origin
     ++		git fetch upstream
      +	)
      +'
      +
      +test_expect_success 'status.compareBranches with upstream and origin remotes' '
     -+	(
     -+		cd test &&
     -+		git checkout -b feature5 upstream/main &&
     -+		git push origin &&
     -+		advance work &&
     -+		git status >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test remote.pushDefault origin &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout -b feature5 upstream/main &&
     ++	git -C test push origin &&
     ++	(cd test && advance work) &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature5
      +	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status.compareBranches supports ordered upstream/push entries' '
     -+	(
     -+		cd test &&
     -+		git checkout -b feature6 upstream/main &&
     -+		git push origin &&
     -+		advance work &&
     -+		git -c status.compareBranches="@{push} @{upstream}" status >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test remote.pushDefault origin &&
     ++	test_config -C test status.compareBranches "@{push} @{upstream}" &&
     ++	git -C test checkout -b feature6 upstream/main &&
     ++	git -C test push origin &&
     ++	(cd test && advance work) &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature6
      +	Your branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status.compareBranches with diverged push branch' '
     -+	(
     -+		cd test &&
     -+		git checkout -b feature7 upstream/main &&
     -+		advance work &&
     -+		git push origin &&
     -+		git reset --hard upstream/main &&
     -+		advance work &&
     -+		git status >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test remote.pushDefault origin &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout -b feature7 upstream/main &&
     ++	(cd test && advance work71) &&
     ++	git -C test push origin &&
     ++	git -C test reset --hard upstream/main &&
     ++	(cd test && advance work72) &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature7
      +	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status.compareBranches shows up to date branches' '
     -+	(
     -+		cd test &&
     -+		git checkout -b feature8 upstream/main &&
     -+		git push origin &&
     -+		git status >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test remote.pushDefault origin &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout -b feature8 upstream/main &&
     ++	git -C test push origin &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature8
      +	Your branch is up to date with ${SQ}upstream/main${SQ}.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status --no-ahead-behind with status.compareBranches up to date' '
     -+	(
     -+		cd test &&
     -+		git checkout feature8 &&
     -+		git push origin &&
     -+		git status --no-ahead-behind >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test remote.pushDefault origin &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout feature8 >actual &&
     ++	git -C test push origin &&
     ++	git -C test status --no-ahead-behind >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature8
      +	Your branch is up to date with ${SQ}upstream/main${SQ}.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'checkout with status.compareBranches shows up to date' '
     -+	(
     -+		cd test &&
     -+		git checkout feature8 >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test remote.pushDefault origin &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout feature8 >actual &&
      +	cat >expect <<-EOF &&
      +	Your branch is up to date with ${SQ}upstream/main${SQ}.
      +
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status.compareBranches with upstream behind and push up to date' '
     -+	(
     -+		cd test &&
     -+		git checkout -b ahead upstream/main &&
     -+		advance work &&
     -+		git push upstream HEAD &&
     -+		git checkout -b feature9 upstream/main &&
     -+		git push origin &&
     -+		git branch --set-upstream-to upstream/ahead &&
     -+		git status >../actual
     -+	) &&
     ++	test_config -C test push.default current &&
     ++	test_config -C test remote.pushDefault origin &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout -b ahead upstream/main &&
     ++	(cd test && advance work) &&
     ++	git -C test push upstream HEAD &&
     ++	git -C test checkout -b feature9 upstream/main &&
     ++	git -C test push origin &&
     ++	git -C test branch --set-upstream-to upstream/ahead &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature9
      +	Your branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status.compareBranches with remapped push refspec' '
     -+	(
     -+		cd test &&
     -+		git checkout -b feature10 origin/main &&
     -+		git config remote.origin.push refs/heads/feature10:refs/heads/remapped &&
     -+		git push &&
     -+		advance work &&
     -+		git status >../actual
     -+	) &&
     ++	test_config -C test remote.origin.push refs/heads/feature10:refs/heads/remapped &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout -b feature10 origin/main &&
     ++	git -C test push &&
     ++	(cd test && advance work) &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature10
      +	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status.compareBranches with remapped push and upstream remote' '
     -+	(
     -+		cd test &&
     -+		git checkout -b feature11 upstream/main &&
     -+		git config remote.origin.push refs/heads/feature11:refs/heads/remapped &&
     -+		git push origin &&
     -+		advance work &&
     -+		git status >../actual
     -+	) &&
     ++	test_config -C test remote.pushDefault origin &&
     ++	test_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout -b feature11 upstream/main &&
     ++	git -C test push origin &&
     ++	(cd test && advance work) &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature11
      +	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	EOF
      +	test_cmp expect actual
      +'
     -+
     -+test_expect_success 'clean up after compareBranches tests' '
     -+	(
     -+		cd test &&
     -+		git config --unset status.compareBranches
     -+	)
     -+'
      +
       test_done
Signature · 1 line
gitgitgadget
#233 lore raw

PATCH v30 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 48 insertions(+), 34 deletions(-)
remote.c +48 -34
index f6980dc656..e9e2f56ed6 100644
@@ -2234,42 +2234,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2278,7 +2257,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2289,7 +2268,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
@@ -2302,12 +2281,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			       "and have %d and %d different commits each, "
 			       "respectively.\n",
 			   ours + theirs),
-			base, ours, theirs);
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#234 lore raw

PATCH v30 2/2 status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Add a new configuration variable status.compareBranches that allows users to specify a space-separated list of branch comparisons in git status output.

Supported values:
- @{upstream} for the current branch's upstream tracking branch
- @{push} for the current branch's push destination

Any other value is ignored and a warning is shown.

When not configured, the default behavior is equivalent to setting status.compareBranches = @{upstream}, preserving backward compatibility.

The advice messages shown are context-aware:
- "git pull" advice is shown only when comparing against @{upstream}
- "git push" advice is shown only when comparing against @{push}
- Divergence advice is shown for upstream branch comparisons

This is useful for triangular workflows where the upstream tracking branch differs from the push destination, allowing users to see their status relative to both branches at once.

Example configuration:

    [status]
        compareBranches = @{upstream} @{push}
---
 Documentation/config/status.adoc |  19 ++
 remote.c                         | 146 ++++++++++++---
 t/t6040-tracking-info.sh         | 310 +++++++++++++++++++++++++++++++
 3 files changed, 447 insertions(+), 28 deletions(-)
Documentation/config/status.adoc +19 -0
index 8caf90f51c..15ccd0116b 100644
@@ -17,6 +17,25 @@ status.aheadBehind::
 	`--no-ahead-behind` by default in linkgit:git-status[1] for
 	non-porcelain status formats.  Defaults to true.
 
+status.compareBranches::
+	A space-separated list of branch comparison specifiers to use in
+	linkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}`
+	are supported. They are interpreted as `branch@{upstream}` and
+	`branch@{push}` for the current branch.
++
+If not set, the default behavior is equivalent to `@{upstream}`, which
+compares against the configured upstream tracking branch.
++
+Example:
++
+----
+[status]
+	compareBranches = @{upstream} @{push}
+----
++
+This would show comparisons against both the configured upstream and push
+tracking branches for the current branch.
+
 status.displayCommentPrefix::
 	If set to true, linkgit:git-status[1] will insert a comment
 	prefix before each output line (starting with
remote.c +118 -28

200 lines not on this page · raw message

t/t6040-tracking-info.sh +310 -0

314 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#235 lore raw

Re: [PATCH v30 2/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 32 lines · Harald Nordgren via GitGitGadget
 t/t6040-tracking-info.sh         | 310 +++++++++++++++++++++++++++++++
 3 files changed, 447 insertions(+), 28 deletions(-)
t/t6040-tracking-info.sh +21 -0
index 0b719bbae6..c24f545036 100755
@@ -292,4 +292,314 @@ test_expect_success '--set-upstream-to @{-1}' '
 	test_cmp expect actual
 '
 
+test_expect_success 'status tracking origin/main shows only main' '
+	(
+		cd test &&
+		git checkout b4 &&
+		git status >../actual
+	) &&
+	cat >expect <<-EOF &&
+	On branch b4
+	Your branch is ahead of ${SQ}origin/main${SQ} by 2 commits.
+	  (use "git push" to publish your local commits)
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '
+	(
+		cd test &&
+		git checkout b4 &&
+		git status --no-ahead-behind >../actual

t/t6040-tracking-info.sh test_expect_success '--set-upstream-to @{-1}' ' · line 325

Quoted 10 lines
t/t6040-tracking-info.sh +10 -0
+	) &&
+	cat >expect <<-EOF &&
+	On branch b4
+	Your branch and ${SQ}origin/main${SQ} refer to different commits.
+	  (use "git status --ahead-behind" for details)
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'

The above two uses ( cd test && git $command ) to do what it can do with "git -C test $command", but the rest of the script seems to mostly stick to the latter. Want to be a bit more consistent?

t/t6040-tracking-info.sh test_expect_success '--set-upstream-to @{-1}' ' · line 336

Quoted 11 lines
t/t6040-tracking-info.sh +11 -0
+test_expect_success 'status.compareBranches from upstream has no duplicates' '
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	git -C test checkout main &&
+	git -C test status >actual &&
+	cat >expect <<-EOF &&
+	On branch main
+	Your branch is up to date with ${SQ}origin/main${SQ}.
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'

We see only one because... @{upstream} and @{push} are both origin/main and we can dedupe and there is no extra advice needed? If @{push} were missing and only @{upstream} existed, we would also see just one, so this test feels a bit under-explained.

t/t6040-tracking-info.sh test_expect_success '--set-upstream-to @{-1}' ' · line 342

Quoted 6 lines
t/t6040-tracking-info.sh +6 -0
+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '
+	test_config -C test push.default current &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	git -C test checkout -b feature2 origin/main &&
+	git -C test push origin HEAD &&
+	(cd test && advance work) &&

One thing the "advance" function does is to call "test_tick" to increment the mock timestamp, but the incremented mock timestamp would not survive beyond the end of a subshell. Not that it matters too much to have commits with the same timestamp in these tests, as long as things are made more reproducible by use of the "advance" function.

t/t6040-tracking-info.sh test_expect_success '--set-upstream-to @{-1}' ' · line 354

Quoted 12 lines
t/t6040-tracking-info.sh +12 -0
+	git -C test status >actual &&
+	cat >expect <<-EOF &&
+	On branch feature2
+	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+
+	Your branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.
+	  (use "git push" to publish your local commits)
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'

Very straight-forward If we ask for @{upstream} and @{push} that resolve to different things, we would see two different reports.

I am curious what should [jc: not asking what the code happens to do; wondering what the sensible behaviour to help users is] happen when the user configures the variable to "@{push} @{upstream} @{push}" in this case. Should we see just two entries (one for @{push}, the other for @{upstream})?

I also am curious what should [jc: ditto] happen when @{upstream} and @{push} point at the same origin/main and our current branch is ahead by 1 commit. The pull side would say "you are ahead of origin/main" and stop wile the push side would say the same thing with advice to push it out for publishing? Or should they get deduped?

t/t6040-tracking-info.sh test_expect_success '--set-upstream-to @{-1}' ' · line 366

Quoted 12 lines
t/t6040-tracking-info.sh +12 -0
+test_expect_success 'checkout with status.compareBranches shows both branches' '
+	test_config -C test push.default current &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	git -C test checkout feature2 >actual &&
+	cat >expect <<-EOF &&
+	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
+
+	Your branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.
+	  (use "git push" to publish your local commits)
+	EOF
+	test_cmp expect actual
+'

If the previous step succeeded, we would be on feature2 branch in the "test" repository, so this checkout is a no-op-only-for-status checkout and we expect to see the same thing as the previous step.

Good.

If the previous step failed, this step may break, but that is what "cd t && sh t6040*.sh -i" is for ;-).

Quoted 26 lines
t/t6040-tracking-info.sh +24 -0
+test_expect_success 'setup for ahead of tracked but diverged from main' '
+	(
+		cd test &&
+		git checkout -b feature4 origin/main &&
+		advance work1 &&
+		git checkout origin/main &&
+		advance work2 &&
+		git push origin HEAD:main &&
+		git checkout feature4 &&
+		advance work3
+	)
+'
+
+test_expect_success 'status.compareBranches shows diverged and ahead' '
+	test_config -C test push.default current &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
 
 
+	git -C test checkout feature4 &&
+	git -C test branch --set-upstream-to origin/main &&
+	git -C test push origin HEAD &&
+	(cd test && advance work) &&
+	git -C test status >actual &&
+	cat >expect <<-EOF &&
+	On branch feature4
+	Your branch and ${SQ}origin/main${SQ} have diverged,

t/t6040-tracking-info.sh test_expect_success '--set-upstream-to @{-1}' '

Quoted 10 lines
t/t6040-tracking-info.sh +10 -0
+	and have 3 and 1 different commits each, respectively.
+	  (use "git pull" if you want to integrate the remote branch with yours)
+
+	Your branch is ahead of ${SQ}origin/feature4${SQ} by 1 commit.
+	  (use "git push" to publish your local commits)
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'

Very understandable.

Quoted 6 lines
t/t6040-tracking-info.sh +6 -0
+test_expect_success 'status --no-ahead-behind with status.compareBranches' '
+	test_config -C test push.default current &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	git -C test checkout feature4 &&
+	git -C test status --no-ahead-behind >actual &&
+	cat >expect <<-EOF &&

t/t6040-tracking-info.sh

Quoted 10 lines
t/t6040-tracking-info.sh +10 -0
+	On branch feature4
+	Your branch and ${SQ}origin/main${SQ} refer to different commits.
+
+	Your branch and ${SQ}origin/feature4${SQ} refer to different commits.
+	  (use "git status --ahead-behind" for details)
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'

OK.

Quoted 17 lines
t/t6040-tracking-info.sh +17 -0
+test_expect_success 'setup upstream remote' '
+	(
+		cd test &&
+		git remote add upstream ../. &&
+		git fetch upstream
+	)
+'
+
+test_expect_success 'status.compareBranches with upstream and origin remotes' '
+	test_config -C test push.default current &&
+	test_config -C test remote.pushDefault origin &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	git -C test checkout -b feature5 upstream/main &&
+	git -C test push origin &&
+	(cd test && advance work) &&
+	git -C test status >actual &&
+	cat >expect <<-EOF &&

t/t6040-tracking-info.sh

Quoted 10 lines
t/t6040-tracking-info.sh +10 -0
+	On branch feature5
+	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+	Your branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit.
+	  (use "git push" to publish your local commits)
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'

OK. There aren't much essential difference from what we have already seen so far, which dealt with remote tracking branch(es) from a single remote repository. Instead of origin/{main,$feature}, we are now dealing with {upstream,origin}/{main,$feature} but we can only pick one for @{upstream} and one for @{push}, we do not have to worry about all 4 possible permutations.

Quoted 9 lines
t/t6040-tracking-info.sh +9 -0
+test_expect_success 'status.compareBranches supports ordered upstream/push entries' '
+	test_config -C test push.default current &&
+	test_config -C test remote.pushDefault origin &&
+	test_config -C test status.compareBranches "@{push} @{upstream}" &&
+	git -C test checkout -b feature6 upstream/main &&
+	git -C test push origin &&
+	(cd test && advance work) &&
+	git -C test status >actual &&
+	cat >expect <<-EOF &&

t/t6040-tracking-info.sh

Quoted 10 lines
t/t6040-tracking-info.sh +10 -0
+	On branch feature6
+	Your branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit.
+	  (use "git push" to publish your local commits)
+
+	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'

Ah, I guess this example hints the answer to my earlier "@{push} @{upstream} @{push}" question? The user can control the order of entries shown by tweaking the order they list things on the configuration variable. So we would see the one for @{push} and then another for @{upstream}. We can show the one for @{push} again after these two and claim that it is working as the user told us to, or we can stop showing after showing @{push} and then @{upstream} and claim that we are deduping. Either is fine, but we need to be consistent and we need to have it documented.

Quoted 11 lines
t/t6040-tracking-info.sh +11 -0
+test_expect_success 'status.compareBranches with diverged push branch' '
+	test_config -C test push.default current &&
+	test_config -C test remote.pushDefault origin &&
+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
+	git -C test checkout -b feature7 upstream/main &&
+	(cd test && advance work71) &&
+	git -C test push origin &&
+	git -C test reset --hard upstream/main &&
+	(cd test && advance work72) &&
+	git -C test status >actual &&
+	cat >expect <<-EOF &&

t/t6040-tracking-info.sh

Quoted 10 lines
t/t6040-tracking-info.sh +10 -0
+	On branch feature7
+	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
+
+	Your branch and ${SQ}origin/feature7${SQ} have diverged,
+	and have 1 and 1 different commits each, respectively.
+
+	nothing to commit, working tree clean
+	EOF
+	test_cmp expect actual
+'

The same as before, I guess. Nothing unexpected to see here, or in the rest of the patch.

So td;lr is that the tests in this step mostly look great. It is just the semantics of "dedup" is a bit unclear, without a test that makes it clear (like the "@{push} @{upstream} @{push}" one), and perhaps a bit more documentation how having multiple tokens on the status.compareBranches variable affects the output. A (n easier) half of that additional documentation could be as small as the following, but I didn't attempt to deal with the "dedup" part.

 Documentation/config/status.adoc | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
Documentation/config/status.adoc +2 -1
index 15ccd0116b..e2c4a014a4 100644
@@ -34,7 +34,8 @@ Example:
 ----
 +
 This would show comparisons against both the configured upstream and push
-tracking branches for the current branch.
+tracking branches for the current branch in this order (as that is how
+upstream and push appear in the value of the variable).
 
 status.displayCommentPrefix::
 	If set to true, linkgit:git-status[1] will insert a comment
#236 lore raw

Re: [PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 3 lines

The above two uses ( cd test && git $command ) to do what it can do with "git -C test $command", but the rest of the script seems to mostly stick to the latter. Want to be a bit more consistent?

Good point, will fix!

Quoted 4 lines

We see only one because... @{upstream} and @{push} are both origin/main and we can dedupe and there is no extra advice needed? If @{push} were missing and only @{upstream} existed, we would also see just one, so this test feels a bit under-explained.

Renaming this this now to clarify that it tests deduplicating. Also added two new tests, for when reach of @{push} and @{upstream} are used alone.

Quoted 6 lines

One thing the "advance" function does is to call "test_tick" to increment the mock timestamp, but the incremented mock timestamp would not survive beyond the end of a subshell. Not that it matters too much to have commits with the same timestamp in these tests, as long as things are made more reproducible by use of the "advance" function.

Yeah, it's not great when used with subshells, I ended up differentiting commits only by name like 'advance work2' because as you say, otherwise they will get the same commit hash.

Quoted 6 lines

I also am curious what should [jc: ditto] happen when @{upstream} and @{push} point at the same origin/main and our current branch is ahead by 1 commit. The pull side would say "you are ahead of origin/main" and stop wile the push side would say the same thing with advice to push it out for publishing? Or should they get deduped?

Jumping in altough the question was not directed to me:

When behind and ahead of the same branch we show the "diverged" message, and since only one line is shown when deduped I think it works in a logical way.

Quoted 7 lines

So td;lr is that the tests in this step mostly look great. It is just the semantics of "dedup" is a bit unclear, without a test that makes it clear (like the "@{push} @{upstream} @{push}" one), and perhaps a bit more documentation how having multiple tokens on the status.compareBranches variable affects the output. A (n easier) half of that additional documentation could be as small as the following, but I didn't attempt to deal with the "dedup" part.

Will add documentation for the depuping and a new test case.

Harald

#237 lore raw

Re: [PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget
Quoted 12 lines · Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected] cc:
Jeff King [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: add status.compareBranches config for multiple branch
comparisons

Quoted 4 lines
 Documentation/config/status.adoc |  19 ++
 remote.c                         | 178 ++++++++++++++----
 t/t6040-tracking-info.sh         | 310 +++++++++++++++++++++++++++++++
 3 files changed, 470 insertions(+), 37 deletions(-)

Will replace. Let's wait to see if we hear further comments on the topic for a day or two and then mark the topic for 'next'. I didn't spot anything unexpected in this round, but haven't had enough time to spend on the test part, which I want to read a bit more carefully before merging.

Thanks.

#238 lore raw

Re: [PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 1 line

Will replace.

What does this mean? 🤗

Quoted 5 lines

Let's wait to see if we hear further comments on the topic for a day or two and then mark the topic for 'next'. I didn't spot anything unexpected in this round, but haven't had enough time to spend on the test part, which I want to read a bit more carefully before merging.

Did you have a chance to look at the tests?

Harald

#239 lore raw

Re: [PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 3 lines · Harald Nordgren
Quoted 1 line

Will replace.

What does this mean? 🤗

That he will replace the branch he has for this topic (hn/status-compare-with-push) with this new round.

Quoted 1 line

[snip]

#240 lore raw

Re: [PATCH v30 0/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren
Quoted 7 lines · Harald Nordgren
Quoted 5 lines

Let's wait to see if we hear further comments on the topic for a day or two and then mark the topic for 'next'. I didn't spot anything unexpected in this round, but haven't had enough time to spend on the test part, which I want to read a bit more carefully before merging.

Did you have a chance to look at the tests?

Not me, it was a bit too messy for reading in a single sitting.

#241 lore raw

v31 cover status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected] cc:
Jeff King [email protected]

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: add status.compareBranches config for multiple branch
comparisons

 Documentation/config/status.adoc |  25 +++
 remote.c                         | 178 ++++++++++++----
 t/t6040-tracking-info.sh         | 354 +++++++++++++++++++++++++++++++
 3 files changed, 520 insertions(+), 37 deletions(-)

base-commit: 50d063e335afd5828fbb9de2f2b2fb44fd884d2b
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v31
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v31
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v30:

 1:  7f517b8c7f = 1:  320c1ce55a refactor format_branch_comparison in preparation
 2:  501bd40294 ! 2:  f07ccb278c status: add status.compareBranches config for multiple branch comparisons
     @@ Documentation/config/status.adoc: status.aheadBehind::
      +If not set, the default behavior is equivalent to `@{upstream}`, which
      +compares against the configured upstream tracking branch.
      ++
     ++The entries are shown in the order they appear in the configuration.
     ++Duplicate entries that resolve to the same ref are suppressed after
     ++their first occurrence, so `@{push} @{upstream} @{push}` shows at
     ++most two comparisons.  When `@{upstream}` and `@{push}` resolve to
     ++the same remote-tracking branch, only one comparison is shown.
     +++
      +Example:
      ++
      +----
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
       '
       
      +test_expect_success 'status tracking origin/main shows only main' '
     -+	(
     -+		cd test &&
     -+		git checkout b4 &&
     -+		git status >../actual
     -+	) &&
     ++	git -C test checkout b4 &&
     ++	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch b4
      +	Your branch is ahead of ${SQ}origin/main${SQ} by 2 commits.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status --no-ahead-behind tracking origin/main shows only main' '
     -+	(
     -+		cd test &&
     -+		git checkout b4 &&
     -+		git status --no-ahead-behind >../actual
     -+	) &&
     ++	git -C test checkout b4 &&
     ++	git -C test status --no-ahead-behind >actual &&
      +	cat >expect <<-EOF &&
      +	On branch b4
      +	Your branch and ${SQ}origin/main${SQ} refer to different commits.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status.compareBranches from upstream has no duplicates' '
     ++test_expect_success 'status.compareBranches deduplicates when upstream and push are the same' '
      +	test_config -C test status.compareBranches "@{upstream} @{push}" &&
      +	git -C test checkout main &&
      +	git -C test status >actual &&
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '
     ++test_expect_success 'status.compareBranches with only upstream shows only upstream' '
     ++	test_config -C test status.compareBranches "@{upstream}" &&
     ++	git -C test checkout main &&
     ++	git -C test status >actual &&
     ++	cat >expect <<-EOF &&
     ++	On branch main
     ++	Your branch is up to date with ${SQ}origin/main${SQ}.
     ++
     ++	nothing to commit, working tree clean
     ++	EOF
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'status.compareBranches with only push shows only push' '
      +	test_config -C test push.default current &&
     -+	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	test_config -C test status.compareBranches "@{push}" &&
      +	git -C test checkout -b feature2 origin/main &&
      +	git -C test push origin HEAD &&
      +	(cd test && advance work) &&
      +	git -C test status >actual &&
      +	cat >expect <<-EOF &&
      +	On branch feature2
     ++	Your branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.
     ++	  (use "git push" to publish your local commits)
     ++
     ++	nothing to commit, working tree clean
     ++	EOF
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' '
     ++	test_config -C test push.default current &&
     ++	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     ++	git -C test checkout -b feature3 origin/main &&
     ++	git -C test push origin HEAD &&
     ++	(cd test && advance work) &&
     ++	git -C test status >actual &&
     ++	cat >expect <<-EOF &&
     ++	On branch feature3
      +	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
      +
     -+	Your branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.
     ++	Your branch is ahead of ${SQ}origin/feature3${SQ} by 1 commit.
      +	  (use "git push" to publish your local commits)
      +
      +	nothing to commit, working tree clean
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +test_expect_success 'checkout with status.compareBranches shows both branches' '
      +	test_config -C test push.default current &&
      +	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     -+	git -C test checkout feature2 >actual &&
     ++	git -C test checkout feature3 >actual &&
      +	cat >expect <<-EOF &&
      +	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
      +
     -+	Your branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit.
     ++	Your branch is ahead of ${SQ}origin/feature3${SQ} by 1 commit.
      +	  (use "git push" to publish your local commits)
      +	EOF
      +	test_cmp expect actual
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_cmp expect actual
      +'
      +
     ++test_expect_success 'status.compareBranches deduplicates repeated specifiers' '
     ++	test_config -C test push.default current &&
     ++	test_config -C test remote.pushDefault origin &&
     ++	test_config -C test status.compareBranches "@{push} @{upstream} @{push}" &&
     ++	git -C test checkout -b feature7 upstream/main &&
     ++	git -C test push origin &&
     ++	(cd test && advance work) &&
     ++	git -C test status >actual &&
     ++	cat >expect <<-EOF &&
     ++	On branch feature7
     ++	Your branch is ahead of ${SQ}origin/feature7${SQ} by 1 commit.
     ++	  (use "git push" to publish your local commits)
     ++
     ++	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
     ++
     ++	nothing to commit, working tree clean
     ++	EOF
     ++	test_cmp expect actual
     ++'
     ++
      +test_expect_success 'status.compareBranches with diverged push branch' '
      +	test_config -C test push.default current &&
      +	test_config -C test remote.pushDefault origin &&
      +	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     -+	git -C test checkout -b feature7 upstream/main &&
     -+	(cd test && advance work71) &&
     ++	git -C test checkout -b feature8 upstream/main &&
     ++	(cd test && advance work81) &&
      +	git -C test push origin &&
      +	git -C test reset --hard upstream/main &&
     -+	(cd test && advance work72) &&
     ++	(cd test && advance work82) &&
      +	git -C test status >actual &&
      +	cat >expect <<-EOF &&
     -+	On branch feature7
     ++	On branch feature8
      +	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
      +
     -+	Your branch and ${SQ}origin/feature7${SQ} have diverged,
     ++	Your branch and ${SQ}origin/feature8${SQ} have diverged,
      +	and have 1 and 1 different commits each, respectively.
      +
      +	nothing to commit, working tree clean
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_config -C test push.default current &&
      +	test_config -C test remote.pushDefault origin &&
      +	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     -+	git -C test checkout -b feature8 upstream/main &&
     ++	git -C test checkout -b feature9 upstream/main &&
      +	git -C test push origin &&
      +	git -C test status >actual &&
      +	cat >expect <<-EOF &&
     -+	On branch feature8
     ++	On branch feature9
      +	Your branch is up to date with ${SQ}upstream/main${SQ}.
      +
     -+	Your branch is up to date with ${SQ}origin/feature8${SQ}.
     ++	Your branch is up to date with ${SQ}origin/feature9${SQ}.
      +
      +	nothing to commit, working tree clean
      +	EOF
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_config -C test push.default current &&
      +	test_config -C test remote.pushDefault origin &&
      +	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     -+	git -C test checkout feature8 >actual &&
     ++	git -C test checkout feature9 >actual &&
      +	git -C test push origin &&
      +	git -C test status --no-ahead-behind >actual &&
      +	cat >expect <<-EOF &&
     -+	On branch feature8
     ++	On branch feature9
      +	Your branch is up to date with ${SQ}upstream/main${SQ}.
      +
     -+	Your branch is up to date with ${SQ}origin/feature8${SQ}.
     ++	Your branch is up to date with ${SQ}origin/feature9${SQ}.
      +
      +	nothing to commit, working tree clean
      +	EOF
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	test_config -C test push.default current &&
      +	test_config -C test remote.pushDefault origin &&
      +	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     -+	git -C test checkout feature8 >actual &&
     ++	git -C test checkout feature9 >actual &&
      +	cat >expect <<-EOF &&
      +	Your branch is up to date with ${SQ}upstream/main${SQ}.
      +
     -+	Your branch is up to date with ${SQ}origin/feature8${SQ}.
     ++	Your branch is up to date with ${SQ}origin/feature9${SQ}.
      +	EOF
      +	test_cmp expect actual
      +'
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +	git -C test checkout -b ahead upstream/main &&
      +	(cd test && advance work) &&
      +	git -C test push upstream HEAD &&
     -+	git -C test checkout -b feature9 upstream/main &&
     ++	git -C test checkout -b feature10 upstream/main &&
      +	git -C test push origin &&
      +	git -C test branch --set-upstream-to upstream/ahead &&
      +	git -C test status >actual &&
      +	cat >expect <<-EOF &&
     -+	On branch feature9
     ++	On branch feature10
      +	Your branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded.
      +	  (use "git pull" to update your local branch)
      +
     -+	Your branch is up to date with ${SQ}origin/feature9${SQ}.
     ++	Your branch is up to date with ${SQ}origin/feature10${SQ}.
      +
      +	nothing to commit, working tree clean
      +	EOF
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +'
      +
      +test_expect_success 'status.compareBranches with remapped push refspec' '
     -+	test_config -C test remote.origin.push refs/heads/feature10:refs/heads/remapped &&
     ++	test_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&
      +	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     -+	git -C test checkout -b feature10 origin/main &&
     ++	git -C test checkout -b feature11 origin/main &&
      +	git -C test push &&
      +	(cd test && advance work) &&
      +	git -C test status >actual &&
      +	cat >expect <<-EOF &&
     -+	On branch feature10
     ++	On branch feature11
      +	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
      +
      +	Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.
     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to @{-1}' '
      +
      +test_expect_success 'status.compareBranches with remapped push and upstream remote' '
      +	test_config -C test remote.pushDefault origin &&
     -+	test_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped &&
     ++	test_config -C test remote.origin.push refs/heads/feature12:refs/heads/remapped &&
      +	test_config -C test status.compareBranches "@{upstream} @{push}" &&
     -+	git -C test checkout -b feature11 upstream/main &&
     ++	git -C test checkout -b feature12 upstream/main &&
      +	git -C test push origin &&
      +	(cd test && advance work) &&
      +	git -C test status >actual &&
      +	cat >expect <<-EOF &&
     -+	On branch feature11
     ++	On branch feature12
      +	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
      +
      +	Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.
Signature · 1 line
gitgitgadget
#242 lore raw

PATCH v31 1/2 refactor format_branch_comparison in preparation

From: Harald Nordgren <[email protected]>

Refactor format_branch_comparison function in preparation for showing comparison with push remote tracking branch.

---
 remote.c | 82 +++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 48 insertions(+), 34 deletions(-)
remote.c +48 -34
index f6980dc656..e9e2f56ed6 100644
@@ -2234,42 +2234,21 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
 	return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
 }
 
-/*
- * Return true when there is anything to report, otherwise false.
- */
-int format_tracking_info(struct branch *branch, struct strbuf *sb,
-			 enum ahead_behind_flags abf,
-			 int show_divergence_advice)
-{
-	int ours, theirs, sti;
-	const char *full_base;
-	char *base;
-	int upstream_is_gone = 0;
-
-	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
-	if (sti < 0) {
-		if (!full_base)
-			return 0;
-		upstream_is_gone = 1;
-	}
-
-	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
-					    full_base, 0);
-	if (upstream_is_gone) {
-		strbuf_addf(sb,
-			_("Your branch is based on '%s', but the upstream is gone.\n"),
-			base);
-		if (advice_enabled(ADVICE_STATUS_HINTS))
-			strbuf_addstr(sb,
-				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
-	} else if (!sti) {
+static void format_branch_comparison(struct strbuf *sb,
+				     bool up_to_date,
+				     int ours, int theirs,
+				     const char *branch_name,
+				     enum ahead_behind_flags abf,
+				     bool show_divergence_advice)
+{
+	if (up_to_date) {
 		strbuf_addf(sb,
 			_("Your branch is up to date with '%s'.\n"),
-			base);
+			branch_name);
 	} else if (abf == AHEAD_BEHIND_QUICK) {
 		strbuf_addf(sb,
 			    _("Your branch and '%s' refer to different commits.\n"),
-			    base);
+			    branch_name);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
 				    "git status --ahead-behind");
@@ -2278,7 +2257,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			Q_("Your branch is ahead of '%s' by %d commit.\n",
 			   "Your branch is ahead of '%s' by %d commits.\n",
 			   ours),
-			base, ours);
+			branch_name, ours);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git push\" to publish your local commits)\n"));
@@ -2289,7 +2268,7 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			   "Your branch is behind '%s' by %d commits, "
 			       "and can be fast-forwarded.\n",
 			   theirs),
-			base, theirs);
+			branch_name, theirs);
 		if (advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" to update your local branch)\n"));
@@ -2302,12 +2281,47 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
 			       "and have %d and %d different commits each, "
 			       "respectively.\n",
 			   ours + theirs),
-			base, ours, theirs);
+			branch_name, ours, theirs);
 		if (show_divergence_advice &&
 		    advice_enabled(ADVICE_STATUS_HINTS))
 			strbuf_addstr(sb,
 				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
 	}
+}
+
+/*
+ * Return true when there is anything to report, otherwise false.
+ */
+int format_tracking_info(struct branch *branch, struct strbuf *sb,
+			 enum ahead_behind_flags abf,
+			 int show_divergence_advice)
+{
+	int ours, theirs, cmp_fetch;
+	const char *full_base;
+	char *base;
+	int upstream_is_gone = 0;
+
+	cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
+	if (cmp_fetch < 0) {
+		if (!full_base)
+			return 0;
+		upstream_is_gone = 1;
+	}
+
+	base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
+					    full_base, 0);
+
+	if (upstream_is_gone) {
+		strbuf_addf(sb,
+			_("Your branch is based on '%s', but the upstream is gone.\n"),
+			base);
+		if (advice_enabled(ADVICE_STATUS_HINTS))
+			strbuf_addstr(sb,
+				_("  (use \"git branch --unset-upstream\" to fixup)\n"));
+	} else {
+		format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
+	}
+
 	free(base);
 	return 1;
 }
Signature · 1 line
gitgitgadget
#243 lore raw

PATCH v31 2/2 status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget

From: Harald Nordgren <[email protected]>

Add a new configuration variable status.compareBranches that allows users to specify a space-separated list of branch comparisons in git status output.

Supported values:
- @{upstream} for the current branch's upstream tracking branch
- @{push} for the current branch's push destination

Any other value is ignored and a warning is shown.

When not configured, the default behavior is equivalent to setting status.compareBranches = @{upstream}, preserving backward compatibility.

The advice messages shown are context-aware:
- "git pull" advice is shown only when comparing against @{upstream}
- "git push" advice is shown only when comparing against @{push}
- Divergence advice is shown for upstream branch comparisons

This is useful for triangular workflows where the upstream tracking branch differs from the push destination, allowing users to see their status relative to both branches at once.

Example configuration:

    [status]
        compareBranches = @{upstream} @{push}
---
 Documentation/config/status.adoc |  25 +++
 remote.c                         | 146 ++++++++++---
 t/t6040-tracking-info.sh         | 354 +++++++++++++++++++++++++++++++
 3 files changed, 497 insertions(+), 28 deletions(-)
Documentation/config/status.adoc +25 -0
index 8caf90f51c..b5dd85b761 100644
@@ -17,6 +17,31 @@ status.aheadBehind::
 	`--no-ahead-behind` by default in linkgit:git-status[1] for
 	non-porcelain status formats.  Defaults to true.
 
+status.compareBranches::
+	A space-separated list of branch comparison specifiers to use in
+	linkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}`
+	are supported. They are interpreted as `branch@{upstream}` and
+	`branch@{push}` for the current branch.
++
+If not set, the default behavior is equivalent to `@{upstream}`, which
+compares against the configured upstream tracking branch.
++
+The entries are shown in the order they appear in the configuration.
+Duplicate entries that resolve to the same ref are suppressed after
+their first occurrence, so `@{push} @{upstream} @{push}` shows at
+most two comparisons.  When `@{upstream}` and `@{push}` resolve to
+the same remote-tracking branch, only one comparison is shown.
++
+Example:
++
+----
+[status]
+	compareBranches = @{upstream} @{push}
+----
++
+This would show comparisons against both the configured upstream and push
+tracking branches for the current branch.
+
 status.displayCommentPrefix::
 	If set to true, linkgit:git-status[1] will insert a comment
 	prefix before each output line (starting with
remote.c +118 -28

200 lines not on this page · raw message

t/t6040-tracking-info.sh +354 -0

358 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#244 lore raw

Re: [PATCH v31 0/2] status: add status.compareBranches config for multiple branch comparisons

In reply to Harald Nordgren via GitGitGadget
Quoted 20 lines · Harald Nordgren via GitGitGadget

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: add status.compareBranches config for multiple branch
comparisons

 Documentation/config/status.adoc |  25 +++
 remote.c                         | 178 ++++++++++++----
 t/t6040-tracking-info.sh         | 354 +++++++++++++++++++++++++++++++
 3 files changed, 520 insertions(+), 37 deletions(-)

base-commit: 50d063e335afd5828fbb9de2f2b2fb44fd884d2b
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v31
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v31
Pull-Request: https://github.com/git/git/pull/2138
...
Range-diff vs v30:

 1:  7f517b8c7f = 1:  320c1ce55a refactor format_branch_comparison in preparation
 2:  501bd40294 ! 2:  f07ccb278c status: add status.compareBranches config for multiple branch comparisons
Quoted 9 lines

@@ Documentation/config/status.adoc: status.aheadBehind::
+If not set, the default behavior is equivalent to @{upstream}, which +compares against the configured upstream tracking branch. ++
++The entries are shown in the order they appear in the configuration. ++Duplicate entries that resolve to the same ref are suppressed after ++their first occurrence, so @{push} @{upstream} @{push} shows at ++most two comparisons. When @{upstream} and @{push} resolve to ++the same remote-tracking branch, only one comparison is shown.

As the previous iteration of this series is already in 'next', we'd need to turn this into an incremental. Luckily, as the difference is fairly clear and only in [2/2], let me try to do so myself to potentially save one roundtrip.

Thanks.

----- >8 -----
From: Harald Nordgren <[email protected]>
Date: Wed, 04 Mar 2026 12:25:31 +0000
Subject: [PATCH 3/2] status: clarify how status.compareBranches deduplicates

The order of output when multiple branches are specified on the configuration variable was not clearly spelled out in the documentation.

Add a paragraph to describe the order and also how the branches are deduplicated. Update t6040 with additional tests to illustrate how multiple branches are shown and deduplicated.

[jc: made a whole replacement into incremental; wrote log message.]
---
 Documentation/config/status.adoc |   6 ++
 t/t6040-tracking-info.sh         | 120 ++++++++++++++++++++++++++-------------
 2 files changed, 88 insertions(+), 38 deletions(-)
Documentation/config/status.adoc +6 -0
index 15ccd0116b..b5dd85b761 100644
@@ -26,6 +26,12 @@ status.compareBranches::
 If not set, the default behavior is equivalent to `@{upstream}`, which
 compares against the configured upstream tracking branch.
 +
+The entries are shown in the order they appear in the configuration.
+Duplicate entries that resolve to the same ref are suppressed after
+their first occurrence, so `@{push} @{upstream} @{push}` shows at
+most two comparisons.  When `@{upstream}` and `@{push}` resolve to
+the same remote-tracking branch, only one comparison is shown.
++
 Example:
 +
 ----
t/t6040-tracking-info.sh +82 -38

244 lines not on this page · raw message

#245 lore raw

Re: [PATCH v31 0/2] status: add status.compareBranches config for multiple branch comparisons

Thanks for all your help here! Is there anything you need from me now?

Harald

#246 lore raw

Re: [PATCH v31 0/2] status: add status.compareBranches config for multiple branch comparisons

Quoted 1 line · Harald Nordgren

Thanks for all your help here! Is there anything you need from me now?

Thanks for all the work. If you haven't heard any breakages from others, and if you haven't found any breakages yourself, there is nothing we need from you right at this moment.

The patch series is cooking in 'next' and unless people find something glaringly wrong, will be merged down to 'master' later (after which, people might still find something wrong in it, of course---any breakages and regressions need to be handled no matter anyway ;-).

#247 lore raw

Re: [PATCH v20 0/2] status: show comparison with push remote tracking branch

In reply to Harald Nordgren via GitGitGadget

"Harald Nordgren via GitGitGadget" <[email protected]> writes:

Here is a place for you to explain what helps people to decypher the differences since the previous iteration in Range-diff (which is not very easy to read).

Quoted 5 lines

cc: Chris Torek [email protected] cc: Yee Cheng Chin [email protected] cc: "brian m. carlson" [email protected]
cc: Ben Knoble [email protected] cc: "Kristoffer Haugsbakk" [email protected] cc: Phillip Wood [email protected]
cc: Nico Williams [email protected] cc: Patrick Steinhardt [email protected]

I am not sure what good these lines are doing (to GGG).

Quoted 32 lines

Harald Nordgren (2):
refactor format_branch_comparison in preparation
status: show comparison with push remote tracking branch

 remote.c                 | 183 ++++++++++++++++++++-------
 t/t6040-tracking-info.sh | 262 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 403 insertions(+), 42 deletions(-)

base-commit: d529f3a197364881746f558e5652f0236131eb86
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2138%2FHaraldNordgren%2Fahead_of_main_status-v20
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2138/HaraldNordgren/ahead_of_main_status-v20
Pull-Request: https://github.com/git/git/pull/2138

Range-diff vs v19:

1: 451d7a4986 ! 1: bb3e00863b refactor format_branch_comparison in preparation
@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
if (advice_enabled(ADVICE_STATUS_HINTS))
strbuf_addstr(sb,
_(" (use \"git pull\" to update your local branch)\n"));
-@@ remote.c: int format_tracking_info(struct branch *branch, struct strbuf *sb,
- "and have %d and %d different commits each, "
- "respectively.\n",
- ours + theirs),
+ } 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, "

Quoted 8 lines

+- "respectively.\n",
+- ours + theirs),
- base, ours, theirs);
++ "Your branch and '%s' have diverged,\n"
++ "and have %d and %d different commits each, respectively.\n",
+ branch_name, ours, theirs);
if (show_divergence_advice &&
advice_enabled(ADVICE_STATUS_HINTS))

Could you not mix the ours+theirs thing into the same step? Either make it a standalone patch to clean up before or after your main 2 patches, or leave it totally outside the series and send it after this series settles.

#248 lore raw

Re: [PATCH v20 0/2] status: show comparison with push remote tracking branch

Quoted 3 lines

Here is a place for you to explain what helps people to decypher the differences since the previous iteration in Range-diff (which is not very easy to read).

?

Quoted 4 lines

Could you not mix the ours+theirs thing into the same step? Either make it a standalone patch to clean up before or after your main 2 patches, or leave it totally outside the series and send it after this series settles.

Got it, I'll revert that change.

Harald

#249 lore raw

Re: [PATCH v20 0/2] status: show comparison with push remote tracking branch

Quoted 5 lines · Harald Nordgren
Quoted 3 lines

Here is a place for you to explain what helps people to decypher the differences since the previous iteration in Range-diff (which is not very easy to read).

?

It’s a bit opaque. He’s saying that you should reserve some space in the cover letter email (PATCH 0/2) for explaining what has changed in this version compared to the previous one.

People often write:

Here is what this patch series does.

### Changes in v3

Fixed the commit message typos.

I don’t know where this should be filled in when using gitgitgadget. ;)

#250 lore raw
In reply to Harald Nordgren via GitGitGadget
Quoted 3 lines

The default branch is determined dynamically by checking:
1. refs/remotes/upstream/HEAD (if upstream remote exists)
2. refs/remotes/origin/HEAD (fallback)

I feel like this is making a lot of assumptions regarding remotes. "origin" and "upstream" are not inherently special names for remotes. I personally have different Git repositories where they could mean slightly different things, and I don't use the "upstream" wording myself (I sometimes use "official" for the upstream branch, and/or "ychin" for my own fork's remote). Feels like we should not be imposing such a hard-coded value when nothing else in Git enforces it.

Also, when there are multiple remotes, it's not always clear which one the user actually cares about. It's not always clear if they care about the upstream or the downstream remote, of a third one that actually matters more.

This would also work poorly with detached branches (e.g. the popular 'gh-pages' branches in a lot of repositories), or permanently foked branches like fixed versions (e.g. v2.x legacy branch when the software main branch moved to v3.0). Seems like for this to work well it would need to be configurable per branch. Even on a repository level there would likely be lots of edge cases with each branch having its unique circumstances.

#251 lore raw

These are very fair points, maybe a reason not to have this feature on by default then.

For me, I work against clear origin/master, origin/master, origin/develop branches at my dayjob, and when I do open-source there is a default branch to work against. The GitHub 'gh' tool defaults to upstream and origin repos, which is why I chose those. But I agree it might not be right for everyone.

Maybe what I wrote in a previous message about making this configurable via 'repo.settings.defaultBranch' (maybe call it 'repo.settings.statusGoalBranch') would be useful, but having it off by default instead of on by default as I originally suggested.

I feel strongly that it should be able to be set be repo-specific (and globally). Having it only per branch defeats a big part of it. Should be straightforward git config to have a repo-wide rule but still allow disabling it for e.g. 'gh-pages', I hope?

Harald

#252 lore raw
Quoted 1 line

I feel strongly that it should be able to be set be repo-specific (and globally). Having it only per branch defeats a big part of it. Should be straightforward git config to have a repo-wide rule but still allow disabling it for e.g. 'gh-pages', I hope?

I don't have that much of an opinion on per-repo / per-branch configuration but I just wanted to point out that some branches would benefit from the ability to turn this off. Perhaps it could be a two layered setting that could be both configured per-repo / per-branch but I don't know if this makes it too complicated.

#253 lore raw
In reply to Yee Cheng Chin
Quoted 6 lines · Yee Cheng Chin
Quoted 3 lines

The default branch is determined dynamically by checking:
1. refs/remotes/upstream/HEAD (if upstream remote exists)
2. refs/remotes/origin/HEAD (fallback)

I feel like this is making a lot of assumptions regarding remotes. "origin" and "upstream" are not inherently special names for remotes.

Good point. There is a mechanism to determine where a branch would be pushed to with "git push", and where the new material to update the branch would come from with "git pull", and these places need to be considered when doing comparisons. This series seems to punt on determining both repository and branch and instead uses a hardcoded "upstream" (or "origin") and "HEAD", which is not satisfactory.

Quoted 11 lines

I personally have different Git repositories where they could mean slightly different things, and I don't use the "upstream" wording myself (I sometimes use "official" for the upstream branch, and/or "ychin" for my own fork's remote). Feels like we should not be imposing such a hard-coded value when nothing else in Git enforces it.

Also, when there are multiple remotes, it's not always clear which one the user actually cares about. It's not always clear if they care about the upstream or the downstream remote, of a third one that actually matters more.

Quoted 7 lines

This would also work poorly with detached branches (e.g. the popular 'gh-pages' branches in a lot of repositories), or permanently foked branches like fixed versions (e.g. v2.x legacy branch when the software main branch moved to v3.0). Seems like for this to work well it would need to be configurable per branch. Even on a repository level there would likely be lots of edge cases with each branch having its unique circumstances.

Yes, unrelated branches like gh-pages gives us a very good example to think about.

Thanks.

#254 lore raw
Quoted 6 lines · Junio C Hamano

Good point. There is a mechanism to determine where a branch would be pushed to with "git push", and where the new material to update the branch would come from with "git pull", and these places need to be considered when doing comparisons. This series seems to punt on determining both repository and branch and instead uses a hardcoded "upstream" (or "origin") and "HEAD", which is not satisfactory.

I may be speaking for the author here, but I think the reason why the upstream discovery (or alternatively an additional configuration in the repo) feature was suggested is that not everyone uses the mechanism you described (which I think is the "pushRemote" config). It's true that if you *do* indeed have separate push/pull remotes for a branch, then both should ideally be shown in git status. However, oftentimes when you set up a feature branch it may not be correct / desirable to set up separate push/pull remotes for those, especially if you need to collaborate with people on the feature and therefore do need to pull from the same feature's branch. In this case, I think the author still wants git status to be able to show the diversion from the original upstream master/main.

Personally I wonder if this feature gets added, people will start asking for a list of "upstream branches" to be able to be compared to, rather than just a fixed upstream/HEAD or origin/HEAD. But maybe I'm thinking too far ahead.

But for example, I maintain MacVim, which is a downstream fork of Vim. As such, "upstream" is a fluid concept for me depending on what I'm doing. It could mean deviation between ychin/macvim vs macvim-dev/macvim, or macvim-dev/macvim vs vim/vim. For complicated forks, it's feasible someone may ask for the ability to see the branch comparison against multiple branches concurrently.

#255 lore raw
Quoted 4 lines · Yee Cheng Chin

I may be speaking for the author here, but I think the reason why the upstream discovery (or alternatively an additional configuration in the repo) feature was suggested is that not everyone uses the mechanism you described (which I think is the "pushRemote" config).

The mention of pushRemote by Harald is a red-herring. My point is that branch.<name>.remote tells what remote we interact with (and pushRemote is used as a fallback), so there is no need to, and it is actively wrong to, assume "upstream" or "origin". Even if you push back to the same repository as you pull from, you may push to a different branch there, in which case you do not need pushRemote, but you'd need to learn what configured remote you are using anyway.

#256 lore raw
Quoted 3 lines

This series seems to punt on
determining both repository and branch and instead uses a hardcoded "upstream" (or "origin") and "HEAD", which is not satisfactory.

I have made a change to the code to not assume "upstream" or "origin" as remote names. Now there is a config setting 'repo.settings.statusGoalBranch' to enable this the check.

I side-stepped the discussion about pushRemotes since what Yee Cheng Chin brought up seems to kill the possibility of determining comparison branch dynamically. Let me know what you think of the latest code and I can handle it if needed 🤗

Quoted 5 lines

I don't have that much of an opinion on per-repo / per-branch configuration but I just wanted to point out that some branches would benefit from the ability to turn this off. Perhaps it could be a two layered setting that could be both configured per-repo / per-branch but I don't know if this makes it too complicated.

I skipped this for now because I couldn't find a precedent in the sourc code for config settings that are "on repo-wide, but turned off for some branches". I could dig into this if we need it. I would like to hear Junio C Hamano's thoughts on this specific thing too 🤗

Harald

#258 lore raw
In reply to Junio C Hamano

I see push and pull remotes mentioned a lot here. And I want to find out how much of the problem that can solve. Let’s say you have two remotes ’upstream’ and ’fork’.

Can push and pull remotes be set up in a way so the user can run these bare commands without and additional args or flags:

git push # push to fork
git pull # pull from upstream

And if so, will ’git status’ also show the status against upstream?