Thread (15 messages) flat view 15 messages, 4 authors, 9d ago

Re: [PATCH v2] completion: zsh: support completion after "git -C <path>"

From: D. Ben Knoble <hidden>
Date: 2026-08-20 12:28:45

On Wed, Aug 19, 2026 at 9:07 AM Lutz Lengemann via GitGitGadget
[off-list ref] wrote:
From: Lutz Lengemann <redacted>

The zsh completion wrapper does not handle the global -C option, so

        git -C <path> <command> <TAB>

offers nothing.  -C is not part of the _arguments specification, and the
wrapper hard-codes __git_cmd_idx=1, i.e. it assumes that the command is
the first argument, so the bash helpers look at the wrong word.  The
latter is not specific to -C; the assumption breaks after any global
option, e.g. "git -p checkout <TAB>" does not complete branch names.

Add -C to the specification, and find the command by skipping over the
global options and, where they take one, their arguments, as __git_main
in git-completion.bash does.  The index is one less than zsh's, as the
helpers count the words from zero.  Collect the paths given to -C into
__git_C_args, or else the helpers run git in the current directory and
fail to resolve the aliases and refs of the repository the command runs
in.

The argument of a -C is still completed without regard for the -C
options before it, i.e. "git -C dir -C <TAB>" offers the directories in
".", not the ones in "dir".

Signed-off-by: Lutz Lengemann <redacted>
---
    completion: zsh: support completion after "git -C "

     * The command is now located by walking the global options in front of
       it, mirroring the loop at the beginning of __git_main in
       git-completion.bash, instead of skipping only leading -C options.
       This also fixes argument completion after other global options, e.g.
       git -p checkout <TAB>.
     * The log message uses the present tense for the pre-image and notes
       that the argument of a -C is completed without regard for the -C
       options before it.

    cc: Ben Knoble ben.knoble@gmail.com cc: Junio C Hamano gitster@pobox.com

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2155%2Fmobilutz%2Fzsh-complete-global-C-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2155/mobilutz/zsh-complete-global-C-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2155

Range-diff vs v1:

 1:  9739cde6fc ! 1:  9984228f1f completion: zsh: support completion after "git -C <path>"
     @@ Metadata
[snip]
          Signed-off-by: Lutz Lengemann [off-list ref]

     @@ contrib/completion/git-completion.zsh: __git_zsh_main ()
                '(- :)--version[prints the git suite version]' \
                '--exec-path=-[path to where your core git programs are installed]:: :_directories' \
      @@ contrib/completion/git-completion.zsh: __git_zsh_main ()
     +          done
                ;;
        (arg)
     -          local command="${words[1]}" __git_dir __git_cmd_idx=1
     +-         local command="${words[1]}" __git_dir __git_cmd_idx=1
     ++         local command="${words[1]}" __git_dir __git_cmd_idx
Ok, this matches what the message describes about __git_cmd_idx not
being able to assume=1; it's different in this version because we are
a bit more sophisticated in our parsing.
      +         local -a __git_C_args
      +         local -i i=2
      +
     -+         while [[ ${orig_words[i]} == -C ]]; do
     -+                 __git_C_args+=(-C ${orig_words[i+1]})
     -+                 (( __git_cmd_idx += 2 ))
     -+                 (( i += 2 ))
     ++         while (( i <= $#orig_words )); do
     ++                 case ${orig_words[i]} in
     ++                 -C)
     ++                         __git_C_args+=(-C ${orig_words[i+1]})
     ++                         (( i++ ))
At first I thought "should that be i+=2?"; then I saw the
unconditional i++ later. Reasonable, though I'm not sure what happens
if we walk off the end of the array here: If i=#orig_words, then
__git_C_args has (-C) and i becomes #orig_words+2; later,
__git_cmd_idx becomes #orig_words+1, which is empty. I'll keep that in
mind when looking at how we handle those variables.

…Ok, those are handled in the Bash completion. AFAICT, they don't do
anything special when the dir is missing either. A bit strange, but
not something this patch needs to solve, I suppose. __git_cmd_idx is
used many places, as we would imagine, and I didn't look carefully at
what happens when it indexes an empty spot (but it looks to mostly be
used in comparisons where that would just go falsy, or in arithmetic I
haven't really checked at all).

(I also haven't thought carefully about the difference between Zsh's
1-based indexing and Bash's 0-based, so I'm not sure if there's an
issue lurking there.)
     ++                         ;;
     ++                 -c|--git-dir|--work-tree|--namespace)
     ++                         (( i++ ))
     ++                         ;;
     ++                 -*)
     ++                         ;;
Yep, unlike Bash (which requires at least one command in the "list"
part between a pattern and the terminator), Zsh accepts empty actions
here.
quoted hunk ↗ jump to hunk
     ++                 *)
     ++                         break
     ++                         ;;
     ++                 esac
     ++                 (( i++ ))
      +         done
     ++
     ++         __git_cmd_idx=$(( i - 1 ))

                if (( $+opt_args[--bare] )); then
                        __git_dir='.'


 contrib/completion/git-completion.zsh | 25 ++++++++++++++++++++++++-
 1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
index c32186a977..d5c526665b 100644
--- a/contrib/completion/git-completion.zsh
+++ b/contrib/completion/git-completion.zsh
@@ -227,6 +227,7 @@ __git_zsh_main ()
                '(-p --paginate --no-pager)'{-p,--paginate}'[pipe all output into ''less'']' \
                '(-p --paginate)--no-pager[do not pipe git output into a pager]' \
                '--git-dir=-[set the path to the repository]: :_directories' \
+               '*-C[run as if git was started in <path>]: :_directories' \
At first I wasn't sure about the blank description (space between 2
colons) of the argument to -C, but I see that _directories
automatically describes the completed thing as "directory," so that's
fine.

Overall, if this version works, I think I'm happy with it. Confirming
the index math works out between the 2 shells might be a useful
exercise, but /shrug.

-- 
D. Ben Knoble
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help