mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-15 23:53:18 +00:00
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:
@@ -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 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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user