71

On a github project you can go to a /branches page and se pretty graphs like this one that for each branch show how far behind and how far ahead each branch is with respect to master.

git branch ahead behind

Is there a command line tool that does something similar? Something that works with remotes as well? For example,

git branch -v -v

is close to what I am looking for, but only works for local branches.

kortina
  • 5,821
  • 4
  • 24
  • 28
  • 4
    You can add '-r' and '-a' for remotes only and all branches respectively to the `git branch`...so `git branch -v -v -a` – seth Oct 14 '11 at 22:03
  • @seth - he is asking against master and for remote branches...I don't like what he wants, but what you say wouldn't do it... – manojlds Oct 14 '11 at 22:44
  • Note: Git1.9/2.0 will provide another way to display that "ahead/behind" status. See [my answer below](http://stackoverflow.com/a/20499690/6309). – VonC Dec 10 '13 at 16:22
  • So you basically want all branches, including the remote branch, to be compared with a single reference branch, such as `master`? –  May 01 '14 at 23:32
  • 1
    With Git 2.5+ (Q2 2015), the actual command will be `git for-each-ref --format="%(push:track)" refs/heads`. See [my revised answer below](http://stackoverflow.com/a/20499690/6309). – VonC Jun 08 '15 at 22:51

3 Answers3

81

I've been curious about this as well, so i just whipped up a git branch-status script that gives this information using git for-each-ref

#!/bin/bash
# by http://github.com/jehiah
# this prints out some branch status (similar to the '... ahead' info you get from git status)
 
# example:
# $ git branch-status
# dns_check (ahead 1) | (behind 112) origin/master
# master (ahead 2) | (behind 0) origin/master
 
git for-each-ref --format="%(refname:short) %(upstream:short)" refs/heads | \
while read local remote
do
    [ -z "$remote" ] && continue
    git rev-list --left-right "${local}...${remote}" -- 2>/dev/null >/tmp/git_upstream_status_delta || continue
    LEFT_AHEAD=$(grep -c '^<' /tmp/git_upstream_status_delta)
    RIGHT_AHEAD=$(grep -c '^>' /tmp/git_upstream_status_delta)
    echo "$local (ahead $LEFT_AHEAD) | (behind $RIGHT_AHEAD) $remote"
done

Usage:

$ git branch-status
dns_check (ahead 1) | (behind 112) origin/master
master (ahead 2) | (behind 0) origin/master
WhyNotHugo
  • 9,423
  • 6
  • 62
  • 70
Jehiah
  • 2,739
  • 22
  • 18
  • 3
    Awesome, thanks Jehiah! This script is great. Wasn't exactly what I was looking for, so I modified it: [git-branches-vs-origin-master](https://gist.github.com/1288703) – kortina Oct 15 '11 at 00:06
  • Is there a way to use this script on Windows? Looks great. :-) – Simon East Nov 12 '12 at 21:46
  • 1
    @kortina - Your gist link is now broken, any chance that you could update it? Thanks, – Mark Booth Feb 11 '13 at 12:31
  • 2
    Sure, I have 2 version now: https://github.com/kortina/bakpak/blob/master/bin/git-branches-vs-origin-master and https://github.com/kortina/bakpak/blob/master/bin/git-current-branch-is-behind-origin-master.sh – kortina Feb 18 '13 at 23:22
  • 3
    Thanks for `git branch-status` *Jehiah*, I've created [my own](https://gist.github.com/Mark-Booth/5058384#file-git-branch-status) (based on [lth2h's](https://gist.github.com/lth2h/4177524#file-git-branch-status)) which only shows the current branch and only generates output if a branch is ahead or behind. It also adds options to show all branches, show output even if the branch isn't ahead or behind and show help. This is really useful when your application is spread over multiple `git` repos and you quickly want to see the status of all which need attention. – Mark Booth Feb 28 '13 at 17:47
  • VonC's answer is good for newer Git versions. I'll note here that in older Git versions, you can simplify this using `git rev-list --count --left-right`: this spits out the two counts directly, so no need to use `grep -c`. – torek May 15 '18 at 16:46
  • Where should script be located in the same folder git repo? @Jehiah – alper Jun 21 '20 at 12:54
  • A word of warning for anybody who, like me, was looking to use this as a way to assess how stale local replicas are and whether they have any un-pushed changes: Ignoring branches with no remote (`[ -z "$remote" ] && continue`) is probably a **dangerous opposite** of what you want! – dingus Dec 08 '21 at 18:17
60

Update 2015

My initial answer below is not ideal as the upstream branch is not necessarily the branch you are pushing t which could be different from the branch you are pulling from.

With Git 2.5+, the correct command is:

git for-each-ref --format="%(refname:short) %(upstream:track) %(upstream:remotename)" refs/heads

See more at "Viewing Unpushed Git Commits".

(As pointed out by void.pointer in the comments, upstream:track is more precise than push:track, depending on the default push policy)

(The (upstream:remotename) part comes from HankCA's comment, to see if a branch had been pushed (or generally had an upstream equivalent).)


Git 2.13 (Q2 2017) uses a more generic ref-filter API with a more complete git for-each-ref push:

See commit 3d9e4ce, commit 949af06, commit 56b4360, commit 6eac70f, commit 1a34728, commit 1a0ca5e, commit 3a42980, commit 17938f1, commit 3ba308c, commit a798410, commit b180e6f, commit 01f9582, commit 7743fcc, commit ffd921d, commit 99c6a71, commit d4919bb, commit 42d0eb0, commit 4f3e3b3, commit c58fc85 (10 Jan 2017) by Karthik Nayak (KarthikNayak).
(Merged by Junio C Hamano -- gitster -- in commit 93e8cd8, 27 Feb 2017)

push:

The name of a local ref which represents the @{push} location for the displayed ref.
Respects :short, :lstrip, :rstrip, :track, and :trackshort options as upstream does.
Produces an empty string if no @{push} ref is configured.

If lstrip=<N> (rstrip=<N>) is appended, strips <N> slash-separated path components from the front (back) of the refname
(e.g. %(refname:lstrip=2) turns refs/tags/foo into foo and %(refname:rstrip=2) turns refs/tags/foo into refs).

If <N> is a negative number, strip as many path components as necessary from the specified end to leave -<N> path components
(e.g. %(refname:lstrip=-2) turns refs/tags/foo into tags/foo and %(refname:rstrip=-1) turns refs/tags/foo into refs)


Original answer (2014)

Another way will be available with Git 1.9/2/0 (Q1 2014).
See commit b28061c from Ramkumar Ramachandra (artagnon):

for-each-ref: introduce %(upstream:track[short])

Introduce:

  • %(upstream:track) to display "[ahead M, behind N]" and
  • %(upstream:trackshort) to display "=", ">", "<", or "<>" appropriately (inspired by contrib/completion/git-prompt.sh).

Now you can use the following format in for-each-ref:

%(refname:short) %(upstream:trackshort)

to display refs with terse tracking information.

Note that :track and :trackshort only work with "upstream", and error out when used with anything else.


Before Git 2.30 (Q1 2021), a commit and tag object may have CR at the end of each and every line (you can create such an object with hash-object or using --cleanup=verbatim to decline the default clean-up action), but it would make it impossible to have a blank line to separate the title from the body of the message.

Be lenient and accept a line with lone CR on it as a blank line, too.

See commit e2f8958, commit 9f75ce3 (29 Oct 2020) by Philippe Blain (phil-blain).
(Merged by Junio C Hamano -- gitster -- in commit 4c7eb63, 09 Nov 2020)

ref-filter: handle CRLF at end-of-line more gracefully

Helped-by: Junio C Hamano
Helped-by: Eric Sunshine
Signed-off-by: Philippe Blain

The ref-filter code does not correctly handle commit or tag messages that use CRLF as the line terminator.

Such messages can be created with the --cleanup=verbatim option of git commit(man) and git tag(man), or by using git commit-tree(man) directly.

The function find_subpos in ref-filter.c looks for two consecutive LFs to find the end of the subject line, a sequence which is absent in messages using CRLF.
This results in the whole message being parsed as the subject line (%(contents:subject)), and the body of the message (%(contents:body)) being empty.

Moreover, in copy_subject, which wants to return the subject as a single line, '\n' is replaced by space, but '\r' is untouched.

This impacts the output of git branch(man), git tag(man) and git for-each-ref(man) `.

This behaviour is a regression for git branch --verbose(man), which bisects down to 949af0684c ("branch: use ref-filter printing APIs", 2017-01-10, Git v2.13.0-rc0 -- merge listed in batch #1).

Adjust the ref-filter code to be more lenient by hardening the logic in copy_subject and find_subpos to correctly parse messages containing CRLF.

Note: this is improved/fixed/amended in Git 2.39.
No more ^M: -----BEGIN PGP SIGNATURE-----^M.
No more extra line

  -----BEGIN PGP SIGNATURE-----

  ...some stuff...
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Is there a way to limit this to only show branches where `%(push:track)` is not empty? – detly Aug 30 '16 at 07:16
  • I found this question as a suggested duplicate of [one I asked](http://stackoverflow.com/questions/39220870/in-git-list-names-of-branches-with-unpushed-commits), so I've revised my own question. – detly Aug 30 '16 at 23:16
  • Note that `push:track` is not the correct solution if you use a `push.default` of `current` (for decentralized workflow) and expect status to reflect remote branches on `origin`. In this case `upstream:track` will be the correct solution, assuming your tracking branch is set to the corresponding branch on remote `origin`. – void.pointer May 15 '17 at 17:57
  • I use it to quickly check which local branches I can safely remove, but this script doesn't indicate at all, that a branch is only local and has not been pushed yet (even if it has ahead commits). Is it possible to change this script so this info is also shown? – Episodex Aug 13 '20 at 06:19
  • @Episodex Do you mean the 2.5+ `git for-each-ref` command? It would display upstream branches only for local one. – VonC Aug 13 '20 at 06:22
  • @VonC yes, but there is no difference between local branch that is equal to remote, or local that was never pushed. I found a small modification though, that gives this info: `git for-each-ref --format="%(refname:short) %(upstream:trackshort)" refs/heads`. In this version `=` is local equal to remote, and ` ` (no sign) is local never pushed (but the info of number of ahead/behind commits is not shown). – Episodex Aug 13 '20 at 06:40
  • @Episodex OK: could you ask a separate question which would illustrate the issue? This is an interesting problem which deserve more than being buried in comments here. – VonC Aug 13 '20 at 06:43
  • I also added `%(upstream:remotename)` so i could see if a branch had been pushed (or generally had an upstream equivalent). `git for-each-ref --format="%(refname:short) %(upstream:track) %(upstream:remotename)" refs/heads` – HankCa Aug 20 '20 at 02:04
  • @HankCa Thank you. I have included your comment in the answer for more visibility. – VonC Aug 20 '20 at 05:54
1

Another option, with Git 2.41+ (Q2 2023), for all branches/tags:

git for-each-ref --format="%(refname)" "refs/heads/*" "refs/tags/*" >allrefs &&
sort -r allrefs | head -n 50 >refs
git for-each-ref --format="%(ahead-behind:HEAD)" --stdin <refs

With Git 2.41 (Q2 2023), "git for-each-ref"(man) learns '%(ahead-behind:<base>)' that computes the distances from a single reference point in the history with bunch of commits in bulk.

It shows two new features.


First: git for-each-ref --stdin <refs

See commit cbfe360, commit 49abcd2, commit fd67d14, commit 2ee11f7, commit 80c928d, commit 368d19b, commit b2c51b7, commit b73dec5 (20 Mar 2023) by Derrick Stolee (derrickstolee).
See commit c08645b (20 Mar 2023) by Taylor Blau (ttaylorr).
(Merged by Junio C Hamano -- gitster -- in commit 7727da9, 06 Apr 2023)

for-each-ref: add --stdin option

Helped-by: Phillip Wood
Signed-off-by: Derrick Stolee

When a user wishes to input a large list of patterns to 'git for-each-ref'(man) (likely a long list of exact refs) there are frequently system limits on the number of command-line arguments.

Add a new --stdin option to instead read the patterns from standard input.
Add tests that check that any unrecognized arguments are considered an error when --stdin is provided.
Also, an empty pattern list is interpreted as the complete ref set.

When reading from stdin, we populate the filter.name_patterns array dynamically as opposed to pointing to the 'argv' array directly.
This is simple when using a strvec, as it is NULL-terminated in the same way.
We then free the memory directly from the strvec.

git for-each-ref now includes in its man page:

[(--sort=)...] [--format=] [ --stdin | ... ]

git for-each-ref now includes in its man page:

--stdin

If --stdin is supplied, then the list of patterns is read from standard input instead of from the argument list.


Second: git for-each-ref --format="%(ahead-behind:HEAD)"

for-each-ref: add ahead-behind format atom

Signed-off-by: Derrick Stolee

The previous change implemented the ahead_behind() method, including an algorithm to compute the ahead/behind values for a number of commit tips relative to a number of commit bases.
Now, integrate that algorithm as part of 'git for-each-ref'(man) hidden behind a new format atom, ahead-behind.
This naturally extends to 'git branch'(man) and 'git tag'(man) builtins, as well.

This format allows specifying multiple bases, if so desired, and all matching references are compared against all of those bases.
For this reason, failing to read a reference provided from these atoms results in an error.

In order to translate the ahead_behind() method information to the format output code in ref-filter.c, we must populate arrays of ahead_behind_count structs.
In struct ref_array, we store the full array that will be passed to ahead_behind().
In struct ref_array_item, we store an array of pointers that point to the relvant items within the full array.
In this way, we can pull all relevant ahead/behind values directly when formatting output for a specific item.
It also ensures the lifetime of the ahead_behind_count structs matches the time that the array is being used.

Also add performance tests in a new p1300-graph-walks.sh script.
This will be useful for more uses in the future, but for now compare the ahead-behind counting algorithm in 'git for-each-ref' to the naive implementation by running 'git rev-list --count'(man) processes for each input.

git for-each-ref now includes in its man page:

ahead-behind:<committish>

Two integers, separated by a space, demonstrating the number of commits ahead and behind, respectively, when comparing the output ref to the <committish> specified in the format.


It also illustrates/explains how ahead/behinf is computed

commit-reach: implement ahead_behind() logic

Co-authored-by: Taylor Blau
Signed-off-by: Taylor Blau
Signed-off-by: Derrick Stolee

Fully implement the commit-counting logic required to determine ahead/behind counts for a batch of commit pairs.
This is a new library method within commit-reach.h.

The interface for ahead_behind() uses two arrays.

  • The first array of commits contains the list of all starting points for the walk.
    This includes all tip commits and base commits.
  • The second array specifies base/tip pairs by pointing to commits within the first array, by index.
    The second array also stores the resulting ahead/behind counts for each of these pairs.

This implementation of ahead_behind() allows multiple bases, if desired.
Even with multiple bases, there is only one commit walk used for counting the ahead/behind values, saving time when the base/tip ranges overlap significantly.

This interface for ahead_behind() also makes it very easy to call ensure_generations_valid() on the entire array of bases and tips.
This call is necessary because it is critical that the walk that counts ahead/behind values never walks a commit more than once.
Without generation numbers on every commit, there is a possibility that a commit date skew could cause the walk to revisit a commit and then double-count it.
For this reason, it is strongly recommended that git ahead-behind is only run in a repository with a commit-graph file that covers most of the reachable commits, storing precomputed generation numbers.
If no commit-graph exists, this walk will be much slower as it must walk all reachable commits in ensure_generations_valid() before performing the counting logic.

It is possible to detect if generation numbers are available at run time and redirect the implementation to another algorithm that does not require this property.
However, that implementation requires a commit walk per base/tip pair and can be slower due to the commit date heuristics required.
Such an implementation could be considered in the future if there is a reason to include it, but most Git hosts should already be generating a commit-graph file as part of repository maintenance.
Most Git clients should also be generating commit-graph files as part of background maintenance or automatic GCs.

Now, let's discuss the ahead/behind counting algorithm.

The first array of commits are considered the starting commits.
The index within that array will play a critical role.

We create a new commit slab that maps commits to a bitmap.
For a given commit (anywhere in the history), its bitmap stores information relative to which of the input commits can reach that commit.
The ith bit will be on if the ith commit from the starting list can reach that commit.
It is important to notice that these bitmaps are not the typical "reachability bitmaps" that are stored in .bitmap files.
Instead of signalling which objects are reachable from the current commit, they instead signal "which starting commits can reach me?" It is also important to know that the bitmap is not necessarily "complete" until we walk that commit.
We will perform a commit walk by generation number in such a way that we can guarantee the bitmap is correct when we visit that commit.

At the beginning of the ahead_behind() method, we initialize the bitmaps for each of the starting commits.
By enabling the ith bit for the ith starting commit, we signal "the ith commit can reach itself."

We walk commits by popping the commit with maximum generation number out of the queue, guaranteeing that we will never walk a child of that commit in any future steps.

As we walk, we load the bitmap for the current commit and perform two main steps.

The second step examines each parent of the current commit and adds the current commit's bitmap bits to each parent's bitmap.
(We create a new bitmap for the parent if this is our first time seeing that parent.) After adding the bits to the parent's bitmap, the parent is added to the walk queue.
Due to this passing of bits to parents, the current commit has a guarantee that the ith bit is enabled on its bitmap if and only if the ith commit can reach the current commit.

The first step of the walk is to examine the bitmask on the current commit and decide which ranges the commit is in or not.
Due to the "bit pushing" in the second step, we have a guarantee that the ith bit of the current commit's bitmap is on if and only if the ith starting commit can reach it.
For each ahead_behind_count struct, check the base_index and tip_index to see if those bits are enabled on the current bitmap.
If exactly one bit is enabled, then increment the corresponding 'ahead' or 'behind' count.
This increment is the reason we absolutely need to walk commits at most once.

The only subtle thing to do with this walk is to check to see if a parent has all bits on in its bitmap, in which case it becomes "stale" and is marked with the STALE bit.
This allows queue_has_nonstale() to be the terminating condition of the walk, which greatly reduces the number of commits walked if all of the commits are nearby in history.
It avoids walking a large number of common commits when there is a deep history.
We also use the helper method insert_no_dup() to add commits to the priority queue without adding them multiple times.
This uses the PARENT2 flag.
Thus, we must clear both the STALE and PARENT2 bits of all commits, in case ahead_behind() is called multiple times in the same process.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • See also [commit d3af1c1](https://github.com/git/git/commit/d3af1c193d4d62668a9d7ea98e2ef3771ac4e65a) for a commit-graph bug fix. – VonC Apr 26 '23 at 20:34