Files
jj.nvim/lua/jj/utils.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

119 lines
3.1 KiB
Lua

local runner = require("jj.core.runner")
--- @class jj.utils
local M = {
executable_cache = {},
dependency_cache = {},
}
-- No-op setup, but keep it for API consistency in case we need it later.
function M.setup(_) end
--- Cache for executable checks to avoid repeated system calls
--- Check if an executable exists in PATH
--- @param name string The name of the executable to check
--- @return boolean True if executable exists, false otherwise
function M.has_executable(name)
if M.executable_cache[name] ~= nil then
return M.executable_cache[name]
end
local exists = vim.fn.executable(name) == 1
M.executable_cache[name] = exists
return exists
end
--- Check if the dependency is currently installed
--- @param module string The dependency module
--- @return boolean
function M.has_dependency(module)
if M.dependency_cache[module] ~= nil then
return true
end
local exists, _ = pcall(require, module)
if not exists then
M.notify(string.format("Module %s not installed", module), vim.log.levels.ERROR)
return false
end
return true
end
--- Clear the executable cache (useful for testing or if PATH changes)
function M.clear_executable_cache()
M.executable_cache = {}
end
--- Check if jj executable exists, show error if not
--- @return boolean True if jj exists, false otherwise
function M.ensure_jj()
if not M.has_executable("jj") then
M.notify("jj command not found", vim.log.levels.ERROR)
return false
end
return true
end
--- Check if we're in a jj repository
--- @return boolean True if in jj repo, false otherwise
function M.is_jj_repo()
if not M.ensure_jj() then
return false
end
-- We require the runner here to avoid a circular dependency loop at startup
local _, success = runner.execute_command("jj status")
return success
end
--- Get jj repository root path
--- @return string|nil The repository root path, or nil if not in a repo
function M.get_jj_root()
if not M.ensure_jj() then
return nil
end
-- We require the runner here to avoid a circular dependency loop at startup
local output, success = runner.execute_command("jj root")
if success and output then
return vim.trim(output)
end
return nil
end
--- Get a list of files modified in the current jj repository.
--- @return string[] A list of modified file paths
function M.get_modified_files()
if not M.ensure_jj() then
return {}
end
-- We require the runner here to avoid a circular dependency loop at startup
local result, success = runner.execute_command("jj diff --name-only", "Error getting diff")
if not success or not result then
return {}
end
local files = {}
-- Split the result into lines and add each file to the table
for file in result:gmatch("[^\r\n]+") do
table.insert(files, file)
end
return files
end
---- Notify function to display messages with a title
--- @param message string The message to display
--- @param level? number The log level (default: INFO)
--- @param timeout number? The timeout duration in milliseconds (default: 3000)
function M.notify(message, level, timeout)
level = level or vim.log.levels.INFO
timeout = timeout or 3000
vim.notify(message, level, { title = "JJ", timeout = timeout })
end
return M