---@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("refresh", 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("refresh", 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)