How to fix tab completion for complex Git aliases
Published on Monday, August 1, 2022
In an earlier post, I talked about how to print the underlying commands for your Git aliases when you run them by utilizing "complex" aliases. This can lead to a better pairing experience, but it can come with a cost when implemented improperly: broken tab completion.
A "complex" Git alias is an alias that is written in one of the following formats in a .gitconfig
file. Prefixing the alias expansion with !
tells Git to treat it as a shell command.
co = !/usr/bin/alt-checkoutco = "!f() { /usr/bin/alt-checkout; }; f"
The problem is that Git cannot infer the underlying Git command for tab completion as it is written. Thankfully, there is a little-known "trick" documented in the Git source code that allows us to specify a command to use for completion.
If you use complex aliases of form '!f() { ... }; f', you can use the null command ':' as the first command in the function body to declare the desired completion style. For example '!f() { : git commit ; ... }; f' will tell the completion to use commit completion. This also works with aliases of form "!sh -c '...'". For example, "!sh -c ': git commit ; ... '".
Knowing this, we can rewrite our aliases to fix tab completion.
co = !bash -c ': git checkout ; /usr/bin/alt-checkout'co = "!f() { : git checkout ; /usr/bin/alt-checkout; }; f"
Take note of the space around the null command both after the :
and before the ;
. These spaces are required, and tab completion will not work without them.
A new discovery, maybe?
Through a bit of trial and error, I discovered a new way to write these aliases that is, in my opinion, a little more readable. Since Git treats the entire expansion as a shell command when you use the !
prefix, you can actually declare the null command immediately after the !
and chain it with your desired command to run.
co = !: git checkout && /usr/bin/alt-checkout
Along with properly tab-completing, this format has the added benefit of being able to pass arguments directly to the underlying script without forwarding arguments via $@
.
Thanks for reading. ✌️