192

I need to delete old and unmaintained branches from our remote repository. I'm trying to find a way with which to list the remote branches by their last modified date, and I can't.

Is there an easy way to list remote branches this way?

Roni Yaniv
  • 57,651
  • 5
  • 19
  • 16
  • 2
    Possible duplicate of [How can I get a list of git branches, ordered by most recent commit?](http://stackoverflow.com/questions/5188320/how-can-i-get-a-list-of-git-branches-ordered-by-most-recent-commit) – Kristján Dec 01 '15 at 18:19
  • 5
    The answers to: http://stackoverflow.com/questions/5188320/how-can-i-get-a-list-of-git-branches-ordered-by-most-recent-commit are all better than the answers here – Software Engineer Mar 16 '16 at 17:15

13 Answers13

207

commandlinefu has 2 interesting propositions:

for k in $(git branch | perl -pe s/^..//); do echo -e $(git show --pretty=format:"%Cgreen%ci %Cblue%cr%Creset" $k -- | head -n 1)\\t$k; done | sort -r

or:

for k in $(git branch | sed s/^..//); do echo -e $(git log --color=always -1 --pretty=format:"%Cgreen%ci %Cblue%cr%Creset" $k --)\\t"$k";done | sort

That is for local branches, in a Unix syntax. Using git branch -r, you can similarly show remote branches:

for k in $(git branch -r | perl -pe 's/^..(.*?)( ->.*)?$/\1/'); do echo -e $(git show --pretty=format:"%Cgreen%ci %Cblue%cr%Creset" $k -- | head -n 1)\\t$k; done | sort -r

Michael Forrest mentions in the comments that zsh requires escapes for the sed expression:

for k in git branch | perl -pe s\/\^\.\.\/\/; do echo -e git show --pretty=format:"%Cgreen%ci %Cblue%cr%Creset" $k -- | head -n 1\\t$k; done | sort -r 

kontinuity adds in the comments:

If you want to add it your zshrc the following escape is needed.

alias gbage='for k in $(git branch -r | perl -pe '\''s/^..(.*?)( ->.*)?$/\1/'\''); do echo -e $(git show --pretty=format:"%Cgreen%ci %Cblue%cr%Creset" $k -- | head -n 1)\\t$k; done | sort -r'

In multiple lines:

alias gbage='for k in $(git branch -r | \
  perl -pe '\''s/^..(.*?)( ->.*)?$/\1/'\''); \
  do echo -e $(git show --pretty=format:"%Cgreen%ci %Cblue%cr%Creset" $k -- | \
     head -n 1)\\t$k; done | sort -r'

Note: n8tr's answer, based on git for-each-ref refs/heads is cleaner. And faster.
See also "Name only option for git branch --list?"

More generally, tripleee reminds us in the comments:

  • Prefer modern $(command substitution) syntax over obsolescent backtick syntax.

(I illustrated that point in 2014 with "What is the difference between $(command) and `command` in shell programming?")

  • Don't read lines with for.
  • Probably switch to git for-each-ref refs/remote to get remote branch names in machine-readable format
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 1
    @hansen j: interesting, isn't it? It launched a few months after the public release of Stack Overflow (http://codeinthehole.com/archives/16-Current-pet-project-Command-Line-Fu.html), and was somewhat inspired by SO. See also http://www.commandlinefu.com/commands/tagged/67/git for more git commandlinefu ;) – VonC Mar 26 '10 at 07:29
  • This answer kicks http://stackoverflow.com/questions/5188320/how-can-i-get-a-list-of-git-branches-ordered-by-most-recent-commit 's ass. :) – Spundun Jan 25 '13 at 03:34
  • @SebastianG not sure: that would be a good question of its own. – VonC Mar 07 '13 at 13:14
  • +1 Pretty awesome how you can add `/json` to the end of any http://commandlinefu.com URL and you will get all the commands as JSON. – Noah Sussman Apr 06 '13 at 00:34
  • Any idea why I get `zsh: no matches found: s/^..//` when running these? – Michael Forrest Sep 19 '13 at 10:07
  • Oh, apparently zsh parses the regex. I got it to work by escaping everything... for k in `git branch | perl -pe s\/\^\.\.\/\/`; do echo -e `git show --pretty=format:"%Cgreen%ci %Cblue%cr%Creset" $k -- | head -n 1`\\t$k; done | sort -r – Michael Forrest Sep 19 '13 at 10:11
  • @MichaelForrest Interesting. I have included your comment in the answer for more visibility. – VonC Sep 19 '13 at 11:28
  • This is great, and almost what I want. However, it chokes on "(detached from xyz)", which is the kind of situation in which I'm most likely to want this command; and I'd love for it to color-code (or otherwise indicate) current vs. local vs. remote branches like "git branch -av" does. Hmmm.... [starts tweaking] – benkc Nov 05 '14 at 20:20
  • Fixing the "(detached from xyz)" was easy enough by tweaking the first sed command to `sed -e s/^..// -e 's/(detached from \(.*\))/\1/'`. Then I got distracted by trying to include the commit subject similar to git branch -v, which is easy but columnating it sensibly is going to require I brush up on my awk I think. Columns that contain whitespace are kind of tricky no? – benkc Nov 05 '14 at 21:01
  • Second seems to be faster than the first (from the ones which show all branches - third one doesn't show branches with dot in their name). – pevik Jan 19 '15 at 07:47
  • If you want to add it your zshrc the following escape is needed. ```alias gbage='for k in `git branch -r | perl -pe '\''s/^..(.*?)( ->.*)?$/\1/'\''`; do echo -e `git show --pretty=format:"%Cgreen%ci %Cblue%cr%Creset" $k -- | head -n 1`\\t$k; done | sort -r'``` – Arif Amirani Aug 23 '16 at 04:56
  • @kontinuity Nice, thank you. I have included your comment in the answer for more visibility. – VonC Aug 23 '16 at 06:35
  • Interestingly but unsurprisingly and somewhat depressingly, commandlinefu seems to proliferate a boatload of shell programming antipatterns. – tripleee Feb 13 '20 at 12:06
  • @tripleee I would welcome an edit on this 2010 answer of mine. I was a young fool back then (I am still a fool now, mind you, just older) ಠ﹏ಠ – VonC Feb 13 '20 at 12:15
  • I can't comment on the zsh parts but I'll be happy to give the general shell script an overhaul, though then the references to commandlinefu etc will be somewhat invalidated. I was thinking of posting a separate answer with a refactoring; maybe I should simply do that and mark it as community wiki, and leave it to you to maybe grab parts or all of it? – tripleee Feb 13 '20 at 12:22
  • @tripleee Sure, I can reference a link to your own answer in my answer. No need to mark it community wiki. – VonC Feb 13 '20 at 12:23
  • I basically ended up reinventing n8tr's answer, so I'll just leave some hints. [Prefer modern `$(command substitution)` syntax](/questions/9449778/what-is-the-benefit-of-using-instead-of-backticks-in-shell-scripts) over obsolescent backtick syntax. [Don't read lines with `for`.](https://mywiki.wooledge.org/DontReadLinesWithFor) Probably switch to [`git for-each-ref refs/remote`](/questions/36026185/name-only-option-for-git-branch-list) to get remote branch names in machine-readable format. – tripleee Feb 13 '20 at 13:30
  • 1
    @tripleee Thank you. I have edited the answer and included your comments for more visibility. – VonC Feb 13 '20 at 13:43
  • I needed --color=always in the log command to get it showing in color for me. Otherwise it didn't send the color codes over through the following pipeline. – Jeffrey Vest Mar 14 '22 at 15:02
  • @JeffreyVest Thank you for the feedback. I have edited the answer accordingly. – VonC Mar 14 '22 at 15:08
180

Here is what I use:

git for-each-ref --sort='-committerdate:iso8601' --format=' %(committerdate:iso8601)%09%(refname)' refs/heads

This is the output:

2014-01-22 11:43:18 +0100       refs/heads/master
2014-01-22 11:43:18 +0100       refs/heads/a
2014-01-17 12:34:01 +0100       refs/heads/b
2014-01-14 15:58:33 +0100       refs/heads/maint
2013-12-11 14:20:06 +0100       refs/heads/d/e
2013-12-09 12:48:04 +0100       refs/heads/f

For remote branches, just use "refs/remotes" instead of "refs/heads":

git for-each-ref --sort='-committerdate:iso8601' --format=' %(committerdate:iso8601)%09%(refname)' refs/remotes

Building on n8tr's answer, if you are also interested in the last author on the branch, and if you have the "column" tool available, you can use:

git for-each-ref --sort='-committerdate:iso8601' --format='%(committerdate:relative)|%(refname:short)|%(committername)' refs/remotes/ | column -s '|' -t

Which will give you:

21 minutes ago  refs/remotes/a        John Doe
6 hours ago     refs/remotes/b        Jane Doe
6 days ago      refs/remotes/master   John Doe

You may want to call git fetch --prune before to have the latest information, or add %(color:<color>) statements in the format to display some fields with a specific color.

ocroquette
  • 3,049
  • 1
  • 23
  • 26
  • 6
    Nice use of for-each-ref and the format options. +1. Sounds easier than the commands I reference in my own answer. – VonC Jan 23 '14 at 11:39
  • 3
    tweaking a little:------- git for-each-ref --sort='-authordate:iso8601' --format=' %(authordate:relative)%09%(refname:short)' refs/heads ------- gives you a relative date and eliminates the refs/heads – n8tr May 29 '14 at 14:57
  • 1
    For those to whom it isn't immediately obvious, I think that this shows info. strictly for local branches. – hBrent Jul 22 '16 at 21:08
  • @hBrent you are right, it didn't answer exactly the question. I have edited my answer accordingly. – ocroquette Jul 24 '16 at 07:05
  • 1
    This sorts and lists the branches by `authordate` (which appears to be when the branch was first created?). If you change `authordate` to `committerdate` then you'll see the dates of the most recent commit in each branch. Like so: `git for-each-ref --sort='-committerdate:iso8601' --format=' %(committerdate:iso8601)%09%(refname)' refs/heads` – Logan Besecker Feb 09 '17 at 19:51
  • For most commits, the author and commit dates are the same, but it's not necessarily the case. The most obvious is when cherry-picking a change: the original author date is kept by default, but the commit date is set to "now". In these cases, the commit date is indeed more useful that then author date to judge the activity on the branch, I have therefore updated the commands to use the commit data. Thanks for bringing this up! – ocroquette Feb 10 '17 at 09:42
  • On some repos, `git for-each-ref refs/remotes` also shows `refs/remotes/origin/HEAD`. Is there a way to exclude that? Of course, I can manually skip over it (e.g. `grep -v '/HEAD$'`), but maybe there is a way to do that with git for-each-ref directly? – Dario Seidl Sep 30 '21 at 13:11
  • Do you know when HEAD is shown, when not? I would have expected every remote to have a HEAD. But I don't know how to hide it in any case. – ocroquette Oct 01 '21 at 06:17
  • Same but with color: `git for-each-ref --sort='-authordate:iso8601' --format=' %(color:green)%(authordate:relative)%(color:reset)%09%(refname:short)' refs/heads` – JohnFlux Oct 20 '22 at 16:31
  • Thanks JohnFlux, I added the hint to the answer. – ocroquette Oct 23 '22 at 10:48
40

Building off of Olivier Croquette, I like using a relative date and shortening the branch name like this:

git for-each-ref --sort='-authordate:iso8601' --format=' %(authordate:relative)%09%(refname:short)' refs/heads

Which gives you output:

21 minutes ago  nathan/a_recent_branch
6 hours ago        master
27 hours ago    nathan/some_other_branch
29 hours ago    branch_c
6 days ago      branch_d

I recommend making a Bash file for adding all your favorite aliases and then sharing the script out to your team. Here's an example to add just this one:

#!/bin/sh

git config --global alias.branches "!echo ' ------------------------------------------------------------' && git for-each-ref --sort='-authordate:iso8601' --format=' %(authordate:relative)%09%(refname:short)' refs/heads && echo ' ------------------------------------------------------------'"

Then you can just do this to get a nicely formatted and sorted local branch list:

git branches
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
n8tr
  • 5,018
  • 2
  • 32
  • 33
21

Just to add to the comment by @VonC, take your preferred solution and add it to your ~/.gitconfig alias list for convenience:

[alias]  
    branchdate = !git for-each-ref --sort='-authordate' --format='%(refname)%09%(authordate)' refs/heads | sed -e 's-refs/heads/--'

Then a simple "git branchdate" prints the list for you...

yngve
  • 1,448
  • 1
  • 13
  • 11
  • 3
    +1 for showing how to use it with .gitconfig! Also fwiw I changed the format string to: `--format='%(authordate)%09%(objectname:short)%09%(refname)'` which gets the short hash of each branch as well. – Noah Sussman Apr 04 '13 at 14:15
  • Nice. I'd add "| tac" to the end to get it sorted in reverse order so the recently-touched branches are quickly visible. – Ben Oct 01 '13 at 18:56
  • 1
    You don't need to `| tac`, just `--sort='authordate'` instead of `-authordate` – Kristján Dec 01 '15 at 18:16
5

Sorted remote branches and the last commit date for each branch.

for branch in `git branch -r | grep -v HEAD`;do echo -e `git show --format="%ci %cr" $branch | head -n 1` \\t$branch; done | sort -r
shweta
  • 8,019
  • 1
  • 40
  • 43
5

Here is what I came up with after also reviewing this.

for REF in $(git for-each-ref --sort=-committerdate --format="%(objectname)" \
    refs/remotes refs/heads)
do
    if [ "$PREV_REF" != "$REF" ]; then
        PREV_REF=$REF
        git log -n1 $REF --date=short \
            --pretty=format:"%C(auto)%ad %h%d %s %C(yellow)[%an]%C(reset)"
    fi
done

The PREV_REF check is to remove duplicates if more than one branch points to the same commit. (As in a local branch that exist in the remote as well.)

NOTE that per the OP request, git branch --merged and git branch --no-merged are useful in identifying which branches can be easily deleted.

[https://git-scm.com/docs/git-branch]

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
go2null
  • 2,080
  • 1
  • 21
  • 17
2

I did a simple alias, not sure if this is what exactly asked, but it is simple

I did this as i wanted to list all the branches not just my local branches, which above commands do only

alias git_brs="git fetch && git branch -av --format='\''%(authordate)%09%(authordate:relative)%09%(refname)'\'"

You can pipe above to grep origin to get only origins

This lists all the branches along with the last date modified, helps me decide which one i should pull for latest version

This results in below type of display

Wed Feb 4 23:21:56 2019 +0230   8 days ago      refs/heads/foo
Tue Feb 3 12:18:04 2019 +0230   10 days ago     refs/heads/master
Mon Feb 9 12:19:33 2019 +0230   4 days ago      refs/heads/bar
Wed Feb 11 16:34:00 2019 +0230  2 days ago      refs/heads/xyz
Tue Feb 3 12:18:04 2019 +0230   10 days ago     refs/remotes/origin/HEAD
Mon Feb 9 12:19:33 2019 +0230   4 days ago      refs/remotes/origin/foo
Tue Feb 3 12:18:04 2019 +0230   10 days ago     refs/remotes/origin/master
Tue Feb 3 12:18:04 2019 +0230   10 days ago     refs/remotes/origin/bar
Tue Feb 3 12:18:04 2019 +0230   10 days ago     refs/remotes/origin/xyz

Try and let me know if it helped, happy gitting

Basav
  • 3,176
  • 1
  • 22
  • 20
1

I made two variants, based on VonC's answer.

My first variant:

for k in `git branch -a | sed -e s/^..// -e 's/(detached from .*)/HEAD/'`; do echo -e `git log -1 --pretty=format:"%Cgreen%ci |%Cblue%cr |%Creset$k |%s" $k --`;done | sort | column -t -s "|"

This handles local and remote branches (-a), handles detached-head state (the longer sed command, though the solution is kind of crude -- it just replaces the detached branch info with the keyword HEAD), adds in the commit subject (%s), and puts things into columns via literal pipe characters in the format string and passing the end result to column -t -s "|". (You could use whatever as the separator, as long as it's something you don't expect in the rest of the output.)

My second variant is quite hacky, but I really wanted something that still has an indicator of "this is the branch you're currently on" like the branch command does.

CURRENT_BRANCH=0
for k in `git branch -a | sed -e 's/\*/CURRENT_BRANCH_MARKER/' -e 's/(detached from .*)/HEAD/'`
do
    if [ "$k" == 'CURRENT_BRANCH_MARKER' ]; then
        # Set flag, skip output
        CURRENT_BRANCH=1
    elif [ $CURRENT_BRANCH == 0 ]; then
        echo -e `git log -1 --pretty=format:"%Cgreen%ci |%Cblue%cr |%Creset$k |%s" $k --`
    else
        echo -e `git log -1 --pretty=format:"%Cgreen%ci |%Cblue%cr |%Creset* %Cgreen$k%Creset |%s" $k --`
        CURRENT_BRANCH=0
    fi
done | sort | column -t -s "|"

This turns the * that marks the current branch into a keyword, and when the loop body sees the keyword it instead sets a flag and outputs nothing. The flag is used to indicate that an alternate formatting should be used for the next line. Like I said, it is totally hacky, but it works! (Mostly. For some reason, my last column is getting outdented on the current branch line.)

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
benkc
  • 3,292
  • 1
  • 28
  • 37
  • Unfortunately the information in VonCs answer is not a great foundation for scripting. See here http://git-blame.blogspot.com/2013/06/checking-current-branch-programatically.html – Andrew C Nov 05 '14 at 22:55
  • Hmm. That shows a way to get the name of the current branch, if it has a name. Is there a [preferred] way to get a machine-friendly list of branches? (And some way to distinguish the current branch, either from that output directly or through somehow asking git "is this the same ref as HEAD?") – benkc Nov 05 '14 at 23:06
  • `git for-each-ref` is the script-friendly way of processing branches. You'd have to run the symbolic-ref once to get current branch. – Andrew C Nov 06 '14 at 06:14
  • +1 for the effort, but that was indeed an *old* answer of mine. http://stackoverflow.com/a/16971547/6309 or (more complete) http://stackoverflow.com/a/19585361/6309 can involve less 'sed'. – VonC Nov 06 '14 at 06:35
1

In PowerShell, the following shows branches on the remote that are already merged and at least two weeks old (the author:relative format starts displaying weeks instead of days at two weeks):

$safeBranchRegex = "origin/(HEAD|master|develop)$";
$remoteMergedBranches = git branch --remote --merged | %{$_.trim()};
git for-each-ref --sort='authordate:iso8601' --format=' %(authordate:relative)%09%(refname:short)' refs/remotes | ?{$_ -match "(weeks|months|years) ago" -and $_ -notmatch "origin/(HEAD|master|qa/)"} | %{$_.substring($_.indexof("origin/"))} | ?{$_ -in $remoteMergedBranches}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dave Neeley
  • 3,526
  • 1
  • 24
  • 42
1

Taking inspiration from VonC's answer and making improvements (e.g. including remote branches, avoiding Perl, replacing git show by git log):

for k in $(git branch -a | sed -e 's/^..//' | grep -v -- '->'); \
    do echo -e $(git log -1 --pretty=format:"%ci %cr" $k) \
    \\t$k; done | sort -r
pdp
  • 4,117
  • 1
  • 17
  • 20
0

Or you can use my PHP script, https://gist.github.com/2780984

#!/usr/bin/env php
<?php
    $local = exec("git branch | xargs $1");
    $lines = explode(" ", $local);
    $limit = strtotime("-2 week");
    $exclude = array("*", "master");
    foreach ($exclude as $i) {
        $k = array_search($i, $lines);
        unset($lines[$k]);
    }
    $k = 0;
    foreach ($lines as $line) {
        $output[$k]['name'] = $line;
        $output[$k]['time'] = exec('git log '.$line.' --pretty=format:"%at" -1');
        if ($limit>$output[$k]['time']) {
            echo "This branch should be deleted $line\n";
            exec("git branch -d $line");
        }
        $k++;
    }
?>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ladislav Prskavec
  • 4,351
  • 3
  • 19
  • 16
0

Here's a function you can add to your bash_profile to make this easier.

Usage when in a Git repository:

  • branch prints all local branches
  • branch -r prints all remote branches

Function:

branch() {
   local pattern="s/^..//"
   local arg=""
   if [[ $@ == "-r" ]]; then
      pattern="s/^..(.*?)( ->.*)?$/\1/"
      arg=" -r "
      echo '-r provided'
   fi
   for k in $(git branch $arg | perl -pe "$pattern"); do
      echo -e $(git show --pretty=format:"%Cgreen%ci %Cblue%cr%Creset" $k -- | head -n 1)\\t$k
   done | sort -r
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
enderland
  • 13,825
  • 17
  • 98
  • 152
0

Here's a variation that sorts by date and puts the branch name before the date, which I find more readable:

git branch -v --sort='-authordate:iso8601' --format='%(align:width=50)%(refname:short)%(end)%(authordate:relative)'

Outputs

upscale-zone-a-3                                  11 minutes ago
master                                            17 minutes ago
value-metric-type                                 2 days ago
Jonathan
  • 5,027
  • 39
  • 48