325 lines
11 KiB
Lua
325 lines
11 KiB
Lua
---@diagnostic disable: access-invisible
|
|
local h = require("test.git.helpers")
|
|
local t = require("test")
|
|
|
|
---@param r ow.Git.Repo
|
|
---@param key string
|
|
---@param timeout integer?
|
|
local function wait_cleared(r, key, timeout)
|
|
t.wait_for(function()
|
|
return r._cache[key] == nil
|
|
end, key .. " cache to clear", timeout or 2000)
|
|
end
|
|
|
|
t.test("list_refs returns heads, tags, remotes (no HEAD)", function()
|
|
local dir = h.make_repo({ a = "x" })
|
|
h.git(dir, "branch", "feature")
|
|
h.git(dir, "tag", "v1")
|
|
local r = assert(require("git.core.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 = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.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 = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.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 = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
t.eq(r:list_stash_refs(), {})
|
|
end)
|
|
|
|
t.test("list_stash_refs lists stash + entries when stash exists", function()
|
|
local dir = h.make_repo({ a = "x" })
|
|
t.write(dir, "a", "modified")
|
|
h.git(dir, "stash")
|
|
local r = assert(require("git.core.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 = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.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 = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
local _ = r:list_refs()
|
|
t.truthy(r._cache.refs)
|
|
t.write(dir, "b", "y")
|
|
h.git(dir, "add", "b")
|
|
h.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 = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
local _ = r:list_refs()
|
|
t.truthy(r._cache.refs)
|
|
h.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 = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
local _ = r:list_refs()
|
|
h.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("resolve_sha returns ok + full sha for a known blob", function()
|
|
local dir = h.make_repo({ a = "hello\n" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
local blob = h.git(dir, "rev-parse", "HEAD:a").stdout
|
|
local short = blob:sub(1, 7)
|
|
local full, status = r:resolve_sha(short)
|
|
t.eq(status, "ok")
|
|
t.eq(full, blob)
|
|
end)
|
|
|
|
t.test("resolve_sha returns missing for an unknown prefix", function()
|
|
local dir = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
local full, status = r:resolve_sha("0000deadbeef")
|
|
t.eq(full, nil)
|
|
t.eq(status, "missing")
|
|
end)
|
|
|
|
t.test("resolve_sha caches by prefix", function()
|
|
local dir = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
local blob = h.git(dir, "rev-parse", "HEAD:a").stdout
|
|
local short = blob:sub(1, 7)
|
|
local _, _ = r:resolve_sha(short)
|
|
t.truthy(r._cache["resolve:" .. short], "result should be cached")
|
|
end)
|
|
|
|
---@param r ow.Git.Repo
|
|
local function wait_initial(r)
|
|
t.wait_for(function()
|
|
return r.status.branch.head ~= nil
|
|
end, "initial fetch to complete", 2000)
|
|
end
|
|
|
|
t.test("_invalidate clears only matching keys for HEAD", function()
|
|
local dir = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
r._cache.head = "h"
|
|
r._cache.refs = { "main" }
|
|
r._cache.pseudo_refs = { "HEAD" }
|
|
r._cache.stash_refs = {}
|
|
r._cache["resolve:abc"] = { "deadbeef", "ok" }
|
|
r:_invalidate("HEAD")
|
|
t.eq(r._cache.head, nil)
|
|
t.eq(r._cache.pseudo_refs, nil)
|
|
t.eq(r._cache["resolve:abc"], nil)
|
|
t.truthy(r._cache.refs)
|
|
t.truthy(r._cache.stash_refs)
|
|
end)
|
|
|
|
t.test("_invalidate clears refs/head/resolve for refs/heads/*", function()
|
|
local dir = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
r._cache.head = "h"
|
|
r._cache.refs = { "main" }
|
|
r._cache.pseudo_refs = { "HEAD" }
|
|
r._cache.stash_refs = {}
|
|
r._cache["resolve:abc"] = { "deadbeef", "ok" }
|
|
r:_invalidate("refs/heads/feature")
|
|
t.eq(r._cache.head, nil)
|
|
t.eq(r._cache.refs, nil)
|
|
t.eq(r._cache["resolve:abc"], nil)
|
|
t.truthy(r._cache.pseudo_refs)
|
|
t.truthy(r._cache.stash_refs)
|
|
end)
|
|
|
|
t.test("_invalidate clears config on .git/config change", function()
|
|
local dir = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
r._cache.config = { core = {} }
|
|
r:_invalidate("config")
|
|
t.eq(r._cache.config, nil)
|
|
end)
|
|
|
|
t.test("status_entry_for: exact match on case-sensitive repo", function()
|
|
local dir = h.make_repo({ Foo = "x" })
|
|
t.write(dir, "Foo", "modified")
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
wait_initial(r)
|
|
t.truthy(r:status_entry_for("Foo"))
|
|
t.eq(r:status_entry_for("foo"), nil, "case mismatch returns nil")
|
|
end)
|
|
|
|
t.test("status_entry_for: case-insensitive fallback when core.ignorecase=true", function()
|
|
local dir = h.make_repo({ Foo = "x" })
|
|
h.git(dir, "config", "core.ignorecase", "true")
|
|
t.write(dir, "Foo", "modified")
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
wait_initial(r)
|
|
t.truthy(r:status_entry_for("Foo"), "exact match")
|
|
t.truthy(r:status_entry_for("foo"), "lowercase finds Foo")
|
|
t.truthy(r:status_entry_for("FOO"), "uppercase finds Foo")
|
|
end)
|
|
|
|
t.test("_invalidate matches stash_refs on refs/stash and logs/refs/stash", function()
|
|
local dir = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
r._cache.stash_refs = {}
|
|
r:_invalidate("refs/stash")
|
|
t.eq(r._cache.stash_refs, nil)
|
|
r._cache.stash_refs = {}
|
|
r:_invalidate("logs/refs/stash")
|
|
t.eq(r._cache.stash_refs, nil)
|
|
end)
|
|
|
|
t.test("refresh with invalidate=true wipes cache on next fetch", function()
|
|
local dir = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
wait_initial(r)
|
|
r._cache.head = "stale"
|
|
r._cache["resolve:abc"] = { "x", "ok" }
|
|
r:refresh({ invalidate = true })
|
|
t.wait_for(function()
|
|
return r._cache.head == nil
|
|
end, "cache wiped after invalidating refresh completes", 2000)
|
|
t.eq(r._cache.head, nil)
|
|
t.eq(r._cache["resolve:abc"], nil)
|
|
end)
|
|
|
|
t.test("refresh emits change.paths listing structurally-changed paths", function()
|
|
local dir = h.make_repo({ a = "1", b = "1" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
wait_initial(r)
|
|
t.write(dir, "a", "2")
|
|
---@type ow.Git.Repo.Change?
|
|
local change_seen
|
|
local unsub = r:on("change", function(change)
|
|
change_seen = change
|
|
end)
|
|
r:refresh()
|
|
t.wait_for(function()
|
|
return change_seen ~= nil
|
|
end, "refresh emit", 2000)
|
|
unsub()
|
|
local change = assert(change_seen)
|
|
t.truthy(change.paths["a"])
|
|
t.falsy(change.paths["b"], "b is unchanged structurally")
|
|
end)
|
|
|
|
t.test("submodule: parent enumerates initialized submodules by default", function()
|
|
local outer_path = h.make_submodule_repo()
|
|
local outer = assert(require("git.core.repo").resolve(outer_path))
|
|
t.truthy(outer._submodules["sub"], "sub recorded as submodule")
|
|
end)
|
|
|
|
t.test("submodule: eagerly creates child Repos and subscribes by default", function()
|
|
local outer_path = h.make_submodule_repo()
|
|
local outer = assert(require("git.core.repo").resolve(outer_path))
|
|
wait_initial(outer)
|
|
local inner = require("git.core.repo").all()[outer_path .. "/sub"]
|
|
t.truthy(inner, "inner Repo eagerly created")
|
|
t.truthy(outer._submodules["sub"] and outer._submodules["sub"].unsub, "inner subscribed by outer")
|
|
|
|
t.write(outer_path .. "/sub", "a", "modified\n")
|
|
---@type ow.Git.Repo.Change?
|
|
local outer_change
|
|
local unsub = outer:on("change", function(change)
|
|
outer_change = change
|
|
end)
|
|
inner:refresh()
|
|
t.wait_for(function()
|
|
return outer_change ~= nil
|
|
end, "outer notified by inner refresh", 2000)
|
|
unsub()
|
|
|
|
local entry = outer.status.entries["sub"]
|
|
t.truthy(entry, "outer sub entry now present")
|
|
t.eq(entry.kind, "changed")
|
|
end)
|
|
|
|
t.test("submodule: no eager creation when flag is explicitly disabled", function()
|
|
vim.g.git_submodule_recursion = false
|
|
t.defer(function()
|
|
vim.g.git_submodule_recursion = nil
|
|
end)
|
|
local outer_path = h.make_submodule_repo()
|
|
local outer = assert(require("git.core.repo").resolve(outer_path))
|
|
wait_initial(outer)
|
|
t.eq(
|
|
require("git.core.repo").all()[outer_path .. "/sub"],
|
|
nil,
|
|
"inner Repo not created when flag is false"
|
|
)
|
|
t.eq(next(outer._submodules), nil)
|
|
end)
|
|
|
|
t.test("submodule: outer created after inner picks up existing child", function()
|
|
local outer_path = h.make_submodule_repo()
|
|
local inner = assert(
|
|
require("git.core.repo").resolve(outer_path .. "/sub")
|
|
)
|
|
wait_initial(inner)
|
|
local outer = assert(require("git.core.repo").resolve(outer_path))
|
|
wait_initial(outer)
|
|
t.truthy(outer._submodules["sub"] and outer._submodules["sub"].unsub, "outer subscribed to pre-existing inner")
|
|
end)
|
|
|
|
t.test("watcher cleans up after a slash-branch dir is removed", function()
|
|
local dir = h.make_repo({ a = "x" })
|
|
local r = assert(require("git.core.repo").resolve(dir))
|
|
h.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"
|
|
t.wait_for(function()
|
|
return r._watchers[feat_path] ~= nil
|
|
end, "watcher to be installed on feat/ subdir", 2000)
|
|
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.
|
|
h.git(dir, "branch", "-D", "feat/foo")
|
|
t.wait_for(function()
|
|
return r._watchers[feat_path] == nil
|
|
end, "watcher on feat/ subdir to close", 2000)
|
|
t.falsy(r._watchers[feat_path], "watcher should self-close")
|
|
end)
|