diff --git a/lua/git/cmd.lua b/lua/git/cmd.lua index f19836d..b934325 100644 --- a/lua/git/cmd.lua +++ b/lua/git/cmd.lua @@ -1,7 +1,6 @@ local git = require("git") -local log = require("log") local repo = require("git.repo") -local util = require("util") +local util = require("git.util") local M = {} @@ -23,7 +22,7 @@ local cached_cmds ---@param result vim.SystemCompleted local function populate_cached_cmds(result) if result.code ~= 0 then - log.error("git --list-cmds failed: %s", vim.trim(result.stderr or "")) + util.error("git --list-cmds failed: %s", vim.trim(result.stderr or "")) return end cached_cmds = {} @@ -140,7 +139,7 @@ local function run_in_split(worktree, args, conf) vim.bo[buf].modifiable = false vim.bo[buf].modified = false if obj.code ~= 0 then - log.error( + util.error( "git %s failed: %s", args[1] or "", vim.trim(obj.stderr or "") @@ -216,7 +215,7 @@ end function M.run(args) local _, worktree = repo.resolve_cwd() if not worktree then - log.warning("not in a git repository") + util.warning("not in a git repository") return end diff --git a/lua/git/commit.lua b/lua/git/commit.lua index bec3bc3..67c0d91 100644 --- a/lua/git/commit.lua +++ b/lua/git/commit.lua @@ -1,6 +1,5 @@ local editor = require("git.editor") local git = require("git") -local log = require("log") local repo = require("git.repo") local M = {} @@ -10,7 +9,7 @@ function M.commit(opts) local amend = opts and opts.amend or false local _, worktree = repo.resolve_cwd() if not worktree then - log.warning("not in a git repository") + util.warning("not in a git repository") return end @@ -46,7 +45,7 @@ function M.commit(opts) local out = vim.api.nvim_buf_get_lines(buf, 0, -1, false) local fw, werr = io.open(file_path, "w") if not fw then - log.error("failed to write %s: %s", file_path, werr or "") + util.error("failed to write %s: %s", file_path, werr or "") return end fw:write(table.concat(out, "\n")) @@ -68,12 +67,12 @@ function M.commit(opts) vim.api.nvim_buf_delete(proxy_buf, { force = true }) end if result.code ~= 0 then - log.error("git commit failed: %s", vim.trim(result.stderr or "")) + util.error("git commit failed: %s", vim.trim(result.stderr or "")) return end local out = vim.trim(result.stdout or "") if out ~= "" then - log.info("%s", out) + util.info("%s", out) end end) end diff --git a/lua/git/diff.lua b/lua/git/diff.lua index eedebbe..10a8245 100644 --- a/lua/git/diff.lua +++ b/lua/git/diff.lua @@ -1,6 +1,5 @@ -local log = require("log") local repo = require("git.repo") -local util = require("util") +local util = require("git.util") local M = {} @@ -15,7 +14,7 @@ local function attach_index_writer(buf, worktree, path) vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n" ) .. "\n" - local hash_stdout = util.system_sync( + local hash_stdout = util.exec( { "git", "hash-object", "-w", "--stdin" }, { cwd = worktree, stdin = body } ) @@ -26,7 +25,7 @@ local function attach_index_writer(buf, worktree, path) local mode = vim.b[buf].git_index_mode if not mode then mode = "100644" - local ls = util.system_sync( + local ls = util.exec( { "git", "ls-files", "-s", "--", path }, { cwd = worktree, silent = true } ) @@ -42,7 +41,7 @@ local function attach_index_writer(buf, worktree, path) -- (mode,sha,path), which doesn't survive paths containing a -- comma. if - not util.system_sync({ + not util.exec({ "git", "update-index", "--cacheinfo", @@ -87,7 +86,7 @@ function M.read_uri(buf) local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd()) if not worktree then - log.error("git BufReadCmd %s: cannot resolve worktree", name) + util.error("git BufReadCmd %s: cannot resolve worktree", name) return end vim.b[buf].git_worktree = worktree @@ -104,10 +103,7 @@ function M.read_uri(buf) end local revspec = ref == "index" and (":" .. path) or (ref .. ":" .. path) - local stdout = util.system_sync( - { "git", "show", revspec }, - { cwd = worktree } - ) + local stdout = util.exec({ "git", "show", revspec }, { cwd = worktree }) if stdout then vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout)) end @@ -207,21 +203,21 @@ function M.split(opts) local cur_buf = vim.api.nvim_get_current_buf() local cur_path = vim.api.nvim_buf_get_name(cur_buf) if cur_path == "" then - log.warning("no file in current buffer") + util.warning("no file in current buffer") return end if vim.bo[cur_buf].buftype ~= "" then - log.warning("cannot diff this buffer (not a worktree file)") + util.warning("cannot diff this buffer (not a worktree file)") return end local _, worktree = repo.resolve(cur_path) if not worktree then - log.warning("not in a git repository") + util.warning("not in a git repository") return end local rel = vim.fs.relpath(worktree, cur_path) if not rel then - log.warning("file is outside the worktree") + util.warning("file is outside the worktree") return end diff --git a/lua/git/editor.lua b/lua/git/editor.lua index 4073372..3569f19 100644 --- a/lua/git/editor.lua +++ b/lua/git/editor.lua @@ -1,5 +1,3 @@ -local log = require("log") - local M = {} local SENTINEL = "__NVIM_GIT_EDIT__" @@ -59,7 +57,7 @@ local function build_stderr_handler(on_open) end local ok, err = pcall(on_open, abs_path, done) if not ok then - log.error("git.editor on_open failed: %s", tostring(err)) + util.error("git.editor on_open failed: %s", tostring(err)) done() end end) diff --git a/lua/git/log.lua b/lua/git/log.lua index 9b680b5..25f1af2 100644 --- a/lua/git/log.lua +++ b/lua/git/log.lua @@ -1,7 +1,6 @@ local git = require("git") -local log = require("log") local repo = require("git.repo") -local util = require("util") +local util = require("git.util") local M = {} @@ -21,7 +20,7 @@ function M.show(opts) local _, worktree = repo.resolve_cwd() if not worktree then - log.warning("not in a git repository") + util.warning("not in a git repository") return end @@ -38,7 +37,7 @@ function M.show(opts) table.insert(cmd, "--max-count=" .. max_count) end - local stdout = util.system_sync(cmd, { cwd = worktree }) + local stdout = util.exec(cmd, { cwd = worktree }) if not stdout then return end diff --git a/lua/git/repo.lua b/lua/git/repo.lua index f62dc22..e975b50 100644 --- a/lua/git/repo.lua +++ b/lua/git/repo.lua @@ -1,5 +1,4 @@ -local log = require("log") -local util = require("util") +local util = require("git.util") local M = {} @@ -76,7 +75,7 @@ function M.resolve(path) f:close() local gitdir = content:match("gitdir:%s*(%S+)") if not gitdir then - log.warning(".git file at %s has no `gitdir:` line", found) + util.warning(".git file at %s has no `gitdir:` line", found) return nil end if not gitdir:match("^/") then @@ -104,7 +103,7 @@ end ---@field buffers table set of registered buffer numbers ---@field watcher? uv.uv_fs_event_t ---@field refresh fun(self: ow.Git.Repo) ----@field refresh_handle ow.Util.DebounceHandle +---@field refresh_handle ow.Git.Util.DebounceHandle local Repo = {} Repo.__index = Repo @@ -188,7 +187,10 @@ local function do_refresh(repo) end end else - log.warning("git status failed: %s", vim.trim(obj.stderr or "")) + util.warning( + "git status failed: %s", + vim.trim(obj.stderr or "") + ) end local dirty = false for buf in pairs(repo.buffers) do @@ -336,7 +338,7 @@ function M.rev_parse(worktree, ref, short) table.insert(cmd, "--short") end table.insert(cmd, ref) - local stdout = util.system_sync(cmd, { cwd = worktree, silent = true }) + local stdout = util.exec(cmd, { cwd = worktree, silent = true }) local trimmed = stdout and vim.trim(stdout) or "" return trimmed ~= "" and trimmed or nil end diff --git a/lua/git/show.lua b/lua/git/show.lua index d3254b7..87cc1b0 100644 --- a/lua/git/show.lua +++ b/lua/git/show.lua @@ -1,8 +1,7 @@ local diff = require("git.diff") local git = require("git") -local log = require("log") local repo = require("git.repo") -local util = require("util") +local util = require("git.util") local M = {} @@ -109,7 +108,7 @@ end ---@param section ow.Git.DiffSection local function show_diff(ctx, section) if not section.pre_blob or not section.post_blob then - log.warning("no index line; cannot determine blob SHAs") + util.warning("no index line; cannot determine blob SHAs") return end local parent = ctx.parent_ref or "0" @@ -152,7 +151,7 @@ function M.open_commit(worktree, ref, opts) return end - local stdout = util.system_sync({ "git", "show", ref }, { cwd = worktree }) + local stdout = util.exec({ "git", "show", ref }, { cwd = worktree }) if not stdout then return end diff --git a/lua/git/sidebar.lua b/lua/git/sidebar.lua index 37003f8..227beb8 100644 --- a/lua/git/sidebar.lua +++ b/lua/git/sidebar.lua @@ -1,6 +1,5 @@ local diff = require("git.diff") local git = require("git") -local log = require("log") local repo = require("git.repo") local M = {} @@ -263,7 +262,7 @@ local function enrich_with_log(worktree, branch, groups) end end else - log.error( + util.error( "git log %s failed: %s", p.f.range, vim.trim(result.stderr or "") @@ -298,7 +297,7 @@ local function fetch_status(worktree, prefetched_stdout, callback) { cwd = worktree, text = true }, vim.schedule_wrap(function(obj) if obj.code ~= 0 then - log.error("git status failed: %s", vim.trim(obj.stderr or "")) + util.error("git status failed: %s", vim.trim(obj.stderr or "")) local branch = { ahead = 0, behind = 0 } local groups = { Untracked = {}, @@ -761,7 +760,7 @@ local function action_stage() { cwd = s.worktree }, vim.schedule_wrap(function(obj) if obj.code ~= 0 then - log.error("git add failed: %s", vim.trim(obj.stderr or "")) + util.error("git add failed: %s", vim.trim(obj.stderr or "")) end end) ) @@ -786,7 +785,7 @@ local function action_unstage() { cwd = s.worktree }, vim.schedule_wrap(function(obj) if obj.code ~= 0 then - log.error( + util.error( "git restore --staged failed: %s", vim.trim(obj.stderr or "") ) @@ -802,7 +801,7 @@ local function action_discard() end ---@cast entry ow.Git.FileEntry if entry.section == "Staged" then - log.warning("file has staged changes; unstage first with 'u'") + util.warning("file has staged changes; unstage first with 'u'") return end @@ -821,7 +820,7 @@ local function action_discard() local target = vim.fs.joinpath(s.worktree, entry.path) local rc = vim.fn.delete(target, is_dir and "rf" or "") if rc ~= 0 then - log.error("failed to delete %s", entry.path) + util.error("failed to delete %s", entry.path) end refresh(vim.api.nvim_get_current_buf()) end @@ -833,7 +832,7 @@ local function action_discard() { cwd = s.worktree }, vim.schedule_wrap(function(obj) if obj.code ~= 0 then - log.error( + util.error( "git checkout failed: %s", vim.trim(obj.stderr or "") ) @@ -950,7 +949,7 @@ function M.toggle() end local _, worktree = repo.resolve_cwd() if not worktree then - log.warning("not in a git repository") + util.warning("not in a git repository") return end open(worktree) diff --git a/lua/git/util.lua b/lua/git/util.lua new file mode 100644 index 0000000..000a83f --- /dev/null +++ b/lua/git/util.lua @@ -0,0 +1,154 @@ +local M = {} + +---@param fmt string +---@param ... any +function M.error(fmt, ...) + vim.notify(fmt:format(...), vim.log.levels.ERROR) +end + +---@param fmt string +---@param ... any +function M.warning(fmt, ...) + vim.notify(fmt:format(...), vim.log.levels.WARN) +end + +---@param fmt string +---@param ... any +function M.info(fmt, ...) + vim.notify(fmt:format(...), vim.log.levels.INFO) +end + +---@param fmt string +---@param ... any +function M.debug(fmt, ...) + vim.notify(fmt:format(...), vim.log.levels.DEBUG) +end + +---Split a string on newlines, dropping the trailing empty element that an +---input ending in `\n` produces. Convenient for slicing subprocess stdout +---into a list of lines without a phantom blank at the end. +---@param content string +---@return string[] +function M.split_lines(content) + local lines = vim.split(content, "\n", { plain = true, trimempty = false }) + if #lines > 0 and lines[#lines] == "" then + table.remove(lines) + end + return lines +end + +---@class ow.Git.Util.DebounceHandle +---@field cancel fun() +---@field flush fun() +---@field pending fun(): boolean +---@field close fun() + +---@generic F: fun(...) +---@param fn F +---@param delay integer +---@return F, ow.Git.Util.DebounceHandle +function M.debounce(fn, delay) + local timer = assert(vim.uv.new_timer()) + local args ---@type table? + local gen = 0 + local fired_gen = 0 + + local cb_main = vim.schedule_wrap(function() + -- Identity check: the libuv fire may have been superseded by a + -- re-arm or a cancel between the timer firing and this scheduled + -- callback running. + if fired_gen ~= gen or args == nil then + return + end + local a = args + args = nil + fn(vim.F.unpack_len(a)) + end) + + local cb_uv = function() + fired_gen = gen + cb_main() + end + + local function call(...) + args = vim.F.pack_len(...) + gen = gen + 1 + timer:start(delay, 0, cb_uv) + end + + return call, + { + cancel = function() + timer:stop() + args = nil + end, + flush = function() + if args == nil then + return + end + timer:stop() + local a = args + args = nil + fn(vim.F.unpack_len(a)) + end, + pending = function() + return args ~= nil + end, + close = function() + timer:stop() + if not timer:is_closing() then + timer:close() + end + args = nil + end, + } +end + +---@class ow.Git.ExecOpts +---@field cwd string? +---@field stdin string? +---@field silent boolean? suppress the auto-log on non-zero exit +---@field on_done fun(stdout: string?)? if set, run async and deliver stdout (or nil on failure) here on the main loop instead of returning sync + +---Run a system command. Default is sync: returns stdout on success or +---nil on failure (logging stderr unless `opts.silent`). When +---`opts.on_done` is set, runs async via `vim.schedule_wrap` and +---delivers the same stdout-or-nil value to that callback instead. +--- +---Async mode returns nil immediately. Callers that need access to the +---raw stderr / exit code in the failure path should opt out of this +---helper and use `vim.system` directly. +---@param cmd string[] +---@param opts ow.Git.ExecOpts? +---@return string? +function M.exec(cmd, opts) + opts = opts or {} + local sys_opts = { cwd = opts.cwd, stdin = opts.stdin, text = true } + + local function handle(result) + if result.code ~= 0 then + if not opts.silent then + local label = cmd[2] and (cmd[1] .. " " .. cmd[2]) + or cmd[1] + or "?" + M.error("%s failed: %s", label, vim.trim(result.stderr or "")) + end + return nil + end + return result.stdout or "" + end + + if opts.on_done then + vim.system( + cmd, + sys_opts, + vim.schedule_wrap(function(result) + opts.on_done(handle(result)) + end) + ) + return nil + end + return handle(vim.system(cmd, sys_opts):wait()) +end + +return M diff --git a/lua/util.lua b/lua/util.lua index 15e8637..39c3425 100644 --- a/lua/util.lua +++ b/lua/util.lua @@ -441,29 +441,4 @@ function M.split_lines(content) return lines end ----Run a system command synchronously and return stdout on success. On ----non-zero exit, logs stderr via `log.error` and returns nil. Pass ----`opts.silent` to suppress the auto-log when failure is expected (e.g. ----probe-style commands like `git rev-parse` against a possibly-missing ----ref). ----@param cmd string[] ----@param opts { cwd: string?, stdin: string?, silent: boolean? }? ----@return string? -function M.system_sync(cmd, opts) - opts = opts or {} - local result = vim.system(cmd, { - cwd = opts.cwd, - stdin = opts.stdin, - text = true, - }):wait() - if result.code ~= 0 then - if not opts.silent then - local label = cmd[2] and (cmd[1] .. " " .. cmd[2]) or cmd[1] or "?" - log.error("%s failed: %s", label, vim.trim(result.stderr or "")) - end - return nil - end - return result.stdout or "" -end - return M