294 lines
8.1 KiB
Lua
294 lines
8.1 KiB
Lua
local git = require("git")
|
|
local repo = require("git.repo")
|
|
local util = require("git.util")
|
|
|
|
local M = {}
|
|
|
|
---@class ow.Git.SplitHandler
|
|
---@field ft string
|
|
---@field needs_ref boolean?
|
|
|
|
---@type table<string, ow.Git.SplitHandler>
|
|
local SPLIT_HANDLERS = {
|
|
log = { ft = "git" },
|
|
show = { ft = "git", needs_ref = true },
|
|
["cat-file"] = { ft = "git", needs_ref = true },
|
|
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
|
|
|
|
---Prime `cached_cmds` asynchronously so the first `:G <Tab>` doesn't block.
|
|
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
|
|
|
|
---Open `<ref>:<path>` in a split via the `git://` BufReadCmd loader.
|
|
---Resolves to a sha first so the URI stays stable if the ref moves.
|
|
---@param worktree string
|
|
---@param user_ref string
|
|
---@param path string
|
|
local function show_file_in_split(worktree, user_ref, path)
|
|
local label = repo.rev_parse(worktree, user_ref, true) or user_ref
|
|
local uri = "git://" .. label .. ":" .. path
|
|
local buf = vim.fn.bufadd(uri)
|
|
vim.b[buf].git_worktree = worktree
|
|
vim.cmd("split " .. vim.fn.fnameescape(uri))
|
|
end
|
|
|
|
---@param worktree string
|
|
---@param args string[]
|
|
---@param conf ow.Git.SplitHandler
|
|
local function run_in_split(worktree, args, conf)
|
|
-- `<ref>:<path>` is a file lookup; the URI must carry the path so
|
|
-- filetype detection has something to match against.
|
|
if args[1] == "show" then
|
|
local arg = first_positional(args, 2)
|
|
if arg then
|
|
local ref, path = arg:match("^(.-):(.+)$")
|
|
if ref then
|
|
---@cast path -nil
|
|
show_file_in_split(worktree, ref, path)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- `cat-file -p <sha>` routes to the gitobject viewer so commits get
|
|
-- the full message + diff view and other types render via cat-file.
|
|
-- Other modes (-t, -s, -e) fall through to the generic dump.
|
|
if args[1] == "cat-file" and vim.list_contains(args, "-p") then
|
|
local ref = first_positional(args, 2)
|
|
if ref then
|
|
require("git.object").open_object(worktree, ref)
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Bracket naming so the buffer name doesn't pretend to be a real
|
|
-- file path (and doesn't collide with the `git://<revspec>` URI
|
|
-- form used by the BufReadCmd loader). Reused on re-run so a
|
|
-- second `:G show HEAD` refreshes the existing buffer in place
|
|
-- instead of leaving a nameless duplicate behind.
|
|
local name = "[git " .. table.concat(args, " ") .. "]"
|
|
local buf = vim.fn.bufnr(name)
|
|
if buf == -1 or not vim.api.nvim_buf_is_loaded(buf) then
|
|
buf = git.new_scratch()
|
|
pcall(vim.api.nvim_buf_set_name, buf, name)
|
|
else
|
|
local win_id = vim.fn.bufwinid(buf)
|
|
if win_id ~= -1 then
|
|
vim.api.nvim_set_current_win(win_id)
|
|
else
|
|
vim.api.nvim_open_win(buf, true, {
|
|
split = vim.o.splitbelow and "below" or "above",
|
|
})
|
|
end
|
|
vim.bo[buf].modifiable = true
|
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, {})
|
|
end
|
|
|
|
vim.b[buf].git_worktree = worktree
|
|
vim.b[buf].git_ref = nil
|
|
vim.b[buf].git_parent_ref = nil
|
|
if conf.needs_ref then
|
|
local user_ref = first_positional(args, 2) or "HEAD"
|
|
local sha = repo.rev_parse(worktree, user_ref, true)
|
|
if sha then
|
|
vim.b[buf].git_ref = sha
|
|
vim.b[buf].git_parent_ref =
|
|
repo.rev_parse(worktree, user_ref .. "^", true)
|
|
end
|
|
end
|
|
vim.bo[buf].filetype = conf.ft
|
|
|
|
local cmd = { "git" }
|
|
vim.list_extend(cmd, args)
|
|
vim.system(
|
|
cmd,
|
|
{ cwd = worktree, text = true },
|
|
vim.schedule_wrap(function(obj)
|
|
if not vim.api.nvim_buf_is_valid(buf) then
|
|
return
|
|
end
|
|
local content = (obj.stdout or "") .. (obj.stderr or "")
|
|
vim.bo[buf].modifiable = true
|
|
vim.api.nvim_buf_set_lines(
|
|
buf,
|
|
0,
|
|
-1,
|
|
false,
|
|
util.split_lines(content)
|
|
)
|
|
vim.bo[buf].modifiable = false
|
|
vim.bo[buf].modified = false
|
|
if obj.code ~= 0 then
|
|
util.error(
|
|
"git %s failed: %s",
|
|
args[1] or "",
|
|
vim.trim(obj.stderr or "")
|
|
)
|
|
end
|
|
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
|
|
require("git.commit").commit({ amend = has_flag(args, "--amend") })
|
|
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
|