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

"D. Ben Knoble" <[email protected]>
Newsgroups org.kernel.vger.git
Message-ID <CALnO6CC35iuyJpKZtkEN7fGuGK7zKd_jbebyZdKSQ1pyfOBRZA@mail.gmail.com>
On Wed, Aug 19, 2026 at 9:07 AM Lutz Lengemann via GitGitGadget
<[email protected]> wrote:
>
> From: Lutz Lengemann <[email protected]>
>
> 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 <[email protected]>
> ---
>     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 [email protected] cc: Junio C Hamano [email protected]
>
> 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 <[email protected]>
>
>      @@ 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.

>      ++                 *)
>      ++                         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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.