@lumy-pack/line-lore 0.0.2 → 0.0.4
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/README.md +280 -42
- package/dist/cache/file-cache.d.ts +3 -0
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/sharded-cache.d.ts +33 -0
- package/dist/cli.mjs +794 -454
- package/dist/components/TraceResult.d.ts +1 -1
- package/dist/core/blame/blame.d.ts +1 -1
- package/dist/core/core.d.ts +2 -1
- package/dist/core/patch-id/patch-id.d.ts +7 -2
- package/dist/core/pr-lookup/pr-lookup.d.ts +7 -1
- package/dist/index.cjs +767 -429
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +762 -429
- package/dist/platform/github/github-adapter.d.ts +6 -0
- package/dist/platform/github/github-enterprise-adapter.d.ts +2 -0
- package/dist/platform/gitlab/gitlab-adapter.d.ts +6 -0
- package/dist/platform/gitlab/gitlab-self-hosted-adapter.d.ts +2 -0
- package/dist/platform/platform.d.ts +1 -1
- package/dist/platform/scheduler/request-scheduler.d.ts +0 -5
- package/dist/types/graph.d.ts +2 -1
- package/dist/types/pipeline.d.ts +0 -1
- package/dist/types/trace.d.ts +5 -11
- package/dist/version.d.ts +1 -1
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -20,6 +20,190 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
20
20
|
};
|
|
21
21
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
22
|
|
|
23
|
+
// src/cache/sharded-cache.ts
|
|
24
|
+
import {
|
|
25
|
+
mkdir,
|
|
26
|
+
readFile,
|
|
27
|
+
readdir,
|
|
28
|
+
rename,
|
|
29
|
+
rm,
|
|
30
|
+
writeFile
|
|
31
|
+
} from "fs/promises";
|
|
32
|
+
import { homedir } from "os";
|
|
33
|
+
import { join } from "path";
|
|
34
|
+
function getShardPrefix(key) {
|
|
35
|
+
return key.slice(0, 2).toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
async function cleanupLegacyCache() {
|
|
38
|
+
const legacyDir = join(homedir(), ".line-lore", "cache");
|
|
39
|
+
try {
|
|
40
|
+
const entries = await readdir(legacyDir, { withFileTypes: true });
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
43
|
+
try {
|
|
44
|
+
await rm(join(legacyDir, entry.name), { force: true });
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
var DEFAULT_CACHE_BASE, DEFAULT_MAX_ENTRIES_PER_SHARD, ShardedCache;
|
|
53
|
+
var init_sharded_cache = __esm({
|
|
54
|
+
"src/cache/sharded-cache.ts"() {
|
|
55
|
+
"use strict";
|
|
56
|
+
DEFAULT_CACHE_BASE = join(homedir(), ".line-lore", "cache");
|
|
57
|
+
DEFAULT_MAX_ENTRIES_PER_SHARD = 1e3;
|
|
58
|
+
ShardedCache = class {
|
|
59
|
+
baseDir;
|
|
60
|
+
maxEntriesPerShard;
|
|
61
|
+
enabled;
|
|
62
|
+
shards = /* @__PURE__ */ new Map();
|
|
63
|
+
constructor(namespace, options) {
|
|
64
|
+
const cacheBase = options?.cacheBase ?? DEFAULT_CACHE_BASE;
|
|
65
|
+
const repoId = options?.repoId ?? {
|
|
66
|
+
host: "_local",
|
|
67
|
+
owner: "_",
|
|
68
|
+
repo: "_default"
|
|
69
|
+
};
|
|
70
|
+
this.baseDir = join(
|
|
71
|
+
cacheBase,
|
|
72
|
+
repoId.host,
|
|
73
|
+
repoId.owner,
|
|
74
|
+
repoId.repo,
|
|
75
|
+
namespace
|
|
76
|
+
);
|
|
77
|
+
this.maxEntriesPerShard = options?.maxEntriesPerShard ?? DEFAULT_MAX_ENTRIES_PER_SHARD;
|
|
78
|
+
this.enabled = options?.enabled ?? true;
|
|
79
|
+
}
|
|
80
|
+
async get(key) {
|
|
81
|
+
if (!this.enabled) return null;
|
|
82
|
+
const data = await this.readShard(getShardPrefix(key));
|
|
83
|
+
const entry = data[key];
|
|
84
|
+
return entry?.value ?? null;
|
|
85
|
+
}
|
|
86
|
+
async has(key) {
|
|
87
|
+
if (!this.enabled) return false;
|
|
88
|
+
const data = await this.readShard(getShardPrefix(key));
|
|
89
|
+
return key in data;
|
|
90
|
+
}
|
|
91
|
+
set(key, value) {
|
|
92
|
+
if (!this.enabled) return Promise.resolve();
|
|
93
|
+
const prefix = getShardPrefix(key);
|
|
94
|
+
const state = this.getShardState(prefix);
|
|
95
|
+
state.writeQueue = state.writeQueue.then(() => this.doSet(prefix, key, value)).catch(() => {
|
|
96
|
+
});
|
|
97
|
+
return state.writeQueue;
|
|
98
|
+
}
|
|
99
|
+
delete(key) {
|
|
100
|
+
if (!this.enabled) return Promise.resolve(false);
|
|
101
|
+
const prefix = getShardPrefix(key);
|
|
102
|
+
const state = this.getShardState(prefix);
|
|
103
|
+
let deleted = false;
|
|
104
|
+
state.writeQueue = state.writeQueue.then(async () => {
|
|
105
|
+
const data = await this.readShard(prefix);
|
|
106
|
+
if (key in data) {
|
|
107
|
+
delete data[key];
|
|
108
|
+
state.store = data;
|
|
109
|
+
await this.writeShard(prefix, data);
|
|
110
|
+
deleted = true;
|
|
111
|
+
}
|
|
112
|
+
}).catch(() => {
|
|
113
|
+
});
|
|
114
|
+
return state.writeQueue.then(() => deleted);
|
|
115
|
+
}
|
|
116
|
+
async clear() {
|
|
117
|
+
this.shards.clear();
|
|
118
|
+
try {
|
|
119
|
+
await rm(this.baseDir, { recursive: true, force: true });
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async size() {
|
|
124
|
+
let total = 0;
|
|
125
|
+
try {
|
|
126
|
+
const files = await readdir(this.baseDir);
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
if (!file.endsWith(".json")) continue;
|
|
129
|
+
const prefix = file.replace(".json", "");
|
|
130
|
+
const data = await this.readShard(prefix);
|
|
131
|
+
total += Object.keys(data).length;
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
return total;
|
|
136
|
+
}
|
|
137
|
+
async destroy() {
|
|
138
|
+
this.shards.clear();
|
|
139
|
+
try {
|
|
140
|
+
await rm(this.baseDir, { recursive: true, force: true });
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
getShardState(prefix) {
|
|
145
|
+
let state = this.shards.get(prefix);
|
|
146
|
+
if (!state) {
|
|
147
|
+
state = { store: null, writeQueue: Promise.resolve() };
|
|
148
|
+
this.shards.set(prefix, state);
|
|
149
|
+
}
|
|
150
|
+
return state;
|
|
151
|
+
}
|
|
152
|
+
async doSet(prefix, key, value) {
|
|
153
|
+
const state = this.getShardState(prefix);
|
|
154
|
+
const data = await this.readShard(prefix);
|
|
155
|
+
data[key] = { key, value, createdAt: Date.now() };
|
|
156
|
+
const keys = Object.keys(data);
|
|
157
|
+
if (keys.length > this.maxEntriesPerShard) {
|
|
158
|
+
const sorted = keys.sort((a, b) => data[a].createdAt - data[b].createdAt);
|
|
159
|
+
const toRemove = sorted.slice(0, keys.length - this.maxEntriesPerShard);
|
|
160
|
+
for (const k of toRemove) {
|
|
161
|
+
delete data[k];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
state.store = data;
|
|
165
|
+
await this.writeShard(prefix, data);
|
|
166
|
+
}
|
|
167
|
+
async readShard(prefix) {
|
|
168
|
+
const state = this.getShardState(prefix);
|
|
169
|
+
if (state.store !== null) return state.store;
|
|
170
|
+
const filePath = join(this.baseDir, `${prefix}.json`);
|
|
171
|
+
try {
|
|
172
|
+
const content = await readFile(filePath, "utf-8");
|
|
173
|
+
state.store = JSON.parse(content);
|
|
174
|
+
return state.store;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ERR_INVALID_JSON") {
|
|
177
|
+
console.warn(
|
|
178
|
+
`[line-lore] Cache shard corrupted, resetting: ${filePath}`
|
|
179
|
+
);
|
|
180
|
+
state.store = {};
|
|
181
|
+
await this.writeShard(prefix, {});
|
|
182
|
+
return state.store;
|
|
183
|
+
}
|
|
184
|
+
state.store = {};
|
|
185
|
+
return state.store;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async writeShard(prefix, data) {
|
|
189
|
+
await mkdir(this.baseDir, { recursive: true });
|
|
190
|
+
const filePath = join(this.baseDir, `${prefix}.json`);
|
|
191
|
+
const tmpPath = `${filePath}.tmp`;
|
|
192
|
+
await writeFile(tmpPath, JSON.stringify(data), "utf-8");
|
|
193
|
+
await rename(tmpPath, filePath);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// src/cache/index.ts
|
|
200
|
+
var init_cache = __esm({
|
|
201
|
+
"src/cache/index.ts"() {
|
|
202
|
+
"use strict";
|
|
203
|
+
init_sharded_cache();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
23
207
|
// src/errors.ts
|
|
24
208
|
var LineLoreErrorCode, LineLoreError;
|
|
25
209
|
var init_errors = __esm({
|
|
@@ -84,9 +268,10 @@ async function execCommand(command, args, options, errorCode) {
|
|
|
84
268
|
});
|
|
85
269
|
const exitCode = result.exitCode ?? 0;
|
|
86
270
|
if (exitCode !== 0 && !allowExitCodes.includes(exitCode)) {
|
|
271
|
+
const isNotGitRepo = exitCode === 128 && command === "git" && /not a git repository/i.test(result.stderr);
|
|
87
272
|
throw new LineLoreError(
|
|
88
|
-
failCode,
|
|
89
|
-
`${command} ${args[0]} failed with exit code ${exitCode}: ${result.stderr}`,
|
|
273
|
+
isNotGitRepo ? LineLoreErrorCode.NOT_GIT_REPO : failCode,
|
|
274
|
+
isNotGitRepo ? `Not a git repository: ${result.stderr.trim()}` : `${command} ${args[0]} failed with exit code ${exitCode}: ${result.stderr}`,
|
|
90
275
|
{ command, args, exitCode, stderr: result.stderr, cwd }
|
|
91
276
|
);
|
|
92
277
|
}
|
|
@@ -135,115 +320,27 @@ var init_executor = __esm({
|
|
|
135
320
|
}
|
|
136
321
|
});
|
|
137
322
|
|
|
138
|
-
// src/cache/file-cache.ts
|
|
139
|
-
import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
|
|
140
|
-
import { homedir } from "os";
|
|
141
|
-
import { join } from "path";
|
|
142
|
-
var DEFAULT_CACHE_DIR, DEFAULT_MAX_ENTRIES, FileCache;
|
|
143
|
-
var init_file_cache = __esm({
|
|
144
|
-
"src/cache/file-cache.ts"() {
|
|
145
|
-
"use strict";
|
|
146
|
-
DEFAULT_CACHE_DIR = join(homedir(), ".line-lore", "cache");
|
|
147
|
-
DEFAULT_MAX_ENTRIES = 1e4;
|
|
148
|
-
FileCache = class {
|
|
149
|
-
filePath;
|
|
150
|
-
maxEntries;
|
|
151
|
-
writeQueue = Promise.resolve();
|
|
152
|
-
constructor(fileName, options) {
|
|
153
|
-
const cacheDir = options?.cacheDir ?? DEFAULT_CACHE_DIR;
|
|
154
|
-
this.filePath = join(cacheDir, fileName);
|
|
155
|
-
this.maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
156
|
-
}
|
|
157
|
-
async get(key) {
|
|
158
|
-
const data = await this.readStore();
|
|
159
|
-
const entry = data[key];
|
|
160
|
-
return entry?.value ?? null;
|
|
161
|
-
}
|
|
162
|
-
async has(key) {
|
|
163
|
-
const data = await this.readStore();
|
|
164
|
-
return key in data;
|
|
165
|
-
}
|
|
166
|
-
set(key, value) {
|
|
167
|
-
this.writeQueue = this.writeQueue.then(() => this.doSet(key, value)).catch(() => {
|
|
168
|
-
});
|
|
169
|
-
return this.writeQueue;
|
|
170
|
-
}
|
|
171
|
-
delete(key) {
|
|
172
|
-
let deleted = false;
|
|
173
|
-
this.writeQueue = this.writeQueue.then(async () => {
|
|
174
|
-
const data = await this.readStore();
|
|
175
|
-
if (key in data) {
|
|
176
|
-
delete data[key];
|
|
177
|
-
await this.writeStore(data);
|
|
178
|
-
deleted = true;
|
|
179
|
-
}
|
|
180
|
-
}).catch(() => {
|
|
181
|
-
});
|
|
182
|
-
return this.writeQueue.then(() => deleted);
|
|
183
|
-
}
|
|
184
|
-
clear() {
|
|
185
|
-
this.writeQueue = this.writeQueue.then(() => this.writeStore({})).catch(() => {
|
|
186
|
-
});
|
|
187
|
-
return this.writeQueue;
|
|
188
|
-
}
|
|
189
|
-
async size() {
|
|
190
|
-
const data = await this.readStore();
|
|
191
|
-
return Object.keys(data).length;
|
|
192
|
-
}
|
|
193
|
-
async doSet(key, value) {
|
|
194
|
-
const data = await this.readStore();
|
|
195
|
-
data[key] = { key, value, createdAt: Date.now() };
|
|
196
|
-
const keys = Object.keys(data);
|
|
197
|
-
if (keys.length > this.maxEntries) {
|
|
198
|
-
const sorted = keys.sort((a, b) => data[a].createdAt - data[b].createdAt);
|
|
199
|
-
const toRemove = sorted.slice(0, keys.length - this.maxEntries);
|
|
200
|
-
for (const k of toRemove) {
|
|
201
|
-
delete data[k];
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
await this.writeStore(data);
|
|
205
|
-
}
|
|
206
|
-
async readStore() {
|
|
207
|
-
try {
|
|
208
|
-
const content = await readFile(this.filePath, "utf-8");
|
|
209
|
-
return JSON.parse(content);
|
|
210
|
-
} catch (error) {
|
|
211
|
-
if (error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ERR_INVALID_JSON") {
|
|
212
|
-
console.warn(
|
|
213
|
-
`[line-lore] Cache file corrupted, resetting: ${this.filePath}`
|
|
214
|
-
);
|
|
215
|
-
await this.writeStore({});
|
|
216
|
-
return {};
|
|
217
|
-
}
|
|
218
|
-
return {};
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
async writeStore(data) {
|
|
222
|
-
const dir = join(this.filePath, "..");
|
|
223
|
-
await mkdir(dir, { recursive: true });
|
|
224
|
-
const tmpPath = `${this.filePath}.tmp`;
|
|
225
|
-
await writeFile(tmpPath, JSON.stringify(data), "utf-8");
|
|
226
|
-
await rename(tmpPath, this.filePath);
|
|
227
|
-
}
|
|
228
|
-
async destroy() {
|
|
229
|
-
try {
|
|
230
|
-
await unlink(this.filePath);
|
|
231
|
-
} catch {
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
|
|
238
323
|
// src/core/ancestry/ancestry.ts
|
|
324
|
+
import { filter as filter4, isTruthy as isTruthy4 } from "@winglet/common-utils";
|
|
239
325
|
async function findMergeCommit(commitSha, options) {
|
|
240
326
|
const ref = options?.ref ?? "HEAD";
|
|
327
|
+
const firstParentResult = await findMergeCommitWithArgs(
|
|
328
|
+
commitSha,
|
|
329
|
+
ref,
|
|
330
|
+
["--first-parent"],
|
|
331
|
+
options
|
|
332
|
+
);
|
|
333
|
+
if (firstParentResult) return firstParentResult;
|
|
334
|
+
return findMergeCommitWithArgs(commitSha, ref, [], options);
|
|
335
|
+
}
|
|
336
|
+
async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
|
|
241
337
|
try {
|
|
242
338
|
const result = await gitExec(
|
|
243
339
|
[
|
|
244
340
|
"log",
|
|
245
341
|
"--merges",
|
|
246
342
|
"--ancestry-path",
|
|
343
|
+
...extraArgs,
|
|
247
344
|
`${commitSha}..${ref}`,
|
|
248
345
|
"--topo-order",
|
|
249
346
|
"--reverse",
|
|
@@ -251,28 +348,30 @@ async function findMergeCommit(commitSha, options) {
|
|
|
251
348
|
],
|
|
252
349
|
{ cwd: options?.cwd, timeout: options?.timeout }
|
|
253
350
|
);
|
|
254
|
-
const lines = result.stdout.trim().split("\n")
|
|
351
|
+
const lines = filter4(result.stdout.trim().split("\n"), isTruthy4);
|
|
255
352
|
if (lines.length === 0) return null;
|
|
256
|
-
|
|
257
|
-
const parts = firstLine.split(" ");
|
|
258
|
-
if (parts.length < 3) return null;
|
|
259
|
-
const mergeCommitSha = parts[0];
|
|
260
|
-
const parentShas = [];
|
|
261
|
-
let subjectStart = 1;
|
|
262
|
-
for (let i = 1; i < parts.length; i++) {
|
|
263
|
-
if (/^[0-9a-f]{40}$/.test(parts[i])) {
|
|
264
|
-
parentShas.push(parts[i]);
|
|
265
|
-
subjectStart = i + 1;
|
|
266
|
-
} else {
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
const subject = parts.slice(subjectStart).join(" ");
|
|
271
|
-
return { mergeCommitSha, parentShas, subject };
|
|
353
|
+
return parseMergeLogLine(lines[0]);
|
|
272
354
|
} catch {
|
|
273
355
|
return null;
|
|
274
356
|
}
|
|
275
357
|
}
|
|
358
|
+
function parseMergeLogLine(line) {
|
|
359
|
+
const parts = line.split(" ");
|
|
360
|
+
if (parts.length < 3) return null;
|
|
361
|
+
const mergeCommitSha = parts[0];
|
|
362
|
+
const parentShas = [];
|
|
363
|
+
let subjectStart = 1;
|
|
364
|
+
for (let i = 1; i < parts.length; i++) {
|
|
365
|
+
if (/^[0-9a-f]{40}$/.test(parts[i])) {
|
|
366
|
+
parentShas.push(parts[i]);
|
|
367
|
+
subjectStart = i + 1;
|
|
368
|
+
} else {
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const subject = parts.slice(subjectStart).join(" ");
|
|
373
|
+
return { mergeCommitSha, parentShas, subject };
|
|
374
|
+
}
|
|
276
375
|
function extractPRFromMergeMessage(subject) {
|
|
277
376
|
const ghMatch = /Merge pull request #(\d+)/.exec(subject);
|
|
278
377
|
if (ghMatch) return parseInt(ghMatch[1], 10);
|
|
@@ -298,14 +397,26 @@ var init_ancestry2 = __esm({
|
|
|
298
397
|
});
|
|
299
398
|
|
|
300
399
|
// src/core/patch-id/patch-id.ts
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
400
|
+
import { filter as filter5, isTruthy as isTruthy5 } from "@winglet/common-utils";
|
|
401
|
+
function repoKey(repoId) {
|
|
402
|
+
return `${repoId.host}/${repoId.owner}/${repoId.repo}`;
|
|
403
|
+
}
|
|
404
|
+
function getCache(repoId, noCache) {
|
|
405
|
+
if (noCache) {
|
|
406
|
+
return new ShardedCache("patch-id", { repoId, enabled: false });
|
|
304
407
|
}
|
|
305
|
-
|
|
408
|
+
const key = repoKey(
|
|
409
|
+
repoId ?? { host: "_local", owner: "_", repo: "_default" }
|
|
410
|
+
);
|
|
411
|
+
let cache = cacheRegistry.get(key);
|
|
412
|
+
if (!cache) {
|
|
413
|
+
cache = new ShardedCache("patch-id", { repoId });
|
|
414
|
+
cacheRegistry.set(key, cache);
|
|
415
|
+
}
|
|
416
|
+
return cache;
|
|
306
417
|
}
|
|
307
418
|
async function computePatchId(commitSha, options) {
|
|
308
|
-
const cache = getCache();
|
|
419
|
+
const cache = getCache(options?.repoId, options?.noCache);
|
|
309
420
|
const cached = await cache.get(commitSha);
|
|
310
421
|
if (cached) return cached;
|
|
311
422
|
try {
|
|
@@ -336,7 +447,7 @@ async function findPatchIdMatch(commitSha, options) {
|
|
|
336
447
|
["log", "--format=%H", `-${scanDepth}`, ref],
|
|
337
448
|
{ cwd: options?.cwd, timeout: options?.timeout }
|
|
338
449
|
);
|
|
339
|
-
const candidates = logResult.stdout.trim().split("\n")
|
|
450
|
+
const candidates = filter5(logResult.stdout.trim().split("\n"), isTruthy5);
|
|
340
451
|
for (const candidateSha of candidates) {
|
|
341
452
|
if (candidateSha === commitSha) continue;
|
|
342
453
|
const candidatePatchId = await computePatchId(candidateSha, options);
|
|
@@ -349,16 +460,16 @@ async function findPatchIdMatch(commitSha, options) {
|
|
|
349
460
|
return null;
|
|
350
461
|
}
|
|
351
462
|
function resetPatchIdCache() {
|
|
352
|
-
|
|
463
|
+
cacheRegistry.clear();
|
|
353
464
|
}
|
|
354
|
-
var DEFAULT_SCAN_DEPTH,
|
|
465
|
+
var DEFAULT_SCAN_DEPTH, cacheRegistry;
|
|
355
466
|
var init_patch_id = __esm({
|
|
356
467
|
"src/core/patch-id/patch-id.ts"() {
|
|
357
468
|
"use strict";
|
|
358
|
-
|
|
469
|
+
init_cache();
|
|
359
470
|
init_executor();
|
|
360
471
|
DEFAULT_SCAN_DEPTH = 500;
|
|
361
|
-
|
|
472
|
+
cacheRegistry = /* @__PURE__ */ new Map();
|
|
362
473
|
}
|
|
363
474
|
});
|
|
364
475
|
|
|
@@ -377,40 +488,58 @@ var init_patch_id2 = __esm({
|
|
|
377
488
|
});
|
|
378
489
|
|
|
379
490
|
// src/core/pr-lookup/pr-lookup.ts
|
|
380
|
-
function
|
|
381
|
-
|
|
382
|
-
|
|
491
|
+
function repoKey2(repoId) {
|
|
492
|
+
return `${repoId.host}/${repoId.owner}/${repoId.repo}`;
|
|
493
|
+
}
|
|
494
|
+
function getCache2(repoId, noCache) {
|
|
495
|
+
if (noCache) {
|
|
496
|
+
return new ShardedCache("pr", { repoId, enabled: false });
|
|
383
497
|
}
|
|
384
|
-
|
|
498
|
+
const key = repoKey2(
|
|
499
|
+
repoId ?? { host: "_local", owner: "_", repo: "_default" }
|
|
500
|
+
);
|
|
501
|
+
let cache = cacheRegistry2.get(key);
|
|
502
|
+
if (!cache) {
|
|
503
|
+
cache = new ShardedCache("pr", { repoId });
|
|
504
|
+
cacheRegistry2.set(key, cache);
|
|
505
|
+
}
|
|
506
|
+
return cache;
|
|
385
507
|
}
|
|
386
508
|
async function lookupPR(commitSha, adapter, options) {
|
|
387
|
-
const cache = getCache2();
|
|
509
|
+
const cache = getCache2(options?.repoId, options?.noCache);
|
|
388
510
|
const cached = await cache.get(commitSha);
|
|
389
511
|
if (cached) return cached;
|
|
512
|
+
let mergeBasedPR = null;
|
|
390
513
|
const mergeResult = await findMergeCommit(commitSha, options);
|
|
391
514
|
if (mergeResult) {
|
|
392
515
|
const prNumber = extractPRFromMergeMessage(mergeResult.subject);
|
|
393
516
|
if (prNumber) {
|
|
394
517
|
if (adapter) {
|
|
395
518
|
const prInfo = await adapter.getPRForCommit(mergeResult.mergeCommitSha);
|
|
396
|
-
if (prInfo) {
|
|
397
|
-
|
|
398
|
-
return prInfo;
|
|
519
|
+
if (prInfo?.mergedAt) {
|
|
520
|
+
mergeBasedPR = prInfo;
|
|
399
521
|
}
|
|
400
522
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
523
|
+
if (!mergeBasedPR) {
|
|
524
|
+
mergeBasedPR = {
|
|
525
|
+
number: prNumber,
|
|
526
|
+
title: mergeResult.subject,
|
|
527
|
+
author: "",
|
|
528
|
+
url: "",
|
|
529
|
+
mergeCommit: mergeResult.mergeCommitSha,
|
|
530
|
+
baseBranch: ""
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (!options?.deep || mergeBasedPR.mergedAt) {
|
|
534
|
+
await cache.set(commitSha, mergeBasedPR);
|
|
535
|
+
return mergeBasedPR;
|
|
536
|
+
}
|
|
411
537
|
}
|
|
412
538
|
}
|
|
413
|
-
const patchIdMatch = await findPatchIdMatch(commitSha,
|
|
539
|
+
const patchIdMatch = await findPatchIdMatch(commitSha, {
|
|
540
|
+
...options,
|
|
541
|
+
scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
|
|
542
|
+
});
|
|
414
543
|
if (patchIdMatch) {
|
|
415
544
|
const result = await lookupPR(patchIdMatch.matchedSha, adapter, options);
|
|
416
545
|
if (result) {
|
|
@@ -418,9 +547,13 @@ async function lookupPR(commitSha, adapter, options) {
|
|
|
418
547
|
return result;
|
|
419
548
|
}
|
|
420
549
|
}
|
|
550
|
+
if (mergeBasedPR) {
|
|
551
|
+
await cache.set(commitSha, mergeBasedPR);
|
|
552
|
+
return mergeBasedPR;
|
|
553
|
+
}
|
|
421
554
|
if (adapter) {
|
|
422
555
|
const prInfo = await adapter.getPRForCommit(commitSha);
|
|
423
|
-
if (prInfo) {
|
|
556
|
+
if (prInfo?.mergedAt) {
|
|
424
557
|
await cache.set(commitSha, prInfo);
|
|
425
558
|
return prInfo;
|
|
426
559
|
}
|
|
@@ -428,16 +561,17 @@ async function lookupPR(commitSha, adapter, options) {
|
|
|
428
561
|
return null;
|
|
429
562
|
}
|
|
430
563
|
function resetPRCache() {
|
|
431
|
-
|
|
564
|
+
cacheRegistry2.clear();
|
|
432
565
|
}
|
|
433
|
-
var
|
|
566
|
+
var cacheRegistry2, DEEP_SCAN_DEPTH;
|
|
434
567
|
var init_pr_lookup = __esm({
|
|
435
568
|
"src/core/pr-lookup/pr-lookup.ts"() {
|
|
436
569
|
"use strict";
|
|
437
|
-
|
|
570
|
+
init_cache();
|
|
438
571
|
init_ancestry2();
|
|
439
572
|
init_patch_id2();
|
|
440
|
-
|
|
573
|
+
cacheRegistry2 = /* @__PURE__ */ new Map();
|
|
574
|
+
DEEP_SCAN_DEPTH = 2e3;
|
|
441
575
|
}
|
|
442
576
|
});
|
|
443
577
|
|
|
@@ -454,6 +588,15 @@ var init_pr_lookup2 = __esm({
|
|
|
454
588
|
}
|
|
455
589
|
});
|
|
456
590
|
|
|
591
|
+
// src/version.ts
|
|
592
|
+
var VERSION;
|
|
593
|
+
var init_version = __esm({
|
|
594
|
+
"src/version.ts"() {
|
|
595
|
+
"use strict";
|
|
596
|
+
VERSION = "0.0.4";
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
457
600
|
// src/output/normalizer.ts
|
|
458
601
|
var normalizer_exports = {};
|
|
459
602
|
__export(normalizer_exports, {
|
|
@@ -465,7 +608,7 @@ function createSuccessResponse(command, data, operatingLevel, options) {
|
|
|
465
608
|
return {
|
|
466
609
|
tool: "line-lore",
|
|
467
610
|
command,
|
|
468
|
-
version:
|
|
611
|
+
version: VERSION,
|
|
469
612
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
470
613
|
status: "success",
|
|
471
614
|
operatingLevel,
|
|
@@ -478,7 +621,7 @@ function createPartialResponse(command, partialData, operatingLevel, warnings) {
|
|
|
478
621
|
return {
|
|
479
622
|
tool: "line-lore",
|
|
480
623
|
command,
|
|
481
|
-
version:
|
|
624
|
+
version: VERSION,
|
|
482
625
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
483
626
|
status: "partial",
|
|
484
627
|
operatingLevel,
|
|
@@ -490,7 +633,7 @@ function createErrorResponse(command, code, message, operatingLevel, options) {
|
|
|
490
633
|
return {
|
|
491
634
|
tool: "line-lore",
|
|
492
635
|
command,
|
|
493
|
-
version:
|
|
636
|
+
version: VERSION,
|
|
494
637
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
495
638
|
status: "error",
|
|
496
639
|
operatingLevel,
|
|
@@ -503,16 +646,10 @@ function createErrorResponse(command, code, message, operatingLevel, options) {
|
|
|
503
646
|
}
|
|
504
647
|
};
|
|
505
648
|
}
|
|
506
|
-
function getVersion() {
|
|
507
|
-
try {
|
|
508
|
-
return "0.0.1";
|
|
509
|
-
} catch {
|
|
510
|
-
return "unknown";
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
649
|
var init_normalizer = __esm({
|
|
514
650
|
"src/output/normalizer.ts"() {
|
|
515
651
|
"use strict";
|
|
652
|
+
init_version();
|
|
516
653
|
}
|
|
517
654
|
});
|
|
518
655
|
|
|
@@ -551,7 +688,12 @@ function respondError(command, code, message, startTime, version2, details) {
|
|
|
551
688
|
// src/cli.ts
|
|
552
689
|
import { Command } from "commander";
|
|
553
690
|
|
|
691
|
+
// src/core/core.ts
|
|
692
|
+
import { createHash as createHash2 } from "crypto";
|
|
693
|
+
import { map as map8 } from "@winglet/common-utils";
|
|
694
|
+
|
|
554
695
|
// src/ast/parser.ts
|
|
696
|
+
import { isNil } from "@winglet/common-utils";
|
|
555
697
|
var astGrep = null;
|
|
556
698
|
var loadAttempted = false;
|
|
557
699
|
var available = false;
|
|
@@ -602,7 +744,7 @@ async function findSymbols(source, lang) {
|
|
|
602
744
|
try {
|
|
603
745
|
const { parse, Lang } = astGrep;
|
|
604
746
|
const langEnum = Lang[lang] ?? Lang[lang.charAt(0).toUpperCase() + lang.slice(1)];
|
|
605
|
-
if (langEnum
|
|
747
|
+
if (isNil(langEnum)) return [];
|
|
606
748
|
const root = parse(langEnum, source).root();
|
|
607
749
|
const symbols = [];
|
|
608
750
|
const kindPatterns = [
|
|
@@ -754,8 +896,14 @@ function findPythonBlockEnd(lines, startIdx) {
|
|
|
754
896
|
return lines.length - 1;
|
|
755
897
|
}
|
|
756
898
|
|
|
899
|
+
// src/core/core.ts
|
|
900
|
+
init_cache();
|
|
901
|
+
init_errors();
|
|
902
|
+
init_executor();
|
|
903
|
+
|
|
757
904
|
// src/git/health.ts
|
|
758
905
|
init_executor();
|
|
906
|
+
import { map } from "@winglet/common-utils";
|
|
759
907
|
var GIT_VERSION_PATTERN = /git version (\d+\.\d+\.\d+)/;
|
|
760
908
|
var BLOOM_FILTER_MIN_VERSION = [2, 27, 0];
|
|
761
909
|
function parseGitVersion(versionStr) {
|
|
@@ -763,7 +911,7 @@ function parseGitVersion(versionStr) {
|
|
|
763
911
|
return match?.[1] ?? "0.0.0";
|
|
764
912
|
}
|
|
765
913
|
function isVersionAtLeast(version2, minVersion) {
|
|
766
|
-
const parts = version2.split(".")
|
|
914
|
+
const parts = map(version2.split("."), Number);
|
|
767
915
|
for (let i = 0; i < 3; i++) {
|
|
768
916
|
if ((parts[i] ?? 0) > minVersion[i]) return true;
|
|
769
917
|
if ((parts[i] ?? 0) < minVersion[i]) return false;
|
|
@@ -835,13 +983,12 @@ async function getRemoteInfo(remoteName = "origin", options) {
|
|
|
835
983
|
|
|
836
984
|
// src/platform/github/github-adapter.ts
|
|
837
985
|
init_executor();
|
|
986
|
+
import { filter, isArray, isTruthy, map as map2 } from "@winglet/common-utils";
|
|
838
987
|
|
|
839
988
|
// src/platform/scheduler/request-scheduler.ts
|
|
840
989
|
var RequestScheduler = class {
|
|
841
990
|
rateLimitInfo = null;
|
|
842
991
|
threshold;
|
|
843
|
-
etagCache = /* @__PURE__ */ new Map();
|
|
844
|
-
responseCache = /* @__PURE__ */ new Map();
|
|
845
992
|
constructor(options) {
|
|
846
993
|
this.threshold = options?.rateLimitThreshold ?? 10;
|
|
847
994
|
}
|
|
@@ -855,16 +1002,6 @@ var RequestScheduler = class {
|
|
|
855
1002
|
getRateLimit() {
|
|
856
1003
|
return this.rateLimitInfo;
|
|
857
1004
|
}
|
|
858
|
-
setEtag(url, etag, response) {
|
|
859
|
-
this.etagCache.set(url, etag);
|
|
860
|
-
this.responseCache.set(url, response);
|
|
861
|
-
}
|
|
862
|
-
getEtag(url) {
|
|
863
|
-
return this.etagCache.get(url) ?? null;
|
|
864
|
-
}
|
|
865
|
-
getCachedResponse(url) {
|
|
866
|
-
return this.responseCache.get(url) ?? null;
|
|
867
|
-
}
|
|
868
1005
|
};
|
|
869
1006
|
|
|
870
1007
|
// src/platform/github/github-adapter.ts
|
|
@@ -872,9 +1009,14 @@ var GitHubAdapter = class {
|
|
|
872
1009
|
platform = "github";
|
|
873
1010
|
scheduler;
|
|
874
1011
|
hostname;
|
|
1012
|
+
defaultBranchCache = null;
|
|
1013
|
+
remoteName;
|
|
1014
|
+
cwd;
|
|
875
1015
|
constructor(options) {
|
|
876
1016
|
this.hostname = options?.hostname ?? "github.com";
|
|
877
1017
|
this.scheduler = options?.scheduler ?? new RequestScheduler();
|
|
1018
|
+
this.remoteName = options?.remoteName ?? "origin";
|
|
1019
|
+
this.cwd = options?.cwd;
|
|
878
1020
|
}
|
|
879
1021
|
async checkAuth() {
|
|
880
1022
|
try {
|
|
@@ -882,6 +1024,7 @@ var GitHubAdapter = class {
|
|
|
882
1024
|
"gh",
|
|
883
1025
|
["auth", "token", "--hostname", this.hostname],
|
|
884
1026
|
{
|
|
1027
|
+
cwd: this.cwd,
|
|
885
1028
|
allowExitCodes: [1]
|
|
886
1029
|
}
|
|
887
1030
|
);
|
|
@@ -897,64 +1040,98 @@ var GitHubAdapter = class {
|
|
|
897
1040
|
async getPRForCommit(sha) {
|
|
898
1041
|
if (this.scheduler.isRateLimited()) return null;
|
|
899
1042
|
try {
|
|
900
|
-
const result = await shellExec(
|
|
901
|
-
"
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1043
|
+
const result = await shellExec(
|
|
1044
|
+
"gh",
|
|
1045
|
+
[
|
|
1046
|
+
"api",
|
|
1047
|
+
`repos/{owner}/{repo}/commits/${sha}/pulls`,
|
|
1048
|
+
"--hostname",
|
|
1049
|
+
this.hostname,
|
|
1050
|
+
"--jq",
|
|
1051
|
+
"[.[] | select(.merged_at != null) | {number, title, user: .user.login, html_url, merge_commit_sha, base: .base.ref, merged_at}] | sort_by(.merged_at)"
|
|
1052
|
+
],
|
|
1053
|
+
{ cwd: this.cwd }
|
|
1054
|
+
);
|
|
1055
|
+
const prs = JSON.parse(result.stdout);
|
|
1056
|
+
if (!isArray(prs) || prs.length === 0) return null;
|
|
1057
|
+
const defaultBranch = await this.detectDefaultBranch();
|
|
1058
|
+
const defaultBranchPR = prs.find(
|
|
1059
|
+
(pr) => pr.base === defaultBranch
|
|
1060
|
+
);
|
|
1061
|
+
const data = defaultBranchPR ?? prs[0];
|
|
910
1062
|
return {
|
|
911
1063
|
number: data.number,
|
|
912
1064
|
title: data.title ?? "",
|
|
913
1065
|
author: data.user ?? "",
|
|
914
1066
|
url: data.html_url ?? "",
|
|
915
1067
|
mergeCommit: data.merge_commit_sha ?? sha,
|
|
916
|
-
baseBranch: data.base ??
|
|
1068
|
+
baseBranch: data.base ?? defaultBranch,
|
|
917
1069
|
mergedAt: data.merged_at
|
|
918
1070
|
};
|
|
919
1071
|
} catch {
|
|
920
1072
|
return null;
|
|
921
1073
|
}
|
|
922
1074
|
}
|
|
1075
|
+
async detectDefaultBranch() {
|
|
1076
|
+
if (this.defaultBranchCache) return this.defaultBranchCache;
|
|
1077
|
+
try {
|
|
1078
|
+
const result = await gitExec(
|
|
1079
|
+
["symbolic-ref", `refs/remotes/${this.remoteName}/HEAD`],
|
|
1080
|
+
{ cwd: this.cwd }
|
|
1081
|
+
);
|
|
1082
|
+
const ref = result.stdout.trim();
|
|
1083
|
+
this.defaultBranchCache = ref.replace(`refs/remotes/${this.remoteName}/`, "") || "main";
|
|
1084
|
+
return this.defaultBranchCache;
|
|
1085
|
+
} catch {
|
|
1086
|
+
return "main";
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
923
1089
|
async getPRCommits(prNumber) {
|
|
924
1090
|
try {
|
|
925
|
-
const result = await shellExec(
|
|
926
|
-
"
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1091
|
+
const result = await shellExec(
|
|
1092
|
+
"gh",
|
|
1093
|
+
[
|
|
1094
|
+
"api",
|
|
1095
|
+
`repos/{owner}/{repo}/pulls/${prNumber}/commits`,
|
|
1096
|
+
"--hostname",
|
|
1097
|
+
this.hostname,
|
|
1098
|
+
"--jq",
|
|
1099
|
+
".[].sha"
|
|
1100
|
+
],
|
|
1101
|
+
{ cwd: this.cwd }
|
|
1102
|
+
);
|
|
1103
|
+
return filter(result.stdout.trim().split("\n"), isTruthy);
|
|
934
1104
|
} catch {
|
|
935
1105
|
return [];
|
|
936
1106
|
}
|
|
937
1107
|
}
|
|
938
1108
|
async getLinkedIssues(prNumber) {
|
|
939
1109
|
try {
|
|
940
|
-
const result = await shellExec(
|
|
941
|
-
"
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1110
|
+
const result = await shellExec(
|
|
1111
|
+
"gh",
|
|
1112
|
+
[
|
|
1113
|
+
"api",
|
|
1114
|
+
"graphql",
|
|
1115
|
+
"--hostname",
|
|
1116
|
+
this.hostname,
|
|
1117
|
+
"-f",
|
|
1118
|
+
`query=query { repository(owner: "{owner}", name: "{repo}") { pullRequest(number: ${prNumber}) { closingIssuesReferences(first: 10) { nodes { number title url state labels(first: 5) { nodes { name } } } } } } }`,
|
|
1119
|
+
"--jq",
|
|
1120
|
+
".data.repository.pullRequest.closingIssuesReferences.nodes"
|
|
1121
|
+
],
|
|
1122
|
+
{ cwd: this.cwd }
|
|
1123
|
+
);
|
|
950
1124
|
const nodes = JSON.parse(result.stdout);
|
|
951
|
-
if (!
|
|
952
|
-
return nodes
|
|
1125
|
+
if (!isArray(nodes)) return [];
|
|
1126
|
+
return map2(nodes, (node) => ({
|
|
953
1127
|
number: node.number,
|
|
954
1128
|
title: node.title ?? "",
|
|
955
1129
|
url: node.url ?? "",
|
|
956
1130
|
state: (node.state ?? "open").toLowerCase(),
|
|
957
|
-
labels: (
|
|
1131
|
+
labels: map2(
|
|
1132
|
+
node.labels?.nodes ?? [],
|
|
1133
|
+
(l) => l.name
|
|
1134
|
+
)
|
|
958
1135
|
}));
|
|
959
1136
|
} catch {
|
|
960
1137
|
return [];
|
|
@@ -962,23 +1139,28 @@ var GitHubAdapter = class {
|
|
|
962
1139
|
}
|
|
963
1140
|
async getLinkedPRs(issueNumber) {
|
|
964
1141
|
try {
|
|
965
|
-
const result = await shellExec(
|
|
966
|
-
"
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1142
|
+
const result = await shellExec(
|
|
1143
|
+
"gh",
|
|
1144
|
+
[
|
|
1145
|
+
"api",
|
|
1146
|
+
`repos/{owner}/{repo}/issues/${issueNumber}/timeline`,
|
|
1147
|
+
"--hostname",
|
|
1148
|
+
this.hostname,
|
|
1149
|
+
"--jq",
|
|
1150
|
+
"[.[] | select(.source.issue.pull_request) | .source.issue] | map({number, title, user: .user.login, html_url, merge_commit_sha: .pull_request.merge_commit_sha, merged_at: .pull_request.merged_at})"
|
|
1151
|
+
],
|
|
1152
|
+
{ cwd: this.cwd }
|
|
1153
|
+
);
|
|
973
1154
|
const prs = JSON.parse(result.stdout);
|
|
974
|
-
if (!
|
|
975
|
-
|
|
1155
|
+
if (!isArray(prs)) return [];
|
|
1156
|
+
const defaultBranch = await this.detectDefaultBranch();
|
|
1157
|
+
return map2(prs, (pr) => ({
|
|
976
1158
|
number: pr.number,
|
|
977
1159
|
title: pr.title ?? "",
|
|
978
1160
|
author: pr.user ?? "",
|
|
979
1161
|
url: pr.html_url ?? "",
|
|
980
1162
|
mergeCommit: pr.merge_commit_sha ?? "",
|
|
981
|
-
baseBranch:
|
|
1163
|
+
baseBranch: defaultBranch,
|
|
982
1164
|
mergedAt: pr.merged_at
|
|
983
1165
|
}));
|
|
984
1166
|
} catch {
|
|
@@ -987,14 +1169,18 @@ var GitHubAdapter = class {
|
|
|
987
1169
|
}
|
|
988
1170
|
async getRateLimit() {
|
|
989
1171
|
try {
|
|
990
|
-
const result = await shellExec(
|
|
991
|
-
"
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1172
|
+
const result = await shellExec(
|
|
1173
|
+
"gh",
|
|
1174
|
+
[
|
|
1175
|
+
"api",
|
|
1176
|
+
"rate_limit",
|
|
1177
|
+
"--hostname",
|
|
1178
|
+
this.hostname,
|
|
1179
|
+
"--jq",
|
|
1180
|
+
".rate | {limit, remaining, reset}"
|
|
1181
|
+
],
|
|
1182
|
+
{ cwd: this.cwd }
|
|
1183
|
+
);
|
|
998
1184
|
const data = JSON.parse(result.stdout);
|
|
999
1185
|
const info = {
|
|
1000
1186
|
limit: data.limit ?? 5e3,
|
|
@@ -1013,19 +1199,30 @@ var GitHubAdapter = class {
|
|
|
1013
1199
|
var GitHubEnterpriseAdapter = class extends GitHubAdapter {
|
|
1014
1200
|
platform = "github-enterprise";
|
|
1015
1201
|
constructor(hostname, options) {
|
|
1016
|
-
super({
|
|
1202
|
+
super({
|
|
1203
|
+
hostname,
|
|
1204
|
+
scheduler: options?.scheduler,
|
|
1205
|
+
remoteName: options?.remoteName,
|
|
1206
|
+
cwd: options?.cwd
|
|
1207
|
+
});
|
|
1017
1208
|
}
|
|
1018
1209
|
};
|
|
1019
1210
|
|
|
1020
1211
|
// src/platform/gitlab/gitlab-adapter.ts
|
|
1021
1212
|
init_executor();
|
|
1213
|
+
import { filter as filter2, isArray as isArray2, isNotNil, map as map3 } from "@winglet/common-utils";
|
|
1022
1214
|
var GitLabAdapter = class {
|
|
1023
1215
|
platform = "gitlab";
|
|
1024
1216
|
scheduler;
|
|
1025
1217
|
hostname;
|
|
1218
|
+
defaultBranchCache = null;
|
|
1219
|
+
remoteName;
|
|
1220
|
+
cwd;
|
|
1026
1221
|
constructor(options) {
|
|
1027
1222
|
this.hostname = options?.hostname ?? "gitlab.com";
|
|
1028
1223
|
this.scheduler = options?.scheduler ?? new RequestScheduler();
|
|
1224
|
+
this.remoteName = options?.remoteName ?? "origin";
|
|
1225
|
+
this.cwd = options?.cwd;
|
|
1029
1226
|
}
|
|
1030
1227
|
async checkAuth() {
|
|
1031
1228
|
if (process.env.GITLAB_TOKEN) {
|
|
@@ -1035,7 +1232,7 @@ var GitLabAdapter = class {
|
|
|
1035
1232
|
const result = await shellExec(
|
|
1036
1233
|
"glab",
|
|
1037
1234
|
["auth", "status", "--hostname", this.hostname],
|
|
1038
|
-
{ allowExitCodes: [1] }
|
|
1235
|
+
{ cwd: this.cwd, allowExitCodes: [1] }
|
|
1039
1236
|
);
|
|
1040
1237
|
return {
|
|
1041
1238
|
authenticated: result.exitCode === 0,
|
|
@@ -1048,54 +1245,93 @@ var GitLabAdapter = class {
|
|
|
1048
1245
|
async getPRForCommit(sha) {
|
|
1049
1246
|
if (this.scheduler.isRateLimited()) return null;
|
|
1050
1247
|
try {
|
|
1051
|
-
const result = await shellExec(
|
|
1052
|
-
"
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1248
|
+
const result = await shellExec(
|
|
1249
|
+
"glab",
|
|
1250
|
+
[
|
|
1251
|
+
"api",
|
|
1252
|
+
`projects/:id/repository/commits/${sha}/merge_requests`,
|
|
1253
|
+
"--hostname",
|
|
1254
|
+
this.hostname
|
|
1255
|
+
],
|
|
1256
|
+
{ cwd: this.cwd }
|
|
1257
|
+
);
|
|
1057
1258
|
const mrs = JSON.parse(result.stdout);
|
|
1058
|
-
if (!
|
|
1059
|
-
const
|
|
1259
|
+
if (!isArray2(mrs) || mrs.length === 0) return null;
|
|
1260
|
+
const mergedMRs = filter2(
|
|
1261
|
+
mrs,
|
|
1262
|
+
(mr2) => mr2.state === "merged" && isNotNil(mr2.merged_at)
|
|
1263
|
+
).sort((a, b) => {
|
|
1264
|
+
const aTime = new Date(a.merged_at).getTime();
|
|
1265
|
+
const bTime = new Date(b.merged_at).getTime();
|
|
1266
|
+
return aTime - bTime;
|
|
1267
|
+
});
|
|
1268
|
+
if (mergedMRs.length === 0) return null;
|
|
1269
|
+
const defaultBranch = await this.detectDefaultBranch();
|
|
1270
|
+
const defaultBranchMR = mergedMRs.find(
|
|
1271
|
+
(mr2) => mr2.target_branch === defaultBranch
|
|
1272
|
+
);
|
|
1273
|
+
const mr = defaultBranchMR ?? mergedMRs[0];
|
|
1060
1274
|
return {
|
|
1061
1275
|
number: mr.iid,
|
|
1062
1276
|
title: mr.title ?? "",
|
|
1063
1277
|
author: mr.author?.username ?? "",
|
|
1064
1278
|
url: mr.web_url ?? "",
|
|
1065
1279
|
mergeCommit: mr.merge_commit_sha ?? sha,
|
|
1066
|
-
baseBranch: mr.target_branch ??
|
|
1280
|
+
baseBranch: mr.target_branch ?? defaultBranch,
|
|
1067
1281
|
mergedAt: mr.merged_at
|
|
1068
1282
|
};
|
|
1069
1283
|
} catch {
|
|
1070
1284
|
return null;
|
|
1071
1285
|
}
|
|
1072
1286
|
}
|
|
1287
|
+
async detectDefaultBranch() {
|
|
1288
|
+
if (this.defaultBranchCache) return this.defaultBranchCache;
|
|
1289
|
+
try {
|
|
1290
|
+
const result = await gitExec(
|
|
1291
|
+
["symbolic-ref", `refs/remotes/${this.remoteName}/HEAD`],
|
|
1292
|
+
{ cwd: this.cwd }
|
|
1293
|
+
);
|
|
1294
|
+
const ref = result.stdout.trim();
|
|
1295
|
+
this.defaultBranchCache = ref.replace(`refs/remotes/${this.remoteName}/`, "") || "main";
|
|
1296
|
+
return this.defaultBranchCache;
|
|
1297
|
+
} catch {
|
|
1298
|
+
return "main";
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1073
1301
|
async getPRCommits(prNumber) {
|
|
1074
1302
|
try {
|
|
1075
|
-
const result = await shellExec(
|
|
1076
|
-
"
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1303
|
+
const result = await shellExec(
|
|
1304
|
+
"glab",
|
|
1305
|
+
[
|
|
1306
|
+
"api",
|
|
1307
|
+
`projects/:id/merge_requests/${prNumber}/commits`,
|
|
1308
|
+
"--hostname",
|
|
1309
|
+
this.hostname
|
|
1310
|
+
],
|
|
1311
|
+
{ cwd: this.cwd }
|
|
1312
|
+
);
|
|
1081
1313
|
const commits = JSON.parse(result.stdout);
|
|
1082
|
-
if (!
|
|
1083
|
-
return commits
|
|
1314
|
+
if (!isArray2(commits)) return [];
|
|
1315
|
+
return map3(commits, (c) => c.id);
|
|
1084
1316
|
} catch {
|
|
1085
1317
|
return [];
|
|
1086
1318
|
}
|
|
1087
1319
|
}
|
|
1088
1320
|
async getLinkedIssues(prNumber) {
|
|
1089
1321
|
try {
|
|
1090
|
-
const result = await shellExec(
|
|
1091
|
-
"
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1322
|
+
const result = await shellExec(
|
|
1323
|
+
"glab",
|
|
1324
|
+
[
|
|
1325
|
+
"api",
|
|
1326
|
+
`projects/:id/merge_requests/${prNumber}/closes_issues`,
|
|
1327
|
+
"--hostname",
|
|
1328
|
+
this.hostname
|
|
1329
|
+
],
|
|
1330
|
+
{ cwd: this.cwd }
|
|
1331
|
+
);
|
|
1096
1332
|
const issues = JSON.parse(result.stdout);
|
|
1097
|
-
if (!
|
|
1098
|
-
return issues
|
|
1333
|
+
if (!isArray2(issues)) return [];
|
|
1334
|
+
return map3(issues, (issue) => ({
|
|
1099
1335
|
number: issue.iid,
|
|
1100
1336
|
title: issue.title ?? "",
|
|
1101
1337
|
url: issue.web_url ?? "",
|
|
@@ -1108,21 +1344,26 @@ var GitLabAdapter = class {
|
|
|
1108
1344
|
}
|
|
1109
1345
|
async getLinkedPRs(issueNumber) {
|
|
1110
1346
|
try {
|
|
1111
|
-
const result = await shellExec(
|
|
1112
|
-
"
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1347
|
+
const result = await shellExec(
|
|
1348
|
+
"glab",
|
|
1349
|
+
[
|
|
1350
|
+
"api",
|
|
1351
|
+
`projects/:id/issues/${issueNumber}/related_merge_requests`,
|
|
1352
|
+
"--hostname",
|
|
1353
|
+
this.hostname
|
|
1354
|
+
],
|
|
1355
|
+
{ cwd: this.cwd }
|
|
1356
|
+
);
|
|
1117
1357
|
const mrs = JSON.parse(result.stdout);
|
|
1118
|
-
if (!
|
|
1119
|
-
|
|
1358
|
+
if (!isArray2(mrs)) return [];
|
|
1359
|
+
const defaultBranch = await this.detectDefaultBranch();
|
|
1360
|
+
return map3(mrs, (mr) => ({
|
|
1120
1361
|
number: mr.iid,
|
|
1121
1362
|
title: mr.title ?? "",
|
|
1122
1363
|
author: mr.author?.username ?? "",
|
|
1123
1364
|
url: mr.web_url ?? "",
|
|
1124
1365
|
mergeCommit: mr.merge_commit_sha ?? "",
|
|
1125
|
-
baseBranch: mr.target_branch ??
|
|
1366
|
+
baseBranch: mr.target_branch ?? defaultBranch,
|
|
1126
1367
|
mergedAt: mr.merged_at
|
|
1127
1368
|
}));
|
|
1128
1369
|
} catch {
|
|
@@ -1138,7 +1379,12 @@ var GitLabAdapter = class {
|
|
|
1138
1379
|
var GitLabSelfHostedAdapter = class extends GitLabAdapter {
|
|
1139
1380
|
platform = "gitlab-self-hosted";
|
|
1140
1381
|
constructor(hostname, options) {
|
|
1141
|
-
super({
|
|
1382
|
+
super({
|
|
1383
|
+
hostname,
|
|
1384
|
+
scheduler: options?.scheduler,
|
|
1385
|
+
remoteName: options?.remoteName,
|
|
1386
|
+
cwd: options?.cwd
|
|
1387
|
+
});
|
|
1142
1388
|
}
|
|
1143
1389
|
};
|
|
1144
1390
|
|
|
@@ -1147,21 +1393,21 @@ async function detectPlatformAdapter(options) {
|
|
|
1147
1393
|
const remote = await getRemoteInfo(options?.remoteName, {
|
|
1148
1394
|
cwd: options?.cwd
|
|
1149
1395
|
});
|
|
1150
|
-
const adapter = createAdapter(remote);
|
|
1396
|
+
const adapter = createAdapter(remote, options?.remoteName, options?.cwd);
|
|
1151
1397
|
return { adapter, remote };
|
|
1152
1398
|
}
|
|
1153
|
-
function createAdapter(remote) {
|
|
1399
|
+
function createAdapter(remote, remoteName, cwd) {
|
|
1154
1400
|
switch (remote.platform) {
|
|
1155
1401
|
case "github":
|
|
1156
|
-
return new GitHubAdapter({ hostname: remote.host });
|
|
1402
|
+
return new GitHubAdapter({ hostname: remote.host, remoteName, cwd });
|
|
1157
1403
|
case "github-enterprise":
|
|
1158
|
-
return new GitHubEnterpriseAdapter(remote.host);
|
|
1404
|
+
return new GitHubEnterpriseAdapter(remote.host, { remoteName, cwd });
|
|
1159
1405
|
case "gitlab":
|
|
1160
|
-
return new GitLabAdapter({ hostname: remote.host });
|
|
1406
|
+
return new GitLabAdapter({ hostname: remote.host, remoteName, cwd });
|
|
1161
1407
|
case "gitlab-self-hosted":
|
|
1162
|
-
return new GitLabSelfHostedAdapter(remote.host);
|
|
1408
|
+
return new GitLabSelfHostedAdapter(remote.host, { remoteName, cwd });
|
|
1163
1409
|
case "unknown":
|
|
1164
|
-
return new GitHubEnterpriseAdapter(remote.host);
|
|
1410
|
+
return new GitHubEnterpriseAdapter(remote.host, { remoteName, cwd });
|
|
1165
1411
|
}
|
|
1166
1412
|
}
|
|
1167
1413
|
|
|
@@ -1208,6 +1454,7 @@ function parseLineNumber(value, originalInput) {
|
|
|
1208
1454
|
}
|
|
1209
1455
|
|
|
1210
1456
|
// src/core/ast-diff/ast-diff.ts
|
|
1457
|
+
import { isTruthy as isTruthy2, map as map4 } from "@winglet/common-utils";
|
|
1211
1458
|
init_executor();
|
|
1212
1459
|
|
|
1213
1460
|
// src/core/ast-diff/comparison/structure-comparator.ts
|
|
@@ -1249,13 +1496,13 @@ function compareSymbolMaps(current, parent) {
|
|
|
1249
1496
|
}
|
|
1250
1497
|
return results;
|
|
1251
1498
|
}
|
|
1252
|
-
function findHashMatch(target,
|
|
1253
|
-
for (const [name, hash] of
|
|
1499
|
+
function findHashMatch(target, map9) {
|
|
1500
|
+
for (const [name, hash] of map9) {
|
|
1254
1501
|
if (hash.exact === target.exact) {
|
|
1255
1502
|
return { name, confidence: "exact" };
|
|
1256
1503
|
}
|
|
1257
1504
|
}
|
|
1258
|
-
for (const [name, hash] of
|
|
1505
|
+
for (const [name, hash] of map9) {
|
|
1259
1506
|
if (hash.structural === target.structural) {
|
|
1260
1507
|
return { name, confidence: "structural" };
|
|
1261
1508
|
}
|
|
@@ -1417,10 +1664,6 @@ var KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
1417
1664
|
// src/core/ast-diff/ast-diff.ts
|
|
1418
1665
|
var MAX_TRAVERSAL_DEPTH = 50;
|
|
1419
1666
|
async function traceByAst(file, line, startCommitSha, options) {
|
|
1420
|
-
if (!isAstAvailable()) {
|
|
1421
|
-
const lang2 = detectLanguage(file);
|
|
1422
|
-
if (!lang2) return null;
|
|
1423
|
-
}
|
|
1424
1667
|
const lang = detectLanguage(file);
|
|
1425
1668
|
if (!lang) return null;
|
|
1426
1669
|
const maxDepth = options?.maxDepth ?? MAX_TRAVERSAL_DEPTH;
|
|
@@ -1444,10 +1687,10 @@ async function traceByAst(file, line, startCommitSha, options) {
|
|
|
1444
1687
|
const parentContent = await getFileAtCommit(parentSha, file, options);
|
|
1445
1688
|
const parentSymbols = await extractSymbols(parentContent, lang);
|
|
1446
1689
|
const currentMap = new Map(
|
|
1447
|
-
[currentSymbol].filter(
|
|
1690
|
+
[currentSymbol].filter(isTruthy2).map((s) => [s.name, computeContentHash(s.bodyText)])
|
|
1448
1691
|
);
|
|
1449
1692
|
const parentMap = new Map(
|
|
1450
|
-
parentSymbols
|
|
1693
|
+
map4(parentSymbols, (s) => [s.name, computeContentHash(s.bodyText)])
|
|
1451
1694
|
);
|
|
1452
1695
|
const comparison = compareSymbolMaps(currentMap, parentMap);
|
|
1453
1696
|
if (comparison.length > 0) {
|
|
@@ -1509,9 +1752,11 @@ async function getParentCommit(sha, options) {
|
|
|
1509
1752
|
|
|
1510
1753
|
// src/core/blame/blame.ts
|
|
1511
1754
|
init_executor();
|
|
1755
|
+
import { forEach as forEach2, map as map6 } from "@winglet/common-utils";
|
|
1512
1756
|
|
|
1513
1757
|
// src/core/blame/detection/cosmetic-detector.ts
|
|
1514
1758
|
init_executor();
|
|
1759
|
+
import { filter as filter3, forEach, isTruthy as isTruthy3, map as map5 } from "@winglet/common-utils";
|
|
1515
1760
|
function isCosmeticDiff(diff) {
|
|
1516
1761
|
const hunks = extractHunks(diff);
|
|
1517
1762
|
if (hunks.length === 0) return { isCosmetic: false };
|
|
@@ -1558,23 +1803,33 @@ function normalize(line) {
|
|
|
1558
1803
|
}
|
|
1559
1804
|
function isWhitespaceOnly(hunks) {
|
|
1560
1805
|
return hunks.every((hunk) => {
|
|
1561
|
-
const removedNorm =
|
|
1562
|
-
|
|
1806
|
+
const removedNorm = [];
|
|
1807
|
+
forEach(hunk.removed, (line) => {
|
|
1808
|
+
const n = normalize(line);
|
|
1809
|
+
if (isTruthy3(n)) removedNorm.push(n);
|
|
1810
|
+
});
|
|
1811
|
+
removedNorm.sort();
|
|
1812
|
+
const addedNorm = [];
|
|
1813
|
+
forEach(hunk.added, (line) => {
|
|
1814
|
+
const n = normalize(line);
|
|
1815
|
+
if (isTruthy3(n)) addedNorm.push(n);
|
|
1816
|
+
});
|
|
1817
|
+
addedNorm.sort();
|
|
1563
1818
|
if (removedNorm.length !== addedNorm.length) return false;
|
|
1564
1819
|
return removedNorm.every((line, idx) => line === addedNorm[idx]);
|
|
1565
1820
|
});
|
|
1566
1821
|
}
|
|
1567
1822
|
function isImportReorder(hunks) {
|
|
1568
1823
|
return hunks.every((hunk) => {
|
|
1569
|
-
const removedImports = hunk.removed
|
|
1570
|
-
const addedImports = hunk.added
|
|
1824
|
+
const removedImports = filter3(hunk.removed, isImportLine);
|
|
1825
|
+
const addedImports = filter3(hunk.added, isImportLine);
|
|
1571
1826
|
if (removedImports.length === 0) return false;
|
|
1572
|
-
if (removedImports.length !== hunk.removed
|
|
1827
|
+
if (removedImports.length !== filter3(hunk.removed, (l) => isTruthy3(l.trim())).length)
|
|
1573
1828
|
return false;
|
|
1574
|
-
if (addedImports.length !== hunk.added
|
|
1829
|
+
if (addedImports.length !== filter3(hunk.added, (l) => isTruthy3(l.trim())).length)
|
|
1575
1830
|
return false;
|
|
1576
|
-
const removedSorted = removedImports
|
|
1577
|
-
const addedSorted = addedImports
|
|
1831
|
+
const removedSorted = map5(removedImports, normalize).sort();
|
|
1832
|
+
const addedSorted = map5(addedImports, normalize).sort();
|
|
1578
1833
|
if (removedSorted.length !== addedSorted.length) return false;
|
|
1579
1834
|
return removedSorted.every((line, idx) => line === addedSorted[idx]);
|
|
1580
1835
|
});
|
|
@@ -1666,31 +1921,36 @@ function parsePorcelainOutput(output) {
|
|
|
1666
1921
|
|
|
1667
1922
|
// src/core/blame/blame.ts
|
|
1668
1923
|
async function executeBlame(file, lineRange, options) {
|
|
1669
|
-
const lineSpec =
|
|
1924
|
+
const lineSpec = `${lineRange.start},${lineRange.end}`;
|
|
1670
1925
|
const result = await gitExec(
|
|
1671
1926
|
["blame", "-w", "-C", "-C", "-M", "--porcelain", "-L", lineSpec, file],
|
|
1672
1927
|
options
|
|
1673
1928
|
);
|
|
1674
1929
|
return parsePorcelainOutput(result.stdout);
|
|
1675
1930
|
}
|
|
1676
|
-
async function analyzeBlameResults(results, options) {
|
|
1677
|
-
const uniqueShas = [...new Set(results
|
|
1931
|
+
async function analyzeBlameResults(results, filePath, options) {
|
|
1932
|
+
const uniqueShas = [...new Set(map6(results, (r) => r.commitHash))];
|
|
1678
1933
|
const cosmeticMap = /* @__PURE__ */ new Map();
|
|
1679
1934
|
const zeroSha = "0".repeat(40);
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1935
|
+
const tasks = [];
|
|
1936
|
+
forEach2(uniqueShas, (sha) => {
|
|
1937
|
+
if (sha === zeroSha) return;
|
|
1938
|
+
tasks.push(
|
|
1939
|
+
(async () => {
|
|
1940
|
+
try {
|
|
1941
|
+
const blameResult = results.find((r) => r.commitHash === sha);
|
|
1942
|
+
if (!blameResult) return;
|
|
1943
|
+
const file = blameResult.originalFile ?? filePath;
|
|
1944
|
+
const diff = await getCosmeticDiff(sha, file, options);
|
|
1945
|
+
cosmeticMap.set(sha, isCosmeticDiff(diff));
|
|
1946
|
+
} catch {
|
|
1947
|
+
cosmeticMap.set(sha, { isCosmetic: false });
|
|
1948
|
+
}
|
|
1949
|
+
})()
|
|
1950
|
+
);
|
|
1951
|
+
});
|
|
1952
|
+
await Promise.all(tasks);
|
|
1953
|
+
return map6(results, (blame) => {
|
|
1694
1954
|
const cosmetic = cosmeticMap.get(blame.commitHash);
|
|
1695
1955
|
return {
|
|
1696
1956
|
blame,
|
|
@@ -1700,6 +1960,98 @@ async function analyzeBlameResults(results, options) {
|
|
|
1700
1960
|
});
|
|
1701
1961
|
}
|
|
1702
1962
|
|
|
1963
|
+
// src/core/issue-graph/issue-graph.ts
|
|
1964
|
+
import { map as map7 } from "@winglet/common-utils";
|
|
1965
|
+
var DEFAULT_MAX_DEPTH = 2;
|
|
1966
|
+
async function traverseIssueGraph(adapter, startType, startNumber, options) {
|
|
1967
|
+
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
1968
|
+
const nodes = [];
|
|
1969
|
+
const edges = [];
|
|
1970
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1971
|
+
await traverse(
|
|
1972
|
+
adapter,
|
|
1973
|
+
startType,
|
|
1974
|
+
startNumber,
|
|
1975
|
+
0,
|
|
1976
|
+
maxDepth,
|
|
1977
|
+
nodes,
|
|
1978
|
+
edges,
|
|
1979
|
+
visited
|
|
1980
|
+
);
|
|
1981
|
+
return { nodes, edges };
|
|
1982
|
+
}
|
|
1983
|
+
async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, visited) {
|
|
1984
|
+
const key = `${type}:${number}`;
|
|
1985
|
+
if (visited.has(key)) return;
|
|
1986
|
+
if (depth > maxDepth) return;
|
|
1987
|
+
visited.add(key);
|
|
1988
|
+
if (type === "pr") {
|
|
1989
|
+
nodes.push({
|
|
1990
|
+
type: "pull_request",
|
|
1991
|
+
trackingMethod: "issue-link",
|
|
1992
|
+
confidence: "exact",
|
|
1993
|
+
prNumber: number
|
|
1994
|
+
});
|
|
1995
|
+
if (depth < maxDepth) {
|
|
1996
|
+
const linkedIssues = await adapter.getLinkedIssues(number);
|
|
1997
|
+
for (const issue of linkedIssues) {
|
|
1998
|
+
edges.push({
|
|
1999
|
+
from: `pr:${number}`,
|
|
2000
|
+
to: `issue:${issue.number}`,
|
|
2001
|
+
relation: "closes"
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
await Promise.all(
|
|
2005
|
+
map7(
|
|
2006
|
+
linkedIssues,
|
|
2007
|
+
(issue) => traverse(
|
|
2008
|
+
adapter,
|
|
2009
|
+
"issue",
|
|
2010
|
+
issue.number,
|
|
2011
|
+
depth + 1,
|
|
2012
|
+
maxDepth,
|
|
2013
|
+
nodes,
|
|
2014
|
+
edges,
|
|
2015
|
+
visited
|
|
2016
|
+
)
|
|
2017
|
+
)
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
} else {
|
|
2021
|
+
nodes.push({
|
|
2022
|
+
type: "issue",
|
|
2023
|
+
trackingMethod: "issue-link",
|
|
2024
|
+
confidence: "exact",
|
|
2025
|
+
issueNumber: number
|
|
2026
|
+
});
|
|
2027
|
+
if (depth < maxDepth) {
|
|
2028
|
+
const linkedPRs = await adapter.getLinkedPRs(number);
|
|
2029
|
+
for (const pr of linkedPRs) {
|
|
2030
|
+
edges.push({
|
|
2031
|
+
from: `issue:${number}`,
|
|
2032
|
+
to: `pr:${pr.number}`,
|
|
2033
|
+
relation: "referenced-by"
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
await Promise.all(
|
|
2037
|
+
map7(
|
|
2038
|
+
linkedPRs,
|
|
2039
|
+
(pr) => traverse(
|
|
2040
|
+
adapter,
|
|
2041
|
+
"pr",
|
|
2042
|
+
pr.number,
|
|
2043
|
+
depth + 1,
|
|
2044
|
+
maxDepth,
|
|
2045
|
+
nodes,
|
|
2046
|
+
edges,
|
|
2047
|
+
visited
|
|
2048
|
+
)
|
|
2049
|
+
)
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1703
2055
|
// src/core/core.ts
|
|
1704
2056
|
init_pr_lookup2();
|
|
1705
2057
|
function computeFeatureFlags(operatingLevel, options) {
|
|
@@ -1707,24 +2059,35 @@ function computeFeatureFlags(operatingLevel, options) {
|
|
|
1707
2059
|
astDiff: isAstAvailable() && !options.noAst,
|
|
1708
2060
|
deepTrace: operatingLevel === 2 && (options.deep ?? false),
|
|
1709
2061
|
commitGraph: false,
|
|
1710
|
-
issueGraph: operatingLevel === 2 && (options.graphDepth ?? 0) > 0,
|
|
1711
2062
|
graphql: operatingLevel === 2
|
|
1712
2063
|
};
|
|
1713
2064
|
}
|
|
2065
|
+
async function resolveRepoIdentity(cwd) {
|
|
2066
|
+
try {
|
|
2067
|
+
const result = await gitExec(["rev-parse", "--show-toplevel"], { cwd });
|
|
2068
|
+
const hash = createHash2("sha256").update(result.stdout.trim()).digest("hex").slice(0, 16);
|
|
2069
|
+
return { host: "_local", owner: "_", repo: hash };
|
|
2070
|
+
} catch {
|
|
2071
|
+
return { host: "_local", owner: "_", repo: "_unknown" };
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
1714
2074
|
async function detectPlatform2(options) {
|
|
1715
2075
|
const warnings = [];
|
|
1716
2076
|
let adapter = null;
|
|
2077
|
+
let remote = null;
|
|
1717
2078
|
let operatingLevel = 0;
|
|
1718
2079
|
try {
|
|
1719
|
-
const
|
|
1720
|
-
remoteName: options.remote
|
|
2080
|
+
const detected = await detectPlatformAdapter({
|
|
2081
|
+
remoteName: options.remote,
|
|
2082
|
+
cwd: options.cwd
|
|
1721
2083
|
});
|
|
1722
|
-
adapter =
|
|
2084
|
+
adapter = detected.adapter;
|
|
2085
|
+
remote = detected.remote;
|
|
1723
2086
|
} catch {
|
|
1724
2087
|
operatingLevel = 0;
|
|
1725
2088
|
warnings.push("Could not detect platform. Running in Level 0 (git only).");
|
|
1726
2089
|
}
|
|
1727
|
-
return { adapter, operatingLevel, warnings };
|
|
2090
|
+
return { adapter, remote, operatingLevel, warnings };
|
|
1728
2091
|
}
|
|
1729
2092
|
async function runBlameAndAuth(adapter, options, execOptions) {
|
|
1730
2093
|
const warnings = [];
|
|
@@ -1732,7 +2095,7 @@ async function runBlameAndAuth(adapter, options, execOptions) {
|
|
|
1732
2095
|
options.endLine ? `${options.line},${options.endLine}` : `${options.line}`
|
|
1733
2096
|
);
|
|
1734
2097
|
const blameChain = executeBlame(options.file, lineRange, execOptions).then(
|
|
1735
|
-
(results) => analyzeBlameResults(results, execOptions)
|
|
2098
|
+
(results) => analyzeBlameResults(results, options.file, execOptions)
|
|
1736
2099
|
);
|
|
1737
2100
|
const [authResult, blameResult] = await Promise.allSettled([
|
|
1738
2101
|
adapter ? adapter.checkAuth() : Promise.resolve({ authenticated: false }),
|
|
@@ -1754,55 +2117,83 @@ async function runBlameAndAuth(adapter, options, execOptions) {
|
|
|
1754
2117
|
}
|
|
1755
2118
|
return { analyzed: blameResult.value, operatingLevel, warnings };
|
|
1756
2119
|
}
|
|
1757
|
-
async function
|
|
2120
|
+
async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId) {
|
|
1758
2121
|
const nodes = [];
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
2122
|
+
const commitNode = {
|
|
2123
|
+
type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
|
|
2124
|
+
sha: entry.blame.commitHash,
|
|
2125
|
+
trackingMethod: "blame-CMw",
|
|
2126
|
+
confidence: "exact",
|
|
2127
|
+
note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
|
|
2128
|
+
};
|
|
2129
|
+
nodes.push(commitNode);
|
|
2130
|
+
if (entry.isCosmetic && featureFlags.astDiff) {
|
|
2131
|
+
const astResult = await traceByAst(
|
|
2132
|
+
options.file,
|
|
2133
|
+
options.line,
|
|
2134
|
+
entry.blame.commitHash,
|
|
2135
|
+
execOptions
|
|
2136
|
+
);
|
|
2137
|
+
if (astResult) {
|
|
2138
|
+
nodes.push({
|
|
2139
|
+
type: "original_commit",
|
|
2140
|
+
sha: astResult.originSha,
|
|
2141
|
+
trackingMethod: "ast-signature",
|
|
2142
|
+
confidence: astResult.confidence
|
|
2143
|
+
});
|
|
1783
2144
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2145
|
+
}
|
|
2146
|
+
const targetSha = nodes[nodes.length - 1].sha;
|
|
2147
|
+
if (targetSha) {
|
|
2148
|
+
const prInfo = await lookupPR(targetSha, adapter, {
|
|
2149
|
+
...execOptions,
|
|
2150
|
+
noCache: options.noCache,
|
|
2151
|
+
deep: featureFlags.deepTrace,
|
|
2152
|
+
repoId
|
|
2153
|
+
});
|
|
2154
|
+
if (prInfo) {
|
|
2155
|
+
nodes.push({
|
|
2156
|
+
type: "pull_request",
|
|
2157
|
+
sha: prInfo.mergeCommit,
|
|
2158
|
+
trackingMethod: prInfo.url ? "api" : "message-parse",
|
|
2159
|
+
confidence: prInfo.url ? "exact" : "heuristic",
|
|
2160
|
+
prNumber: prInfo.number,
|
|
2161
|
+
prUrl: prInfo.url || void 0,
|
|
2162
|
+
prTitle: prInfo.title || void 0,
|
|
2163
|
+
mergedAt: prInfo.mergedAt
|
|
2164
|
+
});
|
|
1799
2165
|
}
|
|
1800
2166
|
}
|
|
1801
2167
|
return nodes;
|
|
1802
2168
|
}
|
|
2169
|
+
async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId) {
|
|
2170
|
+
const results = await Promise.allSettled(
|
|
2171
|
+
map8(
|
|
2172
|
+
analyzed,
|
|
2173
|
+
(entry) => processEntry(entry, featureFlags, adapter, options, execOptions, repoId)
|
|
2174
|
+
)
|
|
2175
|
+
);
|
|
2176
|
+
return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
|
|
2177
|
+
}
|
|
2178
|
+
var legacyCacheCleaned = false;
|
|
1803
2179
|
async function trace(options) {
|
|
1804
|
-
const execOptions = { cwd:
|
|
2180
|
+
const execOptions = { cwd: options.cwd };
|
|
2181
|
+
if (!legacyCacheCleaned) {
|
|
2182
|
+
legacyCacheCleaned = true;
|
|
2183
|
+
cleanupLegacyCache().catch(() => {
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
1805
2186
|
const platform = await detectPlatform2(options);
|
|
2187
|
+
let repoId;
|
|
2188
|
+
if (platform.remote) {
|
|
2189
|
+
repoId = {
|
|
2190
|
+
host: platform.remote.host,
|
|
2191
|
+
owner: platform.remote.owner,
|
|
2192
|
+
repo: platform.remote.repo
|
|
2193
|
+
};
|
|
2194
|
+
} else {
|
|
2195
|
+
repoId = await resolveRepoIdentity(options.cwd);
|
|
2196
|
+
}
|
|
1806
2197
|
const blameAuth = await runBlameAndAuth(
|
|
1807
2198
|
platform.adapter,
|
|
1808
2199
|
options,
|
|
@@ -1816,10 +2207,26 @@ async function trace(options) {
|
|
|
1816
2207
|
featureFlags,
|
|
1817
2208
|
platform.adapter,
|
|
1818
2209
|
options,
|
|
1819
|
-
execOptions
|
|
2210
|
+
execOptions,
|
|
2211
|
+
repoId
|
|
1820
2212
|
);
|
|
1821
2213
|
return { nodes, operatingLevel, featureFlags, warnings };
|
|
1822
2214
|
}
|
|
2215
|
+
async function graph(options) {
|
|
2216
|
+
const { adapter } = await detectPlatformAdapter({
|
|
2217
|
+
remoteName: options.remote
|
|
2218
|
+
});
|
|
2219
|
+
const auth = await adapter.checkAuth();
|
|
2220
|
+
if (!auth.authenticated) {
|
|
2221
|
+
throw new LineLoreError(
|
|
2222
|
+
LineLoreErrorCode.CLI_NOT_AUTHENTICATED,
|
|
2223
|
+
'Platform CLI is not authenticated. Run "gh auth login" or set the appropriate token.'
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
return traverseIssueGraph(adapter, options.type, options.number, {
|
|
2227
|
+
maxDepth: options.depth
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
1823
2230
|
async function health(options) {
|
|
1824
2231
|
const healthReport = await checkGitHealth(options);
|
|
1825
2232
|
let operatingLevel = 0;
|
|
@@ -1833,10 +2240,20 @@ async function health(options) {
|
|
|
1833
2240
|
return { ...healthReport, operatingLevel };
|
|
1834
2241
|
}
|
|
1835
2242
|
async function clearCache() {
|
|
2243
|
+
const { rm: rm2 } = await import("fs/promises");
|
|
2244
|
+
const { homedir: homedir2 } = await import("os");
|
|
2245
|
+
const { join: join2 } = await import("path");
|
|
1836
2246
|
const { resetPRCache: resetPRCache2 } = await Promise.resolve().then(() => (init_pr_lookup2(), pr_lookup_exports));
|
|
1837
2247
|
const { resetPatchIdCache: resetPatchIdCache2 } = await Promise.resolve().then(() => (init_patch_id2(), patch_id_exports));
|
|
1838
2248
|
resetPRCache2();
|
|
1839
2249
|
resetPatchIdCache2();
|
|
2250
|
+
try {
|
|
2251
|
+
await rm2(join2(homedir2(), ".line-lore", "cache"), {
|
|
2252
|
+
recursive: true,
|
|
2253
|
+
force: true
|
|
2254
|
+
});
|
|
2255
|
+
} catch {
|
|
2256
|
+
}
|
|
1840
2257
|
}
|
|
1841
2258
|
|
|
1842
2259
|
// src/commands/cache.tsx
|
|
@@ -1852,98 +2269,15 @@ function registerCacheCommand(program2) {
|
|
|
1852
2269
|
});
|
|
1853
2270
|
}
|
|
1854
2271
|
|
|
1855
|
-
// src/core/issue-graph/issue-graph.ts
|
|
1856
|
-
var DEFAULT_MAX_DEPTH = 2;
|
|
1857
|
-
async function traverseIssueGraph(adapter, startType, startNumber, options) {
|
|
1858
|
-
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
1859
|
-
const nodes = [];
|
|
1860
|
-
const edges = [];
|
|
1861
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1862
|
-
await traverse(
|
|
1863
|
-
adapter,
|
|
1864
|
-
startType,
|
|
1865
|
-
startNumber,
|
|
1866
|
-
0,
|
|
1867
|
-
maxDepth,
|
|
1868
|
-
nodes,
|
|
1869
|
-
edges,
|
|
1870
|
-
visited
|
|
1871
|
-
);
|
|
1872
|
-
return { nodes, edges };
|
|
1873
|
-
}
|
|
1874
|
-
async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, visited) {
|
|
1875
|
-
const key = `${type}:${number}`;
|
|
1876
|
-
if (visited.has(key)) return;
|
|
1877
|
-
if (depth > maxDepth) return;
|
|
1878
|
-
visited.add(key);
|
|
1879
|
-
if (type === "pr") {
|
|
1880
|
-
nodes.push({
|
|
1881
|
-
type: "pull_request",
|
|
1882
|
-
trackingMethod: "issue-link",
|
|
1883
|
-
confidence: "exact",
|
|
1884
|
-
prNumber: number
|
|
1885
|
-
});
|
|
1886
|
-
if (depth < maxDepth) {
|
|
1887
|
-
const linkedIssues = await adapter.getLinkedIssues(number);
|
|
1888
|
-
for (const issue of linkedIssues) {
|
|
1889
|
-
edges.push({
|
|
1890
|
-
from: `pr:${number}`,
|
|
1891
|
-
to: `issue:${issue.number}`,
|
|
1892
|
-
relation: "closes"
|
|
1893
|
-
});
|
|
1894
|
-
await traverse(
|
|
1895
|
-
adapter,
|
|
1896
|
-
"issue",
|
|
1897
|
-
issue.number,
|
|
1898
|
-
depth + 1,
|
|
1899
|
-
maxDepth,
|
|
1900
|
-
nodes,
|
|
1901
|
-
edges,
|
|
1902
|
-
visited
|
|
1903
|
-
);
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
} else {
|
|
1907
|
-
nodes.push({
|
|
1908
|
-
type: "issue",
|
|
1909
|
-
trackingMethod: "issue-link",
|
|
1910
|
-
confidence: "exact",
|
|
1911
|
-
issueNumber: number
|
|
1912
|
-
});
|
|
1913
|
-
if (depth < maxDepth) {
|
|
1914
|
-
const linkedPRs = await adapter.getLinkedPRs(number);
|
|
1915
|
-
for (const pr of linkedPRs) {
|
|
1916
|
-
edges.push({
|
|
1917
|
-
from: `issue:${number}`,
|
|
1918
|
-
to: `pr:${pr.number}`,
|
|
1919
|
-
relation: "referenced-by"
|
|
1920
|
-
});
|
|
1921
|
-
await traverse(
|
|
1922
|
-
adapter,
|
|
1923
|
-
"pr",
|
|
1924
|
-
pr.number,
|
|
1925
|
-
depth + 1,
|
|
1926
|
-
maxDepth,
|
|
1927
|
-
nodes,
|
|
1928
|
-
edges,
|
|
1929
|
-
visited
|
|
1930
|
-
);
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
2272
|
// src/commands/graph.tsx
|
|
2273
|
+
init_errors();
|
|
1937
2274
|
function registerGraphCommand(program2) {
|
|
1938
2275
|
const graphCmd = program2.command("graph").description("Explore the issue/PR graph");
|
|
1939
2276
|
graphCmd.command("pr <number>").description("Show issues linked to a PR").option("--depth <n>", "Traversal depth", "1").option("--json", "Output in JSON format").action(async (number, opts) => {
|
|
1940
2277
|
const prNumber = parseInt(number, 10);
|
|
1941
2278
|
const depth = parseInt(opts.depth, 10) || 1;
|
|
1942
2279
|
try {
|
|
1943
|
-
const {
|
|
1944
|
-
const result = await traverseIssueGraph(adapter, "pr", prNumber, {
|
|
1945
|
-
maxDepth: depth
|
|
1946
|
-
});
|
|
2280
|
+
const result = await graph({ type: "pr", number: prNumber, depth });
|
|
1947
2281
|
if (opts.json) {
|
|
1948
2282
|
console.log(JSON.stringify(result, null, 2));
|
|
1949
2283
|
} else {
|
|
@@ -1956,7 +2290,11 @@ function registerGraphCommand(program2) {
|
|
|
1956
2290
|
}
|
|
1957
2291
|
}
|
|
1958
2292
|
} catch (error) {
|
|
1959
|
-
|
|
2293
|
+
if (error instanceof LineLoreError) {
|
|
2294
|
+
console.error(`Graph traversal failed: ${error.message}`);
|
|
2295
|
+
} else {
|
|
2296
|
+
console.error("Graph traversal failed:", error.message);
|
|
2297
|
+
}
|
|
1960
2298
|
process.exit(1);
|
|
1961
2299
|
}
|
|
1962
2300
|
});
|
|
@@ -1964,10 +2302,7 @@ function registerGraphCommand(program2) {
|
|
|
1964
2302
|
const issueNumber = parseInt(number, 10);
|
|
1965
2303
|
const depth = parseInt(opts.depth, 10) || 1;
|
|
1966
2304
|
try {
|
|
1967
|
-
const {
|
|
1968
|
-
const result = await traverseIssueGraph(adapter, "issue", issueNumber, {
|
|
1969
|
-
maxDepth: depth
|
|
1970
|
-
});
|
|
2305
|
+
const result = await graph({ type: "issue", number: issueNumber, depth });
|
|
1971
2306
|
if (opts.json) {
|
|
1972
2307
|
console.log(JSON.stringify(result, null, 2));
|
|
1973
2308
|
} else {
|
|
@@ -1980,7 +2315,11 @@ function registerGraphCommand(program2) {
|
|
|
1980
2315
|
}
|
|
1981
2316
|
}
|
|
1982
2317
|
} catch (error) {
|
|
1983
|
-
|
|
2318
|
+
if (error instanceof LineLoreError) {
|
|
2319
|
+
console.error(`Graph traversal failed: ${error.message}`);
|
|
2320
|
+
} else {
|
|
2321
|
+
console.error("Graph traversal failed:", error.message);
|
|
2322
|
+
}
|
|
1984
2323
|
process.exit(1);
|
|
1985
2324
|
}
|
|
1986
2325
|
});
|
|
@@ -2076,31 +2415,32 @@ function formatNodeHuman(node) {
|
|
|
2076
2415
|
init_normalizer();
|
|
2077
2416
|
init_errors();
|
|
2078
2417
|
function registerTraceCommand(program2) {
|
|
2079
|
-
program2.command("trace <file>").description("Trace a file line to its originating PR").requiredOption("-L, --line <range>", 'Line number or range (e.g., "42" or "10,50")').option("--deep", "Enable deep trace for squash PRs").option("--
|
|
2418
|
+
program2.command("trace <file>").description("Trace a file line to its originating PR").requiredOption("-L, --line <range>", 'Line number or range (e.g., "42" or "10,50")').option("--deep", "Enable deep trace for squash PRs").option("--no-ast", "Disable AST diff analysis").option("--no-cache", "Disable cache").option("--json", "Output in JSON format").option("-q, --quiet", "Output PR number only").option("--output <format>", "Output format: human, json, llm", "human").option("--no-color", "Disable colored output").action(async (file, opts) => {
|
|
2080
2419
|
const lineStr = opts.line;
|
|
2081
2420
|
const parts = lineStr.split(",");
|
|
2082
2421
|
const line = parseInt(parts[0], 10);
|
|
2083
2422
|
const endLine = parts.length > 1 ? parseInt(parts[1], 10) : void 0;
|
|
2084
|
-
const
|
|
2423
|
+
const traceOptions = {
|
|
2085
2424
|
file,
|
|
2086
2425
|
line,
|
|
2087
2426
|
endLine,
|
|
2088
2427
|
deep: opts.deep,
|
|
2089
|
-
graphDepth: parseInt(opts.graphDepth, 10) || 0,
|
|
2090
2428
|
noAst: opts.ast === false,
|
|
2091
|
-
noCache: opts.cache === false
|
|
2429
|
+
noCache: opts.cache === false
|
|
2430
|
+
};
|
|
2431
|
+
const cliOptions = {
|
|
2092
2432
|
json: opts.json,
|
|
2093
2433
|
quiet: opts.quiet,
|
|
2094
2434
|
output: opts.output ?? "human"
|
|
2095
2435
|
};
|
|
2096
2436
|
try {
|
|
2097
|
-
const result = await trace(
|
|
2437
|
+
const result = await trace(traceOptions);
|
|
2098
2438
|
let output;
|
|
2099
|
-
if (
|
|
2439
|
+
if (cliOptions.quiet) {
|
|
2100
2440
|
output = formatQuiet(result);
|
|
2101
|
-
} else if (
|
|
2441
|
+
} else if (cliOptions.json || cliOptions.output === "json") {
|
|
2102
2442
|
output = formatJson(result);
|
|
2103
|
-
} else if (
|
|
2443
|
+
} else if (cliOptions.output === "llm") {
|
|
2104
2444
|
output = formatLlm(result);
|
|
2105
2445
|
} else {
|
|
2106
2446
|
output = formatHuman(result);
|