Files
nvim/lua/git/cmd.lua
T

263 lines
6.6 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.SplitHandler
---@field ft string
---@field needs_rev boolean?
---@type table<string, ow.Git.SplitHandler>
local SPLIT_HANDLERS = {
log = { ft = "git" },
diff = { ft = "diff" },
}
---@type string[]?
local cached_cmds
---@param result vim.SystemCompleted
local function populate_cached_cmds(result)
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)
end
local function prefetch_cmds()
vim.system(
{ "git", "--list-cmds=main,others,alias" },
{ text = true },
vim.schedule_wrap(populate_cached_cmds)
)
end
---@return string[]
local function git_cmds()
if cached_cmds then
return cached_cmds
end
populate_cached_cmds(
vim.system({ "git", "--list-cmds=main,others,alias" }, { text = true })
:wait()
)
return cached_cmds or {}
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 worktree string
---@param args string[]
---@param conf ow.Git.SplitHandler
local function run_in_split(worktree, args, conf)
local cmd = { "git" }
vim.list_extend(cmd, args)
util.exec(cmd, {
cwd = worktree,
on_done = function(stdout)
if not stdout then
return
end
local name = "[git " .. table.concat(args, " ") .. "]"
local buf = place_split(name)
vim.b[buf].git_worktree = worktree
vim.b[buf].git_sha = nil
vim.b[buf].git_parent_sha = nil
if conf.needs_rev then
local user_rev = first_positional(args, 2) or "HEAD"
local sha = repo.rev_parse(worktree, user_rev, true)
if sha then
vim.b[buf].git_sha = sha
vim.b[buf].git_parent_sha =
repo.rev_parse(worktree, user_rev .. "^", true)
end
end
vim.bo[buf].filetype = conf.ft
vim.bo[buf].modifiable = true
vim.api.nvim_buf_set_lines(
buf,
0,
-1,
false,
util.split_lines(stdout)
)
vim.bo[buf].modifiable = false
vim.bo[buf].modified = false
end,
})
end
---@param worktree string
---@param args string[]
local function run_to_messages(worktree, args)
local cmd = { "git" }
vim.list_extend(cmd, args)
vim.system(
cmd,
{ cwd = 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[]
---@param flag string
---@return boolean
local function has_flag(args, flag)
for _, a in ipairs(args) do
if a == flag then
return true
end
end
return false
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 _, worktree = repo.resolve_cwd()
if not worktree then
util.warning("not in a git repository")
return
end
local sub = args[1]
if sub == "commit" and not has_message(args) then
commit.commit({ amend = has_flag(args, "--amend") })
return
end
if sub == "show" then
local arg = first_positional(args, 2)
if arg and arg:find(":", 1, true) then
object.open_object(worktree, arg)
return
end
run_in_split(worktree, args, { ft = "git", needs_rev = true })
return
end
if sub == "cat-file" then
if vim.list_contains(args, "-p") then
local rev = first_positional(args, 2)
if rev then
object.open_object(worktree, rev)
return
end
end
run_in_split(worktree, args, { ft = "git", needs_rev = true })
return
end
local conf = sub and SPLIT_HANDLERS[sub]
if conf then
run_in_split(worktree, args, conf)
else
run_to_messages(worktree, args)
end
end
---@param arg_lead string
---@param cmd_line string
---@return string[]
local function 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
function M.setup()
prefetch_cmds()
vim.api.nvim_create_user_command("G", function(opts)
M.run(opts.fargs)
end, {
nargs = "*",
complete = complete,
desc = "Run git",
})
end
return M