git-git: The World's Smallest Git Plugin

One of the things I do almost every day at my command line is type:

$ git git whatever

It’s easy to do.

Sometimes I’ll start typing a Git command, but need to look up how to complete it, and when I return to the terminal I’ve forgotten I started typing it and –

$ git git rebase
git: 'git' is not a git command. See 'git --help'.

Did you mean this?

Or sometimes I will be inside a gitsh shell – where the git prefix is implied – but still type git out of habit1. Oops!

I’ve been putting up with this for years, grumbling to myself, “No, I did not mean init…” and a few weeks ago decided to do something about it.

The solution turned out to be hiding in the error message I had read so many times: 'git' is not a git command. What if it was a git command?

Git Commands

What actually happens when you run git status? For a long time, I imagined the git command looked something like this:

subcommand = ARGUMENTS.first
arguments =

case subcommand
when "status": git_status(arguments)
when "commit": git_commit(arguments)
# ...
  abort("'#{subcommand}' is not a git command")

But Git actually takes a very different approach, motivated by the desire to make extensions feel natural to use and easy to implement.

Each Git command is implemented as a standalone command named by convention git-COMMAND2. This includes builtin commands like git status. You can see all of them by looking in the libexec/git-core directory included with your Git installation. The location of this directory varies between different operating systems and installations, but you might find it in /usr/libexec, /usr/local/libexec, or (if you’ve installed Git via Homebrew) /usr/local/Cellar/git/VERSION/libexec, for example:

$ ls /usr/local/Cellar/git/2.9.0/libexec/git-core

# ...
# ...
# ...
# ... 166 commands in total

So the git command actually looks more like this:

subcommand = ARGUMENTS.first
arguments =

if command_exists?("git-#{subcommand}")
  exec("git-#{subcommand}", arguments)
  abort("'#{subcommand}' is not a git command")

When Git checks if the command exists, it looks in the libexec directory. But it also searches for commands on your shell’s path, which is how third-party plugins can extend git’s behavior.


Now that we know how git finds commands, we can implement our new git subcommand. Based on the conventions outlined above, that means we need a new command called git-git on our path.

If you don’t already have a place you store your own terminal commands, a good convention is in $HOME/bin. Create the directory with mkdir -p "$HOME/bin", and add export PATH="$HOME/bin:$PATH" to your bash or zsh config so you can run commands there without typing their full path.

Let’s create the git-git command and make it executable:

$ touch "$HOME/bin/git-git"
$ chmod +x "$HOME/bin/git-git"

If we run git git status we no longer see an error! But it also doesn’t do anything yet, so let’s implement the script.

When we run git git status, our git-git command is run with the single argument status. So to run the command we intended, we can re-execute git with the arguments to the script. Here’s what git-git looks like:

git "$@"

"$@" represents all of the arguments passed to the script3.

What happens now if we run git git status?

$ git git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean

Success! It even works if we run git git git git status.

We had to learn a good deal about how Git is implemented to get there, but in the end it only took a single line of scripting to alleviate a daily irritation and extend git with custom behavior!

Check out the git-git repository on GitHub to see the final script and get instructions to install the extension for your own use.

Thanks to Gabe Berke-Williams, Adarsh Pandit, and Rachael Berecka for reading drafts of this.

  1. George Brocklehurst, the creator of gitsh, pointed out that gitsh can be configured to autocorrect this mistake, which is a nice alternative. 

  2. This is a bit of a simplification. A number of core commands, like git-status and git-show are in fact the same executable as git. They are (hard) links to the same command, and Git first checks $0 (the program’s name, like git-status) for builtin commands before looking for commands on the path. 

  3. It’s important to use "$@", instead of $* or other argument features of bash, because it’s the only one that will pass the arguments intact. For example, if you ran git git add "filename with spaces", only "$@" will correctly maintain the quotes around the filename.