@diegopetrucci/pi-extensions 0.1.74 → 0.1.78
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 +17 -12
- package/extensions/agent-workflow-audit/README.md +1 -1
- package/extensions/agent-workflow-audit/index.ts +1631 -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 +846 -768
- 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/index.html +3 -3
- 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 +6 -6
- package/extensions/code-reviewer/index.ts +1825 -1373
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +50 -52
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/README.md +9 -9
- package/extensions/context-cap/index.ts +313 -305
- package/extensions/context-cap/package.json +2 -2
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/README.md +4 -3
- package/extensions/contrarian/index.ts +1863 -1737
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +37 -41
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +11 -43
- package/extensions/dynamic-context-pruning/index.ts +2131 -2105
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +52 -14
- 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 +87 -103
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/README.md +1 -1
- package/extensions/librarian/index.ts +1877 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +987 -1012
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/minimal-footer/package.json +1 -1
- 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/README.md +4 -3
- package/extensions/oracle/index.ts +1740 -1627
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +3 -0
- package/extensions/permission-gate/index.ts +785 -612
- 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/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/README.md +30 -19
- package/extensions/quiet-tools/index.ts +376 -291
- package/extensions/quiet-tools/package.json +2 -2
- 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 +280 -280
- package/extensions/todo/package.json +1 -1
- package/extensions/triage-comments/README.md +6 -4
- package/extensions/triage-comments/index.ts +2620 -1888
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +9 -6
- 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,989 @@ 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
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function runGitAllowFailure(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
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;
|
|
62
|
+
}
|
|
63
|
+
|
|
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;
|
|
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
|
-
|
|
151
|
+
const code = (parts[0] ?? "")[0];
|
|
140
152
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
}
|
|
147
159
|
|
|
148
|
-
|
|
149
|
-
|
|
160
|
+
const path = parts[1] ?? null;
|
|
161
|
+
if (path == null) return null;
|
|
150
162
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
170
|
+
const lines = output
|
|
171
|
+
.split(/\r?\n/)
|
|
172
|
+
.map((line) => line.trim())
|
|
173
|
+
.filter((line) => line.length > 0);
|
|
162
174
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
".tar",
|
|
382
|
-
".ttf",
|
|
383
|
-
".wasm",
|
|
384
|
-
".webm",
|
|
385
|
-
".webp",
|
|
386
|
-
".woff",
|
|
387
|
-
".woff2",
|
|
388
|
-
".zip",
|
|
381
|
+
".7z",
|
|
382
|
+
".a",
|
|
383
|
+
".avi",
|
|
384
|
+
".bin",
|
|
385
|
+
".class",
|
|
386
|
+
".dll",
|
|
387
|
+
".dylib",
|
|
388
|
+
".eot",
|
|
389
|
+
".exe",
|
|
390
|
+
".gz",
|
|
391
|
+
".jar",
|
|
392
|
+
".lockb",
|
|
393
|
+
".map",
|
|
394
|
+
".mov",
|
|
395
|
+
".mp3",
|
|
396
|
+
".mp4",
|
|
397
|
+
".o",
|
|
398
|
+
".otf",
|
|
399
|
+
".pdf",
|
|
400
|
+
".pyc",
|
|
401
|
+
".so",
|
|
402
|
+
".svgz",
|
|
403
|
+
".tar",
|
|
404
|
+
".ttf",
|
|
405
|
+
".wasm",
|
|
406
|
+
".webm",
|
|
407
|
+
".woff",
|
|
408
|
+
".woff2",
|
|
409
|
+
".zip",
|
|
389
410
|
]);
|
|
390
411
|
|
|
391
412
|
function classifyFilePath(path: string): { kind: ReviewFileKind; mimeType: string | null } {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
413
|
+
const extension = extname(path.toLowerCase());
|
|
414
|
+
const mimeType = imageMimeTypes.get(extension) ?? null;
|
|
415
|
+
if (mimeType != null) return { kind: "image", mimeType };
|
|
416
|
+
if (binaryExtensions.has(extension)) return { kind: "binary", mimeType: null };
|
|
417
|
+
return { kind: "text", mimeType: null };
|
|
397
418
|
}
|
|
398
419
|
|
|
399
420
|
function isIncludedReviewPath(path: string): boolean {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
421
|
+
const lowerPath = path.toLowerCase();
|
|
422
|
+
const fileName = lowerPath.split("/").pop() ?? lowerPath;
|
|
423
|
+
return fileName.length > 0;
|
|
403
424
|
}
|
|
404
425
|
|
|
405
426
|
function bufferToDataUrl(buffer: Buffer, mimeType: string): string {
|
|
406
|
-
|
|
427
|
+
return `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
407
428
|
}
|
|
408
429
|
|
|
409
430
|
function toReviewFile(
|
|
410
|
-
|
|
411
|
-
|
|
431
|
+
change: ChangedPath,
|
|
432
|
+
options: { id: string; worktreeStatus: ChangeStatus | null; hasWorkingTreeFile: boolean },
|
|
412
433
|
): ReviewFile {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
434
|
+
const comparison = toComparison(change);
|
|
435
|
+
const path = change.newPath ?? change.oldPath ?? comparison.displayPath;
|
|
436
|
+
const meta = classifyFilePath(path);
|
|
437
|
+
return {
|
|
438
|
+
id: options.id,
|
|
439
|
+
path,
|
|
440
|
+
worktreeStatus: options.worktreeStatus,
|
|
441
|
+
hasWorkingTreeFile: options.hasWorkingTreeFile,
|
|
442
|
+
inGitDiff: true,
|
|
443
|
+
gitDiff: comparison,
|
|
444
|
+
kind: meta.kind,
|
|
445
|
+
mimeType: meta.mimeType,
|
|
446
|
+
};
|
|
426
447
|
}
|
|
427
448
|
|
|
428
449
|
async function loadBinarySideFromWorkingTree(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
450
|
+
repoRoot: string,
|
|
451
|
+
path: string | null,
|
|
452
|
+
mimeType: string | null,
|
|
432
453
|
): Promise<{ exists: boolean; previewUrl: string | null }> {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
454
|
+
if (path == null) return { exists: false, previewUrl: null };
|
|
455
|
+
const entry = inspectWorkingTreePath(repoRoot, path);
|
|
456
|
+
if (entry.kind === "symlink") return { exists: true, previewUrl: null };
|
|
457
|
+
if (entry.kind !== "file") return { exists: false, previewUrl: null };
|
|
458
|
+
const bytes = await getWorkingTreeBytes(repoRoot, path);
|
|
459
|
+
if (bytes == null) return { exists: false, previewUrl: null };
|
|
460
|
+
return { exists: true, previewUrl: mimeType ? bufferToDataUrl(bytes, mimeType) : null };
|
|
440
461
|
}
|
|
441
462
|
|
|
442
463
|
async function loadBinarySideFromRevision(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
464
|
+
pi: ExtensionAPI,
|
|
465
|
+
repoRoot: string,
|
|
466
|
+
revision: string,
|
|
467
|
+
path: string | null,
|
|
468
|
+
mimeType: string | null,
|
|
448
469
|
): Promise<{ exists: boolean; previewUrl: string | null }> {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
470
|
+
if (path == null) return { exists: false, previewUrl: null };
|
|
471
|
+
const bytes = await getRevisionBytes(pi, repoRoot, revision, path);
|
|
472
|
+
if (bytes == null) return { exists: false, previewUrl: null };
|
|
473
|
+
return { exists: true, previewUrl: mimeType ? bufferToDataUrl(bytes, mimeType) : null };
|
|
453
474
|
}
|
|
454
475
|
|
|
455
476
|
function mergeChangedPaths(...groups: ChangedPath[][]): ChangedPath[] {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
477
|
+
const merged = new Map<string, ChangedPath>();
|
|
478
|
+
for (const group of groups) {
|
|
479
|
+
for (const change of group) {
|
|
480
|
+
const key = change.newPath ?? change.oldPath ?? "";
|
|
481
|
+
if (key.length === 0) continue;
|
|
482
|
+
merged.set(key, change);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return [...merged.values()];
|
|
465
486
|
}
|
|
466
487
|
|
|
467
488
|
function toUntrackedChangedPaths(paths: string[]): ChangedPath[] {
|
|
468
|
-
|
|
489
|
+
return paths.map(
|
|
490
|
+
(path) => ({ status: "added", oldPath: null, newPath: path }) satisfies ChangedPath,
|
|
491
|
+
);
|
|
469
492
|
}
|
|
470
493
|
|
|
471
494
|
function shouldNormalizeBranchChanges(
|
|
472
|
-
|
|
473
|
-
|
|
495
|
+
trackedChanges: ChangedPath[],
|
|
496
|
+
workingTreeStatus: WorkingTreeStatusInfo,
|
|
474
497
|
): boolean {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
498
|
+
if (workingTreeStatus.hasRenames) return true;
|
|
499
|
+
if (!workingTreeStatus.hasUntracked) return false;
|
|
500
|
+
return trackedChanges.some((change) => change.status === "deleted");
|
|
478
501
|
}
|
|
479
502
|
|
|
480
503
|
async function getTrackedBranchReviewChanges(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
504
|
+
pi: ExtensionAPI,
|
|
505
|
+
repoRoot: string,
|
|
506
|
+
branchComparisonBase: string,
|
|
484
507
|
): Promise<ChangedPath[]> {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
508
|
+
return parseNameStatus(
|
|
509
|
+
await runGitAllowFailure(pi, repoRoot, [
|
|
510
|
+
"diff",
|
|
511
|
+
"--find-renames",
|
|
512
|
+
"-M",
|
|
513
|
+
"--name-status",
|
|
514
|
+
branchComparisonBase,
|
|
515
|
+
"--",
|
|
516
|
+
]),
|
|
517
|
+
);
|
|
495
518
|
}
|
|
496
519
|
|
|
497
520
|
async function getWorkingTreeSnapshotChanges(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
521
|
+
pi: ExtensionAPI,
|
|
522
|
+
repoRoot: string,
|
|
523
|
+
baseRevision: string | null,
|
|
501
524
|
): Promise<ChangedPath[]> {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
525
|
+
const scriptLines = [
|
|
526
|
+
"set -euo pipefail",
|
|
527
|
+
'tmp_index=$(mktemp "/tmp/tlh-annotate-git-diff-index.XXXXXX")',
|
|
528
|
+
"trap 'rm -f \"$tmp_index\"' EXIT",
|
|
529
|
+
'export GIT_INDEX_FILE="$tmp_index"',
|
|
530
|
+
];
|
|
531
|
+
if (baseRevision != null) {
|
|
532
|
+
scriptLines.push(`git read-tree ${shellQuote(baseRevision)}`);
|
|
533
|
+
} else {
|
|
534
|
+
scriptLines.push('rm -f "$tmp_index"');
|
|
535
|
+
}
|
|
536
|
+
scriptLines.push("git add -A -- .");
|
|
537
|
+
scriptLines.push(
|
|
538
|
+
baseRevision != null
|
|
539
|
+
? `git diff --cached --find-renames -M --name-status ${shellQuote(baseRevision)} --`
|
|
540
|
+
: "git diff --cached --find-renames -M --name-status --root --",
|
|
541
|
+
);
|
|
542
|
+
const output = await runBashAllowFailure(pi, repoRoot, scriptLines.join("\n"));
|
|
543
|
+
return parseNameStatus(output);
|
|
521
544
|
}
|
|
522
545
|
|
|
523
546
|
async function getBranchReviewChanges(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
547
|
+
pi: ExtensionAPI,
|
|
548
|
+
repoRoot: string,
|
|
549
|
+
branchComparisonBase: string | null,
|
|
550
|
+
workingTreeStatus: WorkingTreeStatusInfo,
|
|
528
551
|
): Promise<ChangedPath[]> {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
552
|
+
if (!branchComparisonBase) return [];
|
|
553
|
+
const trackedChanges = await getTrackedBranchReviewChanges(pi, repoRoot, branchComparisonBase);
|
|
554
|
+
if (shouldNormalizeBranchChanges(trackedChanges, workingTreeStatus)) {
|
|
555
|
+
return getWorkingTreeSnapshotChanges(pi, repoRoot, branchComparisonBase);
|
|
556
|
+
}
|
|
557
|
+
return mergeChangedPaths(
|
|
558
|
+
trackedChanges,
|
|
559
|
+
toUntrackedChangedPaths(workingTreeStatus.untrackedPaths),
|
|
560
|
+
);
|
|
535
561
|
}
|
|
536
562
|
|
|
537
563
|
async function getWorkingTreeReviewChanges(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
564
|
+
pi: ExtensionAPI,
|
|
565
|
+
repoRoot: string,
|
|
566
|
+
repositoryHasHead: boolean,
|
|
541
567
|
): Promise<ChangedPath[]> {
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
async function listReviewableRepositoryPaths(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
568
|
+
return getWorkingTreeSnapshotChanges(pi, repoRoot, repositoryHasHead ? "HEAD" : null);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function listReviewableRepositoryPaths(
|
|
572
|
+
pi: ExtensionAPI,
|
|
573
|
+
repoRoot: string,
|
|
574
|
+
): Promise<string[]> {
|
|
575
|
+
const output = await runGitAllowFailure(pi, repoRoot, [
|
|
576
|
+
"ls-files",
|
|
577
|
+
"--cached",
|
|
578
|
+
"--others",
|
|
579
|
+
"--exclude-standard",
|
|
580
|
+
"-z",
|
|
581
|
+
"--",
|
|
582
|
+
]);
|
|
583
|
+
const seen = new Set<string>();
|
|
584
|
+
const paths: string[] = [];
|
|
585
|
+
for (const path of output.split("\0")) {
|
|
586
|
+
if (path.length === 0 || seen.has(path) || !isIncludedReviewPath(path)) continue;
|
|
587
|
+
seen.add(path);
|
|
588
|
+
paths.push(path);
|
|
589
|
+
}
|
|
590
|
+
return paths;
|
|
555
591
|
}
|
|
556
592
|
|
|
557
593
|
function hasWorkingTreeFile(repoRoot: string, path: string): boolean {
|
|
558
|
-
|
|
559
|
-
|
|
594
|
+
const entry = inspectWorkingTreePath(repoRoot, path);
|
|
595
|
+
return entry.kind === "file" || entry.kind === "symlink";
|
|
560
596
|
}
|
|
561
597
|
|
|
562
598
|
function toSnapshotReviewFile(repoRoot: string, path: string): ReviewFile {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
599
|
+
const meta = classifyFilePath(path);
|
|
600
|
+
const workingTreeFile = hasWorkingTreeFile(repoRoot, path);
|
|
601
|
+
return {
|
|
602
|
+
id: buildSnapshotFileId(path, workingTreeFile),
|
|
603
|
+
path,
|
|
604
|
+
worktreeStatus: null,
|
|
605
|
+
hasWorkingTreeFile: workingTreeFile,
|
|
606
|
+
inGitDiff: false,
|
|
607
|
+
gitDiff: null,
|
|
608
|
+
kind: meta.kind,
|
|
609
|
+
mimeType: meta.mimeType,
|
|
610
|
+
};
|
|
575
611
|
}
|
|
576
612
|
|
|
577
613
|
async function getAllReviewFiles(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
614
|
+
pi: ExtensionAPI,
|
|
615
|
+
repoRoot: string,
|
|
616
|
+
branchChanges: ChangedPath[],
|
|
581
617
|
): Promise<{ allFiles: ReviewFile[]; branchFiles: ReviewFile[] }> {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
618
|
+
const branchFiles = branchChanges.map(toBranchReviewFile);
|
|
619
|
+
const branchFilesByPath = new Map(branchFiles.map((file) => [file.path, file]));
|
|
620
|
+
const seenPaths = new Set<string>();
|
|
621
|
+
const allFiles: ReviewFile[] = [];
|
|
586
622
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
623
|
+
for (const path of await listReviewableRepositoryPaths(pi, repoRoot)) {
|
|
624
|
+
seenPaths.add(path);
|
|
625
|
+
allFiles.push(branchFilesByPath.get(path) ?? toSnapshotReviewFile(repoRoot, path));
|
|
626
|
+
}
|
|
591
627
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
628
|
+
for (const file of branchFiles) {
|
|
629
|
+
if (seenPaths.has(file.path)) continue;
|
|
630
|
+
allFiles.push(file);
|
|
631
|
+
}
|
|
596
632
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
633
|
+
allFiles.sort(compareReviewFiles);
|
|
634
|
+
branchFiles.sort(compareReviewFiles);
|
|
635
|
+
return { allFiles, branchFiles };
|
|
600
636
|
}
|
|
601
637
|
|
|
602
638
|
function compareReviewFiles(a: ReviewFile, b: ReviewFile): number {
|
|
603
|
-
|
|
639
|
+
return a.path.localeCompare(b.path);
|
|
604
640
|
}
|
|
605
641
|
|
|
606
642
|
function toBranchReviewFile(change: ChangedPath): ReviewFile {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
643
|
+
const comparison = toComparison(change);
|
|
644
|
+
const path = change.newPath ?? change.oldPath ?? comparison.displayPath;
|
|
645
|
+
return toReviewFile(change, {
|
|
646
|
+
id: buildBranchFileId(path, change.newPath != null, comparison),
|
|
647
|
+
worktreeStatus: change.status,
|
|
648
|
+
hasWorkingTreeFile: change.newPath != null,
|
|
649
|
+
});
|
|
614
650
|
}
|
|
615
651
|
|
|
616
652
|
export async function getReviewWindowData(
|
|
617
|
-
|
|
618
|
-
|
|
653
|
+
pi: ExtensionAPI,
|
|
654
|
+
cwd: string,
|
|
619
655
|
): Promise<{
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
656
|
+
repoRoot: string;
|
|
657
|
+
files: ReviewFile[];
|
|
658
|
+
commits: ReviewCommitInfo[];
|
|
659
|
+
branchBaseRef: string | null;
|
|
660
|
+
branchMergeBaseSha: string | null;
|
|
661
|
+
repositoryHasHead: boolean;
|
|
626
662
|
}> {
|
|
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
|
-
|
|
663
|
+
const repoRoot = await getRepoRoot(pi, cwd);
|
|
664
|
+
const repositoryHasHead = await hasHead(pi, repoRoot);
|
|
665
|
+
const reviewBase = repositoryHasHead ? await findReviewBase(pi, repoRoot) : null;
|
|
666
|
+
const branchComparisonBase = reviewBase?.mergeBase ?? (repositoryHasHead ? "HEAD" : null);
|
|
667
|
+
const workingTreeStatus = await getWorkingTreeStatusInfo(pi, repoRoot);
|
|
668
|
+
const branchChanges = repositoryHasHead
|
|
669
|
+
? await getBranchReviewChanges(pi, repoRoot, branchComparisonBase, workingTreeStatus)
|
|
670
|
+
: await getWorkingTreeReviewChanges(pi, repoRoot, false);
|
|
671
|
+
const reviewableBranchChanges = branchChanges.filter((change) =>
|
|
672
|
+
isIncludedReviewPath(change.newPath ?? change.oldPath ?? ""),
|
|
673
|
+
);
|
|
674
|
+
const { allFiles: files, branchFiles } = await getAllReviewFiles(
|
|
675
|
+
pi,
|
|
676
|
+
repoRoot,
|
|
677
|
+
reviewableBranchChanges,
|
|
678
|
+
);
|
|
679
|
+
const commits = reviewBase
|
|
680
|
+
? await listRangeCommits(pi, repoRoot, `${reviewBase.mergeBase}..HEAD`, 100)
|
|
681
|
+
: [];
|
|
682
|
+
const workingTreeCommit = workingTreeStatus.hasReviewableChanges
|
|
683
|
+
? [createWorkingTreeCommitInfo()]
|
|
684
|
+
: [];
|
|
685
|
+
const fallbackCommits =
|
|
686
|
+
repositoryHasHead &&
|
|
687
|
+
branchFiles.length === 0 &&
|
|
688
|
+
commits.length === 0 &&
|
|
689
|
+
!workingTreeStatus.hasReviewableChanges
|
|
690
|
+
? await listRangeCommits(pi, repoRoot, "HEAD", 20)
|
|
691
|
+
: commits;
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
repoRoot,
|
|
695
|
+
files,
|
|
696
|
+
commits: [...workingTreeCommit, ...fallbackCommits],
|
|
697
|
+
branchBaseRef: reviewBase?.baseRef ?? null,
|
|
698
|
+
branchMergeBaseSha: branchComparisonBase,
|
|
699
|
+
repositoryHasHead,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function listRangeCommits(
|
|
704
|
+
pi: ExtensionAPI,
|
|
705
|
+
repoRoot: string,
|
|
706
|
+
range: string,
|
|
707
|
+
limit: number,
|
|
661
708
|
): 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
|
-
|
|
709
|
+
const sep = "\x1f";
|
|
710
|
+
const format = ["%H", "%h", "%s", "%an", "%aI"].join(sep);
|
|
711
|
+
const output = await runGitAllowFailure(pi, repoRoot, [
|
|
712
|
+
"log",
|
|
713
|
+
`-${limit}`,
|
|
714
|
+
`--format=${format}`,
|
|
715
|
+
range,
|
|
716
|
+
]);
|
|
717
|
+
return output
|
|
718
|
+
.split(/\r?\n/)
|
|
719
|
+
.map((line) => line.trimEnd())
|
|
720
|
+
.filter((line) => line.length > 0)
|
|
721
|
+
.map((line) => {
|
|
722
|
+
const [sha, shortSha, subject, authorName, authorDate] = line.split(sep);
|
|
723
|
+
return {
|
|
724
|
+
sha: sha ?? "",
|
|
725
|
+
shortSha: shortSha ?? (sha ?? "").slice(0, 7),
|
|
726
|
+
subject: subject ?? "",
|
|
727
|
+
authorName: authorName ?? "",
|
|
728
|
+
authorDate: authorDate ?? "",
|
|
729
|
+
kind: "commit",
|
|
730
|
+
} satisfies ReviewCommitInfo;
|
|
731
|
+
})
|
|
732
|
+
.filter((commit) => commit.sha.length > 0);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export async function getCommitFiles(
|
|
736
|
+
pi: ExtensionAPI,
|
|
737
|
+
repoRoot: string,
|
|
738
|
+
sha: string,
|
|
739
|
+
): Promise<ReviewFile[]> {
|
|
740
|
+
if (isWorkingTreeCommitSha(sha)) {
|
|
741
|
+
const repositoryHasHead = await hasHead(pi, repoRoot);
|
|
742
|
+
const changes = (await getWorkingTreeReviewChanges(pi, repoRoot, repositoryHasHead)).filter(
|
|
743
|
+
(change) => isIncludedReviewPath(change.newPath ?? change.oldPath ?? ""),
|
|
744
|
+
);
|
|
745
|
+
return changes
|
|
746
|
+
.map((change): ReviewFile => {
|
|
747
|
+
const comparison = toComparison(change);
|
|
748
|
+
return toReviewFile(change, {
|
|
749
|
+
id: buildCommitFileId(sha, comparison),
|
|
750
|
+
worktreeStatus: change.status,
|
|
751
|
+
hasWorkingTreeFile: change.newPath != null,
|
|
752
|
+
});
|
|
753
|
+
})
|
|
754
|
+
.sort(compareReviewFiles);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const output = await runGitAllowFailure(pi, repoRoot, [
|
|
758
|
+
"diff-tree",
|
|
759
|
+
"--root",
|
|
760
|
+
"--find-renames",
|
|
761
|
+
"-M",
|
|
762
|
+
"--name-status",
|
|
763
|
+
"--no-commit-id",
|
|
764
|
+
"-r",
|
|
765
|
+
"--end-of-options",
|
|
766
|
+
sha,
|
|
767
|
+
]);
|
|
768
|
+
const changes = parseNameStatus(output).filter((change) =>
|
|
769
|
+
isIncludedReviewPath(change.newPath ?? change.oldPath ?? ""),
|
|
770
|
+
);
|
|
771
|
+
return changes
|
|
772
|
+
.map((change): ReviewFile => {
|
|
773
|
+
const comparison = toComparison(change);
|
|
774
|
+
return toReviewFile(change, {
|
|
775
|
+
id: buildCommitFileId(sha, comparison),
|
|
776
|
+
worktreeStatus: null,
|
|
777
|
+
hasWorkingTreeFile: false,
|
|
778
|
+
});
|
|
779
|
+
})
|
|
780
|
+
.sort(compareReviewFiles);
|
|
725
781
|
}
|
|
726
782
|
|
|
727
783
|
export async function loadReviewFileContents(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
784
|
+
pi: ExtensionAPI,
|
|
785
|
+
repoRoot: string,
|
|
786
|
+
file: ReviewFile,
|
|
787
|
+
scope: ReviewScope,
|
|
788
|
+
commitSha: string | null = null,
|
|
789
|
+
branchMergeBaseSha: string | null = null,
|
|
734
790
|
): 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
|
-
|
|
791
|
+
const emptyBinaryContents: ReviewFileContents = {
|
|
792
|
+
originalContent: "",
|
|
793
|
+
modifiedContent: "",
|
|
794
|
+
kind: file.kind,
|
|
795
|
+
mimeType: file.mimeType,
|
|
796
|
+
originalExists: false,
|
|
797
|
+
modifiedExists: false,
|
|
798
|
+
originalPreviewUrl: null,
|
|
799
|
+
modifiedPreviewUrl: null,
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
if (file.kind !== "text") {
|
|
803
|
+
if (scope === "all") {
|
|
804
|
+
const path = file.path;
|
|
805
|
+
const modifiedSide = file.hasWorkingTreeFile
|
|
806
|
+
? await loadBinarySideFromWorkingTree(repoRoot, path, file.mimeType)
|
|
807
|
+
: await loadBinarySideFromRevision(pi, repoRoot, "HEAD", path, file.mimeType);
|
|
808
|
+
return {
|
|
809
|
+
...emptyBinaryContents,
|
|
810
|
+
modifiedExists: modifiedSide.exists,
|
|
811
|
+
modifiedPreviewUrl: modifiedSide.previewUrl,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const comparison = file.gitDiff;
|
|
816
|
+
if (comparison == null) return emptyBinaryContents;
|
|
817
|
+
|
|
818
|
+
if (scope === "commits") {
|
|
819
|
+
if (!commitSha) return emptyBinaryContents;
|
|
820
|
+
if (isWorkingTreeCommitSha(commitSha)) {
|
|
821
|
+
const repositoryHasHead = await hasHead(pi, repoRoot);
|
|
822
|
+
const originalSide = repositoryHasHead
|
|
823
|
+
? await loadBinarySideFromRevision(
|
|
824
|
+
pi,
|
|
825
|
+
repoRoot,
|
|
826
|
+
"HEAD",
|
|
827
|
+
comparison.oldPath,
|
|
828
|
+
file.mimeType,
|
|
829
|
+
)
|
|
830
|
+
: { exists: false, previewUrl: null };
|
|
831
|
+
const modifiedSide = file.hasWorkingTreeFile
|
|
832
|
+
? await loadBinarySideFromWorkingTree(repoRoot, comparison.newPath, file.mimeType)
|
|
833
|
+
: { exists: false, previewUrl: null };
|
|
834
|
+
return {
|
|
835
|
+
...emptyBinaryContents,
|
|
836
|
+
originalExists: originalSide.exists,
|
|
837
|
+
modifiedExists: modifiedSide.exists,
|
|
838
|
+
originalPreviewUrl: originalSide.previewUrl,
|
|
839
|
+
modifiedPreviewUrl: modifiedSide.previewUrl,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const originalSide = await loadBinarySideFromRevision(
|
|
843
|
+
pi,
|
|
844
|
+
repoRoot,
|
|
845
|
+
`${commitSha}^`,
|
|
846
|
+
comparison.oldPath,
|
|
847
|
+
file.mimeType,
|
|
848
|
+
);
|
|
849
|
+
const modifiedSide = await loadBinarySideFromRevision(
|
|
850
|
+
pi,
|
|
851
|
+
repoRoot,
|
|
852
|
+
commitSha,
|
|
853
|
+
comparison.newPath,
|
|
854
|
+
file.mimeType,
|
|
855
|
+
);
|
|
856
|
+
return {
|
|
857
|
+
...emptyBinaryContents,
|
|
858
|
+
originalExists: originalSide.exists,
|
|
859
|
+
modifiedExists: modifiedSide.exists,
|
|
860
|
+
originalPreviewUrl: originalSide.previewUrl,
|
|
861
|
+
modifiedPreviewUrl: modifiedSide.previewUrl,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (!branchMergeBaseSha) return emptyBinaryContents;
|
|
866
|
+
const originalSide = await loadBinarySideFromRevision(
|
|
867
|
+
pi,
|
|
868
|
+
repoRoot,
|
|
869
|
+
branchMergeBaseSha,
|
|
870
|
+
comparison.oldPath,
|
|
871
|
+
file.mimeType,
|
|
872
|
+
);
|
|
873
|
+
const modifiedSide = file.hasWorkingTreeFile
|
|
874
|
+
? await loadBinarySideFromWorkingTree(repoRoot, comparison.newPath, file.mimeType)
|
|
875
|
+
: await loadBinarySideFromRevision(pi, repoRoot, "HEAD", comparison.newPath, file.mimeType);
|
|
876
|
+
return {
|
|
877
|
+
...emptyBinaryContents,
|
|
878
|
+
originalExists: originalSide.exists,
|
|
879
|
+
modifiedExists: modifiedSide.exists,
|
|
880
|
+
originalPreviewUrl: originalSide.previewUrl,
|
|
881
|
+
modifiedPreviewUrl: modifiedSide.previewUrl,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (scope === "all") {
|
|
886
|
+
const path = file.path;
|
|
887
|
+
const workingTreeExists = file.hasWorkingTreeFile ? hasWorkingTreeFile(repoRoot, path) : false;
|
|
888
|
+
const content = file.hasWorkingTreeFile
|
|
889
|
+
? await getWorkingTreeContent(repoRoot, path)
|
|
890
|
+
: await getRevisionContent(pi, repoRoot, "HEAD", path);
|
|
891
|
+
const exists = file.hasWorkingTreeFile
|
|
892
|
+
? workingTreeExists
|
|
893
|
+
: content.length > 0 || path.length > 0;
|
|
894
|
+
return {
|
|
895
|
+
originalContent: content,
|
|
896
|
+
modifiedContent: content,
|
|
897
|
+
kind: file.kind,
|
|
898
|
+
mimeType: file.mimeType,
|
|
899
|
+
originalExists: exists,
|
|
900
|
+
modifiedExists: exists,
|
|
901
|
+
originalPreviewUrl: null,
|
|
902
|
+
modifiedPreviewUrl: null,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const comparison = file.gitDiff;
|
|
907
|
+
if (comparison == null) {
|
|
908
|
+
return {
|
|
909
|
+
originalContent: "",
|
|
910
|
+
modifiedContent: "",
|
|
911
|
+
kind: file.kind,
|
|
912
|
+
mimeType: file.mimeType,
|
|
913
|
+
originalExists: false,
|
|
914
|
+
modifiedExists: false,
|
|
915
|
+
originalPreviewUrl: null,
|
|
916
|
+
modifiedPreviewUrl: null,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (scope === "commits") {
|
|
921
|
+
if (!commitSha) {
|
|
922
|
+
return {
|
|
923
|
+
originalContent: "",
|
|
924
|
+
modifiedContent: "",
|
|
925
|
+
kind: file.kind,
|
|
926
|
+
mimeType: file.mimeType,
|
|
927
|
+
originalExists: false,
|
|
928
|
+
modifiedExists: false,
|
|
929
|
+
originalPreviewUrl: null,
|
|
930
|
+
modifiedPreviewUrl: null,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
if (isWorkingTreeCommitSha(commitSha)) {
|
|
934
|
+
const repositoryHasHead = await hasHead(pi, repoRoot);
|
|
935
|
+
const originalContent =
|
|
936
|
+
repositoryHasHead && comparison.oldPath != null
|
|
937
|
+
? await getRevisionContent(pi, repoRoot, "HEAD", comparison.oldPath)
|
|
938
|
+
: "";
|
|
939
|
+
const modifiedExists =
|
|
940
|
+
comparison.newPath != null && file.hasWorkingTreeFile
|
|
941
|
+
? hasWorkingTreeFile(repoRoot, comparison.newPath)
|
|
942
|
+
: false;
|
|
943
|
+
const modifiedContent =
|
|
944
|
+
comparison.newPath == null
|
|
945
|
+
? ""
|
|
946
|
+
: file.hasWorkingTreeFile
|
|
947
|
+
? await getWorkingTreeContent(repoRoot, comparison.newPath)
|
|
948
|
+
: "";
|
|
949
|
+
return {
|
|
950
|
+
originalContent,
|
|
951
|
+
modifiedContent,
|
|
952
|
+
kind: file.kind,
|
|
953
|
+
mimeType: file.mimeType,
|
|
954
|
+
originalExists: repositoryHasHead && comparison.oldPath != null,
|
|
955
|
+
modifiedExists,
|
|
956
|
+
originalPreviewUrl: null,
|
|
957
|
+
modifiedPreviewUrl: null,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const originalContent =
|
|
961
|
+
comparison.oldPath == null
|
|
962
|
+
? ""
|
|
963
|
+
: await getRevisionContent(pi, repoRoot, `${commitSha}^`, comparison.oldPath);
|
|
964
|
+
const modifiedContent =
|
|
965
|
+
comparison.newPath == null
|
|
966
|
+
? ""
|
|
967
|
+
: await getRevisionContent(pi, repoRoot, commitSha, comparison.newPath);
|
|
968
|
+
return {
|
|
969
|
+
originalContent,
|
|
970
|
+
modifiedContent,
|
|
971
|
+
kind: file.kind,
|
|
972
|
+
mimeType: file.mimeType,
|
|
973
|
+
originalExists: comparison.oldPath != null,
|
|
974
|
+
modifiedExists: comparison.newPath != null,
|
|
975
|
+
originalPreviewUrl: null,
|
|
976
|
+
modifiedPreviewUrl: null,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (!branchMergeBaseSha) {
|
|
981
|
+
return {
|
|
982
|
+
originalContent: "",
|
|
983
|
+
modifiedContent: "",
|
|
984
|
+
kind: file.kind,
|
|
985
|
+
mimeType: file.mimeType,
|
|
986
|
+
originalExists: false,
|
|
987
|
+
modifiedExists: false,
|
|
988
|
+
originalPreviewUrl: null,
|
|
989
|
+
modifiedPreviewUrl: null,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const originalContent =
|
|
994
|
+
comparison.oldPath == null
|
|
995
|
+
? ""
|
|
996
|
+
: await getRevisionContent(pi, repoRoot, branchMergeBaseSha, comparison.oldPath);
|
|
997
|
+
const modifiedExists =
|
|
998
|
+
comparison.newPath != null && file.hasWorkingTreeFile
|
|
999
|
+
? hasWorkingTreeFile(repoRoot, comparison.newPath)
|
|
1000
|
+
: false;
|
|
1001
|
+
const modifiedContent =
|
|
1002
|
+
comparison.newPath == null
|
|
1003
|
+
? ""
|
|
1004
|
+
: file.hasWorkingTreeFile
|
|
1005
|
+
? await getWorkingTreeContent(repoRoot, comparison.newPath)
|
|
1006
|
+
: await getRevisionContent(pi, repoRoot, "HEAD", comparison.newPath);
|
|
1007
|
+
return {
|
|
1008
|
+
originalContent,
|
|
1009
|
+
modifiedContent,
|
|
1010
|
+
kind: file.kind,
|
|
1011
|
+
mimeType: file.mimeType,
|
|
1012
|
+
originalExists: comparison.oldPath != null,
|
|
1013
|
+
modifiedExists,
|
|
1014
|
+
originalPreviewUrl: null,
|
|
1015
|
+
modifiedPreviewUrl: null,
|
|
1016
|
+
};
|
|
939
1017
|
}
|
|
940
1018
|
|
|
941
1019
|
export const __testing = {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1020
|
+
parseNameStatus,
|
|
1021
|
+
parseStatusPorcelainZ,
|
|
1022
|
+
shouldNormalizeBranchChanges,
|
|
945
1023
|
};
|