-- preamble: common routines local path = require('path') local git = require('gitutil') local matchers = require('matchers') local w = require('tables').wrap local clink_version = require('clink_version') local color = require('color') local parser = clink.arg.new_parser if clink_version.supports_color_settings then settings.add('color.git.star', 'bright green', 'Color for preferred branch completions') end --- -- Lists remote branches based on packed-refs file from git directory -- @param string [dir] Directory where to search file for -- @return table List of remote branches local function list_packed_refs(dir) local result = w() local git_dir = dir or git.get_git_common_dir() if not git_dir then return result end local packed_refs_file = io.open(git_dir..'/packed-refs') if packed_refs_file == nil then return {} end for line in packed_refs_file:lines() do -- SHA is 40 char length + 1 char for space if #line > 41 then local match = line:sub(41):match('refs/remotes/(.*)') if match then table.insert(result, match) end end end packed_refs_file:close() return result end local function list_remote_branches(dir) local git_dir = dir or git.get_git_common_dir() if not git_dir then return w() end return w(path.list_files(git_dir..'/refs/remotes', '/*', --[[recursive=]]true, --[[reverse_separator=]]true)) :concat(list_packed_refs(git_dir)) :sort():dedupe() end --- -- Lists local branches for git repo in git_dir directory. -- -- @param string [dir] Git directory, where to search for remote branches -- @return table List of branches. local function list_local_branches(dir) local git_dir = dir or git.get_git_common_dir() if not git_dir then return w() end local result = w(path.list_files(git_dir..'/refs/heads', '/*', --[[recursive=]]true, --[[reverse_separator=]]true)) return result end local branches = function (token) local git_dir = git.get_git_common_dir() if not git_dir then return w() end return list_local_branches(git_dir) :filter(function(branch) return clink.is_match(token, branch) end) end local function alias(token) local res = w() -- Try to resolve .git directory location local git_dir = git.get_git_dir() if git_dir == nil then return res end local f = io.popen("git config --get-regexp alias 2>nul") if f == nil then return {} end for line in f:lines() do local s = line:find(" ", 1, true) local alias_name = line:sub(7, s - 1) local start = alias_name:find(token, 1, true) if start and start == 1 then table.insert(res, alias_name) end end f:close() return res end local function remotes(token) -- luacheck: no unused args local result = w() local git_dir = git.get_git_common_dir() if not git_dir then return result end local git_config = io.open(git_dir..'/config') -- if there is no gitconfig file (WAT?!), return empty list if git_config == nil then return result end for line in git_config:lines() do local remote = line:match('%[remote "(.*)"%]') if (remote) then table.insert(result, remote) end end git_config:close() return result end local function local_or_remote_branches(token) -- Try to resolve .git directory location local git_dir = git.get_git_common_dir() if not git_dir then return w() end return list_local_branches(git_dir) :concat(list_remote_branches(git_dir)) :filter(function(branch) return clink.is_match(token, branch) end) end local function checkout_spec_generator(token) local files = matchers.files(token) :filter(function(file) return path.is_real_dir(file) end) local git_dir = git.get_git_common_dir() local local_branches = branches(token) local remote_branches = list_remote_branches(git_dir) :filter(function(branch) return clink.is_match(token, branch) end) local predicted_branches = list_remote_branches(git_dir) :map(function (remote_branch) return remote_branch:match('.-/(.+)') end) :filter(function(branch) return branch and clink.is_match(token, branch) -- Filter out those predictions which are already exists as local branches and not local_branches:contains(branch) end) if (#local_branches + #remote_branches + #predicted_branches) == 0 then return files end -- if there is any refspec that matches token then: -- * disable readline's filename completion, otherwise we'll get a list of these specs -- threaten as list of files (without 'path' part), ie. 'some_branch' instead of 'my_remote/some_branch' -- * create display filter for completion table to append path separator to each directory entry -- since it is not added automatically by readline (see previous point) clink.matches_are_files(0) clink.match_display_filter = function () local star = '*' if clink_version.supports_query_rl_var and rl.isvariabletrue('colored-stats') then star = color.get_clink_color('color.git.star')..star..color.get_clink_color('color.filtered') end return files:map(function(file) return clink.is_dir(file) and file..'\\' or file end) :concat(local_branches) :concat(predicted_branches:map(function(branch) return star..branch end)) :concat(remote_branches) end return files :concat(local_branches) :concat(predicted_branches) :concat(remote_branches) end local function push_branch_spec(token) local git_dir = git.get_git_common_dir() if not git_dir then return w() end local plus_prefix = token:sub(0, 1) == '+' -- cut out leading '+' symbol as it is a part of branch spec local branch_spec = plus_prefix and token:sub(2) or token -- check if there a local/remote branch separator local s, e = branch_spec:find(':') -- starting from here we have 2 options: -- * if there is no branch separator complete word with local branches if not s then local b = branches(branch_spec) -- setup display filter to prevent display '+' symbol in completion list clink.match_display_filter = function () return b end return b:map(function(branch) -- append '+' to results if it was specified return plus_prefix and '+'..branch or branch end) else -- * if there is ':' separator then we need to complete remote branch local local_branch_spec = branch_spec:sub(1, s - 1) local remote_branch_spec = branch_spec:sub(e + 1) -- TODO: show remote branches only for remote that has been specified as previous argument local b = w(clink.find_dirs(git_dir..'/refs/remotes/*')) :filter(function(remote) return path.is_real_dir(remote) end) :reduce({}, function(result, remote) return w(path.list_files(git_dir..'/refs/remotes/'..remote, '/*', --[[recursive=]]true, --[[reverse_separator=]]true)) :filter(function(remote_branch) return clink.is_match(remote_branch_spec, remote_branch) end) :concat(result) end) -- setup display filter to prevent display '+' symbol in completion list clink.match_display_filter = function () return b end return b:map(function(branch) return (plus_prefix and '+'..local_branch_spec or local_branch_spec)..':'..branch end) end end local stashes = function(token) -- luacheck: no unused args local git_dir = git.get_git_dir() if not git_dir then return w() end local stash_file = io.open(git_dir..'/logs/refs/stash') -- if there is no stash file, return empty list if stash_file == nil then return w() end local stashes = {} -- make a dictionary of stash time and stash comment to -- be able to sort stashes by date/time created for stash in stash_file:lines() do local stash_time, stash_name = stash:match('(%d%d%d%d%d%d%d%d%d%d) [+-]%d%d%d%d%s+(.*)') if (stash_name and stash_name) then stashes[stash_time] = stash_name end end stash_file:close() -- get times for available stashes into separate table and sort it -- from newest to oldest. This is required because of stash@{0} -- represents _latest_ stash, not the last one in file local stash_times = {} for k in pairs(stashes) do table.insert(stash_times, k) end table.sort(stash_times, function (a, b) return a > b end) -- generate matches and match filter table local ret = {} local ret_filter = {} for i,v in ipairs(stash_times) do local match = "stash@{"..(i-1).."}" table.insert(ret, match) if clink_version.supports_display_filter_description then -- Clink now has a richer match interface. By returning a table, -- the script is able to provide the stash name separately from the -- description. If the script does so, then the popup completion -- window is able to show the stash name plus a dimmed description, -- but only insert the stash name. table.insert(ret_filter, { match=match, type="word", description=stashes[v] }) else table.insert(ret_filter, match.." "..stashes[v]) end end local function filter() return ret_filter end if clink_version.supports_display_filter_description then clink.ondisplaymatches(filter) else clink.match_display_filter = filter end return ret end local color_opts = parser({"true", "false", "always"}) local git_options = { "core.editor", "core.pager", "core.excludesfile", "core.autocrlf"..parser({"true", "false", "input"}), "core.trustctime"..parser({"true", "false"}), "core.whitespace"..parser({ "cr-at-eol", "-cr-at-eol", "indent-with-non-tab", "-indent-with-non-tab", "space-before-tab", "-space-before-tab", "trailing-space", "-trailing-space" }), "commit.template", "color.ui"..color_opts, "color.*"..color_opts, "color.branch"..color_opts, "color.diff"..color_opts, "color.interactive"..color_opts, "color.status"..color_opts, "help.autocorrect", "merge.tool", "mergetool.*.cmd", "mergetool.trustExitCode"..parser({"true", "false"}), "diff.external", "user.name", "user.email", "user.signingkey", } local config_parser = parser( "--system", "--global", "--local", "--file"..parser({matchers.files}), "--int", "--bool", "--path", "-z", "--null", "--add", "--replace-all", "--get", "--get-all", "--get-regexp", "--get-urlmatch", "--unset", "--unset-all", "--rename-section", "--remove-section", "-l", "--list", "--get-color", "--get-colorbool", "-e", "--edit", {git_options} ) local merge_recursive_options = parser({ "ours", "theirs", "renormalize", "no-renormalize", "diff-algorithm="..parser({ "patience", "minimal", "histogram", "myers" }), "patience", "ignore-space-change", "ignore-all-space", "ignore-space-at-eol", "rename-threshold=", -- "subtree="..parser(), "subtree" }) local merge_strategies = parser({ "resolve", "recursive", "ours", "octopus", "subtree" }) local cleanup_options = parser({ "strip", "whitespace", "verbatim", "scissors", "default" }) local git_parser = parser( { {alias}, "add" .. parser({matchers.files}, "-n", "--dry-run", "-v", "--verbose", "-f", "--force", "-i", "--interactive", "-p", "--patch", "-e", "--edit", "-u", "--update", "-A", "--all", "--no-all", "--ignore-removal", "--no-ignore-removal", "-N", "--intent-to-add", "--refresh", "--ignore-errors", "--ignore-missing" ), "add--interactive", "am", "annotate" .. parser({matchers.files}, "-b", "--root", "--show-stats", "-L", "-l", "-t", "-S", "--reverse", "-p", "--porcelain", "--line-porcelain", "--incremental", "--encoding=", "--contents", "--date", "-M", "-C", "-h" ), "apply" .. parser( "--stat", "--numstat", "--summary", "--check", "--index", "--cached", "-3", "--3way", "--build-fake-ancestor=", "-R", "--reverse", "--reject", "-z", "-p", "-C", "--unidiff-zero", "--apply", "--no-add", "--allow-binary-replacement", "--binary", "--exclude=", "--include=", "--ignore-space-change", "--ignore-whitespace", "--whitespace=", "--inaccurate-eof", "-v", "--verbose", "--recount", "--directory=" ), "archive", "bisect", "bisect--helper", "blame", "branch" .. parser( "-v", "--verbose", "-q", "--quiet", "-t", "--track", "--set-upstream", "-u", "--set-upstream-to", "--unset-upstream", "--color", "-r", "--remotes", "--contains" , "--abbrev", "-a", "--all", "-d" .. parser({branches}):loop(1), "--delete" .. parser({branches}):loop(1), "-D" .. parser({branches}):loop(1), "-m", "--move", "-M", "--list", "-l", "--create-reflog", "--edit-description", "-f", "--force", "--no-merged", "--merged", "--column" ), "bundle", "cat-file", "check-attr", "check-ignore", "check-mailmap", "check-ref-format", "checkout" .. parser({checkout_spec_generator}, "-q", "--quiet", "-b", "-B", "-l", "--detach", "-t", "--track", "--orphan", "-2", "--ours", "-3", "--theirs", "-f", "--force", "-m", "--merge", "--overwrite-ignore", "--conflict", "-p", "--patch", "--ignore-skip-worktree-bits" ), "checkout-index", "cherry", "cherry-pick"..parser( "-e", "--edit", "-m", "--mainline ", "-n", "--no-commit", "-r", "-x", "--ff", "-s", "-S", "--gpg-sign", "--allow-empty", "--allow-empty-message", "--keep-redundant-commits", "--strategy"..parser({merge_strategies}), "-X"..parser({merge_recursive_options}), "--strategy-option"..parser({merge_recursive_options}), "--continue", "--quit", "--abort" ), "citool", "clean", "clone" .. parser( "--template", "-l", "--local", "-s", "--shared", "--no-hardlinks", "-q", "--quiet", "-n", "--no-checkout", "--bare", "--mirror", "-o", "--origin", "-b", "--branch", "-u", "--upload-pack", "--reference", "--dissociate", "--separate-git-dir", "--depth", "--single-branch", "--no-single-branch", "--no-tags", "--recurse-submodules", "--shallow-submodules", "--no-shallow-submodules", "--jobs" ), "column", "commit" .. parser( "-a", "--all", "-p", "--patch", "-C", "--reuse-message=", "-c", "--reedit-message=", "--fixup=", "--squash=", "--reset-author", "--short", "--branch", "--porcelain", "--long", "-z", "--null", "-F", "--file=", "--author=", "--date=", "-m", "--message=", "-t", "--template=", "-s", "--signoff", "-n", "--no-verify", "--allow-empty", "--allow-empty-message", "--cleanup"..cleanup_options, "-e", "--edit", "--no-edit", "--amend", "--no-post-rewrite", "-i", "--include", "-o", "--only", "-u", "--untracked-files", "--untracked-files=", -- .. parser({"no", "normal", "all"}), "-v", "--verbose", "-q", "--quiet", "--dry-run", "--status", "--no-status", "-S", "--gpg-sign", "--gpg-sign=", "--" ), "commit-tree", "config"..config_parser, "count-objects", "credential", "credential-store", "credential-wincred", "daemon", "describe", "diff" .. parser({local_or_remote_branches, matchers.files}), "diff-files", "diff-index", "diff-tree", "difftool"..parser( "-d", "--dir-diff", "-y", "--no-prompt", "--prompt", "-t", "--tool=" -- TODO: complete tool (take from config) ), "difftool--helper", "fast-export", "fast-import", "fetch" .. parser({remotes}, "--all", "--prune", "--tags" ), "fetch-pack", "filter-branch", "fmt-merge-msg", "for-each-ref", "format-patch", "fsck", "fsck-objects", "gc", "get-tar-commit-id", "grep", "gui", "gui--askpass", "gui--askyesno", "gui.tcl", "hash-object", "help", "http-backend", "http-fetch", "http-push", "imap-send", "index-pack", "init", "init-db", "log", "lost-found", "ls-files", "ls-remote", "ls-tree", "mailinfo", "mailsplit", "merge" .. parser({branches}, "--commit", "--no-commit", "--edit", "-e", "--no-edit", "--ff", "--no-ff", "--ff-only", "--log", "--no-log", "--stat", "-n", "--no-stat", "--squash", "--no-squash", "-s" .. merge_strategies, "--strategy" .. merge_strategies, "-X" .. merge_recursive_options, "--strategy-option" .. merge_recursive_options, "--verify-signatures", "--no-verify-signatures", "-q", "--quiet", "-v", "--verbose", "--progress", "--no-progress", "-S", "--gpg-sign", "-m", "--rerere-autoupdate", "--no-rerere-autoupdate", "--abort" ), "merge-base", "merge-file", "merge-index", "merge-octopus", "merge-one-file", "merge-ours", "merge-recursive", "merge-resolve", "merge-subtree", "merge-tree", "mergetool", "mergetool--lib", "mktag", "mktree", "mv", "name-rev", "notes", "p4", "pack-objects", "pack-redundant", "pack-refs", "parse-remote", "patch-id", "peek-remote", "prune", "prune-packed", "pull" .. parser( {remotes}, {branches}, "-q", "--quiet", "-v", "--verbose", "--recurse-submodules", --[no-]recurse-submodules[=yes|on-demand|no] "--no-recurse-submodules", "--commit", "--no-commit", "-e", "--edit", "--no-edit", "--ff", "--no-ff", "--ff-only", "--log", "--no-log", "--stat", "-n", "--no-stat", "--squash", "--no-squash", "-s"..merge_strategies, "--strategy"..merge_strategies, "-X"..merge_recursive_options, "--strategy-option"..merge_recursive_options, "--verify-signatures", "--no-verify-signatures", "--summary", "--no-summary", "-r", "--rebase", "--no-rebase", "--all", "-a", "--append", "--depth", "--unshallow", "--update-shallow", "-f", "--force", "-k", "--keep", "--no-tags", "-u", "--update-head-ok", "--upload-pack", "--progress" ), "push" .. parser( {remotes}, {push_branch_spec}, "-v", "--verbose", "-q", "--quiet", "--repo", "--all", "--mirror", "--delete", "--tags", "-n", "--dry-run", "--porcelain", "-f", "--force", "--force-with-lease", "--recurse-submodules", "--thin", "--receive-pack", "--exec", "-u", "--set-upstream", "--progress", "--prune", "--no-verify", "--follow-tags" ), "quiltimport", "read-tree", "rebase" .. parser({local_or_remote_branches}, {branches}, "-i", "--interactive", "--onto" .. parser({branches}), "--continue", "--abort", "--keep-empty", "--skip", "--edit-todo", "-m", "--merge", "-s" .. merge_strategies, "--strategy"..merge_strategies, "-X" .. merge_recursive_options, "--strategy-option"..merge_recursive_options, "-S", "--gpg-sign", "-q", "--quiet", "-v", "--verbose", "--stat", "-n", "--no-stat", "--no-verify", "--verify", "-C", "-f", "--force-rebase", "--fork-point", "--no-fork-point", "--ignore-whitespace", "--whitespace", "--committer-date-is-author-date", "--ignore-date", "-i", "--interactive", "-p", "--preserve-merges", "-x", "--exec", "--root", "--autosquash", "--no-autosquash", "--autostash", "--no-autostash", "--no-ff" ), "receive-pack", "reflog", "remote"..parser({ "add" ..parser( "-t"..parser({branches}), "-m", "-f", "--mirror", "--tags", "--no-tags" ), "rename"..parser({remotes}), "remove"..parser({remotes}), "rm"..parser({remotes}), "set-head"..parser({remotes}, {branches}, "-a", "--auto", "-d", "--delete" ), "set-branches"..parser("--add", {remotes}, {branches}), "set-url"..parser( "--add"..parser("--push", {remotes}), "--delete"..parser("--push", {remotes}) ), "get-url"..parser({remotes}, "--push", "--all"), "show"..parser("-n", {remotes}), "prune"..parser("-n", "--dry-run", {remotes}), "update"..parser({remotes}, "-p", "--prune") }, "-v", "--verbose"), "remote-ext", "remote-fd", "remote-ftp", "remote-ftps", "remote-hg", "remote-http", "remote-https", "remote-testsvn", "repack", "replace", "repo-config", "request-pull", "rerere", -- TODO: Add commit completions "reset"..parser({local_or_remote_branches}, "-q", "-p", "--patch", "--soft", "--mixed", "--hard", "--merge", "--keep" ), "restore"..parser({matchers.files}, "-s", "--source", "-p", "--patch", "-W", "--worktree", "-S", "--staged", "-q", "--quiet", "--progress", "--no-progress", "--ours", "--theirs", "-m", "--merge", "--conflict", "--ignore-unmerged", "--ignore-skip-worktree-bits", "--overlay", "--no-overlay" ), "rev-list", "rev-parse", "revert"..parser( "-e", "--edit", "-m", "--mainline", "--no-edit", "--cleanup"..cleanup_options, "-n", "--no-commit", "-S", "--gpg-sign", "--no-gpg-sign", "-s", "--signoff", "--strategy"..merge_strategies, "-X"..merge_recursive_options, "--strategy-option"..merge_recursive_options, "--rerere-autoupdate", "--no-rerere-autoupdate", "--continue", "--skip", "--quit", "--abort" ), "rm", "send-email", "send-pack", "sh-i18n", "sh-i18n--envsubst", "sh-setup", "shortlog", "show", "show-branch", "show-index", "show-ref", "stage", "stash"..parser({ "list", -- TODO: The command takes options applicable to the git log -- command to control what is shown and how it's done "show"..parser({stashes}), "drop"..parser({stashes}, "-q", "--quiet"), "pop"..parser({stashes}, "--index", "-q", "--quiet"), "apply"..parser({stashes}, "--index", "-q", "--quiet"), "branch"..parser({branches}, {stashes}), "save"..parser( "-p", "--patch", "-k", "--no-keep-index", "--keep-index", "-q", "--quiet", "-u", "--include-untracked", "-a", "--all" ), "clear" }), "status", "stripspace", "submodule"..parser({ "add", "init", "deinit", "foreach", "status"..parser("--cached", "--recursive"), "summary", "sync", "update" }, '--quiet'), "subtree", "switch"..parser({local_or_remote_branches}, "-c", "-C", "--create", "--force-create", "-d", "--detach", "--guess", "--no-guess", "-f", "--force", "--discard-changes", "-m", "--merge", "--conflict", "-q", "--quiet", "--progress", "--no-progress", "-t", "--track", "--no-track", "--orphan", "--ignore-other-worktrees", "--recurse-submodules", "--no-recurse-submodules" ), "svn"..parser({ "init"..parser("-T", "--trunk", "-t", "--tags", "-b", "--branches", "-s", "--stdlayout", "--no-metadata", "--use-svm-props", "--use-svnsync-props", "--rewrite-root", "--rewrite-uuid", "--username", "--prefix"..parser({"origin"}), "--ignore-paths", "--include-paths", "--no-minimize-url"), "fetch"..parser({remotes}, "--localtime", "--parent", "--ignore-paths", "--include-paths", "--log-window-size"), "clone"..parser("-T", "--trunk", "-t", "--tags", "-b", "--branches", "-s", "--stdlayout", "--no-metadata", "--use-svm-props", "--use-svnsync-props", "--rewrite-root", "--rewrite-uuid", "--username", "--prefix"..parser({"origin"}), "--ignore-paths", "--include-paths", "--no-minimize-url", "--preserve-empty-dirs", "--placeholder-filename"), "rebase"..parser({local_or_remote_branches}, {branches}), "dcommit"..parser("--no-rebase", "--commit-url", "--mergeinfo", "--interactive"), "branch"..parser("-m","--message","-t", "--tags", "-d", "--destination", "--username", "--commit-url", "--parents"), "log"..parser("-r", "--revision", "-v", "--verbose", "--limit", "--incremental", "--show-commit", "--oneline"), "find-rev"..parser("--before", "--after"), "reset"..parser("-r", "--revision", "-p", "--parent"), "tag", "blame", "set-tree", "create-ignore", "show-ignore", "mkdirs", "commit-diff", "info", "proplist", "propget", "show-externals", "gc" }), "symbolic-ref", "tag", "tar-tree", "unpack-file", "unpack-objects", "update-index", "update-ref", "update-server-info", "upload-archive", "upload-pack", "var", "verify-pack", "verify-tag", "web--browse", "whatchanged", "worktree"..parser({ "add"..parser( {matchers.dirs}, {branches}, "-f", "--force", "--detach", "--checkout", "--lock", "-b"..parser({branches}) ), "list"..parser("--porcelain"), "lock"..parser("--reason"), "move", "prune"..parser( "-n", "--dry-run", "-v", "--verbose", "--expire" ), "remove"..parser("-f"), "unlock" }), "write-tree", }, "--version", "--help", "-c", "--exec-path", "--html-path", "--man-path", "--info-path", "-p", "--paginate", "--no-pager", "--no-replace-objects", "--bare", "--git-dir=", "--work-tree=", "--namespace=" ) clink.arg.register_parser("git", git_parser)