2019-07-29 Git commands as separate executable files

Today we are going to talk about git commands and how they are implemented. Don’t worry, I won’t get too technical – if only I have not studied Git sources (I do not speak C well enough for that anyway). It is enough for us to read the manpage of the git command itself (which I did, along with some experimentation).

Try saying git --exec-path in the terminal. On my system, the answer is /usr/lib/git-core. Let’s see what’s there – ls /usr/lib/git-core says this:

git
git-add
git-add--interactive
git-am
git-annotate
git-apply
git-archimport
git-archive
git-bisect
git-bisect--helper
git-blame
git-branch
git-bundle
git-cat-file
git-check-attr
git-check-ignore
git-check-mailmap
git-checkout
git-checkout-index
git-check-ref-format
git-cherry
git-cherry-pick
git-citool
git-clean
git-clone
git-column
git-commit
git-commit-graph
git-commit-tree
git-config
git-count-objects
git-credential
git-credential-cache
git-credential-cache--daemon
git-credential-gnome-keyring
git-credential-libsecret
git-credential-store
git-cvsexportcommit
git-cvsimport
git-cvsserver
git-daemon
git-describe
git-diff
git-diff-files
git-diff-index
git-difftool
git-difftool--helper
git-diff-tree
git-fast-export
git-fast-import
git-fetch
git-fetch-pack
git-filter-branch
git-fmt-merge-msg
git-for-each-ref
git-format-patch
git-fsck
git-fsck-objects
git-gc
git-get-tar-commit-id
git-grep
git-gui
git-gui--askpass
git-hash-object
git-help
git-http-backend
git-http-fetch
git-http-push
git-imap-send
git-index-pack
git-init
git-init-db
git-instaweb
git-interpret-trailers
git-legacy-stash
git-log
git-ls-files
git-ls-remote
git-ls-tree
git-mailinfo
git-mailsplit
git-merge
git-merge-base
git-merge-file
git-merge-index
git-merge-octopus
git-merge-one-file
git-merge-ours
git-merge-recursive
git-merge-resolve
git-merge-subtree
git-mergetool
git-mergetool--lib
git-merge-tree
git-mktag
git-mktree
git-multi-pack-index
git-mv
git-mw
git-name-rev
git-notes
git-p4
git-pack-objects
git-pack-redundant
git-pack-refs
git-parse-remote
git-patch-id
git-prune
git-prune-packed
git-pull
git-push
git-quiltimport
git-range-diff
git-read-tree
git-rebase
git-rebase--am
git-rebase--common
git-rebase--preserve-merges
git-receive-pack
git-reflog
git-remote
git-remote-ext
git-remote-fd
git-remote-ftp
git-remote-ftps
git-remote-http
git-remote-https
git-remote-mediawiki
git-remote-testsvn
git-repack
git-replace
git-request-pull
git-rerere
git-reset
git-revert
git-rev-list
git-rev-parse
git-rm
git-send-email
git-send-pack
git-shell
git-sh-i18n
git-sh-i18n--envsubst
git-shortlog
git-show
git-show-branch
git-show-index
git-show-ref
git-sh-setup
git-stage
git-stash
git-status
git-stripspace
git-submodule
git-submodule--helper
git-subtree
git-svn
git-symbolic-ref
git-tag
git-unpack-file
git-unpack-objects
git-update-index
git-update-ref
git-update-server-info
git-upload-archive
git-upload-pack
git-var
git-verify-commit
git-verify-pack
git-verify-tag
git-web--browse
git-whatchanged
git-worktree
git-write-tree
mergetools

Wow. Just wow. This means that all git commands are in fact separate executables, put in this directory. Why is that important?

There are several reasons. One is that it is now clear why – to get help on committing – we need to say man git-commit – we just want to get the manual page for the git-commit command, which is invoked under the hood when we say git commit.

Another one is that we can influence the process during which Git somehow translates git commit to git-commit by using Git aliases, but we can also do another thing (pretty useless, but cool):

echo "#!/bin/sh" > ~/bin/git-pwd
echo "pwd" >> ~/bin/git-pwd
chmod +x ~/bin/git-pwd

We now have a “Git command” git pwd, which just displays the current directory.

Another thing we can do is this (assuming GNU coreutils):

$ find . -maxdepth 1 -executable -type f -exec file \{\} \; | cut -d: -f2 | cut -c2- | cut -d, -f1 | sort -u
ELF 64-bit LSB pie executable
Perl script text executable
POSIX shell script
Python script

As you can see, Git is not written entirely in C – actually, various commands use various languages, and we have shell scripts, Perl and Python code here!

And now for the most important takeaway from this post. We can now better understand the distinction between git options and git command options. For instance, Git has the (very useful in scripts) -C option (which peforms a cd before executing the command proper), which is completely different from e.g. the -C option for git commit (which reuses a message of another commit).

By the way, you can probably guess now what git -C /tmp pwd will display, right?

But wait, there’s more! What would happen if we defined an executable script called git-commit in ~/bin? Well, nothing. How do I know that? Remember the git --exec-path command? In case you are wondering how exactly Git finds the script to run, try putting this:

#!/bin/sh
echo $PATH

somewhere in your $PATH as, say, git-show-path, and say git show-path in your terminal. See? Nothing magical here, just the shell.

To sum it up: I’m not sure how useful all this is (maybe apart from git -C ..., which is really handy in scripts), but it’s definitely interesting to know. Let me know if you find some actual use for all this knowledge!

CategoryEnglish, CategoryBlog, CategoryGit