Something went wrong. Try again.
jujutsu file annotations for neovim (like blame)
Something went wrong. Try again.
5.5 kB · 221 lines
Lua
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222local notify = require("jjannotate.util").notify
local SEP = "\x1f"
local template = string.format( [[ join("%s", commit.change_id().shortest(8), commit.author().name(), commit_timestamp(commit).local().ago() ) ++ "\n"]], SEP)
---@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[]>
for i, line in ipairs(vim.split(vim.trim(raw), "\n")) do if #line > 0 then local row = i - 1 local components = vim.split(vim.trim(line), SEP) local parsed = { range = { start = row, _end = row }, change_id = components[1], author = components[2], timestamp = components[3], }
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 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 revset string---@param cb fun(result: jjannotate.AnnotateResult)local function annotate(repo, path, revset, cb) if path == "" then notify("Can't annotate a scratch buffer", vim.log.levels.WARN) cb(nil) return end
vim.system({ "jj", "file", "annotate", path, "-r", revset, "-T", template, "--no-pager", "--ignore-working-copy", }, { cwd = repo, text = true }, function(out) if out.code ~= 0 then notify( string.format( "Failed to `jj file annotate %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 `jj file annotate`", vim.log.levels.ERROR) cb(nil) return end
cb(parse(out.stdout)) end)end
---@param repo string repo root---@param revset stringlocal function show(repo, revset) vim.cmd.tabnew() vim.fn.jobstart( { "jj", "show", "--no-pager", "--ignore-working-copy", revset }, { 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", revset)) 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 revset string---@param cb fun(contents: string?)local function file_contents(repo, path, revset, cb) vim.system({ "jj", "file", "show", path, "-r", revset, "--no-pager", "--ignore-working-copy", }, { 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, revset, 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_revset(out) if out.code ~= 0 or not out.stdout then notify( string.format( "Failed to get working copy change ID (%d): %s", out.code, out.stderr or "no stderr" ), vim.log.levels.ERROR ) return end
local ids = vim.split(vim.trim(out.stdout), " ", { trimempty = true }) if #ids == 0 then return end if #ids > 1 then notify("Revset must resolve to a single change, got " .. #ids, vim.log.levels.ERROR) return end
return ids[1]end
--- Resolve a revset to a single change ID. If the revset resolves to more than one change, returns nil.--- If `cb` is passed, it is called with the result. Otherwise, this blocks until the result is returned.---@overload fun(repo: string, revset: string?): string?---@overload fun(repo: string, revset: string?, cb: fun(change_id: string?))---@param repo string repo root---@param revset string? defaults to `@`---@param cb fun(change_id: string?)?---@return string?local function resolve_revset(repo, revset, cb) revset = revset or "@"
local obj = vim.system({ "jj", "log", "-r", revset, "-T", "change_id.shortest(8) ++ ' '", "--no-graph", "--no-pager", "--ignore-working-copy", }, { cwd = repo, text = true }, function(out) if cb then cb(parse_resolve_revset(out)) end end)
if not cb then local out = obj:wait() return parse_resolve_revset(out) endend
---@type jjannotate.Backendreturn { name = "jj", marker = ".jj", annotate = annotate, show = show, file_contents = file_contents, resolve_revision = resolve_revset,}