Skip to content

Commit c457419

Browse files
vramanadlyongemallo
authored andcommitted
feat: add Jujutsu (jj) VCS adapter
Add a full JjAdapter for DiffviewOpen with rev parsing, bookmark handling, main→master fallback, merge-base resolution via triple-dot, refresh-revs support, and working-tree diff content refresh. Includes documentation, tests, and the force parameter on FileEntry:destroy() needed by the replace-noop refresh path. Cherry-picked from vramana/diffview.nvim (commits 600a120..fda5b1a).
1 parent e455e14 commit c457419

15 files changed

Lines changed: 1148 additions & 34 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/doc/tags
22
/.tests/
33
/.dev/
4+
/.nvimlog

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ for any git rev.
1313
Vim's diff mode is pretty good, but there is no convenient way to quickly bring
1414
up all modified files in a diffsplit. This plugin aims to provide a simple,
1515
unified, single tabpage interface that lets you easily review all changed files
16-
for any git rev.
16+
for any VCS rev.
1717

1818
## Requirements
1919

2020
- Git ≥ 2.31.0 (for Git support)
2121
- Mercurial ≥ 5.4.0 (for Mercurial support)
22+
- Jujutsu ≥ 0.38.0 (for `:DiffviewOpen` support)
2223
- Neovim ≥ 0.10.0 (with LuaJIT)
2324
- [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) or [mini.icons](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-icons.md) (optional) for file icons
2425

@@ -82,11 +83,11 @@ For more info, see `:h :DiffviewFileHistory`.
8283

8384
## Usage
8485

85-
### `:DiffviewOpen [git rev] [options] [ -- {paths...}]`
86+
### `:DiffviewOpen [rev] [options] [ -- {paths...}]`
8687

87-
Calling `:DiffviewOpen` with no args opens a new Diffview that compares against
88-
the current index. You can also provide any valid git rev to view only changes
89-
for that rev.
88+
Calling `:DiffviewOpen` with no args opens a new Diffview for the current
89+
working copy (the exact baseline depends on VCS adapter). You can also provide
90+
any valid rev/range accepted by your active adapter.
9091

9192
Examples:
9293

@@ -97,6 +98,9 @@ Examples:
9798
- `:DiffviewOpen d4a7b0d^!`
9899
- `:DiffviewOpen d4a7b0d..519b30e`
99100
- `:DiffviewOpen origin/main...HEAD`
101+
- `:DiffviewOpen @-` (Jujutsu)
102+
- `:DiffviewOpen main..@` (Jujutsu)
103+
- `:DiffviewOpen main...@` (Jujutsu)
100104

101105
You can also provide additional paths to narrow down what files are shown:
102106

@@ -105,6 +109,10 @@ You can also provide additional paths to narrow down what files are shown:
105109
For information about additional `[options]`, visit the
106110
[documentation](https://github.com/sindrets/diffview.nvim/blob/main/doc/diffview.txt).
107111

112+
Jujutsu currently supports only `:DiffviewOpen`. The options `--cached`,
113+
`--staged`, and `--imply-local` are Git-only and are ignored by the Jujutsu
114+
adapter with a warning.
115+
108116
Additional commands for convenience:
109117

110118
- `:DiffviewClose`: Close the current diffview. You can also use `:tabclose`.
@@ -188,6 +196,7 @@ require("diffview").setup({
188196
enhanced_diff_hl = false, -- See |diffview-config-enhanced_diff_hl|
189197
git_cmd = { "git" }, -- The git executable followed by default args.
190198
hg_cmd = { "hg" }, -- The hg executable followed by default args.
199+
jj_cmd = { "jj" }, -- The jj executable followed by default args.
191200
rename_threshold = nil, -- Integer 0-100 for rename detection similarity. Nil uses git default (50%). Invalid values are ignored.
192201
use_icons = true, -- Requires nvim-web-devicons or mini.icons
193202
show_help_hints = true, -- Show hints for how to open the help panel

doc/diffview.txt

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,14 @@ Visit the help page for more info.
5050
COMMANDS *diffview-commands*
5151

5252
*:DiffviewOpen*
53-
:DiffviewOpen [git-rev] [options] [ -- {paths...}]
53+
:DiffviewOpen [rev] [options] [ -- {paths...}]
5454

55-
Opens a new Diffview that compares against [git-rev]. If no [git-rev]
56-
is given, defaults to comparing against the index. Additional
57-
{paths...} may be provided to narrow down what files are shown. If the
58-
`-C` flag is not defined, the git top-level is determined from the
59-
current buffer when possible. Otherwise it's determined from the cwd.
55+
Opens a new Diffview that compares against [rev]. If no [rev] is
56+
given, defaults to comparing against the working copy baseline for the
57+
active adapter. Additional {paths...} may be provided to narrow down
58+
what files are shown. If the `-C` flag is not defined, the adapter
59+
repository top-level is determined from the current buffer when
60+
possible. Otherwise it's determined from the cwd.
6061

6162
Examples: >vim
6263

@@ -78,6 +79,11 @@ COMMANDS *diffview-commands*
7879
" Diff HEAD against it's merge base in origin/main:
7980
:DiffviewOpen origin/main...HEAD
8081

82+
" Jujutsu examples:
83+
:DiffviewOpen @-
84+
:DiffviewOpen main..@
85+
:DiffviewOpen main...@
86+
8187
" Limit the scope to the given paths:
8288
:DiffviewOpen HEAD~2 -- lua/diffview plugin
8389

@@ -143,8 +149,10 @@ COMMANDS *diffview-commands*
143149
`true`, `normal`, `all` Show untracked.
144150
`false`, `no` Don't show untracked.
145151
--cached, --staged
146-
Diff staged changes against [git-rev]. If no [git-rev]
152+
Diff staged changes against [rev]. If no [rev]
147153
is given, defaults to `HEAD`.
154+
NOTE: This is Git-specific. For Jujutsu, this option is
155+
ignored with a warning.
148156

149157
--imply-local If a range rev is provided and either end of the range
150158
points to `HEAD`: point that end to local files
@@ -153,6 +161,8 @@ COMMANDS *diffview-commands*
153161
(triple-dot), but you want to be able to utilize the
154162
LSP features that are not available while you're
155163
viewing files created from git.
164+
NOTE: This is Git-specific. For Jujutsu, this option is
165+
ignored with a warning.
156166

157167
-C{path} Run as if git was started in {path} instead of the
158168
current working directory.
@@ -169,6 +179,8 @@ COMMANDS *diffview-commands*
169179
given paths. This is a porcelain interface for git-log. Both [paths]
170180
and [options] may be specified in any order, even interchangeably.
171181

182+
NOTE: The Jujutsu adapter currently supports only |:DiffviewOpen|.
183+
172184
If no [paths] are given, defaults to the top-level of the working
173185
tree. The top-level will be inferred from the current buffer when
174186
possible, otherwise the cwd is used. Multiple [paths] may be provided
@@ -444,6 +456,13 @@ hg_cmd *diffview-config-hg_cmd*
444456
If your Mercurial install bundles the `chg` binary, this can be
445457
configured here to have a significant performance boost.
446458

459+
jj_cmd *diffview-config-jj_cmd*
460+
Type: `string[]`, Default: `{ "jj" }`
461+
462+
This table forms the first part of all Jujutsu commands used
463+
internally in the plugin. The first element should be the Jujutsu
464+
binary. The subsequent elements are passed as arguments.
465+
447466
rename_threshold *diffview-config-rename_threshold*
448467
Type: `integer?`, Default: `nil`
449468

doc/diffview_defaults.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ DEFAULT CONFIG *diffview.defaults*
77
enhanced_diff_hl = false, -- See |diffview-config-enhanced_diff_hl|
88
git_cmd = { "git" }, -- The git executable followed by default args.
99
hg_cmd = { "hg" }, -- The hg executable followed by default args.
10+
jj_cmd = { "jj" }, -- The jj executable followed by default args.
1011
p4_cmd = { "p4" }, -- The p4 executable followed by default args.
1112
rename_threshold = nil, -- Integer 0-100 for rename detection similarity. Nil uses git default (50%). Invalid values are ignored.
1213
use_icons = true, -- Requires nvim-web-devicons or mini.icons

lua/diffview/config.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ M.defaults = {
4141
enhanced_diff_hl = false,
4242
git_cmd = { "git" },
4343
hg_cmd = { "hg" },
44+
jj_cmd = { "jj" },
4445
rename_threshold = nil, -- Similarity threshold for rename detection (e.g. 40 for 40%). Nil uses git default (50%).
4546
use_icons = true,
4647
show_help_hints = true,
@@ -619,6 +620,14 @@ function M.setup(user_config)
619620
M._config.git_cmd = M.defaults.git_cmd
620621
end
621622

623+
if #M._config.hg_cmd == 0 then
624+
M._config.hg_cmd = M.defaults.hg_cmd
625+
end
626+
627+
if #M._config.jj_cmd == 0 then
628+
M._config.jj_cmd = M.defaults.jj_cmd
629+
end
630+
622631
if #M._config.p4_cmd == 0 then
623632
M._config.p4_cmd = M.defaults.p4_cmd
624633
end

lua/diffview/health.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ function M.check()
6666

6767
local has_valid_adapter = false
6868
local adapter_kinds = {
69+
{ class = require("diffview.vcs.adapters.jj").JjAdapter, name = "Jujutsu" },
6970
{ class = require("diffview.vcs.adapters.git").GitAdapter, name = "Git" },
7071
{ class = require("diffview.vcs.adapters.hg").HgAdapter, name = "Mercurial" },
7172
{ class = require("diffview.vcs.adapters.p4").P4Adapter, name = "Perforce" },

lua/diffview/scene/file_entry.lua

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ function FileEntry:init(opt)
8989
self.opened = false
9090
end
9191

92-
function FileEntry:destroy()
92+
---@param force? boolean
93+
function FileEntry:destroy(force)
9394
for _, f in ipairs(self.layout:files()) do
94-
f:destroy()
95+
f:destroy(force)
9596
end
9697

9798
self.layout:destroy()

lua/diffview/scene/views/diff/diff_view.lua

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ local pl = lazy.access(utils, "path") ---@type PathLib
2727

2828
local M = {}
2929

30+
---@param a Rev?
31+
---@param b Rev?
32+
---@return boolean
33+
local function same_rev(a, b)
34+
if a == nil and b == nil then
35+
return true
36+
end
37+
38+
if a == nil or b == nil then
39+
return false
40+
end
41+
42+
return a.type == b.type
43+
and a.commit == b.commit
44+
and a.stage == b.stage
45+
and a.track_head == b.track_head
46+
end
47+
3048
---@class DiffViewOptions
3149
---@field show_untracked? boolean
3250
---@field selected_file? string Path to the preferred initially selected file.
@@ -368,6 +386,17 @@ DiffView.update_files = debounce.debounce_trailing(
368386
local perf = PerfTimer("[DiffView] Status Update")
369387
self:ensure_layout()
370388

389+
local new_left, new_right = self.adapter:refresh_revs(self.rev_arg, self.left, self.right)
390+
if new_left and new_right then
391+
self.left = new_left
392+
self.right = new_right
393+
394+
if not self.rev_arg then
395+
self.panel.rev_pretty_name = self.adapter:rev_to_pretty_string(self.left, self.right)
396+
end
397+
end
398+
perf:lap("refreshed revs")
399+
371400
-- If left is tracking HEAD and right is LOCAL: Update HEAD rev.
372401
local new_head
373402
if self.left.track_head and self.right.type == RevType.LOCAL then
@@ -428,26 +457,49 @@ DiffView.update_files = debounce.debounce_trailing(
428457

429458
for _, opr in ipairs(script) do
430459
if opr == EditToken.NOOP then
431-
-- Update status and stats
432-
-- Guard against nil entries that can occur during async race conditions (#395).
433-
local cur_file = v.cur_files[ai]
460+
local old_file = v.cur_files[ai]
434461
local new_file = v.new_files[bi]
435462

436-
if cur_file and new_file then
437-
local a_stats = cur_file.stats
438-
local b_stats = new_file.stats
463+
-- Guard against nil entries that can occur during async race conditions (#395).
464+
if old_file and new_file then
465+
local replace_noop = self.adapter:force_entry_refresh_on_noop(self.left, self.right)
466+
467+
-- Even with a stable path, rev endpoints can change on refresh
468+
-- (e.g. symbolic revs like `master...@`). Replace the entry so
469+
-- the displayed content comes from the latest rev pair.
470+
if not replace_noop then
471+
replace_noop = not (
472+
same_rev(utils.tbl_access(old_file, "revs.a"), utils.tbl_access(new_file, "revs.a"))
473+
and same_rev(utils.tbl_access(old_file, "revs.b"), utils.tbl_access(new_file, "revs.b"))
474+
and same_rev(utils.tbl_access(old_file, "revs.c"), utils.tbl_access(new_file, "revs.c"))
475+
and same_rev(utils.tbl_access(old_file, "revs.d"), utils.tbl_access(new_file, "revs.d"))
476+
)
477+
end
478+
479+
if replace_noop then
480+
if self.panel.cur_file == old_file then
481+
self.panel:set_cur_file(new_file)
482+
end
439483

440-
if a_stats then
441-
cur_file.stats = vim.tbl_extend("force", a_stats, b_stats or {})
484+
old_file:destroy(true)
485+
v.cur_files[ai] = new_file
442486
else
443-
cur_file.stats = new_file.stats
444-
end
487+
-- Update status and stats
488+
local a_stats = old_file.stats
489+
local b_stats = new_file.stats
445490

446-
cur_file.status = new_file.status
447-
cur_file:validate_stage_buffers(index_stat)
491+
if a_stats then
492+
old_file.stats = vim.tbl_extend("force", a_stats, b_stats or {})
493+
else
494+
old_file.stats = new_file.stats
495+
end
448496

449-
if new_head then
450-
cur_file:update_heads(new_head)
497+
old_file.status = new_file.status
498+
old_file:validate_stage_buffers(index_stat)
499+
500+
if new_head then
501+
old_file:update_heads(new_head)
502+
end
451503
end
452504
end
453505

lua/diffview/tests/functional/file_entry_spec.lua

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
local helpers = require("diffview.tests.helpers")
12
local FileEntry = require("diffview.scene.file_entry").FileEntry
23
local Diff2Hor = require("diffview.scene.layouts.diff_2_hor").Diff2Hor
34
local RevType = require("diffview.vcs.rev").RevType
45
local GitRev = require("diffview.vcs.adapters.git.rev").GitRev
56

6-
describe("diffview.file_entry", function()
7+
local eq = helpers.eq
8+
9+
describe("diffview.scene.file_entry", function()
710
it("convert_layout skips null entries without error (#612)", function()
811
local adapter = { ctx = { toplevel = vim.uv.cwd() } }
912
local entry = FileEntry.new_null_entry(adapter)
@@ -48,4 +51,45 @@ describe("diffview.file_entry", function()
4851
assert.is_not_nil(captured)
4952
assert.False(captured.b.nulled)
5053
end)
54+
55+
it("forwards force flag to contained files when destroyed", function()
56+
local seen = {}
57+
local layout_destroyed = false
58+
59+
local layout = {
60+
files = function()
61+
return {
62+
{
63+
destroy = function(_, force)
64+
seen[#seen + 1] = force
65+
end,
66+
},
67+
{
68+
destroy = function(_, force)
69+
seen[#seen + 1] = force
70+
end,
71+
},
72+
}
73+
end,
74+
destroy = function()
75+
layout_destroyed = true
76+
end,
77+
}
78+
79+
local entry = FileEntry({
80+
adapter = { ctx = { toplevel = "/tmp" } },
81+
path = "a.txt",
82+
oldpath = nil,
83+
revs = {},
84+
layout = layout,
85+
status = "M",
86+
stats = {},
87+
kind = "working",
88+
})
89+
90+
entry:destroy(true)
91+
92+
eq({ true, true }, seen)
93+
eq(true, layout_destroyed)
94+
end)
5195
end)

0 commit comments

Comments
 (0)