mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-05 10:46:08 +00:00
feat(tag): Implement set/delete/push (push only colocated repistories) (#84)
This commit is contained in:
@@ -55,6 +55,7 @@ local split_module = require("jj.cmd.split")
|
||||
--- @field quick_squash? string|string[]
|
||||
--- @field summary? string|string[]
|
||||
--- @field summary_tooltip? jj.cmd.summary_tooltip.keymaps
|
||||
--- @field tag_set? string|string[]
|
||||
|
||||
--- @class jj.cmd.rebase.keymaps
|
||||
--- @field onto? string|string[]
|
||||
@@ -189,6 +190,7 @@ M.config = {
|
||||
edit_immutable = "<S-CR>",
|
||||
},
|
||||
split = "<C-s>",
|
||||
tag_set = "<S-t>",
|
||||
},
|
||||
status = {
|
||||
open_file = "<CR>",
|
||||
@@ -824,6 +826,187 @@ function M.commit(description)
|
||||
end, keymaps)
|
||||
end
|
||||
|
||||
--- Jujutsu tag set
|
||||
--- @param rev string|nil
|
||||
function M.tag_set(rev)
|
||||
if not utils.ensure_jj() then
|
||||
return
|
||||
end
|
||||
|
||||
local should_refresh = terminal.is_log_buffer_open()
|
||||
|
||||
-- If the revision is not provided, ask the user for it,
|
||||
if not rev then
|
||||
if not should_refresh then
|
||||
M.log({})
|
||||
end
|
||||
|
||||
vim.ui.input({ prompt = "Revision to tag: " }, function(input)
|
||||
if input and not input:match("^%s*$") then
|
||||
rev = input
|
||||
M.tag_set(rev)
|
||||
else
|
||||
end
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
-- Ask the user for the tag name
|
||||
vim.ui.input({ prompt = "Tag name: ", default = "" }, function(input)
|
||||
if input and not input:match("^%s*$") then
|
||||
local cmd = string.format("jj tag set %s -r %s", input, rev)
|
||||
runner.execute_command_async(cmd, function()
|
||||
utils.notify(string.format("Tag `%s` set on `%s`.", input, rev), vim.log.levels.INFO)
|
||||
if should_refresh then
|
||||
vim.schedule(function()
|
||||
M.log()
|
||||
end)
|
||||
end
|
||||
end, "Failed to set tag")
|
||||
elseif input then
|
||||
utils.notify("Tag name cannot be empty", vim.log.levels.ERROR)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Jujutsu tag delete
|
||||
--- @param tag string|nil If provided, it will delete the given tag without asking the user
|
||||
function M.tag_delete(tag)
|
||||
if not utils.ensure_jj() then
|
||||
return
|
||||
end
|
||||
|
||||
-- If the tag is provided, delete it directly without asking the user
|
||||
if tag then
|
||||
local cmd = string.format("jj tag delete %s", tag)
|
||||
runner.execute_command_async(cmd, function()
|
||||
utils.notify(string.format("Tag `%s` deleted.", tag), vim.log.levels.INFO)
|
||||
if terminal.is_log_buffer_open() then
|
||||
vim.schedule(function()
|
||||
M.log()
|
||||
end)
|
||||
end
|
||||
end, "Failed to delete tag")
|
||||
return
|
||||
end
|
||||
|
||||
local should_refresh = terminal.is_log_buffer_open()
|
||||
if not should_refresh then
|
||||
M.log({})
|
||||
should_refresh = true
|
||||
end
|
||||
|
||||
local tags = utils.get_all_tags()
|
||||
if #tags == 0 then
|
||||
utils.notify("No tags found to delete", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
vim.ui.select(tags, { prompt = "Select tag to delete: " }, function(choice)
|
||||
if choice then
|
||||
local cmd = string.format("jj tag delete %s", choice)
|
||||
runner.execute_command_async(cmd, function()
|
||||
utils.notify(string.format("Tag `%s` deleted.", choice), vim.log.levels.INFO)
|
||||
if should_refresh then
|
||||
vim.schedule(function()
|
||||
M.log()
|
||||
end)
|
||||
end
|
||||
end, "Failed to delete tag")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Push all tags but only on collocated reposiotries
|
||||
function M.tag_push()
|
||||
if not utils.ensure_jj() then
|
||||
return
|
||||
end
|
||||
|
||||
local should_refresh = terminal.is_log_buffer_open()
|
||||
local is_colocated = utils.is_colocated()
|
||||
local has_git = utils.has_executable("git")
|
||||
|
||||
if not is_colocated then
|
||||
utils.notify("Current repository is not colocated. Cannot push tags.", vim.log.levels.ERROR)
|
||||
return
|
||||
elseif not has_git then
|
||||
utils.notify("Git executable not found. Cannot push tags.", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local remotes = utils.get_remotes()
|
||||
if not remotes or #remotes == 0 then
|
||||
utils.notify("No git remotes found. Cannot push tags.", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- If many remotes we are forced to request the user what to do
|
||||
if remotes and #remotes > 1 then
|
||||
vim.ui.select(remotes, {
|
||||
prompt = "Select remote to push tags to: ",
|
||||
format_item = function(item)
|
||||
return string.format("%s (%s)", item.name, item.url)
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice then
|
||||
local tags = utils.get_all_tags()
|
||||
if not tags or #tags == 0 then
|
||||
utils.notify("No tags found to push", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
vim.ui.select(tags, {
|
||||
prompt = "Select tag to push: ",
|
||||
}, function(tag_choice)
|
||||
if tag_choice then
|
||||
local cmd = string.format("git push %s %s", choice.name, tag_choice)
|
||||
runner.execute_command_async(cmd, function()
|
||||
utils.notify(
|
||||
string.format("Tag `%s` pushed successfully to remote `%s`.", tag_choice, choice.name),
|
||||
vim.log.levels.INFO
|
||||
)
|
||||
if should_refresh then
|
||||
vim.schedule(function()
|
||||
M.log()
|
||||
end)
|
||||
end
|
||||
end, "Failed to push tag")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Otherwise we can push directly to the only remote
|
||||
if remotes and #remotes == 1 then
|
||||
local tags = utils.get_all_tags()
|
||||
if not tags or #tags == 0 then
|
||||
utils.notify("No tags found to push", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
vim.ui.select(tags, {
|
||||
prompt = "Select tag to push: ",
|
||||
}, function(tag_choice)
|
||||
if tag_choice then
|
||||
local cmd = string.format("git push %s %s", remotes[1].name, tag_choice)
|
||||
runner.execute_command_async(cmd, function()
|
||||
utils.notify(
|
||||
string.format("Tag `%s` pushed successfully to remote `%s`.", tag_choice, remotes[1].name),
|
||||
vim.log.levels.INFO
|
||||
)
|
||||
if should_refresh then
|
||||
vim.schedule(function()
|
||||
M.log()
|
||||
end)
|
||||
end
|
||||
end, "Failed to push tag")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param args string|string[] jj command arguments
|
||||
function M.j(args)
|
||||
if not utils.ensure_jj() then
|
||||
@@ -964,6 +1147,22 @@ function M.j(args)
|
||||
commit = function()
|
||||
M.commit(remaining_args_str ~= "" and remaining_args_str or nil)
|
||||
end,
|
||||
tag = function()
|
||||
if remaining_args[1] == "set" or remaining_args[1] == "s" then
|
||||
-- If the user provided a revision, set the tag on that revision, otherwise ask for it in the flow of the command
|
||||
if remaining_args[2] then
|
||||
M.tag_set(remaining_args[2])
|
||||
else
|
||||
M.tag_set(remaining_args[2])
|
||||
end
|
||||
elseif remaining_args[1] == "delete" or remaining_args[1] == "d" then
|
||||
if remaining_args[2] then
|
||||
M.tag_delete(remaining_args[2])
|
||||
else
|
||||
M.tag_delete()
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
if handlers[subcommand] then
|
||||
@@ -1012,6 +1211,7 @@ function M.register_command()
|
||||
"annotate",
|
||||
"annotate_line",
|
||||
"commit",
|
||||
"tag",
|
||||
}
|
||||
local matches = {}
|
||||
for _, cmd in ipairs(subcommands) do
|
||||
|
||||
@@ -582,6 +582,19 @@ function M.handle_log_push_bookmark()
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle seting a tag from `jj log` buffer for the revision under cursor
|
||||
--- @param revset? string Revset to set the tag on, if not provided it will do nothing.
|
||||
function M.handle_log_tag_set(revset)
|
||||
if not revset or revset == "" then
|
||||
revset = revset or get_revset()
|
||||
if not revset or revset == "" then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
require("jj.cmd").tag_set(revset)
|
||||
end
|
||||
|
||||
--- Handle opening a PR/MR from `jj log` buffer for the revision under cursor
|
||||
--- @param list_bookmarks? boolean If true, prompt to select from all bookmarks instead of using current revision
|
||||
function M.handle_log_open_pr(list_bookmarks)
|
||||
@@ -1032,6 +1045,11 @@ function M.log_keymaps()
|
||||
handler = M.handle_log_split,
|
||||
modes = { "n" },
|
||||
},
|
||||
tag_set = {
|
||||
desc = "Set a tag under the current revset",
|
||||
handler = M.handle_log_tag_set,
|
||||
modes = { "n" },
|
||||
},
|
||||
}
|
||||
|
||||
return cmd.merge_keymaps(cmd.resolve_keymaps_from_specs(keymaps, specs), cmd.terminal_keymaps())
|
||||
|
||||
@@ -157,6 +157,52 @@ function M.get_all_bookmarks()
|
||||
return bookmarks
|
||||
end
|
||||
|
||||
--- Get all tags in a repository
|
||||
--- @return string[] List of bookmarks, or empty list if none found
|
||||
function M.get_all_tags()
|
||||
-- Use a custom template to output just the bookmark names, one per line
|
||||
-- This is more reliable than parsing the default output format
|
||||
local bookmarks_output, success = runner.execute_command(
|
||||
[[jj tag list --quiet --sort committer-date- -T 'if(!self.remote(), name ++ if(!self.present(), " (deleted)", "") ++ "\n")']],
|
||||
"Failed to get bookmarks",
|
||||
nil,
|
||||
true
|
||||
)
|
||||
|
||||
if not success or not bookmarks_output then
|
||||
return {}
|
||||
end
|
||||
|
||||
-- Parse tags from template output
|
||||
local tags = {}
|
||||
local seen = {}
|
||||
for line in bookmarks_output:gmatch("[^\n]+") do
|
||||
local tag = vim.trim(line)
|
||||
if tag ~= "" and not seen[tag] then
|
||||
-- Skip tags containing "(deleted)"
|
||||
if not tag:match("%(deleted%)") then
|
||||
table.insert(tags, tag)
|
||||
seen[tag] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return tags
|
||||
end
|
||||
|
||||
--- Whether or not the repository is colocated
|
||||
--- @return boolean
|
||||
function M.is_colocated()
|
||||
local output, success =
|
||||
runner.execute_command("jj git colocation status ", "Failed to determine if repository is colocated", nil, true)
|
||||
|
||||
if not success or not output then
|
||||
return false
|
||||
end
|
||||
|
||||
return vim.startswith(vim.trim(output), "Workspace is currently colocated with Git.")
|
||||
end
|
||||
|
||||
--- Get all bookmarks in the repository, filters out deleted bookmarks
|
||||
--- @return string[] List of bookmarks, or empty list if none found
|
||||
function M.get_untracked_bookmarks()
|
||||
|
||||
Reference in New Issue
Block a user