Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clean-hunk-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Fix malformed `@@` hunk headers so each side's line range and count are emitted correctly.
77 changes: 77 additions & 0 deletions src/core/hunkHeader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, test } from "bun:test";
import type { Hunk } from "@pierre/diffs";
import { formatHunkHeader } from "./hunkHeader";

/**
* Minimal synthesized Hunk that forces the fallback branch of
* `formatHunkHeader` (no parsed `hunkSpecs`). Only the numeric header fields
* the fallback reads are populated; the rest are stubbed to satisfy the type.
*/
function fallbackHunk(overrides: Partial<Hunk> = {}): Hunk {
return {
collapsedBefore: 0,
additionStart: 0,
additionCount: 0,
additionLines: 0,
additionLineIndex: 0,
deletionStart: 0,
deletionCount: 0,
deletionLines: 0,
deletionLineIndex: 0,
hunkContent: [],
splitLineStart: 0,
splitLineCount: 0,
unifiedLineStart: 0,
unifiedLineCount: 0,
...overrides,
} as unknown as Hunk;
}

describe("formatHunkHeader", () => {
test("fallback uses the per-side line count (context + changes) for the @@ ranges", () => {
// A context-bearing hunk where git renders @@ -10,4 +10,4 @@:
// old side = lines 10, 11 (context) + 12 (removed) + 13 (context) = 4
// new side = lines 10, 11 (context) + 13 (context) + 14 (added) = 4
// `*Count` is the header count (context + changes); `*Lines` is only the
// changed `+`/`-` lines (1 each here), so they diverge — exactly the shape
// that used to emit a malformed `@@ -10,1 +10,1 @@`.
const hunk = fallbackHunk({
deletionStart: 10,
deletionCount: 4,
deletionLines: 1,
additionStart: 10,
additionCount: 4,
additionLines: 1,
});

const header = formatHunkHeader(hunk);

expect(header).toBe("@@ -10,4 +10,4 @@");
expect(header).toMatch(/^@@ -\d+,\d+ \+\d+,\d+ @@$/);
});

test("fallback renders a new-file (pure-addition) hunk with a 0,0 old range", () => {
const hunk = fallbackHunk({
deletionStart: 0,
deletionCount: 0,
deletionLines: 0,
additionStart: 1,
additionCount: 3,
additionLines: 3,
});

expect(formatHunkHeader(hunk)).toBe("@@ -0,0 +1,3 @@");
});

test("fallback appends hunkContext after the @@ markers when present", () => {
const hunk = fallbackHunk({
deletionStart: 1,
deletionCount: 1,
additionStart: 1,
additionCount: 1,
hunkContext: "function name()",
});

expect(formatHunkHeader(hunk)).toBe("@@ -1,1 +1,1 @@ function name()");
});
});
5 changes: 4 additions & 1 deletion src/core/hunkHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type { Hunk } from "@pierre/diffs";
export function formatHunkHeader(hunk: Hunk) {
const specs =
hunk.hunkSpecs ??
`@@ -${hunk.deletionStart},${hunk.deletionLines} +${hunk.additionStart},${hunk.additionLines} @@`;
// The header count is the per-side line total (context + changes), i.e.
// `*Count` parsed from `-X,count` / `+X,count` — not `*Lines`, which is
// only the changed `+`/`-` lines and would undercount a context-bearing hunk.
`@@ -${hunk.deletionStart},${hunk.deletionCount} +${hunk.additionStart},${hunk.additionCount} @@`;
return hunk.hunkContext ? `${specs} ${hunk.hunkContext}` : specs;
}
Loading