@monotykamary/localterm-server 2.0.5 → 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 +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 +288 -483
- 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,84 @@ 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
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// All diff passes use diffTreeToWorkdirWithIndex, so the new side is the
|
|
294
|
-
// working tree — read it straight from disk rather than the delta's blob id
|
|
295
|
-
// (which points at the index/HEAD blob for unstaged edits and would read as
|
|
296
|
-
// unchanged vs the old side → zero counts). The old side is the base tree's
|
|
297
|
-
// blob.
|
|
298
|
-
const oldContent = delta.status === "added" ? null : readBlobContent(r, delta.oldId);
|
|
299
|
-
const newContent = delta.status === "deleted"
|
|
300
|
-
? null
|
|
301
|
-
: readWorkingTreeFile(r, delta.path, GIT_MAX_TOTAL_PATCH_BYTES);
|
|
302
|
-
const aPath = delta.oldPath ?? delta.path;
|
|
303
|
-
try {
|
|
304
|
-
const patchResult = 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");
|
|
305
|
-
return {
|
|
306
|
-
patchText: patchResult.patchText || null,
|
|
307
|
-
additions: patchResult.additions,
|
|
308
|
-
deletions: patchResult.deletions,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
catch {
|
|
312
|
-
return { patchText: null, additions: 0, deletions: 0 };
|
|
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 });
|
|
313
204
|
}
|
|
205
|
+
return entries;
|
|
314
206
|
};
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
332
|
-
return null;
|
|
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;
|
|
333
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");
|
|
334
233
|
}
|
|
234
|
+
return statuses;
|
|
335
235
|
};
|
|
336
|
-
// One full diff pass for `(cwd)` against `baseRef
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
const buildDiffCache = async (
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
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)
|
|
351
249
|
return null;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// false until the diff's stats have been materialized, which loads content
|
|
361
|
-
// and sniffs for NUL bytes. Without this, binary files read as text and get
|
|
362
|
-
// a junk patch synthesized from utf8-decoded blob bytes.
|
|
363
|
-
void diff.stats();
|
|
364
|
-
const deltaInfos = collectDeltaInfos(diff);
|
|
365
|
-
const untracked = await collectUntrackedFiles(r);
|
|
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);
|
|
366
258
|
const fileMeta = [];
|
|
367
259
|
const filePatchByPath = new Map();
|
|
368
260
|
const fileBinaryByPath = new Map();
|
|
@@ -371,52 +263,40 @@ const buildDiffCache = async (r, baseRef) => {
|
|
|
371
263
|
let additions = 0;
|
|
372
264
|
let deletions = 0;
|
|
373
265
|
let binaries = 0;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const delta = deltaInfos[index];
|
|
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;
|
|
379
270
|
let patchText = null;
|
|
380
271
|
let patchOmitted = false;
|
|
381
|
-
|
|
382
|
-
let fileDeletions;
|
|
383
|
-
if (delta.binary) {
|
|
384
|
-
fileAdditions = 0;
|
|
385
|
-
fileDeletions = 0;
|
|
272
|
+
if (entry.binary) {
|
|
386
273
|
binaries += 1;
|
|
387
274
|
}
|
|
388
275
|
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
|
-
}
|
|
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) {
|
|
280
|
+
patchOmitted = true;
|
|
402
281
|
}
|
|
403
282
|
else {
|
|
404
|
-
|
|
283
|
+
patchText = rawPatch;
|
|
284
|
+
totalPatchBytes += rawPatch.length;
|
|
405
285
|
}
|
|
286
|
+
additions += entry.additions;
|
|
287
|
+
deletions += entry.deletions;
|
|
406
288
|
}
|
|
407
|
-
additions += fileAdditions;
|
|
408
|
-
deletions += fileDeletions;
|
|
409
289
|
fileMeta.push({
|
|
410
|
-
path:
|
|
411
|
-
oldPath
|
|
412
|
-
status
|
|
413
|
-
additions:
|
|
414
|
-
deletions:
|
|
415
|
-
binary:
|
|
290
|
+
path: entry.path,
|
|
291
|
+
oldPath,
|
|
292
|
+
status,
|
|
293
|
+
additions: entry.additions,
|
|
294
|
+
deletions: entry.deletions,
|
|
295
|
+
binary: entry.binary,
|
|
416
296
|
});
|
|
417
|
-
filePatchByPath.set(
|
|
418
|
-
fileBinaryByPath.set(
|
|
419
|
-
filePatchOmittedByPath.set(
|
|
297
|
+
filePatchByPath.set(entry.path, patchText);
|
|
298
|
+
fileBinaryByPath.set(entry.path, entry.binary);
|
|
299
|
+
filePatchOmittedByPath.set(entry.path, patchOmitted);
|
|
420
300
|
}
|
|
421
301
|
for (const file of untracked) {
|
|
422
302
|
const patch = file.binary
|
|
@@ -456,7 +336,7 @@ const buildDiffCache = async (r, baseRef) => {
|
|
|
456
336
|
additions,
|
|
457
337
|
deletions,
|
|
458
338
|
binaries,
|
|
459
|
-
branch: getCurrentBranch(
|
|
339
|
+
branch: await getCurrentBranch(cwd),
|
|
460
340
|
};
|
|
461
341
|
return {
|
|
462
342
|
summary,
|
|
@@ -467,129 +347,56 @@ const buildDiffCache = async (r, baseRef) => {
|
|
|
467
347
|
builtAt: Date.now(),
|
|
468
348
|
};
|
|
469
349
|
};
|
|
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);
|
|
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);
|
|
479
356
|
if (cached)
|
|
480
357
|
return cached;
|
|
481
|
-
const baseRef = resolveEffectiveBaseRef(
|
|
358
|
+
const baseRef = await resolveEffectiveBaseRef(cwd, options);
|
|
482
359
|
if (baseRef === null)
|
|
483
360
|
return null;
|
|
484
|
-
const cache = await buildDiffCache(
|
|
361
|
+
const cache = await buildDiffCache(cwd, baseRef);
|
|
485
362
|
if (cache)
|
|
486
|
-
writeDiffCache(
|
|
363
|
+
writeDiffCache(cwd, options.mode, options.base ?? null, cache);
|
|
487
364
|
return cache;
|
|
488
365
|
};
|
|
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
366
|
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);
|
|
367
|
+
const cached = readDiffCache(cwd, options.mode, options.base ?? null);
|
|
554
368
|
if (cached)
|
|
555
369
|
return cached.summary;
|
|
370
|
+
if (!(await isGitRepo(cwd)))
|
|
371
|
+
return EMPTY_SUMMARY;
|
|
556
372
|
try {
|
|
557
|
-
const baseRef = resolveEffectiveBaseRef(
|
|
373
|
+
const baseRef = await resolveEffectiveBaseRef(cwd, options);
|
|
558
374
|
if (baseRef === null)
|
|
559
375
|
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
560
|
-
const branch = getCurrentBranch(
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
diff = r.repo.diffTreeToWorkdirWithIndex(baseTree);
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
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)
|
|
569
381
|
return { ...EMPTY_SUMMARY, isRepo: true };
|
|
570
|
-
|
|
571
|
-
|
|
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);
|
|
382
|
+
let additions = 0;
|
|
383
|
+
let deletions = 0;
|
|
580
384
|
let binaries = 0;
|
|
581
|
-
let fileCount =
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
binaries
|
|
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;
|
|
586
391
|
}
|
|
392
|
+
additions += entry.additions;
|
|
393
|
+
deletions += entry.deletions;
|
|
587
394
|
}
|
|
588
|
-
const untracked = await collectUntrackedFiles(
|
|
395
|
+
const untracked = await collectUntrackedFiles(cwd);
|
|
589
396
|
for (const file of untracked) {
|
|
590
|
-
fileCount
|
|
397
|
+
fileCount += 1;
|
|
591
398
|
if (file.binary) {
|
|
592
|
-
binaries
|
|
399
|
+
binaries += 1;
|
|
593
400
|
}
|
|
594
401
|
else {
|
|
595
402
|
additions += file.lines;
|
|
@@ -602,39 +409,41 @@ export const getGitDiffSummary = async (cwd, options = WORKING_OPTIONS) => {
|
|
|
602
409
|
}
|
|
603
410
|
};
|
|
604
411
|
export const getGitDiff = async (cwd, options = WORKING_OPTIONS) => {
|
|
605
|
-
const
|
|
606
|
-
if (
|
|
412
|
+
const cached = readDiffCache(cwd, options.mode, options.base ?? null);
|
|
413
|
+
if (cached) {
|
|
414
|
+
return { isRepo: true, files: mapCacheFiles(cached) };
|
|
415
|
+
}
|
|
416
|
+
if (!(await isGitRepo(cwd)))
|
|
607
417
|
return { isRepo: false, files: [] };
|
|
608
|
-
const cache = await ensureDiffCache(
|
|
418
|
+
const cache = await ensureDiffCache(cwd, options);
|
|
609
419
|
if (!cache)
|
|
610
420
|
return { isRepo: true, files: [] };
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
};
|
|
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
|
+
}));
|
|
623
433
|
export const getGitDiffFiles = async (cwd, options = WORKING_OPTIONS) => {
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
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)))
|
|
626
438
|
return { isRepo: false, files: [] };
|
|
627
|
-
const cache = await ensureDiffCache(
|
|
439
|
+
const cache = await ensureDiffCache(cwd, options);
|
|
628
440
|
if (!cache)
|
|
629
441
|
return { isRepo: true, files: [] };
|
|
630
442
|
return { isRepo: true, files: cache.fileMeta };
|
|
631
443
|
};
|
|
632
444
|
export const getGitDiffFilePatch = async (cwd, requestedPath, options = WORKING_OPTIONS) => {
|
|
633
445
|
const empty = { patch: null, patchOmitted: false, binary: false };
|
|
634
|
-
const
|
|
635
|
-
if (!r)
|
|
636
|
-
return empty;
|
|
637
|
-
const cache = await ensureDiffCache(r, options);
|
|
446
|
+
const cache = await ensureDiffCache(cwd, options);
|
|
638
447
|
if (!cache)
|
|
639
448
|
return empty;
|
|
640
449
|
// O(1) lookup: the full diff pass that the per-file patch needs was already
|
|
@@ -650,11 +459,11 @@ export const getGitDiffFilePatch = async (cwd, requestedPath, options = WORKING_
|
|
|
650
459
|
}
|
|
651
460
|
// An untracked path the cache didn't cover (created between the cache build
|
|
652
461
|
// and this request) falls back to synthesizing from the working tree.
|
|
653
|
-
return getGitDiffFilePatchFromWorkingTree(
|
|
462
|
+
return getGitDiffFilePatchFromWorkingTree(cwd, requestedPath);
|
|
654
463
|
};
|
|
655
|
-
const getGitDiffFilePatchFromWorkingTree = async (
|
|
464
|
+
const getGitDiffFilePatchFromWorkingTree = async (cwd, requestedPath) => {
|
|
656
465
|
const empty = { patch: null, patchOmitted: false, binary: false };
|
|
657
|
-
const absolutePath = path.join(
|
|
466
|
+
const absolutePath = path.join(cwd, requestedPath);
|
|
658
467
|
try {
|
|
659
468
|
const stat = fs.statSync(absolutePath);
|
|
660
469
|
if (!stat.isFile())
|
|
@@ -720,34 +529,33 @@ let activePrFetcher = defaultPrFetcher;
|
|
|
720
529
|
export const setPrFetcher = (fetcher) => {
|
|
721
530
|
activePrFetcher = fetcher;
|
|
722
531
|
};
|
|
723
|
-
const parseGithubRemotes = (
|
|
532
|
+
const parseGithubRemotes = async (cwd) => {
|
|
533
|
+
const result = await runGit(cwd, ["remote", "-v"]);
|
|
534
|
+
if (result.exitCode !== 0)
|
|
535
|
+
return [];
|
|
724
536
|
const raw = [];
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
catch {
|
|
742
|
-
// 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 });
|
|
743
551
|
}
|
|
744
552
|
return memoBy(raw, (remote) => `${remote.name} ${remote.slug}`);
|
|
745
553
|
};
|
|
746
|
-
const detectPr = async (
|
|
747
|
-
const currentBranch = getCurrentBranch(
|
|
554
|
+
const detectPr = async (cwd) => {
|
|
555
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
748
556
|
if (!currentBranch)
|
|
749
557
|
return null;
|
|
750
|
-
const remotes = parseGithubRemotes(
|
|
558
|
+
const remotes = await parseGithubRemotes(cwd);
|
|
751
559
|
if (remotes.length === 0)
|
|
752
560
|
return null;
|
|
753
561
|
const ownRemote = remotes.find((remote) => remote.name === "origin") ?? remotes[0];
|
|
@@ -776,16 +584,32 @@ const detectPr = async (r) => {
|
|
|
776
584
|
}
|
|
777
585
|
: null;
|
|
778
586
|
};
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
|
|
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)
|
|
782
596
|
return [];
|
|
783
|
-
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);
|
|
784
609
|
return memoBy(remotes, (remote) => remote.slug).map((remote) => remote.slug);
|
|
785
610
|
};
|
|
786
611
|
export const getGitBranchInfo = async (cwd) => {
|
|
787
|
-
|
|
788
|
-
if (!r) {
|
|
612
|
+
if (!(await isGitRepo(cwd))) {
|
|
789
613
|
return {
|
|
790
614
|
isRepo: false,
|
|
791
615
|
currentBranch: null,
|
|
@@ -795,42 +619,23 @@ export const getGitBranchInfo = async (cwd) => {
|
|
|
795
619
|
pr: null,
|
|
796
620
|
};
|
|
797
621
|
}
|
|
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);
|
|
622
|
+
const [currentBranch, defaultBase, branches] = await Promise.all([
|
|
623
|
+
getCurrentBranch(cwd),
|
|
624
|
+
resolveDefaultBase(cwd),
|
|
625
|
+
listBranchesByRecency(cwd),
|
|
626
|
+
]);
|
|
821
627
|
return {
|
|
822
628
|
isRepo: true,
|
|
823
629
|
currentBranch,
|
|
824
630
|
defaultBase: defaultBase?.ref ?? null,
|
|
825
631
|
defaultBaseSource: defaultBase?.source ?? null,
|
|
826
|
-
branches
|
|
632
|
+
branches,
|
|
827
633
|
pr: null,
|
|
828
634
|
};
|
|
829
635
|
};
|
|
830
636
|
export const getGitBranchPr = async (cwd) => {
|
|
831
|
-
|
|
832
|
-
if (!r)
|
|
637
|
+
if (!(await isGitRepo(cwd)))
|
|
833
638
|
return null;
|
|
834
|
-
return detectPr(
|
|
639
|
+
return detectPr(cwd);
|
|
835
640
|
};
|
|
836
641
|
//# sourceMappingURL=git-diff.js.map
|