planetp
planetp

Reputation: 16065

How to find out which of 'master' or 'main' branches exists in a repository?

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.

Upvotes: 14

Views: 5170

Answers (6)

Paul Hodges
Paul Hodges

Reputation: 15246

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

If you want to be a little more careful, especially for any automation -

for n in main master; do 
  if [[ -n "$(git branch -r -l "origin/$n")" ]]; then
    git checkout "$n" && break # keep the first success
  fi 
done 

EDIT: I originally had broken code.

Thanks to xmedeko for asking a question that made me look again.
Original bad code is further below, but first...

To answer xmedeko's question in the comments:

What about git branch -r | grep 'origin/HEAD' ?

This is usually a quick, simple, and effective way to find out, but has a few pitfalls. For one, it isn't guaranteed to be true.

$: git remote set-head origin test
$: git branch -r | grep 'origin/HEAD'
  origin/HEAD -> origin/test

In this case I think the OP specifically wants whatever the default branch is, but I work with some teams who make develop the default branch from which they copy to make new feature branches...

For manual checks it's fine, but if you're automating you still have to check that it IS the default, and then convert to whichever you found it on or this is likely to happen, which might not be what you want -

$: git checkout origin/HEAD
Note: switching to 'origin/HEAD'.

You are in 'detached HEAD' state. You can look around, make
experimental changes and commit them, and you can discard any
commits you make in this state without impacting any branches
by switching back to a branch.

If you want to create a new branch to retain commits you create,
you may do so (now or later) by using -c with the switch command. 
Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 28a1f02 Update some-file

Easy enough to switch with something like git branch -r|grep HEAD|sed 's,^.* origin/,,' if you know it's one or the other...

Now to explain what I did wrong before.

Assigning the list returned to an array should work,

I had a warning about spaces, but git branch names can't have spaces. More importantly, I had this code, which doesn't work.

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

That checkout is just wrong.

$: git branch -r | grep origin/ma
  origin/HEAD -> origin/main
  origin/main
  origin/master
$: b=( $(git branch -r | grep origin/ma ) ) # all matches
$: printf '[%s] ' "${b[@]}"; echo
[origin/HEAD] [->] [origin/main] [origin/main] [origin/master]
$: echo "${b[0]}"
origin/HEAD
$: echo "${b[0]##*/}"
HEAD

Doing a checkout of just HEAD is just going to grab whichever local branch you are already on.

$: for b in test main master; do git checkout $b; git checkout HEAD; done
Switched to branch 'test'
Your branch is up to date with 'origin/test'.
Your branch is up to date with 'origin/test'.
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
Your branch is up to date with 'origin/main'.
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
Your branch is up to date with 'origin/master'.

I considered suggesting

$: git remote set-head origin develop # for when this is true
$: git branch -r | grep -Eo 'origin/ma(in|ster)$'
origin/main
origin/master

This will give you whichever exists, or likely main if there's both, as it sorts above.

If HEAD is in fact either of these, then it solves even that.

$: git remote set-head origin master
$: git branch -r | grep -Eo 'origin/ma(in|ster)$'
origin/master
origin/main
origin/master
$: b=( $( git branch -r | grep -Eo 'origin/ma(in|ster)$' ) )
$: echo "${b[0]}"
origin/master

But I think I'd either use the loop above, or one of these:
First, a multiple grep and head (this hurts me...)

$: git remote set-head origin test
$: git branch -r | grep -E 'origin/(HEAD|main|master)$' | grep -Eo 'ma(in|ster)$' | head -1 
main
$: git remote set-head origin master
$: git branch -r | grep -E 'origin/(HEAD|main|master)$' | grep -Eo 'ma(in|ster)$' | head -1 
master
$: git remote set-head origin main
$: git branch -r | grep -E 'origin/(HEAD|main|master)$' | grep -Eo 'ma(in|ster)$' | head -1
main

Or second, the same thing with just one sed.

$: git remote set-head origin test
$: git branch -r | sed -En '/origin\/ma(in|ster)$/{s/^.* origin\///; p; q;}'
main
$: git remote set-head origin main
$: git branch -r | sed -En '/origin\/ma(in|ster)$/{s/^.* origin\///; p; q;}'
main
$: git remote set-head origin master
$: git branch -r | sed -En '/origin\/ma(in|ster)$/{s/^.* origin\///; p; q;}'
master
$: git checkout $(git branch -r | sed -En '/origin\/ma(in|ster)$/{s/^.* origin\///; p; q;}')
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

Upvotes: 4

Eugene Yarmash
Eugene Yarmash

Reputation: 149726

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

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 add --format to prevent that:

git branch -l main master --format '%(refname:short)'

Upvotes: 12

jthill
jthill

Reputation: 60235

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.

Upvotes: 1

Greg Bray
Greg Bray

Reputation: 15697

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
}

Upvotes: 0

torek
torek

Reputation: 487735

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

Upvotes: 5

Marcus M&#252;ller
Marcus M&#252;ller

Reputation: 36327

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

Upvotes: 3

Related Questions