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