Like I mentioned back on Monday, I decided to spend some time over the last week updating the repositories I use regularly to use a main branch instead of master, since the latter has unpleasant an alienating connotations.


Before you tell me that it “obviously” derives from a different meaning, keep in mind that git takes a lot of its ideas from BitKeeper, which includes the concept of a master branch with slave branches. Likewise, if you’re going to argue that it’s from “master copy,” I have some awful news for you…

Similarly, if you’re going to ask why it’s suddenly important…that’s a good question, because this sort of terminology should never have been acceptable, and it’s embarrassing that we need to be reminded that our choice of words has consequences. But since everybody is thinking about it now, it’s a good time to do it, rather than sheepishly operating the minority of repositories that didn’t bother.

In other words, the best time to fix a mistake is always now, because you can’t fix anything before now and waiting until later just leaves the mistake where it is.

Altering the Branch

Plus, regardless of the intent, it’s still an uncomfortable term to have around, and it can easily give the impression that the project (and industry as a whole) isn’t ready to welcome certain kinds of people. So, I’m going to change it where I can. Because I have dozens of repositories floating around, I decided to get it right once and then move the information to a script to make it reproducible. Here’s the final version for GitHub, in case anybody wants to play along.

url=$(grep 'url = ' .git/config | \
      cut -f2- -d'=' | \
      sed 's/^ *//g' | \
      sed 's/\.git *$//g' )
if case $url in git@*) true;; *) false;; esac
  url=$(echo $url | \
        sed 's/:/\//g' | \
        sed 's/^git@/https:\/\//g')
git branch -m master main
git push -u origin main
echo Go update the default "${url}${branchpath}"
echo Then run:
echo git push origin --delete master

It doesn’t have any error checking, so don’t run it outside your repository’s local root folder, where you have administrative control over the remote/upstream repository.

The url variable digs into the git configuration file to find the remote repository. I hate the syntax, but if the url we find starts with git@, modify it to a real GitHub URL instead of GitHub’s format. All the pieces are there, of course. The case syntax looks like a mess, but it works without any special cases to compare the names.

Then we have two git commands. The first moves (-m) or renames the master branch to main. Next, we push the new branch and set the upstream (-u) to point to it. At this point, the local repository only has a main branch and is set to work exclusively with the remote main branch.

Why main and not trunk, primary, or something else entirely? When I change branches (not that I have many public-facing branches), muscle-memory is still git branch m and then let the shell handle auto-completion. You can call yours what you want, obviously, and even update the script to default to a name if you don’t provide it with one.

However, there are still two (manual) steps remaining. Over the GitHub page, the default branch still reads master , so someone needs to change it by hand. So, that url work the script did earlier pays off, producing a link to, where the user can update the default. Obviously, I make no claims about the URL being correct for any host other than GitHub. I really should start looking at GitLab; they seem like a nice company that people should pay some attention to.

Once that’s done, you can delete the master branch entirely and never worry about it again…except for the rest of this post, because there are still some edge cases.

I’m not currently running any CI/CD, but if you choose to work with this script and do, you should take a minute and make sure that it never explicitly references the master branch in any of its work.


It finally dawned on me that this should probably be handled through GitHub’s API, so that the entire experience is just running a script. I didn’t feel like digging into it for this post, because learning how to make and handle API calls takes this from a “quick script” to a full application that needs to handle remote authentication and all the possible error states. So, I wasn’t going down that path…until I remembered that GitHub has a CLI tool.

Installing that takes care of the “application” part of this. And with some research, I was able to work out how change the default branch entirely through the tool.

gh api repos/the-user/the-repository -X PATCH
  -F name="the-repository" \
  -F default_branch="main"

Obviously, change the-user and the-repository (both instances) to yours. But now that we have this piece of information, we can replace the echo lines above with the following.

user=$(echo "${url}" | cut -f4 -d'/')
repo=$(echo "${url}" | cut -f5 -d'/')
gh api repos/${user}/${repo} -X PATCH -F name="${repo}" -F default_branch="main"
git push origin --delete master

We know that a repository URL is going to look like, so we pull off the fourth and fifth items, as delimited by slashes. The first is https:, the second is empty, and so forth.

Then, we call the GitHub CLI tool. The first time you successfully call it, it will ask you to hit Enter to open GitHub in a browser. There, you can authorize the tool to operate on your behalf. Then, once you’re authenticated, the script continues on to delete the branch.

Except for the one-time authentication step, there are no manual steps. I call that a success.

Local Clones

The above won’t work for a clone, because that work is primarily administrative. If you have a clone of a project that has been making changes like this (for example, those of my projects that have migrated over), you’ll see something like this.

From url-to-repository
 * [new branch]      main       -> origin/main
Your configuration specifies to merge with the ref 'refs/heads/master'
from the remote, but no such ref was fetched.

The above script won’t work, because it wants to affect the origin. Instead, you need something more streamlined, like the following.

git checkout master
git branch -m master main
git fetch
git branch --unset-upstream
git branch -u origin/main
git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main

This makes sure that you start out on the master branch (to make sure that you don’t break a different branch), moves/renames the branch as above, gets the latest commits from upstream, unlinks the upstream repository, links the new branch upstream (almost as above, though not quite), and finally updates the clone’s local default branch.

Like above, run it in the clone’s root folder and don’t expect the script to recover easily if there are any errors.

Never Again…

It would, of course, be nice if we never needed to see a master branch again. Most of us don’t yet have the ability to just tell git that we never want a repository with one, but we can create a decent alias to use instead of git init until we do.

git config --global '!git init && git symbolic-ref HEAD refs/heads/main'

Now, we can call git new to initialize our repositories and the alias will create it and set the default branch name.

That’s not an ideal situation, since muscle memory will inevitably type the default command that we can’t override. But as a temporary measure, it should work out for most cases until the latest version of git shows up. To prepare for that, you might as well configure things now.

git config --global init.defaultBranch main

When Git 2.28 comes to your platform (Ubuntu is still on v2.25, for example), it will pick up that option and all of your new local repositories will have default branches with your custom name.

Credits: The header image is tree, nature, branch, winter, black and white, plant, trunk, bark, high, monochrome, twig, trees, twigs, woods, branches, tall, monochrome photography, atmospheric phenomenon, woody plant by an anonymous photographer, released under the terms of CC0 1.0 Universal Public Domain Dedication.