fix(ui): render whole-file patches as complete diffs (#30516)
This commit is contained in:
@@ -18,6 +18,7 @@ describe("apply patch file", () => {
|
|||||||
|
|
||||||
expect(file).toBeDefined()
|
expect(file).toBeDefined()
|
||||||
expect(file?.view.fileDiff.name).toBe("a.ts")
|
expect(file?.view.fileDiff.name).toBe("a.ts")
|
||||||
|
expect(file?.view.fileDiff.isPartial).toBe(false)
|
||||||
expect(text(file!.view, "deletions")).toBe("one\ntwo\n")
|
expect(text(file!.view, "deletions")).toBe("one\ntwo\n")
|
||||||
expect(text(file!.view, "additions")).toBe("one\nthree\n")
|
expect(text(file!.view, "additions")).toBe("one\nthree\n")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import { normalize, resolveFileDiff, text } from "./session-diff"
|
import { normalize, resolveFileDiff, text } from "./session-diff"
|
||||||
|
|
||||||
describe("session diff", () => {
|
describe("session diff", () => {
|
||||||
test("keeps unified patch content", () => {
|
test("renders whole-file unified patches as complete diffs", () => {
|
||||||
const diff = {
|
const diff = {
|
||||||
file: "a.ts",
|
file: "a.ts",
|
||||||
patch:
|
patch:
|
||||||
@@ -14,7 +14,7 @@ describe("session diff", () => {
|
|||||||
const view = normalize(diff)
|
const view = normalize(diff)
|
||||||
|
|
||||||
expect(view.fileDiff.name).toBe("a.ts")
|
expect(view.fileDiff.name).toBe("a.ts")
|
||||||
expect(view.fileDiff.isPartial).toBe(true)
|
expect(view.fileDiff.isPartial).toBe(false)
|
||||||
expect(text(view, "deletions")).toBe("one\ntwo\n")
|
expect(text(view, "deletions")).toBe("one\ntwo\n")
|
||||||
expect(text(view, "additions")).toBe("one\nthree\n")
|
expect(text(view, "additions")).toBe("one\nthree\n")
|
||||||
})
|
})
|
||||||
@@ -34,6 +34,26 @@ describe("session diff", () => {
|
|||||||
expect(text(view, "additions")).toBe("one\nthree")
|
expect(text(view, "additions")).toBe("one\nthree")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("renders whole-file VCS patches as complete diffs", () => {
|
||||||
|
const fileDiff = resolveFileDiff({
|
||||||
|
file: "a.ts",
|
||||||
|
patch: "diff --git a/a.ts b/a.ts\nindex 1a2b3c4..5d6e7f8 100644\n--- a/a.ts\n+++ b/a.ts\n@@ -1,2 +1,2 @@\n one\n-old\n+new\n",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(fileDiff.isPartial).toBe(false)
|
||||||
|
expect(fileDiff.additionLines).toEqual(["one\n", "new\n"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps ordinary leading tool patches partial", () => {
|
||||||
|
const fileDiff = resolveFileDiff({
|
||||||
|
file: "a.ts",
|
||||||
|
patch: "Index: a.ts\n===================================================================\n--- a.ts\n+++ a.ts\n@@ -1,5 +1,5 @@\n-old\n+new\n two\n three\n four\n five\n",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(fileDiff.isPartial).toBe(true)
|
||||||
|
expect(fileDiff.additionLines).toEqual(["new\n", "two\n", "three\n", "four\n", "five\n"])
|
||||||
|
})
|
||||||
|
|
||||||
test("keeps separated patch hunks partial without complete file contents", () => {
|
test("keeps separated patch hunks partial without complete file contents", () => {
|
||||||
const fileDiff = resolveFileDiff({
|
const fileDiff = resolveFileDiff({
|
||||||
file: "project.ts",
|
file: "project.ts",
|
||||||
|
|||||||
@@ -60,13 +60,68 @@ function fileDiffFromPatch(file: string, patch: string) {
|
|||||||
return hit
|
return hit
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = patchInput(file, patch)
|
const contents = completePatchContents(patch)
|
||||||
const value = (input ? parsePatchFiles(input)[0]?.files[0] : undefined) ?? emptyFileDiff(file)
|
const input = contents ? undefined : patchInput(file, patch)
|
||||||
|
const value = contents
|
||||||
|
? fileDiffFromContent(file, contents.before, contents.after)
|
||||||
|
: (input ? parsePatchFiles(input)[0]?.files[0] : undefined) ?? emptyFileDiff(file)
|
||||||
patchFileDiffCache.set(key, value)
|
patchFileDiffCache.set(key, value)
|
||||||
while (patchFileDiffCache.size > diffCacheLimit) patchFileDiffCache.delete(patchFileDiffCache.keys().next().value!)
|
while (patchFileDiffCache.size > diffCacheLimit) patchFileDiffCache.delete(patchFileDiffCache.keys().next().value!)
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function completePatchContents(patch: string) {
|
||||||
|
try {
|
||||||
|
const parsed = parsePatch(patch)[0]
|
||||||
|
if (!parsed || (!parsed.index && !parsed.oldFileName && !parsed.newFileName)) return
|
||||||
|
// Snapshot and VCS producers request full context. Tool patches use jsdiff's shorter default context.
|
||||||
|
if (!patch.startsWith("diff --git ") && !/^--- [^\n]*\t\r?\n\+\+\+ [^\n]*\t(?:\r?\n|$)/m.test(patch)) return
|
||||||
|
// Full patches collapse into one leading hunk. Separated hunks omit ranges and must stay partial.
|
||||||
|
if (parsed.hunks.length !== 1) return
|
||||||
|
|
||||||
|
const hunk = parsed.hunks[0]
|
||||||
|
if (!hunk || hunk.oldStart > 1 || hunk.newStart > 1) return
|
||||||
|
|
||||||
|
const before: Array<{ text: string; newline: boolean }> = []
|
||||||
|
const after: Array<{ text: string; newline: boolean }> = []
|
||||||
|
let previous: "-" | "+" | " " | undefined
|
||||||
|
|
||||||
|
for (const line of hunk.lines) {
|
||||||
|
if (line.startsWith("\\")) {
|
||||||
|
if (previous === "-" || previous === " ") {
|
||||||
|
const value = before.at(-1)
|
||||||
|
if (value) value.newline = false
|
||||||
|
}
|
||||||
|
if (previous === "+" || previous === " ") {
|
||||||
|
const value = after.at(-1)
|
||||||
|
if (value) value.newline = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (line.startsWith("-")) {
|
||||||
|
before.push({ text: line.slice(1), newline: true })
|
||||||
|
previous = "-"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (line.startsWith("+")) {
|
||||||
|
after.push({ text: line.slice(1), newline: true })
|
||||||
|
previous = "+"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!line.startsWith(" ")) return
|
||||||
|
before.push({ text: line.slice(1), newline: true })
|
||||||
|
after.push({ text: line.slice(1), newline: true })
|
||||||
|
previous = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = (lines: Array<{ text: string; newline: boolean }>) =>
|
||||||
|
lines.map((line) => line.text + (line.newline ? "\n" : "")).join("")
|
||||||
|
return { before: text(before), after: text(after) }
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function patchInput(file: string, patch: string) {
|
function patchInput(file: string, patch: string) {
|
||||||
try {
|
try {
|
||||||
const parsed = parsePatch(patch)[0]
|
const parsed = parsePatch(patch)[0]
|
||||||
|
|||||||
Reference in New Issue
Block a user