Skip to content

Commit 93e35ee

Browse files
fix(git): render untracked file contents as diff additions
The body of an untracked file was inlined under a `--- /dev/null` header with no `+` prefixes, so it was not a hunk - it was the file's own syntax sitting where diff syntax was expected. A new markdown file's `- item` read back as a deleted line, front matter as a header, and the generated message described changes nobody made. Each content line is prefixed now. A trailing newline is not turned into a bare `+`, and the binary and unreadable markers stay unprefixed: they annotate the block rather than claiming to be its content. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 518a8fb commit 93e35ee

2 files changed

Lines changed: 46 additions & 3 deletions

File tree

‎src/utils/__tests__/git.spec.ts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,31 @@ describe("git utils", () => {
547547

548548
expect(context.files).toEqual([{ status: "untracked", path: "src/added.ts" }])
549549
expect(context.diff).toContain("+++ b/src/added.ts")
550-
expect(context.diff).toContain("export const answer = 42")
550+
// Prefixed, so the body reads as an addition rather than as pasted text.
551+
expect(context.diff).toContain("+export const answer = 42")
552+
// The file's trailing newline is not a line of its own.
553+
expect(context.diff.split("\n")).not.toContain("+")
554+
})
555+
556+
// An unprefixed body is not a diff, it is the file's own syntax: `- item` reads as a
557+
// deletion and `---` as a header, so the model reports changes that were never made.
558+
it("should render untracked contents as additions rather than diff syntax", async () => {
559+
mockProbes()
560+
mockGit(workingTree(`?? notes.md${NUL}`, ""))
561+
mockUntrackedFile(Buffer.from("---\ntitle: Notes\n---\n\n- first item\n+++ b/not-a-header\n"))
562+
563+
const lines = (await expectContext()).diff.split("\n")
564+
565+
expect(lines).toEqual([
566+
"--- /dev/null",
567+
"+++ b/notes.md",
568+
"+---",
569+
"+title: Notes",
570+
"+---",
571+
"+",
572+
"+- first item",
573+
"++++ b/not-a-header",
574+
])
551575
})
552576

553577
it("should mark binary untracked files instead of inlining them", async () => {

‎src/utils/git.ts‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,9 @@ async function readUntrackedFiles(cwd: string, files: GitFileChange[]): Promise<
588588
const blocks: string[] = []
589589

590590
for (const file of untracked.slice(0, UNTRACKED_FILE_LIMIT)) {
591-
blocks.push(`--- /dev/null\n+++ b/${file.path}\n${await readUntrackedFile(cwd, file.path)}`)
591+
const body = await readUntrackedFile(cwd, file.path)
592+
593+
blocks.push([`--- /dev/null`, `+++ b/${file.path}`, body].filter(Boolean).join("\n"))
592594
}
593595

594596
// The rest are still worth naming - that files were added is part of the change even when there
@@ -602,6 +604,23 @@ async function readUntrackedFiles(cwd: string, files: GitFileChange[]): Promise<
602604
return blocks.join("\n\n")
603605
}
604606

607+
/**
608+
* Marks each line as an addition, which is what makes the block a diff rather than pasted text.
609+
* Unprefixed, a new file's own content is read as diff syntax: a markdown bullet becomes a deleted
610+
* line, front matter becomes a header, and the message describes changes nobody made.
611+
*/
612+
function asAddedLines(text: string): string {
613+
const lines = text.split("\n")
614+
615+
// A file ending in a newline splits into a trailing empty segment. Prefixing it would add a
616+
// bare `+` for a line the file does not have.
617+
if (lines[lines.length - 1] === "") {
618+
lines.pop()
619+
}
620+
621+
return lines.map((line) => `+${line}`).join("\n")
622+
}
623+
605624
/** Reads at most `UNTRACKED_FILE_BYTE_LIMIT` bytes, so a huge file costs one bounded read. */
606625
async function readUntrackedFile(cwd: string, filePath: string): Promise<string> {
607626
let handle
@@ -618,7 +637,7 @@ async function readUntrackedFile(cwd: string, filePath: string): Promise<string>
618637
return "(binary file)"
619638
}
620639

621-
const text = contents.toString("utf8")
640+
const text = asAddedLines(contents.toString("utf8"))
622641

623642
return bytesRead < UNTRACKED_FILE_BYTE_LIMIT ? text : `${text}\n(truncated)`
624643
} catch {

0 commit comments

Comments
 (0)