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
$fpathorder, 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
_compvariable, 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 updatewhere the completion cache was corrupt. You can clear the cache usingzinit cclear. zinit snippetis 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 lucidbeforezinit snippetensures it’s added to the async load stack and will load after any other already-called async loads. - If
{name}.plugin.zshexists in a repo, it will be called instead of{name}.zsh, no in addition to. $ZINITis a magic variable which is a dictionary containing various internal state.._zinitis magic directly in plugins which describe how the plugin was loaded (all of the ice modifiers are )zinit uncompile --allclears out allzwcfiles and can be helpful for debugging weird plugin issuesid-asseems 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 csearchlists out all completions available via plugins. However, if a completion path is already in$fpathit 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 directoryerror,zinit cclearwill fix the issue for you. - Reload completions
autoload -Uz compinit && compinit - The
zcompdumpfile, 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
_gitandt/zsh/_git) only one of them is symlinked to~/.local/share/zinit/completionsand 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/completionsand 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 --cleanto clear our unused plugins and snippets. - Completions cannot be sourced normally, they must be pulled from
$fpathdirectories. You’ll get an error if yousource _git. - zinit does some fancy stuff around completions:
- It overloads
compdefin plugins and delays running the command untilzicompinitand related commands are run. - It can block
$fpathfrom 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
_gitand 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-functionsand installed via homebrew.
Here’s what worked for me:
rm /opt/homebrew/share/zsh/site-functions/_git 2> /dev/nullin 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-completionand 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.