-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. local testimony = require("testimony") local fixture = require("tests.fixture") local repos = require("lilgit.repos") local testify = testimony.new("== lilgit.repos ==") if not fixture.has_git() then print("git binary not available, skipping repos_spec") testify:conclude() return end local fx = fixture.make() assert(fx, "failed to build git fixture") -- Both object layouts: loose (worktree) and packed after gc (bare). local dirs = { worktree = fx.git_dir, bare = fx.bare_dir } for label, git_dir in pairs(dirs) do testify:that("resolve_ref: branch name (" .. label .. ")", function() local sha, is_sha = repos.resolve_ref(git_dir, fx.branch) testimony.assert_equal(fx.head, sha) testimony.assert_false(is_sha) end) testify:that("resolve_ref: slashed branch (" .. label .. ")", function() testimony.assert_equal(fx.feature, (repos.resolve_ref(git_dir, "feature/foo"))) end) testify:that("resolve_ref: annotated tag peels to commit (" .. label .. ")", function() testimony.assert_equal(fx.tag_commit, (repos.resolve_ref(git_dir, "v1.0"))) end) testify:that("resolve_ref: full refs path (" .. label .. ")", function() testimony.assert_equal(fx.head, (repos.resolve_ref(git_dir, "refs/heads/" .. fx.branch))) end) testify:that("resolve_ref: HEAD (" .. label .. ")", function() testimony.assert_equal(fx.head, (repos.resolve_ref(git_dir, "HEAD"))) end) testify:that("resolve_ref: 40-hex commit sha (" .. label .. ")", function() local sha, is_sha = repos.resolve_ref(git_dir, fx.head) testimony.assert_equal(fx.head, sha) testimony.assert_true(is_sha) end) testify:that("resolve_ref: 40-hex tag sha peels to commit (" .. label .. ")", function() local sha, is_sha = repos.resolve_ref(git_dir, fx.tag) testimony.assert_equal(fx.tag_commit, sha) testimony.assert_true(is_sha) end) end testify:that("resolve_ref: rejects garbage", function() testimony.assert_nil(repos.resolve_ref(fx.git_dir, "no-such-ref")) testimony.assert_nil(repos.resolve_ref(fx.git_dir, fx.head:sub(1, 39))) testimony.assert_nil(repos.resolve_ref(fx.git_dir, "")) testimony.assert_nil(repos.resolve_ref(fx.git_dir, "a\0b")) end) testify:that("resolve_ref: sha naming a blob is not browsable", function() testimony.assert_nil(repos.resolve_ref(fx.git_dir, fx.readme_blob)) end) testify:that("walk: empty path yields the root tree", function() local node = repos.walk(fx.git_dir, fx.head, {}) testimony.assert_equal("tree", node.kind) end) testify:that("walk: nested blob", function() local node = repos.walk(fx.git_dir, fx.head, { "src", "main.lua" }) testimony.assert_equal("blob", node.kind) end) testify:that("walk: subdirectory tree", function() local node = repos.walk(fx.git_dir, fx.head, { "assets" }) testimony.assert_equal("tree", node.kind) end) testify:that("walk: symlink", function() local node = repos.walk(fx.git_dir, fx.head, { "link.md" }) testimony.assert_equal("symlink", node.kind) testimony.assert_equal("README.md", repos.read_blob(fx.git_dir, node.sha)) end) testify:that("walk: rejects traversal and bad segments", function() testimony.assert_nil((repos.walk(fx.git_dir, fx.head, { ".." }))) testimony.assert_nil((repos.walk(fx.git_dir, fx.head, { "." }))) testimony.assert_nil((repos.walk(fx.git_dir, fx.head, { "" }))) testimony.assert_nil((repos.walk(fx.git_dir, fx.head, { "a\0b" }))) testimony.assert_nil((repos.walk(fx.git_dir, fx.head, { "no-such-file" }))) testimony.assert_nil((repos.walk(fx.git_dir, fx.head, { "README.md", "deeper" }))) end) testify:that("list_tree: trees first, alphabetical within groups", function() local root = repos.walk(fx.git_dir, fx.head, {}) local listing = repos.list_tree(fx.git_dir, root.sha) local names = {} for _, entry in ipairs(listing) do names[#names + 1] = entry.name end testimony.assert_equal({ "assets", "docs", "src", "README.md", "link.md", "plain.xyz" }, names) testimony.assert_equal("symlink", listing[5].kind) end) testify:that("find_readme: prefers README.md, honors priority order", function() local root = repos.walk(fx.git_dir, fx.head, {}) local listing = repos.list_tree(fx.git_dir, root.sha) testimony.assert_equal("README.md", repos.find_readme(listing).name) local fake = { { name = "readme", kind = "blob" }, { name = "README.dj", kind = "blob" }, { name = "README.md", kind = "tree" }, } testimony.assert_equal("README.dj", repos.find_readme(fake).name) testimony.assert_nil(repos.find_readme({ { name = "other.txt", kind = "blob" } })) end) testify:that("parse_ident: splits name, email and timestamp", function() local ident = repos.parse_ident("Test User 1700000000 +0300") testimony.assert_equal("Test User", ident.name) testimony.assert_equal("test@test", ident.email) testimony.assert_equal(1700000000, ident.time) testimony.assert_equal("garbage", repos.parse_ident("garbage").name) testimony.assert_equal("", repos.parse_ident(nil).name) end) testify:that("read_commit: parses metadata, rejects non-commits", function() local commit = repos.read_commit(fx.git_dir, fx.head) testimony.assert_equal("second", commit.subject) testimony.assert_equal(fx.first, commit.parents[1]) testimony.assert_equal("test", commit.author.name) testimony.assert_true(commit.author.time > 0) testimony.assert_nil(repos.read_commit(fx.git_dir, fx.tag)) testimony.assert_nil(repos.read_commit(fx.git_dir, fx.readme_blob)) end) testify:that("log_walk: first-parent order and pagination cursor", function() local entries, next_sha = repos.log_walk(fx.git_dir, fx.head, 10) testimony.assert_equal(2, #entries) testimony.assert_equal(fx.head, entries[1].sha) testimony.assert_equal(fx.first, entries[2].sha) testimony.assert_nil(next_sha) local page, cursor = repos.log_walk(fx.git_dir, fx.head, 1) testimony.assert_equal(1, #page) testimony.assert_equal(fx.first, cursor) end) testify:that("diff_trees: modified, added and root-commit cases", function() local head = repos.read_commit(fx.git_dir, fx.head) local first = repos.read_commit(fx.git_dir, fx.first) local feature = repos.read_commit(fx.git_dir, fx.feature) local changes = repos.diff_trees(fx.git_dir, first.tree, head.tree) testimony.assert_equal({ { path = "plain.xyz", status = "modified" } }, changes) local feature_changes = repos.diff_trees(fx.git_dir, first.tree, feature.tree) testimony.assert_equal({ { path = "feature.txt", status = "added" } }, feature_changes) local root = repos.diff_trees(fx.git_dir, nil, first.tree) local by_path = {} for _, change in ipairs(root) do testimony.assert_equal("added", change.status) by_path[change.path] = true end testimony.assert_true(by_path["README.md"]) testimony.assert_true(by_path["src/main.lua"]) testimony.assert_true(by_path["assets/logo.png"]) local reverse = repos.diff_trees(fx.git_dir, feature.tree, first.tree) testimony.assert_equal({ { path = "feature.txt", status = "deleted" } }, reverse) end) testify:that("discover: finds both repos, resolves metadata", function() local registry = repos.discover({ repos_dir = fx.root, repos = {} }, nil) testimony.assert_not_nil(registry.by_name.fix) testimony.assert_not_nil(registry.by_name.bare) testimony.assert_equal({ "bare", "fix" }, { registry.ordered[1].name, registry.ordered[2].name }) testimony.assert_equal(fx.branch, registry.by_name.fix.default_branch) testimony.assert_equal(fx.git_dir, registry.by_name.fix.git_dir) testimony.assert_equal(fx.bare_dir, registry.by_name.bare.git_dir) testimony.assert_equal(fx.dir, registry.prefix_map["/fix"]) -- Stock "Unnamed repository" boilerplate is not a description. testimony.assert_nil(registry.by_name.bare.description) end) testify:that("list_branches_tags: branches and peeled tags, sorted", function() for _, git_dir in pairs(dirs) do local branches, tags = repos.list_branches_tags(git_dir) local branch_names = {} for _, ref in ipairs(branches) do branch_names[#branch_names + 1] = ref.name end testimony.assert_match("feature/foo", table.concat(branch_names, " ")) testimony.assert_match(fx.branch, table.concat(branch_names, " ")) testimony.assert_equal(1, #tags) testimony.assert_equal("v1.0", tags[1].name) testimony.assert_equal(fx.tag, tags[1].sha) testimony.assert_equal(fx.tag_commit, tags[1].target) end end) testify:that("list_tree: with_sizes attaches blob sizes, leaves trees bare", function() local root = repos.walk(fx.git_dir, fx.head, {}) local listing = repos.list_tree(fx.git_dir, root.sha, true) for _, entry in ipairs(listing) do if entry.kind == "tree" then testimony.assert_nil(entry.size) else testimony.assert_not_nil(entry.size, "no size on " .. entry.name) end if entry.name == "link.md" then testimony.assert_equal(#"README.md", entry.size) end end local bare = repos.list_tree(fx.git_dir, root.sha) testimony.assert_nil(bare[#bare].size) end) testify:that("discover: explicit config entry wins on name collision", function() local registry = repos.discover({ repos_dir = fx.root, repos = { { name = "fix", path = fx.bare_dir, description = "explicit", default_branch = "custom" } }, }, nil) testimony.assert_equal("explicit", registry.by_name.fix.description) testimony.assert_equal("custom", registry.by_name.fix.default_branch) testimony.assert_equal(fx.bare_dir, registry.by_name.fix.git_dir) end) fixture.destroy(fx) testify:conclude()