feat(git): ambient progress for long-running :G subcommands
This commit is contained in:
@@ -419,3 +419,232 @@ t.test(":G diff <CR> on + line falls back to worktree file", function()
|
||||
t.truthy(require("git.object").open_under_cursor())
|
||||
t.eq(vim.api.nvim_buf_get_name(0), vim.fs.joinpath(dir, "a"))
|
||||
end)
|
||||
|
||||
---Run cmd.run via :lua so vim.fn.execute captures any nvim_echo output
|
||||
---and suppresses it from headless stdout.
|
||||
---@param args string[]
|
||||
---@return string
|
||||
local function run_capturing(args)
|
||||
return vim.trim(
|
||||
vim.fn.execute(
|
||||
string.format([[lua require("git.cmd").run(%s)]], vim.inspect(args))
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
---@return integer? pwin
|
||||
local function find_preview_win()
|
||||
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||
if vim.wo[w].previewwindow then
|
||||
return w
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function close_preview()
|
||||
pcall(vim.cmd.pclose)
|
||||
end
|
||||
|
||||
t.test("quiet :G echoes single-line stdout", function()
|
||||
make_repo({ a = "x" })
|
||||
local out = run_capturing({ "config", "user.email" })
|
||||
t.truthy(
|
||||
out:match("t@t%.com"),
|
||||
"expected output to contain t@t.com, got: " .. out
|
||||
)
|
||||
end)
|
||||
|
||||
t.test("quiet :G is silent on empty-output success", function()
|
||||
make_repo({ a = "x" })
|
||||
t.eq(run_capturing({ "config", "user.email", "new@t.com" }), "")
|
||||
end)
|
||||
|
||||
t.test("quiet :G echoes 'git exited N' on silent failure", function()
|
||||
make_repo({ a = "x" })
|
||||
local out = run_capturing({ "config", "--get", "nonexistent.foo.bar" })
|
||||
t.truthy(
|
||||
out:match("git exited 1"),
|
||||
"expected output to contain 'git exited 1', got: " .. out
|
||||
)
|
||||
end)
|
||||
|
||||
t.test("quiet :G echoes stderr on failure with output", function()
|
||||
make_repo({ a = "x" })
|
||||
local out = run_capturing({ "branch", "-d", "nonexistent-branch" })
|
||||
t.truthy(
|
||||
out:match("nonexistent%-branch"),
|
||||
"expected stderr mentioning the branch, got: " .. out
|
||||
)
|
||||
end)
|
||||
|
||||
---@param fn fun(calls: { chunks: table, history: boolean, opts: table }[])
|
||||
local function with_echo_stub(fn)
|
||||
---@type { chunks: table, history: boolean, opts: table }[]
|
||||
local calls = {}
|
||||
local original = vim.api.nvim_echo
|
||||
vim.api.nvim_echo = function(chunks, history, opts)
|
||||
table.insert(calls, {
|
||||
chunks = chunks,
|
||||
history = history,
|
||||
opts = opts or {},
|
||||
})
|
||||
return original(chunks, history, opts)
|
||||
end
|
||||
local ok, err = pcall(fn, calls)
|
||||
vim.api.nvim_echo = original
|
||||
if not ok then
|
||||
error(err, 0)
|
||||
end
|
||||
end
|
||||
|
||||
---@param calls { opts: table }[]
|
||||
---@param status string
|
||||
---@return boolean
|
||||
local function has_status(calls, status)
|
||||
for _, c in ipairs(calls) do
|
||||
if c.opts.status == status then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
t.test("streaming :G fetch (no bang) does not open a window", function()
|
||||
make_repo({ a = "x" })
|
||||
with_echo_stub(function(calls)
|
||||
local before = #vim.api.nvim_tabpage_list_wins(0)
|
||||
cmd.run({ "fetch" })
|
||||
t.wait_for(function()
|
||||
return has_status(calls, "failed")
|
||||
or has_status(calls, "success")
|
||||
end, "streaming job to terminate", 5000)
|
||||
t.eq(#vim.api.nvim_tabpage_list_wins(0), before, "no new window")
|
||||
t.falsy(find_preview_win(), "no preview window")
|
||||
end)
|
||||
end)
|
||||
|
||||
t.test(
|
||||
"streaming :G fetch (no bang) emits failed progress on bad remote",
|
||||
function()
|
||||
make_repo({ a = "x" })
|
||||
with_echo_stub(function(calls)
|
||||
cmd.run({ "fetch", "nonexistent" })
|
||||
t.wait_for(function()
|
||||
return has_status(calls, "failed")
|
||||
end, "failed progress notification", 5000)
|
||||
---@type { chunks: table, history: boolean, opts: table }?
|
||||
local final
|
||||
for _, c in ipairs(calls) do
|
||||
if c.opts.status == "failed" then
|
||||
final = c
|
||||
break
|
||||
end
|
||||
end
|
||||
t.truthy(final)
|
||||
---@cast final -nil
|
||||
t.eq(final.opts.kind, "progress")
|
||||
t.falsy(final.history, "transient progress, not history")
|
||||
t.truthy(has_status(calls, "running"), "running progress emitted")
|
||||
end)
|
||||
end
|
||||
)
|
||||
|
||||
t.test(
|
||||
"streaming :G fetch (no bang) on failure dumps output to :messages with ErrorMsg",
|
||||
function()
|
||||
make_repo({ a = "x" })
|
||||
with_echo_stub(function(calls)
|
||||
cmd.run({ "fetch", "nonexistent" })
|
||||
t.wait_for(function()
|
||||
return has_status(calls, "failed")
|
||||
end, "failed progress notification", 5000)
|
||||
local body = ""
|
||||
for _, c in ipairs(calls) do
|
||||
if
|
||||
c.history == true
|
||||
and c.chunks[1]
|
||||
and c.chunks[1][2] == "ErrorMsg"
|
||||
then
|
||||
body = c.chunks[1][1]
|
||||
break
|
||||
end
|
||||
end
|
||||
t.truthy(body ~= "", "expected ErrorMsg history dump")
|
||||
local lower = string.lower(body)
|
||||
t.truthy(
|
||||
lower:match("repository")
|
||||
or lower:match("remote")
|
||||
or lower:match("fatal"),
|
||||
"expected error mention in dump, got: " .. body
|
||||
)
|
||||
end)
|
||||
end
|
||||
)
|
||||
|
||||
t.test(
|
||||
"streaming :G fetch (no bang) on success does not dump to :messages",
|
||||
function()
|
||||
local remote = vim.fn.tempname()
|
||||
vim.fn.mkdir(remote, "p")
|
||||
h.git(remote, "init", "-q", "--bare")
|
||||
t.defer(function()
|
||||
vim.fn.delete(remote, "rf")
|
||||
end)
|
||||
|
||||
local dir = make_repo({ a = "x" })
|
||||
h.git(dir, "remote", "add", "origin", remote)
|
||||
h.git(dir, "push", "-q", "origin", "main")
|
||||
|
||||
with_echo_stub(function(calls)
|
||||
cmd.run({ "fetch", "origin" })
|
||||
t.wait_for(function()
|
||||
return has_status(calls, "success")
|
||||
end, "success progress notification", 5000)
|
||||
for _, c in ipairs(calls) do
|
||||
t.falsy(
|
||||
c.history,
|
||||
"success path must not echo to message history"
|
||||
)
|
||||
end
|
||||
end)
|
||||
end
|
||||
)
|
||||
|
||||
t.test("streaming output buffer collects runs (failure path)", function()
|
||||
make_repo({ a = "x" })
|
||||
with_echo_stub(function(calls)
|
||||
cmd.run({ "fetch", "nonexistent" })
|
||||
t.wait_for(function()
|
||||
return has_status(calls, "failed")
|
||||
end, "failed progress notification", 5000)
|
||||
end)
|
||||
require("git.history").open()
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
local found_header, found_fatal = false, false
|
||||
for _, l in ipairs(lines) do
|
||||
if l:match("^%$ git fetch nonexistent$") then
|
||||
found_header = true
|
||||
end
|
||||
if l:match("fatal:") then
|
||||
found_fatal = true
|
||||
end
|
||||
end
|
||||
t.truthy(found_header, "expected '$ git fetch nonexistent' header")
|
||||
t.truthy(found_fatal, "expected fatal: line in output buffer")
|
||||
end)
|
||||
|
||||
t.test(
|
||||
"streaming :G! fetch (bang) opens preview window with terminal buffer",
|
||||
function()
|
||||
make_repo({ a = "x" })
|
||||
t.defer(close_preview)
|
||||
|
||||
cmd.run({ "fetch" }, { bang = true })
|
||||
local pwin = find_preview_win()
|
||||
t.truthy(pwin, "expected preview window to exist")
|
||||
---@cast pwin integer
|
||||
local buf = vim.api.nvim_win_get_buf(pwin)
|
||||
t.eq(vim.bo[buf].buftype, "terminal")
|
||||
end
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user