@monotykamary/localterm-server 2.0.4 → 2.1.0
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 +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -1
- package/dist/git-diff.d.ts +2 -1
- package/dist/git-diff.d.ts.map +1 -1
- package/dist/git-diff.js +353 -551
- package/dist/git-diff.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.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,12 +2,10 @@ 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 { computePatchFromContents } from "./utils/compute-patch.js";
|
|
7
|
-
import { memoBy } from "./utils/memo-by.js";
|
|
8
|
-
import { reconcileFileStats } from "./utils/reconcile-file-stats.js";
|
|
5
|
+
import { runGit } from "./utils/run-git.js";
|
|
9
6
|
import { resolveGithubToken } from "./utils/resolve-github-token.js";
|
|
10
|
-
import {
|
|
7
|
+
import { memoBy } from "./utils/memo-by.js";
|
|
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";
|
|
11
9
|
const WORKING_OPTIONS = { mode: "working" };
|
|
12
10
|
const EMPTY_SUMMARY = {
|
|
13
11
|
isRepo: false,
|
|
@@ -17,130 +15,103 @@ const EMPTY_SUMMARY = {
|
|
|
17
15
|
binaries: 0,
|
|
18
16
|
branch: null,
|
|
19
17
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return "renamed";
|
|
36
|
-
case "Copied":
|
|
37
|
-
return "added";
|
|
38
|
-
case "Untracked":
|
|
39
|
-
return "untracked";
|
|
40
|
-
case "Modified":
|
|
41
|
-
case "Typechange":
|
|
42
|
-
default:
|
|
43
|
-
return "modified";
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
const openRepo = async (cwd) => {
|
|
47
|
-
try {
|
|
48
|
-
const repo = await openRepository(cwd);
|
|
49
|
-
return { cwd, repo };
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
18
|
+
// Nested so a cwd can hold more than one comparison (the working-tree summary
|
|
19
|
+
// is pushed on git-dirty while the viewer may be open in branch mode).
|
|
20
|
+
const diffCacheByCwd = new Map();
|
|
21
|
+
const comparisonKey = (mode, base) => `${mode}:${base ?? ""}`;
|
|
22
|
+
const readDiffCache = (cwd, mode, base) => {
|
|
23
|
+
const byComparison = diffCacheByCwd.get(cwd);
|
|
24
|
+
if (!byComparison)
|
|
25
|
+
return null;
|
|
26
|
+
const entry = byComparison.get(comparisonKey(mode, base));
|
|
27
|
+
if (!entry)
|
|
28
|
+
return null;
|
|
29
|
+
if (Date.now() - entry.builtAt > GIT_CACHE_TTL_MS) {
|
|
30
|
+
byComparison.delete(comparisonKey(mode, base));
|
|
31
|
+
if (byComparison.size === 0)
|
|
32
|
+
diffCacheByCwd.delete(cwd);
|
|
52
33
|
return null;
|
|
53
34
|
}
|
|
35
|
+
return entry;
|
|
54
36
|
};
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
37
|
+
const writeDiffCache = (cwd, mode, base, cache) => {
|
|
38
|
+
let byComparison = diffCacheByCwd.get(cwd);
|
|
39
|
+
if (!byComparison) {
|
|
40
|
+
byComparison = new Map();
|
|
41
|
+
diffCacheByCwd.set(cwd, byComparison);
|
|
60
42
|
}
|
|
61
|
-
|
|
43
|
+
byComparison.set(comparisonKey(mode, base), cache);
|
|
44
|
+
};
|
|
45
|
+
export const invalidateGitDiffCache = (cwd) => {
|
|
46
|
+
diffCacheByCwd.delete(cwd);
|
|
47
|
+
};
|
|
48
|
+
const runGitText = async (cwd, args) => {
|
|
49
|
+
const result = await runGit(cwd, args);
|
|
50
|
+
return result.stdout.toString("utf8");
|
|
51
|
+
};
|
|
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;
|
|
58
|
+
};
|
|
59
|
+
const getCurrentBranch = async (cwd) => {
|
|
60
|
+
const result = await runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
61
|
+
if (result.exitCode !== 0)
|
|
62
62
|
return null;
|
|
63
|
-
|
|
63
|
+
const name = result.stdout.toString("utf8").trim();
|
|
64
|
+
return name === "HEAD" ? null : name;
|
|
64
65
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return GIT_EMPTY_TREE_HASH;
|
|
72
|
-
}
|
|
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;
|
|
73
72
|
};
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// stale origin/HEAD
|
|
88
|
-
}
|
|
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" };
|
|
89
86
|
}
|
|
90
87
|
}
|
|
91
88
|
}
|
|
92
|
-
catch {
|
|
93
|
-
// No origin/HEAD configured
|
|
94
|
-
}
|
|
95
89
|
for (const name of ["main", "master", "develop"]) {
|
|
96
90
|
if (name === currentBranch)
|
|
97
91
|
continue;
|
|
98
92
|
for (const candidate of [`origin/${name}`, name]) {
|
|
99
|
-
|
|
100
|
-
r.repo.revparseSingle(candidate);
|
|
93
|
+
if (await verifyRef(cwd, candidate))
|
|
101
94
|
return { ref: candidate, source: "fallback" };
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
95
|
}
|
|
107
96
|
}
|
|
108
97
|
return null;
|
|
109
98
|
};
|
|
110
|
-
const resolveEffectiveBaseRef = (
|
|
99
|
+
const resolveEffectiveBaseRef = async (cwd, options) => {
|
|
111
100
|
if (options.mode !== "branch")
|
|
112
|
-
return resolveDiffBaseRef(
|
|
101
|
+
return resolveDiffBaseRef(cwd);
|
|
113
102
|
let baseRef = options.base?.trim() || null;
|
|
114
|
-
if (baseRef)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
catch {
|
|
119
|
-
baseRef = null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (!baseRef) {
|
|
123
|
-
const resolved = resolveDefaultBase(r);
|
|
124
|
-
baseRef = resolved?.ref ?? null;
|
|
125
|
-
}
|
|
103
|
+
if (baseRef && !(await verifyRef(cwd, baseRef)))
|
|
104
|
+
baseRef = null;
|
|
105
|
+
if (!baseRef)
|
|
106
|
+
baseRef = (await resolveDefaultBase(cwd))?.ref ?? null;
|
|
126
107
|
if (!baseRef)
|
|
127
108
|
return null;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (mergeBase)
|
|
133
|
-
return mergeBase;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
// Unrelated histories
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
return r.repo.revparseSingle(baseRef);
|
|
140
|
-
}
|
|
141
|
-
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)))
|
|
142
113
|
return null;
|
|
143
|
-
|
|
114
|
+
return runGitText(cwd, ["rev-parse", "--verify", baseRef]);
|
|
144
115
|
};
|
|
145
116
|
const countLines = (text) => {
|
|
146
117
|
if (text.length === 0)
|
|
@@ -154,42 +125,45 @@ const countLines = (text) => {
|
|
|
154
125
|
count += 1;
|
|
155
126
|
return count;
|
|
156
127
|
};
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
break;
|
|
167
|
-
const filePath = entry.path();
|
|
168
|
-
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");
|
|
169
137
|
try {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const { buffer: readBuffer } = await handle.read(buffer, 0, bytesToRead, 0);
|
|
178
|
-
const sniffEnd = Math.min(readBuffer.length, GIT_BINARY_SNIFF_BYTES);
|
|
179
|
-
const binary = readBuffer.subarray(0, sniffEnd).includes(0);
|
|
180
|
-
const truncated = stat.size > GIT_MAX_UNTRACKED_FILE_BYTES;
|
|
181
|
-
const content = binary ? null : truncated ? null : readBuffer.toString("utf8");
|
|
182
|
-
const lines = binary ? 0 : content ? countLines(content) : 0;
|
|
183
|
-
files.push({ path: filePath, binary, lines, content, truncated });
|
|
184
|
-
}
|
|
185
|
-
finally {
|
|
186
|
-
await handle.close();
|
|
187
|
-
}
|
|
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 };
|
|
188
145
|
}
|
|
189
|
-
|
|
190
|
-
|
|
146
|
+
finally {
|
|
147
|
+
await handle.close();
|
|
191
148
|
}
|
|
192
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
|
+
}
|
|
193
167
|
return files;
|
|
194
168
|
};
|
|
195
169
|
export const buildUntrackedPatch = (content) => {
|
|
@@ -203,161 +177,7 @@ export const buildUntrackedPatch = (content) => {
|
|
|
203
177
|
const noNewlineMarker = hasTrailingNewline ? "" : "\n\";
|
|
204
178
|
return `@@ -0,0 +1,${lines.length} @@\n${body}${noNewlineMarker}\n`;
|
|
205
179
|
};
|
|
206
|
-
const readBlobContent = (r, oid) => {
|
|
207
|
-
if (oid === ZERO_OID)
|
|
208
|
-
return null;
|
|
209
|
-
try {
|
|
210
|
-
const obj = r.repo.findObject(oid);
|
|
211
|
-
if (!obj)
|
|
212
|
-
return null;
|
|
213
|
-
const blob = obj.peelToBlob();
|
|
214
|
-
return Buffer.from(blob.content()).toString("utf8");
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
const readWorkingTreeFile = (r, filePath, maxBytes = GIT_MAX_UNTRACKED_FILE_BYTES) => {
|
|
221
|
-
const absolutePath = path.join(r.cwd, filePath);
|
|
222
|
-
try {
|
|
223
|
-
const stat = fs.statSync(absolutePath);
|
|
224
|
-
if (!stat.isFile())
|
|
225
|
-
return null;
|
|
226
|
-
const bytesToRead = Math.min(stat.size, maxBytes);
|
|
227
|
-
const buffer = Buffer.alloc(bytesToRead);
|
|
228
|
-
const handle = fs.openSync(absolutePath, "r");
|
|
229
|
-
fs.readSync(handle, buffer, 0, bytesToRead, 0);
|
|
230
|
-
fs.closeSync(handle);
|
|
231
|
-
const sniffEnd = Math.min(buffer.length, GIT_BINARY_SNIFF_BYTES);
|
|
232
|
-
if (buffer.subarray(0, sniffEnd).includes(0))
|
|
233
|
-
return null;
|
|
234
|
-
if (stat.size > maxBytes)
|
|
235
|
-
return null;
|
|
236
|
-
return buffer.toString("utf8");
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
const collectDeltaMeta = (r, diff, isWorkingTree) => {
|
|
243
|
-
const stats = diff.stats();
|
|
244
|
-
const totalInsertions = Number(stats.insertions);
|
|
245
|
-
const totalDeletions = Number(stats.deletions);
|
|
246
|
-
const deltas = collectIterator(diff.deltas());
|
|
247
|
-
const result = [];
|
|
248
|
-
for (const delta of deltas) {
|
|
249
|
-
const status = deltaTypeToStatus(delta.status());
|
|
250
|
-
if (status === "untracked")
|
|
251
|
-
continue;
|
|
252
|
-
const newFile = delta.newFile();
|
|
253
|
-
const oldFile = delta.oldFile();
|
|
254
|
-
const isBinary = newFile.isBinary();
|
|
255
|
-
result.push({
|
|
256
|
-
path: newFile.path(),
|
|
257
|
-
oldPath: status === "renamed" ? oldFile.path() : null,
|
|
258
|
-
status,
|
|
259
|
-
additions: 0,
|
|
260
|
-
deletions: 0,
|
|
261
|
-
binary: isBinary,
|
|
262
|
-
oldId: oldFile.id(),
|
|
263
|
-
newId: newFile.id(),
|
|
264
|
-
isWorkingTree,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
// Compute per-file additions/deletions from patch text.
|
|
268
|
-
// es-git's DiffStats only gives aggregate totals; we generate
|
|
269
|
-
// patches per-delta and count +/- lines from the structured diff.
|
|
270
|
-
const nonBinary = result.filter((d) => !d.binary);
|
|
271
|
-
if (nonBinary.length === 0)
|
|
272
|
-
return result;
|
|
273
|
-
if (nonBinary.length === 1) {
|
|
274
|
-
nonBinary[0].additions = totalInsertions;
|
|
275
|
-
nonBinary[0].deletions = totalDeletions;
|
|
276
|
-
return result;
|
|
277
|
-
}
|
|
278
|
-
// Compute per-file stats from patch text. Each delta gets its own
|
|
279
|
-
// patch generated from old vs new content, giving exact counts.
|
|
280
|
-
for (const d of result) {
|
|
281
|
-
if (d.binary)
|
|
282
|
-
continue;
|
|
283
|
-
try {
|
|
284
|
-
const oldContent = d.oldId === ZERO_OID || d.oldId === ""
|
|
285
|
-
? d.status === "added"
|
|
286
|
-
? null
|
|
287
|
-
: readBlobContent(r, d.oldId)
|
|
288
|
-
: readBlobContent(r, d.oldId);
|
|
289
|
-
const newContent = d.isWorkingTree
|
|
290
|
-
? readWorkingTreeFile(r, d.path, GIT_MAX_TOTAL_PATCH_BYTES)
|
|
291
|
-
: d.newId === ZERO_OID || d.newId === ""
|
|
292
|
-
? d.status === "deleted"
|
|
293
|
-
? null
|
|
294
|
-
: readBlobContent(r, d.newId)
|
|
295
|
-
: readBlobContent(r, d.newId);
|
|
296
|
-
const aPath = d.oldPath ?? d.path;
|
|
297
|
-
const patchResult = computePatchFromContents(oldContent, newContent, aPath, d.path, null, null, d.oldId === ZERO_OID ? null : d.oldId, d.newId === ZERO_OID ? null : d.newId, d.status === "renamed");
|
|
298
|
-
d.additions = patchResult.additions;
|
|
299
|
-
d.deletions = patchResult.deletions;
|
|
300
|
-
}
|
|
301
|
-
catch {
|
|
302
|
-
// Fallback: proportional split of aggregate totals
|
|
303
|
-
const perFileAdd = Math.floor(totalInsertions / nonBinary.length);
|
|
304
|
-
d.additions = perFileAdd;
|
|
305
|
-
d.deletions = Math.floor(totalDeletions / nonBinary.length);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// Verify totals match and redistribute remainder if needed
|
|
309
|
-
reconcileFileStats(result, totalInsertions, totalDeletions);
|
|
310
|
-
return result;
|
|
311
|
-
};
|
|
312
|
-
const buildBaseTree = (r, baseRef) => {
|
|
313
|
-
try {
|
|
314
|
-
return r.repo.getTree(baseRef);
|
|
315
|
-
}
|
|
316
|
-
catch {
|
|
317
|
-
try {
|
|
318
|
-
const baseOid = r.repo.revparseSingle(baseRef);
|
|
319
|
-
const obj = r.repo.findObject(baseOid);
|
|
320
|
-
if (!obj)
|
|
321
|
-
return null;
|
|
322
|
-
if (obj.type() === "Tree") {
|
|
323
|
-
return obj;
|
|
324
|
-
}
|
|
325
|
-
const commit = r.repo.getCommit(baseOid);
|
|
326
|
-
return commit.tree();
|
|
327
|
-
}
|
|
328
|
-
catch {
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
180
|
export const splitPatchByFile = (raw) => raw.split(/^(?=diff --git )/m).filter((chunk) => chunk.startsWith("diff --git "));
|
|
334
|
-
const computeTrackedDeltas = async (r, baseRef) => {
|
|
335
|
-
const baseTree = buildBaseTree(r, baseRef);
|
|
336
|
-
if (!baseTree)
|
|
337
|
-
return [];
|
|
338
|
-
let diff;
|
|
339
|
-
try {
|
|
340
|
-
diff = r.repo.diffTreeToWorkdirWithIndex(baseTree);
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
return [];
|
|
344
|
-
}
|
|
345
|
-
try {
|
|
346
|
-
diff.findSimilar({ renames: true });
|
|
347
|
-
}
|
|
348
|
-
catch {
|
|
349
|
-
// Rename detection failed
|
|
350
|
-
}
|
|
351
|
-
const deltaMeta = collectDeltaMeta(r, diff, true);
|
|
352
|
-
return deltaMeta.map((d) => ({
|
|
353
|
-
path: d.path,
|
|
354
|
-
oldPath: d.oldPath,
|
|
355
|
-
status: d.status,
|
|
356
|
-
additions: d.additions,
|
|
357
|
-
deletions: d.deletions,
|
|
358
|
-
binary: d.binary,
|
|
359
|
-
}));
|
|
360
|
-
};
|
|
361
181
|
export const parseNumstatZ = (raw) => {
|
|
362
182
|
const tokens = raw.split("\0");
|
|
363
183
|
const entries = [];
|
|
@@ -413,242 +233,236 @@ export const parseNameStatusZ = (raw) => {
|
|
|
413
233
|
}
|
|
414
234
|
return statuses;
|
|
415
235
|
};
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
try {
|
|
430
|
-
diff = r.repo.diffTreeToWorkdirWithIndex(baseTree);
|
|
431
|
-
}
|
|
432
|
-
catch {
|
|
433
|
-
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
434
|
-
}
|
|
435
|
-
try {
|
|
436
|
-
diff.findSimilar({ renames: true });
|
|
437
|
-
}
|
|
438
|
-
catch {
|
|
439
|
-
// Rename detection failed
|
|
440
|
-
}
|
|
441
|
-
const stats = diff.stats();
|
|
442
|
-
let additions = Number(stats.insertions);
|
|
443
|
-
let deletions = Number(stats.deletions);
|
|
444
|
-
let binaries = 0;
|
|
445
|
-
let fileCount = Number(stats.filesChanged);
|
|
446
|
-
const diffDeltas = collectIterator(diff.deltas());
|
|
447
|
-
for (const delta of diffDeltas) {
|
|
448
|
-
if (deltaTypeToStatus(delta.status()) !== "untracked" && delta.newFile().isBinary()) {
|
|
449
|
-
binaries++;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
const untracked = await collectUntrackedFiles(r);
|
|
453
|
-
for (const file of untracked) {
|
|
454
|
-
fileCount++;
|
|
455
|
-
if (file.binary) {
|
|
456
|
-
binaries++;
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
additions += file.lines;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return { isRepo: true, files: fileCount, additions, deletions, binaries, branch };
|
|
463
|
-
}
|
|
464
|
-
catch {
|
|
465
|
-
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
const buildFilePatch = (r, delta) => {
|
|
469
|
-
if (delta.binary)
|
|
236
|
+
// One full diff pass for `(cwd)` against `baseRef`. Three parallel `git diff`
|
|
237
|
+
// invocations (numstat for counts+binary, name-status for the status letter +
|
|
238
|
+
// rename old path, patch for the body) walk the same sorted diff queue, so
|
|
239
|
+
// numstat[i]/name-status.key(patch-path)/patch[i] all describe the same file
|
|
240
|
+
// positionally. Untracked files are folded in from `ls-files` with synthesized
|
|
241
|
+
// patches (git's own diff never lists untracked files).
|
|
242
|
+
const buildDiffCache = async (cwd, baseRef) => {
|
|
243
|
+
const [numstatRes, nameStatusRes, patchRes] = await Promise.all([
|
|
244
|
+
runGit(cwd, ["diff", DIFF_RENAME_FLAG, "--numstat", "-z", baseRef]),
|
|
245
|
+
runGit(cwd, ["diff", DIFF_RENAME_FLAG, "--name-status", "-z", baseRef]),
|
|
246
|
+
runGit(cwd, ["diff", DIFF_RENAME_FLAG, "--patch", baseRef]),
|
|
247
|
+
]);
|
|
248
|
+
if (numstatRes.exitCode !== 0 || patchRes.exitCode !== 0)
|
|
470
249
|
return null;
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const aPath = delta.oldPath ?? delta.path;
|
|
484
|
-
const result = computePatchFromContents(oldContent, newContent, aPath, delta.path, null, null, delta.oldId === ZERO_OID ? null : delta.oldId, delta.newId === ZERO_OID ? null : delta.newId, delta.status === "renamed");
|
|
485
|
-
return result.patchText || null;
|
|
486
|
-
};
|
|
487
|
-
export const getGitDiff = async (cwd, options = WORKING_OPTIONS) => {
|
|
488
|
-
const r = await openRepo(cwd);
|
|
489
|
-
if (!r)
|
|
490
|
-
return { isRepo: false, files: [] };
|
|
491
|
-
const baseRef = resolveEffectiveBaseRef(r, options);
|
|
492
|
-
if (baseRef === null)
|
|
493
|
-
return { isRepo: true, files: [] };
|
|
494
|
-
const baseTree = buildBaseTree(r, baseRef);
|
|
495
|
-
if (!baseTree)
|
|
496
|
-
return { isRepo: true, files: [] };
|
|
497
|
-
let diff;
|
|
498
|
-
try {
|
|
499
|
-
diff = r.repo.diffTreeToWorkdirWithIndex(baseTree);
|
|
500
|
-
}
|
|
501
|
-
catch {
|
|
502
|
-
return { isRepo: true, files: [] };
|
|
503
|
-
}
|
|
504
|
-
try {
|
|
505
|
-
diff.findSimilar({ renames: true });
|
|
506
|
-
}
|
|
507
|
-
catch {
|
|
508
|
-
// Rename detection failed
|
|
509
|
-
}
|
|
510
|
-
const deltaMeta = collectDeltaMeta(r, diff, true);
|
|
250
|
+
const numstat = parseNumstatZ(numstatRes.stdout.toString("utf8"));
|
|
251
|
+
const statuses = parseNameStatusZ(nameStatusRes.stdout.toString("utf8"));
|
|
252
|
+
const patches = splitPatchByFile(patchRes.stdout.toString("utf8"));
|
|
253
|
+
// numstat and --patch share the diff queue's order; if they ever disagree
|
|
254
|
+
// (a git version quirk on some edge diff) we can't trust positional pairing,
|
|
255
|
+
// so degrade: stats stay correct, patches read as omitted.
|
|
256
|
+
const patchesAligned = patches.length === numstat.length;
|
|
257
|
+
const untracked = await collectUntrackedFiles(cwd);
|
|
258
|
+
const fileMeta = [];
|
|
259
|
+
const filePatchByPath = new Map();
|
|
260
|
+
const fileBinaryByPath = new Map();
|
|
261
|
+
const filePatchOmittedByPath = new Map();
|
|
511
262
|
let totalPatchBytes = 0;
|
|
512
|
-
|
|
513
|
-
|
|
263
|
+
let additions = 0;
|
|
264
|
+
let deletions = 0;
|
|
265
|
+
let binaries = 0;
|
|
266
|
+
for (let index = 0; index < numstat.length; index += 1) {
|
|
267
|
+
const entry = numstat[index];
|
|
268
|
+
const status = statuses.get(entry.path) ?? "modified";
|
|
269
|
+
const oldPath = status === "renamed" ? entry.oldPath : null;
|
|
270
|
+
let patchText = null;
|
|
514
271
|
let patchOmitted = false;
|
|
515
272
|
if (entry.binary) {
|
|
516
|
-
|
|
273
|
+
binaries += 1;
|
|
517
274
|
}
|
|
518
275
|
else {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
276
|
+
const rawPatch = patchesAligned ? (patches[index] ?? null) : null;
|
|
277
|
+
if (rawPatch === null ||
|
|
278
|
+
rawPatch.length > GIT_MAX_PATCH_BYTES_PER_FILE ||
|
|
279
|
+
totalPatchBytes + rawPatch.length > GIT_MAX_TOTAL_PATCH_BYTES) {
|
|
523
280
|
patchOmitted = true;
|
|
524
281
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
patch = null;
|
|
529
|
-
patchOmitted = true;
|
|
530
|
-
}
|
|
531
|
-
else {
|
|
532
|
-
totalPatchBytes += patch.length;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
else if (!patchOmitted) {
|
|
536
|
-
patchOmitted = true;
|
|
282
|
+
else {
|
|
283
|
+
patchText = rawPatch;
|
|
284
|
+
totalPatchBytes += rawPatch.length;
|
|
537
285
|
}
|
|
286
|
+
additions += entry.additions;
|
|
287
|
+
deletions += entry.deletions;
|
|
538
288
|
}
|
|
539
|
-
|
|
289
|
+
fileMeta.push({
|
|
540
290
|
path: entry.path,
|
|
541
|
-
oldPath
|
|
542
|
-
status
|
|
291
|
+
oldPath,
|
|
292
|
+
status,
|
|
543
293
|
additions: entry.additions,
|
|
544
294
|
deletions: entry.deletions,
|
|
545
295
|
binary: entry.binary,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
296
|
+
});
|
|
297
|
+
filePatchByPath.set(entry.path, patchText);
|
|
298
|
+
fileBinaryByPath.set(entry.path, entry.binary);
|
|
299
|
+
filePatchOmittedByPath.set(entry.path, patchOmitted);
|
|
300
|
+
}
|
|
551
301
|
for (const file of untracked) {
|
|
552
302
|
const patch = file.binary
|
|
553
303
|
? null
|
|
554
304
|
: file.truncated || file.content === null
|
|
555
305
|
? null
|
|
556
306
|
: buildUntrackedPatch(file.content);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
deletions: 0,
|
|
563
|
-
binary: file.binary,
|
|
564
|
-
patch,
|
|
565
|
-
patchOmitted: file.truncated,
|
|
566
|
-
};
|
|
567
|
-
if (entry.patch !== null) {
|
|
568
|
-
if (entry.patch.length > GIT_MAX_PATCH_BYTES_PER_FILE ||
|
|
569
|
-
totalPatchBytes + entry.patch.length > GIT_MAX_TOTAL_PATCH_BYTES) {
|
|
570
|
-
entry.patch = null;
|
|
571
|
-
entry.patchOmitted = true;
|
|
307
|
+
let patchOmitted = file.truncated;
|
|
308
|
+
if (patch !== null) {
|
|
309
|
+
if (patch.length > GIT_MAX_PATCH_BYTES_PER_FILE ||
|
|
310
|
+
totalPatchBytes + patch.length > GIT_MAX_TOTAL_PATCH_BYTES) {
|
|
311
|
+
patchOmitted = true;
|
|
572
312
|
}
|
|
573
313
|
else {
|
|
574
|
-
totalPatchBytes +=
|
|
314
|
+
totalPatchBytes += patch.length;
|
|
575
315
|
}
|
|
576
316
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const r = await openRepo(cwd);
|
|
583
|
-
if (!r)
|
|
584
|
-
return { isRepo: false, files: [] };
|
|
585
|
-
const baseRef = resolveEffectiveBaseRef(r, options);
|
|
586
|
-
if (baseRef === null)
|
|
587
|
-
return { isRepo: true, files: [] };
|
|
588
|
-
const trackedDeltas = await computeTrackedDeltas(r, baseRef);
|
|
589
|
-
const files = trackedDeltas.map((entry) => ({
|
|
590
|
-
path: entry.path,
|
|
591
|
-
oldPath: entry.oldPath,
|
|
592
|
-
status: entry.status,
|
|
593
|
-
additions: entry.additions,
|
|
594
|
-
deletions: entry.deletions,
|
|
595
|
-
binary: entry.binary,
|
|
596
|
-
}));
|
|
597
|
-
const untracked = await collectUntrackedFiles(r);
|
|
598
|
-
for (const file of untracked) {
|
|
599
|
-
files.push({
|
|
317
|
+
const fileAdditions = file.binary ? 0 : file.lines;
|
|
318
|
+
if (file.binary)
|
|
319
|
+
binaries += 1;
|
|
320
|
+
additions += fileAdditions;
|
|
321
|
+
fileMeta.push({
|
|
600
322
|
path: file.path,
|
|
601
323
|
oldPath: null,
|
|
602
324
|
status: "untracked",
|
|
603
|
-
additions:
|
|
325
|
+
additions: fileAdditions,
|
|
604
326
|
deletions: 0,
|
|
605
327
|
binary: file.binary,
|
|
606
328
|
});
|
|
329
|
+
filePatchByPath.set(file.path, patchOmitted ? null : patch);
|
|
330
|
+
fileBinaryByPath.set(file.path, file.binary);
|
|
331
|
+
filePatchOmittedByPath.set(file.path, patchOmitted);
|
|
607
332
|
}
|
|
608
|
-
|
|
333
|
+
const summary = {
|
|
334
|
+
isRepo: true,
|
|
335
|
+
files: fileMeta.length,
|
|
336
|
+
additions,
|
|
337
|
+
deletions,
|
|
338
|
+
binaries,
|
|
339
|
+
branch: await getCurrentBranch(cwd),
|
|
340
|
+
};
|
|
341
|
+
return {
|
|
342
|
+
summary,
|
|
343
|
+
fileMeta,
|
|
344
|
+
filePatchByPath,
|
|
345
|
+
fileBinaryByPath,
|
|
346
|
+
filePatchOmittedByPath,
|
|
347
|
+
builtAt: Date.now(),
|
|
348
|
+
};
|
|
609
349
|
};
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
350
|
+
const ensureDiffCache = async (cwd, options) => {
|
|
351
|
+
// Read the cache before resolving the base ref — that resolution does git work
|
|
352
|
+
// (rev-parse + merge-base) on every call, so for the per-file patch endpoint
|
|
353
|
+
// (where the cache is warm on nearly every request) checking first keeps it a
|
|
354
|
+
// pure map lookup with no subprocess.
|
|
355
|
+
const cached = readDiffCache(cwd, options.mode, options.base ?? null);
|
|
356
|
+
if (cached)
|
|
357
|
+
return cached;
|
|
358
|
+
const baseRef = await resolveEffectiveBaseRef(cwd, options);
|
|
616
359
|
if (baseRef === null)
|
|
617
|
-
return
|
|
618
|
-
const
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
360
|
+
return null;
|
|
361
|
+
const cache = await buildDiffCache(cwd, baseRef);
|
|
362
|
+
if (cache)
|
|
363
|
+
writeDiffCache(cwd, options.mode, options.base ?? null, cache);
|
|
364
|
+
return cache;
|
|
365
|
+
};
|
|
366
|
+
export const getGitDiffSummary = async (cwd, options = WORKING_OPTIONS) => {
|
|
367
|
+
const cached = readDiffCache(cwd, options.mode, options.base ?? null);
|
|
368
|
+
if (cached)
|
|
369
|
+
return cached.summary;
|
|
370
|
+
if (!(await isGitRepo(cwd)))
|
|
371
|
+
return EMPTY_SUMMARY;
|
|
622
372
|
try {
|
|
623
|
-
|
|
373
|
+
const baseRef = await resolveEffectiveBaseRef(cwd, options);
|
|
374
|
+
if (baseRef === null)
|
|
375
|
+
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
376
|
+
const branch = await getCurrentBranch(cwd);
|
|
377
|
+
// Summary is pushed on every git-dirty signal, so it stays on the cheap
|
|
378
|
+
// numstat-only path (no patch) unless a cache is already warm.
|
|
379
|
+
const numstatRes = await runGit(cwd, ["diff", DIFF_RENAME_FLAG, "--numstat", "-z", baseRef]);
|
|
380
|
+
if (numstatRes.exitCode !== 0)
|
|
381
|
+
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
382
|
+
let additions = 0;
|
|
383
|
+
let deletions = 0;
|
|
384
|
+
let binaries = 0;
|
|
385
|
+
let fileCount = 0;
|
|
386
|
+
for (const entry of parseNumstatZ(numstatRes.stdout.toString("utf8"))) {
|
|
387
|
+
fileCount += 1;
|
|
388
|
+
if (entry.binary) {
|
|
389
|
+
binaries += 1;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
additions += entry.additions;
|
|
393
|
+
deletions += entry.deletions;
|
|
394
|
+
}
|
|
395
|
+
const untracked = await collectUntrackedFiles(cwd);
|
|
396
|
+
for (const file of untracked) {
|
|
397
|
+
fileCount += 1;
|
|
398
|
+
if (file.binary) {
|
|
399
|
+
binaries += 1;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
additions += file.lines;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return { isRepo: true, files: fileCount, additions, deletions, binaries, branch };
|
|
624
406
|
}
|
|
625
407
|
catch {
|
|
626
|
-
return
|
|
627
|
-
}
|
|
628
|
-
try {
|
|
629
|
-
diff.findSimilar({ renames: true });
|
|
408
|
+
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
630
409
|
}
|
|
631
|
-
|
|
632
|
-
|
|
410
|
+
};
|
|
411
|
+
export const getGitDiff = async (cwd, options = WORKING_OPTIONS) => {
|
|
412
|
+
const cached = readDiffCache(cwd, options.mode, options.base ?? null);
|
|
413
|
+
if (cached) {
|
|
414
|
+
return { isRepo: true, files: mapCacheFiles(cached) };
|
|
633
415
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
416
|
+
if (!(await isGitRepo(cwd)))
|
|
417
|
+
return { isRepo: false, files: [] };
|
|
418
|
+
const cache = await ensureDiffCache(cwd, options);
|
|
419
|
+
if (!cache)
|
|
420
|
+
return { isRepo: true, files: [] };
|
|
421
|
+
return { isRepo: true, files: mapCacheFiles(cache) };
|
|
422
|
+
};
|
|
423
|
+
const mapCacheFiles = (cache) => cache.fileMeta.map((meta) => ({
|
|
424
|
+
path: meta.path,
|
|
425
|
+
oldPath: meta.oldPath,
|
|
426
|
+
status: meta.status,
|
|
427
|
+
additions: meta.additions,
|
|
428
|
+
deletions: meta.deletions,
|
|
429
|
+
binary: meta.binary,
|
|
430
|
+
patch: cache.filePatchByPath.get(meta.path) ?? null,
|
|
431
|
+
patchOmitted: cache.filePatchOmittedByPath.get(meta.path) ?? false,
|
|
432
|
+
}));
|
|
433
|
+
export const getGitDiffFiles = async (cwd, options = WORKING_OPTIONS) => {
|
|
434
|
+
const cached = readDiffCache(cwd, options.mode, options.base ?? null);
|
|
435
|
+
if (cached)
|
|
436
|
+
return { isRepo: true, files: cached.fileMeta };
|
|
437
|
+
if (!(await isGitRepo(cwd)))
|
|
438
|
+
return { isRepo: false, files: [] };
|
|
439
|
+
const cache = await ensureDiffCache(cwd, options);
|
|
440
|
+
if (!cache)
|
|
441
|
+
return { isRepo: true, files: [] };
|
|
442
|
+
return { isRepo: true, files: cache.fileMeta };
|
|
443
|
+
};
|
|
444
|
+
export const getGitDiffFilePatch = async (cwd, requestedPath, options = WORKING_OPTIONS) => {
|
|
445
|
+
const empty = { patch: null, patchOmitted: false, binary: false };
|
|
446
|
+
const cache = await ensureDiffCache(cwd, options);
|
|
447
|
+
if (!cache)
|
|
448
|
+
return empty;
|
|
449
|
+
// O(1) lookup: the full diff pass that the per-file patch needs was already
|
|
450
|
+
// computed once for this (cwd, mode, base) and cached. This was the O(N²)
|
|
451
|
+
// regression — each per-file request re-ran the whole-tree diff + a jsdiff
|
|
452
|
+
// for every file. Now it's a map lookup.
|
|
453
|
+
if (cache.filePatchByPath.has(requestedPath)) {
|
|
454
|
+
return {
|
|
455
|
+
patch: cache.filePatchByPath.get(requestedPath) ?? null,
|
|
456
|
+
patchOmitted: cache.filePatchOmittedByPath.get(requestedPath) ?? false,
|
|
457
|
+
binary: cache.fileBinaryByPath.get(requestedPath) ?? false,
|
|
458
|
+
};
|
|
651
459
|
}
|
|
460
|
+
// An untracked path the cache didn't cover (created between the cache build
|
|
461
|
+
// and this request) falls back to synthesizing from the working tree.
|
|
462
|
+
return getGitDiffFilePatchFromWorkingTree(cwd, requestedPath);
|
|
463
|
+
};
|
|
464
|
+
const getGitDiffFilePatchFromWorkingTree = async (cwd, requestedPath) => {
|
|
465
|
+
const empty = { patch: null, patchOmitted: false, binary: false };
|
|
652
466
|
const absolutePath = path.join(cwd, requestedPath);
|
|
653
467
|
try {
|
|
654
468
|
const stat = fs.statSync(absolutePath);
|
|
@@ -715,34 +529,33 @@ let activePrFetcher = defaultPrFetcher;
|
|
|
715
529
|
export const setPrFetcher = (fetcher) => {
|
|
716
530
|
activePrFetcher = fetcher;
|
|
717
531
|
};
|
|
718
|
-
const parseGithubRemotes = (
|
|
532
|
+
const parseGithubRemotes = async (cwd) => {
|
|
533
|
+
const result = await runGit(cwd, ["remote", "-v"]);
|
|
534
|
+
if (result.exitCode !== 0)
|
|
535
|
+
return [];
|
|
719
536
|
const raw = [];
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
catch {
|
|
737
|
-
// No remotes
|
|
537
|
+
const seen = new Set();
|
|
538
|
+
for (const line of result.stdout.toString("utf8").split("\n")) {
|
|
539
|
+
const match = /^(\S+)\t(.+?)\s+\(fetch\)$/.exec(line);
|
|
540
|
+
if (!match)
|
|
541
|
+
continue;
|
|
542
|
+
const [, name, url] = match;
|
|
543
|
+
if (seen.has(name))
|
|
544
|
+
continue;
|
|
545
|
+
seen.add(name);
|
|
546
|
+
const urlMatch = /github\.com[/:]([^\s/]+)\/([^\s]+?)(?:\.git)?$/i.exec(url);
|
|
547
|
+
if (!urlMatch)
|
|
548
|
+
continue;
|
|
549
|
+
const [, owner, repoName] = urlMatch;
|
|
550
|
+
raw.push({ name, slug: `${owner}/${repoName}`, owner });
|
|
738
551
|
}
|
|
739
552
|
return memoBy(raw, (remote) => `${remote.name} ${remote.slug}`);
|
|
740
553
|
};
|
|
741
|
-
const detectPr = async (
|
|
742
|
-
const currentBranch = getCurrentBranch(
|
|
554
|
+
const detectPr = async (cwd) => {
|
|
555
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
743
556
|
if (!currentBranch)
|
|
744
557
|
return null;
|
|
745
|
-
const remotes = parseGithubRemotes(
|
|
558
|
+
const remotes = await parseGithubRemotes(cwd);
|
|
746
559
|
if (remotes.length === 0)
|
|
747
560
|
return null;
|
|
748
561
|
const ownRemote = remotes.find((remote) => remote.name === "origin") ?? remotes[0];
|
|
@@ -771,16 +584,32 @@ const detectPr = async (r) => {
|
|
|
771
584
|
}
|
|
772
585
|
: null;
|
|
773
586
|
};
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
|
|
587
|
+
const listBranchesByRecency = async (cwd) => {
|
|
588
|
+
const result = await runGit(cwd, [
|
|
589
|
+
"for-each-ref",
|
|
590
|
+
"--format=%(refname:short)",
|
|
591
|
+
"--sort=-committerdate",
|
|
592
|
+
"refs/heads",
|
|
593
|
+
"refs/remotes",
|
|
594
|
+
]);
|
|
595
|
+
if (result.exitCode !== 0)
|
|
777
596
|
return [];
|
|
778
|
-
const
|
|
597
|
+
const names = [];
|
|
598
|
+
for (const name of result.stdout.toString("utf8").split("\n")) {
|
|
599
|
+
if (!name || name.endsWith("/HEAD"))
|
|
600
|
+
continue;
|
|
601
|
+
names.push(name);
|
|
602
|
+
if (names.length >= GIT_MAX_BRANCHES)
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
return names;
|
|
606
|
+
};
|
|
607
|
+
export const listGithubRemoteSlugs = async (cwd) => {
|
|
608
|
+
const remotes = await parseGithubRemotes(cwd);
|
|
779
609
|
return memoBy(remotes, (remote) => remote.slug).map((remote) => remote.slug);
|
|
780
610
|
};
|
|
781
611
|
export const getGitBranchInfo = async (cwd) => {
|
|
782
|
-
|
|
783
|
-
if (!r) {
|
|
612
|
+
if (!(await isGitRepo(cwd))) {
|
|
784
613
|
return {
|
|
785
614
|
isRepo: false,
|
|
786
615
|
currentBranch: null,
|
|
@@ -790,50 +619,23 @@ export const getGitBranchInfo = async (cwd) => {
|
|
|
790
619
|
pr: null,
|
|
791
620
|
};
|
|
792
621
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const branchEntries = collectIterator(r.repo.branches());
|
|
799
|
-
const branchData = [];
|
|
800
|
-
for (const b of branchEntries) {
|
|
801
|
-
if (b.name.endsWith("/HEAD"))
|
|
802
|
-
continue;
|
|
803
|
-
try {
|
|
804
|
-
const refName = b.type === "Remote" ? `refs/remotes/${b.name}` : `refs/heads/${b.name}`;
|
|
805
|
-
const ref = r.repo.getReference(refName);
|
|
806
|
-
const target = ref.target();
|
|
807
|
-
if (!target)
|
|
808
|
-
continue;
|
|
809
|
-
const commit = r.repo.getCommit(target);
|
|
810
|
-
branchData.push({ name: b.name, time: commit.time().getTime() });
|
|
811
|
-
}
|
|
812
|
-
catch {
|
|
813
|
-
branchData.push({ name: b.name, time: 0 });
|
|
814
|
-
}
|
|
815
|
-
if (branchData.length >= GIT_MAX_BRANCHES)
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
818
|
-
branchData.sort((a, b) => b.time - a.time);
|
|
622
|
+
const [currentBranch, defaultBase, branches] = await Promise.all([
|
|
623
|
+
getCurrentBranch(cwd),
|
|
624
|
+
resolveDefaultBase(cwd),
|
|
625
|
+
listBranchesByRecency(cwd),
|
|
626
|
+
]);
|
|
819
627
|
return {
|
|
820
628
|
isRepo: true,
|
|
821
629
|
currentBranch,
|
|
822
630
|
defaultBase: defaultBase?.ref ?? null,
|
|
823
631
|
defaultBaseSource: defaultBase?.source ?? null,
|
|
824
|
-
branches
|
|
632
|
+
branches,
|
|
825
633
|
pr: null,
|
|
826
634
|
};
|
|
827
635
|
};
|
|
828
|
-
// Resolves the PR the current branch maps to via the GitHub REST API. Bounded by
|
|
829
|
-
// Octokit's own timeout and a try/catch in the default fetcher (returns []), so
|
|
830
|
-
// it degrades to null on a missing token, network failure, or no PR. Served from
|
|
831
|
-
// /api/git/branches/pr so the fast branch lease (/api/git/branches) never waits
|
|
832
|
-
// on the network.
|
|
833
636
|
export const getGitBranchPr = async (cwd) => {
|
|
834
|
-
|
|
835
|
-
if (!r)
|
|
637
|
+
if (!(await isGitRepo(cwd)))
|
|
836
638
|
return null;
|
|
837
|
-
return detectPr(
|
|
639
|
+
return detectPr(cwd);
|
|
838
640
|
};
|
|
839
641
|
//# sourceMappingURL=git-diff.js.map
|