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.SplitHandler ---@field ft string ---@field needs_ref boolean? ---Subcommands whose output goes to a buffer. Subcommands with their ---own dispatch (`commit`, `show`, `cat-file`) call `run_in_split` ---directly with a one-off conf. ---@type table local SPLIT_HANDLERS = { log = { ft = "git" }, diff = { ft = "diff" }, } ---@type string[]? local cached_cmds ---@param result vim.SystemCompleted local function populate_cached_cmds(result) 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) 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 ---Find or create the named scratch buffer and place it in a window. ---@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 vim.api.nvim_open_win(buf, true, { split = vim.o.splitbelow and "below" or "above", }) end return buf end ---Run `git ` async. On success, drop the output into a named ---scratch split (creating or reusing as needed). On failure, `util.exec` ---notifies and the split is never opened, so a bad ref doesn't leave a ---stray buffer behind. ---@param worktree string ---@param args string[] ---@param conf ow.Git.SplitHandler local function run_in_split(worktree, args, conf) local cmd = { "git" } vim.list_extend(cmd, args) util.exec(cmd, { cwd = worktree, on_done = function(stdout) if not stdout then return end local name = "[git " .. table.concat(args, " ") .. "]" local buf = place_split(name) vim.b[buf].git_worktree = worktree vim.b[buf].git_ref = nil vim.b[buf].git_parent_ref = nil 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 end vim.bo[buf].filetype = conf.ft vim.bo[buf].modifiable = true vim.api.nvim_buf_set_lines( buf, 0, -1, false, util.split_lines(stdout) ) vim.bo[buf].modifiable = false vim.bo[buf].modified = false 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 util.warning("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 -- `:G show :` opens the blob via the BufReadCmd loader -- so the URI carries the path and filetype detection has something -- to match against. Other show invocations dump output to a buffer. if sub == "show" then local arg = first_positional(args, 2) if arg and arg:find(":", 1, true) then object.open_object(worktree, arg) return end run_in_split(worktree, args, { ft = "git", needs_ref = true }) return end -- `:G cat-file -p ` routes to the gitobject viewer so commits -- get the full message + diff view and other types render via -- cat-file. Other modes (-t, -s, -e) dump to a buffer. if sub == "cat-file" then if vim.list_contains(args, "-p") then local ref = first_positional(args, 2) if ref then object.open_object(worktree, ref) return end end run_in_split(worktree, args, { ft = "git", needs_ref = true }) 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