test(git): cover repo caching and :G completion dispatch
This commit is contained in:
@@ -1,6 +1,25 @@
|
||||
local t = require("test")
|
||||
local helpers = require("test.git.helpers")
|
||||
local cmd = require("git.cmd")
|
||||
|
||||
require("git").init()
|
||||
|
||||
local git = helpers.git
|
||||
|
||||
---@param files table<string, string>?
|
||||
---@return string dir
|
||||
local function make_repo(files)
|
||||
return helpers.make_repo(files, { cd = true })
|
||||
end
|
||||
|
||||
---@param actual string[]
|
||||
---@param expected string[]
|
||||
local function eq_sorted(actual, expected, msg)
|
||||
table.sort(actual)
|
||||
table.sort(expected)
|
||||
t.eq(actual, expected, msg)
|
||||
end
|
||||
|
||||
t.test("parse_args splits on whitespace", function()
|
||||
t.eq(
|
||||
cmd.parse_args("config user.name value"),
|
||||
@@ -129,3 +148,89 @@ t.test("positional_index ignores flags", function()
|
||||
t.eq(cmd._positional_index({ "push", "--force", "origin" }), 2)
|
||||
t.eq(cmd._positional_index({ "checkout", "-b", "feature" }), 2)
|
||||
end)
|
||||
|
||||
t.test("complete returns subcommands at first position", function()
|
||||
local matches = cmd.complete("ch", "G ch", 4)
|
||||
t.truthy(vim.tbl_contains(matches, "checkout"))
|
||||
t.truthy(vim.tbl_contains(matches, "cherry-pick"))
|
||||
end)
|
||||
|
||||
t.test("complete returns flags when arg starts with -", function()
|
||||
local matches = cmd.complete("--am", "G commit --am", 13)
|
||||
t.eq(matches, { "--amend" })
|
||||
end)
|
||||
|
||||
t.test("complete branch returns plain refs (no pseudo, no stash)", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
git(dir, "branch", "feature")
|
||||
git(dir, "tag", "v1")
|
||||
t.write(dir, "a", "modified")
|
||||
git(dir, "stash")
|
||||
local matches = cmd.complete("", "G branch ", 9)
|
||||
eq_sorted(matches, { "feature", "main", "v1" })
|
||||
end)
|
||||
|
||||
t.test("complete merge returns refs + pseudo + stash", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
git(dir, "branch", "feature")
|
||||
t.write(dir, "a", "y")
|
||||
git(dir, "stash")
|
||||
local matches = cmd.complete("", "G merge ", 8)
|
||||
eq_sorted(
|
||||
matches,
|
||||
{ "HEAD", "ORIG_HEAD", "feature", "main", "stash", "stash@{0}" }
|
||||
)
|
||||
end)
|
||||
|
||||
t.test("complete push first positional returns remotes", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
git(dir, "remote", "add", "origin", "/tmp/nope")
|
||||
git(dir, "remote", "add", "upstream", "/tmp/nope")
|
||||
local matches = cmd.complete("", "G push ", 7)
|
||||
eq_sorted(matches, { "origin", "upstream" })
|
||||
end)
|
||||
|
||||
t.test("complete push second positional returns refs", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
git(dir, "branch", "feature")
|
||||
local matches = cmd.complete("", "G push origin ", 14)
|
||||
eq_sorted(matches, { "HEAD", "feature", "main" })
|
||||
end)
|
||||
|
||||
t.test("complete add returns only unstaged/untracked paths", function()
|
||||
local dir = make_repo({ tracked = "x" })
|
||||
t.write(dir, "tracked", "modified")
|
||||
t.write(dir, "newfile", "new")
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
r:refresh()
|
||||
vim.wait(500, function()
|
||||
return r.status and #vim.tbl_keys(r.status.entries) > 0
|
||||
end)
|
||||
local matches = cmd.complete("", "G add ", 6)
|
||||
eq_sorted(matches, { "newfile", "tracked" })
|
||||
end)
|
||||
|
||||
t.test("complete after `--` returns tracked paths only", function()
|
||||
local dir = make_repo({ a = "x", b = "y" })
|
||||
t.write(dir, "untracked", "z")
|
||||
local matches = cmd.complete("", "G log -- ", 9)
|
||||
eq_sorted(matches, { "a", "b" })
|
||||
end)
|
||||
|
||||
t.test("complete stash returns subsubcommands", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
local matches = cmd.complete("p", "G stash p", 9)
|
||||
eq_sorted(matches, { "pop", "push" })
|
||||
end)
|
||||
|
||||
t.test("complete show with <rev>:<path> returns tree paths", function()
|
||||
local dir = make_repo({ a = "x", ["sub/b"] = "y" })
|
||||
local matches = cmd.complete("HEAD:", "G show HEAD:", 12)
|
||||
eq_sorted(matches, { "HEAD:a", "HEAD:sub/" })
|
||||
end)
|
||||
|
||||
t.test("complete unknown subcommand falls back to tracked paths", function()
|
||||
local dir = make_repo({ a = "x", b = "y" })
|
||||
local matches = cmd.complete("", "G nonexistent ", 14)
|
||||
eq_sorted(matches, { "a", "b" })
|
||||
end)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
local t = require("test")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param dir string
|
||||
---@vararg string
|
||||
---@return vim.SystemCompleted
|
||||
function M.git(dir, ...)
|
||||
local r = vim.system({ "git", ... }, { cwd = dir, text = true }):wait()
|
||||
if r.code ~= 0 then
|
||||
error(
|
||||
string.format(
|
||||
"git %s failed: %s",
|
||||
table.concat({ ... }, " "),
|
||||
vim.trim(r.stderr or "")
|
||||
),
|
||||
2
|
||||
)
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
---Build a temporary git repo with the given committed contents and queue
|
||||
---cleanup (stop fs watchers, drop test buffers, delete the dir). When
|
||||
---`opts.cd` is true, also `cd` into the repo and restore the previous
|
||||
---working directory on cleanup.
|
||||
---@param files table<string, string>?
|
||||
---@param opts { cd: boolean? }?
|
||||
---@return string dir
|
||||
function M.make_repo(files, opts)
|
||||
local dir = vim.fn.tempname()
|
||||
vim.fn.mkdir(dir, "p")
|
||||
M.git(dir, "init", "-q", "-b", "main")
|
||||
M.git(dir, "config", "user.email", "t@t.com")
|
||||
M.git(dir, "config", "user.name", "t")
|
||||
if files and next(files) then
|
||||
for path, content in pairs(files) do
|
||||
t.write(dir, path, content)
|
||||
end
|
||||
M.git(dir, "add", ".")
|
||||
M.git(dir, "commit", "-q", "-m", "init")
|
||||
end
|
||||
local prev_cwd
|
||||
if opts and opts.cd then
|
||||
prev_cwd = vim.fn.getcwd()
|
||||
vim.cmd.cd(dir)
|
||||
end
|
||||
t.defer(function()
|
||||
if prev_cwd then
|
||||
pcall(vim.cmd.cd, prev_cwd)
|
||||
else
|
||||
pcall(vim.cmd.cd, "/tmp")
|
||||
end
|
||||
pcall(function()
|
||||
require("git.repo").stop_all()
|
||||
end)
|
||||
vim.wait(60)
|
||||
for _, b in ipairs(vim.api.nvim_list_bufs()) do
|
||||
local name = vim.api.nvim_buf_get_name(b)
|
||||
if name:find(dir, 1, true) or name:match("^git[a-z]*://") then
|
||||
pcall(vim.api.nvim_buf_delete, b, { force = true })
|
||||
end
|
||||
end
|
||||
vim.fn.delete(dir, "rf")
|
||||
end)
|
||||
return dir
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,131 @@
|
||||
local t = require("test")
|
||||
local helpers = require("test.git.helpers")
|
||||
|
||||
require("git").init()
|
||||
|
||||
local git = helpers.git
|
||||
local make_repo = helpers.make_repo
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param key string
|
||||
---@param timeout integer?
|
||||
local function wait_cleared(r, key, timeout)
|
||||
vim.wait(timeout or 2000, function()
|
||||
return r._cache[key] == nil
|
||||
end)
|
||||
end
|
||||
|
||||
t.test("list_refs returns heads, tags, remotes (no HEAD)", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
git(dir, "branch", "feature")
|
||||
git(dir, "tag", "v1")
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
local refs = r:list_refs()
|
||||
table.sort(refs)
|
||||
t.eq(refs, { "feature", "main", "v1" })
|
||||
end)
|
||||
|
||||
t.test("list_pseudo_refs always includes HEAD", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
t.eq(r:list_pseudo_refs(), { "HEAD" })
|
||||
end)
|
||||
|
||||
t.test("list_pseudo_refs picks up MERGE_HEAD when present", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
t.write(dir .. "/.git", "MERGE_HEAD", "deadbeef\n")
|
||||
-- Bypass cache (file appeared after first scan).
|
||||
r._cache = {}
|
||||
local refs = r:list_pseudo_refs()
|
||||
table.sort(refs)
|
||||
t.eq(refs, { "HEAD", "MERGE_HEAD" })
|
||||
end)
|
||||
|
||||
t.test("list_stash_refs is empty when no stash", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
t.eq(r:list_stash_refs(), {})
|
||||
end)
|
||||
|
||||
t.test("list_stash_refs lists stash + entries when stash exists", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
t.write(dir, "a", "modified")
|
||||
git(dir, "stash")
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
local refs = r:list_stash_refs()
|
||||
t.eq(#refs, 2)
|
||||
t.eq(refs[1], "stash")
|
||||
t.eq(refs[2], "stash@{0}")
|
||||
end)
|
||||
|
||||
t.test("get_cached memoizes by key", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
local calls = 0
|
||||
local v1 = r:get_cached("k", function()
|
||||
calls = calls + 1
|
||||
return { "first" }
|
||||
end)
|
||||
local v2 = r:get_cached("k", function()
|
||||
calls = calls + 1
|
||||
return { "second" }
|
||||
end)
|
||||
t.eq(calls, 1)
|
||||
t.truthy(v1 == v2, "second call should return cached table")
|
||||
end)
|
||||
|
||||
t.test("cache clears after top-level .git change (commit)", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
local _ = r:list_refs()
|
||||
t.truthy(r._cache.refs)
|
||||
t.write(dir, "b", "y")
|
||||
git(dir, "add", "b")
|
||||
git(dir, "commit", "-q", "-m", "two")
|
||||
wait_cleared(r, "refs")
|
||||
t.falsy(r._cache.refs, "cache should be cleared after commit")
|
||||
end)
|
||||
|
||||
t.test("cache clears after slash-branch creation (polyfill)", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
local _ = r:list_refs()
|
||||
t.truthy(r._cache.refs)
|
||||
git(dir, "branch", "feat/foo")
|
||||
wait_cleared(r, "refs")
|
||||
t.falsy(r._cache.refs, "cache should clear via polyfilled subdir watcher")
|
||||
local refs = r:list_refs()
|
||||
table.sort(refs)
|
||||
t.eq(refs, { "feat/foo", "main" })
|
||||
end)
|
||||
|
||||
t.test("cache clears after deeply nested slash branch", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
local _ = r:list_refs()
|
||||
git(dir, "branch", "deep/a/b/c")
|
||||
wait_cleared(r, "refs")
|
||||
local refs = r:list_refs()
|
||||
table.sort(refs)
|
||||
t.eq(refs, { "deep/a/b/c", "main" })
|
||||
end)
|
||||
|
||||
t.test("watcher cleans up after a slash-branch dir is removed", function()
|
||||
local dir = make_repo({ a = "x" })
|
||||
local r = assert(require("git.repo").resolve(dir))
|
||||
git(dir, "branch", "feat/foo")
|
||||
-- Wait for the dynamic watcher on .git/refs/heads/feat to be added.
|
||||
local feat_path = dir .. "/.git/refs/heads/feat"
|
||||
vim.wait(2000, function()
|
||||
return r._watchers[feat_path] ~= nil
|
||||
end)
|
||||
t.truthy(r._watchers[feat_path], "feat/ subdir should be watched")
|
||||
-- Remove the branch; the feat/ directory becomes empty and is
|
||||
-- pruned by git, triggering the deleted-self event.
|
||||
git(dir, "branch", "-D", "feat/foo")
|
||||
vim.wait(2000, function()
|
||||
return r._watchers[feat_path] == nil
|
||||
end)
|
||||
t.falsy(r._watchers[feat_path], "watcher should self-close")
|
||||
end)
|
||||
@@ -1,57 +1,10 @@
|
||||
local t = require("test")
|
||||
local helpers = require("test.git.helpers")
|
||||
|
||||
require("git").init()
|
||||
|
||||
---@param dir string
|
||||
---@param ... string
|
||||
local function git(dir, ...)
|
||||
local r = vim.system({ "git", ... }, { cwd = dir, text = true }):wait()
|
||||
if r.code ~= 0 then
|
||||
error(
|
||||
string.format(
|
||||
"git %s failed: %s",
|
||||
table.concat({ ... }, " "),
|
||||
vim.trim(r.stderr or "")
|
||||
),
|
||||
2
|
||||
)
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
---Build a temporary git repo with the given committed contents and
|
||||
---queue cleanup (stop fs watchers, drop test buffers, delete the dir).
|
||||
---@param files table<string, string>?
|
||||
---@return string dir
|
||||
local function make_repo(files)
|
||||
local dir = vim.fn.tempname()
|
||||
vim.fn.mkdir(dir, "p")
|
||||
git(dir, "init", "-q", "-b", "main")
|
||||
git(dir, "config", "user.email", "t@t.com")
|
||||
git(dir, "config", "user.name", "t")
|
||||
if files and next(files) then
|
||||
for path, content in pairs(files) do
|
||||
t.write(dir, path, content)
|
||||
end
|
||||
git(dir, "add", ".")
|
||||
git(dir, "commit", "-q", "-m", "init")
|
||||
end
|
||||
t.defer(function()
|
||||
pcall(vim.cmd.cd, "/tmp")
|
||||
pcall(function()
|
||||
require("git.repo").stop_all()
|
||||
end)
|
||||
vim.wait(60)
|
||||
for _, b in ipairs(vim.api.nvim_list_bufs()) do
|
||||
local name = vim.api.nvim_buf_get_name(b)
|
||||
if name:find(dir, 1, true) or name:match("^git[a-z]*://") then
|
||||
pcall(vim.api.nvim_buf_delete, b, { force = true })
|
||||
end
|
||||
end
|
||||
vim.fn.delete(dir, "rf")
|
||||
end)
|
||||
return dir
|
||||
end
|
||||
local git = helpers.git
|
||||
local make_repo = helpers.make_repo
|
||||
|
||||
---Replicate the user's global cursor-restore autocmd. Scoped to a
|
||||
---named augroup + cleanup so it doesn't leak between tests.
|
||||
|
||||
Reference in New Issue
Block a user