local git = require("git") 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 = "git" }, 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 }, vim.schedule_wrap(populate_cached_cmds) ) 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 ---Open `:` in a split via the `git://` BufReadCmd loader. ---Resolves to a sha first so the URI stays stable if the ref moves. ---@param worktree string ---@param user_ref string ---@param path string local function show_file_in_split(worktree, user_ref, path) repo.rev_parse(worktree, user_ref, true, function(sha) local label = sha or user_ref local uri = "git://" .. label .. "//" .. path local buf = vim.fn.bufadd(uri) vim.b[buf].git_worktree = worktree vim.cmd("split " .. vim.fn.fnameescape(uri)) end) end ---@param worktree string ---@param args string[] ---@param conf ow.Git.SplitHandler local function run_in_split(worktree, args, conf) -- `:` is a file lookup; the URI must carry the path so -- filetype detection has something to match against. if args[1] == "show" then local arg = first_positional(args, 2) if arg then local ref, path = arg:match("^(.-):(.+)$") if ref then ---@cast path -nil show_file_in_split(worktree, ref, path) return end end end local buf = git.new_scratch() vim.b[buf].git_worktree = worktree if conf.needs_ref then local user_ref = first_positional(args, 2) or "HEAD" local function apply_name(label) if not vim.api.nvim_buf_is_valid(buf) then return end pcall(vim.api.nvim_buf_set_name, buf, "git://" .. label .. "//") vim.bo[buf].filetype = conf.ft end repo.rev_parse(worktree, user_ref, true, function(sha) if not sha then apply_name(user_ref) return end repo.rev_parse(worktree, user_ref .. "^", true, function(parent) if not vim.api.nvim_buf_is_valid(buf) then return end vim.b[buf].git_ref = sha vim.b[buf].git_parent_ref = parent apply_name(sha) end) end) else vim.bo[buf].filetype = conf.ft end local cmd = { "git" } vim.list_extend(cmd, args) vim.system( cmd, { cwd = worktree, text = true }, vim.schedule_wrap(function(obj) 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 ---@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 }, 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 _, worktree = repo.resolve_cwd() 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