Files
nvim/lua/git/cmd.lua
T

402 lines
11 KiB
Lua

local commit = require("git.commit")
local object = require("git.object")
local repo = require("git.repo")
local util = require("git.util")
local M = {}
---@class ow.Git.Cmd.SplitHandler
---@field ft string
---@field needs_rev boolean?
---@type table<string, ow.Git.Cmd.SplitHandler>
local SPLIT_HANDLERS = {
log = { ft = "git" },
diff = { ft = "diff" },
}
---@type string[]?
local cached_cmds
---@return string[]
local function git_cmds()
if cached_cmds then
return cached_cmds
end
local result = vim.system(
{ "git", "--list-cmds=main,others,alias" },
{ text = true }
)
:wait()
if result.code ~= 0 then
util.error("git --list-cmds failed: %s", vim.trim(result.stderr or ""))
return {}
end
cached_cmds = {}
for line in (result.stdout or ""):gmatch("[^\r\n]+") do
if line ~= "" then
table.insert(cached_cmds, line)
end
end
table.sort(cached_cmds)
return cached_cmds
end
---@param tok string
---@return boolean
local function is_expansion_target(tok)
local first = tok:sub(1, 1)
if first == "%" or first == "#" then
return true
end
if tok:match("^<%w+>") then
return true
end
if tok == "~" or tok:sub(1, 2) == "~/" then
return true
end
return false
end
---@param line string
---@param i integer
---@param buf string[]
---@param escapes string?
---@return integer
local function parse_quoted(line, i, buf, escapes)
local quote = line:sub(i, i)
local n = #line
i = i + 1
while i <= n do
local c = line:sub(i, i)
if c == quote then
return i + 1
elseif escapes and c == "\\" and i < n then
local nxt = line:sub(i + 1, i + 1)
if escapes:find(nxt, 1, true) then
table.insert(buf, nxt)
i = i + 2
else
table.insert(buf, c)
i = i + 1
end
else
table.insert(buf, c)
i = i + 1
end
end
return i
end
---@param line string
---@return string[]
function M.parse_args(line)
local args = {}
local i, n = 1, #line
while i <= n do
local c = line:sub(i, i)
if c == " " or c == "\t" then
i = i + 1
else
local buf = {}
local quoted = false
while i <= n do
c = line:sub(i, i)
if c == " " or c == "\t" then
break
elseif c == "\\" and i < n then
table.insert(buf, line:sub(i + 1, i + 1))
i = i + 2
elseif c == '"' then
quoted = true
i = parse_quoted(line, i, buf, '"\\$`')
elseif c == "'" then
quoted = true
i = parse_quoted(line, i, buf, nil)
else
table.insert(buf, c)
i = i + 1
end
end
local tok = table.concat(buf)
if not quoted and is_expansion_target(tok) then
local expanded = vim.fn.expand(tok) --[[@as string]]
if expanded ~= "" then
tok = expanded
end
end
table.insert(args, tok)
end
end
return args
end
---@param args string[]
---@param start integer
---@return string?
local function first_positional(args, start)
for i = start, #args do
local a = args[i]
if a:sub(1, 1) ~= "-" then
return a
end
end
end
---@param name string
---@return integer buf
local function place_split(name)
local buf = vim.fn.bufnr("\\V" .. name)
if buf == -1 or not vim.api.nvim_buf_is_loaded(buf) then
buf = util.new_scratch()
pcall(vim.api.nvim_buf_set_name, buf, name)
return buf
end
local win_id = vim.fn.bufwinid(buf)
if win_id ~= -1 then
vim.api.nvim_set_current_win(win_id)
else
util.place_buf(buf, nil)
end
return buf
end
---@param r ow.Git.Repo
---@param args string[]
---@param conf ow.Git.Cmd.SplitHandler
local function run_in_split(r, args, conf)
local cmd = { "git" }
vim.list_extend(cmd, args)
util.exec(cmd, {
cwd = r.worktree,
on_done = function(stdout)
if not stdout then
return
end
local name = "[git " .. table.concat(args, " ") .. "]"
local buf = place_split(name)
repo.bind(buf, r)
object.attach_dispatch(buf)
local state = r:state(buf) --[[@as -nil]]
state.sha = nil
state.parent_sha = nil
if conf.needs_rev then
local user_rev = first_positional(args, 2) or "HEAD"
local sha = r:rev_parse(user_rev, true)
if sha then
state.sha = sha
state.parent_sha = r:rev_parse(user_rev .. "^", true)
end
end
vim.bo[buf].filetype = conf.ft
util.set_buf_lines(buf, 0, -1, util.split_lines(stdout))
end,
})
end
---@param r ow.Git.Repo
---@param args string[]
local function run_to_messages(r, args)
local cmd = { "git" }
vim.list_extend(cmd, args)
vim.system(
cmd,
{ cwd = r.worktree, text = true },
vim.schedule_wrap(function(obj)
local out = vim.trim(obj.stdout or "")
local err = vim.trim(obj.stderr or "")
local chunks = {}
if out ~= "" then
table.insert(chunks, { out })
end
if err ~= "" then
if #chunks > 0 then
table.insert(chunks, { "\n" })
end
table.insert(chunks, { err, "ErrorMsg" })
end
if #chunks == 0 and obj.code ~= 0 then
table.insert(
chunks,
{ "git exited " .. tostring(obj.code), "ErrorMsg" }
)
end
if #chunks > 0 then
vim.api.nvim_echo(chunks, true, {})
end
end)
)
end
---@param args string[]
---@return boolean
local function has_message(args)
for _, a in ipairs(args) do
if
a == "-m"
or a == "--message"
or a:match("^%-%-message=")
or a:match("^%-m")
then
return true
end
end
return false
end
---@param args string[]
function M.run(args)
local r = repo.resolve()
if not r then
util.error("not in a git repository")
return
end
local sub = args[1]
if sub == "commit" and not has_message(args) then
commit.commit({ args = vim.list_slice(args, 2) })
return
end
if sub == "show" then
if #args == 2 and args[2]:find(":", 1, true) then
object.open(r, args[2])
return
end
run_in_split(r, args, { ft = "git", needs_rev = true })
return
end
if sub == "cat-file" then
if #args == 3 and args[2] == "-p" then
object.open(r, args[3])
return
end
run_in_split(r, args, { ft = "git", needs_rev = true })
return
end
local conf = sub and SPLIT_HANDLERS[sub]
if conf then
run_in_split(r, args, conf)
else
run_to_messages(r, args)
end
end
---@param arg_lead string
---@return string[]
function M.complete_rev(arg_lead)
local r = repo.resolve()
if not r then
return {}
end
local stage, stage_path_lead = arg_lead:match("^:([0-3]):(.*)$")
if stage then
local out = util.exec(
{ "git", "ls-files", "--stage" },
{ cwd = r.worktree, silent = true }
)
if not out then
return {}
end
local matches = {}
for _, line in ipairs(util.split_lines(out)) do
local row_stage, row_path = line:match("^%S+ %S+ (%d)\t(.*)$")
if
row_stage == stage
and row_path
and row_path:sub(1, #stage_path_lead) == stage_path_lead
then
table.insert(matches, ":" .. stage .. ":" .. row_path)
end
end
return matches
end
local colon = arg_lead:find(":", 1, true)
if not colon then
local matches = {}
for _, ref in ipairs(r:list_refs()) do
if ref:sub(1, #arg_lead) == arg_lead then
table.insert(matches, ref)
end
end
return matches
end
local rev = arg_lead:sub(1, colon - 1)
local path_lead = arg_lead:sub(colon + 1)
local dir, name_lead = path_lead:match("^(.*/)([^/]*)$")
dir = dir or ""
name_lead = name_lead or path_lead
if rev ~= "" then
local cmd = { "git", "ls-tree", rev }
if dir ~= "" then
table.insert(cmd, dir)
end
local out = util.exec(cmd, { cwd = r.worktree, silent = true })
if not out then
return {}
end
local matches = {}
for _, line in ipairs(util.split_lines(out)) do
local typ, full_path = line:match("^%S+ (%S+) %S+\t(.*)$")
if typ and full_path then
local basename = dir == "" and full_path
or full_path:sub(#dir + 1)
if typ == "tree" then
basename = basename .. "/"
end
if basename:sub(1, #name_lead) == name_lead then
table.insert(matches, rev .. ":" .. dir .. basename)
end
end
end
return matches
end
local cmd = { "git", "ls-files" }
if dir ~= "" then
table.insert(cmd, dir)
end
local out = util.exec(cmd, { cwd = r.worktree, silent = true })
if not out then
return {}
end
local matches = {}
local seen = {}
for _, full_path in ipairs(util.split_lines(out)) do
local rel = dir == "" and full_path or full_path:sub(#dir + 1)
local slash = rel:find("/", 1, true)
local segment = slash and rel:sub(1, slash) or rel
if not seen[segment] and segment:sub(1, #name_lead) == name_lead then
seen[segment] = true
table.insert(matches, ":" .. dir .. segment)
end
end
return matches
end
---@param arg_lead string
---@param cmd_line string
---@return string[]
function M.complete(arg_lead, cmd_line, _)
local rest = cmd_line:gsub("^%s*%S+%s*", "", 1)
local words = vim.split(rest, "%s+", { trimempty = false })
if #words > 1 then
return {}
end
local matches = {}
for _, c in ipairs(git_cmds()) do
if c:sub(1, #arg_lead) == arg_lead then
table.insert(matches, c)
end
end
return matches
end
return M