mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-15 23:53:18 +00:00
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
This commit is contained in:
+14
-45
@@ -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
|
||||
|
||||
+68
-5
@@ -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 = "<cmd>close!<CR>",
|
||||
},
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
+14
-7
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user