388 lines
12 KiB
Lua
388 lines
12 KiB
Lua
local t = require("test")
|
|
local status = require("git.core.status")
|
|
|
|
local NUL = "\0"
|
|
|
|
---@param parts string[]
|
|
---@return string
|
|
local function nul(parts)
|
|
return table.concat(parts, NUL) .. NUL
|
|
end
|
|
|
|
t.test("branch headers: initial repo, no commits", function()
|
|
local s = status.parse(nul({
|
|
"# branch.oid (initial)",
|
|
"# branch.head main",
|
|
}))
|
|
t.eq(s.branch.oid, nil)
|
|
t.eq(s.branch.head, "main")
|
|
t.eq(s.branch.upstream, nil)
|
|
t.eq(s.branch.ahead, 0)
|
|
t.eq(s.branch.behind, 0)
|
|
end)
|
|
|
|
t.test("branch headers: detached HEAD", function()
|
|
local s = status.parse(nul({
|
|
"# branch.oid 1234567890abcdef1234567890abcdef12345678",
|
|
"# branch.head (detached)",
|
|
}))
|
|
t.eq(s.branch.oid, "1234567890abcdef1234567890abcdef12345678")
|
|
t.eq(s.branch.head, nil)
|
|
end)
|
|
|
|
t.test("branch headers: with upstream and ahead/behind", function()
|
|
local s = status.parse(nul({
|
|
"# branch.oid abc123",
|
|
"# branch.head main",
|
|
"# branch.upstream origin/main",
|
|
"# branch.ab +3 -2",
|
|
}))
|
|
t.eq(s.branch.head, "main")
|
|
t.eq(s.branch.upstream, "origin/main")
|
|
t.eq(s.branch.ahead, 3)
|
|
t.eq(s.branch.behind, 2)
|
|
end)
|
|
|
|
t.test("type 1: staged-only modification", function()
|
|
local s = status.parse(nul({
|
|
"1 M. N... 100644 100644 100644 abc abc foo.lua",
|
|
}))
|
|
local e = s.entries["foo.lua"]
|
|
---@cast e ow.Git.Status.ChangedEntry
|
|
t.eq(e.kind, "changed")
|
|
t.eq(e.path, "foo.lua")
|
|
t.eq(e.staged, "modified")
|
|
t.eq(e.unstaged, nil)
|
|
t.eq(e.orig, nil)
|
|
end)
|
|
|
|
t.test("type 1: unstaged-only modification", function()
|
|
local s = status.parse(nul({
|
|
"1 .M N... 100644 100644 100644 abc abc foo.lua",
|
|
}))
|
|
local e = s.entries["foo.lua"]
|
|
---@cast e ow.Git.Status.ChangedEntry
|
|
t.eq(e.staged, nil)
|
|
t.eq(e.unstaged, "modified")
|
|
end)
|
|
|
|
t.test("type 1: both sides modified", function()
|
|
local s = status.parse(nul({
|
|
"1 MM N... 100644 100644 100644 abc abc foo.lua",
|
|
}))
|
|
local e = s.entries["foo.lua"]
|
|
---@cast e ow.Git.Status.ChangedEntry
|
|
t.eq(e.staged, "modified")
|
|
t.eq(e.unstaged, "modified")
|
|
end)
|
|
|
|
t.test("type 1: deleted (unstaged)", function()
|
|
local s = status.parse(nul({
|
|
"1 .D N... 100644 100644 000000 abc abc foo.lua",
|
|
}))
|
|
local e = s.entries["foo.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
|
t.eq(e.unstaged, "deleted")
|
|
end)
|
|
|
|
t.test("type 1: added (staged)", function()
|
|
local s = status.parse(nul({
|
|
"1 A. N... 000000 100644 100644 abc abc new.lua",
|
|
}))
|
|
local e = s.entries["new.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
|
t.eq(e.staged, "added")
|
|
end)
|
|
|
|
t.test("type 1: type-changed (unstaged)", function()
|
|
local s = status.parse(nul({
|
|
"1 .T N... 100644 100644 120000 abc abc foo.lua",
|
|
}))
|
|
local e = s.entries["foo.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
|
t.eq(e.unstaged, "type_changed")
|
|
end)
|
|
|
|
t.test("type 2: renamed with orig", function()
|
|
local s = status.parse(nul({
|
|
"2 R. N... 100644 100644 100644 abc abc R100 new.lua",
|
|
"old.lua",
|
|
}))
|
|
local e = s.entries["new.lua"]
|
|
---@cast e ow.Git.Status.ChangedEntry
|
|
t.eq(e.kind, "changed")
|
|
t.eq(e.path, "new.lua")
|
|
t.eq(e.staged, "renamed")
|
|
t.eq(e.orig, "old.lua")
|
|
end)
|
|
|
|
t.test("type 2: copied with orig", function()
|
|
local s = status.parse(nul({
|
|
"2 C. N... 100644 100644 100644 abc abc C90 copy.lua",
|
|
"src.lua",
|
|
}))
|
|
local e = s.entries["copy.lua"]
|
|
---@cast e ow.Git.Status.ChangedEntry
|
|
t.eq(e.staged, "copied")
|
|
t.eq(e.orig, "src.lua")
|
|
end)
|
|
|
|
t.test("type u: all seven conflict types", function()
|
|
local cases = {
|
|
{ xy = "DD", expected = "both_deleted" },
|
|
{ xy = "AU", expected = "added_by_us" },
|
|
{ xy = "UD", expected = "deleted_by_them" },
|
|
{ xy = "UA", expected = "added_by_them" },
|
|
{ xy = "DU", expected = "deleted_by_us" },
|
|
{ xy = "AA", expected = "both_added" },
|
|
{ xy = "UU", expected = "both_modified" },
|
|
}
|
|
for _, c in ipairs(cases) do
|
|
local s = status.parse(nul({
|
|
string.format(
|
|
"u %s N... 100644 100644 100644 100644 abc abc abc conflict.lua",
|
|
c.xy
|
|
),
|
|
}))
|
|
local e = s.entries["conflict.lua"]
|
|
t.eq(e.kind, "unmerged", "kind for " .. c.xy)
|
|
t.eq(
|
|
(e --[[@as ow.Git.Status.UnmergedEntry]]).conflict,
|
|
c.expected,
|
|
"conflict for " .. c.xy
|
|
)
|
|
end
|
|
end)
|
|
|
|
t.test("type ?: untracked", function()
|
|
local s = status.parse(nul({ "? new.txt" }))
|
|
local e = s.entries["new.txt"]
|
|
t.eq(e.kind, "untracked")
|
|
t.eq(e.path, "new.txt")
|
|
end)
|
|
|
|
t.test("type !: ignored", function()
|
|
local s = status.parse(nul({ "! .secret" }))
|
|
local e = s.entries[".secret"]
|
|
t.eq(e.kind, "ignored")
|
|
end)
|
|
|
|
t.test("mixed: branch + multiple variants", function()
|
|
local s = status.parse(nul({
|
|
"# branch.oid abc",
|
|
"# branch.head main",
|
|
"# branch.upstream origin/main",
|
|
"# branch.ab +0 -0",
|
|
"1 M. N... 100644 100644 100644 a a staged.lua",
|
|
"1 .M N... 100644 100644 100644 a a unstaged.lua",
|
|
"1 MM N... 100644 100644 100644 a a both.lua",
|
|
"u UU N... 100644 100644 100644 100644 a a a conflict.lua",
|
|
"? untracked.txt",
|
|
"! ignored.txt",
|
|
}))
|
|
t.eq(s.branch.head, "main")
|
|
local staged = s.entries["staged.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
|
local unstaged = s.entries["unstaged.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
|
local both = s.entries["both.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
|
t.eq(staged.staged, "modified")
|
|
t.eq(unstaged.unstaged, "modified")
|
|
t.eq(both.staged, "modified")
|
|
t.eq(both.unstaged, "modified")
|
|
t.eq(s.entries["conflict.lua"].kind, "unmerged")
|
|
t.eq(s.entries["untracked.txt"].kind, "untracked")
|
|
t.eq(s.entries["ignored.txt"].kind, "ignored")
|
|
end)
|
|
|
|
t.test("paths with spaces survive splitting", function()
|
|
local s = status.parse(nul({
|
|
"1 .M N... 100644 100644 100644 a a path with spaces.lua",
|
|
}))
|
|
local e = s.entries["path with spaces.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
|
t.eq(e.unstaged, "modified")
|
|
end)
|
|
|
|
t.test("mark_for: changed staged modified", function()
|
|
local entry = {
|
|
kind = "changed",
|
|
path = "x",
|
|
staged = "modified",
|
|
}
|
|
t.eq(status.mark_for(entry, "staged"), { char = "M", hl = "GitStagedModified" })
|
|
end)
|
|
|
|
t.test("mark_for: changed unstaged deleted uses GitUnstagedDeleted", function()
|
|
local entry = {
|
|
kind = "changed",
|
|
path = "x",
|
|
unstaged = "deleted",
|
|
}
|
|
t.eq(status.mark_for(entry, "unstaged"), { char = "D", hl = "GitUnstagedDeleted" })
|
|
end)
|
|
|
|
t.test("mark_for: changed renamed uses per-side renamed hl", function()
|
|
local entry = {
|
|
kind = "changed",
|
|
path = "x",
|
|
staged = "renamed",
|
|
orig = "y",
|
|
}
|
|
t.eq(status.mark_for(entry, "staged"), { char = "R", hl = "GitStagedRenamed" })
|
|
end)
|
|
|
|
t.test("mark_for: untracked / ignored / unmerged ignore side", function()
|
|
t.eq(
|
|
status.mark_for({ kind = "untracked", path = "x" } --[[@as ow.Git.Status.Entry]]),
|
|
{ char = "?", hl = "GitUntracked" }
|
|
)
|
|
t.eq(
|
|
status.mark_for({ kind = "ignored", path = "x" } --[[@as ow.Git.Status.Entry]]),
|
|
{ char = "i", hl = "GitIgnored" }
|
|
)
|
|
t.eq(
|
|
status.mark_for({
|
|
kind = "unmerged",
|
|
path = "x",
|
|
conflict = "both_modified",
|
|
} --[[@as ow.Git.Status.Entry]]),
|
|
{ char = "!", hl = "GitUnmergedBothModified" }
|
|
)
|
|
end)
|
|
|
|
t.test("marks_for: changed with both sides yields two marks", function()
|
|
local entry = {
|
|
kind = "changed",
|
|
path = "x",
|
|
staged = "modified",
|
|
unstaged = "modified",
|
|
}
|
|
local marks = status.marks_for(entry)
|
|
t.eq(#marks, 2)
|
|
t.eq(marks[1], { char = "M", hl = "GitStagedModified" })
|
|
t.eq(marks[2], { char = "M", hl = "GitUnstagedModified" })
|
|
end)
|
|
|
|
t.test("marks_for: changed one-sided yields one mark", function()
|
|
local entry = { kind = "changed", path = "x", staged = "added" }
|
|
local marks = status.marks_for(entry)
|
|
t.eq(#marks, 1)
|
|
t.eq(marks[1], { char = "A", hl = "GitStagedAdded" })
|
|
end)
|
|
|
|
t.test("marks_for: untracked yields one mark", function()
|
|
local entry = { kind = "untracked", path = "x" } --[[@as ow.Git.Status.Entry]]
|
|
local marks = status.marks_for(entry)
|
|
t.eq(#marks, 1)
|
|
t.eq(marks[1], { char = "?", hl = "GitUntracked" })
|
|
end)
|
|
|
|
t.test("Status:rows buckets by section", function()
|
|
local s = status.parse(nul({
|
|
"1 M. N... 100644 100644 100644 a a staged.lua",
|
|
"1 .M N... 100644 100644 100644 a a unstaged.lua",
|
|
"1 MM N... 100644 100644 100644 a a both.lua",
|
|
"? untracked.txt",
|
|
}))
|
|
t.eq(#s:rows("staged"), 2, "staged section: staged.lua + both.lua")
|
|
t.eq(#s:rows("unstaged"), 2, "unstaged section: unstaged.lua + both.lua")
|
|
t.eq(#s:rows("untracked"), 1)
|
|
t.eq(#s:rows("unmerged"), 0)
|
|
t.eq(#s:rows("ignored"), 0)
|
|
end)
|
|
|
|
t.test("Status:rows for staged carries side='staged'", function()
|
|
local s = status.parse(nul({
|
|
"1 M. N... 100644 100644 100644 a a x.lua",
|
|
}))
|
|
local row = assert(s:rows("staged")[1])
|
|
t.eq(row.section, "staged")
|
|
t.eq(row.side, "staged")
|
|
t.eq(row.entry.kind, "changed")
|
|
end)
|
|
|
|
t.test("Status:rows for untracked has nil side", function()
|
|
local s = status.parse(nul({ "? x.txt" }))
|
|
local row = assert(s:rows("untracked")[1])
|
|
t.eq(row.section, "untracked")
|
|
t.eq(row.side, nil)
|
|
end)
|
|
|
|
t.test("Status:aggregate_at dedups marks under prefix", function()
|
|
local s = status.parse(nul({
|
|
"1 .M N... 100644 100644 100644 a a sub/a.lua",
|
|
"1 .M N... 100644 100644 100644 a a sub/b.lua",
|
|
"? sub/c.txt",
|
|
}))
|
|
local marks = s:aggregate_at("sub")
|
|
t.eq(#marks, 2, "modified ('M') and untracked ('?') deduped")
|
|
local m1 = assert(marks[1])
|
|
local m2 = assert(marks[2])
|
|
local hls = { m1.hl, m2.hl }
|
|
table.sort(hls)
|
|
t.eq(hls, { "GitUnstagedModified", "GitUntracked" })
|
|
end)
|
|
|
|
t.test("Status:aggregate_at with prefix '.' includes everything", function()
|
|
local s = status.parse(nul({
|
|
"1 .M N... 100644 100644 100644 a a a.lua",
|
|
"? b.txt",
|
|
}))
|
|
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)
|
|
|