mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-15 23:53:18 +00:00
feat(resolve): add resolve command and log integration (#125)
Add first-class conflict resolution support to jj.nvim.
- add `jj.cmd.resolve` to run `jj resolve` in a floating terminal or via an
external tool
- add `:J resolve` with support for `-r/--revision`, `--tool`,
`--external/--ext`, and positional filesets
- integrate resolve into the log buffer with the `gr` mapping and optional
`resolve_strategies` picker
- add `utils.is_change_conflicted()` and avoid opening resolve when the
selected revision has no conflicts
- document resolve usage, config, and keymaps in the README
- add tests for resolve arg parsing and conflict detection helpers
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
- [Manage tags from the log buffer](#manage-tags-from-the-log-buffer)
|
||||
- [Squash changes from the log buffer](#squash-changes-from-the-log-buffer)
|
||||
- [Split changes from the log buffer](#split-changes-from-the-log-buffer)
|
||||
- [Resolve conflicts from the log buffer](#resolve-conflicts-from-the-log-buffer)
|
||||
- [Rebase changes from the log buffer](#rebase-changes-from-the-log-buffer)
|
||||
- [Duplicate changes from the log buffer](#duplicate-changes-from-the-log-buffer)
|
||||
- [Open a PR/MR from the log buffer](#open-a-prmr-from-the-log-buffer)
|
||||
@@ -39,6 +40,7 @@
|
||||
- [Configuration Examples](#configuration-examples)
|
||||
- [Editor Options](#editor-options)
|
||||
- [New Command Options](#new-command-options)
|
||||
- [Resolve Command Options](#resolve-command-options)
|
||||
- [Push Command Options](#push-command-options)
|
||||
- [Bookmark Management Command Options](#bookmark-management-command-options)
|
||||
- [Open PR/MR Command Options](#open-prmr-command-options)
|
||||
@@ -68,6 +70,7 @@
|
||||
- `edit` - Edit a change
|
||||
- `squash` - Squash the current diff to its parent or interactive squash mode from the log buffer
|
||||
- `split` - Split a change interactively in a floating terminal
|
||||
- `resolve` - Resolve conflicts interactively in a floating terminal or via external strategy
|
||||
- `rebase` - Rebase changes to a destination
|
||||
- `duplicate` - Duplicate one or more changes to a destination revision
|
||||
- `bookmark create/delete/track/forget` - Create, delete, track, and forget (untrack) bookmarks
|
||||
@@ -250,6 +253,23 @@ cmd.split({ filesets = { "src/" } }) -- Only include specific filese
|
||||
cmd.split({ ignore_immutable = true }) -- Split an immutable revision
|
||||
```
|
||||
|
||||
### Resolve conflicts from the log buffer
|
||||
|
||||
Resolve conflicts for the revision under the cursor directly from the log buffer:
|
||||
|
||||
- `gr` - Resolve conflicts for the selected revision
|
||||
|
||||
How it integrates with the log buffer:
|
||||
|
||||
- Uses `jj resolve --revision <revset>` for the revision under cursor
|
||||
- If `cmd.resolve_strategies` has more than one strategy, `vim.ui.select` prompts you to choose one
|
||||
- If a strategy is selected, its `args` and `external` options are passed to `cmd.resolve(...)`
|
||||
- On success, the log buffer is automatically reloaded so conflict state is refreshed
|
||||
- When using floating terminals, cancelled/failed resolve attempts restore the log view
|
||||
|
||||
> [!NOTE]
|
||||
> See the [Example config](#example-config) below for a complete `resolve_strategies` setup.
|
||||
|
||||
### Rebase changes from the log buffer
|
||||
|
||||
Enter an interactive rebase mode directly from the log buffer to rebase one or more changes:
|
||||
@@ -401,17 +421,25 @@ 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 resolve " Resolve conflicts for @
|
||||
:J resolve -r abc123 --tool mergiraf --external src/ " Resolve rev/filesets with an external tool
|
||||
:J resolve " Resolve conflicts for @
|
||||
:J resolve -r abc123 " Resolve a specific revision
|
||||
:J resolve --revision abc123 src/ " Resolve only matching filesets
|
||||
:J resolve --tool mergiraf --external " Resolve using an external tool
|
||||
:J resolve --tool meld --ext src/ " --ext alias + fileset filtering
|
||||
: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/track/forget
|
||||
:J tag set " Set a tag (prompts for revision and tag name)
|
||||
:J tag set abc123 " Set a tag on a specific revision
|
||||
:J tag delete " Delete a tag via picker
|
||||
:J tag delete v1.0 " Delete a specific tag
|
||||
:J tag set " Set a tag (prompts for revision and tag name)
|
||||
:J tag set abc123 " Set a tag on a specific revision
|
||||
:J tag delete " Delete a tag via picker
|
||||
:J tag delete v1.0 " Delete a specific tag
|
||||
:J # This will use your defined default command
|
||||
:J <your-alias>
|
||||
:J commit " Opens your configured editor describes @ and then creates a new change -A immediately
|
||||
:J commit " Opens your configured editor describes @ and then creates a new change -A immediately
|
||||
:J commit <any text here> " Automatically describes @ and creates a new change -A immediately
|
||||
|
||||
```
|
||||
|
||||
### Diff Commands
|
||||
@@ -539,6 +567,20 @@ revision via `jj diffedit`. Immutable revisions show an error on write.
|
||||
close_on_edit = false, -- Close log buffer after editing a change
|
||||
},
|
||||
|
||||
-- Optional resolve strategy picker shared across cmd integrations
|
||||
resolve_strategies = {
|
||||
{
|
||||
name = "Meld",
|
||||
args = { "--tool", "meld" },
|
||||
external = true,
|
||||
},
|
||||
{
|
||||
name = "Mergiraf",
|
||||
args = { "--tool", "mergiraf" },
|
||||
external = true,
|
||||
},
|
||||
},
|
||||
|
||||
-- Configure bookmark command
|
||||
bookmark = {
|
||||
prefix = ""
|
||||
@@ -594,6 +636,7 @@ revision via `jj diffedit`. Immutable revisions show an error on write.
|
||||
},
|
||||
quick_squash = "<S-s>", -- Quick squash revision under cursor into its parent (ignore immutability)
|
||||
split = "<C-s>", -- Split the revision under cursor
|
||||
resolve = "gr", -- Resolve conflicts for revision under cursor
|
||||
history = "<S-h>", -- Show a history-aware diff for the selected revision range
|
||||
change_revset = "<C-r>", -- Change the revset(s) being viewed in the log buffer
|
||||
tag_set = "<S-t>", -- Create a tag on the revision under cursor
|
||||
@@ -784,6 +827,39 @@ cmd.new({ show_log = true, with_input = true }) -- Prompt for parent
|
||||
cmd.new({ args = "--before @" }) -- Pass custom args
|
||||
```
|
||||
|
||||
### Resolve Command Options
|
||||
|
||||
The `resolve` function accepts an options table:
|
||||
|
||||
```lua
|
||||
local cmd = require("jj.cmd")
|
||||
cmd.resolve({
|
||||
rev = "@", -- Revision to resolve (default: "@")
|
||||
filesets = { "src/" }, -- Optional filesets to limit what gets resolved
|
||||
args = { "--tool", "meld" }, -- Extra args passed to `jj resolve`
|
||||
external = true, -- Run as an external command instead of in an nvim floating terminal
|
||||
})
|
||||
|
||||
-- Examples:
|
||||
cmd.resolve() -- Resolve @ in floating terminal
|
||||
cmd.resolve({ rev = "abc123" }) -- Resolve a specific revision
|
||||
cmd.resolve({ rev = "abc123", filesets = { "lua/" } }) -- Resolve only selected filesets
|
||||
cmd.resolve({ external = true, args = { "--tool", "kdiff3" } }) -- Use an external merge tool
|
||||
```
|
||||
|
||||
When called from the log buffer via `gr`, `jj.nvim` can optionally prompt for a strategy using `cmd.resolve_strategies`.
|
||||
|
||||
> [!NOTE]
|
||||
> See [Example config](#example-config) for a full `cmd.resolve_strategies` example.
|
||||
|
||||
CLI flags for `:J resolve`:
|
||||
|
||||
- `-r <rev>` or `--revision <rev>`: target revision (default: `@`)
|
||||
- `--tool <name>`: pass merge tool to `jj resolve`
|
||||
- `--external` or `--ext`: run outside the floating terminal
|
||||
- trailing positional args are treated as filesets
|
||||
- unknown long options are rejected to avoid ambiguity with filesets
|
||||
|
||||
### Push Command Options
|
||||
|
||||
The `push` function accepts an options table:
|
||||
@@ -1098,6 +1174,18 @@ vim.keymap.set("n", "<leader>jA", annotate.line, { desc = "JJ annotate line" })
|
||||
bookmark = {
|
||||
prefix = "feat/"
|
||||
},
|
||||
resolve_strategies = {
|
||||
{
|
||||
name = "Meld",
|
||||
args = { "--tool", "meld" },
|
||||
external = true,
|
||||
},
|
||||
{
|
||||
name = "Mergiraf",
|
||||
args = { "--tool", "mergiraf" },
|
||||
external = true,
|
||||
},
|
||||
},
|
||||
keymaps = {
|
||||
log = {
|
||||
edit = "<CR>",
|
||||
@@ -1105,6 +1193,7 @@ vim.keymap.set("n", "<leader>jA", annotate.line, { desc = "JJ annotate line" })
|
||||
diff = "<S-d>",
|
||||
abandon = "<S-a>",
|
||||
fetch = "<S-f>",
|
||||
resolve = "gr", -- Resolve conflicts for revision under cursor
|
||||
},
|
||||
status = {
|
||||
open_file = "<CR>",
|
||||
|
||||
+103
-7
@@ -11,6 +11,7 @@ local log_module = require("jj.cmd.log")
|
||||
local describe_module = require("jj.cmd.describe")
|
||||
local status_module = require("jj.cmd.status")
|
||||
local split_module = require("jj.cmd.split")
|
||||
local resolve_module = require("jj.cmd.resolve")
|
||||
|
||||
-- Config for cmd module
|
||||
--- @class jj.cmd.describe.editor.keymaps
|
||||
@@ -115,9 +116,22 @@ local split_module = require("jj.cmd.split")
|
||||
---@field parallel? boolean Run operations in parallel
|
||||
---@field on_exit? fun(exit_code: number) Callback invoked when command exits
|
||||
|
||||
--- @class jj.cmd.resolve.strategy
|
||||
--- @field name string Label shown in vim.ui.select
|
||||
--- @field args? string[] Extra args passed to jj resolve
|
||||
--- @field external? boolean Run externally instead of floating terminal
|
||||
|
||||
---@class jj.cmd.resolve.opts
|
||||
---@field rev string|nil The revision to resolve, defaults to "@"
|
||||
---@field filesets string[]|nil The filesets to resolve, defaults to all filesets
|
||||
---@field external boolean|nil Whether to use external merge tool, if this boolean is set the command will not be ran in a floating terminal inside neovim (defaults: false)
|
||||
---@field args string[]|nil Additional arguments to pass to the resolve command
|
||||
---@field on_exit? fun(exit_code: number) Callback invoked when command exits
|
||||
|
||||
--- @class jj.cmd.opts
|
||||
--- @field describe? jj.cmd.describe
|
||||
--- @field log? jj.cmd.log
|
||||
--- @field resolve_strategies? jj.cmd.resolve.strategy[] List of conflict resolve strategies shared across cmd integrations
|
||||
--- @field bookmark? jj.cmd.bookmark
|
||||
--- @field keymaps? jj.cmd.keymaps Keymaps for the buffers containing the of the commands
|
||||
---
|
||||
@@ -157,6 +171,7 @@ M.config = {
|
||||
log = {
|
||||
close_on_edit = false,
|
||||
},
|
||||
resolve_strategies = {},
|
||||
bookmark = {
|
||||
prefix = "",
|
||||
},
|
||||
@@ -214,6 +229,7 @@ M.config = {
|
||||
edit_file = "o",
|
||||
},
|
||||
split = "<C-s>",
|
||||
resolve = "gr",
|
||||
tag_set = "<S-t>",
|
||||
history = "<S-h>",
|
||||
change_revset = "<C-r>",
|
||||
@@ -248,6 +264,8 @@ M.describe = describe_module.describe
|
||||
M.status = status_module.status
|
||||
-- Rexport split function
|
||||
M.split = split_module.split
|
||||
-- Reexport resolve function
|
||||
M.resolve = resolve_module.resolve
|
||||
|
||||
--- Merge multiple keymap arrays into one
|
||||
--- @param ... jj.core.buffer.keymap[][] Keymap arrays to merge
|
||||
@@ -1240,6 +1258,63 @@ function M.fetch_pr(opts)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Parse arguments passed to `:J resolve`
|
||||
--- @param args string[]
|
||||
--- @return jj.cmd.resolve.opts|nil opts
|
||||
--- @return string|nil err
|
||||
function M.parse_resolve_args(args)
|
||||
local opts = { rev = "@" } --[[@as jj.cmd.resolve.opts]]
|
||||
local already_set = {
|
||||
rev = false,
|
||||
tool = false,
|
||||
}
|
||||
|
||||
local i = 1
|
||||
while i <= #args do
|
||||
local arg = args[i]
|
||||
|
||||
if arg == "--external" or arg == "--ext" then
|
||||
opts.external = true
|
||||
elseif arg == "--tool" then
|
||||
local tool = args[i + 1]
|
||||
if not tool or tool:sub(1, 1) == "-" then
|
||||
return nil, "Missing value for --tool"
|
||||
end
|
||||
if already_set.tool then
|
||||
return nil, "Tool already set. Cannot specify multiple tools."
|
||||
end
|
||||
|
||||
opts.args = opts.args or {}
|
||||
table.insert(opts.args, "--tool")
|
||||
table.insert(opts.args, tool)
|
||||
already_set.tool = true
|
||||
i = i + 1
|
||||
elseif arg == "--revision" or arg == "-r" then
|
||||
local rev = args[i + 1]
|
||||
if not rev or rev:sub(1, 1) == "-" then
|
||||
return nil, "Missing value for --revision/-r"
|
||||
end
|
||||
if already_set.rev then
|
||||
return nil, "Revision already set. Cannot specify multiple revisions."
|
||||
end
|
||||
|
||||
opts.rev = rev
|
||||
already_set.rev = true
|
||||
i = i + 1
|
||||
elseif arg:sub(1, 2) == "--" then
|
||||
return nil, string.format("Unknown option: %s", arg)
|
||||
else
|
||||
-- Positional args are treated as filesets.
|
||||
opts.filesets = opts.filesets or {}
|
||||
table.insert(opts.filesets, arg)
|
||||
end
|
||||
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
return opts, nil
|
||||
end
|
||||
|
||||
--- @param args string|string[] jj command arguments
|
||||
function M.j(args)
|
||||
if not utils.ensure_jj() then
|
||||
@@ -1304,27 +1379,39 @@ function M.j(args)
|
||||
M.log({ raw_flags = remaining_args_str ~= "" and remaining_args_str or nil })
|
||||
end,
|
||||
split = function()
|
||||
local rev = remaining_args and remaining_args[1] or "@"
|
||||
|
||||
local opts = {
|
||||
rev = rev,
|
||||
rev = "@",
|
||||
}
|
||||
|
||||
local index = 2
|
||||
for i = index, #remaining_args do
|
||||
local i = 1
|
||||
while i <= #remaining_args do
|
||||
local arg = remaining_args[i]
|
||||
|
||||
if arg == "--parallel" then
|
||||
opts.parallel = true
|
||||
elseif arg == "--ignore-immutable" then
|
||||
opts.ignore_immutable = true
|
||||
elseif arg == "--message" and remaining_args[i + 1] then
|
||||
opts.message = remaining_args[i + 1]
|
||||
index = i + 1
|
||||
i = i + 1
|
||||
elseif arg == "--fileset" and remaining_args[i + 1] then
|
||||
opts.filesets = opts.filesets or {}
|
||||
table.insert(opts.filesets, remaining_args[i + 1])
|
||||
index = i + 1
|
||||
i = i + 1
|
||||
elseif arg:sub(1, 2) == "--" then
|
||||
-- Unknown option: ignore it here.
|
||||
utils.notify(string.format("Unknown option: %s", arg), vim.log.levels.WARN)
|
||||
return
|
||||
else
|
||||
if opts.rev == "@" then
|
||||
opts.rev = arg
|
||||
else
|
||||
opts.filesets = opts.filesets or {}
|
||||
table.insert(opts.filesets, arg)
|
||||
end
|
||||
end
|
||||
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
require("jj.cmd.split").split(opts)
|
||||
@@ -1430,6 +1517,14 @@ function M.j(args)
|
||||
fetch_pr = function()
|
||||
M.fetch_pr()
|
||||
end,
|
||||
resolve = function()
|
||||
local opts, err = M.parse_resolve_args(remaining_args)
|
||||
if err then
|
||||
utils.notify(err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
require("jj.cmd.resolve").resolve(opts)
|
||||
end,
|
||||
}
|
||||
|
||||
if handlers[subcommand] then
|
||||
@@ -1481,6 +1576,7 @@ function M.register_command()
|
||||
"commit",
|
||||
"tag",
|
||||
"fetch_pr",
|
||||
"resolve",
|
||||
}
|
||||
local matches = {}
|
||||
for _, cmd in ipairs(subcommands) do
|
||||
|
||||
@@ -943,6 +943,60 @@ function M.handle_log_split()
|
||||
})
|
||||
end
|
||||
|
||||
--- Handle log resolve conflict
|
||||
function M.handle_log_resolve()
|
||||
local revset = get_revset()
|
||||
if not revset or revset == "" then
|
||||
return
|
||||
end
|
||||
|
||||
if not utils.is_change_conflicted(revset) then
|
||||
utils.notify(string.format("Revision `%s` has no conflicts to resolve", revset), vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
local exit_func = function(exit_code)
|
||||
if exit_code == 0 then
|
||||
utils.notify(string.format("Successfully resolved `%s`", revset), vim.log.levels.INFO)
|
||||
vim.schedule(function()
|
||||
M.log({})
|
||||
end)
|
||||
else
|
||||
-- Since we previously replaced the floating with the resolve we actually want to re run the log cmd
|
||||
if require("jj").config.terminal.window.type == "floating" then
|
||||
vim.schedule(function()
|
||||
M.log({})
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local strategies = require("jj.cmd").config.resolve_strategies
|
||||
if strategies and #strategies > 1 then
|
||||
vim.ui.select(strategies, {
|
||||
prompt = "Select a resolve strategy: ",
|
||||
format_item = function(item)
|
||||
return item.name
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice then
|
||||
require("jj.cmd").resolve({
|
||||
rev = revset,
|
||||
args = choice.args,
|
||||
external = choice.external,
|
||||
on_exit = exit_func,
|
||||
})
|
||||
end
|
||||
end)
|
||||
elseif strategies and #strategies == 1 then
|
||||
local choice = strategies[1]
|
||||
require("jj.cmd").resolve({ rev = revset, args = choice.args, external = choice.external, on_exit = exit_func })
|
||||
else
|
||||
-- Simply resolve with the default
|
||||
require("jj.cmd").resolve({ rev = revset, on_exit = exit_func })
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle diff action in summary tooltip
|
||||
--- Diffs the file at revset against its parent (revset-)
|
||||
--- Opens a floating diff, and returns focus to tooltip when closed
|
||||
@@ -1322,6 +1376,11 @@ function M.log_keymaps()
|
||||
handler = M.handle_log_select_prev_revision,
|
||||
modes = { "n", "v" },
|
||||
},
|
||||
resolve = {
|
||||
desc = "Resolve conflicts for revision under cursor",
|
||||
handler = M.handle_log_resolve,
|
||||
modes = { "n" },
|
||||
},
|
||||
}
|
||||
|
||||
local merged_keymaps = cmd.merge_keymaps(cmd.resolve_keymaps_from_specs(keymaps, specs), cmd.terminal_keymaps())
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
local M = {}
|
||||
|
||||
local utils = require("jj.utils")
|
||||
local terminal = require("jj.ui.terminal")
|
||||
local runner = require("jj.core.runner")
|
||||
|
||||
--- Resolve conflicts in the current change.
|
||||
--- @param opts? jj.cmd.resolve.opts
|
||||
function M.resolve(opts)
|
||||
opts = vim.tbl_deep_extend("force", {}, opts or {}) --[[@as jj.cmd.resolve.opts]]
|
||||
local rev = opts.rev or "@"
|
||||
local filesets = opts.filesets or {}
|
||||
local args = opts.args or {}
|
||||
|
||||
if not utils.ensure_jj() then
|
||||
return
|
||||
end
|
||||
|
||||
local cmd_args = { "jj", "resolve", "--revision", rev }
|
||||
-- Extra arguments
|
||||
vim.list_extend(cmd_args, args)
|
||||
-- Append the filestes
|
||||
vim.list_extend(cmd_args, filesets)
|
||||
|
||||
local escaped_cmd_args = {}
|
||||
for _, arg in ipairs(cmd_args) do
|
||||
table.insert(escaped_cmd_args, vim.fn.shellescape(arg))
|
||||
end
|
||||
local cmd = table.concat(escaped_cmd_args, " ")
|
||||
|
||||
utils.notify(string.format("Resolving conflicts in change `%s`...", rev), vim.log.levels.INFO)
|
||||
|
||||
-- If external is set, run the command asynchronously and invoke the on_exit callback if provided
|
||||
if opts.external then
|
||||
-- Run the command asynchronously and notify the user of the result
|
||||
runner.execute_command_async(
|
||||
cmd,
|
||||
function(output)
|
||||
if output and output ~= "" then
|
||||
utils.notify(output, vim.log.levels.INFO)
|
||||
end
|
||||
if opts.on_exit then
|
||||
opts.on_exit(0)
|
||||
end
|
||||
end,
|
||||
string.format("Could not resolve conflicts in `%s`", rev),
|
||||
nil,
|
||||
nil,
|
||||
function()
|
||||
if opts.on_exit then
|
||||
opts.on_exit(1)
|
||||
end
|
||||
end
|
||||
)
|
||||
else
|
||||
-- Otherwise, run in a floating terminal
|
||||
terminal.run_floating(cmd, nil, {
|
||||
title = " JJ Resolve ",
|
||||
modifiable = true,
|
||||
keep_modifiable = true,
|
||||
interactive = true,
|
||||
on_exit = opts.on_exit or nil,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -17,7 +17,8 @@ end
|
||||
--- @return boolean success Whether the command succeeded
|
||||
function M.execute_command(cmd, error_prefix, input, silent)
|
||||
local stderr_file = vim.fn.tempname()
|
||||
local output = vim.fn.system({ "sh", "-c", string.format("(%s) 2>%s", cmd, vim.fn.shellescape(stderr_file)) }, input)
|
||||
local output =
|
||||
vim.fn.system({ "sh", "-c", string.format("(%s) 2>%s", cmd, vim.fn.shellescape(stderr_file)) }, input)
|
||||
local success = vim.v.shell_error == 0
|
||||
|
||||
if not success then
|
||||
|
||||
@@ -644,6 +644,24 @@ function M.is_change_empty(revset)
|
||||
return vim.trim(output) == "true"
|
||||
end
|
||||
|
||||
--- Check if a given revset has conflicts
|
||||
--- @param revset string The revset to check
|
||||
--- @return boolean True if the revset has conflicts, false otherwise
|
||||
function M.is_change_conflicted(revset)
|
||||
local output, success = runner.execute_command(
|
||||
string.format("jj log --no-graph -r %s -T 'conflict' --quiet", vim.fn.shellescape(revset)),
|
||||
"Error checking if revset has conflicts",
|
||||
nil,
|
||||
true
|
||||
)
|
||||
|
||||
if not success or not output then
|
||||
return false
|
||||
end
|
||||
|
||||
return vim.trim(output) == "true"
|
||||
end
|
||||
|
||||
--- Build describe text for a given revision
|
||||
--- @param revset? string The revision to describe (default: @)
|
||||
--- @return string[]|nil
|
||||
|
||||
+199
-17
@@ -493,7 +493,195 @@ end)
|
||||
run_test("build_log_cmd: default opts produces valid command", function()
|
||||
local cmd = log.build_log_cmd({})
|
||||
assert_equals(true, cmd:find("^jj log %-%-no%-pager") ~= nil, "Expected command to start with jj log --no-pager")
|
||||
assert_equals(true, cmd:find("--limit 20") ~= nil, "Expected default --limit 20")
|
||||
end)
|
||||
|
||||
print("\n=== Running resolve arg parsing tests ===\n")
|
||||
|
||||
local cmd = require("jj.cmd")
|
||||
|
||||
run_test("parse_resolve_args: defaults to @", function()
|
||||
local opts, err = cmd.parse_resolve_args({})
|
||||
assert_is_nil(err)
|
||||
assert_table_equals({ rev = "@" }, opts)
|
||||
end)
|
||||
|
||||
run_test("parse_resolve_args: parses revision, tool, external and filesets", function()
|
||||
local opts, err = cmd.parse_resolve_args({
|
||||
"-r",
|
||||
"abc123",
|
||||
"--tool",
|
||||
"mergiraf",
|
||||
"--external",
|
||||
"src/",
|
||||
"README.md",
|
||||
})
|
||||
assert_is_nil(err)
|
||||
assert_table_equals({
|
||||
rev = "abc123",
|
||||
external = true,
|
||||
args = { "--tool", "mergiraf" },
|
||||
filesets = { "src/", "README.md" },
|
||||
}, opts)
|
||||
end)
|
||||
|
||||
run_test("parse_resolve_args: errors on unknown long option", function()
|
||||
local opts, err = cmd.parse_resolve_args({ "--summary", "src/" })
|
||||
assert_is_nil(opts)
|
||||
assert_equals("Unknown option: --summary", err)
|
||||
end)
|
||||
|
||||
run_test("parse_resolve_args: errors on duplicate --tool", function()
|
||||
local opts, err = cmd.parse_resolve_args({ "--tool", "meld", "--tool", "mergiraf" })
|
||||
assert_is_nil(opts)
|
||||
assert_equals("Tool already set. Cannot specify multiple tools.", err)
|
||||
end)
|
||||
|
||||
run_test("parse_resolve_args: errors on duplicate revision", function()
|
||||
local opts, err = cmd.parse_resolve_args({ "-r", "a", "--revision", "b" })
|
||||
assert_is_nil(opts)
|
||||
assert_equals("Revision already set. Cannot specify multiple revisions.", err)
|
||||
end)
|
||||
|
||||
run_test("parse_resolve_args: errors on missing --tool value", function()
|
||||
local opts, err = cmd.parse_resolve_args({ "--tool" })
|
||||
assert_is_nil(opts)
|
||||
assert_equals("Missing value for --tool", err)
|
||||
end)
|
||||
|
||||
run_test("parse_resolve_args: errors on missing -r/--revision value", function()
|
||||
local opts, err = cmd.parse_resolve_args({ "-r" })
|
||||
assert_is_nil(opts)
|
||||
assert_equals("Missing value for --revision/-r", err)
|
||||
end)
|
||||
|
||||
local resolve = require("jj.cmd.resolve")
|
||||
|
||||
run_test("resolve: shellescapes args for external execution", function()
|
||||
local runner = require("jj.core.runner")
|
||||
local original_execute_command_async = runner.execute_command_async
|
||||
local original_notify = utils.notify
|
||||
local original_ensure_jj = utils.ensure_jj
|
||||
|
||||
local captured_cmd = nil
|
||||
runner.execute_command_async = function(cmd)
|
||||
captured_cmd = cmd
|
||||
end
|
||||
utils.notify = function() end
|
||||
utils.ensure_jj = function()
|
||||
return true
|
||||
end
|
||||
|
||||
local ok, err = pcall(function()
|
||||
resolve.resolve({
|
||||
rev = "abc 123",
|
||||
args = { "--tool", "my tool" },
|
||||
filesets = { "dir with spaces/", "glob:*" },
|
||||
external = true,
|
||||
})
|
||||
assert_equals(
|
||||
"'jj' 'resolve' '--revision' 'abc 123' '--tool' 'my tool' 'dir with spaces/' 'glob:*'",
|
||||
captured_cmd
|
||||
)
|
||||
end)
|
||||
|
||||
runner.execute_command_async = original_execute_command_async
|
||||
utils.notify = original_notify
|
||||
utils.ensure_jj = original_ensure_jj
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
end)
|
||||
|
||||
run_test("resolve: shellescapes args for floating execution", function()
|
||||
local terminal = require("jj.ui.terminal")
|
||||
local original_run_floating = terminal.run_floating
|
||||
local original_notify = utils.notify
|
||||
local original_ensure_jj = utils.ensure_jj
|
||||
|
||||
local captured_cmd = nil
|
||||
terminal.run_floating = function(cmd)
|
||||
captured_cmd = cmd
|
||||
end
|
||||
utils.notify = function() end
|
||||
utils.ensure_jj = function()
|
||||
return true
|
||||
end
|
||||
|
||||
local ok, err = pcall(function()
|
||||
resolve.resolve({
|
||||
rev = "abc 123",
|
||||
args = { "--tool", "my tool" },
|
||||
filesets = { "dir with spaces/", "glob:*" },
|
||||
})
|
||||
assert_equals(
|
||||
"'jj' 'resolve' '--revision' 'abc 123' '--tool' 'my tool' 'dir with spaces/' 'glob:*'",
|
||||
captured_cmd
|
||||
)
|
||||
end)
|
||||
|
||||
terminal.run_floating = original_run_floating
|
||||
utils.notify = original_notify
|
||||
utils.ensure_jj = original_ensure_jj
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
end)
|
||||
|
||||
print("\n=== Running utils helper tests ===\n")
|
||||
|
||||
run_test("is_change_conflicted: returns true when jj reports conflict", function()
|
||||
local runner = require("jj.core.runner")
|
||||
local original_execute_command = runner.execute_command
|
||||
|
||||
runner.execute_command = function(cmd, error_prefix, input, silent)
|
||||
assert_equals("jj log --no-graph -r 'abc123' -T 'conflict' --quiet", cmd)
|
||||
assert_equals("Error checking if revset has conflicts", error_prefix)
|
||||
assert_is_nil(input)
|
||||
assert_equals(true, silent)
|
||||
return "true\n", true
|
||||
end
|
||||
|
||||
local ok, err = pcall(function()
|
||||
assert_equals(true, utils.is_change_conflicted("abc123"))
|
||||
end)
|
||||
runner.execute_command = original_execute_command
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
end)
|
||||
|
||||
run_test("is_change_conflicted: returns false when jj reports no conflict", function()
|
||||
local runner = require("jj.core.runner")
|
||||
local original_execute_command = runner.execute_command
|
||||
|
||||
runner.execute_command = function()
|
||||
return "false\n", true
|
||||
end
|
||||
|
||||
local ok, err = pcall(function()
|
||||
assert_equals(false, utils.is_change_conflicted("abc123"))
|
||||
end)
|
||||
runner.execute_command = original_execute_command
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
end)
|
||||
|
||||
run_test("is_change_conflicted: returns false when jj command fails", function()
|
||||
local runner = require("jj.core.runner")
|
||||
local original_execute_command = runner.execute_command
|
||||
|
||||
runner.execute_command = function()
|
||||
return nil, false
|
||||
end
|
||||
|
||||
local ok, err = pcall(function()
|
||||
assert_equals(false, utils.is_change_conflicted("abc123"))
|
||||
end)
|
||||
runner.execute_command = original_execute_command
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
end)
|
||||
|
||||
print("\n=== Running utils.parse_bookmark_names tests ===\n")
|
||||
@@ -505,14 +693,11 @@ end)
|
||||
|
||||
run_test("parse_bookmark_names: parses multiple bookmarks", function()
|
||||
local input = "main::true feature-1::true feature-2::false"
|
||||
assert_table_equals(
|
||||
{
|
||||
{ name = "main", is_deleted = false },
|
||||
{ name = "feature-1", is_deleted = false },
|
||||
{ name = "feature-2", is_deleted = true },
|
||||
},
|
||||
utils.parse_bookmark_names(input)
|
||||
)
|
||||
assert_table_equals({
|
||||
{ name = "main", is_deleted = false },
|
||||
{ name = "feature-1", is_deleted = false },
|
||||
{ name = "feature-2", is_deleted = true },
|
||||
}, utils.parse_bookmark_names(input))
|
||||
end)
|
||||
|
||||
run_test("parse_bookmark_names: strips asterisks", function()
|
||||
@@ -538,14 +723,11 @@ end)
|
||||
|
||||
run_test("parse_bookmark_names: handles mixed input", function()
|
||||
local input = "main*::true feature-1::true feature-1@origin::true feature-2*::false"
|
||||
assert_table_equals(
|
||||
{
|
||||
{ name = "main", is_deleted = false },
|
||||
{ name = "feature-1", is_deleted = false },
|
||||
{ name = "feature-2", is_deleted = true },
|
||||
},
|
||||
utils.parse_bookmark_names(input)
|
||||
)
|
||||
assert_table_equals({
|
||||
{ name = "main", is_deleted = false },
|
||||
{ name = "feature-1", is_deleted = false },
|
||||
{ name = "feature-2", is_deleted = true },
|
||||
}, utils.parse_bookmark_names(input))
|
||||
end)
|
||||
|
||||
run_test("parse_bookmark_names: handles empty input", function()
|
||||
|
||||
Reference in New Issue
Block a user