Writing your own Git Scripts

ยท 858 words ยท 5 minute read

Git is designed as a system of numerous independent commands that all start with git. Some well-known examples are git commit, git rebase, git merge etc. Have you ever wondered how you can extend the git suite with your own?

Git’s command system ๐Ÿ”—

Originally, git’s architecture is based on the unix philosophy: do one thing and do it well. This might not be obvious if you look at the multitude of git commands and options, but if you have ever looked at the implementation, you will see that git in fact is structured as one “super command” called git, which searches and calls other commands starting with git- that each do one thing. For example, if you run git commit, git will look for a command called git-commit and run that1.

Git searches for executables in both the $PATH, and a path that can be shown with git --exec-path. E.g.

$ git --exec-path

$ ls $(git --exec-path)
git                           git-merge-ours
git-add                       git-merge-recursive
git-add--interactive          git-merge-resolve
git-am                        git-merge-subtree
git-annotate                  git-mergetool
git-apply                     git-mergetool--lib
git-archive                   git-merge-tree
git-bisect                    git-mktag
git-bisect--helper            git-mktree
git-blame                     git-multi-pack-index
git-branch                    git-mv
git-commit                    git-reflog

The commands can be implemented in any language, as long as the git command is executable. If you browse the commands, you will see a mix of shell scripts, perl scripts, regular ELF executables, and a bunch of symlinks back to git, which represent special “builtin” commands that are implemented directly in git.

Adding your own command ๐Ÿ”—

Implementing your own git command is as simple as prefixing it with git- and putting it where git can find it! Here is an example git-hello that will simply print a message.

$ cat > ~/bin/git-hello <<EOF
echo hello, world

$ chmod +x ~/bin/git-hello

$ git hello
hello, world

While that is technically all you need to add a git command, you will usually want to interact with git itself (otherwise, why would it be called by git?). The git super command will pass information to the child command via environment variables. It will set GIT_EXEC_PATH and GIT_CONFIG_PARAMETERS (if configuration parameters have been passed to git), and you can use those in your own command.

For shell scripts, git has a helper git-sh-setup “scriplet”, which you can source. It sets some environment variables and provides some helper functions related to git, which are commonly useful when needing to interact with git.

Here is an example of how to use it:

$ cat > ~/bin/git-check <<EOF
# This script checks if it is being called from a place that 'has' a git
# directory, and prints it.

# source the helper scriptlet
. "$(git --exec-path)/git-sh-setup"

# GIT_DIR is one of the things set up by git-sh-setup
echo "git dir: $GIT_DIR"

$ chmod +x ~/bin/git-check

$ git check
fatal: not a git repository (or any of the parent directories): .git

$ git init test

$ git -C test git check
git dir is: $HOME/test/.git

$ cd test && git check
git dir is: $HOME/test/.git

The helper scriplet has many more useful features, including integration with help dialogues, which are all documented in its man page.

Let’s look at one more involved example.

git-wip ๐Ÿ”—

This is a script which will display the latest modified branches that match a certain name, along with some information on oldest and newest commits on the branch. This is useful if you forget branch names because you have many going on concurrently, but you prefix all your branches with a common prefix.


# git-sh-setup: USAGE and LONG_USAGE will be displayed if `-h` is passed as an
# argument
LONG_USAGE="list most recently used branches that start with <pattern>"

# git-sh-setup integration: source the helper
. "$(git --exec-path)/git-sh-setup"

# you can change the value of how many entries to display in the `.gitconfig`
# file, under section [wip], or by passing `-c wip.length=??` as a top-level
# argument to git
length=$(git config --get --int --default=5 wip.length)

branches=$(git branch --sort=committerdate --list "$1*" --no-merged master --format='%(refname:short)'| tail -n "$length" )
for branch in $branches; do
    echo -e "\033[1m$branch\033[0m"
    echo "first: " "$(git log master.."$branch" --oneline | tail -1)"
    echo "last:  " "$(git log -1 "$branch" --oneline)"
    echo ""

example usage:

$ git -c wip.length=3 wip jo
first:  f20f8a0 configparse: factor out builder
last:   5c6d070 configparse: rename builder

first:  c25fb23 Add configuration parsing module
last:   e18d151 configparse: extract docs from doc comments

first:  6feb834 Refactor annotation-based API
last:   6feb834 Refactor annotation-based API

Closing ๐Ÿ”—

Git’s pattern of using a super command with independently callable sub commands is an elegant design: it is a composable architecture where concerns can be well separated, yet where the user experience still feels coherent. Combining this with a helper scriplet that allows re-including common functionality via sourcing in scripts makes it very extensible too.

Nothing about this pattern is specific to git however, and any command line tool doing more than one action can take inspiration from it.

  1. Have a look at the section “Plumbing and Porcelain” of the ProGit book, if you are interested in what the various commands actually do. In this article, we’re only focusing on the command machinery, not what git actually does. ↩︎

comments powered by Disqus