Git Completions & Tooling on the Command Line
Tags: productivity, shell • Categories: Productivity
I enjoy tuning my terminal environment. I’ve recently learned tmux, switched to zsh, and constantly incrementally improve my personal dotfiles.
I’ve put together a stack of tools for working with git on the command line. I find this much faster than working in a GUI. Here’s what I use:
- git-fuzzy for generating commits (git patch is especially useful here)
- forgit for switching branches, viewing logs, stash list, git fixup, etc
- git (aliased as
g
) for misc git commands (like cherry-pick, etc) with a handful of config customization - custom functions and aliases for various shortcuts, including interacting with the gh-cli command
However, one piece of the puzzle for me wasn’t working properly: tab completions on the core git command. I went down a rabbit hole to understand zsh and zinit. Here’s what I learned!
Zsh Completions
zsh completions on their own are pretty easy to understand. Here’s a quick walkthrough.
- Homebrew installs completions in
/opt/homebrew/share/zsh/site-functions/
- Homebrew’s completion directory is top in the
$fpath
order, which is the order completions are loaded. If a completion exists twice, the one found first is used. - You can see the full completion mapping in
~/.zcompdump
- There’s a massive list of builtin command completions here
- You can uninstall completions within a shell session by removing them from the
_comp
variable, or by removing the completion file in$fpath
- You can find a completion’s location using
fd '_git$' $fpath
- By default, zsh will build completions for aliases. Amazing.
Zinit Package/Plugin Management
zinit
is a very weird piece of software. There’s no --help
, the syntax is very strange and unintuitive, and the documentation is hard to read and spread across a github readme and a nicer-looking-but-more-limited docs site, and some functions are just completely undocumented (like zicompdef
). If you feel like you aren’t "getting it" when using zinit, don’t feel bad.
ChatGPT is super helpful in understanding how zinit works. It’s great for poorly documented CLI tools.
But, it’s the best plugin manager out there. So it’s best to understand it a bit:
- ZInit plugins, cache, etc is stored in
~/.local/share/zinit
. You can nuke this entire folder and zinit will reload everything for you next time the terminal is opened. - Installing zinit with brew may cause you issues due to folder structure assumptions across the zinit ecosystem. Better to install via a script in your zshrc.
- I ran into an issue after running a
zinit update
where the completion cache was corrupt. You can clear the cache usingzinit cclear
. zinit snippet
is the right way to load a single-file plugin which isn’t in a repo. Here’s an example which loads the github-copilot-cliGitHub- Adding
zinit ice wait lucid
beforezinit snippet
ensures it’s added to the async load stack and will load after any other already-called async loads. - If
{name}.plugin.zsh
exists in a repo, it will be called instead of{name}.zsh
, no in addition to. $ZINIT
is a magic variable which is a dictionary containing various internal state.._zinit
is magic directly in plugins which describe how the plugin was loaded (all of the ice modifiers are )zinit uncompile --all
clears out allzwc
files and can be helpful for debugging weird plugin issuesid-as
seems to rename the plugin directory completely (if you are not pulling a snippet)
Zinit Completions
zinit has a lot of tooling to manage completions in plugins. It’s complex, but once you understand it, it’s really powerful.
zinit creinstall -q .
installs completions that exist in a specific directory. You can also install via plugin name.zinit csearch
lists out all completions available via plugins. However, if a completion path is already in$fpath
it will not report this as installed (this occurs with zsh-completion-generator)- zinit completions are installed in
~/.local/share/zinit/completions/
- If you are running into a
compinit:527: no such file or directory
error,zinit cclear
will fix the issue for you. - Reload completions
autoload -Uz compinit && compinit
- The
zcompdump
file, which stores completion cache, could be located in~/.zcompdump
atinit"zicompinit; zicdreplay"
only needs to be done once per zinit run and sets up all of the completions.- If there are multiple files with the same name in a plugin’s directory structure (say a completion
_git
andt/zsh/_git
) only one of them is symlinked to~/.local/share/zinit/completions
and you cannot determine which one. This is dangerous! - Completions in a plugin that are a symlink seem to break: the original linked file name is used.
- Completions discovered by zinit on installation are symlinked in
~/.local/share/zinit/completions
and not relinked if you manually remove the plugin. - Completions are not removed automatically when a plugin is removed. You have to run
zinit cclear
. - Plugins are not removed if not referenced, and their linked completions are not removed. Removing a plugin from your zinit plugins config does not remove it completely! Run
zinit delete --clean
to clear our unused plugins and snippets. - Completions cannot be sourced normally, they must be pulled from
$fpath
directories. You’ll get an error if yousource _git
. - zinit does some fancy stuff around completions:
- It overloads
compdef
in plugins and delays running the command untilzicompinit
and related commands are run. - It can block
$fpath
from being modified by a plugin viablockf
as'completion'
searches for completion-like (something along the lines of**/_*
) files in a plugin and automatically symlinks them
- It overloads
Git Completion
Finally, we can fix my git completion problem.
There are a couple of different options for git completion:
- OMZ gitfast. This doesn’t work with zinit since (a) OMZ plugins are loaded via snippets which assume a single file and (b) the plugin needs both
_git
and the bash completion functions. - git-completions fork. This did not work for me and did not provide detailed completion descriptions or pretty-printed completion groups with zsh-fzf. However, this does look the most complete; I think the repo is just in a weird state right now.
- zsh builtin completion Located in
/opt/homebrew/Cellar/zsh/5.9/share/zsh/functions
- git builtin completion. Located in
/opt/homebrew/share/zsh/site-functions
and installed via homebrew.
Here’s what worked for me:
rm /opt/homebrew/share/zsh/site-functions/_git 2> /dev/null
in my zshrc to remove git-installed completions.- Omit gitfast and the git-completions fork. I was able to get git-completions loaded properly using
blockf ver"zinit-fixed" as"completion" nocompile mv'git-completion.zsh -> _git' iloveitaly/git-completion
and will use this if the repo gets into a better state. - Load forgit completions using
atinit'zicompinit' atpull'zinit creinstall .' src'completions/git-forgit.zsh' wfxr/forgit
- Load git-fuzzy using
as"program" pick"bin/git-fuzzy" bigH/git-fuzzy
Open Questions
There were some bits about the completion system which I couldn’t figure out:
- Somehow, and I couldn’t figure out, aliases are automatically given the same completion as their underlying commands.
- I still don’t understand exactly what mechanism in zsh sources completions and how they determine they are completions and not normal functions.