8000 mini.nvim/lua/mini/git.lua at main · W4RH4WK/mini.nvim · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
1709 lines (1484 loc) · 70.2 KB

git.lua

File metadata and controls

1709 lines (1484 loc) · 70.2 KB
H.ensure_git_subcommands = function()
if H.git_subcommands ~= nil then return end
local git_subcommands = {}
-- Compute all supported commands. All 'list-' are taken from Git source
-- 'command-list.txt' file. Be so granular and not just `main,nohelpers` in
-- order to not include purely man-page worthy items (like "remote-ext").
local lists_all = {
'list-mainporcelain',
'list-ancillarymanipulators', 'list-ancillaryinterrogators',
'list-foreignscminterface',
'list-plumbingmanipulators', 'list-plumbinginterrogators',
'others', 'alias',
}
local supported = H.git_cli_output({ '--list-cmds=' .. table.concat(lists_all, ',') })
if #supported == 0 then
-- Fall back only on basics if previous one failed for some reason
supported = {
'add', 'bisect', 'branch', 'clone', 'commit', 'diff', 'fetch', 'grep', 'init', 'log', 'merge',
'mv', 'pull', 'push', 'rebase', 'reset', 'restore', 'rm', 'show', 'status', 'switch', 'tag',
}
end
table.sort(supported)
git_subcommands.supported = supported
-- Compute complete list for commands by enhancing with two word commands.
-- Keep those lists manual as there is no good way to compute lazily.
local complete = vim.deepcopy(supported)
local add_twoword = function(prefix, suffixes)
if not vim.tbl_contains(supported, prefix) then return end
for _, suf in ipairs(suffixes) do table.insert(complete, prefix .. ' ' .. suf) end
end
add_twoword('bundle', { 'create', 'list-heads', 'unbundle', 'verify' })
add_twoword('bisect', { 'bad', 'good', 'log', 'replay', 'reset', 'run', 'skip', 'start', 'terms', 'view', 'visualize' })
add_twoword('commit-graph', { 'verify', 'write' })
add_twoword('maintenance', { 'run', 'start', 'stop', 'register', 'unregister' })
add_twoword('multi-pack-index', { 'expire', 'repack', 'verify', 'write' })
add_twoword('notes', { 'add', 'append', 'copy', 'edit', 'get-ref', 'list', 'merge', 'prune', 'remove', 'show' })
add_twoword('p4', { 'clone', 'rebase', 'submit', 'sync' })
add_twoword('reflog', { 'delete', 'exists', 'expire', 'show' })
add_twoword('remote', { 'add', 'get-url', 'prune', 'remove', 'rename', 'rm', 'set-branches', 'set-head', 'set-url', 'show', 'update' })
add_twoword('rerere', { 'clear', 'diff', 'forget', 'gc', 'remaining', 'status' })
add_twoword('sparse-checkout', { 'add', 'check-rules', 'disable', 'init', 'list', 'reapply', 'set' })
add_twoword('stash', { 'apply', 'branch', 'clear', 'create', 'drop', 'list', 'pop', 'save', 'show', 'store' })
add_twoword('submodule', { 'absorbgitdirs', 'add', 'deinit', 'foreach', 'init', 'set-branch', 'set-url', 'status', 'summary', 'sync', 'update' })
add_twoword('subtree', { 'add', 'merge', 'pull', 'push', 'split' })
add_twoword('worktree', { 'add', 'list', 'lock', 'move', 'prune', 'remove', 'repair', 'unlock' })
git_subcommands.complete = complete
-- Compute commands which are meant to show information. These will show CLI
-- output in separate buffer opposed to `vim.notify`.
local info_args = { '--list-cmds=list-info,list-ancillaryinterrogators,list-plumbinginterrogators' }
local info_commands = H.git_cli_output(info_args)
if #info_commands == 0 then info_commands = { 'bisect', 'diff', 'grep', 'log', 'show', 'status' } end
local info = {}
for _, cmd in ipairs(info_commands) do
info[cmd] = true
end
git_subcommands.info = info
-- Compute commands which aliases rely on
local alias_data = H.git_cli_output({ 'config', '--get-regexp', 'alias.*' })
local alias = {}
for _, l in ipairs(alias_data) do
-- Assume simple alias of the form `alias.xxx subcommand ...`
local alias_cmd, cmd = string.match(l, '^alias%.(%S+) (%S+)')
if vim.tbl_contains(supported, cmd) then alias[alias_cmd] = cmd end
end
git_subcommands.alias = alias
-- Initialize cache for command options. Initialize with `false` so that
-- actual values are computed lazily when needed for a command.
local options = { git = false }
for _, command in ipairs(supported) do
options[command] = false
end
git_subcommands.options = options
-- Cache results
H.git_subcommands = git_subcommands
end
H.ensure_git_editor = function(mods)
if H.git_editor_config == nil or not vim.fn.filereadable(H.git_editor_config) == 0 then
H.git_editor_config = vim.fn.tempname()
end
-- Create a private function responsible for editing Git file
MiniGit._edit = function(path, servername)
-- Define editor state before and after editing path
H.skip_timeout, H.skip_sync = true, true
local cleanup = function()
local _, channel = pcall(vim.fn.sockconnect, 'pipe', servername, { rpc = true })
local has_exec2 = vim.fn.has('nvim-0.9') == 1
local method, opts = has_exec2 and 'nvim_exec2' or 'nvim_exec', has_exec2 and {} or false
pcall(vim.rpcnotify, channel, method, 'quitall!', opts)
H.skip_timeout, H.skip_sync = false, false
end
-- Start file edit with proper modifiers in a special window
mods = H.ensure_mods_is_split(mods)
vim.cmd(mods .. ' split ' .. vim.fn.fnameescape(path))
H.define_minigit_window(cleanup)
end
-- Start editing file from first argument (as how `GIT_EDITOR` works) in
-- current instance and don't close until explicitly closed later from this
-- instance as set up in `MiniGit._edit()`
local lines = {
'lua << EOF',
string.format('local channel = vim.fn.sockconnect("pipe", %s, { rpc = true })', vim.inspect(vim.v.servername)),
'local ins = vim.inspect',
'local lua_cmd = string.format("MiniGit._edit(%s, %s)", ins(vim.fn.argv(0)), ins(vim.v.servername))',
'vim.rpcrequest(channel, "nvim_exec_lua", lua_cmd, {})',
'EOF',
}
vim.fn.writefile(lines, H.git_editor_config)
end
H.get_git_cwd = function()
local buf_cache = H.cache[vim.api.nvim_get_current_buf()] or {}
return buf_cache.root or vim.fn.getcwd()
end
H.command_make_on_done = function(cmd_data, is_done_track)
return vim.schedule_wrap(function(code, out, err)
-- Register that command is done executing (to enable sync execution)
is_done_track.done = true
-- Trigger "done" event
cmd_data.git_subcommand = H.command_parse_subcommand(cmd_data.git_command)
cmd_data.exit_code, cmd_data.stdout, cmd_data.stderr = code, out, err
H.trigger_event('MiniGitCommandDone', cmd_data)
-- Show stderr and stdout
if H.cli_err_notify(code, out, err) then return end
H.command_show_stdout(cmd_data)
-- Ensure that all buffers are up to date (avoids "The file has been
-- changed since reading it" warning)
vim.tbl_map(function(buf_id) vim.cmd('checktime ' .. buf_id) end, vim.api.nvim_list_bufs())
end)
end
H.command_show_stdout = function(cmd_data)
local stdout, mods, subcommand = cmd_data.stdout, cmd_data.cmd_input.mods, cmd_data.git_subcommand
if stdout == '' or (mods:find('silent') ~= nil and mods:find('unsilent') == nil) then return end
-- Show in split if explicitly forced or the command shows info.
-- Use `vim.notify` otherwise.
local should_split = H.mods_is_split(mods) or H.git_subcommands.info[subcommand]
if not should_split then return H.notify(stdout, 'INFO') end
local lines = vim.split(stdout, '\n')
local name = table.concat(cmd_data.git_command, ' ')
cmd_data.win_source, cmd_data.win_stdout = H.show_in_split(mods, lines, subcommand, name)
-- Trigger "split" event
H.trigger_event('MiniGitCommandSplit', cmd_data)
end
H.command_parse_subcommand = function(command)
local res
for _, cmd in ipairs(command) do
if res == nil and vim.tbl_contains(H.git_subcommands.supported, cmd) then res = cmd end
end
return H.git_subcommands.alias[res] or res
end
H.command_complete = function(_, line, col)
-- Compute completion base manually to be "at cursor" and respect `\ `
local base = H.get_complete_base(line:sub(1, col))
local candidates, compl_type = H.command_get_complete_candidates(line, col, base)
-- Allow several "//" at the end for path completion for easier "chaining"
if compl_type == 'path' then base = base:gsub('/+$', '/') end
return vim.tbl_filter(function(x) return vim.startswith(x, base) end, candidates)
end
H.get_complete_base = function(line)
local from, _, res = line:find('(%S*)$')
while from ~= nil do
local cur_from, _, cur_res = line:sub(1, from - 1):find('(%S*\\ )$')
if cur_res ~= nil then res = cur_res .. res end
from = cur_from
end
return (res:gsub([[\ ]], ' '))
end
H.command_get_complete_candidates = function(line, col, base)
H.ensure_git_subcommands()
-- Determine current Git subcommand as the earliest present supported one
local subcmd, subcmd_end = nil, math.huge
for _, cmd in pairs(H.git_subcommands.supported) do
local _, ind = line:find(' ' .. cmd .. ' ', 1, true)
if ind ~= nil and ind < subcmd_end then
subcmd, subcmd_end = cmd, ind
end
end
subcmd = subcmd or 'git'
local cwd = H.get_git_cwd()
-- Determine command candidates:
-- - Commannd options if complete base starts with "-".
-- - Paths if after explicit "--".
-- - Git commands if there is none fully formed yet or cursor is at the end
-- of the command (to also suggest subcommands).
-- - Command targets specific for each command (if present).
if vim.startswith(base, '-') then return H.command_complete_option(subcmd) end
if line:sub(1, col):find(' -- ') ~= nil then return H.command_complete_path(cwd, base) end
if subcmd_end == math.huge or (subcmd_end - 1) == col then return H.git_subcommands.complete, 'subcommand' end
subcmd = H.git_subcommands.alias[subcmd] or subcmd
local complete_targets = H.command_complete_subcommand_targets[subcmd]
if complete_targets == nil then return {}, nil end
return complete_targets(cwd, base, line)
end
H.command_complete_option = function(command)
local cached_candidates = H.git_subcommands.options[command]
if cached_candidates == nil then return {} end
if type(cached_candidates) == 'table' then return cached_candidates end
-- Use alias's command to compute the options but store cache for alias
local orig_command = command
command = H.git_subcommands.alias[command] or command
-- Find command's flag options by parsing its help page. Needs a bit
-- heuristic approach and ensuring proper `git help` output (as it is done
-- through `man`), but seems to work good enough.
-- Alternative is to call command with `--git-completion-helper-all` flag (as
-- is done in bash and vim-fugitive completion). This has both pros and cons:
-- - Pros: faster; more targeted suggestions (like for two word subcommands);
-- presumably more reliable.
-- - Cons: works on smaller number of commands (for example, `rev-parse` or
-- pure `git` do not work); does not provide single dash suggestions;
-- does not work when not inside Git repo; needs recognizing two word
-- commands before asking for completion.
local env = H.make_spawn_env({ MANPAGER = 'cat', NO_COLOR = 1, PAGER = 'cat' })
local lines = H.git_cli_output({ 'help', '--man', command }, nil, env)
-- - Exit early before caching to try again later
if #lines == 0 then return {} end
-- - On some systems (like Mac), output still might contain formatting
-- sequences, like "a\ba" and "_\ba" meaning bold and italic.
-- See https://github.com/echasnovski/mini.nvim/issues/918
lines = vim.tbl_map(function(l) return l:gsub('.\b', '') end, lines)
-- Construct non-duplicating candidates by parsing lines of help page
local candidates_map = {}
-- Options are assumed to be listed inside "OPTIONS" or "XXX OPTIONS" (like
-- "MODE OPTIONS" of `git rebase`) section on dedicated lines. Whether a line
-- contains only options is determined heuristically: it is assumed to start
-- exactly with " -" indicating proper indent for subsection start.
-- Known not parsable options:
-- - `git reset <mode>` (--soft, --hard, etc.): not listed in "OPTIONS".
-- - All -<number> options, as they are not really completeable.
local is_in_options_section = false
for _, l in ipairs(lines) do
if is_in_options_section and l:find('^%u[%u ]+$') ~= nil then is_in_options_section = false end
if not is_in_options_section and l:find('^%u?[%u ]*OPTIONS$') ~= nil then is_in_options_section = true end
if is_in_options_section and l:find('^ %-') ~= nil then H.parse_options(candidates_map, l) end
end
-- Finalize candidates. Should not contain "almost duplicates".
-- Should also be sorted by relevance: short flags before regular flags.
-- Inside groups sort alphabetically ignoring case.
candidates_map['--'] = nil
for cmd, _ in pairs(candidates_map) do
-- There can be two explicitly documented options "--xxx" and "--xxx=".
-- Use only one of them (without "=").
if cmd:sub(-1, -1) == '=' and candidates_map[cmd:sub(1, -2)] ~= nil then candidates_map[cmd] = nil end
end
local res = vim.tbl_keys(candidates_map)
table.sort(res, function(a, b)
local a2, b2 = a:sub(2, 2) == '-', b:sub(2, 2) == '-'
if a2 and not b2 then return false end
if not a2 and b2 then return true end
0