Skip to content

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 using zinit 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 before zinit 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 all zwc files and can be helpful for debugging weird plugin issues
  • id-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 and t/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 you source _git.
  • zinit does some fancy stuff around completions:
    • It overloads compdef in plugins and delays running the command until zicompinit and related commands are run.
    • It can block $fpath from being modified by a plugin via blockf
    • as'completion' searches for completion-like (something along the lines of **/_* ) files in a plugin and automatically symlinks them

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.