"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Mikhail Pchelin <misha@freebsd.org>
Subject:
[PATCH] zsh tab completion
To:
gameoftrees@openbsd.org
Date:
Fri, 24 Jul 2026 20:04:19 +0300

Download raw body.

Thread
One thing which was bothering me while using got is absence of tab
completion, like with git (remotes, branches, revert/stage/unstage smart
file paths, flags and commands help, etc).

Zsh completion file is inlined, the patch puts it into new contrib/
folder, brief instructions are on the top.

Thoughts?

diff refs/heads/main eaf445109b5ff00a17b47362c62aa4e2f703cc79
commit - b3611acb7c736d2f45369e1eea2806450e9d11b2
commit + eaf445109b5ff00a17b47362c62aa4e2f703cc79
blob - /dev/null
blob + b39618582cba0fbc9a7c405323eb29b3a96e6649 (mode 644)
--- /dev/null
+++ contrib/completion/zsh/_got
@@ -0,0 +1,660 @@
+#compdef got
+
+# Zsh tab completion for got 0.128-current, for 'got <tab>' we complete only
+# aliases, to not mess with 'got up<tab>' which matches both 'up' alias and
+# 'update', probably most people use only aliases in their interactive sessions
+# anyway, but if the user has typed 'got update -<tab>' we will catch that and
+# help them too.
+#
+# To use this file, copy it to ~/.zsh/completions/ and add to .zshrc:
+# fpath=(~/.zsh/completions/ $fpath)
+# autoload -U compinit && compinit
+#
+# To make it globally available place it (on OpenBSD) into:
+# /usr/local/share/zsh/site-functions
+
+# TODO: this file can be improved with options excluding logic, for example
+# '(-t)-T[send all tags from the local repository]' in _got_send can be used to
+# make zsh remove '-t' from completion if -T is used, but let's keep it simple
+# for now
+
+_got_branches() {
+	local branches
+
+        branches=$(got br -l 2>/dev/null | awk '{print $(NF-1)}' | sed 's/:$//')
+        reply=(${(f)branches})
+}
+
+_got_branch_name() {
+	local -a candidates
+	local name
+
+	_got_branches
+	for name in $reply; do
+		candidates+=("$name")
+	done
+
+	compadd -a candidates
+}
+
+_got_worktree_repository_path() {
+	local dir=$PWD
+
+	reply=()
+	while true; do
+		if [[ -f $dir/.got/repository ]]; then
+			reply=("$(<$dir/.got/repository)")
+			return
+		fi
+		[[ $dir == / ]] && return
+		dir=${dir:h}
+	done
+}
+
+_got_remotes() {
+	local repo remotes
+
+	reply=()
+	_got_worktree_repository_path
+	repo=$reply[1]
+	reply=()
+	if [[ -z $repo || ! -f $repo/got.conf ]]; then
+		return
+	fi
+
+        remotes=$(grep -E '^remote' "$repo/got.conf" | awk -F'"' '{print $2}')
+        reply=(${(f)remotes})
+}
+
+_got_remote_name() {
+	local -a candidates
+	local name
+
+	_got_remotes
+	for name in $reply; do
+		candidates+=("$name")
+	done
+
+	compadd -a candidates
+}
+
+_got_ssh_config_hosts() {
+	local hosts
+	local -a candidates
+
+	[[ -r ~/.ssh/config ]] || return
+
+	# awk loop is needed for "Host host1 host2 host3" syntax, grep to exclude "Host pattern* pattern?"
+	hosts=$(grep -iE '^[[:space:]]*host(name)?[[:space:]]' ~/.ssh/config | awk '{for (i=2;i<=NF;i++) print $i}' | grep -vE '[*?]')
+	candidates=(${(f)hosts})
+	compadd -a candidates
+}
+
+_got_tags() {
+	local tags
+
+	tags=$(got ref -l refs/tags 2>/dev/null | awk '{print $1}' | sed 's#^refs/tags/##; s/:$//')
+	reply=(${(f)tags})
+}
+
+_got_tag_name() {
+	local -a candidates
+	local name
+
+	_got_tags
+	for name in $reply; do
+		candidates+=("$name")
+	done
+
+	compadd -a candidates
+}
+
+_got_refs() {
+	local refs
+
+	refs=$(got ref -l 2>/dev/null | awk '{print $1}' | sed 's/:$//')
+	reply=(${(f)refs})
+}
+
+_got_ref_name() {
+	local -a candidates
+	local name
+
+	_got_refs
+	for name in $reply; do
+		candidates+=("$name")
+	done
+
+	compadd -a candidates
+}
+
+_got_commit_or_keyword() {
+	local -a candidates
+	local name
+
+	candidates=(
+		':base'
+		':head'
+	)
+
+	_got_branches
+	for name in $reply; do
+		candidates+=("$name")
+	done
+
+	compadd -a candidates
+}
+
+_got_status_codes() {
+	_values -s '' 'status code' \
+		'M[modified file]' \
+		'A[file scheduled for addition]' \
+		'D[file scheduled for deletion]' \
+		'C[modified or added file which contains merge conflicts]' \
+		'\![versioned file was expected on disk but is missing]' \
+		'~[versioned file is obstructed by a non-regular file]' \
+		'?[unversioned item not tracked by got]' \
+		'm[modified file mode (executable bit only)]' \
+		'N[non-existent path specified on the command line]'
+}
+
+_got_remove_status_codes() {
+	_values -s '' 'status code' \
+		'M[modified file (implies -f)]' \
+		'\![versioned file expected on disk but missing]' \
+		'?[unversioned file (deletion is permanent)]'
+}
+
+_got_add_paths() {
+	local paths
+
+	# use substr because path can contain spaces, same below
+	paths=$(got status 2>/dev/null | grep '^?' | awk '{print substr($0, 4)}')
+	reply=(${(f)paths})
+}
+
+_got_add_files() {
+	_got_add_paths
+	compadd -a reply
+}
+
+_got_revert_paths() {
+	local paths
+
+	# XXX: only these status codes make sense for 'got rv', same technique
+	# for other commands with other codes below, substr() is used because
+	# filenames can contain spaces
+	paths=$(got status 2>/dev/null | grep '^[MADC!~m]' | awk '{print substr($0, 4)}')
+	reply=(${(f)paths})
+}
+
+_got_revert_files() {
+	_got_revert_paths
+	compadd -a reply
+}
+
+_got_stage_paths() {
+	local paths
+
+	paths=$(got status 2>/dev/null | grep '^[AMD]' | awk '{print substr($0, 4)}')
+	reply=(${(f)paths})
+}
+
+_got_stage_files() {
+	_got_stage_paths
+	compadd -a reply
+}
+
+_got_unstage_paths() {
+	local paths
+
+	paths=$(got status 2>/dev/null | grep '^.[AMD]' | awk '{print substr($0, 4)}')
+	reply=(${(f)paths})
+}
+
+_got_unstage_files() {
+	_got_unstage_paths
+	compadd -a reply
+}
+
+_got_init() {
+	_arguments : \
+		'-A[configure the hashing algorithm used for object IDs]:hashing algorithm:(sha1 sha256)' \
+		'-b[make HEAD point at the specified branch instead of main]:branch:' \
+		'1:repository path:_files -/'
+}
+
+_got_import() {
+	_arguments : \
+		'-b[create the specified branch]:branch:' \
+		'*-I[ignore files or directories matching the given pattern]:pattern:' \
+		'-m[use the specified log message]:message:' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'1:directory:_files -/'
+}
+
+_got_clone() {
+	_arguments -s : \
+		'-a[fetch all branches from the remote repository]' \
+		'*-b[fetch the specified branch]:branch:' \
+		'-i[specify an SSH identity file]:identity file:_files' \
+		'-J[specify a jumphost for SSH connections]:jumphost:_got_ssh_config_hosts' \
+		'-l[list branches and tags available for fetching and exit]' \
+		'-m[create the cloned repository as a mirror]' \
+		'-q[suppress progress reporting]' \
+		'*-R[fetch an additional reference]:reference:' \
+		'*-v[verbose mode]' \
+		'1:repository URL:' \
+		'2::directory:_files -/'
+}
+
+_got_checkout() {
+	_arguments : \
+		'-b[check out the specified branch]:branch:_got_branch_name' \
+		'-c[check out the specified commit]:commit:_got_commit_or_keyword' \
+		'-E[proceed even if the work tree directory is not empty]' \
+		'-p[restrict the work tree to the specified path prefix]:path prefix:' \
+		'-q[silence progress output]' \
+		'1:repository path:_files -/' \
+		'2::work tree path:_files -/'
+}
+
+_got_update() {
+	_arguments : \
+		'-b[switch the work tree to the specified branch]:branch:_got_branch_name' \
+		'-c[update the work tree to the specified commit]:commit:_got_commit_or_keyword' \
+		'-q[silence progress output]' \
+		'*:path:_files'
+}
+
+_got_log() {
+	_arguments : \
+		'-b[display commits merged in from other branches]' \
+		'-C[set the number of diff context lines shown with -p]:context lines:' \
+		'-c[start traversing history at the specified commit]:commit:_got_commit_or_keyword' \
+		'-d[display diffstat of changes in each commit]' \
+		'-l[limit history traversal to the given number of commits]:limit:' \
+		'-P[display paths changed in each commit]' \
+		'-p[display the patch of modifications in each commit]' \
+		'-R[display commits in reverse order]' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'-S[only show commits matching the given search pattern]:search pattern:' \
+		'-s[display a short one-line summary of each commit]' \
+		'-t[display commits in topological order]' \
+		'-x[stop traversing history after the specified commit]:commit:_got_commit_or_keyword' \
+		'*:path:_files'
+}
+
+_got_diff() {
+	_arguments : \
+		'-a[treat file contents as ASCII text]' \
+		'-C[set the number of context lines shown in the diff]:context lines:' \
+		'*-c[show differences between commits]:commit:_got_commit_or_keyword' \
+		'-d[display diffstat of changes]' \
+		'-P[interpret all arguments as paths]' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'-s[show staged changes]' \
+		'-w[ignore whitespace-only changes]' \
+		'*:object or path:_files'
+}
+
+_got_blame() {
+	_arguments : \
+		'-c[start traversing history at the specified commit]:commit:_got_commit_or_keyword' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'1:path:_files'
+}
+
+_got_tree() {
+	_arguments : \
+		'-c[list files and directories as they appear in the specified commit]:commit:_got_commit_or_keyword' \
+		'-i[show object IDs of files and directories]' \
+		'-R[recurse into sub-directories]' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'*:path:_files'
+}
+
+_got_ref() {
+	_arguments : \
+		'-c[create or change the reference to point at the specified object]:object:_got_commit_or_keyword' \
+		'-d[delete the reference]' \
+		'-l[list references]' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'-s[create or change a symbolic reference to point at the specified reference]:reference:_got_ref_name' \
+		'-t[sort listed references by modification time (requires -l)]' \
+		'1::reference name:_got_ref_name'
+}
+
+_got_branch() {
+	_arguments : \
+		'-c[make the new branch point at the specified commit]:commit:_got_commit_or_keyword' \
+		'-d[delete the specified branch]:branch:_got_branch_name' \
+		'-l[list all existing branches]' \
+		'-n[do not switch and update the work tree after creating a branch]' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'-t[sort listed branches by modification time (requires -l)]' \
+		'1::branch name:'
+}
+
+_got_tag() {
+	_arguments -s : \
+		'-c[make the new tag point at the specified commit]:commit:_got_commit_or_keyword' \
+		'-l[list all existing tags]' \
+		'-m[use the specified tag message]:message:' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'-S[sign the tag with the given signer identity]:signer identity file:_files' \
+		'-s[display a short one-line summary of each tag (requires -l)]' \
+		'-V[verify tag object signatures]' \
+		'*-v[verbose mode]' \
+		'1::tag name:_got_tag_name'
+}
+
+_got_add() {
+	_arguments : \
+		'-I[add files even if they match an ignore pattern]' \
+		'-R[permit recursion into directories]' \
+		'*:path:_got_add_files'
+}
+
+_got_remove() {
+	_arguments : \
+		'-f[perform the operation even if a file is modified or missing]' \
+		'-I[remove unversioned files even if they match an ignore pattern]' \
+		'-k[keep affected files on disk]' \
+		'-R[permit recursion into directories]' \
+		'-s[only delete files matching the given status codes]:status codes:_got_remove_status_codes' \
+		'*:path:_files'
+}
+
+_got_patch() {
+	_arguments : \
+		'-c[use the specified commit as a merge base]:commit:_got_commit_or_keyword' \
+		'-n[do not modify the work tree]' \
+		'-p[strip the given number of leading path components]:strip count:' \
+		'-R[reverse the patch before applying it]' \
+		'1::patch file:_files'
+}
+
+_got_revert() {
+	_arguments : \
+		'-F[read y/n/q responses from the specified file]:response script:_files' \
+		'-p[interactively select or reject changes to revert]' \
+		'-R[permit recursion into directories]' \
+		'*:path:_got_revert_files'
+}
+
+_got_commit() {
+	_arguments : \
+		'-A[set author information for the new commit]:author:' \
+		'-C[allow committing files in conflicted status]' \
+		'-F[use the prepared log message stored at the specified path]:log message file:_files' \
+		'-m[use the specified log message]:message:' \
+		'-N[do not open the commit message in an editor]' \
+		'-n[do not generate a diff of to-be-committed changes]' \
+		'-S[allow symbolic links which point outside of version control]' \
+		'*:path:_files'
+}
+
+_got_fetch() {
+	_arguments -s : \
+		'-a[fetch all branches from the remote repository]' \
+		'*-b[fetch the specified branch from the remote repository]:branch:_got_branch_name' \
+		'-d[delete branches and tags no longer present on the remote]' \
+		'-i[specify an SSH identity file]:identity file:_files' \
+		'-J[specify a jumphost for SSH connections]:jumphost:_got_ssh_config_hosts' \
+		'-l[list branches and tags available for fetching and exit]' \
+		'-q[suppress progress reporting]' \
+		'*-R[fetch an additional reference]:reference:' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'-t[replace existing tags]' \
+		'*-v[verbose mode]' \
+		'-X[delete all references for the given remote repository]' \
+		'1::remote repository:_got_remote_name'
+}
+
+_got_cherrypick() {
+	_arguments : \
+		'-l[list log messages recorded by cherrypick operations]' \
+		'-X[delete log messages created by previous cherrypick operations]' \
+		'1::commit:_got_commit_or_keyword'
+}
+
+_got_backout() {
+	_arguments : \
+		'-l[list log messages recorded by backout operations]' \
+		'-X[delete log messages created by previous backout operations]' \
+		'1::commit:_got_commit_or_keyword'
+}
+
+_got_rebase() {
+	_arguments : \
+		'-a[abort an interrupted rebase operation]' \
+		'-c[continue an interrupted rebase operation]' \
+		'-C[allow files in conflicted status when continuing a rebase (requires -c)]' \
+		'-l[list past rebase operations]' \
+		'-X[delete backups of past rebase operations]' \
+		'1::branch:_got_branch_name'
+}
+
+_got_histedit() {
+	_arguments : \
+		'-a[abort an interrupted histedit operation]' \
+		'-c[continue an interrupted histedit operation]' \
+		'-C[allow files in conflicted status when continuing (requires -c)]' \
+		'-d[drop all commits]' \
+		'-e[interrupt for editing after merging each commit]' \
+		'-F[use the specified histedit script]:histedit script:_files' \
+		'-f[fold all commits into a single commit]' \
+		'-l[list past histedit operations]' \
+		'-m[edit log messages only]' \
+		'-X[delete backups of past histedit operations]' \
+		'1::branch:_got_branch_name'
+}
+
+_got_integrate() {
+	_arguments : \
+		'1:branch:_got_branch_name'
+}
+
+_got_merge() {
+	_arguments : \
+		'-a[abort an interrupted merge operation]' \
+		'-c[continue an interrupted merge operation]' \
+		'-C[allow files in conflicted status when continuing (requires -c)]' \
+		'-M[create a merge commit even if the branches have not diverged]' \
+		'-n[merge changes but do not create a merge commit immediately]' \
+		'1::branch:_got_branch_name'
+}
+
+_got_stage() {
+	_arguments : \
+		'-F[read y/n/q responses from the specified file]:response script:_files' \
+		'-l[list already staged paths]' \
+		'-p[interactively select or reject changes for staging]' \
+		'-S[allow staging of symbolic links pointing outside version control]' \
+		'*:path:_got_stage_files'
+}
+
+_got_unstage() {
+	_arguments : \
+		'-F[read y/n/q responses from the specified file]:response script:_files' \
+		'-p[interactively select or reject changes for unstaging]' \
+		'*:path:_got_unstage_files'
+}
+
+_got_cat() {
+	_arguments : \
+		'-c[look up paths in the specified commit]:commit:_got_commit_or_keyword' \
+		'-P[interpret all arguments as paths]' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'*:object or path:_files'
+}
+
+_got_info() {
+	_arguments : \
+		'*:path:_files'
+}
+
+_got_send() {
+	_arguments -s : \
+		'-a[send all branches from the local repository]' \
+		'*-b[send the specified branch]:branch:_got_branch_name' \
+		'*-d[delete the specified branch on the remote repository]:branch:_got_branch_name' \
+		'-f[force the server to overwrite existing branches or tags]' \
+		'-i[specify an SSH identity file]:identity file:_files' \
+		'-J[specify a jumphost for SSH connections]:jumphost:_got_ssh_config_hosts' \
+		'-q[suppress progress reporting]' \
+		'-r[use the repository at the specified path]:repository path:_files -/' \
+		'-T[send all tags from the local repository]' \
+		'*-t[send the specified tag]:tag:_got_tag_name' \
+		'*-v[verbose mode]' \
+		'1::remote repository:_got_remote_name'
+}
+
+_got_status() {
+	_arguments : \
+		'-I[show unversioned files even if they match an ignore pattern]' \
+		'-S[suppress files matching the given status codes]:status codes:_got_status_codes' \
+		'-s[only show files matching the given status codes]:status codes:_got_status_codes' \
+		'*:path:_files'
+}
+
+_got() {
+	local line state
+
+	_arguments -C \
+		'1: :->command' \
+		'*::arg:->args'
+
+	case $state in
+	command)
+		_values 'got command' \
+			'init[create a new empty repository]' \
+			'im[create an initial commit from a directory (alias for import)]' \
+			'cl[clone a repository (alias for clone)]' \
+			'fe[fetch new changes from a remote repository (alias for fetch)]' \
+			'co[check out a work tree from a repository (alias for checkout)]' \
+			'up[update a work tree (alias for update)]' \
+			'log[display history of a repository]' \
+			'di[display differences between files or commits (alias for diff)]' \
+			'bl[display line-by-line history of a file (alias for blame)]' \
+			'tr[display a repository tree listing (alias for tree)]' \
+			'st[show the modification status of files in a work tree (alias for status)]' \
+			'ref[manage references in a repository]' \
+			'br[create, list, or delete branches (alias for branch)]' \
+			'tag[manage tags in a repository]' \
+			'add[schedule unversioned files for addition]' \
+			'rm[remove files from a work tree (alias for remove)]' \
+			'pa[apply changes from a patch file (alias for patch)]' \
+			'rv[revert local changes in a work tree (alias for revert)]' \
+			'ci[create a new commit (alias for commit)]' \
+			'se[send new changes to a remote repository (alias for send)]' \
+			'cy[merge changes from a single commit (alias for cherrypick)]' \
+			'bo[reverse-merge changes from a single commit (alias for backout)]' \
+			'rb[rebase commits onto the tip of the current branch (alias for rebase)]' \
+			'he[edit commit history (alias for histedit)]' \
+			'ig[integrate a branch into the current branch (alias for integrate)]' \
+			'mg[merge a branch into the current branch (alias for merge)]' \
+			'sg[stage local changes for the next commit (alias for stage)]' \
+			'ug[merge staged changes back into the work tree (alias for unstage)]' \
+			'cat[print contents of repository objects]' \
+			'info[display work tree meta-data]'
+		;;
+	args)
+		case $line[1] in
+		init)
+			_got_init
+			;;
+		import|im)
+			_got_import
+			;;
+		clone|cl)
+			_got_clone
+			;;
+		fetch|fe)
+			_got_fetch
+			;;
+		checkout|co)
+			_got_checkout
+			;;
+		update|up)
+			_got_update
+			;;
+		log)
+			_got_log
+			;;
+		diff|di)
+			_got_diff
+			;;
+		blame|bl)
+			_got_blame
+			;;
+		tree|tr)
+			_got_tree
+			;;
+		status|st)
+			_got_status
+			;;
+		ref)
+			_got_ref
+			;;
+		branch|br)
+			_got_branch
+			;;
+		tag)
+			_got_tag
+			;;
+		add)
+			_got_add
+			;;
+		remove|rm)
+			_got_remove
+			;;
+		patch|pa)
+			_got_patch
+			;;
+		revert|rv)
+			_got_revert
+			;;
+		commit|ci)
+			_got_commit
+			;;
+		send|se)
+			_got_send
+			;;
+		cherrypick|cy)
+			_got_cherrypick
+			;;
+		backout|bo)
+			_got_backout
+			;;
+		rebase|rb)
+			_got_rebase
+			;;
+		histedit|he)
+			_got_histedit
+			;;
+		integrate|ig)
+			_got_integrate
+			;;
+		merge|mg)
+			_got_merge
+			;;
+		stage|sg)
+			_got_stage
+			;;
+		unstage|ug)
+			_got_unstage
+			;;
+		cat)
+			_got_cat
+			;;
+		info)
+			_got_info
+			;;
+		esac
+		;;
+	esac
+}
+
+_got "$@"