feat(diff): Add the diff_history command (#97)

This new command is natively implemented by both the `codediff` and `diffview` backends allowing to easily navigate the changes between two revsets while diffing the. 

It also gives a new `:J diff_hisotyr` command that optionally takes a range of revsets formatted `<rev1>..<rev2>` otherwise it prompts the user for the range.
This commit is contained in:
Nicolas GB
2026-03-12 18:03:23 +01:00
committed by GitHub
parent ec44937e1b
commit 1c3d38a626
8 changed files with 184 additions and 3 deletions
+31 -2
View File
@@ -14,6 +14,7 @@
- [Enhanced Integrations](#enhanced-integrations)
- [View change summary from the log buffer](#view-change-summary-from-the-log-buffer)
- [Diff any change](#diff-any-change)
- [Diff revision history](#diff-revision-history)
- [Describe a change](#describe-a-change)
- [Edit changes](#edit-changes)
- [Create new changes from the log buffer](#create-new-changes-from-the-log-buffer)
@@ -73,6 +74,7 @@
- `browse` - Open the current file on your remote at the current line (or selected range)
- `annotate` / `annotate_line` - View file blame and line history with change ID, author, and timestamp
- `commit` - Describe the current change and create a new one after
- `diff_history` - Open a history-aware diff between two revisions when supported by your diff backend
- Diff commands
- `:Jdiff [revision]` - Vertical split diff against a jj revision
- `:Jhdiff [revision]` - Horizontal split diff
@@ -101,13 +103,25 @@ From the summary view, you can:
### Diff any change
You can diff any change in your log history by pressing `<S-d>` on its line or on a summary file change. You can also visually select multiple changes to diff between the first and last selected.
You can diff any change in your log history by pressing `<S-d>` on its line or on a summary file change. This opens the configured diff backend for the revision under the cursor.
> [!NOTE]
> Integrates with your preferred diff plugin or uses your native jj diff config. See [Diff Module](#diff-module).
![Diff-from-log](https://github.com/NicolasGB/jj.nvim/raw/main/assets/diff-log.gif)
### Diff revision history
You can now open a history-aware diff between two revisions:
- In the log buffer, visually select multiple revisions and press `<S-h>`
- `jj.nvim` uses the first and last selected revisions as the range boundaries
- `diffview` opens `DiffviewFileHistory` for that range
- `codediff` opens `CodeDiff history` for that range
- The `native` backend currently shows a warning because history mode is not supported there yet
This is useful when you want to inspect the evolution of a stack or compare a revision range with commit history preserved instead of showing a single plain diff.
### Describe a change
You can describe any change directly from the log buffer:
@@ -339,6 +353,8 @@ The plugin provides a `:J` command that accepts jj subcommands:
:Jbrowse " Open current file on remote at cursor line
:Jbrowse main " Open current file on remote at the given revset
:J split " Split a change interactively
:J diff_history " Prompt for a `left..right` range and open a history-aware diff
:J diff_history main..@ " Open a history-aware diff between main and the working copy
:J bookmark create/move/delete
:J tag set " Set a tag (prompts for revision and tag name)
:J tag set abc123 " Set a tag on a specific revision
@@ -475,6 +491,7 @@ The plugin also provides `:Jdiff`, `:Jvdiff`, and `:Jhdiff` commands for diffing
},
quick_squash = "<S-s>", -- Quick squash revision under cursor into its parent (ignore immutability)
split = "<C-s>", -- Split the revision under cursor
history = "<S-h>", -- Show a history-aware diff for the selected revision range
tag_create = "<S-t>", -- Create a tag on the revision under cursor
summary = "<S-k>", -- Show summary tooltip for revision under cursor
summary_tooltip = {
@@ -718,6 +735,11 @@ diff.show_revision({ rev = "abc123" })
-- Diff between two revisions
diff.diff_revisions({ left = "main", right = "@" })
-- Open a history-aware diff between two revisions
-- Supported by the `diffview` and `codediff` backends
-- The `native` backend currently warns instead
diff.diff_history_revisions({ left = "main", right = "@" })
-- Convenience functions (LEGACY FUNCTIONS)
diff.open_vdiff() -- Vertical split diff against parent
diff.open_vdiff({ rev = "main" }) -- Vertical split against specific revision
@@ -730,6 +752,7 @@ diff.open_hdiff({ rev = "@-2" }) -- Horizontal split against @-2
The diff module integrates seamlessly with the log buffer:
- `<S-d>` - Show diff for the revision under cursor in a floating window
- `<S-h>` - In visual mode, open a history-aware diff for the first and last selected revisions
These actions use the configured diff backend, allowing you to leverage your preferred diff viewer directly from the log.
@@ -762,6 +785,12 @@ diff.register_backend("my-backend", {
-- opts.path: optional file filter
-- opts.display: "floating", "tab", or "split"
end,
-- Open a history-aware diff between two revisions
diff_history_revisions = function(opts)
-- opts.left: left/base revision
-- opts.right: right/target revision
end,
})
```
@@ -781,7 +810,7 @@ Or use it per-call:
diff.diff_current({ backend = "my-backend", rev = "main" })
```
All three functions are optional—missing ones fall back to the `native` implementation.
All four backend functions are optional—missing ones fall back to the `native` implementation.
### Annotations
+49
View File
@@ -55,6 +55,7 @@ local split_module = require("jj.cmd.split")
--- @field summary? string|string[]
--- @field summary_tooltip? jj.cmd.summary_tooltip.keymaps
--- @field tag_set? string|string[]
--- @field history? string|string[]
--- @class jj.cmd.rebase.keymaps
--- @field onto? string|string[]
@@ -130,6 +131,10 @@ local split_module = require("jj.cmd.split")
--- @class jj.cmd.fetch_pr_opts
--- @field limit? number Limit the number of PRs to select from
--- @class jj.cmd.diff_history_opts
--- @field left? string The left revision for the range
--- @field right? string The right revision for the range
--- @type jj.cmd.opts
M.config = {
describe = {
@@ -193,6 +198,7 @@ M.config = {
},
split = "<C-s>",
tag_set = "<S-t>",
history = "<S-h>",
},
status = {
open_file = "<CR>",
@@ -421,6 +427,34 @@ function M.diff(opts)
end
end
-- Diff history between two revisions
--- @param opts? jj.cmd.diff_history_opts
function M.diff_history(opts)
if not utils.ensure_jj() then
return
end
local diff_module = require("jj.diff")
if opts then
return diff_module.diff_history_revisions({ left = opts.left, right = opts.right })
end
-- Otherwise prompt the user for input
vim.ui.input({ prompt = "Range to diff with history" }, function(choice)
if choice then
local range = parser.parse_diff_range(choice)
if range then
diff_module.diff_history_revisions({ left = range.left, right = range.right })
else
utils.notify("Invalid range format. Use `left..right` or `left...right`.", vim.log.levels.ERROR)
end
else
terminal.close_terminal_buffer()
end
end)
end
-- Jujutsu rebase
function M.rebase()
if not utils.ensure_jj() then
@@ -1188,6 +1222,20 @@ function M.j(args)
diff = function()
M.diff({ current = false })
end,
diff_history = function()
if remaining_args_str == "" then
return M.diff_history()
end
-- Otherwise it means a range has been provided
local parts = parser.parse_diff_range(remaining_args_str)
if not parts then
utils.notify("Invalid history range. Formant must be `<left>..<right>`", vim.log.levels.ERROR)
return
end
M.diff_history({ left = parts.left, right = parts.right })
end,
status = function()
M.status()
end,
@@ -1286,6 +1334,7 @@ function M.register_command()
"bookmark",
"describe",
"diff",
"diff_history",
"edit",
"fetch",
"git",
+25
View File
@@ -428,6 +428,26 @@ function M.handle_log_diff()
end
end
--- Handle log history
function M.handle_log_history()
local revsets = extract_revsets_from_terminal_buffer()
if not revsets then
utils.notify("No valid revision found in the log line", vim.log.levels.ERROR)
return
end
-- Delete the pipes from the revsets string since jj new expects space separated revsets
revsets = revsets:gsub("| ", "")
local is_multiple = revsets:find(" ") ~= nil
if is_multiple then
-- Get the first and the last revset to diff
local list = vim.split(revsets, "%s+", { trimempty = true })
diff.diff_history_revisions({ left = list[1], right = list[#list] })
end
end
--- Handle describing a log line
function M.handle_log_describe()
-- Store the cursor pos for when we exit the describe
@@ -1128,6 +1148,11 @@ function M.log_keymaps()
handler = M.handle_log_tag_set,
modes = { "n" },
},
history = {
desc = "Show history diff between revisions (only works with multiple revsets selected)",
handler = M.handle_log_history,
modes = { "v" },
},
}
local merged_keymaps = cmd.merge_keymaps(cmd.resolve_keymaps_from_specs(keymaps, specs), cmd.terminal_keymaps())
+16
View File
@@ -229,4 +229,20 @@ function M.parse_annotation_line(line)
return result
end
-- Given a range with the format "left..right", parse and return the two revisions
--- @param range_str string The range string to parse (e.g., "left..right")
--- @return {left: string, right: string}|nil A table
function M.parse_diff_range(range_str)
if not range_str then
return nil
end
local left, right = range_str:match("^(%S+)%.%.(%S+)$")
if left and right then
return { left = left, right = right }
end
return nil
end
return M
+23
View File
@@ -28,6 +28,21 @@ local function diff_two_changes(left, right)
vim.cmd(string.format("CodeDiff %s %s", commit_id_right, commit_id_left))
end
local function diff_two_changes_with_history(left, right)
local commit_id_left = utils.get_commit_id(left)
if commit_id_left == nil then
return
end
local commit_id_right = utils.get_commit_id(right)
if commit_id_right == nil then
return
end
-- test
vim.cmd(string.format("CodeDiff history %s..%s", commit_id_right, commit_id_left))
end
-----------------------------------------------------------------------
-- Codediff Backend
-----------------------------------------------------------------------
@@ -67,4 +82,12 @@ diff.register_backend("codediff", {
diff_two_changes(opts.left, opts.right)
end,
diff_history_revisions = function(opts)
if not utils.has_dependency("codediff") then
return
end
diff_two_changes_with_history(opts.left, opts.right)
end,
})
+22
View File
@@ -24,6 +24,21 @@ local function diff_two_changes(left, right)
vim.cmd(string.format("DiffviewOpen %s..%s", commit_id_right, commit_id_left))
end
local function diff_two_changes_with_history(left, right)
local commit_id_left = utils.get_commit_id(left)
if commit_id_left == nil then
return
end
local commit_id_right = utils.get_commit_id(right)
if commit_id_right == nil then
return
end
-- test
vim.cmd(string.format("DiffviewFileHistory --range=%s..%s", commit_id_right, commit_id_left))
end
-- Register the diffview backend
diff.register_backend("diffview", {
diff_current = function(opts)
@@ -59,4 +74,11 @@ diff.register_backend("diffview", {
diff_two_changes(opts.left, opts.right)
end,
diff_history_revisions = function(opts)
if not utils.has_dependency("diffview") then
return
end
diff_two_changes_with_history(opts.left, opts.right)
end,
})
+12 -1
View File
@@ -28,6 +28,7 @@ local utils = require("jj.utils")
---@field diff_current? fun(opts: jj.diff.current_opts)
---@field show_revision? fun(opts: jj.diff.revision_opts)
---@field diff_revisions? fun(opts: jj.diff.revisions_opts)
---@field diff_history_revisions? fun(opts: jj.diff.revisions_opts)
---@class jj.diff.config
---@field backend? jj.diff.backend
@@ -99,7 +100,7 @@ end
-----------------------------------------------------------------------
--- Single dispatcher (canonical entry point)
---@param kind "current"|"revision"|"revisions"
---@param kind "current"|"revision"|"revisions"|"history"
---@param opts table
function M.open(kind, opts)
opts = opts or {}
@@ -120,6 +121,10 @@ function M.open(kind, opts)
return backend.diff_revisions(opts)
end
return backends.native.diff_revisions(opts)
elseif kind == "history" then
if backend.diff_history_revisions then
return backend.diff_history_revisions(opts)
end
else
utils.notify("[Diff] unknown diff kind: " .. tostring(kind), vim.log.levels.ERROR)
end
@@ -143,6 +148,12 @@ function M.diff_revisions(opts)
return M.open("revisions", opts)
end
-- Diff between two revisions with history mode active
--- @param opts jj.diff.revisions_opts
function M.diff_history_revisions(opts)
return M.open("history", opts)
end
---
-----------------------------------------------------------------------
-- BACKWARDS COMPATIBLE API
+6
View File
@@ -124,4 +124,10 @@ diff.register_backend("native", {
local cmd = string.format("jj diff -f %s -t %s --quiet --no-pager", opts.left, opts.right)
terminal.run_floating(cmd, require("jj.cmd").floating_keymaps())
end,
diff_history_revisions = function(_)
utils.notify(
"Diffing revisions with history mode is not supported on the `native` backend.",
vim.log.levels.WARN
)
end,
})