local log = require("log") local repo = require("git.repo") local util = require("util") local M = {} ---@class ow.Git.SplitHandler ---@field ft string ---@field needs_ref boolean? ---@type table local SPLIT_HANDLERS = { log = { ft = "gitlog" }, 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 log.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 ` doesn't block. local function prefetch_cmds() vim.system( { "git", "--list-cmds=main,others,alias" }, { text = true }, function(result) vim.schedule(function() populate_cached_cmds(result) end) end ) 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 ---@param worktree string ---@param args string[] ---@param conf ow.Git.SplitHandler local function run_in_split(worktree, args, conf) vim.cmd("new") local buf = vim.api.nvim_get_current_buf() vim.bo[buf].buftype = "nofile" vim.bo[buf].bufhidden = "hide" vim.bo[buf].swapfile = false vim.bo[buf].modifiable = false vim.b[buf].git_worktree = worktree 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 pcall( vim.api.nvim_buf_set_name, buf, "git://" .. (sha or user_ref) .. "/" ) end vim.bo[buf].filetype = conf.ft local cmd = { "git" } vim.list_extend(cmd, args) vim.system(cmd, { cwd = worktree, text = true }, function(obj) vim.schedule(function() 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 log.error( "git %s failed: %s", args[1] or "", vim.trim(obj.stderr or "") ) end 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 }, function(obj) vim.schedule(function() 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) 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 path = vim.api.nvim_buf_get_name(0) if path == "" then path = vim.fn.getcwd() end local _, worktree = repo.resolve(path) if not worktree then log.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