megareviewer 0.1.0 → 0.2.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/server.js +295 -87
- package/dist/web/assets/{core-vltS_6PD.js → core-B6rr2vvd.js} +1 -1
- package/dist/web/assets/index-ClcSjEKy.css +1 -0
- package/dist/web/assets/index-DwjurLQh.js +65 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-COk-CpeI.js +0 -65
- package/dist/web/assets/index-DYdJE6_f.css +0 -1
package/dist/server.js
CHANGED
|
@@ -26356,7 +26356,7 @@ import { fileURLToPath } from "node:url";
|
|
|
26356
26356
|
// package.json
|
|
26357
26357
|
var package_default = {
|
|
26358
26358
|
name: "megareviewer",
|
|
26359
|
-
version: "0.
|
|
26359
|
+
version: "0.2.0",
|
|
26360
26360
|
description: "Guided code review for GitHub and Bitbucket Cloud pull requests: intent first, changes sorted by risk, decisions to approve.",
|
|
26361
26361
|
keywords: [
|
|
26362
26362
|
"code-review",
|
|
@@ -26510,6 +26510,7 @@ init_credentials();
|
|
|
26510
26510
|
import { randomBytes } from "node:crypto";
|
|
26511
26511
|
import { mkdirSync } from "node:fs";
|
|
26512
26512
|
import { join as join4 } from "node:path";
|
|
26513
|
+
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
26513
26514
|
|
|
26514
26515
|
// node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/external.js
|
|
26515
26516
|
var external_exports = {};
|
|
@@ -47189,11 +47190,14 @@ var AnalysisError = external_exports.object({
|
|
|
47189
47190
|
message: external_exports.string()
|
|
47190
47191
|
}).readonly();
|
|
47191
47192
|
var AnalysisStatus = external_exports.enum(["running", "complete", "failed"]);
|
|
47193
|
+
var FilesFetched = external_exports.object({ done: external_exports.number().int().nonnegative(), total: external_exports.number().int().nonnegative() }).readonly();
|
|
47192
47194
|
var Analysis = external_exports.object({
|
|
47193
47195
|
id: AnalysisId,
|
|
47194
47196
|
key: AnalysisKey,
|
|
47195
47197
|
status: AnalysisStatus,
|
|
47196
47198
|
stages: external_exports.record(StageName, StageStatus).readonly(),
|
|
47199
|
+
/** null until Collect starts fetching. Defaults for analyses stored before it existed. */
|
|
47200
|
+
filesFetched: FilesFetched.nullable().default(null),
|
|
47197
47201
|
pullRequest: PullRequest.nullable(),
|
|
47198
47202
|
files: external_exports.array(FileChange).readonly(),
|
|
47199
47203
|
tickets: external_exports.array(Ticket).readonly(),
|
|
@@ -47567,6 +47571,7 @@ var networkError = (error62) => {
|
|
|
47567
47571
|
const cause = error62.cause instanceof Error ? error62.cause.message : error62.message;
|
|
47568
47572
|
return hostError("network", `Can't reach Bitbucket: ${cause}`);
|
|
47569
47573
|
};
|
|
47574
|
+
var timedOut = (timeoutMs) => hostError("network", `Bitbucket didn't answer within ${timeoutMs / 1e3} seconds`);
|
|
47570
47575
|
var invalidResponse = (what, error62) => hostError(
|
|
47571
47576
|
"invalid-response",
|
|
47572
47577
|
`Unexpected response from Bitbucket for ${what}: ${typeof error62 === "string" ? error62 : external_exports.prettifyError(error62)}`
|
|
@@ -47578,10 +47583,12 @@ var API_ORIGIN = new URL(API).origin;
|
|
|
47578
47583
|
var REDIRECTS = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
47579
47584
|
var MAX_REDIRECTS = 5;
|
|
47580
47585
|
var MAX_PAGES = 10;
|
|
47586
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
47581
47587
|
var createBitbucketClient = (options) => {
|
|
47582
47588
|
const { logger } = options;
|
|
47589
|
+
const timeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
47583
47590
|
const authorization = `Basic ${Buffer.from(`${options.username}:${options.password}`).toString("base64")}`;
|
|
47584
|
-
const send = async (url2, accept, request2
|
|
47591
|
+
const send = async (url2, accept, request2, signal) => {
|
|
47585
47592
|
let target = url2;
|
|
47586
47593
|
for (let hop = 0; ; hop += 1) {
|
|
47587
47594
|
const toApi = new URL(target).origin === API_ORIGIN;
|
|
@@ -47591,6 +47598,7 @@ var createBitbucketClient = (options) => {
|
|
|
47591
47598
|
response = await options.fetch(target, {
|
|
47592
47599
|
method: request2.method ?? "GET",
|
|
47593
47600
|
redirect: "manual",
|
|
47601
|
+
signal,
|
|
47594
47602
|
headers: {
|
|
47595
47603
|
accept,
|
|
47596
47604
|
...toApi ? { authorization } : {},
|
|
@@ -47609,17 +47617,28 @@ var createBitbucketClient = (options) => {
|
|
|
47609
47617
|
target = new URL(location, target).href;
|
|
47610
47618
|
}
|
|
47611
47619
|
};
|
|
47612
|
-
const
|
|
47613
|
-
const
|
|
47614
|
-
|
|
47615
|
-
|
|
47616
|
-
|
|
47617
|
-
|
|
47620
|
+
const call = async (url2, accept, request2, read) => {
|
|
47621
|
+
const controller = new AbortController();
|
|
47622
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
47623
|
+
try {
|
|
47624
|
+
const response = await send(url2, accept, request2, controller.signal);
|
|
47625
|
+
if (!response.ok) return response;
|
|
47626
|
+
if (!response.value.ok) {
|
|
47627
|
+
const { status, headers } = response.value;
|
|
47628
|
+
const body2 = await response.value.text();
|
|
47629
|
+
return err2(fromErrorResponse({ status, headers, body: body2 }, request2.call ?? "read"));
|
|
47630
|
+
}
|
|
47631
|
+
return await read(response.value);
|
|
47632
|
+
} catch (error62) {
|
|
47633
|
+
if (controller.signal.aborted) return err2(timedOut(timeoutMs));
|
|
47634
|
+
if (error62 instanceof TypeError) return err2(networkError(error62));
|
|
47635
|
+
throw error62;
|
|
47636
|
+
} finally {
|
|
47637
|
+
clearTimeout(timer);
|
|
47638
|
+
}
|
|
47618
47639
|
};
|
|
47619
|
-
const json3 = async (what, schema, url2, request2 = {}) => {
|
|
47620
|
-
const
|
|
47621
|
-
if (!response.ok) return response;
|
|
47622
|
-
const text = await response.value.text();
|
|
47640
|
+
const json3 = async (what, schema, url2, request2 = {}) => call(url2, "application/json", request2, async (response) => {
|
|
47641
|
+
const text = await response.text();
|
|
47623
47642
|
let data;
|
|
47624
47643
|
try {
|
|
47625
47644
|
data = text === "" ? null : JSON.parse(text);
|
|
@@ -47628,7 +47647,7 @@ var createBitbucketClient = (options) => {
|
|
|
47628
47647
|
}
|
|
47629
47648
|
const parsed = schema.safeParse(data);
|
|
47630
47649
|
return parsed.success ? ok(parsed.data) : err2(invalidResponse(what, parsed.error));
|
|
47631
|
-
};
|
|
47650
|
+
});
|
|
47632
47651
|
const pages = async (what, item, url2) => {
|
|
47633
47652
|
const page = BitbucketPage(item);
|
|
47634
47653
|
const items = [];
|
|
@@ -47648,11 +47667,15 @@ var createBitbucketClient = (options) => {
|
|
|
47648
47667
|
return {
|
|
47649
47668
|
json: json3,
|
|
47650
47669
|
pages,
|
|
47651
|
-
|
|
47652
|
-
|
|
47653
|
-
|
|
47654
|
-
|
|
47655
|
-
|
|
47670
|
+
text: (url2, accept) => (
|
|
47671
|
+
// Bitbucket sends files and diffs in the repository's encoding, with no charset.
|
|
47672
|
+
call(
|
|
47673
|
+
url2,
|
|
47674
|
+
accept,
|
|
47675
|
+
{},
|
|
47676
|
+
async (response) => ok(new TextDecoder().decode(await response.arrayBuffer()))
|
|
47677
|
+
)
|
|
47678
|
+
)
|
|
47656
47679
|
};
|
|
47657
47680
|
};
|
|
47658
47681
|
|
|
@@ -47713,6 +47736,7 @@ var VERDICT_ENDPOINTS = {
|
|
|
47713
47736
|
// src/adapters/bitbucket/bitbucket-host.ts
|
|
47714
47737
|
var SCANNED_REPOS = 50;
|
|
47715
47738
|
var SCAN_CONCURRENCY = 5;
|
|
47739
|
+
var FILE_CONCURRENCY = 8;
|
|
47716
47740
|
var PAGE_SIZE = 50;
|
|
47717
47741
|
var FATAL = /* @__PURE__ */ new Set([
|
|
47718
47742
|
"unauthorized",
|
|
@@ -47739,7 +47763,8 @@ var createBitbucketHost = (options) => {
|
|
|
47739
47763
|
username: options.username,
|
|
47740
47764
|
password: options.password,
|
|
47741
47765
|
logger,
|
|
47742
|
-
fetch: options.fetch ?? globalThis.fetch
|
|
47766
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
47767
|
+
requestTimeoutMs: options.requestTimeoutMs
|
|
47743
47768
|
});
|
|
47744
47769
|
let cachedUser = null;
|
|
47745
47770
|
const currentUser = async () => {
|
|
@@ -47863,11 +47888,21 @@ var createBitbucketHost = (options) => {
|
|
|
47863
47888
|
})
|
|
47864
47889
|
);
|
|
47865
47890
|
},
|
|
47866
|
-
async
|
|
47867
|
-
const
|
|
47868
|
-
|
|
47869
|
-
|
|
47870
|
-
|
|
47891
|
+
async getFiles(repo, files) {
|
|
47892
|
+
const contents = await mapLimit(files, FILE_CONCURRENCY, async ({ sha, path: path4 }) => {
|
|
47893
|
+
const encodedPath = path4.split("/").map(segment).join("/");
|
|
47894
|
+
const content = await client.text(`${repoUrl(repo)}/src/${sha}/${encodedPath}`, "*/*");
|
|
47895
|
+
if (!content.ok && (content.error.kind === "not-found" || content.error.kind === "too-large")) {
|
|
47896
|
+
return ok(null);
|
|
47897
|
+
}
|
|
47898
|
+
return content;
|
|
47899
|
+
});
|
|
47900
|
+
const found = [];
|
|
47901
|
+
for (const content of contents) {
|
|
47902
|
+
if (!content.ok) return content;
|
|
47903
|
+
found.push(content.value);
|
|
47904
|
+
}
|
|
47905
|
+
return ok(found);
|
|
47871
47906
|
},
|
|
47872
47907
|
async listPullRequests() {
|
|
47873
47908
|
if (workspace === null) return ok([]);
|
|
@@ -52542,6 +52577,15 @@ var GitHubText = external_exports.union([
|
|
|
52542
52577
|
external_exports.string(),
|
|
52543
52578
|
external_exports.instanceof(ArrayBuffer).transform((buffer) => new TextDecoder().decode(buffer))
|
|
52544
52579
|
]);
|
|
52580
|
+
var GitHubObject = external_exports.object({
|
|
52581
|
+
__typename: external_exports.string(),
|
|
52582
|
+
text: external_exports.string().nullable().optional(),
|
|
52583
|
+
isBinary: external_exports.boolean().nullable().optional(),
|
|
52584
|
+
isTruncated: external_exports.boolean().optional()
|
|
52585
|
+
});
|
|
52586
|
+
var GitHubBlobs = external_exports.object({
|
|
52587
|
+
repository: external_exports.record(external_exports.string(), GitHubObject.nullable())
|
|
52588
|
+
});
|
|
52545
52589
|
var GitHubSearchResult = external_exports.object({
|
|
52546
52590
|
total_count: external_exports.number().int().nonnegative(),
|
|
52547
52591
|
incomplete_results: external_exports.boolean(),
|
|
@@ -52585,6 +52629,10 @@ var OctokitError = external_exports.object({
|
|
|
52585
52629
|
data: external_exports.unknown()
|
|
52586
52630
|
}).optional()
|
|
52587
52631
|
});
|
|
52632
|
+
var GraphqlResponseError2 = external_exports.object({
|
|
52633
|
+
name: external_exports.literal("GraphqlResponseError"),
|
|
52634
|
+
errors: external_exports.array(external_exports.object({ type: external_exports.string().optional(), message: external_exports.string() })).min(1)
|
|
52635
|
+
});
|
|
52588
52636
|
var hostError2 = (kind, message, retryAfterSeconds3 = null) => ({ kind, message, retryAfterSeconds: retryAfterSeconds3 });
|
|
52589
52637
|
var header = (response, name2) => {
|
|
52590
52638
|
const value = response.headers[name2];
|
|
@@ -52636,12 +52684,19 @@ var fromErrorResponse2 = (response, now) => {
|
|
|
52636
52684
|
return hostError2("unexpected", `GitHub answered ${status}: ${message}`);
|
|
52637
52685
|
};
|
|
52638
52686
|
var fromOctokitError = (error62, now) => {
|
|
52687
|
+
const graphql3 = GraphqlResponseError2.safeParse(error62);
|
|
52688
|
+
if (graphql3.success) {
|
|
52689
|
+
const { errors } = graphql3.data;
|
|
52690
|
+
const message2 = `GitHub answered: ${errors.map((detail) => detail.message).join("; ")}`;
|
|
52691
|
+
return hostError2(errors[0]?.type === "NOT_FOUND" ? "not-found" : "unexpected", message2);
|
|
52692
|
+
}
|
|
52639
52693
|
const parsed = OctokitError.safeParse(error62);
|
|
52640
52694
|
if (!parsed.success) throw error62;
|
|
52641
52695
|
const { response, message } = parsed.data;
|
|
52642
52696
|
if (response === void 0) return hostError2("network", `Can't reach GitHub: ${message}`);
|
|
52643
52697
|
return fromErrorResponse2(response, now);
|
|
52644
52698
|
};
|
|
52699
|
+
var timedOut2 = (timeoutMs) => hostError2("network", `GitHub didn't answer within ${timeoutMs / 1e3} seconds`);
|
|
52645
52700
|
var invalidResponse2 = (what, error62) => hostError2(
|
|
52646
52701
|
"invalid-response",
|
|
52647
52702
|
`Unexpected response from GitHub for ${what}: ${external_exports.prettifyError(error62)}`
|
|
@@ -52738,8 +52793,23 @@ var DIFF = "application/vnd.github.diff";
|
|
|
52738
52793
|
var RAW = "application/vnd.github.raw";
|
|
52739
52794
|
var MAX_COMMITS = 250;
|
|
52740
52795
|
var SEARCH_PAGE_SIZE = 50;
|
|
52796
|
+
var FILES_PER_QUERY = 25;
|
|
52797
|
+
var REQUEST_TIMEOUT_MS2 = 6e4;
|
|
52798
|
+
var blobsQuery = (count2) => {
|
|
52799
|
+
const indexes = Array.from({ length: count2 }, (_, i2) => i2);
|
|
52800
|
+
const variables = indexes.map((i2) => `$e${i2}: String!`).join(", ");
|
|
52801
|
+
const objects = indexes.map(
|
|
52802
|
+
(i2) => `f${i2}: object(expression: $e${i2}) { __typename ... on Blob { text isBinary isTruncated } }`
|
|
52803
|
+
).join("\n ");
|
|
52804
|
+
return `query ($owner: String!, $repo: String!, ${variables}) {
|
|
52805
|
+
repository(owner: $owner, name: $repo) {
|
|
52806
|
+
${objects}
|
|
52807
|
+
}
|
|
52808
|
+
}`;
|
|
52809
|
+
};
|
|
52741
52810
|
var createGitHubHost = (options) => {
|
|
52742
52811
|
const { clock, logger } = options;
|
|
52812
|
+
const timeoutMs = options.requestTimeoutMs ?? REQUEST_TIMEOUT_MS2;
|
|
52743
52813
|
const octokit = new Octokit2({
|
|
52744
52814
|
auth: options.token,
|
|
52745
52815
|
userAgent: `megareviewer/${options.version}`,
|
|
@@ -52753,23 +52823,44 @@ var createGitHubHost = (options) => {
|
|
|
52753
52823
|
error: (message) => logger.debug(message)
|
|
52754
52824
|
}
|
|
52755
52825
|
});
|
|
52756
|
-
const read = async (what, schema, send) => {
|
|
52826
|
+
const read = async (what, schema, send, { deadline = true } = {}) => {
|
|
52827
|
+
const controller = deadline ? new AbortController() : null;
|
|
52828
|
+
const timer = controller === null ? null : setTimeout(() => controller.abort(), timeoutMs);
|
|
52757
52829
|
let data;
|
|
52758
52830
|
try {
|
|
52759
|
-
data = await send();
|
|
52831
|
+
data = await send(controller === null ? {} : { signal: controller.signal });
|
|
52760
52832
|
} catch (error62) {
|
|
52833
|
+
if (controller?.signal.aborted) return err2(timedOut2(timeoutMs));
|
|
52761
52834
|
return err2(fromOctokitError(error62, clock.now()));
|
|
52835
|
+
} finally {
|
|
52836
|
+
if (timer !== null) clearTimeout(timer);
|
|
52762
52837
|
}
|
|
52763
52838
|
const parsed = schema.safeParse(data);
|
|
52764
52839
|
return parsed.success ? ok(parsed.data) : err2(invalidResponse2(what, parsed.error));
|
|
52765
52840
|
};
|
|
52841
|
+
const fileOverRest = async (repo, { sha, path: path4 }) => {
|
|
52842
|
+
const encodedPath = path4.split("/").map(encodeURIComponent).join("/");
|
|
52843
|
+
const content = await read(`${path4} at ${sha}`, GitHubText, async (request2) => {
|
|
52844
|
+
return (await octokit.request(`GET /repos/{owner}/{repo}/contents/${encodedPath}`, {
|
|
52845
|
+
owner: repo.owner,
|
|
52846
|
+
repo: repo.repo,
|
|
52847
|
+
ref: sha,
|
|
52848
|
+
headers: { accept: RAW },
|
|
52849
|
+
request: request2
|
|
52850
|
+
})).data;
|
|
52851
|
+
});
|
|
52852
|
+
if (!content.ok && (content.error.kind === "not-found" || content.error.kind === "too-large")) {
|
|
52853
|
+
return ok(null);
|
|
52854
|
+
}
|
|
52855
|
+
return content;
|
|
52856
|
+
};
|
|
52766
52857
|
let cachedUser = null;
|
|
52767
52858
|
return {
|
|
52768
52859
|
host: "github",
|
|
52769
52860
|
async currentUser() {
|
|
52770
52861
|
if (cachedUser !== null) return ok(cachedUser);
|
|
52771
|
-
const user = await read("the current user", GitHubUser, async () => {
|
|
52772
|
-
return (await octokit.request("GET /user")).data;
|
|
52862
|
+
const user = await read("the current user", GitHubUser, async (request2) => {
|
|
52863
|
+
return (await octokit.request("GET /user", { request: request2 })).data;
|
|
52773
52864
|
});
|
|
52774
52865
|
if (!user.ok) return user;
|
|
52775
52866
|
cachedUser = toHostUser2(user.value);
|
|
@@ -52778,39 +52869,43 @@ var createGitHubHost = (options) => {
|
|
|
52778
52869
|
async getPullRequest(ref) {
|
|
52779
52870
|
const { owner, repo } = ref.repo;
|
|
52780
52871
|
const pull_number = ref.number;
|
|
52781
|
-
const pullRequest = await read("the pull request", GitHubPullRequest, async () => {
|
|
52872
|
+
const pullRequest = await read("the pull request", GitHubPullRequest, async (request2) => {
|
|
52782
52873
|
return (await octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", {
|
|
52783
52874
|
owner,
|
|
52784
52875
|
repo,
|
|
52785
|
-
pull_number
|
|
52876
|
+
pull_number,
|
|
52877
|
+
request: request2
|
|
52786
52878
|
})).data;
|
|
52787
52879
|
});
|
|
52788
52880
|
if (!pullRequest.ok) return pullRequest;
|
|
52789
52881
|
const basehead = `${pullRequest.value.base.sha}...${pullRequest.value.head.sha}`;
|
|
52790
52882
|
const [diff, comparison, commits] = await Promise.all([
|
|
52791
|
-
read("the diff", GitHubText, async () => {
|
|
52883
|
+
read("the diff", GitHubText, async (request2) => {
|
|
52792
52884
|
return (await octokit.request("GET /repos/{owner}/{repo}/compare/{basehead}", {
|
|
52793
52885
|
owner,
|
|
52794
52886
|
repo,
|
|
52795
52887
|
basehead,
|
|
52796
|
-
headers: { accept: DIFF }
|
|
52888
|
+
headers: { accept: DIFF },
|
|
52889
|
+
request: request2
|
|
52797
52890
|
})).data;
|
|
52798
52891
|
}),
|
|
52799
|
-
read("the merge base", GitHubComparison, async () => {
|
|
52892
|
+
read("the merge base", GitHubComparison, async (request2) => {
|
|
52800
52893
|
return (await octokit.request("GET /repos/{owner}/{repo}/compare/{basehead}", {
|
|
52801
52894
|
owner,
|
|
52802
52895
|
repo,
|
|
52803
|
-
basehead
|
|
52896
|
+
basehead,
|
|
52897
|
+
request: request2
|
|
52804
52898
|
})).data;
|
|
52805
52899
|
}),
|
|
52806
52900
|
read(
|
|
52807
52901
|
"the commits",
|
|
52808
52902
|
external_exports.array(GitHubCommit),
|
|
52809
|
-
() => octokit.paginate(
|
|
52903
|
+
(request2) => octokit.paginate(octokit.rest.pulls.listCommits, {
|
|
52810
52904
|
owner,
|
|
52811
52905
|
repo,
|
|
52812
52906
|
pull_number,
|
|
52813
|
-
per_page: 100
|
|
52907
|
+
per_page: 100,
|
|
52908
|
+
request: request2
|
|
52814
52909
|
})
|
|
52815
52910
|
)
|
|
52816
52911
|
]);
|
|
@@ -52830,24 +52925,42 @@ var createGitHubHost = (options) => {
|
|
|
52830
52925
|
})
|
|
52831
52926
|
);
|
|
52832
52927
|
},
|
|
52833
|
-
async
|
|
52834
|
-
const
|
|
52835
|
-
|
|
52836
|
-
|
|
52837
|
-
|
|
52838
|
-
|
|
52839
|
-
|
|
52840
|
-
|
|
52841
|
-
|
|
52842
|
-
|
|
52843
|
-
|
|
52844
|
-
|
|
52928
|
+
async getFiles(repo, files) {
|
|
52929
|
+
const found = [];
|
|
52930
|
+
for (let start3 = 0; start3 < files.length; start3 += FILES_PER_QUERY) {
|
|
52931
|
+
const chunk = files.slice(start3, start3 + FILES_PER_QUERY);
|
|
52932
|
+
const blobs = await read(
|
|
52933
|
+
`${chunk.length} files`,
|
|
52934
|
+
GitHubBlobs,
|
|
52935
|
+
(request2) => octokit.graphql(blobsQuery(chunk.length), {
|
|
52936
|
+
owner: repo.owner,
|
|
52937
|
+
repo: repo.repo,
|
|
52938
|
+
...Object.fromEntries(chunk.map(({ sha, path: path4 }, i2) => [`e${i2}`, `${sha}:${path4}`])),
|
|
52939
|
+
request: request2
|
|
52940
|
+
})
|
|
52941
|
+
);
|
|
52942
|
+
if (!blobs.ok) return blobs;
|
|
52943
|
+
for (const [i2, file2] of chunk.entries()) {
|
|
52944
|
+
const object2 = blobs.value.repository[`f${i2}`];
|
|
52945
|
+
if (object2 === null) {
|
|
52946
|
+
found.push(null);
|
|
52947
|
+
} else if (object2?.__typename === "Blob" && typeof object2.text === "string" && object2.isBinary !== true && object2.isTruncated !== true) {
|
|
52948
|
+
found.push(object2.text);
|
|
52949
|
+
} else {
|
|
52950
|
+
const content = await fileOverRest(repo, file2);
|
|
52951
|
+
if (!content.ok) return content;
|
|
52952
|
+
found.push(content.value);
|
|
52953
|
+
}
|
|
52954
|
+
}
|
|
52955
|
+
}
|
|
52956
|
+
return ok(found);
|
|
52845
52957
|
},
|
|
52846
52958
|
async listPullRequests() {
|
|
52847
|
-
const search = (qualifier) => read("the pull request search", GitHubSearchResult, async () => {
|
|
52959
|
+
const search = (qualifier) => read("the pull request search", GitHubSearchResult, async (request2) => {
|
|
52848
52960
|
return (await octokit.request("GET /search/issues", {
|
|
52849
52961
|
q: `is:pr is:open archived:false ${qualifier}`,
|
|
52850
|
-
per_page: SEARCH_PAGE_SIZE
|
|
52962
|
+
per_page: SEARCH_PAGE_SIZE,
|
|
52963
|
+
request: request2
|
|
52851
52964
|
})).data;
|
|
52852
52965
|
});
|
|
52853
52966
|
const [reviewing, authored] = await Promise.all([
|
|
@@ -52880,24 +52993,33 @@ var createGitHubHost = (options) => {
|
|
|
52880
52993
|
if (plan === null) return ok({ url: null, publishedAt: clock.now(), postedCommentIds: {} });
|
|
52881
52994
|
const { owner, repo } = ref.repo;
|
|
52882
52995
|
const pull_number = ref.number;
|
|
52883
|
-
const review = await read(
|
|
52884
|
-
|
|
52885
|
-
|
|
52886
|
-
|
|
52887
|
-
pull_number,
|
|
52888
|
-
|
|
52889
|
-
|
|
52890
|
-
|
|
52891
|
-
|
|
52996
|
+
const review = await read(
|
|
52997
|
+
"the new review",
|
|
52998
|
+
GitHubReview,
|
|
52999
|
+
async () => {
|
|
53000
|
+
return (await octokit.request("POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews", {
|
|
53001
|
+
owner,
|
|
53002
|
+
repo,
|
|
53003
|
+
pull_number,
|
|
53004
|
+
...plan.request,
|
|
53005
|
+
comments: [...plan.request.comments]
|
|
53006
|
+
})).data;
|
|
53007
|
+
},
|
|
53008
|
+
{ deadline: false }
|
|
53009
|
+
);
|
|
52892
53010
|
if (!review.ok) return review;
|
|
52893
53011
|
const reviewId = String(review.value.id);
|
|
52894
53012
|
const hostComments = plan.comments.length === 0 ? ok([]) : await read(
|
|
52895
53013
|
"the review's comments",
|
|
52896
53014
|
external_exports.array(GitHubReviewComment),
|
|
52897
|
-
() => octokit.paginate(
|
|
52898
|
-
|
|
52899
|
-
|
|
52900
|
-
|
|
53015
|
+
(request2) => octokit.paginate(octokit.rest.pulls.listCommentsForReview, {
|
|
53016
|
+
owner,
|
|
53017
|
+
repo,
|
|
53018
|
+
pull_number,
|
|
53019
|
+
review_id: review.value.id,
|
|
53020
|
+
per_page: 100,
|
|
53021
|
+
request: request2
|
|
53022
|
+
})
|
|
52901
53023
|
);
|
|
52902
53024
|
if (!hostComments.ok) {
|
|
52903
53025
|
logger.warn("Couldn't read the review's comments back; recording the review's ID", {
|
|
@@ -56972,6 +57094,16 @@ var MIGRATIONS = [
|
|
|
56972
57094
|
published_at TEXT NOT NULL,
|
|
56973
57095
|
receipt TEXT NOT NULL -- PublicationReceipt as JSON
|
|
56974
57096
|
);
|
|
57097
|
+
`,
|
|
57098
|
+
`
|
|
57099
|
+
CREATE TABLE file_contents (
|
|
57100
|
+
host TEXT NOT NULL, owner TEXT NOT NULL, repo TEXT NOT NULL,
|
|
57101
|
+
sha TEXT NOT NULL, path TEXT NOT NULL,
|
|
57102
|
+
content TEXT, -- NULL: the file isn't at this commit
|
|
57103
|
+
fetched_at TEXT NOT NULL,
|
|
57104
|
+
PRIMARY KEY (host, owner, repo, sha, path)
|
|
57105
|
+
);
|
|
57106
|
+
CREATE INDEX file_contents_fetched_at ON file_contents (fetched_at);
|
|
56975
57107
|
`
|
|
56976
57108
|
];
|
|
56977
57109
|
var UserVersion = external_exports.object({ user_version: external_exports.number().int().nonnegative() });
|
|
@@ -57006,6 +57138,7 @@ var migrate = (db, migrations) => {
|
|
|
57006
57138
|
};
|
|
57007
57139
|
|
|
57008
57140
|
// src/adapters/sqlite/sqlite-analysis-store.ts
|
|
57141
|
+
var FILE_CACHE_DAYS = 30;
|
|
57009
57142
|
var PayloadRow = external_exports.object({ id: external_exports.string(), payload: external_exports.string() });
|
|
57010
57143
|
var VerdictRow = external_exports.object({
|
|
57011
57144
|
decision_id: external_exports.string(),
|
|
@@ -57015,6 +57148,7 @@ var VerdictRow = external_exports.object({
|
|
|
57015
57148
|
});
|
|
57016
57149
|
var PostedCommentRow = external_exports.object({ comment_key: external_exports.string(), host_comment_id: external_exports.string() });
|
|
57017
57150
|
var ReceiptRow = external_exports.object({ receipt: external_exports.string() });
|
|
57151
|
+
var FileRow = external_exports.object({ content: external_exports.string().nullable() });
|
|
57018
57152
|
var parseJson3 = (text) => {
|
|
57019
57153
|
try {
|
|
57020
57154
|
return JSON.parse(text);
|
|
@@ -57075,6 +57209,20 @@ var openSqliteAnalysisStore = (options) => {
|
|
|
57075
57209
|
ON CONFLICT (analysis_id, comment_key) DO UPDATE SET
|
|
57076
57210
|
host_comment_id = excluded.host_comment_id, posted_at = excluded.posted_at`
|
|
57077
57211
|
);
|
|
57212
|
+
const selectFile = db.prepare(
|
|
57213
|
+
`SELECT content FROM file_contents
|
|
57214
|
+
WHERE host = ? AND owner = ? AND repo = ? AND sha = ? AND path = ?`
|
|
57215
|
+
);
|
|
57216
|
+
const upsertFile = db.prepare(
|
|
57217
|
+
`INSERT INTO file_contents (host, owner, repo, sha, path, content, fetched_at)
|
|
57218
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
57219
|
+
ON CONFLICT (host, owner, repo, sha, path) DO UPDATE SET
|
|
57220
|
+
content = excluded.content, fetched_at = excluded.fetched_at`
|
|
57221
|
+
);
|
|
57222
|
+
const pruned = db.prepare("DELETE FROM file_contents WHERE fetched_at < ?").run(new Date(Date.parse(clock.now()) - FILE_CACHE_DAYS * 24 * 60 * 60 * 1e3).toISOString());
|
|
57223
|
+
if (Number(pruned.changes) > 0) {
|
|
57224
|
+
logger.debug("Dropped old cached files", { count: Number(pruned.changes) });
|
|
57225
|
+
}
|
|
57078
57226
|
const selectPublication = db.prepare("SELECT receipt FROM publications WHERE analysis_id = ?");
|
|
57079
57227
|
const upsertPublication = db.prepare(
|
|
57080
57228
|
`INSERT INTO publications (analysis_id, url, published_at, receipt)
|
|
@@ -57184,6 +57332,13 @@ var openSqliteAnalysisStore = (options) => {
|
|
|
57184
57332
|
async recordPublication(id, receipt) {
|
|
57185
57333
|
upsertPublication.run(id, receipt.url, receipt.publishedAt, JSON.stringify(receipt));
|
|
57186
57334
|
},
|
|
57335
|
+
async get(repo, { sha, path: path4 }) {
|
|
57336
|
+
const row = selectFile.get(repo.host, repo.owner, repo.repo, sha, path4);
|
|
57337
|
+
return row === void 0 ? null : { content: FileRow.parse(row).content };
|
|
57338
|
+
},
|
|
57339
|
+
async put(repo, { sha, path: path4 }, content) {
|
|
57340
|
+
upsertFile.run(repo.host, repo.owner, repo.repo, sha, path4, content, clock.now());
|
|
57341
|
+
},
|
|
57187
57342
|
close() {
|
|
57188
57343
|
if (!open2) return;
|
|
57189
57344
|
db.close();
|
|
@@ -61432,6 +61587,33 @@ var createAnalysisEvents = () => {
|
|
|
61432
61587
|
};
|
|
61433
61588
|
};
|
|
61434
61589
|
|
|
61590
|
+
// src/application/retries/retrying-git-host.ts
|
|
61591
|
+
var RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
|
|
61592
|
+
var retryingGitHost = (host, { sleep: sleep3, logger }) => {
|
|
61593
|
+
const retried = async (what, attempt) => {
|
|
61594
|
+
let result = await attempt();
|
|
61595
|
+
for (const delayMs of RETRY_DELAYS_MS) {
|
|
61596
|
+
if (result.ok || result.error.kind !== "network") return result;
|
|
61597
|
+
logger.warn(`Retrying ${what} after a network error`, {
|
|
61598
|
+
host: host.host,
|
|
61599
|
+
error: result.error.message,
|
|
61600
|
+
delayMs
|
|
61601
|
+
});
|
|
61602
|
+
await sleep3(delayMs);
|
|
61603
|
+
result = await attempt();
|
|
61604
|
+
}
|
|
61605
|
+
return result;
|
|
61606
|
+
};
|
|
61607
|
+
return {
|
|
61608
|
+
host: host.host,
|
|
61609
|
+
currentUser: () => retried("the current user", () => host.currentUser()),
|
|
61610
|
+
getPullRequest: (ref) => retried("the pull request", () => host.getPullRequest(ref)),
|
|
61611
|
+
getFiles: (repo, files) => retried(`${files.length} files`, () => host.getFiles(repo, files)),
|
|
61612
|
+
listPullRequests: () => retried("the pull request list", () => host.listPullRequests()),
|
|
61613
|
+
publish: (ref, publication, options) => host.publish(ref, publication, options)
|
|
61614
|
+
};
|
|
61615
|
+
};
|
|
61616
|
+
|
|
61435
61617
|
// src/application/use-cases/errors.ts
|
|
61436
61618
|
var appError = (code2, message) => ({ code: code2, message });
|
|
61437
61619
|
var fromHostError = (error62) => appError(error62.kind === "not-found" ? "not-found" : "host-error", error62.message);
|
|
@@ -61456,9 +61638,11 @@ var mapLimit2 = async (items, limit2, map2) => {
|
|
|
61456
61638
|
};
|
|
61457
61639
|
|
|
61458
61640
|
// src/application/use-cases/collect.ts
|
|
61459
|
-
var
|
|
61641
|
+
var FILES_PER_BATCH = 10;
|
|
61642
|
+
var BATCH_CONCURRENCY = 2;
|
|
61460
61643
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
61461
|
-
var collect = async (deps, host, pullRequest) => {
|
|
61644
|
+
var collect = async (deps, host, pullRequest, onFilesFetched = async () => {
|
|
61645
|
+
}) => {
|
|
61462
61646
|
const parsed = parseUnifiedDiff(pullRequest.diff);
|
|
61463
61647
|
if (!parsed.ok) {
|
|
61464
61648
|
const { line, message } = parsed.error;
|
|
@@ -61471,17 +61655,6 @@ var collect = async (deps, host, pullRequest) => {
|
|
|
61471
61655
|
language: deps.parser.languageFor(file2.path)
|
|
61472
61656
|
}));
|
|
61473
61657
|
const { repo } = pullRequest.ref;
|
|
61474
|
-
const fetchFile = async (sha, path4) => {
|
|
61475
|
-
const result = await host.getFile(repo, sha, path4);
|
|
61476
|
-
if (result.ok) return ok(fitsSize(result.value));
|
|
61477
|
-
if (result.error.kind === "too-large") return ok(null);
|
|
61478
|
-
return err2(
|
|
61479
|
-
appError(
|
|
61480
|
-
"host-error",
|
|
61481
|
-
`Couldn't fetch ${path4} at ${sha.slice(0, 7)}: ${result.error.message}`
|
|
61482
|
-
)
|
|
61483
|
-
);
|
|
61484
|
-
};
|
|
61485
61658
|
const fetches = [
|
|
61486
61659
|
{ side: "attributes", sha: pullRequest.baseSha, path: ".gitattributes" },
|
|
61487
61660
|
...files.filter((file2) => !file2.binary).flatMap((file2) => [
|
|
@@ -61489,10 +61662,35 @@ var collect = async (deps, host, pullRequest) => {
|
|
|
61489
61662
|
...file2.status === "deleted" ? [] : [{ side: "head", sha: pullRequest.headSha, path: file2.path }]
|
|
61490
61663
|
])
|
|
61491
61664
|
];
|
|
61492
|
-
const
|
|
61493
|
-
|
|
61494
|
-
|
|
61495
|
-
|
|
61665
|
+
const cached2 = await Promise.all(fetches.map((fetch2) => deps.fileCache.get(repo, fetch2)));
|
|
61666
|
+
const missing = fetches.map((fetch2, index) => ({ ...fetch2, index })).filter(({ index }) => cached2[index] === null);
|
|
61667
|
+
const batches = Array.from(
|
|
61668
|
+
{ length: Math.ceil(missing.length / FILES_PER_BATCH) },
|
|
61669
|
+
(_, i2) => missing.slice(i2 * FILES_PER_BATCH, (i2 + 1) * FILES_PER_BATCH)
|
|
61670
|
+
);
|
|
61671
|
+
const fetched = /* @__PURE__ */ new Map();
|
|
61672
|
+
let done = fetches.length - missing.length;
|
|
61673
|
+
await onFilesFetched({ done, total: fetches.length });
|
|
61674
|
+
const failures = await mapLimit2(batches, BATCH_CONCURRENCY, async (batch) => {
|
|
61675
|
+
const contents = await host.getFiles(repo, batch);
|
|
61676
|
+
if (!contents.ok) return contents.error;
|
|
61677
|
+
for (const [i2, file2] of batch.entries()) {
|
|
61678
|
+
const content = contents.value[i2] ?? null;
|
|
61679
|
+
fetched.set(file2.index, content);
|
|
61680
|
+
if (content === null || fitsSize(content) !== null) {
|
|
61681
|
+
await deps.fileCache.put(repo, file2, content);
|
|
61682
|
+
}
|
|
61683
|
+
}
|
|
61684
|
+
done += batch.length;
|
|
61685
|
+
await onFilesFetched({ done, total: fetches.length });
|
|
61686
|
+
return null;
|
|
61687
|
+
});
|
|
61688
|
+
const failure2 = failures.find((error62) => error62 !== null) ?? null;
|
|
61689
|
+
if (failure2 !== null) {
|
|
61690
|
+
return err2(
|
|
61691
|
+
appError("host-error", `Couldn't fetch the pull request's files: ${failure2.message}`)
|
|
61692
|
+
);
|
|
61693
|
+
}
|
|
61496
61694
|
let gitattributes = null;
|
|
61497
61695
|
const baseContents = /* @__PURE__ */ new Map();
|
|
61498
61696
|
const headContents = /* @__PURE__ */ new Map();
|
|
@@ -61500,10 +61698,10 @@ var collect = async (deps, host, pullRequest) => {
|
|
|
61500
61698
|
if (file2.status === "added") baseContents.set(file2.path, null);
|
|
61501
61699
|
if (file2.status === "deleted") headContents.set(file2.path, null);
|
|
61502
61700
|
}
|
|
61503
|
-
for (const { side, path: path4
|
|
61504
|
-
|
|
61505
|
-
if (side === "attributes") gitattributes = content
|
|
61506
|
-
else (side === "base" ? baseContents : headContents).set(path4, content
|
|
61701
|
+
for (const [index, { side, path: path4 }] of fetches.entries()) {
|
|
61702
|
+
const content = fitsSize(cached2[index]?.content ?? fetched.get(index) ?? null);
|
|
61703
|
+
if (side === "attributes") gitattributes = content;
|
|
61704
|
+
else (side === "base" ? baseContents : headContents).set(path4, content);
|
|
61507
61705
|
}
|
|
61508
61706
|
return ok({
|
|
61509
61707
|
files,
|
|
@@ -61828,6 +62026,10 @@ var createTracker = (deps, initial) => {
|
|
|
61828
62026
|
}));
|
|
61829
62027
|
deps.events.emit(analysis.id, { type: "updated", stage, status });
|
|
61830
62028
|
};
|
|
62029
|
+
const setFilesFetched = async (filesFetched) => {
|
|
62030
|
+
await save({ filesFetched });
|
|
62031
|
+
deps.events.emit(analysis.id, { type: "updated", stage: "collect", status: "running" });
|
|
62032
|
+
};
|
|
61831
62033
|
const finish = async (error62) => {
|
|
61832
62034
|
await save({ status: error62 === null ? "complete" : "failed", error: error62 });
|
|
61833
62035
|
deps.events.emit(analysis.id, { type: "done" });
|
|
@@ -61838,6 +62040,7 @@ var createTracker = (deps, initial) => {
|
|
|
61838
62040
|
},
|
|
61839
62041
|
save,
|
|
61840
62042
|
setStage,
|
|
62043
|
+
setFilesFetched,
|
|
61841
62044
|
finish,
|
|
61842
62045
|
fail: async (stage, failure2) => {
|
|
61843
62046
|
await setStage(stage, "failed");
|
|
@@ -61864,7 +62067,7 @@ var runPipeline = async (deps, host, analysis, pullRequest) => {
|
|
|
61864
62067
|
var stages = async (deps, host, pullRequest, tracker) => {
|
|
61865
62068
|
const { model, logger } = deps;
|
|
61866
62069
|
await tracker.setStage("collect", "running");
|
|
61867
|
-
const collected = await collect(deps, host, pullRequest);
|
|
62070
|
+
const collected = await collect(deps, host, pullRequest, tracker.setFilesFetched);
|
|
61868
62071
|
if (!collected.ok) return tracker.fail("collect", collected.error);
|
|
61869
62072
|
const { files, tickets, missingTicketKeys } = collected.value;
|
|
61870
62073
|
await tracker.setStage("collect", "done", (analysis) => ({
|
|
@@ -61986,6 +62189,7 @@ var newAnalysis = (deps, key, pullRequest, createdAt) => ({
|
|
|
61986
62189
|
decisions: "pending",
|
|
61987
62190
|
tour: "pending"
|
|
61988
62191
|
},
|
|
62192
|
+
filesFetched: null,
|
|
61989
62193
|
pullRequest,
|
|
61990
62194
|
files: [],
|
|
61991
62195
|
tickets: [],
|
|
@@ -62248,12 +62452,15 @@ var NO_ANTHROPIC_CREDENTIALS = "No Anthropic credentials found. Set ANTHROPIC_AP
|
|
|
62248
62452
|
var createHosts = (config2, options) => {
|
|
62249
62453
|
const { github, bitbucket } = config2;
|
|
62250
62454
|
const { clock, logger } = options;
|
|
62455
|
+
const retrying = (host) => retryingGitHost(host, { sleep: sleep2, logger });
|
|
62251
62456
|
return {
|
|
62252
62457
|
...github !== null && {
|
|
62253
|
-
github:
|
|
62458
|
+
github: retrying(
|
|
62459
|
+
createGitHubHost({ token: github.token, version: options.version, clock, logger })
|
|
62460
|
+
)
|
|
62254
62461
|
},
|
|
62255
62462
|
...bitbucket !== null && {
|
|
62256
|
-
bitbucket: createBitbucketHost({ ...bitbucket, clock, logger })
|
|
62463
|
+
bitbucket: retrying(createBitbucketHost({ ...bitbucket, clock, logger }))
|
|
62257
62464
|
}
|
|
62258
62465
|
};
|
|
62259
62466
|
};
|
|
@@ -62324,6 +62531,7 @@ var compose2 = async (config2, options) => {
|
|
|
62324
62531
|
model: modelConfigured ? claude : unconfiguredModel(claude.version),
|
|
62325
62532
|
modelConfigured,
|
|
62326
62533
|
store,
|
|
62534
|
+
fileCache: store,
|
|
62327
62535
|
parser: await createTreeSitterCodeParser({ grammarsDir: options.grammarsDir }),
|
|
62328
62536
|
clock,
|
|
62329
62537
|
logger
|