feat(tag): Implement set/delete/push (push only colocated repistories) (#84)

This commit is contained in:
Nicolas GB
2026-02-22 08:53:51 -08:00
committed by GitHub
parent 86d15cecec
commit 19db4e8178
4 changed files with 304 additions and 0 deletions
+200
View File
@@ -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
+18
View File
@@ -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())
+46
View File
@@ -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()