@monotykamary/localterm-server 2.0.5 → 2.1.1
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/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -1
- package/dist/git-diff.d.ts.map +1 -1
- package/dist/git-diff.js +420 -475
- package/dist/git-diff.js.map +1 -1
- package/dist/utils/run-git.d.ts +7 -0
- package/dist/utils/run-git.d.ts.map +1 -0
- package/dist/utils/run-git.js +44 -0
- package/dist/utils/run-git.js.map +1 -0
- package/package.json +1 -3
- package/dist/cdp/open-background-tab.d.ts +0 -33
- package/dist/cdp/open-background-tab.d.ts.map +0 -1
- package/dist/cdp/open-background-tab.js +0 -98
- package/dist/cdp/open-background-tab.js.map +0 -1
- package/dist/finding-store.d.ts +0 -21
- package/dist/finding-store.d.ts.map +0 -1
- package/dist/finding-store.js +0 -155
- package/dist/finding-store.js.map +0 -1
- package/dist/utils/compute-patch.d.ts +0 -7
- package/dist/utils/compute-patch.d.ts.map +0 -1
- package/dist/utils/compute-patch.js +0 -74
- package/dist/utils/compute-patch.js.map +0 -1
- package/dist/utils/parse-osc-finding.d.ts +0 -7
- package/dist/utils/parse-osc-finding.d.ts.map +0 -1
- package/dist/utils/parse-osc-finding.js +0 -55
- package/dist/utils/parse-osc-finding.js.map +0 -1
- package/dist/utils/reconcile-file-stats.d.ts +0 -7
- package/dist/utils/reconcile-file-stats.d.ts.map +0 -1
- package/dist/utils/reconcile-file-stats.js +0 -45
- package/dist/utils/reconcile-file-stats.js.map +0 -1
- package/dist/utils/resolve-cwd-for-pid.d.ts +0 -2
- package/dist/utils/resolve-cwd-for-pid.d.ts.map +0 -1
- package/dist/utils/resolve-cwd-for-pid.js +0 -29
- package/dist/utils/resolve-cwd-for-pid.js.map +0 -1
- package/dist/utils/rewrite-kitty-file-transmission.d.ts +0 -2
- package/dist/utils/rewrite-kitty-file-transmission.d.ts.map +0 -1
- package/dist/utils/rewrite-kitty-file-transmission.js +0 -147
- package/dist/utils/rewrite-kitty-file-transmission.js.map +0 -1
package/dist/git-diff.js
CHANGED
|
@@ -2,10 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import fsPromises from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Octokit } from "@octokit/rest";
|
|
5
|
-
import {
|
|
6
|
-
import { memoBy } from "./utils/memo-by.js";
|
|
7
|
-
import { computePatchFromContents } from "./utils/compute-patch.js";
|
|
5
|
+
import { runGit } from "./utils/run-git.js";
|
|
8
6
|
import { resolveGithubToken } from "./utils/resolve-github-token.js";
|
|
7
|
+
import { memoBy } from "./utils/memo-by.js";
|
|
9
8
|
import { GIT_BINARY_SNIFF_BYTES, GIT_CACHE_TTL_MS, GIT_EMPTY_TREE_HASH, GIT_MAX_BRANCHES, GIT_MAX_PATCH_BYTES_PER_FILE, GIT_MAX_TOTAL_PATCH_BYTES, GIT_MAX_UNTRACKED_FILE_BYTES, GIT_MAX_UNTRACKED_FILES, } from "./constants.js";
|
|
10
9
|
const WORKING_OPTIONS = { mode: "working" };
|
|
11
10
|
const EMPTY_SUMMARY = {
|
|
@@ -16,7 +15,6 @@ const EMPTY_SUMMARY = {
|
|
|
16
15
|
binaries: 0,
|
|
17
16
|
branch: null,
|
|
18
17
|
};
|
|
19
|
-
const ZERO_OID = "0000000000000000000000000000000000000000";
|
|
20
18
|
// Nested so a cwd can hold more than one comparison (the working-tree summary
|
|
21
19
|
// is pushed on git-dirty while the viewer may be open in branch mode).
|
|
22
20
|
const diffCacheByCwd = new Map();
|
|
@@ -47,128 +45,73 @@ const writeDiffCache = (cwd, mode, base, cache) => {
|
|
|
47
45
|
export const invalidateGitDiffCache = (cwd) => {
|
|
48
46
|
diffCacheByCwd.delete(cwd);
|
|
49
47
|
};
|
|
50
|
-
const
|
|
51
|
-
const result =
|
|
52
|
-
|
|
53
|
-
result.push(item);
|
|
54
|
-
return result;
|
|
55
|
-
};
|
|
56
|
-
const deltaTypeToStatus = (delta) => {
|
|
57
|
-
switch (delta) {
|
|
58
|
-
case "Added":
|
|
59
|
-
return "added";
|
|
60
|
-
case "Deleted":
|
|
61
|
-
return "deleted";
|
|
62
|
-
case "Renamed":
|
|
63
|
-
return "renamed";
|
|
64
|
-
case "Copied":
|
|
65
|
-
return "added";
|
|
66
|
-
case "Untracked":
|
|
67
|
-
return "untracked";
|
|
68
|
-
case "Modified":
|
|
69
|
-
case "Typechange":
|
|
70
|
-
default:
|
|
71
|
-
return "modified";
|
|
72
|
-
}
|
|
48
|
+
const runGitText = async (cwd, args) => {
|
|
49
|
+
const result = await runGit(cwd, args);
|
|
50
|
+
return result.stdout.toString("utf8");
|
|
73
51
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
52
|
+
// `git diff` flags shared by every diff invocation so numstat, name-status and
|
|
53
|
+
// patch all walk the same diff queue (same rename detection, same file order).
|
|
54
|
+
const DIFF_RENAME_FLAG = "--find-renames";
|
|
55
|
+
const isGitRepo = async (cwd) => {
|
|
56
|
+
const result = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
57
|
+
return result.exitCode === 0;
|
|
82
58
|
};
|
|
83
|
-
const getCurrentBranch = (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const shorthand = head.shorthand();
|
|
87
|
-
return shorthand && shorthand !== "HEAD" ? shorthand : null;
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
59
|
+
const getCurrentBranch = async (cwd) => {
|
|
60
|
+
const result = await runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
61
|
+
if (result.exitCode !== 0)
|
|
90
62
|
return null;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// stale origin/HEAD
|
|
116
|
-
}
|
|
63
|
+
const name = result.stdout.toString("utf8").trim();
|
|
64
|
+
return name === "HEAD" ? null : name;
|
|
65
|
+
};
|
|
66
|
+
// working mode compares against HEAD (or the empty tree when the branch is
|
|
67
|
+
// unborn). branch mode compares against the merge-base of HEAD and the base
|
|
68
|
+
// ref, so only changes since the branches diverged are shown.
|
|
69
|
+
const resolveDiffBaseRef = async (cwd) => {
|
|
70
|
+
const result = await runGit(cwd, ["rev-parse", "--verify", "-q", "HEAD"]);
|
|
71
|
+
return result.exitCode === 0 ? "HEAD" : GIT_EMPTY_TREE_HASH;
|
|
72
|
+
};
|
|
73
|
+
const verifyRef = async (cwd, ref) => {
|
|
74
|
+
const result = await runGit(cwd, ["rev-parse", "--verify", "-q", ref]);
|
|
75
|
+
return result.exitCode === 0;
|
|
76
|
+
};
|
|
77
|
+
const resolveDefaultBase = async (cwd) => {
|
|
78
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
79
|
+
const symbolic = await runGit(cwd, ["symbolic-ref", "-q", "refs/remotes/origin/HEAD"]);
|
|
80
|
+
if (symbolic.exitCode === 0) {
|
|
81
|
+
const target = symbolic.stdout.toString("utf8").trim();
|
|
82
|
+
if (target.startsWith("refs/remotes/")) {
|
|
83
|
+
const shortName = target.slice("refs/remotes/".length);
|
|
84
|
+
if (shortName !== currentBranch && (await verifyRef(cwd, shortName))) {
|
|
85
|
+
return { ref: shortName, source: "remoteHead" };
|
|
117
86
|
}
|
|
118
87
|
}
|
|
119
88
|
}
|
|
120
|
-
catch {
|
|
121
|
-
// No origin/HEAD configured
|
|
122
|
-
}
|
|
123
89
|
for (const name of ["main", "master", "develop"]) {
|
|
124
90
|
if (name === currentBranch)
|
|
125
91
|
continue;
|
|
126
92
|
for (const candidate of [`origin/${name}`, name]) {
|
|
127
|
-
|
|
128
|
-
r.repo.revparseSingle(candidate);
|
|
93
|
+
if (await verifyRef(cwd, candidate))
|
|
129
94
|
return { ref: candidate, source: "fallback" };
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
95
|
}
|
|
135
96
|
}
|
|
136
97
|
return null;
|
|
137
98
|
};
|
|
138
|
-
const resolveEffectiveBaseRef = (
|
|
99
|
+
const resolveEffectiveBaseRef = async (cwd, options) => {
|
|
139
100
|
if (options.mode !== "branch")
|
|
140
|
-
return resolveDiffBaseRef(
|
|
101
|
+
return resolveDiffBaseRef(cwd);
|
|
141
102
|
let baseRef = options.base?.trim() || null;
|
|
142
|
-
if (baseRef)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
catch {
|
|
147
|
-
baseRef = null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (!baseRef) {
|
|
151
|
-
const resolved = resolveDefaultBase(r);
|
|
152
|
-
baseRef = resolved?.ref ?? null;
|
|
153
|
-
}
|
|
103
|
+
if (baseRef && !(await verifyRef(cwd, baseRef)))
|
|
104
|
+
baseRef = null;
|
|
105
|
+
if (!baseRef)
|
|
106
|
+
baseRef = (await resolveDefaultBase(cwd))?.ref ?? null;
|
|
154
107
|
if (!baseRef)
|
|
155
108
|
return null;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (mergeBase)
|
|
161
|
-
return mergeBase;
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// Unrelated histories
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
return r.repo.revparseSingle(baseRef);
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
109
|
+
const mergeBase = await runGit(cwd, ["merge-base", baseRef, "HEAD"]);
|
|
110
|
+
if (mergeBase.exitCode === 0)
|
|
111
|
+
return mergeBase.stdout.toString("utf8").trim();
|
|
112
|
+
if (!(await verifyRef(cwd, baseRef)))
|
|
170
113
|
return null;
|
|
171
|
-
|
|
114
|
+
return runGitText(cwd, ["rev-parse", "--verify", baseRef]);
|
|
172
115
|
};
|
|
173
116
|
const countLines = (text) => {
|
|
174
117
|
if (text.length === 0)
|
|
@@ -182,42 +125,45 @@ const countLines = (text) => {
|
|
|
182
125
|
count += 1;
|
|
183
126
|
return count;
|
|
184
127
|
};
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
break;
|
|
195
|
-
const filePath = entry.path();
|
|
196
|
-
const absolutePath = path.join(r.cwd, filePath);
|
|
128
|
+
const readUntrackedFile = async (cwd, filePath) => {
|
|
129
|
+
const absolutePath = path.join(cwd, filePath);
|
|
130
|
+
try {
|
|
131
|
+
const stat = await fsPromises.stat(absolutePath);
|
|
132
|
+
if (!stat.isFile())
|
|
133
|
+
return null;
|
|
134
|
+
const bytesToRead = Math.min(stat.size, GIT_MAX_UNTRACKED_FILE_BYTES);
|
|
135
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
136
|
+
const handle = await fsPromises.open(absolutePath, "r");
|
|
197
137
|
try {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
const { buffer: readBuffer } = await handle.read(buffer, 0, bytesToRead, 0);
|
|
206
|
-
const sniffEnd = Math.min(readBuffer.length, GIT_BINARY_SNIFF_BYTES);
|
|
207
|
-
const binary = readBuffer.subarray(0, sniffEnd).includes(0);
|
|
208
|
-
const truncated = stat.size > GIT_MAX_UNTRACKED_FILE_BYTES;
|
|
209
|
-
const content = binary ? null : truncated ? null : readBuffer.toString("utf8");
|
|
210
|
-
const lines = binary ? 0 : content ? countLines(content) : 0;
|
|
211
|
-
files.push({ path: filePath, binary, lines, content, truncated });
|
|
212
|
-
}
|
|
213
|
-
finally {
|
|
214
|
-
await handle.close();
|
|
215
|
-
}
|
|
138
|
+
const { buffer: readBuffer } = await handle.read(buffer, 0, bytesToRead, 0);
|
|
139
|
+
const sniffEnd = Math.min(readBuffer.length, GIT_BINARY_SNIFF_BYTES);
|
|
140
|
+
const binary = readBuffer.subarray(0, sniffEnd).includes(0);
|
|
141
|
+
const truncated = stat.size > GIT_MAX_UNTRACKED_FILE_BYTES;
|
|
142
|
+
const content = binary ? null : truncated ? null : readBuffer.toString("utf8");
|
|
143
|
+
const lines = binary ? 0 : content ? countLines(content) : 0;
|
|
144
|
+
return { path: filePath, binary, lines, content, truncated };
|
|
216
145
|
}
|
|
217
|
-
|
|
218
|
-
|
|
146
|
+
finally {
|
|
147
|
+
await handle.close();
|
|
219
148
|
}
|
|
220
149
|
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const collectUntrackedFiles = async (cwd) => {
|
|
155
|
+
const result = await runGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
156
|
+
if (result.exitCode !== 0)
|
|
157
|
+
return [];
|
|
158
|
+
const paths = result.stdout.toString("utf8").split("\0").filter(Boolean);
|
|
159
|
+
const files = [];
|
|
160
|
+
for (const filePath of paths) {
|
|
161
|
+
if (files.length >= GIT_MAX_UNTRACKED_FILES)
|
|
162
|
+
break;
|
|
163
|
+
const file = await readUntrackedFile(cwd, filePath);
|
|
164
|
+
if (file)
|
|
165
|
+
files.push(file);
|
|
166
|
+
}
|
|
221
167
|
return files;
|
|
222
168
|
};
|
|
223
169
|
export const buildUntrackedPatch = (content) => {
|
|
@@ -231,138 +177,216 @@ export const buildUntrackedPatch = (content) => {
|
|
|
231
177
|
const noNewlineMarker = hasTrailingNewline ? "" : "\n\";
|
|
232
178
|
return `@@ -0,0 +1,${lines.length} @@\n${body}${noNewlineMarker}\n`;
|
|
233
179
|
};
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const blob = obj.peelToBlob();
|
|
242
|
-
return Buffer.from(blob.content()).toString("utf8");
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
const readWorkingTreeFile = (r, filePath, maxBytes = GIT_MAX_UNTRACKED_FILE_BYTES) => {
|
|
249
|
-
const absolutePath = path.join(r.cwd, filePath);
|
|
250
|
-
try {
|
|
251
|
-
const stat = fs.statSync(absolutePath);
|
|
252
|
-
if (!stat.isFile())
|
|
253
|
-
return null;
|
|
254
|
-
const bytesToRead = Math.min(stat.size, maxBytes);
|
|
255
|
-
const buffer = Buffer.alloc(bytesToRead);
|
|
256
|
-
const handle = fs.openSync(absolutePath, "r");
|
|
257
|
-
fs.readSync(handle, buffer, 0, bytesToRead, 0);
|
|
258
|
-
fs.closeSync(handle);
|
|
259
|
-
const sniffEnd = Math.min(buffer.length, GIT_BINARY_SNIFF_BYTES);
|
|
260
|
-
if (buffer.subarray(0, sniffEnd).includes(0))
|
|
261
|
-
return null;
|
|
262
|
-
if (stat.size > maxBytes)
|
|
263
|
-
return null;
|
|
264
|
-
return buffer.toString("utf8");
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
const collectDeltaInfos = (diff) => {
|
|
271
|
-
const deltas = collectIterator(diff.deltas());
|
|
272
|
-
const result = [];
|
|
273
|
-
for (const delta of deltas) {
|
|
274
|
-
const status = deltaTypeToStatus(delta.status());
|
|
275
|
-
if (status === "untracked")
|
|
180
|
+
export const splitPatchByFile = (raw) => raw.split(/^(?=diff --git )/m).filter((chunk) => chunk.startsWith("diff --git "));
|
|
181
|
+
export const parseNumstatZ = (raw) => {
|
|
182
|
+
const tokens = raw.split("\0");
|
|
183
|
+
const entries = [];
|
|
184
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
185
|
+
const token = tokens[index];
|
|
186
|
+
if (!token)
|
|
276
187
|
continue;
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
188
|
+
const match = /^(\d+|-)\t(\d+|-)\t(.*)$/s.exec(token);
|
|
189
|
+
if (!match)
|
|
190
|
+
continue;
|
|
191
|
+
const binary = match[1] === "-" || match[2] === "-";
|
|
192
|
+
const additions = binary ? 0 : Number.parseInt(match[1], 10);
|
|
193
|
+
const deletions = binary ? 0 : Number.parseInt(match[2], 10);
|
|
194
|
+
if (match[3]) {
|
|
195
|
+
entries.push({ path: match[3], oldPath: null, additions, deletions, binary });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const oldPath = tokens[index + 1];
|
|
199
|
+
const newPath = tokens[index + 2];
|
|
200
|
+
index += 2;
|
|
201
|
+
if (!oldPath || !newPath)
|
|
202
|
+
continue;
|
|
203
|
+
entries.push({ path: newPath, oldPath, additions, deletions, binary });
|
|
287
204
|
}
|
|
288
|
-
return
|
|
205
|
+
return entries;
|
|
289
206
|
};
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
207
|
+
export const parseNameStatusZ = (raw) => {
|
|
208
|
+
const tokens = raw.split("\0");
|
|
209
|
+
const statuses = new Map();
|
|
210
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
211
|
+
const statusToken = tokens[index];
|
|
212
|
+
if (!statusToken)
|
|
213
|
+
continue;
|
|
214
|
+
const letter = statusToken[0];
|
|
215
|
+
if (letter === "R" || letter === "C") {
|
|
216
|
+
const newPath = tokens[index + 2];
|
|
217
|
+
index += 2;
|
|
218
|
+
if (!newPath)
|
|
219
|
+
continue;
|
|
220
|
+
statuses.set(newPath, letter === "R" ? "renamed" : "added");
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const filePath = tokens[index + 1];
|
|
224
|
+
index += 1;
|
|
225
|
+
if (!filePath)
|
|
226
|
+
continue;
|
|
227
|
+
if (letter === "A")
|
|
228
|
+
statuses.set(filePath, "added");
|
|
229
|
+
else if (letter === "D")
|
|
230
|
+
statuses.set(filePath, "deleted");
|
|
231
|
+
else
|
|
232
|
+
statuses.set(filePath, "modified");
|
|
313
233
|
}
|
|
234
|
+
return statuses;
|
|
314
235
|
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
236
|
+
// Undo git's C-style path quoting (paths with spaces/special chars get wrapped
|
|
237
|
+
// in "..." with \-escapes; core.quotepath=false leaves only those, not non-ASCII).
|
|
238
|
+
const unquoteGitPath = (raw) => {
|
|
239
|
+
if (raw.length < 2 || raw[0] !== '"' || raw[raw.length - 1] !== '"')
|
|
240
|
+
return raw;
|
|
241
|
+
let out = "";
|
|
242
|
+
let index = 1;
|
|
243
|
+
const end = raw.length - 1;
|
|
244
|
+
while (index < end) {
|
|
245
|
+
const char = raw[index];
|
|
246
|
+
if (char === "\\" && index + 1 < end) {
|
|
247
|
+
const next = raw[index + 1];
|
|
248
|
+
if (next === "n") {
|
|
249
|
+
out += "\n";
|
|
250
|
+
index += 2;
|
|
251
|
+
}
|
|
252
|
+
else if (next === "t") {
|
|
253
|
+
out += "\t";
|
|
254
|
+
index += 2;
|
|
255
|
+
}
|
|
256
|
+
else if (next === '"') {
|
|
257
|
+
out += '"';
|
|
258
|
+
index += 2;
|
|
259
|
+
}
|
|
260
|
+
else if (next === "\\") {
|
|
261
|
+
out += "\\";
|
|
262
|
+
index += 2;
|
|
263
|
+
}
|
|
264
|
+
else if (next >= "0" && next <= "7" && index + 3 < end) {
|
|
265
|
+
out += String.fromCharCode(Number.parseInt(raw.slice(index + 1, index + 4), 8));
|
|
266
|
+
index += 4;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
out += char;
|
|
270
|
+
index += 1;
|
|
327
271
|
}
|
|
328
|
-
const commit = r.repo.getCommit(baseOid);
|
|
329
|
-
return commit.tree();
|
|
330
272
|
}
|
|
331
|
-
|
|
332
|
-
|
|
273
|
+
else {
|
|
274
|
+
out += char;
|
|
275
|
+
index += 1;
|
|
333
276
|
}
|
|
334
277
|
}
|
|
278
|
+
return out;
|
|
335
279
|
};
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// large branch diff never blocks the WS terminal during this one pass.
|
|
342
|
-
const buildDiffCache = async (r, baseRef) => {
|
|
343
|
-
const baseTree = buildBaseTree(r, baseRef);
|
|
344
|
-
if (!baseTree)
|
|
280
|
+
// Strip the `+++ ` / `--- ` prefix and the `b/` / `a/` namespace, unquoting if
|
|
281
|
+
// git wrapped the path. `/dev/null` (a deletion's +++ side) yields null so the
|
|
282
|
+
// caller can fall back to the --- side.
|
|
283
|
+
const pathFromLine = (line, prefix) => {
|
|
284
|
+
if (!line.startsWith(prefix))
|
|
345
285
|
return null;
|
|
346
|
-
let
|
|
347
|
-
|
|
348
|
-
diff = r.repo.diffTreeToWorkdirWithIndex(baseTree);
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
286
|
+
let rest = line.slice(prefix.length);
|
|
287
|
+
if (rest === "/dev/null")
|
|
351
288
|
return null;
|
|
289
|
+
rest = unquoteGitPath(rest);
|
|
290
|
+
// git appends a literal tab after an unquoted path in ---/+++ lines when the
|
|
291
|
+
// path contains a space, to keep it from running into hunk content; the
|
|
292
|
+
// closing quote already disambiguates quoted paths, so this only affects
|
|
293
|
+
// the unquoted branch. Numstat/name-status never carry that tab, so strip it
|
|
294
|
+
// or the path keys won't line up.
|
|
295
|
+
if (rest.endsWith("\t"))
|
|
296
|
+
rest = rest.slice(0, -1);
|
|
297
|
+
if (rest.startsWith("b/"))
|
|
298
|
+
return rest.slice(2);
|
|
299
|
+
if (rest.startsWith("a/"))
|
|
300
|
+
return rest.slice(2);
|
|
301
|
+
return rest;
|
|
302
|
+
};
|
|
303
|
+
// Pull the new-side path out of one `diff --git` chunk: prefer `+++ b/<path>`
|
|
304
|
+
// (added/modified/rename target), fall back to `--- a/<path>` for deletions
|
|
305
|
+
// (whose +++ is /dev/null), then to `rename to <path>` for a pure rename with
|
|
306
|
+
// no content change (which has no ---/+++ lines at all).
|
|
307
|
+
const extractPatchPath = (chunk) => {
|
|
308
|
+
const lines = chunk.split("\n");
|
|
309
|
+
for (const line of lines) {
|
|
310
|
+
if (line.startsWith("+++ ")) {
|
|
311
|
+
const fromNew = pathFromLine(line, "+++ ");
|
|
312
|
+
if (fromNew !== null)
|
|
313
|
+
return fromNew;
|
|
314
|
+
for (const fallback of lines) {
|
|
315
|
+
if (fallback.startsWith("--- ")) {
|
|
316
|
+
const fromOld = pathFromLine(fallback, "--- ");
|
|
317
|
+
if (fromOld !== null)
|
|
318
|
+
return fromOld;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
352
323
|
}
|
|
353
|
-
|
|
354
|
-
|
|
324
|
+
for (const line of lines) {
|
|
325
|
+
if (line.startsWith("rename to "))
|
|
326
|
+
return unquoteGitPath(line.slice("rename to ".length));
|
|
355
327
|
}
|
|
356
|
-
|
|
357
|
-
|
|
328
|
+
return null;
|
|
329
|
+
};
|
|
330
|
+
// Index `git diff --patch` output by path. A single path can map to several
|
|
331
|
+
// `diff --git` blocks (a symlink re-added as a regular file emits a deletion +
|
|
332
|
+
// an addition for one path), so those are concatenated back into one patch.
|
|
333
|
+
const indexPatchesByPath = (raw) => {
|
|
334
|
+
const chunksByPath = new Map();
|
|
335
|
+
for (const chunk of splitPatchByFile(raw)) {
|
|
336
|
+
const patchPath = extractPatchPath(chunk);
|
|
337
|
+
if (patchPath === null)
|
|
338
|
+
continue;
|
|
339
|
+
let chunks = chunksByPath.get(patchPath);
|
|
340
|
+
if (!chunks) {
|
|
341
|
+
chunks = [];
|
|
342
|
+
chunksByPath.set(patchPath, chunks);
|
|
343
|
+
}
|
|
344
|
+
chunks.push(chunk);
|
|
345
|
+
}
|
|
346
|
+
const result = new Map();
|
|
347
|
+
for (const [patchPath, chunks] of chunksByPath) {
|
|
348
|
+
result.set(patchPath, chunks.join(""));
|
|
358
349
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
350
|
+
return result;
|
|
351
|
+
};
|
|
352
|
+
// One full diff pass for `(cwd)` against `baseRef`. Three parallel `git diff`
|
|
353
|
+
// invocations (numstat for counts+binary, name-status for the status letter +
|
|
354
|
+
// rename old path, patch for the body) walk the same diff queue. numstat and
|
|
355
|
+
// name-status are NUL-delimited and unambiguous; they're the source of truth
|
|
356
|
+
// for the file list. The patch output is keyed by path rather than paired
|
|
357
|
+
// positionally, because a single numstat entry can span several `diff --git`
|
|
358
|
+
// blocks (a symlink is deleted as mode 120000 and re-added as a regular file:
|
|
359
|
+
// git emits that as a deletion + an addition sharing one path, so there's no
|
|
360
|
+
// 1:1 with numstat entries). Untracked files are folded in from `ls-files`
|
|
361
|
+
// with synthesized patches (git's own diff never lists untracked files).
|
|
362
|
+
const buildDiffCache = async (cwd, baseRef) => {
|
|
363
|
+
const [numstatRes, nameStatusRes, patchRes] = await Promise.all([
|
|
364
|
+
runGit(cwd, [
|
|
365
|
+
"-c",
|
|
366
|
+
"core.quotepath=false",
|
|
367
|
+
"diff",
|
|
368
|
+
DIFF_RENAME_FLAG,
|
|
369
|
+
"--numstat",
|
|
370
|
+
"-z",
|
|
371
|
+
baseRef,
|
|
372
|
+
]),
|
|
373
|
+
runGit(cwd, [
|
|
374
|
+
"-c",
|
|
375
|
+
"core.quotepath=false",
|
|
376
|
+
"diff",
|
|
377
|
+
DIFF_RENAME_FLAG,
|
|
378
|
+
"--name-status",
|
|
379
|
+
"-z",
|
|
380
|
+
baseRef,
|
|
381
|
+
]),
|
|
382
|
+
runGit(cwd, ["-c", "core.quotepath=false", "diff", DIFF_RENAME_FLAG, "--patch", baseRef]),
|
|
383
|
+
]);
|
|
384
|
+
if (numstatRes.exitCode !== 0 || patchRes.exitCode !== 0)
|
|
385
|
+
return null;
|
|
386
|
+
const numstat = parseNumstatZ(numstatRes.stdout.toString("utf8"));
|
|
387
|
+
const statuses = parseNameStatusZ(nameStatusRes.stdout.toString("utf8"));
|
|
388
|
+
const patchesByPath = indexPatchesByPath(patchRes.stdout.toString("utf8"));
|
|
389
|
+
const untracked = await collectUntrackedFiles(cwd);
|
|
366
390
|
const fileMeta = [];
|
|
367
391
|
const filePatchByPath = new Map();
|
|
368
392
|
const fileBinaryByPath = new Map();
|
|
@@ -371,52 +395,40 @@ const buildDiffCache = async (r, baseRef) => {
|
|
|
371
395
|
let additions = 0;
|
|
372
396
|
let deletions = 0;
|
|
373
397
|
let binaries = 0;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const delta = deltaInfos[index];
|
|
398
|
+
for (let index = 0; index < numstat.length; index += 1) {
|
|
399
|
+
const entry = numstat[index];
|
|
400
|
+
const status = statuses.get(entry.path) ?? "modified";
|
|
401
|
+
const oldPath = status === "renamed" ? entry.oldPath : null;
|
|
379
402
|
let patchText = null;
|
|
380
403
|
let patchOmitted = false;
|
|
381
|
-
|
|
382
|
-
let fileDeletions;
|
|
383
|
-
if (delta.binary) {
|
|
384
|
-
fileAdditions = 0;
|
|
385
|
-
fileDeletions = 0;
|
|
404
|
+
if (entry.binary) {
|
|
386
405
|
binaries += 1;
|
|
387
406
|
}
|
|
388
407
|
else {
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (patchText.length > GIT_MAX_PATCH_BYTES_PER_FILE ||
|
|
395
|
-
totalPatchBytes + patchText.length > GIT_MAX_TOTAL_PATCH_BYTES) {
|
|
396
|
-
patchText = null;
|
|
397
|
-
patchOmitted = true;
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
totalPatchBytes += patchText.length;
|
|
401
|
-
}
|
|
408
|
+
const rawPatch = patchesByPath.get(entry.path) ?? null;
|
|
409
|
+
if (rawPatch === null ||
|
|
410
|
+
rawPatch.length > GIT_MAX_PATCH_BYTES_PER_FILE ||
|
|
411
|
+
totalPatchBytes + rawPatch.length > GIT_MAX_TOTAL_PATCH_BYTES) {
|
|
412
|
+
patchOmitted = true;
|
|
402
413
|
}
|
|
403
414
|
else {
|
|
404
|
-
|
|
415
|
+
patchText = rawPatch;
|
|
416
|
+
totalPatchBytes += rawPatch.length;
|
|
405
417
|
}
|
|
418
|
+
additions += entry.additions;
|
|
419
|
+
deletions += entry.deletions;
|
|
406
420
|
}
|
|
407
|
-
additions += fileAdditions;
|
|
408
|
-
deletions += fileDeletions;
|
|
409
421
|
fileMeta.push({
|
|
410
|
-
path:
|
|
411
|
-
oldPath
|
|
412
|
-
status
|
|
413
|
-
additions:
|
|
414
|
-
deletions:
|
|
415
|
-
binary:
|
|
422
|
+
path: entry.path,
|
|
423
|
+
oldPath,
|
|
424
|
+
status,
|
|
425
|
+
additions: entry.additions,
|
|
426
|
+
deletions: entry.deletions,
|
|
427
|
+
binary: entry.binary,
|
|
416
428
|
});
|
|
417
|
-
filePatchByPath.set(
|
|
418
|
-
fileBinaryByPath.set(
|
|
419
|
-
filePatchOmittedByPath.set(
|
|
429
|
+
filePatchByPath.set(entry.path, patchText);
|
|
430
|
+
fileBinaryByPath.set(entry.path, entry.binary);
|
|
431
|
+
filePatchOmittedByPath.set(entry.path, patchOmitted);
|
|
420
432
|
}
|
|
421
433
|
for (const file of untracked) {
|
|
422
434
|
const patch = file.binary
|
|
@@ -456,7 +468,7 @@ const buildDiffCache = async (r, baseRef) => {
|
|
|
456
468
|
additions,
|
|
457
469
|
deletions,
|
|
458
470
|
binaries,
|
|
459
|
-
branch: getCurrentBranch(
|
|
471
|
+
branch: await getCurrentBranch(cwd),
|
|
460
472
|
};
|
|
461
473
|
return {
|
|
462
474
|
summary,
|
|
@@ -467,129 +479,64 @@ const buildDiffCache = async (r, baseRef) => {
|
|
|
467
479
|
builtAt: Date.now(),
|
|
468
480
|
};
|
|
469
481
|
};
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
// per-file patch endpoint (where the cache is warm on nearly every
|
|
477
|
-
// request) checking first keeps it a pure map lookup.
|
|
478
|
-
const cached = readDiffCache(r.cwd, options.mode, options.base ?? null);
|
|
482
|
+
const ensureDiffCache = async (cwd, options) => {
|
|
483
|
+
// Read the cache before resolving the base ref — that resolution does git work
|
|
484
|
+
// (rev-parse + merge-base) on every call, so for the per-file patch endpoint
|
|
485
|
+
// (where the cache is warm on nearly every request) checking first keeps it a
|
|
486
|
+
// pure map lookup with no subprocess.
|
|
487
|
+
const cached = readDiffCache(cwd, options.mode, options.base ?? null);
|
|
479
488
|
if (cached)
|
|
480
489
|
return cached;
|
|
481
|
-
const baseRef = resolveEffectiveBaseRef(
|
|
490
|
+
const baseRef = await resolveEffectiveBaseRef(cwd, options);
|
|
482
491
|
if (baseRef === null)
|
|
483
492
|
return null;
|
|
484
|
-
const cache = await buildDiffCache(
|
|
493
|
+
const cache = await buildDiffCache(cwd, baseRef);
|
|
485
494
|
if (cache)
|
|
486
|
-
writeDiffCache(
|
|
495
|
+
writeDiffCache(cwd, options.mode, options.base ?? null, cache);
|
|
487
496
|
return cache;
|
|
488
497
|
};
|
|
489
|
-
export const splitPatchByFile = (raw) => raw.split(/^(?=diff --git )/m).filter((chunk) => chunk.startsWith("diff --git "));
|
|
490
|
-
export const parseNumstatZ = (raw) => {
|
|
491
|
-
const tokens = raw.split("\0");
|
|
492
|
-
const entries = [];
|
|
493
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
494
|
-
const token = tokens[index];
|
|
495
|
-
if (!token)
|
|
496
|
-
continue;
|
|
497
|
-
const match = /^(\d+|-)\t(\d+|-)\t(.*)$/s.exec(token);
|
|
498
|
-
if (!match)
|
|
499
|
-
continue;
|
|
500
|
-
const binary = match[1] === "-" || match[2] === "-";
|
|
501
|
-
const additions = binary ? 0 : Number.parseInt(match[1], 10);
|
|
502
|
-
const deletions = binary ? 0 : Number.parseInt(match[2], 10);
|
|
503
|
-
if (match[3]) {
|
|
504
|
-
entries.push({ path: match[3], oldPath: null, additions, deletions, binary });
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
const oldPath = tokens[index + 1];
|
|
508
|
-
const newPath = tokens[index + 2];
|
|
509
|
-
index += 2;
|
|
510
|
-
if (!oldPath || !newPath)
|
|
511
|
-
continue;
|
|
512
|
-
entries.push({ path: newPath, oldPath, additions, deletions, binary });
|
|
513
|
-
}
|
|
514
|
-
return entries;
|
|
515
|
-
};
|
|
516
|
-
export const parseNameStatusZ = (raw) => {
|
|
517
|
-
const tokens = raw.split("\0");
|
|
518
|
-
const statuses = new Map();
|
|
519
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
520
|
-
const statusToken = tokens[index];
|
|
521
|
-
if (!statusToken)
|
|
522
|
-
continue;
|
|
523
|
-
const letter = statusToken[0];
|
|
524
|
-
if (letter === "R" || letter === "C") {
|
|
525
|
-
const newPath = tokens[index + 2];
|
|
526
|
-
index += 2;
|
|
527
|
-
if (!newPath)
|
|
528
|
-
continue;
|
|
529
|
-
statuses.set(newPath, letter === "R" ? "renamed" : "added");
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
const filePath = tokens[index + 1];
|
|
533
|
-
index += 1;
|
|
534
|
-
if (!filePath)
|
|
535
|
-
continue;
|
|
536
|
-
if (letter === "A")
|
|
537
|
-
statuses.set(filePath, "added");
|
|
538
|
-
else if (letter === "D")
|
|
539
|
-
statuses.set(filePath, "deleted");
|
|
540
|
-
else
|
|
541
|
-
statuses.set(filePath, "modified");
|
|
542
|
-
}
|
|
543
|
-
return statuses;
|
|
544
|
-
};
|
|
545
498
|
export const getGitDiffSummary = async (cwd, options = WORKING_OPTIONS) => {
|
|
546
|
-
const
|
|
547
|
-
if (!r)
|
|
548
|
-
return EMPTY_SUMMARY;
|
|
549
|
-
// Summary is pushed on every git-dirty signal (per-keystroke during edits),
|
|
550
|
-
// so it must stay cheap even when the full diff cache is cold. Read only the
|
|
551
|
-
// aggregate stats from the tree diff — no per-file jsdiff — unless a cache
|
|
552
|
-
// is already warm, in which case reuse its per-file summary.
|
|
553
|
-
const cached = readDiffCache(r.cwd, options.mode, options.base ?? null);
|
|
499
|
+
const cached = readDiffCache(cwd, options.mode, options.base ?? null);
|
|
554
500
|
if (cached)
|
|
555
501
|
return cached.summary;
|
|
502
|
+
if (!(await isGitRepo(cwd)))
|
|
503
|
+
return EMPTY_SUMMARY;
|
|
556
504
|
try {
|
|
557
|
-
const baseRef = resolveEffectiveBaseRef(
|
|
505
|
+
const baseRef = await resolveEffectiveBaseRef(cwd, options);
|
|
558
506
|
if (baseRef === null)
|
|
559
507
|
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
560
|
-
const branch = getCurrentBranch(
|
|
561
|
-
|
|
562
|
-
|
|
508
|
+
const branch = await getCurrentBranch(cwd);
|
|
509
|
+
// Summary is pushed on every git-dirty signal, so it stays on the cheap
|
|
510
|
+
// numstat-only path (no patch) unless a cache is already warm.
|
|
511
|
+
const numstatRes = await runGit(cwd, [
|
|
512
|
+
"-c",
|
|
513
|
+
"core.quotepath=false",
|
|
514
|
+
"diff",
|
|
515
|
+
DIFF_RENAME_FLAG,
|
|
516
|
+
"--numstat",
|
|
517
|
+
"-z",
|
|
518
|
+
baseRef,
|
|
519
|
+
]);
|
|
520
|
+
if (numstatRes.exitCode !== 0)
|
|
563
521
|
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
564
|
-
let
|
|
565
|
-
|
|
566
|
-
diff = r.repo.diffTreeToWorkdirWithIndex(baseTree);
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
569
|
-
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
570
|
-
}
|
|
571
|
-
try {
|
|
572
|
-
diff.findSimilar({ renames: true });
|
|
573
|
-
}
|
|
574
|
-
catch {
|
|
575
|
-
// Rename detection failed
|
|
576
|
-
}
|
|
577
|
-
const stats = diff.stats();
|
|
578
|
-
let additions = Number(stats.insertions);
|
|
579
|
-
let deletions = Number(stats.deletions);
|
|
522
|
+
let additions = 0;
|
|
523
|
+
let deletions = 0;
|
|
580
524
|
let binaries = 0;
|
|
581
|
-
let fileCount =
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
binaries
|
|
525
|
+
let fileCount = 0;
|
|
526
|
+
for (const entry of parseNumstatZ(numstatRes.stdout.toString("utf8"))) {
|
|
527
|
+
fileCount += 1;
|
|
528
|
+
if (entry.binary) {
|
|
529
|
+
binaries += 1;
|
|
530
|
+
continue;
|
|
586
531
|
}
|
|
532
|
+
additions += entry.additions;
|
|
533
|
+
deletions += entry.deletions;
|
|
587
534
|
}
|
|
588
|
-
const untracked = await collectUntrackedFiles(
|
|
535
|
+
const untracked = await collectUntrackedFiles(cwd);
|
|
589
536
|
for (const file of untracked) {
|
|
590
|
-
fileCount
|
|
537
|
+
fileCount += 1;
|
|
591
538
|
if (file.binary) {
|
|
592
|
-
binaries
|
|
539
|
+
binaries += 1;
|
|
593
540
|
}
|
|
594
541
|
else {
|
|
595
542
|
additions += file.lines;
|
|
@@ -602,39 +549,41 @@ export const getGitDiffSummary = async (cwd, options = WORKING_OPTIONS) => {
|
|
|
602
549
|
}
|
|
603
550
|
};
|
|
604
551
|
export const getGitDiff = async (cwd, options = WORKING_OPTIONS) => {
|
|
605
|
-
const
|
|
606
|
-
if (
|
|
552
|
+
const cached = readDiffCache(cwd, options.mode, options.base ?? null);
|
|
553
|
+
if (cached) {
|
|
554
|
+
return { isRepo: true, files: mapCacheFiles(cached) };
|
|
555
|
+
}
|
|
556
|
+
if (!(await isGitRepo(cwd)))
|
|
607
557
|
return { isRepo: false, files: [] };
|
|
608
|
-
const cache = await ensureDiffCache(
|
|
558
|
+
const cache = await ensureDiffCache(cwd, options);
|
|
609
559
|
if (!cache)
|
|
610
560
|
return { isRepo: true, files: [] };
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
};
|
|
561
|
+
return { isRepo: true, files: mapCacheFiles(cache) };
|
|
562
|
+
};
|
|
563
|
+
const mapCacheFiles = (cache) => cache.fileMeta.map((meta) => ({
|
|
564
|
+
path: meta.path,
|
|
565
|
+
oldPath: meta.oldPath,
|
|
566
|
+
status: meta.status,
|
|
567
|
+
additions: meta.additions,
|
|
568
|
+
deletions: meta.deletions,
|
|
569
|
+
binary: meta.binary,
|
|
570
|
+
patch: cache.filePatchByPath.get(meta.path) ?? null,
|
|
571
|
+
patchOmitted: cache.filePatchOmittedByPath.get(meta.path) ?? false,
|
|
572
|
+
}));
|
|
623
573
|
export const getGitDiffFiles = async (cwd, options = WORKING_OPTIONS) => {
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
574
|
+
const cached = readDiffCache(cwd, options.mode, options.base ?? null);
|
|
575
|
+
if (cached)
|
|
576
|
+
return { isRepo: true, files: cached.fileMeta };
|
|
577
|
+
if (!(await isGitRepo(cwd)))
|
|
626
578
|
return { isRepo: false, files: [] };
|
|
627
|
-
const cache = await ensureDiffCache(
|
|
579
|
+
const cache = await ensureDiffCache(cwd, options);
|
|
628
580
|
if (!cache)
|
|
629
581
|
return { isRepo: true, files: [] };
|
|
630
582
|
return { isRepo: true, files: cache.fileMeta };
|
|
631
583
|
};
|
|
632
584
|
export const getGitDiffFilePatch = async (cwd, requestedPath, options = WORKING_OPTIONS) => {
|
|
633
585
|
const empty = { patch: null, patchOmitted: false, binary: false };
|
|
634
|
-
const
|
|
635
|
-
if (!r)
|
|
636
|
-
return empty;
|
|
637
|
-
const cache = await ensureDiffCache(r, options);
|
|
586
|
+
const cache = await ensureDiffCache(cwd, options);
|
|
638
587
|
if (!cache)
|
|
639
588
|
return empty;
|
|
640
589
|
// O(1) lookup: the full diff pass that the per-file patch needs was already
|
|
@@ -650,11 +599,11 @@ export const getGitDiffFilePatch = async (cwd, requestedPath, options = WORKING_
|
|
|
650
599
|
}
|
|
651
600
|
// An untracked path the cache didn't cover (created between the cache build
|
|
652
601
|
// and this request) falls back to synthesizing from the working tree.
|
|
653
|
-
return getGitDiffFilePatchFromWorkingTree(
|
|
602
|
+
return getGitDiffFilePatchFromWorkingTree(cwd, requestedPath);
|
|
654
603
|
};
|
|
655
|
-
const getGitDiffFilePatchFromWorkingTree = async (
|
|
604
|
+
const getGitDiffFilePatchFromWorkingTree = async (cwd, requestedPath) => {
|
|
656
605
|
const empty = { patch: null, patchOmitted: false, binary: false };
|
|
657
|
-
const absolutePath = path.join(
|
|
606
|
+
const absolutePath = path.join(cwd, requestedPath);
|
|
658
607
|
try {
|
|
659
608
|
const stat = fs.statSync(absolutePath);
|
|
660
609
|
if (!stat.isFile())
|
|
@@ -720,34 +669,33 @@ let activePrFetcher = defaultPrFetcher;
|
|
|
720
669
|
export const setPrFetcher = (fetcher) => {
|
|
721
670
|
activePrFetcher = fetcher;
|
|
722
671
|
};
|
|
723
|
-
const parseGithubRemotes = (
|
|
672
|
+
const parseGithubRemotes = async (cwd) => {
|
|
673
|
+
const result = await runGit(cwd, ["remote", "-v"]);
|
|
674
|
+
if (result.exitCode !== 0)
|
|
675
|
+
return [];
|
|
724
676
|
const raw = [];
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
catch {
|
|
742
|
-
// No remotes
|
|
677
|
+
const seen = new Set();
|
|
678
|
+
for (const line of result.stdout.toString("utf8").split("\n")) {
|
|
679
|
+
const match = /^(\S+)\t(.+?)\s+\(fetch\)$/.exec(line);
|
|
680
|
+
if (!match)
|
|
681
|
+
continue;
|
|
682
|
+
const [, name, url] = match;
|
|
683
|
+
if (seen.has(name))
|
|
684
|
+
continue;
|
|
685
|
+
seen.add(name);
|
|
686
|
+
const urlMatch = /github\.com[/:]([^\s/]+)\/([^\s]+?)(?:\.git)?$/i.exec(url);
|
|
687
|
+
if (!urlMatch)
|
|
688
|
+
continue;
|
|
689
|
+
const [, owner, repoName] = urlMatch;
|
|
690
|
+
raw.push({ name, slug: `${owner}/${repoName}`, owner });
|
|
743
691
|
}
|
|
744
692
|
return memoBy(raw, (remote) => `${remote.name} ${remote.slug}`);
|
|
745
693
|
};
|
|
746
|
-
const detectPr = async (
|
|
747
|
-
const currentBranch = getCurrentBranch(
|
|
694
|
+
const detectPr = async (cwd) => {
|
|
695
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
748
696
|
if (!currentBranch)
|
|
749
697
|
return null;
|
|
750
|
-
const remotes = parseGithubRemotes(
|
|
698
|
+
const remotes = await parseGithubRemotes(cwd);
|
|
751
699
|
if (remotes.length === 0)
|
|
752
700
|
return null;
|
|
753
701
|
const ownRemote = remotes.find((remote) => remote.name === "origin") ?? remotes[0];
|
|
@@ -776,16 +724,32 @@ const detectPr = async (r) => {
|
|
|
776
724
|
}
|
|
777
725
|
: null;
|
|
778
726
|
};
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
|
|
727
|
+
const listBranchesByRecency = async (cwd) => {
|
|
728
|
+
const result = await runGit(cwd, [
|
|
729
|
+
"for-each-ref",
|
|
730
|
+
"--format=%(refname:short)",
|
|
731
|
+
"--sort=-committerdate",
|
|
732
|
+
"refs/heads",
|
|
733
|
+
"refs/remotes",
|
|
734
|
+
]);
|
|
735
|
+
if (result.exitCode !== 0)
|
|
782
736
|
return [];
|
|
783
|
-
const
|
|
737
|
+
const names = [];
|
|
738
|
+
for (const name of result.stdout.toString("utf8").split("\n")) {
|
|
739
|
+
if (!name || name.endsWith("/HEAD"))
|
|
740
|
+
continue;
|
|
741
|
+
names.push(name);
|
|
742
|
+
if (names.length >= GIT_MAX_BRANCHES)
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
return names;
|
|
746
|
+
};
|
|
747
|
+
export const listGithubRemoteSlugs = async (cwd) => {
|
|
748
|
+
const remotes = await parseGithubRemotes(cwd);
|
|
784
749
|
return memoBy(remotes, (remote) => remote.slug).map((remote) => remote.slug);
|
|
785
750
|
};
|
|
786
751
|
export const getGitBranchInfo = async (cwd) => {
|
|
787
|
-
|
|
788
|
-
if (!r) {
|
|
752
|
+
if (!(await isGitRepo(cwd))) {
|
|
789
753
|
return {
|
|
790
754
|
isRepo: false,
|
|
791
755
|
currentBranch: null,
|
|
@@ -795,42 +759,23 @@ export const getGitBranchInfo = async (cwd) => {
|
|
|
795
759
|
pr: null,
|
|
796
760
|
};
|
|
797
761
|
}
|
|
798
|
-
const currentBranch =
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
if (b.name.endsWith("/HEAD"))
|
|
804
|
-
continue;
|
|
805
|
-
try {
|
|
806
|
-
const refName = b.type === "Remote" ? `refs/remotes/${b.name}` : `refs/heads/${b.name}`;
|
|
807
|
-
const ref = r.repo.getReference(refName);
|
|
808
|
-
const target = ref.target();
|
|
809
|
-
if (!target)
|
|
810
|
-
continue;
|
|
811
|
-
const commit = r.repo.getCommit(target);
|
|
812
|
-
branchData.push({ name: b.name, time: commit.time().getTime() });
|
|
813
|
-
}
|
|
814
|
-
catch {
|
|
815
|
-
branchData.push({ name: b.name, time: 0 });
|
|
816
|
-
}
|
|
817
|
-
if (branchData.length >= GIT_MAX_BRANCHES)
|
|
818
|
-
break;
|
|
819
|
-
}
|
|
820
|
-
branchData.sort((a, b) => b.time - a.time);
|
|
762
|
+
const [currentBranch, defaultBase, branches] = await Promise.all([
|
|
763
|
+
getCurrentBranch(cwd),
|
|
764
|
+
resolveDefaultBase(cwd),
|
|
765
|
+
listBranchesByRecency(cwd),
|
|
766
|
+
]);
|
|
821
767
|
return {
|
|
822
768
|
isRepo: true,
|
|
823
769
|
currentBranch,
|
|
824
770
|
defaultBase: defaultBase?.ref ?? null,
|
|
825
771
|
defaultBaseSource: defaultBase?.source ?? null,
|
|
826
|
-
branches
|
|
772
|
+
branches,
|
|
827
773
|
pr: null,
|
|
828
774
|
};
|
|
829
775
|
};
|
|
830
776
|
export const getGitBranchPr = async (cwd) => {
|
|
831
|
-
|
|
832
|
-
if (!r)
|
|
777
|
+
if (!(await isGitRepo(cwd)))
|
|
833
778
|
return null;
|
|
834
|
-
return detectPr(
|
|
779
|
+
return detectPr(cwd);
|
|
835
780
|
};
|
|
836
781
|
//# sourceMappingURL=git-diff.js.map
|