import { diffFileName, type DiffFile, type NiceDiff, type TextFragment } from "./diff"; import { hexFromBytes } from "./repo"; // port of appview/repo/rawdiff.go const OP_CHARS = [" ", "-", "+"] as const; const opChar = (op: number): string => OP_CHARS[op] ?? "?"; const noEol = (line: string): boolean => line.length === 0 || !line.endsWith("\n"); const fragmentHeader = (fragment: TextFragment): string => { const header = `@@ -${fragment.OldPosition ?? 0},${fragment.OldLines ?? 0} +${fragment.NewPosition ?? 0},${fragment.NewLines ?? 0} @@`; return fragment.Comment ? `${header} ${fragment.Comment}` : header; }; const fragmentString = (fragment: TextFragment): string => { let out = `${fragmentHeader(fragment)}\n`; for (const line of fragment.Lines ?? []) { out += opChar(line.Op) + line.Line; if (noEol(line.Line)) out += "\n\\ No newline at end of file\n"; } return out; }; export const fileStats = (file: DiffFile): { insertions: number; deletions: number } => { let insertions = 0; let deletions = 0; for (const fragment of file.text_fragments ?? []) { insertions += fragment.LinesAdded ?? 0; deletions += fragment.LinesDeleted ?? 0; } return { insertions, deletions }; }; export const renderUnifiedDiff = (niceDiff: NiceDiff | null | undefined): string => { if (!niceDiff) return ""; let out = ""; for (const file of niceDiff.diff ?? []) { const oldName = file.name.old || file.name.new; const newName = file.name.new || file.name.old; out += `diff --git a/${oldName} b/${newName}\n`; if (file.is_new) { out += "new file mode 100644\n"; out += "--- /dev/null\n"; out += `+++ b/${newName}\n`; } else if (file.is_delete) { out += "deleted file mode 100644\n"; out += `--- a/${oldName}\n`; out += "+++ /dev/null\n"; } else if (file.is_rename) { out += `rename from ${oldName}\n`; out += `rename to ${newName}\n`; out += `--- a/${oldName}\n`; out += `+++ b/${newName}\n`; } else { out += `--- a/${oldName}\n`; out += `+++ b/${newName}\n`; } for (const fragment of file.text_fragments ?? []) { out += fragmentString(fragment); } } return out; }; const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const; const MONTHS = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] as const; const pad = (value: number): string => String(value).padStart(2, "0"); // RFC1123Z, always utc const formatPatchDate = (input: string): string => { const date = new Date(input); // a missing When is go's zero time, formats to this exact string if (Number.isNaN(date.getTime())) return "Mon, 01 Jan 0001 00:00:00 +0000"; return ( `${WEEKDAYS[date.getUTCDay()]}, ${pad(date.getUTCDate())} ${MONTHS[date.getUTCMonth()]} ` + `${String(date.getUTCFullYear()).padStart(4, "0")} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} +0000` ); }; export const renderFormatPatch = (niceDiff: NiceDiff | null | undefined): string => { if (!niceDiff) return ""; const commit = niceDiff.commit ?? {}; const message = commit.message ?? ""; // format-patch takes the first LINE as the subject, unlike splitMessage's // first paragraph const newline = message.indexOf("\n"); const subject = newline === -1 ? message : message.slice(0, newline); const separator = message.indexOf("\n\n"); const body = separator === -1 ? "" : message.slice(separator + 2).replace(/\n+$/, ""); const hash = commit.this || hexFromBytes(commit.hash ?? []); let out = `From ${hash} Mon Sep 17 00:00:00 2001\n`; out += `From: ${commit.author?.Name ?? ""} <${commit.author?.Email ?? ""}>\n`; out += `Date: ${formatPatchDate(commit.author?.When ?? "")}\n`; out += `Subject: [PATCH] ${subject}\n`; out += "\n"; if (body !== "") out += `${body}\n`; out += "---\n"; for (const file of niceDiff.diff ?? []) { const name = diffFileName(file); const { insertions, deletions } = fileStats(file); out += ` ${name} | ${insertions + deletions} ${"+".repeat(insertions)}${"-".repeat(deletions)}\n`; } const stat = niceDiff.stat ?? { insertions: 0, deletions: 0, files_changed: 0 }; out += ` ${stat.files_changed} file(s) changed, ${stat.insertions} insertion(s)(+), ${stat.deletions} deletion(s)(-)\n\n`; out += renderUnifiedDiff(niceDiff); out += "\n--\ntangled.sh\n"; return out; };