Skip to content

status: add status.compareBranches config for multiple branch comparisons#2138

Open
HaraldNordgren wants to merge 2 commits intogit:masterfrom
HaraldNordgren:ahead_of_main_status
Open

status: add status.compareBranches config for multiple branch comparisons#2138
HaraldNordgren wants to merge 2 commits intogit:masterfrom
HaraldNordgren:ahead_of_main_status

Conversation

@HaraldNordgren
Copy link
Contributor

@HaraldNordgren HaraldNordgren commented Dec 23, 2025

cc: Chris Torek chris.torek@gmail.com
cc: Yee Cheng Chin ychin.macvim@gmail.com
cc: "brian m. carlson" sandals@crustytoothpaste.net
cc: Ben Knoble ben.knoble@gmail.com
cc: "Kristoffer Haugsbakk" kristofferhaugsbakk@fastmail.com
cc: Phillip Wood phillip.wood123@gmail.com
cc: Nico Williams nico@cryptonector.com
cc: Patrick Steinhardt ps@pks.im
cc: Jeff King peff@peff.net

@gitgitgadget-git
Copy link

There is an issue in commit 08472e8:
status: show default branch comparison when tracking non-default branch

  • Lines in the body of the commit messages should be wrapped between 60 and 76 characters.
    Indented lines, and lines without whitespace, are exempt

@HaraldNordgren HaraldNordgren force-pushed the ahead_of_main_status branch 3 times, most recently from 9ce374d to 2acdd07 Compare December 23, 2025 00:30
@HaraldNordgren
Copy link
Contributor Author

/preview

@gitgitgadget-git
Copy link

Preview email sent as pull.2138.git.git.1766450096285.gitgitgadget@gmail.com

@HaraldNordgren HaraldNordgren marked this pull request as ready for review December 23, 2025 00:49
@HaraldNordgren
Copy link
Contributor Author

/submit

@gitgitgadget-git
Copy link

Submitted as pull.2138.git.git.1766451217075.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-git-2138/HaraldNordgren/ahead_of_main_status-v1

To fetch this version to local tag pr-git-2138/HaraldNordgren/ahead_of_main_status-v1:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-git-2138/HaraldNordgren/ahead_of_main_status-v1

@gitgitgadget-git
Copy link

On the Git mailing list, Junio C Hamano wrote (reply to this):

"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> When a branch tracks a non-default remote branch (e.g.,
> origin/feature), git status now also displays how the branch
> compares to the default branch (origin/main or upstream/main).

"now" meaning what"?

The usual way to compose a log message of this project is to

 - Give an observation on how the current system works in the
   present tense (so no need to say "Currently X is Y", or
   "Previously X was Y" to describe the state before your change;
   just "X is Y" is enough), and discuss what you perceive as a
   problem in it.

 - Propose a solution (optional---often, problem description
   trivially leads to an obvious solution in reader's minds).

 - Give commands to somebody editing the codebase to "make it so",
   instead of saying "This commit does X".

in this order.

So if you are following the convention, "now also displays" ought to
be about what the current code without this patch does, but I am
sensing that it probably is not the case.

Start your explanation by decribing what the users see in "git
status" output without this patch.  Perhaps like

    "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" (i.e. you have built new history,
    while others are not touching it), "behind" (i.e. you haven't
    added any work since you were in-sync, while others have added
    their work on the branch), "diverged" (i.e. you have commits
    that you haven't pushed out, while others have added commits).

That is the "giving an observation" part.  And then describe why
that comparison with a single remote branch may be insufficient to
learn the current status.  Your reasoning might be something like

    When you fork a branch 'feature' from the 'main' branch of the
    remote, but then create 'feature' branch at the remote and push
    there, while you still occasionally pull from or rebase onto
    their 'main', you'd _also_ want to know how much you have
    diverged from 'main', in addition to how your 'feature' and
    their 'feature' compares.  Currently the comparison with 'main'
    is not given.

That's the "discuss your problem with the status quo" part.

Only after that, propose to show two sets of comparison.

> This helps users understand if their branch has drifted from the
> main development line even when it's in sync with its tracking
> branch.

Describe what does it help to know that after that sentence, like
"... to get the feel of when to start thinking about rebasing", or
something.

> The comparison is shown as a separate line after the tracking
> branch status:
> - "Ahead of 'origin/main' by N commits" when purely ahead
> - "Behind 'origin/main' by N commits" when purely behind
> - "Diverged from 'origin/main' by N commits" when diverged

In other words, exactly the same way as what we show with the
tracking branch?

The triangular workflow involves two remote things.  One is where
you pull from to catch up.  After building on top, you push to
somewhere else to publish your work.  This may be a different branch
in the same repository you pull from, or a branch in a completely
different repository.  What you pushed out may be processed by
others and may come back in the branch you pull from eventually to
complete the triangle.

In such a triangular workflow, comparison with these two remote
things may be needed. One with the branch you forked your work from
to know how much work _other_ people added to the branch to learn
when to start thinking about catching up, and with the branch you
are pushing your work to to know how much work you are holding
locally without pushing out.

I am not sure what you mean by the word "default" here, though.

You seem to be using the "what would a new user get when they clone
the remote (by virtue of their HEAD pointing at that branch)", but
I am not sure if that is a good way to determine the other remote
thing to compare with.

Even if one remote branch you pull from (but not push to) has a name
that is not one of those usual ones like 'main', 'master', 'trunk',
'default', you would want to compare with it in addition to where
you are pushing to.  So branch.<name>.merge + branch.<name>.remote
that defines where you pull from is one thing to compare with.  To
learn the other, the destination of a push of this branch, would
involve poking at remote.pushdefault, branch.<name>.pushRemote,
branch.<name>.remote to find out which remote repository it goes,
and then remote.<remote>.push to find out where this branch goes,
but the helper functions to learn all that are already available.

So, I think the topic addresses a good problem, its presentation
needs a bit more work, and its design (not the implementation) of
how to figure out the other thing to compare I am not sure about.

Thanks.

@gitgitgadget-git
Copy link

On the Git mailing list, Harald Nordgren wrote (reply to this):

> In other words, exactly the same way as what we show with the
> tracking branch?
> 
> The triangular workflow involves two remote things.  One is where
> you pull from to catch up.  After building on top, you push to
> somewhere else to publish your work.  This may be a different branch
> in the same repository you pull from, or a branch in a completely
> different repository.  What you pushed out may be processed by
> others and may come back in the branch you pull from eventually to
> complete the triangle.
> 
> In such a triangular workflow, comparison with these two remote
> things may be needed. One with the branch you forked your work from
> to know how much work _other_ people added to the branch to learn
> when to start thinking about catching up, and with the branch you
> are pushing your work to to know how much work you are holding
> locally without pushing out.

Yes, it's the same as when tracking master/main, but with far less complexity for the end-user. For many years I have had the habit of running these on my feature branches:

	git checkout -b feature_branch
	git branch --set-upstream-to origin/$(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@')

	# To merge in other's code early
	git pull --rebase origin $(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@')

	# To push to correct branch, because I'm tracking default instead "feature_branch"
	git push origin $(git rev-parse --abbrev-ref HEAD)

I have found this very hard to explain the benefits of this to other developers. Git is already scary to many, they are afraid of losing work, so they would rather not mess with the tracking branches and break the regular "git push" functionality.

For me, I feel blind of I can't see how my branch compares the master/main at all times.

> I am not sure what you mean by the word "default" here, though.
> 
> You seem to be using the "what would a new user get when they clone
> the remote (by virtue of their HEAD pointing at that branch)", but
> I am not sure if that is a good way to determine the other remote
> thing to compare with.
>
> Even if one remote branch you pull from (but not push to) has a name
> that is not one of those usual ones like 'main', 'master', 'trunk',
> 'default'

Agreed, this should be as agnostic as possible. "Default" might be me using GitHub terminology. However, it seems that 'git symbolic-ref refs/remotes/upstream/HEAD' always produces the desires result, so for the sake of discussion we can call it "upstream/HEAD" instead of "default".

Harald

@gitgitgadget-git
Copy link

On the Git mailing list, Harald Nordgren wrote (reply to this):

(Adding another comment to my own thread to reply to Junio C Hamano)

> branch.<name>.merge + branch.<name>.remote
> that defines where you pull from is one thing to compare with.  To
> learn the other, the destination of a push of this branch, would
> involve poking at remote.pushdefault, branch.<name>.pushRemote,
> branch.<name>.remote to find out which remote repository it goes,
> and then remote.<remote>.push to find out where this branch goes,
> but the helper functions to learn all that are already available.

When a new branch is created it has no push settings:

	git checkout -b ahead_of_main_status__tmp2

	git push
	fatal: The current branch ahead_of_main_status__tmp2 has no upstream branch.
	To push the current branch and set the remote as upstream, use

    	git push --set-upstream origin ahead_of_main_status__tmp2

	To have this happen automatically for branches without a tracking
	upstream, see 'push.autoSetupRemote' in 'git help config'.

Once the users runs that suggested command

	git push --set-upstream origin ahead_of_main_status__tmp2

then the 'branch.<name>.merge' and 'branch.<name>.remote' no longer hold the reference to "upstream/HEAD".

For sure, it would be great to re-use previous logic for this, but can it really be done without new logic?

Harald

@gitgitgadget-git
Copy link

On the Git mailing list, Chris Torek wrote (reply to this):

On Tue, Dec 23, 2025 at 3:36 AM Harald Nordgren
<haraldnordgren@gmail.com> wrote:
> Once the users runs that suggested command
>
>         git push --set-upstream origin ahead_of_main_status__tmp2
>
> then the 'branch.<name>.merge' and 'branch.<name>.remote' no longer hold the reference to "upstream/HEAD".

Right.

And, as Junio noted, to:

>> learn the other, the destination of a push of this branch, would
>> involve poking at remote.pushdefault, branch.<name>.pushRemote,
>> branch.<name>.remote to find out which remote repository it goes,
>> and then remote.<remote>.push to find out where this branch goes,

That is, there are some separate configuration items that can change
where `git push` goes.

Using `git push --set-upstream` sets ones that affect `git pull`, `git status`,
and `git push`, but it's possible to set ones that affect only `git push`.

That still leaves `git status` with the problem you (Harald) have observed,
so perhaps the path forward is to have `git status` check things like
branch.<name>.pushRemote to see if they exist and differ from
branch.<name>.remote.

Chris

@gitgitgadget-git
Copy link

User Chris Torek <chris.torek@gmail.com> has been added to the cc: list.

@gitgitgadget-git
Copy link

On the Git mailing list, Junio C Hamano wrote (reply to this):

Harald Nordgren <haraldnordgren@gmail.com> writes:

>> You seem to be using the "what would a new user get when they clone
>> the remote (by virtue of their HEAD pointing at that branch)", but
>> I am not sure if that is a good way to determine the other remote
>> thing to compare with.
>>
>> Even if one remote branch you pull from (but not push to) has a name
>> that is not one of those usual ones like 'main', 'master', 'trunk',
>> 'default'
>
> Agreed, this should be as agnostic as possible. "Default" might be
> me using GitHub terminology. However, it seems that 'git
> symbolic-ref refs/remotes/upstream/HEAD' always produces the
> desires result, so for the sake of discussion we can call it
> "upstream/HEAD" instead of "default".

I didn't exactly question the terminology, but was wondering more
about the wisdom of using remotes/*/HEAD.

If a project uses the same remote repository to maintain its
maintenance and development tracks, their HEAD might point at the
'main' (used for development), but some of your branches you used to
work on fixes that can later be merged to the maintenance track, it
is likely that you'll fork from their 'maint', and while you keep
polishing your fixes, you may push your 'fix' branch to their 'fix'
branch.  Comparing your 'fix' with their 'fix' is what we already
do, and it gives two thirds of what you need, but the missing
comparison is with their 'maint', not with their HEAD that points at
their 'main'.

Thanks.

@gitgitgadget-git
Copy link

On the Git mailing list, Harald Nordgren wrote (reply to this):

> If a project uses the same remote repository to maintain its
> maintenance and development tracks, their HEAD might point at the
> 'main' (used for development), but some of your branches you used to
> work on fixes that can later be merged to the maintenance track, it
> is likely that you'll fork from their 'maint', and while you keep
> polishing your fixes, you may push your 'fix' branch to their 'fix'
> branch.  Comparing your 'fix' with their 'fix' is what we already
> do, and it gives two thirds of what you need, but the missing
> comparison is with their 'maint', not with their HEAD that points at
> their 'main'.

That's a fair point. I am used to trunk-based development, but I recognize
that there are other paradigms out there.

One possibly solution could be to make this adjustable in repo-wide config
(maybe 'repo.settings.defaultBranch=maint') and when unset it compares to
'refs/remotes/upstream/HEAD'?

@gitgitgadget-git
Copy link

On the Git mailing list, Harald Nordgren wrote (reply to this):

> That still leaves `git status` with the problem you (Harald) have observed,
> so perhaps the path forward is to have `git status` check things like
> branch.<name>.pushRemote to see if they exist and differ from
> branch.<name>.remote.

I played around with pushRemote and I'm not getting it to do something
that's useful for me here. Granted I'm not an expert there so happy to take
some hints on how to use it more specifically.

I checked some of my other projects and it seems pushRemote doesn't get
set, even when working with one fork and one upstream repo:

	$ git remote -v
	HaraldNordgren	git@github.com:HaraldNordgren/brew.git (fetch)
	HaraldNordgren	git@github.com:HaraldNordgren/brew.git (push)
	origin	git@github.com:Homebrew/brew.git (fetch)
	origin	git@github.com:Homebrew/brew.git (push)

Harald

@gitgitgadget-git
Copy link

On the Git mailing list, Chris Torek wrote (reply to this):

On Tue, Dec 23, 2025 at 6:18 AM Harald Nordgren
<haraldnordgren@gmail.com> wrote:
> I played around with pushRemote and I'm not getting it to do something
> that's useful for me here. Granted I'm not an expert there so happy to take
> some hints on how to use it more specifically.

I've never actually used it myself. You do have to set it manually
(with `git config` for instance).

I've never been completely convinced that Git's triangular workflow
setups are The Right Way, or even a Good Way. I've always just
done everything manually. But they're documented, so they ought
to work.

Chris

@gitgitgadget-git
Copy link

There are issues in commit bafcde1:
try to remove unused

  • Commit checks stopped - the message is too short
  • Commit not signed off

@HaraldNordgren
Copy link
Contributor Author

/submit

@gitgitgadget-git
Copy link

Submitted as pull.2138.v2.git.git.1766530448.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-git-2138/HaraldNordgren/ahead_of_main_status-v2

To fetch this version to local tag pr-git-2138/HaraldNordgren/ahead_of_main_status-v2:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-git-2138/HaraldNordgren/ahead_of_main_status-v2

@gitgitgadget-git
Copy link

On the Git mailing list, Yee Cheng Chin wrote (reply to this):

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

@gitgitgadget-git
Copy link

User Yee Cheng Chin <ychin.macvim@gmail.com> has been added to the cc: list.

@gitgitgadget-git
Copy link

On the Git mailing list, Harald Nordgren wrote (reply to this):

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

@gitgitgadget-git
Copy link

On the Git mailing list, Harald Nordgren wrote (reply to this):

By the way, I'm using my own version of git while working on this, and I'm loving it 😅

	$ /Users/Harald/git-repos/github.com/git/git/git status
	On branch ahead_of_main_status
	Your branch and 'origin/ahead_of_main_status' have diverged,
	and have 1 and 1 different commits each, respectively.
	  (use "git pull" if you want to integrate the remote branch with yours)

	Ahead of 'upstream/master' by 2 commits.

	nothing to commit, working tree clean

@HaraldNordgren
Copy link
Contributor Author

/submit

@gitgitgadget-git
Copy link

Submitted as pull.2138.v28.git.git.1769112471.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-git-2138/HaraldNordgren/ahead_of_main_status-v28

To fetch this version to local tag pr-git-2138/HaraldNordgren/ahead_of_main_status-v28:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-git-2138/HaraldNordgren/ahead_of_main_status-v28

@@ -17,6 +17,26 @@ status.aheadBehind::
`--no-ahead-behind` by default in linkgit:git-status[1] for

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:

> diff --git a/remote.c b/remote.c
> index fd592ec659..e256cc9b81 100644
> --- a/remote.c
> +++ b/remote.c
> @@ -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;
> +	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.

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

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"?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

> 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

Harald Nordgren <haraldnordgren@gmail.com> writes:

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

Junio C Hamano <gitster@pobox.com> writes:

> Harald Nordgren <haraldnordgren@gmail.com> writes:
>
>>> 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.
> ...
> 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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jeff King wrote on the Git mailing list (how to reply to this email):

On Thu, Jan 22, 2026 at 12:37:16PM -0800, Junio C Hamano wrote:

> > +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}"))
> > +		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):

diff --git a/object-name.c b/object-name.c
index 8b862c124e..925a487d84 100644
--- a/object-name.c
+++ b/object-name.c
@@ -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) {
diff --git a/object-name.h b/object-name.h
index cda4934cd5..962f99b0f6 100644
--- a/object-name.h
+++ b/object-name.h
@@ -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.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jeff King wrote on the Git mailing list (how to reply to this email):

On Thu, Jan 22, 2026 at 05:44:27PM -0500, Jeff King wrote:

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

diff --git a/refs.c b/refs.c
index 627b7f8698..2316d0b8a9 100644
--- a/refs.c
+++ b/refs.c
@@ -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:

diff --git a/object-name.c b/object-name.c
index 8b862c124e..0663946f81 100644
--- a/object-name.c
+++ b/object-name.c
@@ -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;
 }
 
diff --git a/object-name.h b/object-name.h
index cda4934cd5..20393cb213 100644
--- a/object-name.h
+++ b/object-name.h
@@ -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,
diff --git a/refs.c b/refs.c
index 2316d0b8a9..58f945e213 100644
--- a/refs.c
+++ b/refs.c
@@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

Jeff King <peff@peff.net> writes:

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

Harald Nordgren <haraldnordgren@gmail.com> writes:

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Harald Nordgren wrote on the Git mailing list (how to reply to this email):

Hi Jeff!

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


Harald

@gitgitgadget-git
Copy link

There was a status update in the "Cooking" section about the branch hn/status-compare-with-push on the Git mailing list:

"git status" learned to show comparison between the current branch
and its push destination as well as its upstream, when the two are
different (i.e., triangular workflow).

Under further discussion?
cf. <20260122220154.GA2107958@coredump.intra.peff.net>
source: <pull.2138.v28.git.git.1769112471.gitgitgadget@gmail.com>

@HaraldNordgren HaraldNordgren force-pushed the ahead_of_main_status branch 2 times, most recently from f19d7ff to 73c7fdc Compare January 25, 2026 12:49
Refactor format_branch_comparison function in preparation for showing
comparison with push remote tracking branch.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
…sons

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}

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
@gitgitgadget-git
Copy link

This patch series was integrated into seen via 9952e40.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via 27bdc83.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via 35a9571.

@gitgitgadget-git
Copy link

There was a status update in the "Cooking" section about the branch hn/status-compare-with-push on the Git mailing list:

"git status" learned to show comparison between the current branch
and its push destination as well as its upstream, when the two are
different (i.e., triangular workflow).

Under further discussion?
cf. <20260122220154.GA2107958@coredump.intra.peff.net>
source: <pull.2138.v28.git.git.1769112471.gitgitgadget@gmail.com>

@gitgitgadget-git
Copy link

This patch series was integrated into seen via aabdf39.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via e079225.

@gitgitgadget-git
Copy link

There was a status update in the "Cooking" section about the branch hn/status-compare-with-push on the Git mailing list:

"git status" learned to show comparison between the current branch
and its push destination as well as its upstream, when the two are
different (i.e., triangular workflow).

Under further discussion?
cf. <20260122220154.GA2107958@coredump.intra.peff.net>
source: <pull.2138.v28.git.git.1769112471.gitgitgadget@gmail.com>

@gitgitgadget-git
Copy link

This patch series was integrated into seen via e3198ca.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via b6dd44f.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via 70d7e73.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via a428700.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via c87aa89.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via 971d1fb.

@gitgitgadget-git
Copy link

There was a status update in the "Cooking" section about the branch hn/status-compare-with-push on the Git mailing list:

"git status" learned to show comparison between the current branch
and its push destination as well as its upstream, when the two are
different (i.e., triangular workflow).

Under further discussion?
cf. <20260122220154.GA2107958@coredump.intra.peff.net>
source: <pull.2138.v28.git.git.1769112471.gitgitgadget@gmail.com>

@gitgitgadget-git
Copy link

This patch series was integrated into seen via dbcfb68.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via a4634be.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via dbcfb68.

@gitgitgadget-git
Copy link

This patch series was integrated into seen via 965fdb6.

@gitgitgadget-git
Copy link

There was a status update in the "Cooking" section about the branch hn/status-compare-with-push on the Git mailing list:

"git status" learned to show comparison between the current branch
and its push destination as well as its upstream, when the two are
different (i.e., triangular workflow).

What's the status of this topic?
cf. <20260122220154.GA2107958@coredump.intra.peff.net>
source: <pull.2138.v28.git.git.1769112471.gitgitgadget@gmail.com>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants