session-diff.ts 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. import { parseDiffFromFile, type FileDiffMetadata } from "@pierre/diffs"
  2. import { formatPatch, parsePatch, structuredPatch } from "diff"
  3. import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
  4. type LegacyDiff = {
  5. file: string
  6. patch?: string
  7. before?: string
  8. after?: string
  9. additions: number
  10. deletions: number
  11. status?: "added" | "deleted" | "modified"
  12. }
  13. type ReviewDiff = SnapshotFileDiff | VcsFileDiff | LegacyDiff
  14. export type ViewDiff = {
  15. file: string
  16. patch: string
  17. additions: number
  18. deletions: number
  19. status?: "added" | "deleted" | "modified"
  20. fileDiff: FileDiffMetadata
  21. }
  22. const cache = new Map<string, FileDiffMetadata>()
  23. function patch(diff: ReviewDiff) {
  24. if (typeof diff.patch === "string") {
  25. const [patch] = parsePatch(diff.patch)
  26. const beforeLines = []
  27. const afterLines = []
  28. for (const hunk of patch.hunks) {
  29. for (const line of hunk.lines) {
  30. if (line.startsWith("-")) {
  31. beforeLines.push(line.slice(1))
  32. } else if (line.startsWith("+")) {
  33. afterLines.push(line.slice(1))
  34. } else {
  35. // context line (starts with ' ')
  36. beforeLines.push(line.slice(1))
  37. afterLines.push(line.slice(1))
  38. }
  39. }
  40. }
  41. return { before: beforeLines.join("\n"), after: afterLines.join("\n"), patch: diff.patch }
  42. }
  43. return {
  44. before: "before" in diff && typeof diff.before === "string" ? diff.before : "",
  45. after: "after" in diff && typeof diff.after === "string" ? diff.after : "",
  46. patch: formatPatch(
  47. structuredPatch(
  48. diff.file,
  49. diff.file,
  50. "before" in diff && typeof diff.before === "string" ? diff.before : "",
  51. "after" in diff && typeof diff.after === "string" ? diff.after : "",
  52. "",
  53. "",
  54. { context: Number.MAX_SAFE_INTEGER },
  55. ),
  56. ),
  57. }
  58. }
  59. function file(file: string, patch: string, before: string, after: string) {
  60. const hit = cache.get(patch)
  61. if (hit) return hit
  62. const value = parseDiffFromFile({ name: file, contents: before }, { name: file, contents: after })
  63. cache.set(patch, value)
  64. return value
  65. }
  66. export function normalize(diff: ReviewDiff): ViewDiff {
  67. const next = patch(diff)
  68. return {
  69. file: diff.file,
  70. patch: next.patch,
  71. additions: diff.additions,
  72. deletions: diff.deletions,
  73. status: diff.status,
  74. fileDiff: file(diff.file, next.patch, next.before, next.after),
  75. }
  76. }
  77. export function text(diff: ViewDiff, side: "deletions" | "additions") {
  78. if (side === "deletions") return diff.fileDiff.deletionLines.join("")
  79. return diff.fileDiff.additionLines.join("")
  80. }