local 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.AnnotateResult local 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 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 string local 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", "bwipe!", { buffer = buf, desc = "Close" }) vim.keymap.set("t", "q", ":bwipe!", { 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) end end ---@type jjannotate.Backend return { name = "jj", marker = ".jj", annotate = annotate, show = show, file_contents = file_contents, resolve_revision = resolve_revset, }