feat(git): per-key cache invalidation and optional submodule tracking

This commit is contained in:
2026-05-19 09:50:31 +02:00
parent c66b2f04d2
commit 5f956401c1
7 changed files with 611 additions and 69 deletions
+150
View File
@@ -134,6 +134,156 @@ t.test("resolve_sha caches by prefix", function()
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 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 with flag on", function()
vim.g.git_submodule_recursion = true
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))
t.truthy(outer._submodules["sub"], "sub recorded as submodule")
end)
t.test("submodule: recursion flag eagerly creates child Repos and subscribes", function()
vim.g.git_submodule_recursion = true
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)
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 off", function()
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 off"
)
t.eq(next(outer._submodules), nil)
end)
t.test("submodule: outer created after inner picks up existing child", function()
vim.g.git_submodule_recursion = true
t.defer(function()
vim.g.git_submodule_recursion = nil
end)
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))