How to iterate through all git branches using bash script
Asked Answered
H

18

138

How can I iterate through all the local branches in my repository using bash script. I need to iterate and check is there any difference between the branch and some remote branches. Ex

for branch in $(git branch); 
do
    git log --oneline $branch ^remotes/origin/master;
done

I need to do something like given above, but the issue I'm facing is $(git branch) gives me the folders inside the repository folder along with the branches present in the repository.

Is this the correct way to solve this issue? Or is there another way to do it?

Thank you

Hinshelwood answered 2/10, 2010 at 15:33 Comment(6)
Possible duplicate of for loop over all git branches with certain nameQuitclaim
@Quitclaim This was written before the linked question.Zaidazailer
Eg: - for b in "$(git branch)"; do git branch -D $b; doneFuentes
Not a duplicate. This question goes for all branches while the other goes for ones with specific nameToback
@Fuentes YOU GOT ME! lol I didn't even notice the -D in thereXerophagy
Also, for i in $(git branch | grep feature/); do if [[ $i == "feature/"* ]]; then echo $i; fi; done. For all branches starting with feature/Fuentes
M
196

You should not use git branch when writing scripts. Git provides a “plumbing” interface that is explicitly designed for use in scripting (many current and historical implementations of normal Git commands (add, checkout, merge, etc.) use this same interface).

The plumbing command you want is git for-each-ref:

git for-each-ref --shell \
  --format='git log --oneline %(refname) ^origin/master' \
  refs/heads/

Note: You do not need the remotes/ prefix on the remote ref unless you have other refs that cause origin/master to match multiple places in the ref name search path (see “A symbolic ref name. …” in the Specifying Revisions section of git-rev-parse(1)). If you are trying to explictly avoid ambiguity, then go with the full ref name: refs/remotes/origin/master.

You will get output like this:

git log --oneline 'refs/heads/master' ^origin/master
git log --oneline 'refs/heads/other' ^origin/master
git log --oneline 'refs/heads/pu' ^origin/master

You can pipe this output into sh.

If you do not like the idea of generating the shell code, you could give up a bit of robustness* and do this:

for branch in $(git for-each-ref --format='%(refname)' refs/heads/); do
    git log --oneline "$branch" ^origin/master
done

* Ref names should be safe from the shell’s word splitting (see git-check-ref-format(1)). Personally I would stick with the former version (generated shell code); I am more confident that nothing inappropriate can happen with it.

Since you specified bash and it supports arrays, you could maintain safety and still avoid generating the guts of your loop:

branches=()
eval "$(git for-each-ref --shell --format='branches+=(%(refname))' refs/heads/)"
for branch in "${branches[@]}"; do
    # …
done

You could do something similar with $@ if you are not using a shell that supports arrays (set -- to initialize and set -- "$@" %(refname) to add elements).

Mccubbin answered 2/10, 2010 at 21:24 Comment(8)
Seriously. There isn't a simpler way to do this?Lenticel
But what if I want to use one of the filtering options of git branch, like --merged, would I have to duplicate the logic in git branch? There has to be a better way to do this.Outpost
Simplier version: git for-each-ref refs/heads | cut -d/ -f3-Hon
@wid: Or, simply, git for-each-ref refs/heads --format='%(refname)'Homy
If the output is safe from newlines, one can do this: git for-each-ref --format='%(refname)' refs/heads | while read x ; do echo === $x === ; done. Note that this puts the while loop into a subshell. If you want the while loop in the current shell, then this: while read x ; do echo === $x === ; done < <( git for-each-ref --format='%(refname)' refs/heads )Outstay
json needs to be used in command line tools, I can't believe Git and others haven't done this yetGlow
@Thayne: this question is old, but the Git folks have finally addressed the problem: for-each-ref now supports all the branch selectors like --merged and git branch and git tag are now actually implemented in terms of git for-each-ref itself, at least for the list-existing cases. (Creating new branches and tags is not, and should not be, part of for-each-ref.)Kirby
I think the answer stackoverflow.com/a/57748047 is what people coming to this question are looking for. Please upvote it!Nada
G
58

This is because git branch marks the current branch with an asterisk, e.g.:

$ git branch
* master
  mybranch
$ 

so $(git branch) expands to e.g. * master mybranch, and then the * expands to the list of files in the current directory.

I don't see an obvious option for not printing the asterisk in the first place; but you could chop it off:

$(git branch | cut -c 3-)
Garrote answered 2/10, 2010 at 15:49 Comment(5)
If you surround in double-quotes you can stop bash from expanding the asterisk - though you'll still want to remove it from the output. A more robust way of removing an asterisk from any point would be $(git branch | sed -e s/\\*//g).Doorkeeper
nice, i really like your 3- solution.Banerjee
Slightly simpler sed version: $(git branch | sed 's/^..//')Nudicaul
slightly simpler tr version: $(git branch | tr -d " *")Sunshinesunspot
This should be the marked answer. The currently marked answer is just preachy without answering the question about why it's not workingJuliennejuliet
P
21

The bash builtin, mapfile, is built for this

all git branches: git branch --all --format='%(refname:short)'

all local git branches: git branch --format='%(refname:short)'

all remote git branches: git branch --remotes --format='%(refname:short)'

iterate through all git branches: mapfile -t -C my_callback -c 1 < <( get_branches )

example:

my_callback () {
  INDEX=${1}
  BRANCH=${2}
  echo "${INDEX} ${BRANCH}"
}
get_branches () {
  git branch --all --format='%(refname:short)'
}
# mapfile -t -C my_callback -c 1 BRANCHES < <( get_branches ) # if you want the branches that were sent to mapfile in a new array as well
# echo "${BRANCHES[@]}"
mapfile -t -C my_callback -c 1 < <( get_branches )

for the OP's specific situation:

#!/usr/bin/env bash


_map () {
  ARRAY=${1?}
  CALLBACK=${2?}
  mapfile -t -C "${CALLBACK}" -c 1 <<< "${ARRAY[@]}"
}


get_history_differences () {
  REF1=${1?}
  REF2=${2?}
  shift
  shift
  git log --oneline "${REF1}" ^"${REF2}" "${@}"
}


has_different_history () {
  REF1=${1?}
  REF2=${2?}
  HIST_DIFF=$( get_history_differences "${REF1}" "${REF2}" )
  return $( test -n "${HIST_DIFF}" )
}


print_different_branches () {
  read -r -a ARGS <<< "${@}"
  LOCAL=${ARGS[-1]?}
  for REMOTE in "${SOME_REMOTE_BRANCHES[@]}"; do
    if has_different_history "${LOCAL}" "${REMOTE}"; then
      # { echo; echo; get_history_differences "${LOCAL}" "${REMOTE}" --color=always; } # show differences
      echo local branch "${LOCAL}" is different than remote branch "${REMOTE}";
    fi
  done
}


get_local_branches () {
  git branch --format='%(refname:short)'
}


get_different_branches () {
  _map "$( get_local_branches )" print_different_branches
}


# read -r -a SOME_REMOTE_BRANCHES <<< "${@}" # use this instead for command line input
declare -a SOME_REMOTE_BRANCHES
SOME_REMOTE_BRANCHES=( origin/master remotes/origin/another-branch another-remote/another-interesting-branch )
DIFFERENT_BRANCHES=$( get_different_branches )

echo "${DIFFERENT_BRANCHES}"

source: List all local git branches without an asterisk

Poeticize answered 12/11, 2018 at 22:30 Comment(0)
B
9
for branch in $(git for-each-ref --format='%(refname:short)' refs/heads); do
    ...
done

This uses git plumbing commands, which are designed for scripting. It's also simple and standard.

Reference: Git's Bash completion

Boito answered 1/9, 2019 at 17:19 Comment(0)
G
6

I iterate as it for example :

for BRANCH in `git branch --list|sed 's/\*//g'`;
  do 
    git checkout $BRANCH
    git fetch
    git branch --set-upstream-to=origin/$BRANCH $BRANCH
  done
git checkout master;
Goldbrick answered 21/3, 2016 at 11:40 Comment(0)
R
4

The accepted answer is correct and really should be the approach used, but solving the problem in bash is a great exercise in understanding how shells work. The trick to doing this using bash without performing additional text manipulation, is to ensure the output of git branch never gets expanded as part of a command to be executed by the shell. This prevents the asterisk from ever being expanding in the file name expansion (step 8) of shell expansion (see http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_03_04.html)

Use the bash while construct with a read command to chop the git branch output into lines. The '*' will be read in as a literal character. Use a case statement to match it, paying special attention to the matching patterns.

git branch | while read line ; do                                                                                                        
    case $line in
        \*\ *) branch=${line#\*\ } ;;  # match the current branch
        *) branch=$line ;;             # match all the other branches
    esac
    git log --oneline $branch ^remotes/origin/master
done

The asterisks in both the bash case construct and in the parameter substitution need to be escaped with backslashes to prevent the shell interpreting them as pattern matching characters. The spaces are also escaped (to prevent tokenization) because you are literally matching '* '.

Reprint answered 30/1, 2016 at 8:32 Comment(0)
A
4

Keep it simple

The simple way of getting branch name in loop using bash script.

#!/bin/bash

for branch in $(git for-each-ref --format='%(refname)' refs/heads/); do
    echo "${branch/'refs/heads/'/''}" 
done

Output:

master
other
Assurbanipal answered 10/4, 2019 at 10:29 Comment(0)
D
4

Finally nailed how to make an output for git branch without an asterisk and without magic in git for-each-ref arguments:

$ git branch --format="%(refname:short)"

Why this is valuable? git branch command has some additional filters like --merged, which is not easy to implement using git for-each-ref (at least as far as I see).

Descendible answered 11/3, 2023 at 13:14 Comment(0)
P
3

I would suggest $(git branch|grep -o "[0-9A-Za-z]\+") if your local branches are named by digits, a-z, and/or A-Z letters only

Pascasia answered 20/7, 2012 at 3:43 Comment(0)
R
3

Easiest option to remember in my opinion:

git branch | grep "[^* ]+" -Eo

Output:

bamboo
develop
master

Grep's -o option (--only-matching) restricts the output to only the matching parts of the input.

Since neither space nor * are valid in Git branch names, this returns the list of branches without the extra characters.

Edit: If you're in 'detached head' state, you'll need to filter out the current entry:

git branch --list | grep -v "HEAD detached" | grep "[^* ]+" -oE

Raspy answered 1/8, 2017 at 14:0 Comment(0)
U
2

What I ended up doing, applied to your question (& inspired by ccpizza mentioning tr):

git branch | tr -d ' *' | while IFS='' read -r line; do git log --oneline "$line" ^remotes/origin/master; done

(I use while loops a lot. While for particular things you'd definitely want to use a pointed variable name ["branch", for example], most of the time I am only concerned with doing something with each line of input. Using 'line' here instead of 'branch' is a nod to reusability/muscle memory/efficiency.)

Umeh answered 30/1, 2018 at 18:36 Comment(0)
H
2

Googlian's answer, but without using for

git for-each-ref --format='%(refname:lstrip=-1)' refs/heads/
Housecoat answered 18/7, 2019 at 10:13 Comment(1)
This doesn't work for namespaced branch names, as in branches that have a slash in them. This means that branches created by dependabot, which look something like "dependabot/npm_and_yarn/typescript-3.9.5", will appear instead as "typescript-3.9.5".Rook
B
1

If you're at this state:

git branch -a

* master

  remotes/origin/HEAD -> origin/master

  remotes/origin/branch1

  remotes/origin/branch2

  remotes/origin/branch3

  remotes/origin/master

And you run this code:

git branch -a | grep remotes/origin/*

for BRANCH in `git branch -a | grep remotes/origin/*` ;

do
    A="$(cut -d'/' -f3 <<<"$BRANCH")"
    echo $A

done        

You'll get this result:

branch1

branch2

branch3

master
Biochemistry answered 7/3, 2019 at 3:53 Comment(3)
This looks less complex solution to me +1Fuentes
Eg: Delete all branches starting with abc: for b in $(git branch | grep abc); do git branch -D $b; doneFuentes
Also, for i in $(git branch | grep feature/); do if [[ $i == "feature/"* ]]; then echo $i; fi; done. For all branches starting with feature/Fuentes
N
1

The correct way to simply iterate over local branch names is to use for-each-ref over refs/heads/. For example:

for branch in $(git for-each-ref --format='%(refname:short)' refs/heads/); do
  echo branch="${branch}"
done

This works with standard/default values for IFS because spaces are illegal in branch names in git.

Nada answered 8/6, 2022 at 21:27 Comment(0)
O
0

Extending on from @finn's answer (thank you!), the following will let you iterate over the branches without creating an intervening shell script. It's robust enough, as long as there's no newlines in the branch name :)

git for-each-ref --format='%(refname)' refs/heads  | while read x ; do echo === $x === ; done

The while loop runs in a subshell, which is usually fine unless you're setting shell variables that you want to access in the current shell. In that case you use process substitution to reverse the pipe:

while read x ; do echo === $x === ; done < <( git for-each-ref --format='%(refname)' refs/heads )
Outstay answered 23/10, 2017 at 22:31 Comment(0)
H
0

List heads (branches) in the local repository

git show-ref --heads

This will list the heads something like

682e47c01dc8d0f4e4102f183190a48aaf34a3f0 refs/heads/main
....

so if you're only interested in the name, you can use something like sed to obtain the output you want

git show-ref --heads | sed 's/.*refs\/heads\///'

Iterate through the branches

With this output you can easily iterate through it, say using a bash loop, xargs, whatever floats your boat

for SHA in $(git show-ref --heads | awk '{ print $1 }'); do
 echo "magic! $SHA"
done
  • git show-ref --heads get the branches as per above
  • awk '{ print $1 }' obtain the SHA
  • echo "magic! $SHA" <- this is where you would do your magic
House answered 31/5, 2021 at 16:38 Comment(0)
S
0

Of course in theory one should use a special interface that Git indeed has for use when scripting. But often you want something simpler — handy for a oneliner. Something that doesn't urge you remember stuff like git for-each-ref --format … refs … amen. And it's UNIX anyways finally. Then it goes like that:

  1. There's an utility widely known for its obscure but a terse way to print the last column.
  2. git branch puts asterisk before the branch name. Meaning we're seemingly always interested in the last column exactly.

Resulting:

git branch | awk '{print $NF}'

This works because awk has an interval variable NF which is the number of the last field. Prefixing this number with $ will yield the contents of that field witch is exactly what is need here.

Slouch answered 9/5, 2022 at 11:1 Comment(0)
A
-1
#/bin/bash
for branch in $(git branch -r); 
do
    echo $branch
done
Aerosphere answered 13/10, 2021 at 18:56 Comment(1)
This includes -> and both origin/HEAD and origin/main for me...Kearney

© 2022 - 2024 — McMap. All rights reserved.