How to iterate through all git branches using bash script

51,096

Solution 1

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

Solution 2

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

Solution 3

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

Solution 4

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;

Solution 5

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

Share:
51,096
Arun P Johny
Author by

Arun P Johny

LinkedIn

Updated on October 14, 2021

Comments

  • Arun P Johny
    Arun P Johny over 2 years

    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

  • Ali Hidim
    Ali Hidim over 13 years
    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).
  • Andrei-Niculae Petre
    Andrei-Niculae Petre almost 11 years
    nice, i really like your 3- solution.
  • jdg
    jdg about 9 years
    Slightly simpler sed version: $(git branch | sed 's/^..//')
  • ccpizza
    ccpizza about 9 years
    slightly simpler tr version: $(git branch | tr -d " *")
  • Jim Fell
    Jim Fell about 9 years
    Seriously. There isn't a simpler way to do this?
  • Thayne
    Thayne about 9 years
    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.
  • wid
    wid over 8 years
    Simplier version: git for-each-ref refs/heads | cut -d/ -f3-
  • dhara tcrails
    dhara tcrails almost 8 years
    @wid: Or, simply, git for-each-ref refs/heads --format='%(refname)'
  • Chris Cogdon
    Chris Cogdon over 6 years
    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 )
  • Alexander Mills
    Alexander Mills over 5 years
    json needs to be used in command line tools, I can't believe Git and others haven't done this yet
  • torek
    torek over 5 years
    @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.)
  • Abhi
    Abhi over 4 years
    This looks less complex solution to me +1
  • ecbrodie
    ecbrodie almost 4 years
    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".
  • Abhi
    Abhi almost 3 years
    Eg: Delete all branches starting with abc: for b in $(git branch | grep abc); do git branch -D $b; done
  • joanis
    joanis over 2 years
    This includes -> and both origin/HEAD and origin/main for me...
  • binki
    binki almost 2 years
    I think the answer stackoverflow.com/a/57748047 is what people coming to this question are looking for. Please upvote it!