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

From: Phillip Wood <[email-masked]>
Date: Fri, 9 Jan 2026 14:56:38 +0000

Hi Harald

On 05/01/2026 10:17, Harald Nordgren via GitGitGadget wrote:
> From: Harald Nordgren <[email-masked]>
> 
> "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 advice looks good

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

Sounds sensible

> diff --git a/remote.c b/remote.c
> index 7163a8ec28..1f87b85b22 100644
> --- a/remote.c
> +++ b/remote.c
> @@ -29,6 +29,11 @@
>   
>   enum map_direction { FROM_SRC, FROM_DST };
>   
> +enum branch_mode_flags {
> +	BRANCH_MODE_PULL = (1 << 0),
> +	BRANCH_MODE_PUSH = (1 << 1),
> +};

Using an enum for a set of flags is a bit confusing.

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

On reflection I wonder if we should be calling branch_get_push() instead 
of remote_ref_for_branch() and tracking_for_push_dest() as it respects 
'push.default' and so the branch it returns is the one that "git push" 
without any arguments would push to.

> +	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)

I think it would be simpler to just return the full refname and let the 
caller shorten it.

> +		*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,
>   				     int sti,
>   				     int ours, int theirs,
>   				     const char *branch_name,
>   				     enum ahead_behind_flags abf,
> +				     enum branch_mode_flags advice_flags,
>   				     int show_divergence_advice)
>   {
>   	if (!sti) {
> @@ -2252,7 +2311,8 @@ 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 ((advice_flags & BRANCH_MODE_PUSH) &&

Why are we checking for BRANCH_MODE_PUSH here? Don't we want to show 
this advice regardless of the mode?

 > +		    advice_enabled(ADVICE_STATUS_HINTS))
>   			strbuf_addf(sb, _("  (use \"%s\" for details)\n"),
>   				    "git status --ahead-behind");
>   	} else if (!theirs) {
> @@ -2261,7 +2321,8 @@ 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 ((advice_flags & BRANCH_MODE_PUSH) &&
> +		    advice_enabled(ADVICE_STATUS_HINTS))

Having to test the flags each time is a bit cumbersome. We could define
a couple of local variables to simplify this

	bool want_push_advice = (advice_flags & BRANCH_MODE_PUSH) &&
				advice_enabled(ADVICE_STATUS_HINTS);
	bool want_pull_advice = advice_flags & BRANCH_MODE_PULL &&
				advice_enabled(ADVICE_STATUS_HINTS);

Then we can simplify the above to

	if (want_push_advice)

>   			strbuf_addstr(sb,
>   				_("  (use \"git push\" to publish your local commits)\n"));
>   	} else if (!ours) {
 > [...]
> @@ -2285,7 +2347,8 @@ static void format_branch_comparison(struct strbuf *sb,
>   			       "respectively.\n",
>   			   ours + theirs),
>   			branch_name, ours, theirs);
> -		if (show_divergence_advice &&
> +		if ((advice_flags & BRANCH_MODE_PULL) &&
> +		    show_divergence_advice &&

If we don't want to show this can't we set show_divergance_adivce to 
false when we call this function - why is it guarded by BRANCH_MODE_PULL 
as well?

>   		    advice_enabled(ADVICE_STATUS_HINTS))
>   			strbuf_addstr(sb,
>   				_("  (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
> @@ -2303,6 +2366,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
>   	const char *full_base;
>   	char *base;
>   	int upstream_is_gone = 0;
> +	enum branch_mode_flags base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH;

Here we set an enum to a value that is not a member of the enum.

> +	int push_ours, push_theirs, push_sti;
> +	char *full_push = NULL;
> +	char *push = NULL;
> +	enum branch_mode_flags push_branch_modes = 0;
>   
>   	sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
>   	if (sti < 0) {
> @@ -2314,6 +2382,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)) {

This is good - we only show the push branch separately if it differs 
from the upstream branch.

> +		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;
> +		}

This combined with checking "push_branch_modes & BRANCH_MODE_PUSH" below 
ensures we skip the push branch if push_sti < 0. That's good but it is a 
bit hard to follow.

Thanks

Phillip

> +	}
> +
>   	if (upstream_is_gone) {
>   		strbuf_addf(sb,
>   			_("Your branch is based on '%s', but the upstream is gone.\n"),
> @@ -2322,10 +2400,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, 0);
>   	}
>   
>   	free(base);
> +	free(full_push);
> +	free(push);
>   	return 1;
>   }
>   
> diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh
> index 0b719bbae6..cf5a926dcd 100755
> --- a/t/t6040-tracking-info.sh
> +++ b/t/t6040-tracking-info.sh
> @@ -292,4 +292,266 @@ 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
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'status shows ahead of both origin/main and feature branch' '
> +	(
> +		cd test &&
> +		git checkout -b feature2 origin/main &&
> +		git push origin HEAD &&
> +		advance work &&
> +		git 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
> +'
> +
> +test_expect_success 'checkout shows ahead of both origin/main and feature branch' '
> +	(
> +		cd test &&
> +		git 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
> +'
> +
> +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 shows diverged from origin/main and ahead of feature branch' '
> +	(
> +		cd test &&
> +		git checkout feature4 &&
> +		git branch --set-upstream-to origin/main &&
> +		git push origin HEAD &&
> +		advance work &&
> +		git status >../actual
> +	) &&
> +	cat >expect <<-EOF &&
> +	On branch feature4
> +	Your branch and ${SQ}origin/main${SQ} 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 ${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
> +'
> +
> +test_expect_success 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' '
> +	(
> +		cd test &&
> +		git checkout feature4 &&
> +		git status --no-ahead-behind >../actual
> +	) &&
> +	cat >expect <<-EOF &&
> +	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
> +'
> +
> +test_expect_success 'setup upstream remote' '
> +	(
> +		cd test &&
> +		git remote add upstream ../. &&
> +		git fetch upstream &&
> +		git config remote.pushDefault origin
> +	)
> +'
> +
> +test_expect_success 'status with upstream remote and push.default set to origin' '
> +	(
> +		cd test &&
> +		git checkout -b feature5 upstream/main &&
> +		git push origin &&
> +		advance work &&
> +		git status >../actual
> +	) &&
> +	cat >expect <<-EOF &&
> +	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
> +'
> +
> +test_expect_success 'status with upstream remote and push.default set to origin and diverged' '
> +	(
> +		cd test &&
> +		git checkout -b feature6 upstream/main &&
> +		advance work &&
> +		git push origin &&
> +		git reset --hard upstream/main &&
> +		advance work &&
> +		git status >../actual
> +	) &&
> +	cat >expect <<-EOF &&
> +	On branch feature6
> +	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
> +
> +	Your branch and ${SQ}origin/feature6${SQ} have diverged,
> +	and have 1 and 1 different commits each, respectively.
> +
> +	nothing to commit, working tree clean
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'status with upstream remote and push branch up to date' '
> +	(
> +		cd test &&
> +		git checkout -b feature7 upstream/main &&
> +		git push origin &&
> +		git status >../actual
> +	) &&
> +	cat >expect <<-EOF &&
> +	On branch feature7
> +	Your branch is up to date with ${SQ}upstream/main${SQ}.
> +
> +	Your branch is up to date with ${SQ}origin/feature7${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' '
> +	(
> +		cd test &&
> +		git checkout feature7 &&
> +		git push origin &&
> +		git status --no-ahead-behind >../actual
> +	) &&
> +	cat >expect <<-EOF &&
> +	On branch feature7
> +	Your branch is up to date with ${SQ}upstream/main${SQ}.
> +
> +	Your branch is up to date with ${SQ}origin/feature7${SQ}.
> +
> +	nothing to commit, working tree clean
> +	EOF
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'checkout shows push branch up to date' '
> +	(
> +		cd test &&
> +		git checkout feature7 >../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}.
> +	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 push &&
> +		advance work &&
> +		git status >../actual
> +	) &&
> +	cat >expect <<-EOF &&
> +	On branch feature8
> +	Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit.
> +
> +	Your branch is ahead of ${SQ}origin/remapped${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 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 push origin &&
> +		advance work &&
> +		git status >../actual
> +	) &&
> +	cat >expect <<-EOF &&
> +	On branch feature9
> +	Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit.
> +
> +	Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit.
> +	  (use "git push" to publish your local commits)
> +
> +	nothing to commit, working tree clean
> +	EOF
> +	test_cmp expect actual
> +'
> +
>   test_done