diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..dfed0e1a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:5-24-trixie", + "features": { + "ghcr.io/shyim/devcontainers-features/bun:0": { "version": "latest" } + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/packages/core/src/patch.ts b/packages/core/src/patch.ts index a4370d44..7e561d3a 100644 --- a/packages/core/src/patch.ts +++ b/packages/core/src/patch.ts @@ -22,6 +22,54 @@ export interface FileUpdate { readonly bom: boolean } +const ADD_PREFIX = "*** Add File:" +const DELETE_PREFIX = "*** Delete File:" +const UPDATE_PREFIX = "*** Update File:" +const MOVE_PREFIX = "*** Move to:" + +interface ParsedHunk { + readonly hunk: Hunk + readonly next: number +} + +function directivePath(line: string, prefix: string, error: string): string { + const path = line.slice(prefix.length).trim() + if (!path) throw new Error(error) + return path +} + +function parseAddHunk(lines: ReadonlyArray, index: number): ParsedHunk { + const path = directivePath(lines[index]!, ADD_PREFIX, "Invalid add file path") + const parsed = parseAdd(lines, index + 1) + return { hunk: { type: "add", path, contents: parsed.content }, next: parsed.next } +} + +function parseDeleteHunk(lines: ReadonlyArray, index: number): ParsedHunk { + const path = directivePath(lines[index]!, DELETE_PREFIX, "Invalid delete file path") + return { hunk: { type: "delete", path }, next: index + 1 } +} + +function parseUpdateHunk(lines: ReadonlyArray, index: number): ParsedHunk { + const path = directivePath(lines[index]!, UPDATE_PREFIX, "Invalid update file path") + let next = index + 1 + let movePath: string | undefined + if (lines[next]?.startsWith(MOVE_PREFIX)) { + movePath = directivePath(lines[next]!, MOVE_PREFIX, "Invalid move file path") + next++ + } + const parsed = parseUpdate(lines, next) + if (parsed.chunks.length === 0) throw new Error(`Invalid update hunk for ${path}: expected at least one @@ chunk`) + return { hunk: { type: "update", path, movePath, chunks: parsed.chunks }, next: parsed.next } +} + +function parseHunk(lines: ReadonlyArray, index: number): ParsedHunk { + const line = lines[index]! + if (line.startsWith(ADD_PREFIX)) return parseAddHunk(lines, index) + if (line.startsWith(DELETE_PREFIX)) return parseDeleteHunk(lines, index) + if (line.startsWith(UPDATE_PREFIX)) return parseUpdateHunk(lines, index) + throw new Error(`Invalid patch line: ${line}`) +} + export function parse(patchText: string): ReadonlyArray { const lines = stripHeredoc(patchText.trim()).split("\n") const begin = lines.findIndex((line) => line.trim() === "*** Begin Patch") @@ -31,39 +79,9 @@ export function parse(patchText: string): ReadonlyArray { const hunks: Hunk[] = [] let index = begin + 1 while (index < end) { - const line = lines[index]! - if (line.startsWith("*** Add File:")) { - const path = line.slice("*** Add File:".length).trim() - if (!path) throw new Error("Invalid add file path") - const parsed = parseAdd(lines, index + 1) - hunks.push({ type: "add", path, contents: parsed.content }) - index = parsed.next - continue - } - if (line.startsWith("*** Delete File:")) { - const path = line.slice("*** Delete File:".length).trim() - if (!path) throw new Error("Invalid delete file path") - hunks.push({ type: "delete", path }) - index++ - continue - } - if (line.startsWith("*** Update File:")) { - const path = line.slice("*** Update File:".length).trim() - if (!path) throw new Error("Invalid update file path") - let next = index + 1 - let movePath: string | undefined - if (lines[next]?.startsWith("*** Move to:")) { - movePath = lines[next]!.slice("*** Move to:".length).trim() - if (!movePath) throw new Error("Invalid move file path") - next++ - } - const parsed = parseUpdate(lines, next) - if (parsed.chunks.length === 0) throw new Error(`Invalid update hunk for ${path}: expected at least one @@ chunk`) - hunks.push({ type: "update", path, movePath, chunks: parsed.chunks }) - index = parsed.next - continue - } - throw new Error(`Invalid patch line: ${line}`) + const parsed = parseHunk(lines, index) + hunks.push(parsed.hunk) + index = parsed.next } return hunks }