| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | */ |
| 7 | |
| 8 | import type {Hunk, ParsedDiff} from './types'; |
| 9 | import {DiffType} from './types'; |
| 10 | |
| 11 | /** |
| 12 | * Convert a parsed diff back to git diff format string. |
| 13 | * |
| 14 | * This is the reverse operation of parsePatch in parse.ts. |
| 15 | */ |
| 16 | export function stringifyPatch(parsedDiffs: ParsedDiff[]): string { |
| 17 | return parsedDiffs.map(stringifyDiff).join(''); |
| 18 | } |
| 19 | |
| 20 | function stringifyDiff(diff: ParsedDiff): string { |
| 21 | const parts: string[] = []; |
| 22 | |
| 23 | // diff header |
| 24 | if (diff.oldFileName && diff.newFileName) { |
| 25 | parts.push(`diff --git ${diff.oldFileName} ${diff.newFileName}\n`); |
| 26 | } |
| 27 | |
| 28 | // extended header lines |
| 29 | if (diff.type === DiffType.Renamed) { |
| 30 | const oldName = diff.oldFileName?.replace(/^a\//, '') ?? ''; |
| 31 | const newName = diff.newFileName?.replace(/^b\//, '') ?? ''; |
| 32 | parts.push(`rename from ${oldName}\n`); |
| 33 | parts.push(`rename to ${newName}\n`); |
| 34 | } |
| 35 | |
| 36 | if (diff.type === DiffType.Copied) { |
| 37 | const oldName = diff.oldFileName?.replace(/^a\//, '') ?? ''; |
| 38 | const newName = diff.newFileName?.replace(/^b\//, '') ?? ''; |
| 39 | parts.push(`copy from ${oldName}\n`); |
| 40 | parts.push(`copy to ${newName}\n`); |
| 41 | } |
| 42 | |
| 43 | if (diff.oldMode && diff.newMode && diff.oldMode !== diff.newMode) { |
| 44 | parts.push(`old mode ${diff.oldMode}\n`); |
| 45 | parts.push(`new mode ${diff.newMode}\n`); |
| 46 | } |
| 47 | |
| 48 | if (diff.type === DiffType.Added && diff.newMode) { |
| 49 | parts.push(`new file mode ${diff.newMode}\n`); |
| 50 | } |
| 51 | |
| 52 | if (diff.type === DiffType.Removed && diff.newMode) { |
| 53 | parts.push(`deleted file mode ${diff.newMode}\n`); |
| 54 | } |
| 55 | |
| 56 | // file headers |
| 57 | if (diff.hunks.length > 0) { |
| 58 | const oldFile = diff.type === DiffType.Added ? '/dev/null' : (diff.oldFileName ?? '/dev/null'); |
| 59 | const newFile = |
| 60 | diff.type === DiffType.Removed ? '/dev/null' : (diff.newFileName ?? '/dev/null'); |
| 61 | parts.push(`--- ${oldFile}\n`); |
| 62 | parts.push(`+++ ${newFile}\n`); |
| 63 | } |
| 64 | |
| 65 | // hunks |
| 66 | diff.hunks.forEach(hunk => { |
| 67 | parts.push(stringifyHunk(hunk)); |
| 68 | }); |
| 69 | |
| 70 | return parts.join(''); |
| 71 | } |
| 72 | |
| 73 | function stringifyHunk(hunk: Hunk): string { |
| 74 | const parts: string[] = []; |
| 75 | |
| 76 | // Handle the Unified Diff Format quirk: |
| 77 | // If the hunk size is 0, the start line is one higher than stored |
| 78 | let oldStart = hunk.oldStart; |
| 79 | let newStart = hunk.newStart; |
| 80 | |
| 81 | if (hunk.oldLines === 0) { |
| 82 | oldStart -= 1; |
| 83 | } |
| 84 | if (hunk.newLines === 0) { |
| 85 | newStart -= 1; |
| 86 | } |
| 87 | |
| 88 | // hunk header - always include line count |
| 89 | const oldRange = `${oldStart},${hunk.oldLines}`; |
| 90 | const newRange = `${newStart},${hunk.newLines}`; |
| 91 | parts.push(`@@ -${oldRange} +${newRange} @@\n`); |
| 92 | |
| 93 | // hunk lines |
| 94 | hunk.lines.forEach((line, index) => { |
| 95 | const delimiter = hunk.linedelimiters[index] ?? '\n'; |
| 96 | parts.push(line + delimiter); |
| 97 | }); |
| 98 | |
| 99 | return parts.join(''); |
| 100 | } |
| 101 | |