338 lines
9.0 KiB
Lua
338 lines
9.0 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 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
|
|
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 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[]
|
|
---@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 r = repo.resolve()
|
|
if not r 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(r, arg)
|
|
return
|
|
end
|
|
run_in_split(r, 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(r, rev)
|
|
return
|
|
end
|
|
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
|