209 lines
5.5 KiB
Lua
209 lines
5.5 KiB
Lua
local util = require("git.util")
|
|
|
|
local M = {}
|
|
|
|
---@param path string
|
|
---@return string? gitdir
|
|
---@return string? worktree
|
|
function M.resolve(path)
|
|
local found = vim.fs.find(".git", { upward = true, path = path })[1]
|
|
if not found then
|
|
return nil
|
|
end
|
|
local worktree = vim.fs.dirname(found)
|
|
local stat = vim.uv.fs_stat(found)
|
|
if not stat then
|
|
return nil
|
|
end
|
|
if stat.type == "directory" then
|
|
return found, worktree
|
|
end
|
|
local f = io.open(found, "r")
|
|
if not f then
|
|
return nil
|
|
end
|
|
local content = f:read("*a")
|
|
f:close()
|
|
local gitdir = content:match("gitdir:%s*(%S+)")
|
|
if not gitdir then
|
|
util.warning(".git file at %s has no `gitdir:` line", found)
|
|
return nil
|
|
end
|
|
if not gitdir:match("^/") then
|
|
gitdir = vim.fs.joinpath(worktree, gitdir)
|
|
end
|
|
return vim.fs.normalize(gitdir), worktree
|
|
end
|
|
|
|
---@return string? gitdir
|
|
---@return string? worktree
|
|
function M.resolve_cwd()
|
|
local path = vim.api.nvim_buf_get_name(0)
|
|
if path == "" or path:match("^%a+://") then
|
|
path = vim.fn.getcwd()
|
|
end
|
|
return M.resolve(path)
|
|
end
|
|
|
|
---@param path string
|
|
---@return string?
|
|
function M.head(path)
|
|
local gitdir = M.resolve(path)
|
|
if not gitdir then
|
|
return nil
|
|
end
|
|
local f = io.open(vim.fs.joinpath(gitdir, "HEAD"), "r")
|
|
if not f then
|
|
return nil
|
|
end
|
|
local first = f:read("*l")
|
|
f:close()
|
|
if not first then
|
|
return nil
|
|
end
|
|
local branch = first:match("^ref:%s*refs/heads/(%S+)")
|
|
if branch then
|
|
return branch
|
|
end
|
|
local sha = first:match("^(%x+)")
|
|
if sha then
|
|
return sha:sub(1, 7)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---@param worktree string
|
|
---@return string[]
|
|
function M.list_refs(worktree)
|
|
local out = util.exec({
|
|
"git",
|
|
"for-each-ref",
|
|
"--format=%(refname:short)",
|
|
"refs/heads",
|
|
"refs/tags",
|
|
"refs/remotes",
|
|
}, { cwd = worktree, silent = true })
|
|
if not out then
|
|
return {}
|
|
end
|
|
local refs = util.split_lines(out)
|
|
table.insert(refs, 1, "HEAD")
|
|
return refs
|
|
end
|
|
|
|
---@param arg_lead string
|
|
---@return string[]
|
|
function M.complete_rev(arg_lead)
|
|
local _, worktree = M.resolve_cwd()
|
|
if not worktree 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 = 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(M.list_refs(worktree)) 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 = 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 = 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 worktree string
|
|
---@param rev string
|
|
---@param short boolean
|
|
---@return string?
|
|
function M.rev_parse(worktree, rev, short)
|
|
local cmd = { "git", "rev-parse", "--verify", "--quiet" }
|
|
if short then
|
|
table.insert(cmd, "--short")
|
|
end
|
|
table.insert(cmd, rev)
|
|
local stdout = util.exec(cmd, { cwd = worktree, silent = true })
|
|
local trimmed = stdout and vim.trim(stdout) or ""
|
|
return trimmed ~= "" and trimmed or nil
|
|
end
|
|
|
|
return M
|