Skip to content

Instantly share code, notes, and snippets.

@davidstosik
Last active January 7, 2021 13:41
Show Gist options
  • Save davidstosik/3d58aac3481eebf5499adc9ff07904c5 to your computer and use it in GitHub Desktop.
Save davidstosik/3d58aac3481eebf5499adc9ff07904c5 to your computer and use it in GitHub Desktop.

Never check out master instead of main again with this weird trick!

GitHub recently changed the repository's default branch name from master to main, challenging millions of developers' muscle memory:

git checkout master

error: pathspec 'master' did not match any file(s) known to git

git checkout main

Switched to branch 'main'

It's going to take a while to get used to this 😀

@iamharoon9 on Twitter

$ # In a recent repo that did the switch:
$ git checkout master
error: pathspec 'master' did not match any file(s) known to git

$ # In an older repo that hasn't switched yet:
$ git checkout main
error: pathspec 'main' did not match any file(s) known to git

This is an attempt to improve a developer's workflow by offering a fallback system.

Limitations

Unfortunately, git does not allow its core commands to be overridden, neither with an alias, nor a custom command script, as I try to show below:

$ git config --global alias.hw '!hw(){ echo "Hello world";};hw'
$ git hw
Hello world
$ git config --global alias.checkout '!hw(){ echo "Hello world";};hw'
$ git checkout
--Does not output Hello world--

$ echo "#!/usr/bin/env sh
echo 'Hello world'" > "$HOME/bin/git-helloworld"
$ chmod u+x "$HOME/bin/git-helloworld"
$ git helloworld
Hello world
$ mv "$HOME/bin/git-helloworld" "$HOME/bin/git-checkout"
$ git checkout
--Does not output Hello world--

A new alias

My first approach, rather lean, is to define a new alias, cm, which will check out either main or master, whichever it finds first.

$ git config --global alias.cm '!cm(){ git checkout main || git checkout master;}; cm'

$ # In an old repo:
$ git cm
error: pathspec 'main' did not match any file(s) known to git
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

$ # In a newer repo:
$ git cm
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

The behaviour when both master and main branches exist can be decided by the order in which the commands are written:

$ # If both exist, this will check out `main`
$ git config --global alias.cm '!cm(){ git checkout main || git checkout master;}; cm'

$ # If both exist, this will check out `master`
$ git config --global alias.cm '!cm(){ git checkout master || git checkout main;}; cm'

The custom script strikes back

Luckily, I'm used to a simple git alias, co, which has been in my .gitconfig file for years now:

$ git config --global alias.co checkout
$ git co master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

As a result, I haven't used git checkout in years and only use git co instead. Although I cannot override git checkout's behaviour, the closest thing I can do is redefine git co to be a bit smarter.

I decided to go with a custom command script, as I prefer readable code and did not want to cram too much unreadable code in my .gitconfig file. I went with Ruby as it is my favorite language, but you can replace it with whatever language you like.

The Ruby script shared in this Gist needs to be named git-co, be executable and somewhere in your path.

$ # Setup
$ MY_BIN="$HOME/bin"
$ mkdir -p "$MY_BIN"
$ export PATH="$MY_BIN:$PATH"
$ cp git-co "$MY_BIN"
$ chmod u+x "$MY_BIN"/git-co

$ # In an old repo, with `master` but no `main`:
$ git co main
'main' does not exist, falling back to 'master'.
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git co other
Switched to branch 'other'
$ git co master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

$ # In a newer repo, with `main` but no `master`:
$ git co master
'master' does not exist, falling back to 'main'.
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
$ git co other
Switched to branch 'other'
$ git co main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

$ # In an updated legacy repo, with both `main` and `master` branches:
$ git co master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git co main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

Limitations

Autocomplete

Unfortunately, I realized after a few minutes that this script introduces a huge drawback to my usual git co workflow: autocompletion does not work anymore!

$ # Before, with a simple git alias
$ git co [Tab]
HEAD	main	other-branch

$ # Now, with ~/bin/git-co
$ git co [Tab]
CONTRIBUTING.md	README.md ...

To fix this, we need to tell the shell (bash, zsh, etc) how git co autocompletes. Fortunately, git's completion scripts make that task fairly easy. With the very basic understanding I have of it, we just need to implement a function named _git_co and have it do something similar to what the existing _git_checkout function does.

This is the simplest thing I managed to write:

$ _git_co() { _git_checkout ; }

The result is satisfying:

$ git co [Tab]
Display all 16570 possibilities? (y or n)

$ git co mas[Tab]
master    mater

$ git co -b [Tab]
Display all 4862 possibilities? (y or n)

I really need to cleanup my branches, but you get the idea! The last one also reproduced the original git checkout autocomplete behaviour that only shows local branches when using the -b flag.

Complex arguments

Currently, the Ruby script I wrote will help only if git co was passed a single argument (assumedly a branch or reference name), because I did not want to spend time parsing the arguments passed to git co. If you happen to pass more than one argument to git co, then it will not try to be smart and may fail to send you to the proper master/main branch:

$ git co main
'main' does not exist, falling back to 'master'.
Already on 'master'
Your branch is up to date with 'origin/master'.

$ git co --quiet main
error: pathspec 'main' did not match any file(s) known to git

All in all, I'm rather satisfied with this solution, even if it's not perfect and required some fiddling. I achieved the goal I had set myself: this provides developers with a crutch for the most common frustration scenario related to the switch from master to main.

There still is a lot to explore related to this change, such as how CI and other tooling may need an update which could be tricky, and how automation could alleviate the issue, but that will be for another time!

#!/usr/bin/env ruby
class GitCo
FALLBACKS = {
"main" => "master",
"master" => "main"
}
def initialize(argv)
@argv = argv
end
def run
fall_back || forward
end
private
def fall_back
return unless fall_back?
puts "'#{original_ref}' does not exist, falling back to '#{fallback_ref}'."
checkout_with_args([fallback_ref])
end
def fall_back?
@argv.size == 1 && fallback_ref && !original_ref_exists?
end
def forward
checkout_with_args(@argv)
end
def checkout_with_args(args)
system("git", "checkout", *args)
end
def original_ref_exists?
system("git", "rev-parse", "--verify", original_ref, out: File::NULL, err: File::NULL)
end
def fallback_ref
FALLBACKS[original_ref]
end
def original_ref
@argv.first
end
end
GitCo.new(ARGV).run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment