local util = require("git.util") local M = {} local SENTINEL = "__NVIM_GIT_EDIT__" -- Each git editor invocation runs this body afresh under `sh -c`. The -- body picks a per-invocation flag file via `$$` (the wrapping shell's -- pid), prints the sentinel + flag-path + abs-path on stderr so the -- running Neovim can find both, and polls the flag until Neovim writes -- it. Concurrent edits inside one git call (e.g. `rebase -i`'s todo -- plus N reword commits) get distinct flags because each invocation is -- a fresh shell with a fresh `$$`. local SCRIPT = string.format( [=[set -eu flag="${TMPDIR:-/tmp}/nvim-git-editor-$$.done" trap 'rm -f "$flag"' EXIT abs=$(realpath "$1") printf '%s\t%%s\t%%s\n' "$flag" "$abs" >&2 while [ ! -e "$flag" ]; do sleep 0.05 done ]=], SENTINEL ) ---POSIX shell single-quote escape: foo'bar -> 'foo'\''bar'. ---@param s string ---@return string local function shq(s) return "'" .. s:gsub("'", "'\\''") .. "'" end local GIT_EDITOR = "sh -c " .. shq(SCRIPT) .. " --" ---Build a stderr callback that strips our sentinel lines, accumulates ---the rest, and dispatches `on_open(abs, done)` for each sentinel seen. ---The `finalize(result)` helper drains any trailing partial line into ---`result.stderr` so the caller can treat `result` like a plain ---`vim.system` result. ---@param on_open fun(file_path: string, done: fun()) ---@return fun(err: string?, data: string?), fun(result: vim.SystemCompleted) local function build_stderr_handler(on_open) local pending = "" local stderr_buf = {} local function dispatch(flag_path, abs_path) vim.schedule(function() local fired = false local function done() if fired then return end fired = true local fw = io.open(flag_path, "w") if fw then fw:close() end end local ok, err = pcall(on_open, abs_path, done) if not ok then util.error("git.editor on_open failed: %s", tostring(err)) done() end end) end local pattern = "^" .. SENTINEL .. "\t(.-)\t(.+)$" local function on_stderr(_, data) if not data or data == "" then return end pending = pending .. data while true do local nl = pending:find("\n", 1, true) if not nl then break end local line = pending:sub(1, nl - 1) pending = pending:sub(nl + 1) local flag, abs = line:match(pattern) if flag then dispatch(flag, abs) else table.insert(stderr_buf, line) table.insert(stderr_buf, "\n") end end end local function finalize(result) if pending ~= "" then table.insert(stderr_buf, pending) end result.stderr = table.concat(stderr_buf) end return on_stderr, finalize end ---Run a git command with an editor proxy active. When git invokes the ---editor, `on_open` fires with the absolute file path git wants edited ---plus a `done` callback. The caller opens the file in a buffer and ---invokes `done()` once the user is finished (typically from a ---`BufWipeout` autocmd). For commands that fire the editor more than ---once in a single git invocation (`git rebase -i`, with one call for ---the todo and one per `reword`), `on_open` is invoked once per ---editor handoff. --- ---`on_exit` is called on the main loop with a `vim.SystemCompleted`- ---shaped result. `result.stderr` has our protocol sentinels stripped. ---@param cmd string[] ---@param opts? { cwd?: string, env?: table } ---@param on_open fun(file_path: string, done: fun()) ---@param on_exit fun(result: vim.SystemCompleted) function M.run(cmd, opts, on_open, on_exit) opts = opts or {} local on_stderr, finalize = build_stderr_handler(on_open) local env = vim.tbl_extend("force", opts.env or {}, { GIT_EDITOR = GIT_EDITOR, GIT_SEQUENCE_EDITOR = GIT_EDITOR, }) vim.system( cmd, { cwd = opts.cwd, text = true, env = env, stderr = on_stderr, }, vim.schedule_wrap(function(result) finalize(result) on_exit(result) end) ) end return M