Migrating git repos away from a “master” branch without breaking anyone’s local clones

We’ve started creating all new repos with a main default branch, but there are a number of existing git repositories that still use master as their default branch name. GitHub has been making a similar transition on their site along with the git community.

Searching online will reveal a lot of articles about how to change the default branch on github.com or changing the default branch on a local machine, but we have found that making changes to the origin can cause problems for people who don’t also make certain changes locally. We use some tools which can get confused if git’s internal files (the .git directory) are not accurate anymore.

Below I’ll document the steps we take when we change the default branch of a repo. Then I’ll dig into what the symbolic refs are and why they might not get updated automatically if one follows some other online instructions.

How we change the default branch both remotely and locally

Using the github.com UI, we rename the master branch to main.

Screenshot of GitHub showing that a branch will be renamed to main.

This blog post is assuming the remote is named origin; if your remote has a different name then you’ll have to substitute it.

We then locally run:

$ git fetch origin
$ git remote set-head origin -a
$ git branch -m master main
$ git branch -u origin/main main

Below I’ll try to explain each command in detail.

1) git fetch origin

First, we’ve made a change on github.com so we fetch to update the local .git objects and refs about the remote. Fetching will add any missing objects into .git/objects and update the files in .git/refs/remotes/origin/. We can see how those change by lsing before and after the fetch:

$ ls .git/refs/remotes/origin
HEAD  master

$ git fetch origin
From github.com:shareup/base64url-apple
 - [deleted]         (none)     -> origin/master
   (refs/remotes/origin/HEAD has become dangling)
 * [new branch]      main       -> origin/main
$ ls .git/refs/remotes/origin
HEAD  main

We can see the file master is gone and main has appeared. There also is the file HEAD which is curious because there is also a (strange) message from fetch that “refs/remotes/origin/HEAD has become dangling.”

The HEAD symbolic ref for a remote is “optional” according to the documentation for git remote, but every repo on all our machines has it set and some of our internal tooling relies on it being accurate.

We can check what git thinks the HEAD of the origin remote is:

$ git symbolic-ref refs/remotes/origin/HEAD

We could also just cat the file:

$ cat .git/refs/remotes/origin/HEAD 
ref: refs/remotes/origin/master

The git fetch command doesn’t update this HEAD file. The git remote command does.

2) git remote set-head origin -a

The git fetch above showed a strange “dangling” error and we can use another command to check on where git thinks different remote branches point to:

$ git branch -r 
warning: ignoring broken ref refs/remotes/origin/HEAD

We need to update the HEAD of origin in our local .git database over to the main branch.

We can use git remote to query the remote repo which should output the correct HEAD:

$ git remote show origin
* remote origin
  Fetch URL: git@github.com:shareup/base64url-apple.git
  Push  URL: git@github.com:shareup/base64url-apple.git
  HEAD branch: main
  Remote branch:
    main tracked
  Local branch configured for 'git pull':
    master merges with remote master

So we ask git remote to update the local HEAD ref automatically (that’s what -a means):

$ git remote set-head origin -a
origin/HEAD set to main

And now our remote tracking information from git branch is accurate again:

$ git branch -r 
  origin/HEAD -> origin/main

3) git branch -m master main

Next we rename the local master branch to main. A local branch doesn’t have to be the same name as the remote branch it tracks, but it’s super confusing if it’s not the same. This command doesn’t have any output if it works.

However, renaming a local branch does not change its upstream tracking settings. We can check that with git branch:

$ git branch -vv
* main c75a5de [origin/master] Add test…

We could also cat the .git/config to see the same info:

$ cat .git/config 
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
	ignorecase = true
	precomposeunicode = true
[remote "origin"]
	url = git@github.com:shareup/base64url-apple.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
	remote = origin
	merge = refs/heads/master

4) git branch -u origin/main main

Finally we repoint the local branch to track the new remote branch which will update .git/config:

$ git branch -u origin/main main
Branch 'main' set up to track remote branch 'main' from 'origin'.

We can check and it has indeed been updated:

$ git branch -vv
* main c75a5de [origin/main] Add test…

$ cat .git/config 
[branch "main"]
	remote = origin
	merge = refs/heads/main

And we can use git pull to verify that all is working and wired up correctly:

$ git pull
Already up to date.

Why write yet another article about how to rename a git branch?

Many articles online (and GitHub themselves) say to rename the origin’s branch up on github.com and then run some commands locally which end up leaving the remote HEAD ref set incorrectly, leaving an old master branch, or other similar problems.

Update: I spoke to a friend at GitHub and they’ve updated their instructions to include a git remote set-head origin -a line. So now if you follow what the GitHub UI shows everything should work out. 😎 🆒 (•_•) ( •_•)>⌐■-■ (⌐■_■)

Maybe you’ve already renamed your branch away from master and now you are getting some error that seems related. You can check where origin’s HEAD is and update it like this:

# Check where it is now
$ git symbolic-ref refs/remotes/origin/HEAD

# Update it to point to its main
$ git remote set-head origin -a

What even is a symbolic ref?

Symbolic refs are little text files inside the .git/refs directory. Long ago, git used symbolic links to keep track of what a branch “points to”, but now is using little text files because it’s compatible with more OS’s.

We can actually print them out to see what they point to. Here are some examples from my local repo:

$ cat .git/refs/remotes/origin/HEAD
ref: refs/remotes/origin/main

$ cat .git/refs/remotes/origin/main 

$ cat .git/refs/heads/main 

$ cat .git/HEAD 
ref: refs/heads/main

You can see from this that my origin’s HEAD is pointed to its main branch. The origin’s main branch is pointed to commit c75a5de and so is my local main branch. And finally you can see my local HEAD is pointed at my local main branch. 🤓

Happy git branch renaming 🥳🎊🎉

I hope this can help if you are changing a bunch of default branches (like I am today) 😆