loading...

Changing the Primary Git Branch

jcolag profile image John Colagioia Originally published at john.colagioia.net on ・7 min read

As a quick note, I released this post on my blog, this morning, so it can get to be (as I tend to be) a bit rambling. Oh, and the original text is on GitHub (licensed CC-BY-SA), so if anything seems muddy, by all means:

  • Leave a comment here,
  • Leave a comment on the blog,
  • File an issue on GitHub, or
  • Add a pull request!

GitHub logo jcolag / entropy-arbitrage

John Colagioia's Blog Posts


Recently, I decided to spend some time updating the repositories I use regularly to use a main branch instead of master, since the latter has unpleasant an alienating connotations.

Branches

Before you tell me that the term “obviously” derives from some 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,” well, I have some awful news for you…

Similarly, if you’re going to ask why it’s suddenly important…that’s actually 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 the 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 when I stumble on another forgotten repository. Here’s the almost-final version of the script for GitHub, in case anybody wants to play along.

#!/bin/sh
branchpath=/settings/branches
url=$(grep 'url = ' .git/config | \
  cut -f2- -d'=' | \
  sed 's/^ *//g' | \
  sed 's/\.git *$//g' )
if case $url in git@*) true;; *) false;; esac
then
  url=$(echo $url | \
    sed 's/:/\//g' | \
    sed 's/^git@/https:\/\//g')
fi
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. If the url we find starts with git@, modify it to a real GitHub URL instead of GitHub’s git@github.com:name/project.git format. All the pieces are there, of course. The case syntax looks like a mess, but it works without any special exceptions to compare the names and should be completely portable across shell scripts.

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, production, or something else entirely? When I change branches (not that I have many public-facing branches), my muscle-memory is still for typing git branch ma and then letting 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 without too much trouble.

However, there are still two manual steps remaining. Over on 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 here, producing a link to https://github.com/NAME/PROJECT/settings/branches, 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 and improvements.

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 the tool never explicitly references the master branch in any of its work, at this point.

Advanced

It eventually dawned on me that the big manual step (changing the default branch) should probably be handled through GitHub’s API, so that the entire experience is just running a script. I initially didn’t feel like digging into it for this post, because learning how to make and handle API calls takes this project 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 https://github.com/the-user/the-repository/, 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 is all well and good, but it won’t work for an ordinary clone, because the work there 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 an error something like the following.

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, which has already been affected and you might not have access to. Instead, you need something more straightforward, like the following.

#!/bin/sh
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, disconnects the upstream repository, links the new branch upstream (almost the same as above, though not quite), and finally updates the clone’s local default branch to point to the right place.

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 alias.new '!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, and if you're on a machine that doesn't auto-update git, you're probably always a few versions behind, too), 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.

Discussion

pic
Editor guide