From a5e2195bf1a3ceeffa21d58a9fb11ea75a824309 Mon Sep 17 00:00:00 2001 From: David Yonge-Mallo Date: Wed, 1 Jul 2026 16:18:33 +0200 Subject: [PATCH] feat(jj): show `change_id` in the file-history panel --- .../tests/functional/jj_adapter_spec.lua | 36 ++++---- lua/diffview/vcs/adapters/jj/init.lua | 83 ++++++++++--------- lua/diffview/vcs/adapters/jj/rev.lua | 8 ++ 3 files changed, 67 insertions(+), 60 deletions(-) diff --git a/lua/diffview/tests/functional/jj_adapter_spec.lua b/lua/diffview/tests/functional/jj_adapter_spec.lua index 43f81521..8ea11d5c 100644 --- a/lua/diffview/tests/functional/jj_adapter_spec.lua +++ b/lua/diffview/tests/functional/jj_adapter_spec.lua @@ -1329,8 +1329,8 @@ describe("diffview.vcs.adapters.jj", function() return remote end - local function head_commit_id() - return repo.jj({ "log", "--no-graph", "-T", "commit_id", "-r", "@" }) + local function head_change_id() + return repo.jj({ "log", "--no-graph", "-T", "change_id", "-r", "@" }) end local function push_main_to(remote) @@ -1353,17 +1353,17 @@ describe("diffview.vcs.adapters.jj", function() -- then add a third commit that's strictly newer than the bookmark. repo.write("a.txt", "alpha\n") repo.jj({ "describe", "-m", "first" }) - local first = head_commit_id() + local first = head_change_id() repo.jj({ "new", "-m", "second" }) repo.write("a.txt", "beta\n") - local second = head_commit_id() + local second = head_change_id() push_main_to(remote) repo.jj({ "new", "-m", "third" }) repo.write("a.txt", "gamma\n") - local third = head_commit_id() + local third = head_change_id() local adapter = repo.adapter() local set = adapter:fh_compute_pushed_set({}) @@ -1409,11 +1409,11 @@ describe("diffview.vcs.adapters.jj", function() -- only one of those paths must drop the unrelated commit. repo.write("kept.txt", "k\n") repo.jj({ "describe", "-m", "touch kept" }) - local kept_sha = head_commit_id() + local kept_sha = head_change_id() repo.jj({ "new", "-m", "touch other" }) repo.write("other.txt", "o\n") - local other_sha = head_commit_id() + local other_sha = head_change_id() push_main_to(remote) @@ -1541,9 +1541,8 @@ describe("diffview.vcs.adapters.jj", function() "A" .. US .. "b.txt" .. US .. "5" .. US .. "0", }, RS) local data = structure_fh_data(build_line({ - "deadbeef", - "qpzqyx", - "cafebabe", + "qpzqyxopwsrlkrnpzrtxqvvywmwmylnu", -- change_id + "yntnsyqklvpsnmywyvsrppowkmqukpyy", -- parent change_id "alice@example.com", "1700000000", "+0200", @@ -1554,8 +1553,8 @@ describe("diffview.vcs.adapters.jj", function() })) assert.is_not_nil(data) - assert.equals("deadbeef", data.right_hash) - assert.equals("cafebabe", data.left_hash) + assert.equals("qpzqyxopwsrlkrnpzrtxqvvywmwmylnu", data.right_hash) + assert.equals("yntnsyqklvpsnmywyvsrppowkmqukpyy", data.left_hash) assert.is_nil(data.merge_hash) assert.equals("alice@example.com", data.author) assert.equals(1700000000, data.time) @@ -1569,10 +1568,9 @@ describe("diffview.vcs.adapters.jj", function() }, data.namestat) end) - it("returns nil for the root commit (null-tree hash)", function() + it("returns nil for the root commit (null change_id)", function() local line = build_line({ - JjRev.NULL_TREE_SHA, - "zzzz", + JjRev.NULL_CHANGE_ID, "", "", "0", @@ -1585,11 +1583,10 @@ describe("diffview.vcs.adapters.jj", function() assert.is_nil(structure_fh_data(line)) end) - it("drops a null-tree parent so the entry is treated as a root-child", function() + it("drops the null-root parent so the entry is treated as a root-child", function() local line = build_line({ "abc123", - "qpzqyx", - JjRev.NULL_TREE_SHA, -- parent is the synthetic root + JjRev.NULL_CHANGE_ID, -- parent is the synthetic root "", "1700000000", "+0000", @@ -1607,7 +1604,6 @@ describe("diffview.vcs.adapters.jj", function() -- the previous line-per-field parser. local line = build_line({ "abc", - "xyz", "parent", "", -- no author email "1700000000", @@ -1632,7 +1628,6 @@ describe("diffview.vcs.adapters.jj", function() it("yields an empty namestat when the commit has no diff", function() local data = structure_fh_data(build_line({ "abc", - "xyz", "parent", "", "1700000000", @@ -1658,7 +1653,6 @@ describe("diffview.vcs.adapters.jj", function() }, RS) local data = structure_fh_data(build_line({ "abc", - "xyz", "parent", "", "1700000000", diff --git a/lua/diffview/vcs/adapters/jj/init.lua b/lua/diffview/vcs/adapters/jj/init.lua index 83904ed6..8ffb2fad 100644 --- a/lua/diffview/vcs/adapters/jj/init.lua +++ b/lua/diffview/vcs/adapters/jj/init.lua @@ -808,12 +808,12 @@ end ---@field merged_set? table Commit hashes reachable from `trunk()`, restricted to commits touching `path_args`. Absent unless `subject_highlight = "merge_aware"` (or the query failed). ---Run `jj log -r ` (optionally intersected with `files(...)` when ----`path_args` is non-empty) and collect the resulting commit hashes into a set. +---`path_args` is non-empty) and collect the resulting change_ids into a set. ---Each path is fileset-escaped via `quote_path_args` so literal paths ---containing fileset metacharacters (`(`, `|`, ...) don't get parsed as ----operators. The all-zeros root sha that jj emits when the revset (e.g. +---operators. The all-`z` synthetic root that jj emits when the revset (e.g. ---`::trunk()`) resolves to nothing is stripped, since downstream comparisons ----key off real commit hashes only. Returns `nil` if jj exits non-zero. +---key off real change_ids only. Returns `nil` if jj exits non-zero. ---@param self JjAdapter ---@param base_revset string ---@param path_args string[] @@ -830,7 +830,7 @@ local function fh_collect_revset(self, base_revset, path_args, label) "log", "--no-graph", "-T", - [[ commit_id ++ "\n" ]], + [[ change_id ++ "\n" ]], "-r", revset, }, { @@ -843,9 +843,9 @@ local function fh_collect_revset(self, base_revset, path_args, label) end local set = {} - for _, sha in ipairs(out) do - if #sha > 0 and sha ~= JjRev.NULL_TREE_SHA then - set[sha] = true + for _, cid in ipairs(out) do + if #cid > 0 and cid ~= JjRev.NULL_CHANGE_ID then + set[cid] = true end end return set @@ -993,22 +993,27 @@ end ---files show as `0/0`), so the swap doesn't change which files surface in ---the panel. --- +---Uses `change_id` (not `commit_id`) as the primary identifier and for parent +---links. jj resolves either form as a revset, but change_ids are the stable +---jj-native identity (they survive amends, rebases, and `describe`) and match +---what `jj log` shows, so downstream operations (yank-and-paste into `jj -r`, +---the commit-log panel, single-commit inspection) all round-trip against the +---identifier the user sees in the panel. +--- ---Field order, by index: ---- 1. commit_id (full hash) ---- 2. change_id ---- 3. parent commit_ids (space-separated; empty for the root commit) ---- 4. author email ---- 5. author timestamp (unix epoch, UTC) ---- 6. author timestamp offset (e.g. `+0200`, `-0500`) ---- 7. relative date (e.g. `2 minutes ago`) ---- 8. ref names (comma-separated local bookmarks + tags) ---- 9. subject (first line of description) ---- 10. files blob (`\x1e`-separated entries, each ---- `\x1f\x1f\x1f`) +--- 1. change_id (full k-z form) +--- 2. parent change_ids (space-separated; empty for the root commit) +--- 3. author email +--- 4. author timestamp (unix epoch, UTC) +--- 5. author timestamp offset (e.g. `+0200`, `-0500`) +--- 6. relative date (e.g. `2 minutes ago`) +--- 7. ref names (comma-separated local bookmarks + tags) +--- 8. subject (first line of description) +--- 9. files blob (`\x1e`-separated entries, each +--- `\x1f\x1f\x1f`) local FH_TEMPLATE = table.concat({ - [[ commit_id ++ "\x01" ]], - [[ ++ change_id ++ "\x01" ]], - [[ ++ parents.map(|p| p.commit_id()).join(" ") ++ "\x01" ]], + [[ change_id ++ "\x01" ]], + [[ ++ parents.map(|p| p.change_id()).join(" ") ++ "\x01" ]], [[ ++ author.email() ++ "\x01" ]], [[ ++ author.timestamp().format("%s") ++ "\x01" ]], [[ ++ author.timestamp().format("%z") ++ "\x01" ]], @@ -1027,27 +1032,27 @@ local FH_TEMPLATE = table.concat({ ---@return table? local function structure_fh_data(line) local fields = vim.split(line, "\x01", { plain = true }) - local commit_id = fields[1] - if not commit_id or commit_id == "" then + local change_id = fields[1] + if not change_id or change_id == "" then return nil end - -- The root commit's hash is all zeros and has no description, parents, or - -- diff entries. Skip it: it doesn't belong in the file-history list. - if commit_id == JjRev.NULL_TREE_SHA then + -- The root commit's change_id is all `z`s and has no description, parents, + -- or diff entries. Skip it: it doesn't belong in the file-history list. + if change_id == JjRev.NULL_CHANGE_ID then return nil end - -- Strip empty and null-tree parent slots: empty appears when the parents - -- field rendered to `""` (no parents on the root commit); null-tree - -- appears when jj surfaces the synthetic zero-hash root as a parent. + -- Strip empty and null-root parent slots: empty appears when the parents + -- field rendered to `""` (no parents on the root commit); the null change + -- appears when jj surfaces the synthetic root as a parent. local function clean_parent(p) - if not p or p == "" or p == JjRev.NULL_TREE_SHA then + if not p or p == "" or p == JjRev.NULL_CHANGE_ID then return nil end return p end - local parents = utils.str_split(fields[3] or "") + local parents = utils.str_split(fields[2] or "") local left_hash = clean_parent(parents[1]) local merge_hash = clean_parent(parents[2]) @@ -1058,7 +1063,7 @@ local function structure_fh_data(line) -- `stats = nil`, matching how the git adapter handles `- -` numstat for -- binaries (see `git/parser.lua:parse_namestat_entry`). local namestat = {} - local files_blob = fields[10] or "" + local files_blob = fields[9] or "" if files_blob ~= "" then for _, entry in ipairs(vim.split(files_blob, "\x1e", { plain = true })) do local parts = vim.split(entry, "\x1f", { plain = true }) @@ -1080,15 +1085,15 @@ local function structure_fh_data(line) end return { - right_hash = commit_id, + right_hash = change_id, left_hash = left_hash, merge_hash = merge_hash, - author = fields[4] or "", - time = tonumber(fields[5] or "0") or 0, - time_offset = fields[6] or "", - rel_date = fields[7] or "", - ref_names = fields[8] or "", - subject = fields[9] or "", + author = fields[3] or "", + time = tonumber(fields[4] or "0") or 0, + time_offset = fields[5] or "", + rel_date = fields[6] or "", + ref_names = fields[7] or "", + subject = fields[8] or "", namestat = namestat, } end diff --git a/lua/diffview/vcs/adapters/jj/rev.lua b/lua/diffview/vcs/adapters/jj/rev.lua index d4068f90..9ff3e075 100644 --- a/lua/diffview/vcs/adapters/jj/rev.lua +++ b/lua/diffview/vcs/adapters/jj/rev.lua @@ -7,8 +7,16 @@ local M = {} ---@class JjRev : Rev local JjRev = oop.create_class("JjRev", Rev) +-- 40-hex-zero commit_id emitted by jj for the null tree (added by the +-- git-backend for the empty root parent). Used by the null-tree Rev that +-- backs the left side of an initial-commit diff. JjRev.NULL_TREE_SHA = "0000000000000000000000000000000000000000" +-- 32-`z` change_id emitted by jj for the synthetic root commit. Used by the +-- file-history parser to strip the root from its parent list and from the +-- pushed/merged reachability sets, which key by change_id. +JjRev.NULL_CHANGE_ID = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + ---@param rev_type RevType ---@param revision? string|number ---@param track_head? boolean