Something went wrong. Try again.
jujutsu file annotations for neovim (like blame)
Something went wrong. Try again.
5.8 kB · 214 lines
Lua
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215local notify = require("jjannotate.util").notify
---@param epoch integer---@return stringlocal function relative_time(epoch) local seconds = os.time() - epoch if seconds < 1 then return "just now" end
local units = { { 31536000, "year" }, { 2592000, "month" }, { 604800, "week" }, { 86400, "day" }, { 3600, "hour" }, { 60, "minute" }, { 1, "second" }, } for _, unit in ipairs(units) do local n = math.floor(seconds / unit[1]) if n >= 1 then return string.format("%d %s%s ago", n, unit[2], n == 1 and "" or "s") end end return "just now"end
---@param raw string---@return jjannotate.AnnotateResultlocal function parse(raw) local lines = {} ---@type jjannotate.Line[] local longest_timestamp = 0 local last_change_id ---@type string | nil local row_index = {} ---@type jjannotate.Line[] local change_id_index = {} ---@type table<string, jjannotate.Line[]>
local change_id, author, timestamp ---@type string?, string?, string? local row = 0
for _, line in ipairs(vim.split(raw, "\n")) do local sha = line:match("^(%x+) %d+ %d+") if sha then change_id = sha:sub(1, 8) author, timestamp = nil, nil elseif line:match("^author ") then author = line:sub(8) elseif line:match("^author%-time ") then local epoch = tonumber(line:match("^author%-time (%d+)")) timestamp = epoch and relative_time(epoch) or "" elseif line:sub(1, 1) == "\t" then local parsed = { range = { start = row, _end = row }, change_id = change_id or "", author = author or "", timestamp = timestamp or "", }
if parsed.change_id == last_change_id then lines[#lines].range._end = row row_index[row] = lines[#lines] else table.insert(lines, parsed) longest_timestamp = math.max(longest_timestamp, #parsed.timestamp) row_index[row] = parsed local cid = parsed.change_id if not change_id_index[cid] then change_id_index[cid] = {} end table.insert(change_id_index[cid], parsed) end
last_change_id = parsed.change_id row = row + 1 end end
return { lines = lines, longest_timestamp = longest_timestamp, row_index = row_index, change_id_index = change_id_index, }end
---@param repo string repo root---@param path string path within `repo`---@param revision string---@param cb fun(result: jjannotate.AnnotateResult)local function annotate(repo, path, revision, cb) if path == "" then notify("Can't annotate a scratch buffer", vim.log.levels.WARN) cb(nil) return end
vim.system({ "git", "blame", "--line-porcelain", revision, "--", path, }, { cwd = repo, text = true }, function(out) if out.code ~= 0 then notify( string.format( "Failed to `git blame %s` (%d): %s", path, out.code, out.stderr or "no stderr" ), vim.log.levels.ERROR ) cb(nil) return end
if not out.stdout or out.stdout == "" then notify("No output from `git blame`", vim.log.levels.ERROR) cb(nil) return end
cb(parse(out.stdout)) end)end
---@param repo string repo root---@param revision stringlocal function show(repo, revision) vim.cmd.tabnew() vim.fn.jobstart({ "git", "--no-pager", "show", revision }, { term = true, cwd = repo })
local buf = vim.api.nvim_get_current_buf() vim.bo[buf].buflisted = false vim.api.nvim_buf_set_name(buf, string.format("jjannotate://show/%s", revision)) vim.keymap.set("n", "q", "<cmd>bwipe!<cr>", { buffer = buf, desc = "Close" }) vim.keymap.set("t", "q", "<C-\\><C-n>:bwipe!<cr>", { buffer = buf, desc = "Close" })end
---@param repo string repo root---@param path string path within `repo`---@param revision string---@param cb fun(contents: string?)local function file_contents(repo, path, revision, cb) vim.system({ "git", "--no-pager", "show", string.format("%s:%s", revision, path), }, { cwd = repo, text = true }, function(out) if out.code ~= 0 then notify( string.format( "Failed to get file contents for %s@%s (%d): %s", path, revision, out.code, out.stderr or "no stderr" ), vim.log.levels.ERROR ) cb(nil) return end
cb(out.stdout) end)end
---@param out vim.SystemCompleted---@return string?local function parse_resolve_revision(out) local id = out.stdout and vim.trim(out.stdout) or "" if out.code ~= 0 or id == "" then notify( string.format("Failed to resolve revision (%d): %s", out.code, out.stderr or "no stderr"), vim.log.levels.ERROR ) return end return idend
--- Resolve a revision to a single commit hash. If `cb` is passed, it is called--- with the result. Otherwise, this blocks until the result is returned.---@overload fun(repo: string, revision: string?): string?---@overload fun(repo: string, revision: string?, cb: fun(commit: string?))---@param repo string repo root---@param revision string? defaults to `HEAD`---@param cb fun(commit: string?)?---@return string?local function resolve_revision(repo, revision, cb) revision = revision or "HEAD"
local obj = vim.system({ "git", "rev-parse", "--verify", revision .. "^{commit}", }, { cwd = repo, text = true }, function(out) if cb then cb(parse_resolve_revision(out)) end end)
if not cb then local out = obj:wait() return parse_resolve_revision(out) endend
---@type jjannotate.Backendreturn { name = "git", marker = ".git", annotate = annotate, show = show, file_contents = file_contents, resolve_revision = resolve_revision,}