mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-05 02:36:07 +00:00
feat(log): add bookmark deletion command (#106)
This commit is contained in:
@@ -180,6 +180,8 @@ You can fetch and push directly from the log buffer:
|
||||
- `b` - Create a new bookmark or move an existing one to the revision under cursor
|
||||
- Select from existing bookmarks to move them
|
||||
- Or create a new bookmark at that revision
|
||||
- `B` - Delete a bookmark at the revision under cursor
|
||||
- If multiple bookmarks are present, select from the list or delete all of them
|
||||
|
||||
### Manage tags from the log buffer
|
||||
|
||||
@@ -512,6 +514,7 @@ revision via `jj diffedit`. Immutable revisions show an error on write.
|
||||
redo = "<S-r>", -- Redo last undone operation
|
||||
abandon = "a", -- Abandon revision under cursor
|
||||
bookmark = "b", -- Create or move bookmark to revision under cursor
|
||||
bookmark_del = "B", -- Delete bookmark of revision under cursor
|
||||
fetch = "f", -- Fetch from remote
|
||||
push = "p", -- Push bookmark of revision under cursor
|
||||
push_all = "<S-p>", -- Push all changes to remote
|
||||
|
||||
@@ -47,6 +47,7 @@ local split_module = require("jj.cmd.split")
|
||||
--- @field open_pr? string|string[]
|
||||
--- @field open_pr_list? string|string[]
|
||||
--- @field bookmark? string|string[]
|
||||
--- @field bookmark_del? string|string[]
|
||||
--- @field rebase? string|string[]
|
||||
--- @field rebase_mode? jj.cmd.rebase.keymaps
|
||||
--- @field squash? string|string[]
|
||||
@@ -164,6 +165,7 @@ M.config = {
|
||||
open_pr = "o",
|
||||
open_pr_list = "<S-o>",
|
||||
bookmark = "b",
|
||||
bookmark_del = "B",
|
||||
rebase = "r",
|
||||
rebase_mode = {
|
||||
onto = { "<CR>", "o" },
|
||||
|
||||
+112
-52
@@ -578,29 +578,35 @@ function M.handle_log_fetch()
|
||||
end
|
||||
|
||||
--- Handle pushing from `jj log` buffer.
|
||||
--- Askip the user for wich local bookmark to push.
|
||||
--- Ask the user which local bookmark to push.
|
||||
function M.handle_log_push_from_all()
|
||||
local bookkmarks = utils.get_all_bookmarks()
|
||||
local bookmarks = utils.get_all_bookmarks_with_status()
|
||||
local cmd = "jj git push"
|
||||
if not bookkmarks or #bookkmarks == 0 then
|
||||
if not bookmarks or #bookmarks == 0 then
|
||||
utils.notify("No bookmarks found to push", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
vim.ui.select(bookkmarks, {
|
||||
vim.ui.select(bookmarks, {
|
||||
prompt = "Select bookmark to push: ",
|
||||
format_item = function(item)
|
||||
if item.is_deleted then
|
||||
return item.name .. " (deleted)"
|
||||
end
|
||||
return item.name
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice then
|
||||
local push_cmd = string.format("%s -b %s", cmd, choice)
|
||||
utils.notify(string.format("Pushing bookmark `%s`...", choice), vim.log.levels.INFO)
|
||||
local push_cmd = string.format("%s -b %s", cmd, choice.name)
|
||||
utils.notify(string.format("Pushing bookmark `%s`...", choice.name), vim.log.levels.INFO)
|
||||
runner.execute_command_async(push_cmd, function(output)
|
||||
if output and string.find(output, "Nothing changed%.") then
|
||||
utils.notify("Nothing changed.", vim.log.levels.INFO)
|
||||
else
|
||||
utils.notify(string.format("Successfully pushed bookmark `%s`", choice), vim.log.levels.INFO)
|
||||
utils.notify(string.format("Successfully pushed bookmark `%s`", choice.name), vim.log.levels.INFO)
|
||||
M.log({})
|
||||
end
|
||||
end, string.format("Error pushing bookmark `%s`", choice))
|
||||
end, string.format("Error pushing bookmark `%s`", choice.name))
|
||||
else
|
||||
return
|
||||
end
|
||||
@@ -614,14 +620,9 @@ function M.handle_log_push_bookmark()
|
||||
return
|
||||
end
|
||||
|
||||
-- If we found a revfision get it's bookmark and push it
|
||||
local bookmark, success = runner.execute_command(
|
||||
string.format("jj log -r %s -T 'bookmarks' --no-graph", revset),
|
||||
string.format("Error retrieving bookmark for `%s`", revset),
|
||||
nil,
|
||||
false
|
||||
)
|
||||
if not success or not bookmark then
|
||||
local bookmarks = utils.get_bookmarks_for_rev(revset)
|
||||
if not bookmarks or #bookmarks == 0 then
|
||||
utils.notify("No bookmark found for revision", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -637,33 +638,28 @@ function M.handle_log_push_bookmark()
|
||||
end, string.format("Error pushing bookmark for `%s`", revset))
|
||||
end
|
||||
|
||||
-- If there's a * trim it (bookmarks with modifications have *)
|
||||
bookmark = bookmark:gsub("%*", ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
|
||||
if bookmark == "" then
|
||||
utils.notify("No bookmark found for revision", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- If there are multiple bookmarks user must choose
|
||||
if bookmark:find(" ") then
|
||||
-- Split by whitespace
|
||||
local bookmarks = {}
|
||||
bookmarks = vim.split(bookmark, "%s+", { trimempty = true })
|
||||
table.insert(bookmarks, "[All]")
|
||||
if #bookmarks > 1 then
|
||||
table.insert(bookmarks, { name = "[All]", is_deleted = false })
|
||||
|
||||
vim.ui.select(bookmarks, {
|
||||
prompt = "Which bookmark do you want to push?",
|
||||
format_item = function(item)
|
||||
if item.is_deleted then
|
||||
return item.name .. " (deleted)"
|
||||
end
|
||||
return item.name
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice then
|
||||
local cmd = "jj git push"
|
||||
if choice == "[All]" then
|
||||
if choice.name == "[All]" then
|
||||
-- Push all bookmarks
|
||||
cmd = string.format("%s --all", cmd)
|
||||
utils.notify("Pushing `ALL` bookmarks", vim.log.levels.INFO)
|
||||
else
|
||||
utils.notify(string.format("Pushing bookmark `%s`...", choice), vim.log.levels.INFO)
|
||||
cmd = string.format("%s -b %s", cmd, choice)
|
||||
utils.notify(string.format("Pushing bookmark `%s`...", choice.name), vim.log.levels.INFO)
|
||||
cmd = string.format("%s -b %s", cmd, choice.name)
|
||||
end
|
||||
push(cmd)
|
||||
else
|
||||
@@ -672,9 +668,9 @@ function M.handle_log_push_bookmark()
|
||||
end)
|
||||
else
|
||||
-- If there's only one bookmark simply push it
|
||||
-- Push the bookmark from the revset found
|
||||
local cmd = string.format("jj git push -b %s", bookmark)
|
||||
utils.notify(string.format("Pushing bookmark `%s`...", bookmark), vim.log.levels.INFO)
|
||||
local b = bookmarks[1].name
|
||||
local cmd = string.format("jj git push -b %s", b)
|
||||
utils.notify(string.format("Pushing bookmark `%s`...", b), vim.log.levels.INFO)
|
||||
push(cmd)
|
||||
end
|
||||
end
|
||||
@@ -732,27 +728,86 @@ function M.handle_log_open_pr(list_bookmarks)
|
||||
end
|
||||
|
||||
-- Get the bookmark for this revision
|
||||
local bookmark, success = runner.execute_command(
|
||||
string.format("jj log -r %s -T 'bookmarks' --no-graph", revset),
|
||||
string.format("Error retrieving bookmark for `%s`", revset),
|
||||
nil,
|
||||
false
|
||||
)
|
||||
|
||||
if not success or not bookmark then
|
||||
return
|
||||
end
|
||||
|
||||
-- Trim and clean the bookmark (remove asterisks and whitespace)
|
||||
bookmark = bookmark:match("^%*?(.-)%*?$"):gsub("%s+", "")
|
||||
|
||||
if bookmark == "" then
|
||||
local bookmarks = utils.get_bookmarks_for_rev(revset)
|
||||
if not bookmarks or #bookmarks == 0 then
|
||||
utils.notify("[OPEN PR] No bookmark found for revision", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Open the PR using the utility function
|
||||
utils.open_pr_for_bookmark(bookmark)
|
||||
if #bookmarks > 1 then
|
||||
vim.ui.select(bookmarks, {
|
||||
prompt = "Which bookmark do you want to open PR for?",
|
||||
format_item = function(item)
|
||||
if item.is_deleted then
|
||||
return item.name .. " (deleted)"
|
||||
end
|
||||
return item.name
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice then
|
||||
utils.open_pr_for_bookmark(choice.name)
|
||||
end
|
||||
end)
|
||||
else
|
||||
-- Open the PR using the utility function
|
||||
utils.open_pr_for_bookmark(bookmarks[1].name)
|
||||
end
|
||||
end
|
||||
|
||||
function M.handle_log_bookmark_del()
|
||||
local revset = get_revset()
|
||||
if not revset or revset == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local curr_bookmarks = utils.get_bookmarks_for_rev(revset)
|
||||
if not curr_bookmarks or #curr_bookmarks == 0 then
|
||||
utils.notify("No bookmark found for revision", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
---@param b_names string[] Bookmark names to delete
|
||||
local function delete_bookmarks(b_names)
|
||||
local escaped_list = {}
|
||||
for _, b in ipairs(b_names) do
|
||||
table.insert(escaped_list, vim.fn.shellescape(b))
|
||||
end
|
||||
local escaped_bookmarks = table.concat(escaped_list, " ")
|
||||
local cmd = string.format("jj bookmark delete %s", escaped_bookmarks)
|
||||
local b_str = table.concat(b_names, " ")
|
||||
runner.execute_command_async(cmd, function()
|
||||
utils.notify(string.format("Deleted bookmark `%s`", b_str), vim.log.levels.INFO)
|
||||
M.log({})
|
||||
end, string.format("Error deleting bookmark `%s`", b_str))
|
||||
end
|
||||
|
||||
table.insert(curr_bookmarks, { name = "[All]", is_deleted = false })
|
||||
|
||||
vim.ui.select(curr_bookmarks, {
|
||||
prompt = "Which bookmark do you want to delete?",
|
||||
format_item = function(item)
|
||||
if item.is_deleted then
|
||||
return item.name .. " (deleted)"
|
||||
end
|
||||
return item.name
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice then
|
||||
if choice.name == "[All]" then
|
||||
local all_names = vim.iter(curr_bookmarks)
|
||||
:filter(function(b)
|
||||
return b.name ~= "[All]"
|
||||
end)
|
||||
:map(function(b)
|
||||
return b.name
|
||||
end)
|
||||
:totable()
|
||||
delete_bookmarks(all_names)
|
||||
else
|
||||
delete_bookmarks({ choice.name })
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Create or move bookmark at revision under cursor in `jj log` buffer
|
||||
@@ -1191,6 +1246,11 @@ function M.log_keymaps()
|
||||
handler = M.handle_log_bookmark,
|
||||
modes = { "n" },
|
||||
},
|
||||
bookmark_del = {
|
||||
desc = "Delete bookmark at revision under cursor",
|
||||
handler = M.handle_log_bookmark_del,
|
||||
modes = { "n" },
|
||||
},
|
||||
rebase = {
|
||||
desc = "Rebase bookmark(s)",
|
||||
handler = M.handle_log_rebase,
|
||||
|
||||
@@ -370,6 +370,84 @@ function M.get_all_bookmarks()
|
||||
return bookmarks
|
||||
end
|
||||
|
||||
--- Get all bookmarks in the repository, including deleted ones
|
||||
--- @return {name: string, is_deleted: boolean}[] bookmarks List of bookmarks
|
||||
function M.get_all_bookmarks_with_status()
|
||||
local bookmarks_output, success = runner.execute_command(
|
||||
[[jj bookmark list -T 'if(!self.remote(), name ++ if(!self.present(), " (deleted)", "") ++ "\n")' --quiet]],
|
||||
"Failed to get bookmarks",
|
||||
nil,
|
||||
true
|
||||
)
|
||||
|
||||
if not success or not bookmarks_output then
|
||||
return {}
|
||||
end
|
||||
|
||||
local bookmarks = {}
|
||||
local seen = {}
|
||||
for line in bookmarks_output:gmatch("[^\n]+") do
|
||||
local trimmed = vim.trim(line)
|
||||
if trimmed ~= "" then
|
||||
local is_deleted = trimmed:match(" %(deleted%)$") ~= nil
|
||||
local name = trimmed:gsub(" %(deleted%)$", "")
|
||||
if not seen[name] then
|
||||
table.insert(bookmarks, { name = name, is_deleted = is_deleted })
|
||||
seen[name] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return bookmarks
|
||||
end
|
||||
|
||||
--- Get unique base bookmark names from a string of space-separated bookmarks.
|
||||
--- Handles the output of the template `bookmarks.map(|b| b.name() ++ "::" ++ b.present()).join(" ")`
|
||||
--- Strips asterisks, strips @remote, and deduplicates.
|
||||
--- @param bookmark_str string
|
||||
--- @return {name: string, is_deleted: boolean}[] List of unique bookmark objects
|
||||
function M.parse_bookmark_names(bookmark_str)
|
||||
local raw_items = vim.split(bookmark_str, "%s+", { trimempty = true })
|
||||
local unique_bookmarks = {}
|
||||
local seen = {}
|
||||
|
||||
for _, item in ipairs(raw_items) do
|
||||
local parts = vim.split(item, "::", { plain = true })
|
||||
if #parts == 2 then
|
||||
-- Strip asterisks and @remote
|
||||
local base_name = parts[1]:gsub("%*", ""):gsub("@.*$", "")
|
||||
local is_deleted = parts[2] == "false"
|
||||
|
||||
if base_name ~= "" and not seen[base_name] then
|
||||
table.insert(unique_bookmarks, { name = base_name, is_deleted = is_deleted })
|
||||
seen[base_name] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return unique_bookmarks
|
||||
end
|
||||
|
||||
--- Get unique base bookmark names and statuses for a given revset
|
||||
--- @param revset string
|
||||
--- @return {name: string, is_deleted: boolean}[]|nil ret List of bookmark objects, or nil on failure
|
||||
function M.get_bookmarks_for_rev(revset)
|
||||
-- Retrieve name and deleted status
|
||||
local cmd = string.format(
|
||||
[[jj log -r %s -T 'bookmarks.map(|b| b.name() ++ "::" ++ b.present()).join(" ")' --no-graph]],
|
||||
vim.fn.shellescape(revset)
|
||||
)
|
||||
|
||||
local output, success =
|
||||
runner.execute_command(cmd, string.format("Error retrieving bookmark for `%s`", revset), nil, false)
|
||||
|
||||
if not success or not output then
|
||||
return nil
|
||||
end
|
||||
|
||||
return M.parse_bookmark_names(output)
|
||||
end
|
||||
|
||||
--- Get all tags in a repository
|
||||
--- @return string[] List of bookmarks, or empty list if none found
|
||||
function M.get_all_tags()
|
||||
|
||||
+74
-8
@@ -9,6 +9,8 @@ package.path = package.path .. ";lua/?.lua;lua/?/init.lua"
|
||||
-- Load the parser module
|
||||
local parser = require("jj.core.parser")
|
||||
|
||||
local utils = require("jj.utils")
|
||||
|
||||
local tests_passed = 0
|
||||
local tests_failed = 0
|
||||
local failures = {}
|
||||
@@ -29,15 +31,19 @@ end
|
||||
|
||||
local function assert_table_equals(expected, actual, msg)
|
||||
if type(expected) ~= "table" or type(actual) ~= "table" then
|
||||
error(string.format("%s\nExpected table, got: %s and %s", msg or "Assertion failed", type(expected), type(actual)))
|
||||
error(
|
||||
string.format("%s\nExpected table, got: %s and %s", msg or "Assertion failed", type(expected), type(actual))
|
||||
)
|
||||
end
|
||||
if #expected ~= #actual then
|
||||
error(string.format("%s\nLength mismatch: expected %d, got %d", msg or "Assertion failed", #expected, #actual))
|
||||
end
|
||||
for i, v in ipairs(expected) do
|
||||
if v ~= actual[i] then
|
||||
error(string.format("%s\nAt index %d: expected %s, got %s", msg or "Assertion failed", i, tostring(v), tostring(actual[i])))
|
||||
end
|
||||
if not vim.deep_equal(expected, actual) then
|
||||
error(
|
||||
string.format(
|
||||
"%s\nExpected: %s\nGot: %s",
|
||||
msg or "Assertion failed",
|
||||
vim.inspect(expected),
|
||||
vim.inspect(actual)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -432,6 +438,66 @@ run_test("build_log_cmd: default opts produces valid command", function()
|
||||
assert_equals(true, cmd:find("--limit 20") ~= nil, "Expected default --limit 20")
|
||||
end)
|
||||
|
||||
print("\n=== Running utils.parse_bookmark_names tests ===\n")
|
||||
|
||||
run_test("parse_bookmark_names: parses simple bookmark", function()
|
||||
local input = "main::true"
|
||||
assert_table_equals({ { name = "main", is_deleted = false } }, utils.parse_bookmark_names(input))
|
||||
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)
|
||||
)
|
||||
end)
|
||||
|
||||
run_test("parse_bookmark_names: strips asterisks", function()
|
||||
local input = "main*::true feature-1*::true"
|
||||
assert_table_equals(
|
||||
{ { name = "main", is_deleted = false }, { name = "feature-1", is_deleted = false } },
|
||||
utils.parse_bookmark_names(input)
|
||||
)
|
||||
end)
|
||||
|
||||
run_test("parse_bookmark_names: strips remote suffixes", function()
|
||||
local input = "main@origin::true feature-1@remote::false"
|
||||
assert_table_equals(
|
||||
{ { name = "main", is_deleted = false }, { name = "feature-1", is_deleted = true } },
|
||||
utils.parse_bookmark_names(input)
|
||||
)
|
||||
end)
|
||||
|
||||
run_test("parse_bookmark_names: deduplicates bookmarks", function()
|
||||
local input = "main::true main@origin::true main*::true"
|
||||
assert_table_equals({ { name = "main", is_deleted = false } }, utils.parse_bookmark_names(input))
|
||||
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)
|
||||
)
|
||||
end)
|
||||
|
||||
run_test("parse_bookmark_names: handles empty input", function()
|
||||
assert_table_equals({}, utils.parse_bookmark_names(""))
|
||||
end)
|
||||
|
||||
run_test("parse_bookmark_names: handles whitespace only", function()
|
||||
assert_table_equals({}, utils.parse_bookmark_names(" "))
|
||||
end)
|
||||
|
||||
-- Print summary
|
||||
print(string.format("\n=== Test Summary ==="))
|
||||
print(string.format("Passed: %d", tests_passed))
|
||||
|
||||
Reference in New Issue
Block a user