Git is designed as a system of numerous independent commands that all start with
git. Some well-known examples are
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
which searches and calls other commands starting with
git- that each do one
thing. For example, if you run
git will look for a command
git-commit and run that1.
Git searches for executables in both the
$PATH, and a path that can be shown
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
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?).
git super command will pass information to the child command via
environment variables. It will set
(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
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.
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
$ 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
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.