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
.
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 ls
ing 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
refs/remotes/origin/master
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
origin/main
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
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
[core]
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
refs/remotes/origin/master
# 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
c75a5dee7dc3bce2eba6d5ee116c4c921022d871
$ cat .git/refs/heads/main
c75a5dee7dc3bce2eba6d5ee116c4c921022d871
$ 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) 😆