Git Logo by Jason Long is licensed under the CC BY 3.0 License

Git aliases gotchas


3 - 4 minutes read, 811 words
Categories: version control systems
Keywords: git version control systems

One of the many nice things about git is the possibility to create aliases.

Aliases can simply be an alternate name for a command, like

[alias]
	d = diff

This way, one can write git d instead of git diff, but with aliases, it is also possible to set default arguments, like

[alias]
	d = diff --ignore-all-space

With this alias, writing git d is equivalent to writing git diff --ignore-all-space.

In both cases, other parameters are passed as is. For example git d --staged file1.txt file2.txt would be equivalent to git diff --ignore-all-space --staged file1.txt file2.txt, ie further arguments are not dropped.

But with aliases it is also possible to execute external commands, git included. In this case, the alias needs to be prefixed with an exclamation mark and will be treated as a shell command. An example would be

[alias]
	start = !git init && git commit --allow-empty -m '🎉 First commit'

With git start one can create a new repository with an empty initial commit. This is useful as the first commit does not have any parent, thus working with it (for example while rebasing) is more complicated.

Arguments are simply forwarded, thus git start --edit executes git init && git commit --allow-empty -m '🎉 First commit' --edit.

It seems thus that aliases are equivalent to writing the defined alias in the command line, but there are a couple of differences (documented of course) that are worth knowing.

As shell commands are, as the name implies, executed in a shell, it is possible to create more complex commands without resorting to external programs and scripts and manipulate command-line arguments directly.

This is normally done by defining a function and invoking it directly, for example:

[alias]
	log-change-stat= "!f(){ git log --no-merges --pretty=format:@%h --shortstat \"$@\" | tr '\n' ' ' | tr @ '\n' | awk NF | awk '{if(!/\\yinsertion.*\\y/)gsub(/changed,/, \"changed, 0 insertions(+),\");print }' | awk '{if(!/\\ydeletion.*\\y/)gsub(/insertions\\(\\+\\)/, \"insertions(+), 0 deletions(-)\");print }' | less --quit-if-one-screen --no-init; }; f"

git log-change-stat will create an output similar to git log --shortstat, but every log entry will be on one line, and , and it always shows insertions and deletions. It can also be used for showing the statistics in a given range, like git log-change-stat HEAD~3..HEAD.

GIT_PREFIX

Shell commands/aliases prefixed with ! are always executed from the top-level directory of the repository. This is generally not the current directory. This is problematic when the alias depends on the current directory or can accept relative paths to files.

Given an alias like

[alias]
	foo = "!f(){ git diff \"$@\"| do-something-with-the-output; }; f"

git foo will work as expected if called from the root directory of the repository. When invoked from another location and with a file as an argument, it generally won’t.

Thanks to GIT_PREFIX, if set, it is possible to restore the working directory as expected: cd -- "${GIT_PREFIX:-.}"

If GIT_PREFIX is not set, the command expends to cd -- ., which is a (hopefully successful) no-op. If set, it expands to cd -- "$GIT_PREFIX".

Notice the double-dash; it ensures that even with a path that could be named like cd options, like --help or -L are treated as paths.

Long story short, if a command depends on the current directory or might accept a path of a file under revision control, better use GIT_PREFIX appropriately:

[alias]
	foo = "!f(){ cd -- \"${GIT_PREFIX:-.}\" && git diff \"$@\"| do-something-with-the-output; }; f"

GIT_DIR

Shell commands are not only executed from the top-level directory of the repository, in a certain configuration they even ignore if a command refers to another repository!

Suppose that there is one alias that will do some operation on another repository. This might be useful when working on multiple projects, and if one project depends on another.

[alias]
	status-of-other-project = !git -C /path/to/other/git/project status

This command will work correctly unless the current directory is in a git worktree or git submodule.

This is not a but as I believed at the beginning a bug, but the specified behavior, even if I’m still unsure how this is a useful feature.

The reason is that in the case of git worktree and git submodule, git defines GIT_DIR; before executing the shell commands. The effect is that git invoked from the shell command will use as git directory what’s in $GIT_DIR and ignore the current directory and other command-line options (like -C).

For having a consistent behavior (ie alias working the same, with a "normal" repository, a submodule, or a worktree), when one command depends on another repository, you should unset GIT_DIR;.

Thus one should write

[alias]
	status-of-other-project = "!f(){ unset GIT_DIR; git -C /path/to/other/git/project status \"$@\"; }; f"

Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me here.