local repo = require("git.repo") local util = require("git.util") local M = {} M.URI_PREFIX = "gitlog://" local LOG_FORMAT = "%h %ad {%an}%d %s" local cr = vim.api.nvim_replace_termcodes("", true, false, true) ---@param buf integer local function attach_dispatch(buf) vim.keymap.set("n", "", function() local r = repo.resolve(buf) -- Anchor past the leading graph chars (matches the leading sha -- column, not any hex word that happens to appear later in the -- subject). local sha = r and vim.api .nvim_get_current_line() :match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)") if sha then ---@cast r -nil require("git.object").open(r, sha, { split = false }) else vim.api.nvim_feedkeys(cr, "n", false) end end, { buffer = buf, silent = true, desc = "Open commit" }) end ---@param worktree string ---@param max_count integer? ---@return string? local function fetch(worktree, max_count) local cmd = { "git", "log", "--graph", "--all", "--decorate", "--date=short", "--format=format:" .. LOG_FORMAT, } if max_count then table.insert(cmd, "--max-count=" .. max_count) end return util.exec(cmd, { cwd = worktree }) end ---@param buf integer ---@param r ow.Git.Repo local function populate(buf, r) local state = r:state(buf) if not state then return end local stdout = fetch(r.worktree, state.log_max_count) if not stdout then return end local new_lines = util.split_lines(stdout) local old_str = table.concat( vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n" ) .. "\n" local new_str = table.concat(new_lines, "\n") .. "\n" local hunks = vim.text.diff(old_str, new_str, { result_type = "indices", algorithm = "histogram", }) ---@cast hunks [integer, integer, integer, integer][] if #hunks == 0 then return end for i = #hunks, 1, -1 do local sa, ca, sb, cb = unpack(hunks[i]) local start = ca == 0 and sa or sa - 1 util.buf_set_lines( buf, start, start + ca, false, vim.list_slice(new_lines, sb, sb + cb - 1) ) end end ---@param buf integer function M.read_uri(buf) local name = vim.api.nvim_buf_get_name(buf) local worktree = name:sub(#M.URI_PREFIX + 1) if worktree == "" then return end local r = repo.resolve(worktree) if not r then return end repo.bind(buf, r) vim.bo[buf].swapfile = false vim.bo[buf].bufhidden = "hide" vim.bo[buf].buftype = "nofile" vim.bo[buf].modifiable = false if vim.bo[buf].filetype ~= "gitlog" then vim.bo[buf].filetype = "gitlog" end attach_dispatch(buf) populate(buf, r) end ---@class ow.Git.Log.OpenOpts ---@field max_count integer? ---@type table M.opt_parsers = { max_count = tonumber, } ---@param opts ow.Git.Log.OpenOpts? function M.open(opts) opts = opts or {} local r = repo.resolve() if not r then util.error("not in a git repository") return end local buf = vim.fn.bufadd(M.URI_PREFIX .. r.worktree) repo.bind(buf, r) local state = r:state(buf) --[[@as -nil]] state.log_max_count = opts.max_count local was_loaded = vim.api.nvim_buf_is_loaded(buf) local win = vim.fn.bufwinid(buf) if win == -1 then util.place_buf(buf, nil) else vim.api.nvim_set_current_win(win) end if was_loaded then populate(buf, r) end end ---@param cmd_opts table function M.run_glog(cmd_opts) local parsed = { max_count = 1000 } for _, a in ipairs(cmd_opts.fargs) do local k, v = a:match("^([%w_]+)=(.*)$") if not k then util.error("invalid argument: %s", a) return end ---@cast v -nil local parser = M.opt_parsers[k] if parser then local value = parser(v) if value ~= nil then parsed[k] = value end end end M.open(parsed) end ---@param arg_lead string ---@return string[] function M.complete_glog(arg_lead) local matches = {} for k in pairs(M.opt_parsers) do local prefix = k .. "=" if prefix:sub(1, #arg_lead) == arg_lead then table.insert(matches, prefix) end end table.sort(matches) return matches end repo.on_uri_refresh(M.URI_PREFIX, populate) return M