8

I'm working on a Bash script that does some Git repository maintenance. Some of repositories use 'master' as the main branch while others use 'main'. What Git command can I use to return the first branch of these which exists?

P.S. I want to check local branches of a repo, since there will always be a local 'master' or 'main' branch.

planetp
  • 14,248
  • 20
  • 86
  • 160
  • You'd have to define "the first which exists". If you meant the first which has been created, git doesn't keep that kind of branch metadata. But you might want to check `git config --get init.defaultbranch` on recent enough git repos. – Romain Valeri Jun 22 '21 at 15:07
  • 1
    @RomainValeri that would only tell you which would be the default branch when initializing a new repo, not of the current one – Marcus Müller Jun 22 '21 at 15:09
  • Not the first which has been created, just the first from the list which exists. I assume there will be one of these. BTW, `git config --get init.defaultbranch` doesn't return anything for me. – planetp Jun 22 '21 at 15:10
  • 1
    what do you do if both exist? – Marcus Müller Jun 22 '21 at 15:12
  • @MarcusMüller As I said, there will always be just one of them. Why would you need to have both? – planetp Jun 22 '21 at 15:29
  • @planetp for example, a medium-sized FOSS project I work closely with migrated to `main`. But they couldn't just waltz in and immediately delete `master`, because there's people (and projects) whose automation depends on the name of branches, so `master` and `main` both exist, but `master` simply sees no further updates. – Marcus Müller Jun 22 '21 at 15:30

6 Answers6

9

To find out which of the two local branches exists, you can use git branch with the -l/--list argument:

git branch -l master main  # outputs 'master' or 'main', provided only one exists

Git also marks the current branch with an asterisk, so you could use sed to strip it:

git branch -l master main | sed 's/^* //'
Eugene Yarmash
  • 142,882
  • 41
  • 325
  • 378
  • 1
    Better use `git branch -l master main | sed -r 's/^[* ] //' | head -n 1`: removes the first double space or space star according to the current branch state, then remove any other result line which could corrupt the variable content. – jbaptperez Jul 01 '22 at 14:54
4

To find out what some other Git repository's HEAD is, use git ls-remote:

$ git ls-remote --symref origin HEAD
ref: refs/heads/master  HEAD
670b81a890388c60b7032a4f5b879f2ece8c4558    HEAD

This assumes both your Git, and the remote's Git, are not so old that they do not support --symref (it must be supported on both sides). Note that this command can be run outside any repository by using a URL directly, instead of a remote name like origin.

To find out what branch names exist in some other Git repository, either clone it and inspect the resulting remote-tracking names, or use git ls-remote. Note that you can specify just refs/heads to limit the output to branch names only, or omit it entirely to get everything (all branch and tag names and any other names they choose to expose).

torek
  • 448,244
  • 59
  • 642
  • 775
3

What Git command can I use to return the first branch of these which exists?

git branch -r returns a list of remotes.
grep origin/ma will match main and master.
If you need to be pickier, use grep -E 'origin/(main|master)'

Assigning the list returned to an array should work, but be wary of names with spaces &c because the array elements are delimited by whitespace and the whole thing can blow up.

b=( $(git branch -r | grep origin/ma ) ) # all matches
git checkout "${b[0]##*/}"               # first hit

If you want to be a little more careful,

for n in main master; do 
  b="$(git branch -r -l "origin/$n")"
  if [[ -n "$b" ]]; then
    git checkout "${b[0]##*/}"
    break # keep the first success
  fi 
done 
   
Paul Hodges
  • 13,382
  • 1
  • 17
  • 36
1

Well, "default branch" has no meaning locally; it only makes sense if your repo is a remote to someone else, or in terms of a remote that someone else hosts. (Also, in terms of "what branch do I pull by default when I pull from this local branch, but if you knew that, you wouldn't be asking this.)

git fetch
git branch -r --list 'origin/HEAD' | grep '>'
Marcus Müller
  • 34,677
  • 4
  • 53
  • 94
0

I wanted a script that worked with multiple remotes (origin fork or canonical upstream) and repositories that used main or master. Based on Paul Hodges answer I ended up with:

#Update local/fork default to match remote. Can also delete current local branch using -D (to clean up a finished feature branch)
gum(){
  REMOTES=( $(git branch -r | grep -Eo "  (origin|upstream)/(master|main)") )
  RNAME=${REMOTES[-1]%%/*}    # Prefix of last one in the list (prefer fetching from upstream over origin)
  RBRANCH=${REMOTES[-1]##*/}  # Suffix of last matching remote
  LBRANCH="$(git rev-parse --abbrev-ref HEAD)"  # Name of current checked out branch

  if [[ "$LBRANCH" != "$RBRANCH" && "$1" != "-D" ]]; then
    echo "error current branch is '$LBRANCH' not $RBRANCH and -D not specified to delete current branch"
  elif [[ ! -z $(git status -s) ]]; then
    echo "error pending changes on local branch"
  else
    # Switch to local default, delete current if requested
    if [[ "$LBRANCH" != "$RBRANCH" && "$1" == "-D" ]]; then
      git checkout $RBRANCH
      git branch -D $LBRANCH
    fi
    # fetch all and prune, then update local default
    git fetch --all -p && git merge $RNAME/$RBRANCH && if [[ "$RNAME" != "origin" ]]; then
      git push ${REMOTES[0]%%/*} $RBRANCH # also update default branch on fork
    fi
  fi
}
Greg Bray
  • 14,929
  • 12
  • 80
  • 104
0

I want to check local branches of a repo, since there will always be a local master or main branch

Taking that as a reliable description of your intended use-case environment, since in general use it's often enough not true,

What Git command can I use to return the first branch of these which exists?

A concise way of doing it is

if git cat-file -e refs/heads/master 2>&-; then echo master
elif git cat-file -e refs/heads/main 2>&-; then echo main
fi

but for anything even a little more elaborate you're likely to want for-each-ref,

{ git for-each-ref --format='%(refname)' refs/{heads,remotes/*}/master;
  git for-each-ref --format='%(refname)' refs/{heads,remotes/*}/main;
} | sed -E 's,(.*)/,\1 ,' | sort -usk1,1

which gives the answer for local branches and all tracked remotes without any foofaraw.

In my git repo this reports refs/remotes/origin master, for instance, and the Git project keeps both branch names in lockstep. But as someone else pointed out, some projects keep master around as a record of where it was when they switched away, and as you might have noticed I don't have a master or main branch. My make branch tracks origin/next, that's the one I run my deployments from, and there's a couple others for little projects.

To do just local branches do

{ git for-each-ref --format='%(refname:short)' refs/heads/master;
  git for-each-ref --format='%(refname:short)' refs/heads/main;
} | head -1

See the for-each-ref examples, it was built for this.

jthill
  • 55,082
  • 5
  • 77
  • 137