From 149ae9ebc7f86f60e0fb376e880ddaa1a87e7fa2 Mon Sep 17 00:00:00 2001 From: NicolasGB Date: Wed, 4 Feb 2026 11:13:23 +0100 Subject: [PATCH] fix(commit): Migrate from simulated commit to jj's native command. - This required some improvements on the editor buffer logic, introduced a `on_write` (previously on_done but it wasn't the right meaning) hook and an `on_unload` to allow for both describe and commit correct workflows. - Introduced an utils function to extract the description text and clean it post buffer write --- lua/jj/cmd/describe.lua | 59 ++++++++------------------------- lua/jj/cmd/init.lua | 73 ++++++++++++++++++++++++++++++++++++++--- lua/jj/ui/editor.lua | 21 ++++++++---- lua/jj/ui/terminal.lua | 9 +++++ lua/jj/utils.lua | 63 +++++++++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 57 deletions(-) diff --git a/lua/jj/cmd/describe.lua b/lua/jj/cmd/describe.lua index e51c53a..c180bdd 100644 --- a/lua/jj/cmd/describe.lua +++ b/lua/jj/cmd/describe.lua @@ -3,7 +3,6 @@ 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") @@ -18,8 +17,7 @@ local default_describe_opts = { --- Execute jj describe command with the given description --- @param description string The description text --- @param revset? string The revision to describe ---- @param on_close function|nil The function to run on success -local function execute_describe(description, revset, on_close) +local function execute_describe(description, revset) if not description or description == "" then utils.notify("Description cannot be empty", vim.log.levels.ERROR) return @@ -34,12 +32,6 @@ local function execute_describe(description, revset, on_close) -- Use --stdin to properly handle multi-line and special characters runner.execute_command_async(cmd, function() utils.notify("Description set.", vim.log.levels.INFO) - -- If an on close callback is provided, call it - if on_close then - vim.schedule(function() - on_close() - end) - end end, "Failed to describe", description) end @@ -69,7 +61,7 @@ function M.describe(description, revset, opts, on_close) -- Check if a description was provided otherwise require for input if description then -- Description provided directly - execute_describe(description, revset, on_close) + execute_describe(description, revset) return end @@ -83,48 +75,20 @@ function M.describe(description, revset, opts, on_close) -- Use buffer editor mode (defaults to "buffer" if not configured) local editor_mode = merged_opts.type or cmd.config.describe.editor.type or "buffer" if editor_mode == "buffer" then - local jj_cmd = "jj log -r " .. revset .. " --quiet --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 + local text = utils.get_describe_text(revset) + if not text then return end - local log_cmd = "jj log -r " .. revset .. " --quiet --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') - -- 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 + local trimmed_description = utils.extract_description_from_describe(buf_lines) + if not trimmed_description then + -- If nothing is provide simply exit + return 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() @@ -148,10 +112,15 @@ function M.describe(description, revset, opts, on_close) }, function(input) -- If the user inputed something, execute the describe command if input then - execute_describe(input, revset, on_close) + execute_describe(input, revset) end -- Close the current terminal when finished terminal.close_terminal_buffer() + if on_close then + vim.schedule(function() + on_close() + end) + end end) end end diff --git a/lua/jj/cmd/init.lua b/lua/jj/cmd/init.lua index c1cc8b5..49e7ff9 100644 --- a/lua/jj/cmd/init.lua +++ b/lua/jj/cmd/init.lua @@ -3,8 +3,8 @@ 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") local diff = require("jj.diff") local log_module = require("jj.cmd.log") @@ -701,10 +701,73 @@ function M.commit(description) return end - M.describe(description, nil, nil, function() - -- Opinionated -A flag since it seems more intuitive when editing and commiting past changes and has no effect on top changes - M.new({ args = "-A @" }) - end) + local should_refresh = terminal.is_log_buffer_open() + + if description and description ~= "" then + local cmd = "jj commit --message " .. vim.fn.shellescape(description) + runner.execute_command_async(cmd, function() + utils.notify("Committed.", vim.log.levels.INFO) + if should_refresh then + vim.schedule(function() + M.log() + end) + end + end, "Failed to commit") + return + end + + local editor_mode = M.config.describe.editor.type or "buffer" + if editor_mode == "input" then + M.status() + vim.ui.input({ prompt = "Description: ", default = "" }, function(input) + if input and not input:match("^%s*$") then + local cmd = "jj commit --message " .. vim.fn.shellescape(input) + runner.execute_command_async(cmd, function() + utils.notify("Committed.", vim.log.levels.INFO) + if should_refresh then + vim.schedule(function() + M.log() + end) + end + end, "Failed to commit") + elseif input then + utils.notify("Description cannot be empty", vim.log.levels.ERROR) + end + terminal.close_terminal_buffer() + end) + return + end + + local text = utils.get_describe_text("@") + if not text then + return + end + + terminal.close_terminal_buffer() + + local keymaps = M.resolve_keymaps_from_specs(M.config.describe.editor.keymaps or {}, { + close = { + desc = "Close commit editor without saving", + handler = "close!", + }, + }) + + editor.open_editor(text, nil, function(buf_lines) + local trimmed_description = utils.extract_description_from_describe(buf_lines) + if not trimmed_description then + utils.notify("Description cannot be empty", vim.log.levels.ERROR) + return + end + local cmd = "jj commit --message " .. vim.fn.shellescape(trimmed_description) + runner.execute_command_async(cmd, function() + utils.notify("Committed.", vim.log.levels.INFO) + if should_refresh then + vim.schedule(function() + M.log() + end) + end + end, "Failed to commit") + end, keymaps) end --- @param args string|string[] jj command arguments diff --git a/lua/jj/ui/editor.lua b/lua/jj/ui/editor.lua index 2db4d5d..08d1336 100644 --- a/lua/jj/ui/editor.lua +++ b/lua/jj/ui/editor.lua @@ -60,10 +60,10 @@ function M.setup(opts) end ---@param initial_text string[] Lines to initialize the buffer with ----@param on_done fun(buf: string[])? Optional callback called with user text on buffer write ----@param on_unload? fun()? Optional callback called when the buffer is closed +---@param on_write fun(buf: string[])? Optional callback called with user text on buffer write +---@param on_unload? fun(buf: string[])? Optional callback with user text called when the buffer is closed ---@param keymaps? jj.core.buffer.keymap[] Optional keymaps for the buffer -function M.open_editor(initial_text, on_done, on_unload, keymaps) +function M.open_editor(initial_text, on_write, on_unload, keymaps) -- Initialize highlight groups once init_highlights() @@ -137,11 +137,15 @@ function M.open_editor(initial_text, on_done, on_unload, keymaps) vim.api.nvim_create_autocmd("BufWriteCmd", { buffer = buf, callback = function() + -- Get current buffer lines local buf_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) - if on_done then - on_done(buf_lines) - end + -- Update the last written lines variable + vim.b[buf].jj_last_written_lines = buf_lines vim.bo[buf].modified = false + -- Call the on_write callback if provided + if on_write then + on_write(buf_lines) + end end, }) @@ -150,7 +154,10 @@ function M.open_editor(initial_text, on_done, on_unload, keymaps) vim.api.nvim_create_autocmd("BufWipeout", { buffer = buf, callback = function() - on_unload() + local last_written = vim.b[buf].jj_last_written_lines + if last_written then + on_unload(last_written) + end end, }) end diff --git a/lua/jj/ui/terminal.lua b/lua/jj/ui/terminal.lua index 9a3dcc0..cb5f310 100644 --- a/lua/jj/ui/terminal.lua +++ b/lua/jj/ui/terminal.lua @@ -207,6 +207,15 @@ function M.restore_cursor_position() state.cursor_restore_pos = nil end +--- Check whether the log buffer is currently active +--- @return boolean +function M.is_log_buffer_open() + if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then + return false + end + return state.buf_cmd == "log" +end + --- Run the command in a floating window --- @param cmd string The command to run in the floating window --- @param keymaps jj.core.buffer.keymap[]|nil Additional keymaps to set for this floating buffer diff --git a/lua/jj/utils.lua b/lua/jj/utils.lua index 1cb8c52..36e38a6 100644 --- a/lua/jj/utils.lua +++ b/lua/jj/utils.lua @@ -273,6 +273,50 @@ function M.is_change_immutable(revset) return vim.trim(output) == "true" end + +--- Build describe text for a given revision +--- @param revset? string The revision to describe (default: @) +--- @return string[]|nil +function M.get_describe_text(revset) + if not revset or revset == "" then + revset = "@" + end + + local parser = require("jj.core.parser") + local old_description_raw, success = runner.execute_command( + "jj log -r " .. revset .. " --quiet --no-graph -T 'coalesce(description, \"\\n\")'", + "Failed to get old description" + ) + if not old_description_raw or not success then + return nil + end + + local status_result, success2 = runner.execute_command( + "jj log -r " .. revset .. " --quiet --no-graph -T 'self.diff().summary()'", + "Error getting status" + ) + if not success2 then + return nil + end + + local status_files = parser.get_status_files(status_result) + local old_description = vim.trim(old_description_raw) + 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, "") + 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:") + table.insert(text, 'JJ: Lines starting with "JJ:" (like this one) will be removed') + + return text +end --- --- Get the commit id from a given revision --- @param revset string The revset to extract the commit id from @@ -293,4 +337,23 @@ function M.get_commit_id(revset) return vim.trim(output) end +--- Extract the description from the describe text +--- @param lines string[] The lines of a described change +--- @return string|nil +function M.extract_description_from_describe(lines) + local final_lines = {} + for _, line in ipairs(lines) do + if not line:match("^JJ:") then + table.insert(final_lines, line) + end + end + -- Join lines and trim leading/trailing whitespace + local trimmed_description = table.concat(final_lines, "\n"):gsub("^%s+", ""):gsub("%s+$", "") + if trimmed_description == "" then + -- If nothing return nil + return + end + return trimmed_description +end + return M