Add zsh completion

This PR adds `completion.zsh` to allow for relatively intelligent
completion of `repo` command-line arguments for all commands of the
`repo` CLI.

The `completion.zsh` can be added to your zsh completions by copying
(and renaming) the completion script to a directory that you have added
to your zsh completion path.

```
cp completion.zsh $ZSH_COMPLETION_DIRECTORY/_repo
```

You must name the file `_repo` for it to register as completion for
`repo`.

You can add a directory to your zsh completion by updating your `fpath`
variable before calling `compinit` as follows:

```
fpath=( /path/to/zsh-completions/dir $fpath )
```

Future work: Generate this file using the subcommand classes and python
reflection.

Change-Id: I6a4e785c1efcf9076bd693976ac03578836b691b
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/579561
Commit-Queue: Mike Frysinger <vapier@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Kamal Sacranie <sacranie@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
diff --git a/completion.zsh b/completion.zsh
new file mode 100644
index 0000000..507ed2b
--- /dev/null
+++ b/completion.zsh
@@ -0,0 +1,449 @@
+#compdef _repo repo
+
+# Copyright (C) 2026 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+_repo() {
+  local context state line
+  typeset -A opt_args
+
+  local -a subcommands
+  subcommands=(
+    'abandon:Abandon a development branch'
+    'branches:View current topic branches'
+    'checkout:Checkout a branch'
+    'cherry-pick:Cherry-pick a change'
+    'diff:Show changes between commit and working tree'
+    'diffmanifests:Show differences between two manifests'
+    'download:Download and find a change'
+    'forall:Run a shell command in each project'
+    'gc:Garbage collect'
+    'grep:Print lines matching a pattern'
+    'help:Display help about a command'
+    'info:Get info about the manifest'
+    'init:Initialize a repo client'
+    'list:List projects and their names'
+    'manifest:Manifest management'
+    'overview:Display overview of topic branches'
+    'prune:Prune topic branches'
+    'rebase:Rebase local branches'
+    'selfupdate:Update repo'
+    'smartsync:Update working tree to latest known good revision'
+    'stage:Stage file(s) for commit'
+    'start:Start a new branch for development'
+    'status:Show the working tree status'
+    'sync:Update working tree'
+    'upload:Upload changes to the review server'
+    'version:Display version'
+    'wipe:Wipe uncommitted changes'
+  )
+
+  local -a global_opts
+  global_opts=(
+    '(-h --help)'{-h,--help}'[Show help message]'
+    '--help-all[Show help message for all commands]'
+    '(-p --paginate)'{-p,--paginate}'[Pipe all output into less]'
+    '--no-pager[Do not pipe output into a pager]'
+    '--color=[Control color output]:color:(always never auto)'
+    '--trace[Trace git command execution]'
+    '--trace-to-stderr[Trace git command execution to stderr]'
+    '--trace-python[Trace python execution]'
+    '--time[Time execute of command]'
+    '--version[Show version]'
+    '--show-toplevel[Show top level of workspace]'
+    '--event-log=[File to log events to]:file:_files'
+    '--git-trace2-event-log=[File to log git trace2 events to]:file:_files'
+    '--submanifest-path=[Path to submanifest]:path:_files'
+  )
+
+  _arguments -C \
+    ${global_opts} \
+    '1: :->cmds' \
+    '*:: :->subcmds'
+
+  case ${state} in
+  cmds)
+    _describe -t commands 'repo command' subcommands
+    ;;
+  subcmds)
+    local cmd=${words[1]}
+    curcontext="${curcontext%:*}-${cmd}:"
+
+    local -a common_opts
+    common_opts=(
+      '(-h --help)'{-h,--help}'[Show help message]'
+      '(-v --verbose)'{-v,--verbose}'[Show verbose messages]'
+      '(-q --quiet)'{-q,--quiet}'[Show only errors]'
+      '(-j --jobs)'{-j,--jobs=}'[Number of jobs to run in parallel]:jobs:'
+      '--outer-manifest[Use outer manifest]'
+      '--no-outer-manifest[Do not use outer manifest]'
+      '--this-manifest-only[Only use this manifest]'
+      '--no-this-manifest-only[Do not only use this manifest]'
+      '--all-manifests[Use all manifests]'
+    )
+
+    case ${cmd} in
+    abandon)
+      _arguments \
+        ${common_opts} \
+        '--all[Abandon all branches]' \
+        '1: :->branch' \
+        '*: :->project'
+      ;;
+    branches)
+      _arguments \
+        ${common_opts} \
+        '*: :->project'
+      ;;
+    checkout)
+      _arguments \
+        ${common_opts} \
+        '1: :->branch' \
+        '*: :->project'
+      ;;
+    cherry-pick)
+      _arguments \
+        ${common_opts} \
+        '1: :_message "sha1"'
+      ;;
+    diff)
+      _arguments \
+        ${common_opts} \
+        '(-u --absolute)'{-u,--absolute}'[Show absolute paths]' \
+        '*: :->project'
+      ;;
+    diffmanifests)
+      _arguments \
+        '--raw[Show raw diff]' \
+        '--no-color[Do not show color]' \
+        '--pretty-format=[Pretty format]:format:' \
+        '1: :_files -g "*.xml"' \
+        '2: :_files -g "*.xml"'
+      ;;
+    download)
+      _arguments \
+        ${common_opts} \
+        '(-b --branch)'{-b,--branch=}'[One or more branches to check]:branch:' \
+        '(-c --cherry-pick)'{-c,--cherry-pick}'[Cherry-pick the change]' \
+        '(-x --record-origin)'{-x,--record-origin}'[Record origin of cherry-pick]' \
+        '(-r --revert)'{-r,--revert}'[Revert the change]' \
+        '(-f --ff-only)'{-f,--ff-only}'[Force fast-forward]' \
+        '*: :_message "change[/patchset]"'
+      ;;
+    forall)
+      _arguments \
+        ${common_opts} \
+        '(-r --regex)'{-r,--regex}'[Execute command only on projects matching regex]' \
+        '(-i --inverse-regex)'{-i,--inverse-regex}'[Execute command only on projects not matching regex]' \
+        '(-g --groups)'{-g,--groups=}'[Execute command only on projects matching groups]:groups:' \
+        '(-c --command)'{-c,--command}'[Command to execute]' \
+        '(-e --abort-on-errors)'{-e,--abort-on-errors}'[Abort on errors]' \
+        '--ignore-missing[Ignore missing projects]' \
+        '--interactive[Run interactively]' \
+        '-p[Show project headers]' \
+        '*: :->project'
+      ;;
+    gc)
+      _arguments \
+        ${common_opts} \
+        '(-n --dry-run)'{-n,--dry-run}'[Dry run]' \
+        '(-y --yes)'{-y,--yes}'[Answer yes to all prompts]' \
+        '--repack[Repack objects]'
+      ;;
+    grep)
+      _arguments \
+        ${common_opts} \
+        '--cached[Search cached files]' \
+        '(-r --revision)'{-r,--revision=}'[Search in revision]:revision:' \
+        '-e[Pattern]' \
+        '(-i --ignore-case)'{-i,--ignore-case}'[Ignore case]' \
+        '(-a --text)'{-a,--text}'[Treat all files as text]' \
+        '-I[Do not match binary files]' \
+        '(-w --word-regexp)'{-w,--word-regexp}'[Match word boundaries]' \
+        '(-v --invert-match)'{-v,--invert-match}'[Invert match]' \
+        '(-G --basic-regexp)'{-G,--basic-regexp}'[Basic regexp]' \
+        '(-E --extended-regexp)'{-E,--extended-regexp}'[Extended regexp]' \
+        '(-F --fixed-strings)'{-F,--fixed-strings}'[Fixed strings]' \
+        '--all-match[All match]' \
+        '--and[And]' \
+        '--or[Or]' \
+        '--not[Not]' \
+        '-([Open paren]' \
+        '-)[Close paren]' \
+        '-n[Line number]' \
+        '-C[Context]:lines:' \
+        '-B[Before context]:lines:' \
+        '-A[After context]:lines:' \
+        '-l[Name only]' \
+        '--name-only[Name only]' \
+        '--files-with-matches[Files with matches]' \
+        '-L[Files without match]' \
+        '--files-without-match[Files without match]' \
+        '1: :->pattern' \
+        '*: :->project'
+      ;;
+    help)
+      _arguments \
+        '(-a --all)'{-a,--all}'[Show all commands]' \
+        '--help-all[Show help message for all commands]' \
+        '1: :->help_cmds'
+      ;;
+    info)
+      _arguments \
+        ${common_opts} \
+        '(-d --diff)'{-d,--diff}'[Show diff]' \
+        '(-o --overview)'{-o,--overview}'[Show overview]' \
+        '(-c --current-branch)'{-c,--current-branch}'[Show current branch]' \
+        '--no-current-branch[Do not show current branch]' \
+        '(-l --local-only)'{-l,--local-only}'[Local only]' \
+        '*: :->project'
+      ;;
+    init)
+      _arguments \
+        '(-u --manifest-url)'{-u,--manifest-url=}'[Manifest URL]:url:' \
+        '(-b --manifest-branch)'{-b,--manifest-branch=}'[Manifest branch]:branch:' \
+        '--manifest-upstream-branch=[Manifest upstream branch]:branch:' \
+        '(-m --manifest-name)'{-m,--manifest-name=}'[Manifest name]:file:_files -g "*.xml"' \
+        '(-g --groups)'{-g,--groups=}'[Restrict manifest projects to groups]:groups:' \
+        '(-p --platform)'{-p,--platform=}'[Restrict manifest projects to platform]:platform:' \
+        '--submodules[Sync submodules]' \
+        '--standalone-manifest[Standalone manifest]' \
+        '--manifest-depth=[Manifest depth]:depth:' \
+        '(-c --current-branch)'{-c,--current-branch}'[Sync current branch only]' \
+        '--no-current-branch[Do not sync current branch only]' \
+        '--tags[Sync tags]' \
+        '--no-tags[Do not sync tags]' \
+        '--mirror[Mirror]' \
+        '--archive[Archive]' \
+        '--worktree[Worktree]' \
+        '--reference=[Reference repository]:repository:_files -/' \
+        '--dissociate[Dissociate from reference]' \
+        '--depth=[Depth]:depth:' \
+        '--partial-clone[Partial clone]' \
+        '--no-partial-clone[Do not partial clone]' \
+        '--partial-clone-exclude=[Exclude from partial clone]:projects:' \
+        '--clone-filter=[Clone filter]:filter:' \
+        '--use-superproject[Use superproject]' \
+        '--no-use-superproject[Do not use superproject]' \
+        '--clone-bundle[Use clone bundle]' \
+        '--no-clone-bundle[Do not use clone bundle]' \
+        '--git-lfs[Use git lfs]' \
+        '--no-git-lfs[Do not use git lfs]' \
+        '--repo-url=[Repo URL]:url:' \
+        '--repo-rev=[Repo revision]:revision:' \
+        '--no-repo-verify[Do not verify repo]' \
+        '--config-name[Use config name]'
+      ;;
+    list)
+      _arguments \
+        ${common_opts} \
+        '(-r --regex)'{-r,--regex}'[Filter by regex]' \
+        '(-g --groups)'{-g,--groups=}'[Filter by groups]:groups:' \
+        '(-a --all)'{-a,--all}'[Show all projects]' \
+        '(-n --name-only)'{-n,--name-only}'[Show only names]' \
+        '(-p --path-only)'{-p,--path-only}'[Show only paths]' \
+        '(-f --fullpath)'{-f,--fullpath}'[Show full paths]' \
+        '--relative-to=[Show paths relative to]:path:_files -/' \
+        '*: :->project'
+      ;;
+    manifest)
+      _arguments \
+        '(-r --revision-as-HEAD)'{-r,--revision-as-HEAD}'[Save revisions as current HEAD]' \
+        '(-m --manifest-name)'{-m,--manifest-name=}'[Manifest name]:file:_files -g "*.xml"' \
+        '--suppress-upstream-revision[Suppress upstream revision]' \
+        '--suppress-dest-branch[Suppress dest branch]' \
+        '--format=[Output format]:format:(xml json)' \
+        '--pretty[Pretty print]' \
+        '--no-local-manifests[Ignore local manifests]' \
+        '(-o --output-file)'{-o,--output-file=}'[Output file]:file:_files'
+      ;;
+    overview)
+      _arguments \
+        ${common_opts} \
+        '(-c --current-branch)'{-c,--current-branch}'[Show current branch]' \
+        '--no-current-branch[Do not show current branch]' \
+        '*: :->project'
+      ;;
+    prune)
+      _arguments \
+        ${common_opts} \
+        '*: :->project'
+      ;;
+    rebase)
+      _arguments \
+        ${common_opts} \
+        '--fail-fast[Fail fast]' \
+        '(-f --force-rebase)'{-f,--force-rebase}'[Force rebase]' \
+        '--no-ff[No fast forward]' \
+        '--autosquash[Autosquash]' \
+        '--whitespace=[Whitespace option]:option:' \
+        '--auto-stash[Auto stash]' \
+        '(-m --onto-manifest)'{-m,--onto-manifest}'[Rebase onto manifest]' \
+        '(-i --interactive)'{-i,--interactive}'[Interactive rebase]' \
+        '*: :->project'
+      ;;
+    selfupdate)
+      _arguments \
+        '--no-repo-verify[Do not verify repo]'
+      ;;
+    smartsync | sync)
+      _arguments \
+        ${common_opts} \
+        '--jobs-network=[Number of network jobs]:jobs:' \
+        '--jobs-checkout=[Number of checkout jobs]:jobs:' \
+        '(-f --force-broken)'{-f,--force-broken}'[Continue sync even if a project fails]' \
+        '--fail-fast[Fail fast]' \
+        '--force-sync[Overwrite existing git directories]' \
+        '--force-checkout[Force checkout]' \
+        '--force-remove-dirty[Force remove dirty]' \
+        '--rebase[Rebase local branches]' \
+        '(-l --local-only)'{-l,--local-only}'[Only use local files]' \
+        '--no-manifest-update[Do not update manifest]' \
+        '--nmu[Do not update manifest]' \
+        '--interleaved[Interleave output]' \
+        '--no-interleaved[Do not interleave output]' \
+        '(-n --network-only)'{-n,--network-only}'[Only fetch]' \
+        '(-d --detach)'{-d,--detach}'[Detach HEAD]' \
+        '(-c --current-branch)'{-c,--current-branch}'[Fetch only current branch]' \
+        '--no-current-branch[Do not fetch only current branch]' \
+        '(-m --manifest-name)'{-m,--manifest-name=}'[Manifest name]:file:_files -g "*.xml"' \
+        '--clone-bundle[Use clone bundle]' \
+        '--no-clone-bundle[Do not use clone bundle]' \
+        '(-u --manifest-server-username)'{-u,--manifest-server-username=}'[Username for manifest server]:username:' \
+        '(-p --manifest-server-password)'{-p,--manifest-server-password=}'[Password for manifest server]:password:' \
+        '--fetch-submodules[Fetch submodules]' \
+        '--use-superproject[Use superproject]' \
+        '--no-use-superproject[Do not use superproject]' \
+        '--tags[Sync tags]' \
+        '--no-tags[Do not sync tags]' \
+        '--optimized-fetch[Optimized fetch]' \
+        '--retry-fetches=[Retry fetches]:count:' \
+        '--prune[Prune]' \
+        '--no-prune[Do not prune]' \
+        '--auto-gc[Run auto gc]' \
+        '--no-auto-gc[Do not run auto gc]' \
+        '(-s --smart-sync)'{-s,--smart-sync}'[Smart sync]' \
+        '(-t --smart-tag)'{-t,--smart-tag=}'[Smart tag]:tag:' \
+        '--no-repo-verify[Do not verify repo]' \
+        '--no-verify[Do not verify]' \
+        '--verify[Verify]' \
+        '--ignore-hooks[Ignore hooks]' \
+        '*: :->project'
+      ;;
+    stage)
+      _arguments \
+        ${common_opts} \
+        '(-i --interactive)'{-i,--interactive}'[Interactive staging]' \
+        '*: :->project'
+      ;;
+    start)
+      _arguments \
+        ${common_opts} \
+        '--all[Start branch in all projects]' \
+        '(-r --rev --revision)'{-r,--rev=,--revision=}'[Revision]:revision:' \
+        '--head[Head]' \
+        '--HEAD[Head]' \
+        '1: :->newbranch' \
+        '*: :->project'
+      ;;
+    status)
+      _arguments \
+        ${common_opts} \
+        '(-o --orphans)'{-o,--orphans}'[Show orphans]' \
+        '*: :->project'
+      ;;
+    upload)
+      _arguments \
+        ${common_opts} \
+        '(-t --topic-branch)'{-t,--topic-branch}'[Topic branch]' \
+        '--topic=[Topic]:topic:' \
+        '--hashtag=[Hashtag]:hashtag:' \
+        '--ht=[Hashtag]:hashtag:' \
+        '--hashtag-branch[Hashtag branch]' \
+        '--htb[Hashtag branch]' \
+        '(-l --label)'{-l,--label=}'[Label]:label:' \
+        '--pd=[Patchset description]:description:' \
+        '--patchset-description=[Patchset description]:description:' \
+        '--re=[Reviewers]:reviewers:' \
+        '--reviewers=[Reviewers]:reviewers:' \
+        '--cc=[CC]:cc:' \
+        '--br=[Branch]:branch:' \
+        '--branch=[Branch]:branch:' \
+        '(-c --current-branch)'{-c,--current-branch}'[Upload current branch]' \
+        '--no-current-branch[Do not upload current branch]' \
+        '--ne[No emails]' \
+        '--no-emails[No emails]' \
+        '(-p --private)'{-p,--private}'[Private]' \
+        '(-w --wip)'{-w,--wip}'[Work in progress]' \
+        '(-r --ready)'{-r,--ready}'[Ready]' \
+        '(-o --push-option)'{-o,--push-option=}'[Push option]:option:' \
+        '(-D --destination --dest)'{-D,--destination=,--dest=}'[Destination]:destination:' \
+        '(-n --dry-run)'{-n,--dry-run}'[Dry run]' \
+        '(-y --yes)'{-y,--yes}'[Answer yes to all prompts]' \
+        '--ignore-untracked-files[Ignore untracked files]' \
+        '--no-ignore-untracked-files[Do not ignore untracked files]' \
+        '--no-cert-checks[Do not check certificates]' \
+        '--no-verify[Do not verify]' \
+        '--verify[Verify]' \
+        '--ignore-hooks[Ignore hooks]' \
+        '*: :->project'
+      ;;
+    version)
+      _arguments ${common_opts}
+      ;;
+    wipe)
+      _arguments \
+        ${common_opts} \
+        '(-f --force)'{-f,--force}'[Force wipe]' \
+        '--force-uncommitted[Force uncommitted]' \
+        '--force-shared[Force shared]' \
+        '*: :->project'
+      ;;
+    esac
+
+    # Handle states for positional arguments.
+    case ${state} in
+    branch)
+      [[ ${PREFIX} != -* ]] && _repo_branches
+      ;;
+    project)
+      [[ ${PREFIX} != -* ]] && _repo_projects
+      ;;
+    pattern)
+      _message 'pattern'
+      ;;
+    newbranch)
+      _message 'new branch name'
+      ;;
+    help_cmds)
+      _describe -t commands 'repo command' subcommands
+      ;;
+    esac
+    ;;
+  esac
+}
+
+_repo_branches() {
+  local -a branches
+  branches=(${(f)"$(_call_program branches repo branches 2>/dev/null | sed -E 's/^.*[[:space:]]([^[:space:]]+)[[:space:]]+\|.*$/\1/' | awk '{print $1}')"})
+  _describe -t branches 'branch' branches
+}
+
+_repo_projects() {
+  local -a projects
+  projects=(${(f)"$(_call_program projects repo list -n 2>/dev/null)"})
+  _describe -t projects 'project' projects
+}
+
+_repo "$@"