Files
jj.nvim/lua/jj/cmd/describe.lua
T
NicolasGB e22bf5ec4e feat: Add fetch/push commands and log keybinds, make long-running operations non-blocking
Convert blocking jj commands to async using vim.fn.jobstart() to prevent UI
freezing during fetch, push, rebase, squash, and other heavy operations.

Changes:
- Implement execute_command_async() in runner with proper error handling
  - Captures stdout from job and passes to success callback
  - Supports stdin input for commands that require it
  - Includes silent flag to suppress error notifications
  - Replicates sync version's error handling behavior

- Convert blocking operations to async:
  - handle_log_fetch, handle_log_push_all, handle_log_new
  - handle_log_edit, handle_log_abandon, handle_log_describe
  - describe.execute_describe, all init.lua commands

- Add push/fetch as public commands with CLI support:
  - M.fetch() - fetch from remote
  - M.push(opts) - push all changes or specific bookmark
  - CLI: :J push, :J push <bookmark>, :J fetch

- Implement handle_log_push_bookmark() for revision under cursor:
  - Retrieves and strips modified bookmark indicator (*)
  - Pushes specific bookmark to remote
  - Keybind: Shift+p in log buffer

- Update README with new keybinds:
  - a: abandon revision under cursor
  - f: fetch from remote
  - p: push all changes
  - Shift+p: push revision's bookmark

- Add Lua API docs for push command options
2025-11-27 20:58:34 +01:00

154 lines
4.7 KiB
Lua

--- @class jj.cmd.describe
local M = {}
local utils = require("jj.utils")
local runner = require("jj.core.runner")
local parser = require("jj.core.parser")
local terminal = require("jj.ui.terminal")
local editor = require("jj.ui.editor")
--- @class jj.cmd.describe_opts
--- @field with_status boolean: Whether or not `jj st` should be displayed in a buffer while describing the commit
--- @type jj.cmd.describe_opts
local default_describe_opts = {
with_status = true,
}
--- Execute jj describe command with the given description
--- @param description string The description text
--- @param revset? string The revision to describe
local function execute_describe(description, revset)
if not description or description == "" then
utils.notify("Description cannot be empty", vim.log.levels.ERROR)
return
end
local cmd = "jj describe"
if revset then
cmd = cmd .. " -r " .. revset
end
cmd = cmd .. " --stdin"
-- Use --stdin to properly handle multi-line and special characters
runner.execute_command_async(cmd, function()
utils.notify("Description set.", vim.log.levels.INFO)
end, "Failed to describe", description)
end
--- Resolve describe editor keymaps from config
--- @return jj.core.buffer.keymap
local function describe_editor_keymaps()
local cmd = require("jj.cmd")
local cfg = cmd.config.describe.editor.keymaps or {}
return cmd.resolve_keymaps_from_specs(cfg, {
close = {
desc = "Close describe editor without saving",
handler = "<cmd>close!<CR>",
},
})
end
--- Jujutsu describe
--- @param description? string Optional description text
--- @param revset? string The revision to describe
--- @param opts? jj.cmd.describe_opts Optional command options
function M.describe(description, revset, opts)
if not utils.ensure_jj() then
return
end
-- Check if a description was provided otherwise require for input
if description then
-- Description provided directly
execute_describe(description, revset)
return
end
if not revset then
revset = "@"
end
local cmd = require("jj.cmd")
-- Use buffer editor mode (defaults to "buffer" if not configured)
local editor_mode = cmd.config.describe.editor.type or "buffer"
if editor_mode == "buffer" then
local jj_cmd = "jj log -r " .. revset .. " --no-graph -T 'coalesce(description, \"\n\")'"
local old_description_raw, success = runner.execute_command(jj_cmd, "Failed to get old description")
if not old_description_raw or not success then
return
end
local log_cmd = "jj log -r " .. revset .. " --no-graph -T 'self.diff().summary()'"
local status_result, success2 = runner.execute_command(log_cmd, "Error getting status")
if not success2 then
return
end
local status_files = parser.get_status_files(status_result)
local old_description = vim.trim(old_description_raw)
-- Split description into lines to preserve multiline descriptions
local description_lines = vim.split(old_description, "\n")
local text = {}
for _, line in ipairs(description_lines) do
table.insert(text, line)
end
table.insert(text, "") -- Empty line to separate from user input
table.insert(text, "JJ: Change ID: " .. revset)
table.insert(text, "JJ: This commit contains the following changes:")
for _, item in ipairs(status_files) do
table.insert(text, string.format("JJ: %s %s", item.status, item.file))
end
table.insert(text, "JJ:") -- blank line
table.insert(text, 'JJ: Lines starting with "JJ:" (like this one) will be removed')
-- Check if we're coming from the log view so we can reopen it after editing
local open_log_on_close = terminal.state.buf_cmd == "log"
-- Close the terminal buffer before opening editor
terminal.close_terminal_buffer()
editor.open_editor(text, function(buf_lines)
local user_lines = {}
for _, line in ipairs(buf_lines) do
if not line:match("^JJ:") then
table.insert(user_lines, line)
end
end
-- Join lines and trim leading/trailing whitespace
local trimmed_description = table.concat(user_lines, "\n"):gsub("^%s+", ""):gsub("%s+$", "")
execute_describe(trimmed_description, revset)
-- Once editing is done, reopen the log if we came from there
end, function()
if open_log_on_close then
vim.schedule(function()
cmd.log({})
end)
end
end, describe_editor_keymaps())
else
-- Use input mode
local merged_opts = vim.tbl_deep_extend("force", default_describe_opts, opts or {})
if merged_opts.with_status then
-- Show the status in a terminal buffer
cmd.status()
end
vim.ui.input({
prompt = "Description: ",
default = "",
}, function(input)
-- If the user inputed something, execute the describe command
if input then
execute_describe(input, revset)
end
-- Close the current terminal when finished
terminal.close_terminal_buffer()
end)
end
end
return M