@diegopetrucci/pi-extensions 0.1.42 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -0
- package/extensions/annotate-git-diff/README.md +43 -0
- package/extensions/annotate-git-diff/clipboard.ts +143 -0
- package/extensions/annotate-git-diff/git.ts +943 -0
- package/extensions/annotate-git-diff/glimpseui.d.ts +89 -0
- package/extensions/annotate-git-diff/index.ts +412 -0
- package/extensions/annotate-git-diff/package.json +50 -0
- package/extensions/annotate-git-diff/prompt.ts +65 -0
- package/extensions/annotate-git-diff/quiet-glimpse.ts +156 -0
- package/extensions/annotate-git-diff/types.ts +202 -0
- package/extensions/annotate-git-diff/ui.ts +71 -0
- package/extensions/annotate-git-diff/watch.ts +104 -0
- package/extensions/annotate-git-diff/web/app.js +2381 -0
- package/extensions/annotate-git-diff/web/index.html +913 -0
- package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -0
- package/extensions/annotate-last-message/README.md +40 -0
- package/extensions/annotate-last-message/glimpseui.d.ts +89 -0
- package/extensions/annotate-last-message/index.ts +165 -0
- package/extensions/annotate-last-message/package.json +45 -0
- package/extensions/annotate-last-message/prompt.ts +93 -0
- package/extensions/annotate-last-message/quiet-glimpse.ts +156 -0
- package/extensions/annotate-last-message/session.ts +112 -0
- package/extensions/annotate-last-message/types.ts +46 -0
- package/extensions/annotate-last-message/ui.ts +23 -0
- package/extensions/annotate-last-message/web/app.js +229 -0
- package/extensions/annotate-last-message/web/index.html +322 -0
- package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -0
- package/extensions/illustrations-to-explain-things/README.md +52 -0
- package/extensions/illustrations-to-explain-things/package.json +31 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/SKILL.md +112 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/agents/openai.yaml +6 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/01-two-breakpoints.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/02-minimum-loop.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/03-sort-by-purpose.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/04-one-fish-many-uses.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/05-handoff-path.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/06-three-sources.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/07-three-content-jobs.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/08-handoff-copy-toolbox.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/09-common-pits-no-title.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/10-information-well.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/11-idea-press.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/12-content-fermentation.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/13-system-bearing.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/assets/examples/14-trust-bridge.png +0 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/references/composition-patterns.md +91 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/references/prompt-template.md +51 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/references/qa-checklist.md +48 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/references/style-dna.md +49 -0
- package/extensions/illustrations-to-explain-things/skills/illustrations-to-explain-things/references/xiaohei-ip.md +53 -0
- package/package.json +18 -8
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
import { lstatSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { extname, relative, resolve } from "node:path";
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type {
|
|
6
|
+
ChangeStatus,
|
|
7
|
+
ReviewCommitInfo,
|
|
8
|
+
ReviewFile,
|
|
9
|
+
ReviewFileComparison,
|
|
10
|
+
ReviewFileContents,
|
|
11
|
+
ReviewFileKind,
|
|
12
|
+
ReviewScope,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
|
|
15
|
+
interface ChangedPath {
|
|
16
|
+
status: ChangeStatus;
|
|
17
|
+
oldPath: string | null;
|
|
18
|
+
newPath: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ReviewBaseInfo {
|
|
22
|
+
mergeBase: string;
|
|
23
|
+
baseRef: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface WorkingTreeStatusInfo {
|
|
27
|
+
hasChanges: boolean;
|
|
28
|
+
hasReviewableChanges: boolean;
|
|
29
|
+
hasUntracked: boolean;
|
|
30
|
+
hasTrackedDeletions: boolean;
|
|
31
|
+
hasRenames: boolean;
|
|
32
|
+
untrackedPaths: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const WORKING_TREE_COMMIT_SHA = "__tlh_working_tree__";
|
|
36
|
+
const WORKING_TREE_COMMIT_SHORT_SHA = "WT";
|
|
37
|
+
const WORKING_TREE_COMMIT_SUBJECT = "Uncommitted changes";
|
|
38
|
+
|
|
39
|
+
export function isWorkingTreeCommitSha(sha: string): boolean {
|
|
40
|
+
return sha === WORKING_TREE_COMMIT_SHA;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createWorkingTreeCommitInfo(): ReviewCommitInfo {
|
|
44
|
+
return {
|
|
45
|
+
sha: WORKING_TREE_COMMIT_SHA,
|
|
46
|
+
shortSha: WORKING_TREE_COMMIT_SHORT_SHA,
|
|
47
|
+
subject: WORKING_TREE_COMMIT_SUBJECT,
|
|
48
|
+
authorName: "",
|
|
49
|
+
authorDate: "",
|
|
50
|
+
kind: "working-tree",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function runGitAllowFailure(pi: ExtensionAPI, repoRoot: string, args: string[]): Promise<string> {
|
|
55
|
+
const result = await pi.exec("git", args, { cwd: repoRoot });
|
|
56
|
+
if (result.code !== 0) {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
return result.stdout;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function runBashAllowFailure(pi: ExtensionAPI, repoRoot: string, script: string): Promise<string> {
|
|
63
|
+
const result = await pi.exec("bash", ["-lc", script], { cwd: repoRoot });
|
|
64
|
+
if (result.code !== 0) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
return result.stdout;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function getRepoRoot(pi: ExtensionAPI, cwd: string): Promise<string> {
|
|
71
|
+
const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
72
|
+
if (result.code !== 0) {
|
|
73
|
+
throw new Error("Not inside a git repository.");
|
|
74
|
+
}
|
|
75
|
+
return result.stdout.trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function hasHead(pi: ExtensionAPI, repoRoot: string): Promise<boolean> {
|
|
79
|
+
const result = await pi.exec("git", ["rev-parse", "--verify", "HEAD"], { cwd: repoRoot });
|
|
80
|
+
return result.code === 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function currentBranch(pi: ExtensionAPI, repoRoot: string): Promise<string> {
|
|
84
|
+
const result = await pi.exec("git", ["branch", "--show-current"], { cwd: repoRoot });
|
|
85
|
+
return result.code === 0 ? result.stdout.trim() || "HEAD" : "HEAD";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function getUpstreamRef(pi: ExtensionAPI, repoRoot: string): Promise<string | null> {
|
|
89
|
+
const output = await runGitAllowFailure(pi, repoRoot, [
|
|
90
|
+
"rev-parse",
|
|
91
|
+
"--abbrev-ref",
|
|
92
|
+
"--symbolic-full-name",
|
|
93
|
+
"@{upstream}",
|
|
94
|
+
]);
|
|
95
|
+
const value = output.trim();
|
|
96
|
+
return value.length > 0 ? value : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getOriginHeadRef(pi: ExtensionAPI, repoRoot: string): Promise<string | null> {
|
|
100
|
+
const output = await runGitAllowFailure(pi, repoRoot, ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
|
|
101
|
+
const value = output.trim();
|
|
102
|
+
return value.length > 0 ? value : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isSameBranchRef(ref: string, branch: string): boolean {
|
|
106
|
+
if (!branch || branch === "HEAD") return false;
|
|
107
|
+
return ref === branch || ref.endsWith(`/${branch}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function findReviewBase(pi: ExtensionAPI, repoRoot: string): Promise<ReviewBaseInfo | null> {
|
|
111
|
+
const branch = await currentBranch(pi, repoRoot);
|
|
112
|
+
const candidates: string[] = [];
|
|
113
|
+
const upstreamRef = await getUpstreamRef(pi, repoRoot);
|
|
114
|
+
if (upstreamRef && !isSameBranchRef(upstreamRef, branch)) {
|
|
115
|
+
candidates.push(upstreamRef);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const originHeadRef = await getOriginHeadRef(pi, repoRoot);
|
|
119
|
+
if (originHeadRef) {
|
|
120
|
+
candidates.push(originHeadRef);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
candidates.push("origin/main", "origin/master", "origin/develop", "main", "master", "develop");
|
|
124
|
+
|
|
125
|
+
const seen = new Set<string>();
|
|
126
|
+
for (const candidate of candidates) {
|
|
127
|
+
if (!candidate || seen.has(candidate)) continue;
|
|
128
|
+
seen.add(candidate);
|
|
129
|
+
const mergeBase = (await runGitAllowFailure(pi, repoRoot, ["merge-base", "HEAD", candidate])).trim();
|
|
130
|
+
if (mergeBase.length > 0) {
|
|
131
|
+
return { mergeBase, baseRef: candidate };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseNameStatusLine(parts: string[]): ChangedPath | null {
|
|
139
|
+
const code = (parts[0] ?? "")[0];
|
|
140
|
+
|
|
141
|
+
if (code === "R") {
|
|
142
|
+
const oldPath = parts[1] ?? null;
|
|
143
|
+
const newPath = parts[2] ?? null;
|
|
144
|
+
if (oldPath == null || newPath == null) return null;
|
|
145
|
+
return { status: "renamed", oldPath, newPath };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const path = parts[1] ?? null;
|
|
149
|
+
if (path == null) return null;
|
|
150
|
+
|
|
151
|
+
if (code === "M") return { status: "modified", oldPath: path, newPath: path };
|
|
152
|
+
if (code === "A") return { status: "added", oldPath: null, newPath: path };
|
|
153
|
+
if (code === "D") return { status: "deleted", oldPath: path, newPath: null };
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseNameStatus(output: string): ChangedPath[] {
|
|
158
|
+
const lines = output
|
|
159
|
+
.split(/\r?\n/)
|
|
160
|
+
.map((line) => line.trim())
|
|
161
|
+
.filter((line) => line.length > 0);
|
|
162
|
+
|
|
163
|
+
const changes: ChangedPath[] = [];
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
const change = parseNameStatusLine(line.split("\t"));
|
|
166
|
+
if (change != null) changes.push(change);
|
|
167
|
+
}
|
|
168
|
+
return changes;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseStatusPorcelainZ(output: string): WorkingTreeStatusInfo {
|
|
172
|
+
const info: WorkingTreeStatusInfo = {
|
|
173
|
+
hasChanges: false,
|
|
174
|
+
hasReviewableChanges: false,
|
|
175
|
+
hasUntracked: false,
|
|
176
|
+
hasTrackedDeletions: false,
|
|
177
|
+
hasRenames: false,
|
|
178
|
+
untrackedPaths: [],
|
|
179
|
+
};
|
|
180
|
+
const tokens = output.split("\0");
|
|
181
|
+
|
|
182
|
+
for (let index = 0; index < tokens.length; ) {
|
|
183
|
+
const token = tokens[index] ?? "";
|
|
184
|
+
if (token.length === 0) {
|
|
185
|
+
index += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const code = token.slice(0, 2);
|
|
190
|
+
const path = token.slice(3);
|
|
191
|
+
const isRenameOrCopy = code.includes("R") || code.includes("C");
|
|
192
|
+
const isReviewablePath = code !== "!!" && path.length > 0 && isIncludedReviewPath(path);
|
|
193
|
+
if (code !== "!!") {
|
|
194
|
+
info.hasChanges = true;
|
|
195
|
+
}
|
|
196
|
+
if (isReviewablePath) {
|
|
197
|
+
info.hasReviewableChanges = true;
|
|
198
|
+
}
|
|
199
|
+
if (code === "??") {
|
|
200
|
+
if (isReviewablePath) {
|
|
201
|
+
info.hasUntracked = true;
|
|
202
|
+
info.untrackedPaths.push(path);
|
|
203
|
+
}
|
|
204
|
+
} else if (isReviewablePath) {
|
|
205
|
+
if (code.includes("D")) info.hasTrackedDeletions = true;
|
|
206
|
+
if (isRenameOrCopy) info.hasRenames = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
index += isRenameOrCopy ? 2 : 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return info;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function getWorkingTreeStatusInfo(pi: ExtensionAPI, repoRoot: string): Promise<WorkingTreeStatusInfo> {
|
|
216
|
+
const output = await runGitAllowFailure(pi, repoRoot, ["status", "--porcelain=1", "--untracked-files=all", "-z"]);
|
|
217
|
+
return parseStatusPorcelainZ(output);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function toDisplayPath(change: ChangedPath): string {
|
|
221
|
+
if (change.status === "renamed") {
|
|
222
|
+
return `${change.oldPath ?? ""} -> ${change.newPath ?? ""}`;
|
|
223
|
+
}
|
|
224
|
+
return change.newPath ?? change.oldPath ?? "(unknown)";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function toComparison(change: ChangedPath): ReviewFileComparison {
|
|
228
|
+
return {
|
|
229
|
+
status: change.status,
|
|
230
|
+
oldPath: change.oldPath,
|
|
231
|
+
newPath: change.newPath,
|
|
232
|
+
displayPath: toDisplayPath(change),
|
|
233
|
+
hasOriginal: change.oldPath != null,
|
|
234
|
+
hasModified: change.newPath != null,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function buildBranchFileId(path: string, hasWorkingTreeFile: boolean, gitDiff: ReviewFileComparison): string {
|
|
239
|
+
return ["branch", path, hasWorkingTreeFile ? "working" : "gone", gitDiff.displayPath].join("::");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildSnapshotFileId(path: string, hasWorkingTreeFile: boolean): string {
|
|
243
|
+
return ["snapshot", path, hasWorkingTreeFile ? "working" : "head"].join("::");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function buildCommitFileId(sha: string, comparison: ReviewFileComparison): string {
|
|
247
|
+
return ["commit", sha, comparison.displayPath].join("::");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function getRevisionContent(pi: ExtensionAPI, repoRoot: string, revision: string, path: string): Promise<string> {
|
|
251
|
+
const result = await pi.exec("git", ["show", `${revision}:${path}`], { cwd: repoRoot });
|
|
252
|
+
if (result.code !== 0) {
|
|
253
|
+
return "";
|
|
254
|
+
}
|
|
255
|
+
return result.stdout;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const WORKING_TREE_SYMLINK_CONTENT = "[working tree symlink omitted for safety]\n";
|
|
259
|
+
|
|
260
|
+
type WorkingTreeEntry =
|
|
261
|
+
| { kind: "file"; absolutePath: string }
|
|
262
|
+
| { kind: "symlink" }
|
|
263
|
+
| { kind: "missing" | "other" | "unsafe" };
|
|
264
|
+
|
|
265
|
+
function inspectWorkingTreePath(repoRoot: string, path: string): WorkingTreeEntry {
|
|
266
|
+
const repoBase = resolve(repoRoot);
|
|
267
|
+
const candidate = resolve(repoBase, path);
|
|
268
|
+
const relativePath = relative(repoBase, candidate);
|
|
269
|
+
if (relativePath.length === 0 || /^(\.\.(?:[/\\]|$))/.test(relativePath)) {
|
|
270
|
+
return { kind: "unsafe" };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const parts = relativePath.split(/[\\/]+/).filter(Boolean);
|
|
274
|
+
let currentPath = repoBase;
|
|
275
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
276
|
+
currentPath = resolve(currentPath, parts[index]);
|
|
277
|
+
let stat;
|
|
278
|
+
try {
|
|
279
|
+
stat = lstatSync(currentPath);
|
|
280
|
+
} catch {
|
|
281
|
+
return { kind: "missing" };
|
|
282
|
+
}
|
|
283
|
+
const isFinal = index === parts.length - 1;
|
|
284
|
+
if (stat.isSymbolicLink()) {
|
|
285
|
+
return isFinal ? { kind: "symlink" } : { kind: "unsafe" };
|
|
286
|
+
}
|
|
287
|
+
if (!isFinal) {
|
|
288
|
+
if (!stat.isDirectory()) return { kind: "unsafe" };
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (stat.isFile()) return { kind: "file", absolutePath: currentPath };
|
|
292
|
+
return { kind: "other" };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { kind: "unsafe" };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function getWorkingTreeContent(repoRoot: string, path: string): Promise<string> {
|
|
299
|
+
const entry = inspectWorkingTreePath(repoRoot, path);
|
|
300
|
+
if (entry.kind === "symlink") return WORKING_TREE_SYMLINK_CONTENT;
|
|
301
|
+
if (entry.kind !== "file") return "";
|
|
302
|
+
try {
|
|
303
|
+
return await readFile(entry.absolutePath, "utf8");
|
|
304
|
+
} catch {
|
|
305
|
+
return "";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function getWorkingTreeBytes(repoRoot: string, path: string): Promise<Buffer | null> {
|
|
310
|
+
const entry = inspectWorkingTreePath(repoRoot, path);
|
|
311
|
+
if (entry.kind !== "file") return null;
|
|
312
|
+
try {
|
|
313
|
+
return await readFile(entry.absolutePath);
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function shellQuote(value: string): string {
|
|
320
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function getRevisionBytes(
|
|
324
|
+
pi: ExtensionAPI,
|
|
325
|
+
repoRoot: string,
|
|
326
|
+
revision: string,
|
|
327
|
+
path: string,
|
|
328
|
+
): Promise<Buffer | null> {
|
|
329
|
+
const spec = shellQuote(`${revision}:${path}`);
|
|
330
|
+
const result = await pi.exec("bash", ["-lc", `git show ${spec} | base64 | tr -d '\\n'`], { cwd: repoRoot });
|
|
331
|
+
if (result.code !== 0) return null;
|
|
332
|
+
const encoded = (result.stdout ?? "").trim();
|
|
333
|
+
try {
|
|
334
|
+
return Buffer.from(encoded, "base64");
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const imageMimeTypes = new Map<string, string>([
|
|
341
|
+
[".avif", "image/avif"],
|
|
342
|
+
[".bmp", "image/bmp"],
|
|
343
|
+
[".gif", "image/gif"],
|
|
344
|
+
[".ico", "image/x-icon"],
|
|
345
|
+
[".jpeg", "image/jpeg"],
|
|
346
|
+
[".jpg", "image/jpeg"],
|
|
347
|
+
[".png", "image/png"],
|
|
348
|
+
[".webp", "image/webp"],
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
const binaryExtensions = new Set([
|
|
352
|
+
".7z",
|
|
353
|
+
".a",
|
|
354
|
+
".avi",
|
|
355
|
+
".avif",
|
|
356
|
+
".bin",
|
|
357
|
+
".bmp",
|
|
358
|
+
".class",
|
|
359
|
+
".dll",
|
|
360
|
+
".dylib",
|
|
361
|
+
".eot",
|
|
362
|
+
".exe",
|
|
363
|
+
".gif",
|
|
364
|
+
".gz",
|
|
365
|
+
".ico",
|
|
366
|
+
".jar",
|
|
367
|
+
".jpeg",
|
|
368
|
+
".jpg",
|
|
369
|
+
".lockb",
|
|
370
|
+
".map",
|
|
371
|
+
".mov",
|
|
372
|
+
".mp3",
|
|
373
|
+
".mp4",
|
|
374
|
+
".o",
|
|
375
|
+
".otf",
|
|
376
|
+
".pdf",
|
|
377
|
+
".png",
|
|
378
|
+
".pyc",
|
|
379
|
+
".so",
|
|
380
|
+
".svgz",
|
|
381
|
+
".tar",
|
|
382
|
+
".ttf",
|
|
383
|
+
".wasm",
|
|
384
|
+
".webm",
|
|
385
|
+
".webp",
|
|
386
|
+
".woff",
|
|
387
|
+
".woff2",
|
|
388
|
+
".zip",
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
function classifyFilePath(path: string): { kind: ReviewFileKind; mimeType: string | null } {
|
|
392
|
+
const extension = extname(path.toLowerCase());
|
|
393
|
+
const mimeType = imageMimeTypes.get(extension) ?? null;
|
|
394
|
+
if (mimeType != null) return { kind: "image", mimeType };
|
|
395
|
+
if (binaryExtensions.has(extension)) return { kind: "binary", mimeType: null };
|
|
396
|
+
return { kind: "text", mimeType: null };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isIncludedReviewPath(path: string): boolean {
|
|
400
|
+
const lowerPath = path.toLowerCase();
|
|
401
|
+
const fileName = lowerPath.split("/").pop() ?? lowerPath;
|
|
402
|
+
return fileName.length > 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function bufferToDataUrl(buffer: Buffer, mimeType: string): string {
|
|
406
|
+
return `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function toReviewFile(
|
|
410
|
+
change: ChangedPath,
|
|
411
|
+
options: { id: string; worktreeStatus: ChangeStatus | null; hasWorkingTreeFile: boolean },
|
|
412
|
+
): ReviewFile {
|
|
413
|
+
const comparison = toComparison(change);
|
|
414
|
+
const path = change.newPath ?? change.oldPath ?? comparison.displayPath;
|
|
415
|
+
const meta = classifyFilePath(path);
|
|
416
|
+
return {
|
|
417
|
+
id: options.id,
|
|
418
|
+
path,
|
|
419
|
+
worktreeStatus: options.worktreeStatus,
|
|
420
|
+
hasWorkingTreeFile: options.hasWorkingTreeFile,
|
|
421
|
+
inGitDiff: true,
|
|
422
|
+
gitDiff: comparison,
|
|
423
|
+
kind: meta.kind,
|
|
424
|
+
mimeType: meta.mimeType,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function loadBinarySideFromWorkingTree(
|
|
429
|
+
repoRoot: string,
|
|
430
|
+
path: string | null,
|
|
431
|
+
mimeType: string | null,
|
|
432
|
+
): Promise<{ exists: boolean; previewUrl: string | null }> {
|
|
433
|
+
if (path == null) return { exists: false, previewUrl: null };
|
|
434
|
+
const entry = inspectWorkingTreePath(repoRoot, path);
|
|
435
|
+
if (entry.kind === "symlink") return { exists: true, previewUrl: null };
|
|
436
|
+
if (entry.kind !== "file") return { exists: false, previewUrl: null };
|
|
437
|
+
const bytes = await getWorkingTreeBytes(repoRoot, path);
|
|
438
|
+
if (bytes == null) return { exists: false, previewUrl: null };
|
|
439
|
+
return { exists: true, previewUrl: mimeType ? bufferToDataUrl(bytes, mimeType) : null };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function loadBinarySideFromRevision(
|
|
443
|
+
pi: ExtensionAPI,
|
|
444
|
+
repoRoot: string,
|
|
445
|
+
revision: string,
|
|
446
|
+
path: string | null,
|
|
447
|
+
mimeType: string | null,
|
|
448
|
+
): Promise<{ exists: boolean; previewUrl: string | null }> {
|
|
449
|
+
if (path == null) return { exists: false, previewUrl: null };
|
|
450
|
+
const bytes = await getRevisionBytes(pi, repoRoot, revision, path);
|
|
451
|
+
if (bytes == null) return { exists: false, previewUrl: null };
|
|
452
|
+
return { exists: true, previewUrl: mimeType ? bufferToDataUrl(bytes, mimeType) : null };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function mergeChangedPaths(...groups: ChangedPath[][]): ChangedPath[] {
|
|
456
|
+
const merged = new Map<string, ChangedPath>();
|
|
457
|
+
for (const group of groups) {
|
|
458
|
+
for (const change of group) {
|
|
459
|
+
const key = change.newPath ?? change.oldPath ?? "";
|
|
460
|
+
if (key.length === 0) continue;
|
|
461
|
+
merged.set(key, change);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return [...merged.values()];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function toUntrackedChangedPaths(paths: string[]): ChangedPath[] {
|
|
468
|
+
return paths.map((path) => ({ status: "added", oldPath: null, newPath: path }) satisfies ChangedPath);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function shouldNormalizeBranchChanges(
|
|
472
|
+
trackedChanges: ChangedPath[],
|
|
473
|
+
workingTreeStatus: WorkingTreeStatusInfo,
|
|
474
|
+
): boolean {
|
|
475
|
+
if (workingTreeStatus.hasRenames) return true;
|
|
476
|
+
if (!workingTreeStatus.hasUntracked) return false;
|
|
477
|
+
return trackedChanges.some((change) => change.status === "deleted");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function getTrackedBranchReviewChanges(
|
|
481
|
+
pi: ExtensionAPI,
|
|
482
|
+
repoRoot: string,
|
|
483
|
+
branchComparisonBase: string,
|
|
484
|
+
): Promise<ChangedPath[]> {
|
|
485
|
+
return parseNameStatus(
|
|
486
|
+
await runGitAllowFailure(pi, repoRoot, [
|
|
487
|
+
"diff",
|
|
488
|
+
"--find-renames",
|
|
489
|
+
"-M",
|
|
490
|
+
"--name-status",
|
|
491
|
+
branchComparisonBase,
|
|
492
|
+
"--",
|
|
493
|
+
]),
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function getWorkingTreeSnapshotChanges(
|
|
498
|
+
pi: ExtensionAPI,
|
|
499
|
+
repoRoot: string,
|
|
500
|
+
baseRevision: string | null,
|
|
501
|
+
): Promise<ChangedPath[]> {
|
|
502
|
+
const scriptLines = [
|
|
503
|
+
"set -euo pipefail",
|
|
504
|
+
'tmp_index=$(mktemp "/tmp/tlh-annotate-git-diff-index.XXXXXX")',
|
|
505
|
+
"trap 'rm -f \"$tmp_index\"' EXIT",
|
|
506
|
+
'export GIT_INDEX_FILE="$tmp_index"',
|
|
507
|
+
];
|
|
508
|
+
if (baseRevision != null) {
|
|
509
|
+
scriptLines.push(`git read-tree ${shellQuote(baseRevision)}`);
|
|
510
|
+
} else {
|
|
511
|
+
scriptLines.push('rm -f "$tmp_index"');
|
|
512
|
+
}
|
|
513
|
+
scriptLines.push("git add -A -- .");
|
|
514
|
+
scriptLines.push(
|
|
515
|
+
baseRevision != null
|
|
516
|
+
? `git diff --cached --find-renames -M --name-status ${shellQuote(baseRevision)} --`
|
|
517
|
+
: "git diff --cached --find-renames -M --name-status --root --",
|
|
518
|
+
);
|
|
519
|
+
const output = await runBashAllowFailure(pi, repoRoot, scriptLines.join("\n"));
|
|
520
|
+
return parseNameStatus(output);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function getBranchReviewChanges(
|
|
524
|
+
pi: ExtensionAPI,
|
|
525
|
+
repoRoot: string,
|
|
526
|
+
branchComparisonBase: string | null,
|
|
527
|
+
workingTreeStatus: WorkingTreeStatusInfo,
|
|
528
|
+
): Promise<ChangedPath[]> {
|
|
529
|
+
if (!branchComparisonBase) return [];
|
|
530
|
+
const trackedChanges = await getTrackedBranchReviewChanges(pi, repoRoot, branchComparisonBase);
|
|
531
|
+
if (shouldNormalizeBranchChanges(trackedChanges, workingTreeStatus)) {
|
|
532
|
+
return getWorkingTreeSnapshotChanges(pi, repoRoot, branchComparisonBase);
|
|
533
|
+
}
|
|
534
|
+
return mergeChangedPaths(trackedChanges, toUntrackedChangedPaths(workingTreeStatus.untrackedPaths));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function getWorkingTreeReviewChanges(
|
|
538
|
+
pi: ExtensionAPI,
|
|
539
|
+
repoRoot: string,
|
|
540
|
+
repositoryHasHead: boolean,
|
|
541
|
+
): Promise<ChangedPath[]> {
|
|
542
|
+
return getWorkingTreeSnapshotChanges(pi, repoRoot, repositoryHasHead ? "HEAD" : null);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function listReviewableRepositoryPaths(pi: ExtensionAPI, repoRoot: string): Promise<string[]> {
|
|
546
|
+
const output = await runGitAllowFailure(pi, repoRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z", "--"]);
|
|
547
|
+
const seen = new Set<string>();
|
|
548
|
+
const paths: string[] = [];
|
|
549
|
+
for (const path of output.split("\0")) {
|
|
550
|
+
if (path.length === 0 || seen.has(path) || !isIncludedReviewPath(path)) continue;
|
|
551
|
+
seen.add(path);
|
|
552
|
+
paths.push(path);
|
|
553
|
+
}
|
|
554
|
+
return paths;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function hasWorkingTreeFile(repoRoot: string, path: string): boolean {
|
|
558
|
+
const entry = inspectWorkingTreePath(repoRoot, path);
|
|
559
|
+
return entry.kind === "file" || entry.kind === "symlink";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function toSnapshotReviewFile(repoRoot: string, path: string): ReviewFile {
|
|
563
|
+
const meta = classifyFilePath(path);
|
|
564
|
+
const workingTreeFile = hasWorkingTreeFile(repoRoot, path);
|
|
565
|
+
return {
|
|
566
|
+
id: buildSnapshotFileId(path, workingTreeFile),
|
|
567
|
+
path,
|
|
568
|
+
worktreeStatus: null,
|
|
569
|
+
hasWorkingTreeFile: workingTreeFile,
|
|
570
|
+
inGitDiff: false,
|
|
571
|
+
gitDiff: null,
|
|
572
|
+
kind: meta.kind,
|
|
573
|
+
mimeType: meta.mimeType,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function getAllReviewFiles(
|
|
578
|
+
pi: ExtensionAPI,
|
|
579
|
+
repoRoot: string,
|
|
580
|
+
branchChanges: ChangedPath[],
|
|
581
|
+
): Promise<{ allFiles: ReviewFile[]; branchFiles: ReviewFile[] }> {
|
|
582
|
+
const branchFiles = branchChanges.map(toBranchReviewFile);
|
|
583
|
+
const branchFilesByPath = new Map(branchFiles.map((file) => [file.path, file]));
|
|
584
|
+
const seenPaths = new Set<string>();
|
|
585
|
+
const allFiles: ReviewFile[] = [];
|
|
586
|
+
|
|
587
|
+
for (const path of await listReviewableRepositoryPaths(pi, repoRoot)) {
|
|
588
|
+
seenPaths.add(path);
|
|
589
|
+
allFiles.push(branchFilesByPath.get(path) ?? toSnapshotReviewFile(repoRoot, path));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
for (const file of branchFiles) {
|
|
593
|
+
if (seenPaths.has(file.path)) continue;
|
|
594
|
+
allFiles.push(file);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
allFiles.sort(compareReviewFiles);
|
|
598
|
+
branchFiles.sort(compareReviewFiles);
|
|
599
|
+
return { allFiles, branchFiles };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function compareReviewFiles(a: ReviewFile, b: ReviewFile): number {
|
|
603
|
+
return a.path.localeCompare(b.path);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function toBranchReviewFile(change: ChangedPath): ReviewFile {
|
|
607
|
+
const comparison = toComparison(change);
|
|
608
|
+
const path = change.newPath ?? change.oldPath ?? comparison.displayPath;
|
|
609
|
+
return toReviewFile(change, {
|
|
610
|
+
id: buildBranchFileId(path, change.newPath != null, comparison),
|
|
611
|
+
worktreeStatus: change.status,
|
|
612
|
+
hasWorkingTreeFile: change.newPath != null,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export async function getReviewWindowData(
|
|
617
|
+
pi: ExtensionAPI,
|
|
618
|
+
cwd: string,
|
|
619
|
+
): Promise<{
|
|
620
|
+
repoRoot: string;
|
|
621
|
+
files: ReviewFile[];
|
|
622
|
+
commits: ReviewCommitInfo[];
|
|
623
|
+
branchBaseRef: string | null;
|
|
624
|
+
branchMergeBaseSha: string | null;
|
|
625
|
+
repositoryHasHead: boolean;
|
|
626
|
+
}> {
|
|
627
|
+
const repoRoot = await getRepoRoot(pi, cwd);
|
|
628
|
+
const repositoryHasHead = await hasHead(pi, repoRoot);
|
|
629
|
+
const reviewBase = repositoryHasHead ? await findReviewBase(pi, repoRoot) : null;
|
|
630
|
+
const branchComparisonBase = reviewBase?.mergeBase ?? (repositoryHasHead ? "HEAD" : null);
|
|
631
|
+
const workingTreeStatus = await getWorkingTreeStatusInfo(pi, repoRoot);
|
|
632
|
+
const branchChanges = repositoryHasHead
|
|
633
|
+
? await getBranchReviewChanges(pi, repoRoot, branchComparisonBase, workingTreeStatus)
|
|
634
|
+
: await getWorkingTreeReviewChanges(pi, repoRoot, false);
|
|
635
|
+
const reviewableBranchChanges = branchChanges.filter((change) =>
|
|
636
|
+
isIncludedReviewPath(change.newPath ?? change.oldPath ?? ""),
|
|
637
|
+
);
|
|
638
|
+
const { allFiles: files, branchFiles } = await getAllReviewFiles(pi, repoRoot, reviewableBranchChanges);
|
|
639
|
+
const commits = reviewBase ? await listRangeCommits(pi, repoRoot, `${reviewBase.mergeBase}..HEAD`, 100) : [];
|
|
640
|
+
const workingTreeCommit = workingTreeStatus.hasReviewableChanges ? [createWorkingTreeCommitInfo()] : [];
|
|
641
|
+
const fallbackCommits =
|
|
642
|
+
repositoryHasHead && branchFiles.length === 0 && commits.length === 0 && !workingTreeStatus.hasReviewableChanges
|
|
643
|
+
? await listRangeCommits(pi, repoRoot, "HEAD", 20)
|
|
644
|
+
: commits;
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
repoRoot,
|
|
648
|
+
files,
|
|
649
|
+
commits: [...workingTreeCommit, ...fallbackCommits],
|
|
650
|
+
branchBaseRef: reviewBase?.baseRef ?? null,
|
|
651
|
+
branchMergeBaseSha: branchComparisonBase,
|
|
652
|
+
repositoryHasHead,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export async function listRangeCommits(
|
|
657
|
+
pi: ExtensionAPI,
|
|
658
|
+
repoRoot: string,
|
|
659
|
+
range: string,
|
|
660
|
+
limit: number,
|
|
661
|
+
): Promise<ReviewCommitInfo[]> {
|
|
662
|
+
const sep = "\x1f";
|
|
663
|
+
const format = ["%H", "%h", "%s", "%an", "%aI"].join(sep);
|
|
664
|
+
const output = await runGitAllowFailure(pi, repoRoot, ["log", `-${limit}`, `--format=${format}`, range]);
|
|
665
|
+
return output
|
|
666
|
+
.split(/\r?\n/)
|
|
667
|
+
.map((line) => line.trimEnd())
|
|
668
|
+
.filter((line) => line.length > 0)
|
|
669
|
+
.map((line) => {
|
|
670
|
+
const [sha, shortSha, subject, authorName, authorDate] = line.split(sep);
|
|
671
|
+
return {
|
|
672
|
+
sha: sha ?? "",
|
|
673
|
+
shortSha: shortSha ?? (sha ?? "").slice(0, 7),
|
|
674
|
+
subject: subject ?? "",
|
|
675
|
+
authorName: authorName ?? "",
|
|
676
|
+
authorDate: authorDate ?? "",
|
|
677
|
+
kind: "commit",
|
|
678
|
+
} satisfies ReviewCommitInfo;
|
|
679
|
+
})
|
|
680
|
+
.filter((commit) => commit.sha.length > 0);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export async function getCommitFiles(pi: ExtensionAPI, repoRoot: string, sha: string): Promise<ReviewFile[]> {
|
|
684
|
+
if (isWorkingTreeCommitSha(sha)) {
|
|
685
|
+
const repositoryHasHead = await hasHead(pi, repoRoot);
|
|
686
|
+
const changes = (await getWorkingTreeReviewChanges(pi, repoRoot, repositoryHasHead)).filter((change) =>
|
|
687
|
+
isIncludedReviewPath(change.newPath ?? change.oldPath ?? ""),
|
|
688
|
+
);
|
|
689
|
+
return changes
|
|
690
|
+
.map((change): ReviewFile => {
|
|
691
|
+
const comparison = toComparison(change);
|
|
692
|
+
return toReviewFile(change, {
|
|
693
|
+
id: buildCommitFileId(sha, comparison),
|
|
694
|
+
worktreeStatus: change.status,
|
|
695
|
+
hasWorkingTreeFile: change.newPath != null,
|
|
696
|
+
});
|
|
697
|
+
})
|
|
698
|
+
.sort(compareReviewFiles);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const output = await runGitAllowFailure(pi, repoRoot, [
|
|
702
|
+
"diff-tree",
|
|
703
|
+
"--root",
|
|
704
|
+
"--find-renames",
|
|
705
|
+
"-M",
|
|
706
|
+
"--name-status",
|
|
707
|
+
"--no-commit-id",
|
|
708
|
+
"-r",
|
|
709
|
+
sha,
|
|
710
|
+
]);
|
|
711
|
+
const changes = parseNameStatus(output).filter((change) =>
|
|
712
|
+
isIncludedReviewPath(change.newPath ?? change.oldPath ?? ""),
|
|
713
|
+
);
|
|
714
|
+
return changes
|
|
715
|
+
.map((change): ReviewFile => {
|
|
716
|
+
const comparison = toComparison(change);
|
|
717
|
+
return toReviewFile(change, {
|
|
718
|
+
id: buildCommitFileId(sha, comparison),
|
|
719
|
+
worktreeStatus: null,
|
|
720
|
+
hasWorkingTreeFile: false,
|
|
721
|
+
});
|
|
722
|
+
})
|
|
723
|
+
.sort(compareReviewFiles);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export async function loadReviewFileContents(
|
|
727
|
+
pi: ExtensionAPI,
|
|
728
|
+
repoRoot: string,
|
|
729
|
+
file: ReviewFile,
|
|
730
|
+
scope: ReviewScope,
|
|
731
|
+
commitSha: string | null = null,
|
|
732
|
+
branchMergeBaseSha: string | null = null,
|
|
733
|
+
): Promise<ReviewFileContents> {
|
|
734
|
+
const emptyBinaryContents: ReviewFileContents = {
|
|
735
|
+
originalContent: "",
|
|
736
|
+
modifiedContent: "",
|
|
737
|
+
kind: file.kind,
|
|
738
|
+
mimeType: file.mimeType,
|
|
739
|
+
originalExists: false,
|
|
740
|
+
modifiedExists: false,
|
|
741
|
+
originalPreviewUrl: null,
|
|
742
|
+
modifiedPreviewUrl: null,
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
if (file.kind !== "text") {
|
|
746
|
+
if (scope === "all") {
|
|
747
|
+
const path = file.path;
|
|
748
|
+
const modifiedSide = file.hasWorkingTreeFile
|
|
749
|
+
? await loadBinarySideFromWorkingTree(repoRoot, path, file.mimeType)
|
|
750
|
+
: await loadBinarySideFromRevision(pi, repoRoot, "HEAD", path, file.mimeType);
|
|
751
|
+
return {
|
|
752
|
+
...emptyBinaryContents,
|
|
753
|
+
modifiedExists: modifiedSide.exists,
|
|
754
|
+
modifiedPreviewUrl: modifiedSide.previewUrl,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const comparison = file.gitDiff;
|
|
759
|
+
if (comparison == null) return emptyBinaryContents;
|
|
760
|
+
|
|
761
|
+
if (scope === "commits") {
|
|
762
|
+
if (!commitSha) return emptyBinaryContents;
|
|
763
|
+
if (isWorkingTreeCommitSha(commitSha)) {
|
|
764
|
+
const repositoryHasHead = await hasHead(pi, repoRoot);
|
|
765
|
+
const originalSide = repositoryHasHead
|
|
766
|
+
? await loadBinarySideFromRevision(pi, repoRoot, "HEAD", comparison.oldPath, file.mimeType)
|
|
767
|
+
: { exists: false, previewUrl: null };
|
|
768
|
+
const modifiedSide = file.hasWorkingTreeFile
|
|
769
|
+
? await loadBinarySideFromWorkingTree(repoRoot, comparison.newPath, file.mimeType)
|
|
770
|
+
: { exists: false, previewUrl: null };
|
|
771
|
+
return {
|
|
772
|
+
...emptyBinaryContents,
|
|
773
|
+
originalExists: originalSide.exists,
|
|
774
|
+
modifiedExists: modifiedSide.exists,
|
|
775
|
+
originalPreviewUrl: originalSide.previewUrl,
|
|
776
|
+
modifiedPreviewUrl: modifiedSide.previewUrl,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
const originalSide = await loadBinarySideFromRevision(
|
|
780
|
+
pi,
|
|
781
|
+
repoRoot,
|
|
782
|
+
`${commitSha}^`,
|
|
783
|
+
comparison.oldPath,
|
|
784
|
+
file.mimeType,
|
|
785
|
+
);
|
|
786
|
+
const modifiedSide = await loadBinarySideFromRevision(pi, repoRoot, commitSha, comparison.newPath, file.mimeType);
|
|
787
|
+
return {
|
|
788
|
+
...emptyBinaryContents,
|
|
789
|
+
originalExists: originalSide.exists,
|
|
790
|
+
modifiedExists: modifiedSide.exists,
|
|
791
|
+
originalPreviewUrl: originalSide.previewUrl,
|
|
792
|
+
modifiedPreviewUrl: modifiedSide.previewUrl,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (!branchMergeBaseSha) return emptyBinaryContents;
|
|
797
|
+
const originalSide = await loadBinarySideFromRevision(
|
|
798
|
+
pi,
|
|
799
|
+
repoRoot,
|
|
800
|
+
branchMergeBaseSha,
|
|
801
|
+
comparison.oldPath,
|
|
802
|
+
file.mimeType,
|
|
803
|
+
);
|
|
804
|
+
const modifiedSide = file.hasWorkingTreeFile
|
|
805
|
+
? await loadBinarySideFromWorkingTree(repoRoot, comparison.newPath, file.mimeType)
|
|
806
|
+
: await loadBinarySideFromRevision(pi, repoRoot, "HEAD", comparison.newPath, file.mimeType);
|
|
807
|
+
return {
|
|
808
|
+
...emptyBinaryContents,
|
|
809
|
+
originalExists: originalSide.exists,
|
|
810
|
+
modifiedExists: modifiedSide.exists,
|
|
811
|
+
originalPreviewUrl: originalSide.previewUrl,
|
|
812
|
+
modifiedPreviewUrl: modifiedSide.previewUrl,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (scope === "all") {
|
|
817
|
+
const path = file.path;
|
|
818
|
+
const workingTreeExists = file.hasWorkingTreeFile ? hasWorkingTreeFile(repoRoot, path) : false;
|
|
819
|
+
const content = file.hasWorkingTreeFile
|
|
820
|
+
? await getWorkingTreeContent(repoRoot, path)
|
|
821
|
+
: await getRevisionContent(pi, repoRoot, "HEAD", path);
|
|
822
|
+
const exists = file.hasWorkingTreeFile ? workingTreeExists : content.length > 0 || path.length > 0;
|
|
823
|
+
return {
|
|
824
|
+
originalContent: content,
|
|
825
|
+
modifiedContent: content,
|
|
826
|
+
kind: file.kind,
|
|
827
|
+
mimeType: file.mimeType,
|
|
828
|
+
originalExists: exists,
|
|
829
|
+
modifiedExists: exists,
|
|
830
|
+
originalPreviewUrl: null,
|
|
831
|
+
modifiedPreviewUrl: null,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const comparison = file.gitDiff;
|
|
836
|
+
if (comparison == null) {
|
|
837
|
+
return {
|
|
838
|
+
originalContent: "",
|
|
839
|
+
modifiedContent: "",
|
|
840
|
+
kind: file.kind,
|
|
841
|
+
mimeType: file.mimeType,
|
|
842
|
+
originalExists: false,
|
|
843
|
+
modifiedExists: false,
|
|
844
|
+
originalPreviewUrl: null,
|
|
845
|
+
modifiedPreviewUrl: null,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (scope === "commits") {
|
|
850
|
+
if (!commitSha) {
|
|
851
|
+
return {
|
|
852
|
+
originalContent: "",
|
|
853
|
+
modifiedContent: "",
|
|
854
|
+
kind: file.kind,
|
|
855
|
+
mimeType: file.mimeType,
|
|
856
|
+
originalExists: false,
|
|
857
|
+
modifiedExists: false,
|
|
858
|
+
originalPreviewUrl: null,
|
|
859
|
+
modifiedPreviewUrl: null,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (isWorkingTreeCommitSha(commitSha)) {
|
|
863
|
+
const repositoryHasHead = await hasHead(pi, repoRoot);
|
|
864
|
+
const originalContent =
|
|
865
|
+
repositoryHasHead && comparison.oldPath != null
|
|
866
|
+
? await getRevisionContent(pi, repoRoot, "HEAD", comparison.oldPath)
|
|
867
|
+
: "";
|
|
868
|
+
const modifiedExists =
|
|
869
|
+
comparison.newPath != null && file.hasWorkingTreeFile
|
|
870
|
+
? hasWorkingTreeFile(repoRoot, comparison.newPath)
|
|
871
|
+
: false;
|
|
872
|
+
const modifiedContent =
|
|
873
|
+
comparison.newPath == null
|
|
874
|
+
? ""
|
|
875
|
+
: file.hasWorkingTreeFile
|
|
876
|
+
? await getWorkingTreeContent(repoRoot, comparison.newPath)
|
|
877
|
+
: "";
|
|
878
|
+
return {
|
|
879
|
+
originalContent,
|
|
880
|
+
modifiedContent,
|
|
881
|
+
kind: file.kind,
|
|
882
|
+
mimeType: file.mimeType,
|
|
883
|
+
originalExists: repositoryHasHead && comparison.oldPath != null,
|
|
884
|
+
modifiedExists,
|
|
885
|
+
originalPreviewUrl: null,
|
|
886
|
+
modifiedPreviewUrl: null,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
const originalContent =
|
|
890
|
+
comparison.oldPath == null ? "" : await getRevisionContent(pi, repoRoot, `${commitSha}^`, comparison.oldPath);
|
|
891
|
+
const modifiedContent =
|
|
892
|
+
comparison.newPath == null ? "" : await getRevisionContent(pi, repoRoot, commitSha, comparison.newPath);
|
|
893
|
+
return {
|
|
894
|
+
originalContent,
|
|
895
|
+
modifiedContent,
|
|
896
|
+
kind: file.kind,
|
|
897
|
+
mimeType: file.mimeType,
|
|
898
|
+
originalExists: comparison.oldPath != null,
|
|
899
|
+
modifiedExists: comparison.newPath != null,
|
|
900
|
+
originalPreviewUrl: null,
|
|
901
|
+
modifiedPreviewUrl: null,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (!branchMergeBaseSha) {
|
|
906
|
+
return {
|
|
907
|
+
originalContent: "",
|
|
908
|
+
modifiedContent: "",
|
|
909
|
+
kind: file.kind,
|
|
910
|
+
mimeType: file.mimeType,
|
|
911
|
+
originalExists: false,
|
|
912
|
+
modifiedExists: false,
|
|
913
|
+
originalPreviewUrl: null,
|
|
914
|
+
modifiedPreviewUrl: null,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const originalContent =
|
|
919
|
+
comparison.oldPath == null ? "" : await getRevisionContent(pi, repoRoot, branchMergeBaseSha, comparison.oldPath);
|
|
920
|
+
const modifiedExists =
|
|
921
|
+
comparison.newPath != null && file.hasWorkingTreeFile ? hasWorkingTreeFile(repoRoot, comparison.newPath) : false;
|
|
922
|
+
const modifiedContent =
|
|
923
|
+
comparison.newPath == null
|
|
924
|
+
? ""
|
|
925
|
+
: file.hasWorkingTreeFile
|
|
926
|
+
? await getWorkingTreeContent(repoRoot, comparison.newPath)
|
|
927
|
+
: await getRevisionContent(pi, repoRoot, "HEAD", comparison.newPath);
|
|
928
|
+
return {
|
|
929
|
+
originalContent,
|
|
930
|
+
modifiedContent,
|
|
931
|
+
kind: file.kind,
|
|
932
|
+
mimeType: file.mimeType,
|
|
933
|
+
originalExists: comparison.oldPath != null,
|
|
934
|
+
modifiedExists,
|
|
935
|
+
originalPreviewUrl: null,
|
|
936
|
+
modifiedPreviewUrl: null,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
export const __testing = {
|
|
941
|
+
parseStatusPorcelainZ,
|
|
942
|
+
shouldNormalizeBranchChanges,
|
|
943
|
+
};
|