This repository has no description
1import { diffFileName, type DiffFile, type NiceDiff, type TextFragment } from "./diff";
2import { hexFromBytes } from "./repo";
3
4// port of appview/repo/rawdiff.go
5
6const OP_CHARS = [" ", "-", "+"] as const;
7
8const opChar = (op: number): string => OP_CHARS[op] ?? "?";
9
10const noEol = (line: string): boolean => line.length === 0 || !line.endsWith("\n");
11
12const fragmentHeader = (fragment: TextFragment): string => {
13 const header = `@@ -${fragment.OldPosition ?? 0},${fragment.OldLines ?? 0} +${fragment.NewPosition ?? 0},${fragment.NewLines ?? 0} @@`;
14 return fragment.Comment ? `${header} ${fragment.Comment}` : header;
15};
16
17const fragmentString = (fragment: TextFragment): string => {
18 let out = `${fragmentHeader(fragment)}\n`;
19 for (const line of fragment.Lines ?? []) {
20 out += opChar(line.Op) + line.Line;
21 if (noEol(line.Line)) out += "\n\\ No newline at end of file\n";
22 }
23 return out;
24};
25
26export const fileStats = (file: DiffFile): { insertions: number; deletions: number } => {
27 let insertions = 0;
28 let deletions = 0;
29 for (const fragment of file.text_fragments ?? []) {
30 insertions += fragment.LinesAdded ?? 0;
31 deletions += fragment.LinesDeleted ?? 0;
32 }
33 return { insertions, deletions };
34};
35
36export const renderUnifiedDiff = (niceDiff: NiceDiff | null | undefined): string => {
37 if (!niceDiff) return "";
38 let out = "";
39 for (const file of niceDiff.diff ?? []) {
40 const oldName = file.name.old || file.name.new;
41 const newName = file.name.new || file.name.old;
42
43 out += `diff --git a/${oldName} b/${newName}\n`;
44 if (file.is_new) {
45 out += "new file mode 100644\n";
46 out += "--- /dev/null\n";
47 out += `+++ b/${newName}\n`;
48 } else if (file.is_delete) {
49 out += "deleted file mode 100644\n";
50 out += `--- a/${oldName}\n`;
51 out += "+++ /dev/null\n";
52 } else if (file.is_rename) {
53 out += `rename from ${oldName}\n`;
54 out += `rename to ${newName}\n`;
55 out += `--- a/${oldName}\n`;
56 out += `+++ b/${newName}\n`;
57 } else {
58 out += `--- a/${oldName}\n`;
59 out += `+++ b/${newName}\n`;
60 }
61
62 for (const fragment of file.text_fragments ?? []) {
63 out += fragmentString(fragment);
64 }
65 }
66 return out;
67};
68
69const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
70const MONTHS = [
71 "Jan",
72 "Feb",
73 "Mar",
74 "Apr",
75 "May",
76 "Jun",
77 "Jul",
78 "Aug",
79 "Sep",
80 "Oct",
81 "Nov",
82 "Dec"
83] as const;
84
85const pad = (value: number): string => String(value).padStart(2, "0");
86
87// RFC1123Z, always utc
88const formatPatchDate = (input: string): string => {
89 const date = new Date(input);
90// a missing When is go's zero time, formats to this exact string
91 if (Number.isNaN(date.getTime())) return "Mon, 01 Jan 0001 00:00:00 +0000";
92 return (
93 `${WEEKDAYS[date.getUTCDay()]}, ${pad(date.getUTCDate())} ${MONTHS[date.getUTCMonth()]} ` +
94 `${String(date.getUTCFullYear()).padStart(4, "0")} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} +0000`
95 );
96};
97
98export const renderFormatPatch = (niceDiff: NiceDiff | null | undefined): string => {
99 if (!niceDiff) return "";
100 const commit = niceDiff.commit ?? {};
101 const message = commit.message ?? "";
102
103 // format-patch takes the first LINE as the subject, unlike splitMessage's
104 // first paragraph
105 const newline = message.indexOf("\n");
106 const subject = newline === -1 ? message : message.slice(0, newline);
107
108 const separator = message.indexOf("\n\n");
109 const body = separator === -1 ? "" : message.slice(separator + 2).replace(/\n+$/, "");
110
111 const hash = commit.this || hexFromBytes(commit.hash ?? []);
112
113 let out = `From ${hash} Mon Sep 17 00:00:00 2001\n`;
114 out += `From: ${commit.author?.Name ?? ""} <${commit.author?.Email ?? ""}>\n`;
115 out += `Date: ${formatPatchDate(commit.author?.When ?? "")}\n`;
116 out += `Subject: [PATCH] ${subject}\n`;
117 out += "\n";
118 if (body !== "") out += `${body}\n`;
119 out += "---\n";
120
121 for (const file of niceDiff.diff ?? []) {
122 const name = diffFileName(file);
123 const { insertions, deletions } = fileStats(file);
124 out += ` ${name} | ${insertions + deletions} ${"+".repeat(insertions)}${"-".repeat(deletions)}\n`;
125 }
126 const stat = niceDiff.stat ?? { insertions: 0, deletions: 0, files_changed: 0 };
127 out += ` ${stat.files_changed} file(s) changed, ${stat.insertions} insertion(s)(+), ${stat.deletions} deletion(s)(-)\n\n`;
128
129 out += renderUnifiedDiff(niceDiff);
130 out += "\n--\ntangled.sh\n";
131 return out;
132};