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 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 util.set_buf_lines(buf, 0, -1, util.split_lines(stdout)) 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.error("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