import { describe, expect, it } from "vitest"; import { LineOp, type NiceDiff } from "./diff"; import { renderFormatPatch, renderUnifiedDiff } from "./rawdiff"; const modifiedFile: NiceDiff = { diff: [ { name: { old: "foo.go", new: "foo.go" }, text_fragments: [ { Comment: "func main()", OldPosition: 1, OldLines: 3, NewPosition: 1, NewLines: 3, Lines: [ { Op: LineOp.Context, Line: "package main\n" }, { Op: LineOp.Delete, Line: "// old comment\n" }, { Op: LineOp.Add, Line: "// new comment\n" }, { Op: LineOp.Context, Line: "func main() {}\n" } ] } ] } ] }; describe("renderUnifiedDiff", () => { it("renders nothing for a missing diff", () => { expect(renderUnifiedDiff(null)).toBe(""); expect(renderUnifiedDiff(undefined)).toBe(""); }); it("renders a modified file with the hunk comment", () => { expect(renderUnifiedDiff(modifiedFile)).toBe( "diff --git a/foo.go b/foo.go\n" + "--- a/foo.go\n" + "+++ b/foo.go\n" + "@@ -1,3 +1,3 @@ func main()\n" + " package main\n" + "-// old comment\n" + "+// new comment\n" + " func main() {}\n" ); }); it("renders a new file", () => { const got = renderUnifiedDiff({ diff: [ { name: { old: "", new: "new.go" }, is_new: true, text_fragments: [ { OldPosition: 0, OldLines: 0, NewPosition: 1, NewLines: 2, Lines: [ { Op: LineOp.Add, Line: "package main\n" }, { Op: LineOp.Add, Line: "func main() {}\n" } ] } ] } ] }); expect(got).toBe( "diff --git a/new.go b/new.go\n" + "new file mode 100644\n" + "--- /dev/null\n" + "+++ b/new.go\n" + "@@ -0,0 +1,2 @@\n" + "+package main\n" + "+func main() {}\n" ); }); it("renders a deleted file", () => { const got = renderUnifiedDiff({ diff: [ { name: { old: "old.go", new: "" }, is_delete: true, text_fragments: [ { OldPosition: 1, OldLines: 2, NewPosition: 0, NewLines: 0, Lines: [ { Op: LineOp.Delete, Line: "package main\n" }, { Op: LineOp.Delete, Line: "func main() {}\n" } ] } ] } ] }); expect(got).toBe( "diff --git a/old.go b/old.go\n" + "deleted file mode 100644\n" + "--- a/old.go\n" + "+++ /dev/null\n" + "@@ -1,2 +0,0 @@\n" + "-package main\n" + "-func main() {}\n" ); }); it("renders a renamed file with multiple fragments", () => { const got = renderUnifiedDiff({ diff: [ { name: { old: "old.go", new: "renamed.go" }, is_rename: true, text_fragments: [ { OldPosition: 1, OldLines: 2, NewPosition: 1, NewLines: 2, Lines: [ { Op: LineOp.Context, Line: "package main\n" }, { Op: LineOp.Delete, Line: "func old() {}\n" }, { Op: LineOp.Add, Line: "func renamed() {}\n" } ] }, { Comment: "func init()", OldPosition: 10, OldLines: 1, NewPosition: 10, NewLines: 1, Lines: [{ Op: LineOp.Context, Line: "var x = 1\n" }] } ] } ] }); expect(got).toBe( "diff --git a/old.go b/renamed.go\n" + "rename from old.go\n" + "rename to renamed.go\n" + "--- a/old.go\n" + "+++ b/renamed.go\n" + "@@ -1,2 +1,2 @@\n" + " package main\n" + "-func old() {}\n" + "+func renamed() {}\n" + "@@ -10,1 +10,1 @@ func init()\n" + " var x = 1\n" ); }); it("renders multiple files", () => { const file = (name: string, oldLine: string, newLine: string) => ({ name: { old: name, new: name }, text_fragments: [ { OldPosition: 1, OldLines: 1, NewPosition: 1, NewLines: 1, Lines: [ { Op: LineOp.Delete, Line: `${oldLine}\n` }, { Op: LineOp.Add, Line: `${newLine}\n` } ] } ] }); const got = renderUnifiedDiff({ diff: [file("a.go", "old a", "new a"), file("b.go", "old b", "new b")] }); expect(got).toBe( "diff --git a/a.go b/a.go\n" + "--- a/a.go\n" + "+++ b/a.go\n" + "@@ -1,1 +1,1 @@\n" + "-old a\n" + "+new a\n" + "diff --git a/b.go b/b.go\n" + "--- a/b.go\n" + "+++ b/b.go\n" + "@@ -1,1 +1,1 @@\n" + "-old b\n" + "+new b\n" ); }); it("marks a missing trailing newline on delete and context lines", () => { const got = renderUnifiedDiff({ diff: [ { name: { old: "foo.go", new: "foo.go" }, text_fragments: [ { OldPosition: 1, OldLines: 2, NewPosition: 1, NewLines: 2, Lines: [ { Op: LineOp.Delete, Line: "old" }, { Op: LineOp.Add, Line: "new\n" }, { Op: LineOp.Context, Line: "tail" } ] } ] } ] }); expect(got).toBe( "diff --git a/foo.go b/foo.go\n" + "--- a/foo.go\n" + "+++ b/foo.go\n" + "@@ -1,2 +1,2 @@\n" + "-old\n" + "\\ No newline at end of file\n" + "+new\n" + " tail\n" + "\\ No newline at end of file\n" ); }); }); describe("renderFormatPatch", () => { it("renders nothing for a missing diff", () => { expect(renderFormatPatch(null)).toBe(""); expect(renderFormatPatch(undefined)).toBe(""); }); it("renders the full patch for a rename and a delete", () => { const got = renderFormatPatch({ commit: { hash: [0xab, 0xc1, 0x23, 0x45, 0x67, 0x89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], message: "Fix the bug\n\nThis patch resolves the long-standing issue.\n\n\n", author: { Name: "Alice Dev", Email: "alice@example.com", When: "2024-03-15T10:30:00Z" } }, stat: { files_changed: 2, insertions: 1, deletions: 3 }, diff: [ { name: { old: "old.go", new: "renamed.go" }, is_rename: true, text_fragments: [ { OldPosition: 1, OldLines: 2, NewPosition: 1, NewLines: 2, LinesAdded: 1, LinesDeleted: 1, Lines: [ { Op: LineOp.Context, Line: "package main\n" }, { Op: LineOp.Delete, Line: "func old() {}\n" }, { Op: LineOp.Add, Line: "func renamed() {}\n" } ] } ] }, { name: { old: "gone.go", new: "" }, is_delete: true, text_fragments: [ { OldPosition: 1, OldLines: 2, NewPosition: 0, NewLines: 0, LinesAdded: 0, LinesDeleted: 2, Lines: [ { Op: LineOp.Delete, Line: "package main\n" }, { Op: LineOp.Delete, Line: "func main() {}\n" } ] } ] } ] }); expect(got).toBe( "From abc1234567890000000000000000000000000000 Mon Sep 17 00:00:00 2001\n" + "From: Alice Dev \n" + "Date: Fri, 15 Mar 2024 10:30:00 +0000\n" + "Subject: [PATCH] Fix the bug\n" + "\n" + "This patch resolves the long-standing issue.\n" + "---\n" + " renamed.go | 2 +-\n" + " gone.go | 2 --\n" + " 2 file(s) changed, 1 insertion(s)(+), 3 deletion(s)(-)\n" + "\n" + "diff --git a/old.go b/renamed.go\n" + "rename from old.go\n" + "rename to renamed.go\n" + "--- a/old.go\n" + "+++ b/renamed.go\n" + "@@ -1,2 +1,2 @@\n" + " package main\n" + "-func old() {}\n" + "+func renamed() {}\n" + "diff --git a/gone.go b/gone.go\n" + "deleted file mode 100644\n" + "--- a/gone.go\n" + "+++ /dev/null\n" + "@@ -1,2 +0,0 @@\n" + "-package main\n" + "-func main() {}\n" + "\n--\ntangled.sh\n" ); }); it("omits the body for a single-line message and uses the zero time for a missing When", () => { const got = renderFormatPatch({ commit: { message: "Single line commit", author: { Name: "", Email: "", When: "" } } }); expect(got).toBe( "From Mon Sep 17 00:00:00 2001\n" + "From: <>\n" + "Date: Mon, 01 Jan 0001 00:00:00 +0000\n" + "Subject: [PATCH] Single line commit\n" + "\n" + "---\n" + " 0 file(s) changed, 0 insertion(s)(+), 0 deletion(s)(-)\n" + "\n" + "\n--\ntangled.sh\n" ); }); it("uses the zero time for an unparseable When", () => { const got = renderFormatPatch({ commit: { this: "abc123", message: "x", author: { Name: "A", Email: "a@b.c", When: "not a date" } } }); expect(got).toContain("Date: Mon, 01 Jan 0001 00:00:00 +0000\n"); }); it("pads the year to four digits", () => { const got = renderFormatPatch({ commit: { this: "abc123", message: "x", author: { Name: "A", Email: "a@b.c", When: "0999-06-15T10:30:00Z" } } }); expect(got).toContain("Date: Sat, 15 Jun 0999 10:30:00 +0000\n"); }); });