local t = require("test") local status = require("git.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)