Zsh git filename completion with “–git-dir=… –work-tree=…”: not a git repository

autocompletegitzsh

I track my dotfiles using this method:

  • A bare repository resides in $HOME/repos/dotfiles.
  • All my dotfiles reside in their normal location, e.g. $HOME/.vim/vimrc, not $HOME/repos/dotfiles/vimrc.
  • I run git --git-dir=$HOME/repos/dotfiles --work-tree=$HOME ... to manage things.

(Actually, I have a function g() that expands to the above command when I'm in $HOME, and to just git otherwise.)

Everything works great, except…

The problem: Zsh git filename completion doesn't work.

Example:

% pwd
/home/brian
% g status                                                                                                                                         ~
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   .vim/vimrc
        modified:   .xmonad/xmonad.hs

no changes added to commit (use "git add" and/or "git commit -a")
% g add <Tab>
Completing not a git repository

(The "Completing …" stuff is due to zstyle ':completion:*' format $'%{\e[0;31m%}Completing %B%d%b%{\e[0m%}'.)

Notably, it would not suffice to somehow tell zsh's git completion to "move to/follow" the given value of --work-tree, as if git were being called from that directory, because explicitly doing that doesn't work either:

% cd repos/dotfiles
% g status
fatal: This operation must be run in a work tree
% g add <Tab>
Completing not a git repository

The question: Is there an easy way to get extend zsh's git completion to this kind of a case?

Best Answer

Sadly, its a bug in zsh completion for git. You can find discussion on 'zsh' mailing list here.

Daniel Shahaf did provided a patch for '_git':

diff --git a/Completion/Unix/Command/_git b/Completion/Unix/Command/_git
index 518e6d198..45a0fa622 100644
--- a/Completion/Unix/Command/_git
+++ b/Completion/Unix/Command/_git
@@ -6609,20 +6609,33 @@ __git_files_relative () {
 (( $+functions[__git_files] )) ||
 __git_files () {
   local compadd_opts opts tag description gitcdup gitprefix files expl
+  local pref

   zparseopts -D -E -a compadd_opts V: J: 1 2 n f X: M: P: S: r: R: q F:
   zparseopts -D -E -a opts -- -cached -deleted -modified -others -ignored -unmerged -killed x+: --exclude+:
   tag=$1 description=$2; shift 2

-  gitcdup=$(_call_program gitcdup git rev-parse --show-cdup 2>/dev/null)
-  __git_command_successful $pipestatus || return 1
+  case $(_call_program gitinworktree git rev-parse --is-inside-work-tree 2>/dev/null) in
+    (true)
+      gitcdup=$(_call_program gitcdup git rev-parse --show-cdup 2>/dev/null)
+      __git_command_successful $pipestatus || return 1

-  gitprefix=$(_call_program gitprefix git rev-parse --show-prefix 2>/dev/null)
-  __git_command_successful $pipestatus || return 1
+      gitprefix=$(_call_program gitprefix git rev-parse --show-prefix 2>/dev/null)
+      __git_command_successful $pipestatus || return 1
+
+      local pref=$gitcdup$gitprefix$PREFIX
+      ;;
+    (false)
+      local pref=
+      ;;
+    (*)
+      # XXX what to do?
+      return 1
+      ;;
+  esac

   # TODO: --directory should probably be added to $opts when --others is given.

-  local pref=$gitcdup$gitprefix$PREFIX

   # First allow ls-files to pattern-match in case of remote repository
   files=(${(0)"$(_call_program files git ls-files -z --exclude-standard ${(q)opts} -- ${(q)${pref:+$pref\\\*}} 2>/dev/null)"})
@@ -7585,7 +7598,8 @@ _git() {
         ;;
       (option-or-argument)
         curcontext=${curcontext%:*:*}:git-$words[1]:
-       (( $+opt_args[--git-dir] )) && local -x GIT_DIR=$opt_args[--git-dir]
+        (( $+opt_args[--git-dir] )) && local -x GIT_DIR=${(e)opt_args[--git-dir]}
+        (( $+opt_args[--work-tree] )) && local -x GIT_WORK_TREE=${(e)opt_args[--work-tree]}
        if ! _call_function ret _git-$words[1]; then
          if zstyle -T :completion:$curcontext: use-fallback; then
            _default && ret=0

It applies clearly to zsh 5.4.1 but it didn't work form me,YMMV.

I'll update this answer as the issue is being worked on.

EDIT:

With the above patch it does work, but where you put add is important - it has to be at the end:

git --git-dir=$HOME/.dotfiles --work-tree=$HOME/ add

One more thing worth noticing - its somewhat slow.

Related Question