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
/usr/lib/git-core
$ 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
#!/bin/sh
echo hello, world
EOF
$ 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
#!/bin/sh
# 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"
EOF
$ 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.
#!/bin/bash
# git-sh-setup: USAGE and LONG_USAGE will be displayed if `-h` is passed as an
# argument
USAGE="<pattern>"
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 ""
done
example usage:
$ git -c wip.length=3 wip jo
jo/branch1
first: f20f8a0 configparse: factor out builder
last: 5c6d070 configparse: rename builder
jo/branch2
first: c25fb23 Add configuration parsing module
last: e18d151 configparse: extract docs from doc comments
jo/branch3
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.
-
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. ↩︎