feat(git): per-key cache invalidation and optional submodule tracking
This commit is contained in:
@@ -73,4 +73,25 @@ function M.make_repo(files, opts)
|
||||
return dir
|
||||
end
|
||||
|
||||
---Build an outer repo with one nested submodule at `sub/`. Both the
|
||||
---outer and inner repo are committed and registered for cleanup.
|
||||
---@return string outer
|
||||
---@return string inner
|
||||
function M.make_submodule_repo()
|
||||
local inner = M.make_repo({ a = "x\n" })
|
||||
local outer = M.make_repo({ x = "x\n" })
|
||||
vim.system({
|
||||
"git",
|
||||
"-c",
|
||||
"protocol.file.allow=always",
|
||||
"submodule",
|
||||
"add",
|
||||
"--quiet",
|
||||
inner,
|
||||
"sub",
|
||||
}, { cwd = outer, text = true }):wait()
|
||||
M.git(outer, "commit", "-q", "-m", "add submodule")
|
||||
return outer, inner
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -325,3 +325,63 @@ t.test("Status:aggregate_at with prefix '.' includes everything", function()
|
||||
}))
|
||||
t.eq(#s:aggregate_at("."), 2)
|
||||
end)
|
||||
|
||||
t.test("entry_equal: identical changed entries", function()
|
||||
local a = { kind = "changed", path = "x", staged = "modified" }
|
||||
local b = { kind = "changed", path = "x", staged = "modified" }
|
||||
t.truthy(status.entry_equal(a, b))
|
||||
end)
|
||||
|
||||
t.test("entry_equal: differing staged side returns false", function()
|
||||
local a = { kind = "changed", path = "x", staged = "modified" }
|
||||
local b = { kind = "changed", path = "x", staged = "added" }
|
||||
t.falsy(status.entry_equal(a, b))
|
||||
end)
|
||||
|
||||
t.test("entry_equal: differing orig returns false", function()
|
||||
local a = { kind = "changed", path = "x", staged = "renamed", orig = "y" }
|
||||
local b = { kind = "changed", path = "x", staged = "renamed", orig = "z" }
|
||||
t.falsy(status.entry_equal(a, b))
|
||||
end)
|
||||
|
||||
t.test("entry_equal: nil vs nil is true", function()
|
||||
t.truthy(status.entry_equal(nil, nil))
|
||||
end)
|
||||
|
||||
t.test("entry_equal: nil vs entry is false", function()
|
||||
t.falsy(status.entry_equal(nil, { kind = "untracked", path = "x" }))
|
||||
end)
|
||||
|
||||
t.test("entry_equal: different kinds returns false", function()
|
||||
local a = { kind = "untracked", path = "x" }
|
||||
local b = { kind = "ignored", path = "x" }
|
||||
t.falsy(status.entry_equal(a, b))
|
||||
end)
|
||||
|
||||
t.test("entry_equal: differing unmerged conflict returns false", function()
|
||||
local a = { kind = "unmerged", path = "x", conflict = "both_added" }
|
||||
local b = { kind = "unmerged", path = "x", conflict = "both_modified" }
|
||||
t.falsy(status.entry_equal(a, b))
|
||||
end)
|
||||
|
||||
t.test("diff_entries: detects additions, removals, and modifications", function()
|
||||
local prior = {
|
||||
a = { kind = "changed", path = "a", staged = "modified" },
|
||||
b = { kind = "untracked", path = "b" },
|
||||
}
|
||||
local next_ = {
|
||||
a = { kind = "changed", path = "a", staged = "added" },
|
||||
c = { kind = "untracked", path = "c" },
|
||||
}
|
||||
local changed = status.diff_entries(prior, next_)
|
||||
t.truthy(changed.a, "a modified")
|
||||
t.truthy(changed.b, "b removed")
|
||||
t.truthy(changed.c, "c added")
|
||||
end)
|
||||
|
||||
t.test("diff_entries: empty when entries match", function()
|
||||
local prior = { a = { kind = "untracked", path = "a" } }
|
||||
local next_ = { a = { kind = "untracked", path = "a" } }
|
||||
t.eq(status.diff_entries(prior, next_), {})
|
||||
end)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user