local util = require("git.util") local M = {} ---@param path string ---@return string? gitdir ---@return string? worktree ---@return string? path function M.resolve(path) path = vim.fn.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, path 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, path end ---@return string? gitdir ---@return string? worktree function M.current_repo() local path = vim.api.nvim_buf_get_name(0) if path == "" or path:match("^%a+://") then path = vim.fn.getcwd() end local gitdir, worktree, _ = M.resolve(path) return gitdir, worktree 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.current_repo() 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