287 lines
7.5 KiB
TypeScript
287 lines
7.5 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { parseGitNumstat, parseGitDiff, parseShortstat } from "../gitDiff";
|
|
|
|
describe("parseGitNumstat", () => {
|
|
test("parses single file", () => {
|
|
const result = parseGitNumstat("5\t3\tsrc/foo.ts");
|
|
expect(result.stats).toEqual({
|
|
filesCount: 1,
|
|
linesAdded: 5,
|
|
linesRemoved: 3,
|
|
});
|
|
expect(result.perFileStats.get("src/foo.ts")).toEqual({
|
|
added: 5,
|
|
removed: 3,
|
|
isBinary: false,
|
|
});
|
|
});
|
|
|
|
test("parses multiple files", () => {
|
|
const input = "10\t2\ta.ts\n3\t0\tb.ts\n0\t7\tc.ts";
|
|
const result = parseGitNumstat(input);
|
|
expect(result.stats).toEqual({
|
|
filesCount: 3,
|
|
linesAdded: 13,
|
|
linesRemoved: 9,
|
|
});
|
|
expect(result.perFileStats.size).toBe(3);
|
|
});
|
|
|
|
test("handles binary file with dash counts", () => {
|
|
const result = parseGitNumstat("-\t-\timage.png");
|
|
expect(result.perFileStats.get("image.png")).toEqual({
|
|
added: 0,
|
|
removed: 0,
|
|
isBinary: true,
|
|
});
|
|
});
|
|
|
|
test("handles rename format", () => {
|
|
const result = parseGitNumstat("1\t0\told.txt => new.txt");
|
|
const entry = result.perFileStats.get("old.txt => new.txt");
|
|
expect(entry).not.toBeUndefined();
|
|
expect(entry!.added).toBe(1);
|
|
expect(entry!.isBinary).toBe(false);
|
|
});
|
|
|
|
test("handles filename with tabs", () => {
|
|
const result = parseGitNumstat('1\t0\t"tab\tfile.txt"');
|
|
// parts.slice(2).join('\t') preserves the rest
|
|
expect(result.stats.filesCount).toBe(1);
|
|
});
|
|
|
|
test("returns empty for empty string", () => {
|
|
const result = parseGitNumstat("");
|
|
expect(result.stats).toEqual({
|
|
filesCount: 0,
|
|
linesAdded: 0,
|
|
linesRemoved: 0,
|
|
});
|
|
expect(result.perFileStats.size).toBe(0);
|
|
});
|
|
|
|
test("skips lines with fewer than 3 tab-separated parts", () => {
|
|
const result = parseGitNumstat("invalid-line\n5\t3\tsrc/foo.ts");
|
|
expect(result.stats.filesCount).toBe(1);
|
|
});
|
|
|
|
test("handles zero additions and zero deletions", () => {
|
|
const result = parseGitNumstat("0\t0\tempty-change.ts");
|
|
expect(result.perFileStats.get("empty-change.ts")).toEqual({
|
|
added: 0,
|
|
removed: 0,
|
|
isBinary: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("parseGitDiff", () => {
|
|
test("parses single file with one hunk", () => {
|
|
const input = [
|
|
"diff --git a/foo.ts b/foo.ts",
|
|
"index abc..def 100644",
|
|
"--- a/foo.ts",
|
|
"+++ b/foo.ts",
|
|
"@@ -1,3 +1,4 @@",
|
|
" line1",
|
|
"+added",
|
|
" line2",
|
|
" line3",
|
|
].join("\n");
|
|
|
|
const result = parseGitDiff(input);
|
|
expect(result.size).toBe(1);
|
|
const hunks = result.get("foo.ts")!;
|
|
expect(hunks).toHaveLength(1);
|
|
expect(hunks[0].oldStart).toBe(1);
|
|
expect(hunks[0].oldLines).toBe(3);
|
|
expect(hunks[0].newStart).toBe(1);
|
|
expect(hunks[0].newLines).toBe(4);
|
|
expect(hunks[0].lines).toEqual([" line1", "+added", " line2", " line3"]);
|
|
});
|
|
|
|
test("parses multiple hunks in one file", () => {
|
|
const input = [
|
|
"diff --git a/bar.ts b/bar.ts",
|
|
"index abc..def 100644",
|
|
"--- a/bar.ts",
|
|
"+++ b/bar.ts",
|
|
"@@ -1,2 +1,3 @@",
|
|
" a",
|
|
"+b",
|
|
" c",
|
|
"@@ -10,2 +11,2 @@",
|
|
" d",
|
|
"-e",
|
|
"+f",
|
|
].join("\n");
|
|
|
|
const result = parseGitDiff(input);
|
|
const hunks = result.get("bar.ts")!;
|
|
expect(hunks).toHaveLength(2);
|
|
expect(hunks[0].oldStart).toBe(1);
|
|
expect(hunks[1].oldStart).toBe(10);
|
|
});
|
|
|
|
test("skips binary files marker", () => {
|
|
const input = [
|
|
"diff --git a/img.png b/img.png",
|
|
"Binary files a/img.png and b/img.png differ",
|
|
].join("\n");
|
|
|
|
const result = parseGitDiff(input);
|
|
// Binary file has no hunks, so it's not in the result
|
|
expect(result.size).toBe(0);
|
|
});
|
|
|
|
test("parses new file mode", () => {
|
|
const input = [
|
|
"diff --git a/new.ts b/new.ts",
|
|
"new file mode 100644",
|
|
"--- /dev/null",
|
|
"+++ b/new.ts",
|
|
"@@ -0,0 +1,2 @@",
|
|
"+line1",
|
|
"+line2",
|
|
].join("\n");
|
|
|
|
const result = parseGitDiff(input);
|
|
const hunks = result.get("new.ts")!;
|
|
expect(hunks).toHaveLength(1);
|
|
expect(hunks[0].lines).toEqual(["+line1", "+line2"]);
|
|
});
|
|
|
|
test("parses deleted file", () => {
|
|
const input = [
|
|
"diff --git a/old.ts b/old.ts",
|
|
"deleted file mode 100644",
|
|
"--- a/old.ts",
|
|
"+++ /dev/null",
|
|
"@@ -1,2 +0,0 @@",
|
|
"-line1",
|
|
"-line2",
|
|
].join("\n");
|
|
|
|
const result = parseGitDiff(input);
|
|
const hunks = result.get("old.ts")!;
|
|
expect(hunks).toHaveLength(1);
|
|
});
|
|
|
|
test("returns empty map for empty input", () => {
|
|
const result = parseGitDiff("");
|
|
expect(result.size).toBe(0);
|
|
});
|
|
|
|
test("handles multiple files", () => {
|
|
const input = [
|
|
"diff --git a/a.ts b/a.ts",
|
|
"--- a/a.ts",
|
|
"+++ b/a.ts",
|
|
"@@ -1 +1 @@",
|
|
"-old",
|
|
"+new",
|
|
"diff --git a/b.ts b/b.ts",
|
|
"--- a/b.ts",
|
|
"+++ b/b.ts",
|
|
"@@ -1 +1 @@",
|
|
"-x",
|
|
"+y",
|
|
].join("\n");
|
|
|
|
const result = parseGitDiff(input);
|
|
expect(result.size).toBe(2);
|
|
expect(result.has("a.ts")).toBe(true);
|
|
expect(result.has("b.ts")).toBe(true);
|
|
});
|
|
|
|
test("skips hunk without comma (single line)", () => {
|
|
const input = [
|
|
"diff --git a/solo.ts b/solo.ts",
|
|
"--- a/solo.ts",
|
|
"+++ b/solo.ts",
|
|
"@@ -1 +1 @@",
|
|
"-old",
|
|
"+new",
|
|
].join("\n");
|
|
|
|
const result = parseGitDiff(input);
|
|
const hunks = result.get("solo.ts")!;
|
|
expect(hunks[0].oldLines).toBe(1); // default when no comma
|
|
expect(hunks[0].newLines).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("parseShortstat", () => {
|
|
test("parses full shortstat with insertions and deletions", () => {
|
|
const result = parseShortstat(" 3 files changed, 10 insertions(+), 5 deletions(-)");
|
|
expect(result).toEqual({
|
|
filesCount: 3,
|
|
linesAdded: 10,
|
|
linesRemoved: 5,
|
|
});
|
|
});
|
|
|
|
test("parses single file", () => {
|
|
const result = parseShortstat(" 1 file changed, 2 insertions(+), 1 deletion(-)");
|
|
expect(result).toEqual({
|
|
filesCount: 1,
|
|
linesAdded: 2,
|
|
linesRemoved: 1,
|
|
});
|
|
});
|
|
|
|
test("parses insertions only", () => {
|
|
const result = parseShortstat(" 2 files changed, 5 insertions(+)");
|
|
expect(result).toEqual({
|
|
filesCount: 2,
|
|
linesAdded: 5,
|
|
linesRemoved: 0,
|
|
});
|
|
});
|
|
|
|
test("parses deletions only", () => {
|
|
const result = parseShortstat(" 1 file changed, 3 deletions(-)");
|
|
expect(result).toEqual({
|
|
filesCount: 1,
|
|
linesAdded: 0,
|
|
linesRemoved: 3,
|
|
});
|
|
});
|
|
|
|
test("parses files changed only (no insertions or deletions)", () => {
|
|
const result = parseShortstat(" 2 files changed");
|
|
expect(result).toEqual({
|
|
filesCount: 2,
|
|
linesAdded: 0,
|
|
linesRemoved: 0,
|
|
});
|
|
});
|
|
|
|
test("returns null for empty string", () => {
|
|
expect(parseShortstat("")).toBeNull();
|
|
});
|
|
|
|
test("returns null for non-matching string", () => {
|
|
expect(parseShortstat("nothing to see here")).toBeNull();
|
|
});
|
|
|
|
test("handles large numbers", () => {
|
|
const result = parseShortstat(" 100 files changed, 50000 insertions(+), 30000 deletions(-)");
|
|
expect(result).toEqual({
|
|
filesCount: 100,
|
|
linesAdded: 50000,
|
|
linesRemoved: 30000,
|
|
});
|
|
});
|
|
|
|
test("handles zero insertions and deletions explicitly", () => {
|
|
// git can output "0 insertions(+), 0 deletions(-)"
|
|
const result = parseShortstat(" 1 file changed, 0 insertions(+), 0 deletions(-)");
|
|
expect(result).toEqual({
|
|
filesCount: 1,
|
|
linesAdded: 0,
|
|
linesRemoved: 0,
|
|
});
|
|
});
|
|
});
|