co-maintainer 0.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +9 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +2 -0
- package/dist/package.json +54 -0
- package/dist/src/ai/batch.d.ts +16 -0
- package/dist/src/ai/batch.js +117 -0
- package/dist/src/ai/fake.d.ts +7 -0
- package/dist/src/ai/fake.js +40 -0
- package/dist/src/ai/hetzner.d.ts +16 -0
- package/dist/src/ai/hetzner.js +69 -0
- package/dist/src/ai/mermaid.d.ts +27 -0
- package/dist/src/ai/mermaid.js +457 -0
- package/dist/src/ai/mermaid_loop.d.ts +12 -0
- package/dist/src/ai/mermaid_loop.js +100 -0
- package/dist/src/ai/openrouter.d.ts +9 -0
- package/dist/src/ai/openrouter.js +29 -0
- package/dist/src/ai/provider.d.ts +4 -0
- package/dist/src/ai/provider.js +63 -0
- package/dist/src/cli/args.d.ts +5 -0
- package/dist/src/cli/args.js +252 -0
- package/dist/src/cli/commands/probe.d.ts +2 -0
- package/dist/src/cli/commands/probe.js +122 -0
- package/dist/src/cli/commands/review.d.ts +3 -0
- package/dist/src/cli/commands/review.js +84 -0
- package/dist/src/cli/commands/serve.d.ts +18 -0
- package/dist/src/cli/commands/serve.js +183 -0
- package/dist/src/cli/commands/set.d.ts +7 -0
- package/dist/src/cli/commands/set.js +171 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +27 -0
- package/dist/src/cli/prompt.d.ts +4 -0
- package/dist/src/cli/prompt.js +26 -0
- package/dist/src/cli/review_args.d.ts +28 -0
- package/dist/src/cli/review_args.js +84 -0
- package/dist/src/cli/review_output.d.ts +2 -0
- package/dist/src/cli/review_output.js +10 -0
- package/dist/src/cli/review_result.d.ts +81 -0
- package/dist/src/cli/review_result.js +281 -0
- package/dist/src/config.d.ts +108 -0
- package/dist/src/config.js +138 -0
- package/dist/src/git/default_branch.d.ts +5 -0
- package/dist/src/git/default_branch.js +22 -0
- package/dist/src/github/app.d.ts +43 -0
- package/dist/src/github/app.js +120 -0
- package/dist/src/github/client.d.ts +17 -0
- package/dist/src/github/client.js +66 -0
- package/dist/src/github/collect.d.ts +7 -0
- package/dist/src/github/collect.js +368 -0
- package/dist/src/github/gh.d.ts +8 -0
- package/dist/src/github/gh.js +63 -0
- package/dist/src/github/jwt.d.ts +12 -0
- package/dist/src/github/jwt.js +84 -0
- package/dist/src/github/oauth.d.ts +9 -0
- package/dist/src/github/oauth.js +51 -0
- package/dist/src/github/pat.d.ts +10 -0
- package/dist/src/github/pat.js +60 -0
- package/dist/src/knowledge/facts.d.ts +3 -0
- package/dist/src/knowledge/facts.js +325 -0
- package/dist/src/knowledge/guide.d.ts +6 -0
- package/dist/src/knowledge/guide.js +82 -0
- package/dist/src/knowledge/probe.d.ts +12 -0
- package/dist/src/knowledge/probe.js +144 -0
- package/dist/src/knowledge/sections.d.ts +2 -0
- package/dist/src/knowledge/sections.js +13 -0
- package/dist/src/knowledge/skill.d.ts +10 -0
- package/dist/src/knowledge/skill.js +106 -0
- package/dist/src/knowledge/synthesis.d.ts +7 -0
- package/dist/src/knowledge/synthesis.js +315 -0
- package/dist/src/knowledge/types.d.ts +49 -0
- package/dist/src/knowledge/types.js +1 -0
- package/dist/src/knowledge/validate.d.ts +6 -0
- package/dist/src/knowledge/validate.js +134 -0
- package/dist/src/local/carry_over_store.d.ts +18 -0
- package/dist/src/local/carry_over_store.js +40 -0
- package/dist/src/local/codegraph_exclude.d.ts +3 -0
- package/dist/src/local/codegraph_exclude.js +26 -0
- package/dist/src/local/codegraph_prepare.d.ts +17 -0
- package/dist/src/local/codegraph_prepare.js +51 -0
- package/dist/src/local/git_ops.d.ts +12 -0
- package/dist/src/local/git_ops.js +98 -0
- package/dist/src/local/git_parse.d.ts +27 -0
- package/dist/src/local/git_parse.js +131 -0
- package/dist/src/local/git_revision.d.ts +15 -0
- package/dist/src/local/git_revision.js +141 -0
- package/dist/src/local/git_untracked.d.ts +21 -0
- package/dist/src/local/git_untracked.js +79 -0
- package/dist/src/local/review_local.d.ts +4 -0
- package/dist/src/local/review_local.js +273 -0
- package/dist/src/local/review_lock.d.ts +7 -0
- package/dist/src/local/review_lock.js +45 -0
- package/dist/src/pr/checkout.d.ts +25 -0
- package/dist/src/pr/checkout.js +136 -0
- package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
- package/dist/src/pr/codegraph_tool_args.js +34 -0
- package/dist/src/pr/codegraph_tools.d.ts +15 -0
- package/dist/src/pr/codegraph_tools.js +408 -0
- package/dist/src/pr/diff_summary.d.ts +6 -0
- package/dist/src/pr/diff_summary.js +52 -0
- package/dist/src/pr/findings.d.ts +14 -0
- package/dist/src/pr/findings.js +94 -0
- package/dist/src/pr/github_revision.d.ts +4 -0
- package/dist/src/pr/github_revision.js +39 -0
- package/dist/src/pr/hunks.d.ts +18 -0
- package/dist/src/pr/hunks.js +73 -0
- package/dist/src/pr/reviewer.d.ts +40 -0
- package/dist/src/pr/reviewer.js +582 -0
- package/dist/src/pr/rounds.d.ts +17 -0
- package/dist/src/pr/rounds.js +43 -0
- package/dist/src/pr/scope.d.ts +35 -0
- package/dist/src/pr/scope.js +84 -0
- package/dist/src/pr/snapshot.d.ts +11 -0
- package/dist/src/pr/snapshot.js +1 -0
- package/dist/src/pr/suggestion.d.ts +17 -0
- package/dist/src/pr/suggestion.js +70 -0
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +199 -0
- package/dist/src/remote/client_tools.d.ts +13 -0
- package/dist/src/remote/client_tools.js +30 -0
- package/dist/src/remote/fixtures.d.ts +1 -0
- package/dist/src/remote/fixtures.js +30 -0
- package/dist/src/remote/revision_from_submit.d.ts +2 -0
- package/dist/src/remote/revision_from_submit.js +26 -0
- package/dist/src/remote/schema.d.ts +14 -0
- package/dist/src/remote/schema.js +30 -0
- package/dist/src/remote/server/body.d.ts +18 -0
- package/dist/src/remote/server/body.js +73 -0
- package/dist/src/remote/server/routes.d.ts +1 -0
- package/dist/src/remote/server/routes.js +150 -0
- package/dist/src/remote/server/sessions.d.ts +16 -0
- package/dist/src/remote/server/sessions.js +48 -0
- package/dist/src/remote/server/sync.d.ts +3 -0
- package/dist/src/remote/server/sync.js +90 -0
- package/dist/src/remote/tool_bridge.d.ts +15 -0
- package/dist/src/remote/tool_bridge.js +84 -0
- package/dist/src/remote/validate.d.ts +8 -0
- package/dist/src/remote/validate.js +248 -0
- package/dist/src/review/carry_over.d.ts +63 -0
- package/dist/src/review/carry_over.js +372 -0
- package/dist/src/review/engine.d.ts +13 -0
- package/dist/src/review/engine.js +4 -0
- package/dist/src/review/guides.d.ts +12 -0
- package/dist/src/review/guides.js +44 -0
- package/dist/src/review/revision.d.ts +26 -0
- package/dist/src/review/revision.js +57 -0
- package/dist/src/server/api/activity.d.ts +1 -0
- package/dist/src/server/api/activity.js +12 -0
- package/dist/src/server/api/analytics.d.ts +1 -0
- package/dist/src/server/api/analytics.js +9 -0
- package/dist/src/server/api/installations.d.ts +4 -0
- package/dist/src/server/api/installations.js +24 -0
- package/dist/src/server/api/jobs.d.ts +1 -0
- package/dist/src/server/api/jobs.js +86 -0
- package/dist/src/server/api/json_body.d.ts +1 -0
- package/dist/src/server/api/json_body.js +14 -0
- package/dist/src/server/api/remote_tokens.d.ts +1 -0
- package/dist/src/server/api/remote_tokens.js +71 -0
- package/dist/src/server/api/repos.d.ts +4 -0
- package/dist/src/server/api/repos.js +136 -0
- package/dist/src/server/api/settings.d.ts +1 -0
- package/dist/src/server/api/settings.js +210 -0
- package/dist/src/server/app.d.ts +27 -0
- package/dist/src/server/app.js +147 -0
- package/dist/src/server/assets.d.ts +3 -0
- package/dist/src/server/assets.js +21 -0
- package/dist/src/server/auth.d.ts +44 -0
- package/dist/src/server/auth.js +188 -0
- package/dist/src/server/errors.d.ts +8 -0
- package/dist/src/server/errors.js +10 -0
- package/dist/src/server/http.d.ts +8 -0
- package/dist/src/server/http.js +117 -0
- package/dist/src/server/logo.d.ts +1 -0
- package/dist/src/server/logo.js +2 -0
- package/dist/src/server/pages/activity.d.ts +4 -0
- package/dist/src/server/pages/activity.js +200 -0
- package/dist/src/server/pages/add_repo.d.ts +9 -0
- package/dist/src/server/pages/add_repo.js +56 -0
- package/dist/src/server/pages/analytics.d.ts +2 -0
- package/dist/src/server/pages/analytics.js +193 -0
- package/dist/src/server/pages/client.d.ts +2 -0
- package/dist/src/server/pages/client.js +2 -0
- package/dist/src/server/pages/home.d.ts +16 -0
- package/dist/src/server/pages/home.js +87 -0
- package/dist/src/server/pages/knowledge.d.ts +2 -0
- package/dist/src/server/pages/knowledge.js +64 -0
- package/dist/src/server/pages/layout.d.ts +32 -0
- package/dist/src/server/pages/layout.js +230 -0
- package/dist/src/server/pages/pr.d.ts +2 -0
- package/dist/src/server/pages/pr.js +109 -0
- package/dist/src/server/pages/repo.d.ts +2 -0
- package/dist/src/server/pages/repo.js +92 -0
- package/dist/src/server/pages/repo_prs.d.ts +2 -0
- package/dist/src/server/pages/repo_prs.js +75 -0
- package/dist/src/server/pages/repo_remote.d.ts +2 -0
- package/dist/src/server/pages/repo_remote.js +38 -0
- package/dist/src/server/pages/repo_settings.d.ts +3 -0
- package/dist/src/server/pages/repo_settings.js +127 -0
- package/dist/src/server/pages/router.d.ts +17 -0
- package/dist/src/server/pages/router.js +312 -0
- package/dist/src/server/pages/settings.d.ts +2 -0
- package/dist/src/server/pages/settings.js +337 -0
- package/dist/src/server/pages/setup.d.ts +5 -0
- package/dist/src/server/pages/setup.js +38 -0
- package/dist/src/server/pages/styles.d.ts +2 -0
- package/dist/src/server/pages/styles.js +2 -0
- package/dist/src/server/webhook/index.d.ts +1 -0
- package/dist/src/server/webhook/index.js +64 -0
- package/dist/src/server/webhook/signature.d.ts +7 -0
- package/dist/src/server/webhook/signature.js +41 -0
- package/dist/src/services/credentials.d.ts +19 -0
- package/dist/src/services/credentials.js +146 -0
- package/dist/src/services/dashboard.d.ts +165 -0
- package/dist/src/services/dashboard.js +200 -0
- package/dist/src/services/drift.d.ts +13 -0
- package/dist/src/services/drift.js +76 -0
- package/dist/src/services/jobs.d.ts +34 -0
- package/dist/src/services/jobs.js +222 -0
- package/dist/src/services/remote_review.d.ts +10 -0
- package/dist/src/services/remote_review.js +274 -0
- package/dist/src/services/remote_review_abort.d.ts +3 -0
- package/dist/src/services/remote_review_abort.js +11 -0
- package/dist/src/services/remote_token_jobs.d.ts +1 -0
- package/dist/src/services/remote_token_jobs.js +20 -0
- package/dist/src/services/remote_tokens.d.ts +13 -0
- package/dist/src/services/remote_tokens.js +57 -0
- package/dist/src/services/replies.d.ts +7 -0
- package/dist/src/services/replies.js +248 -0
- package/dist/src/services/review.d.ts +35 -0
- package/dist/src/services/review.js +671 -0
- package/dist/src/services/setup.d.ts +29 -0
- package/dist/src/services/setup.js +364 -0
- package/dist/src/services/webhook.d.ts +22 -0
- package/dist/src/services/webhook.js +401 -0
- package/dist/src/store/app_db.d.ts +10 -0
- package/dist/src/store/app_db.js +85 -0
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +63 -0
- package/dist/src/store/deliveries.d.ts +12 -0
- package/dist/src/store/deliveries.js +23 -0
- package/dist/src/store/drift.d.ts +4 -0
- package/dist/src/store/drift.js +22 -0
- package/dist/src/store/findings.d.ts +31 -0
- package/dist/src/store/findings.js +64 -0
- package/dist/src/store/installations.d.ts +9 -0
- package/dist/src/store/installations.js +35 -0
- package/dist/src/store/job_logs.d.ts +5 -0
- package/dist/src/store/job_logs.js +17 -0
- package/dist/src/store/jobs.d.ts +27 -0
- package/dist/src/store/jobs.js +86 -0
- package/dist/src/store/migrations.d.ts +5 -0
- package/dist/src/store/migrations.js +258 -0
- package/dist/src/store/remote_review_inputs.d.ts +11 -0
- package/dist/src/store/remote_review_inputs.js +25 -0
- package/dist/src/store/remote_tokens.d.ts +10 -0
- package/dist/src/store/remote_tokens.js +45 -0
- package/dist/src/store/replies.d.ts +24 -0
- package/dist/src/store/replies.js +79 -0
- package/dist/src/store/repos.d.ts +11 -0
- package/dist/src/store/repos.js +59 -0
- package/dist/src/store/reviews.d.ts +86 -0
- package/dist/src/store/reviews.js +220 -0
- package/dist/src/store/rows.d.ts +179 -0
- package/dist/src/store/rows.js +4 -0
- package/dist/src/store/sessions.d.ts +8 -0
- package/dist/src/store/sessions.js +34 -0
- package/dist/src/store/skill_state.d.ts +3 -0
- package/dist/src/store/skill_state.js +24 -0
- package/dist/src/store/sqlite.d.ts +39 -0
- package/dist/src/store/sqlite.js +77 -0
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/subjects.js +78 -0
- package/dist/src/tools/codegraph.d.ts +58 -0
- package/dist/src/tools/codegraph.js +165 -0
- package/dist/src/tools/codegraph_exec.d.ts +13 -0
- package/dist/src/tools/codegraph_exec.js +66 -0
- package/dist/src/types.d.ts +89 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util/atomic.d.ts +4 -0
- package/dist/src/util/atomic.js +78 -0
- package/dist/src/util/keyed_lock.d.ts +3 -0
- package/dist/src/util/keyed_lock.js +33 -0
- package/dist/src/util/log.d.ts +11 -0
- package/dist/src/util/log.js +42 -0
- package/dist/src/util/redact.d.ts +5 -0
- package/dist/src/util/redact.js +30 -0
- package/dist/src/util/runtime.d.ts +90 -0
- package/dist/src/util/runtime.js +206 -0
- package/dist/src/util/time.d.ts +9 -0
- package/dist/src/util/time.js +36 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +5 -0
- package/package.json +54 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { githubFetch, GitHubHttpError, paginate } from "./client.js";
|
|
2
|
+
import { importAppPrivateKey, signAppJwt } from "./jwt.js";
|
|
3
|
+
const API = "https://api.github.com";
|
|
4
|
+
function headers(token) {
|
|
5
|
+
return {
|
|
6
|
+
Accept: "application/vnd.github+json",
|
|
7
|
+
Authorization: `Bearer ${token}`,
|
|
8
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
async function call(endpoint, token, init = {}) {
|
|
12
|
+
const response = await githubFetch(`${API}/${endpoint}`, {
|
|
13
|
+
...init,
|
|
14
|
+
headers: { ...headers(token), ...(init.headers ?? {}) },
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new GitHubHttpError(response.status, await response.text());
|
|
18
|
+
}
|
|
19
|
+
if (response.status === 204)
|
|
20
|
+
return undefined;
|
|
21
|
+
return (await response.json());
|
|
22
|
+
}
|
|
23
|
+
async function get(endpoint, token) {
|
|
24
|
+
return await call(endpoint, token);
|
|
25
|
+
}
|
|
26
|
+
const installationTokens = new Map();
|
|
27
|
+
/** App-level calls: list installations, mint an installation access token.
|
|
28
|
+
* Not scoped to one installation — `AppClient` below is. */
|
|
29
|
+
export class AppJwtClient {
|
|
30
|
+
appId;
|
|
31
|
+
privateKeyPem;
|
|
32
|
+
constructor(appId, privateKeyPem) {
|
|
33
|
+
this.appId = appId;
|
|
34
|
+
this.privateKeyPem = privateKeyPem;
|
|
35
|
+
}
|
|
36
|
+
async jwt() {
|
|
37
|
+
const key = await importAppPrivateKey(this.privateKeyPem);
|
|
38
|
+
return await signAppJwt(this.appId, key);
|
|
39
|
+
}
|
|
40
|
+
async listInstallations() {
|
|
41
|
+
const jwt = await this.jwt();
|
|
42
|
+
return await paginate((endpoint) => get(endpoint, jwt), "app/installations");
|
|
43
|
+
}
|
|
44
|
+
/** Cached until 60s before the token's own `expires_at`. */
|
|
45
|
+
async installationToken(installationId) {
|
|
46
|
+
const cached = installationTokens.get(installationId);
|
|
47
|
+
if (cached && cached.expiresAt > Date.now() + 60_000)
|
|
48
|
+
return cached.token;
|
|
49
|
+
const jwt = await this.jwt();
|
|
50
|
+
const response = await githubFetch(`${API}/app/installations/${installationId}/access_tokens`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: headers(jwt),
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`GitHub API ${response.status}: ${await response.text()}`);
|
|
56
|
+
}
|
|
57
|
+
const body = (await response.json());
|
|
58
|
+
const expiresAt = new Date(body.expires_at).getTime();
|
|
59
|
+
installationTokens.set(installationId, { token: body.token, expiresAt });
|
|
60
|
+
return body.token;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** A `GitHubClient` scoped to one installation. */
|
|
64
|
+
export class AppClient {
|
|
65
|
+
app;
|
|
66
|
+
installationId;
|
|
67
|
+
constructor(appId, privateKeyPem, installationId) {
|
|
68
|
+
this.app = new AppJwtClient(appId, privateKeyPem);
|
|
69
|
+
this.installationId = installationId;
|
|
70
|
+
}
|
|
71
|
+
async request(endpoint) {
|
|
72
|
+
return await get(endpoint, await this.app.installationToken(this.installationId));
|
|
73
|
+
}
|
|
74
|
+
async pages(endpoint, limit, progress) {
|
|
75
|
+
const token = await this.app.installationToken(this.installationId);
|
|
76
|
+
return await paginate((e) => get(e, token), endpoint, limit, progress);
|
|
77
|
+
}
|
|
78
|
+
async write(endpoint, body) {
|
|
79
|
+
return await call(endpoint, await this.app.installationToken(this.installationId), {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "content-type": "application/json" },
|
|
82
|
+
body: JSON.stringify(body),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async createCheckRun(endpoint, body) {
|
|
86
|
+
return await this.write(endpoint, body);
|
|
87
|
+
}
|
|
88
|
+
async updateCheckRun(endpoint, body) {
|
|
89
|
+
return await call(endpoint, await this.app.installationToken(this.installationId), {
|
|
90
|
+
method: "PATCH",
|
|
91
|
+
headers: { "content-type": "application/json" },
|
|
92
|
+
body: JSON.stringify(body),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** For `GET /api/installations`: every installation the App has, each with
|
|
97
|
+
* the repos it can see. */
|
|
98
|
+
export async function listInstallationsWithRepos(appId, privateKeyPem) {
|
|
99
|
+
const app = new AppJwtClient(appId, privateKeyPem);
|
|
100
|
+
const installations = await app.listInstallations();
|
|
101
|
+
const result = [];
|
|
102
|
+
for (const installation of installations) {
|
|
103
|
+
const token = await app.installationToken(installation.id);
|
|
104
|
+
const body = await get("installation/repositories", token);
|
|
105
|
+
const repos = Array.isArray(body.repositories)
|
|
106
|
+
? body.repositories.map((repo) => ({
|
|
107
|
+
fullName: String(repo.full_name ?? ""),
|
|
108
|
+
private: Boolean(repo.private),
|
|
109
|
+
}))
|
|
110
|
+
: [];
|
|
111
|
+
result.push({ installation, repos });
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
export async function findInstallationForRepo(appId, privateKeyPem, fullName) {
|
|
116
|
+
const normalized = fullName.toLowerCase();
|
|
117
|
+
const installations = await listInstallationsWithRepos(appId, privateKeyPem);
|
|
118
|
+
const match = installations.find(({ repos }) => repos.some((repo) => repo.fullName.toLowerCase() === normalized));
|
|
119
|
+
return match?.installation.id;
|
|
120
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Shared by every `GitHubClient` implementation that talks over `fetch`
|
|
2
|
+
* (pat.ts, and app.ts in P5). `GhClient` shells out to the `gh` binary,
|
|
3
|
+
* which already retries on its own, so it never calls `githubFetch` — it
|
|
4
|
+
* still shares `paginate`, the one 20-line loop every client used to carry
|
|
5
|
+
* its own copy of. */
|
|
6
|
+
export declare class GitHubHttpError extends Error {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
constructor(status: number, body: string);
|
|
9
|
+
}
|
|
10
|
+
export declare function isAccessDenied(error: unknown): boolean;
|
|
11
|
+
/** A secondary (abuse-detection) rate limit carries a short `retry-after`
|
|
12
|
+
* and is retried in place, up to three attempts total. A primary limit at
|
|
13
|
+
* zero remaining fails the request right away, with the reset time in the
|
|
14
|
+
* message, rather than pausing the caller for however long that reset
|
|
15
|
+
* takes — the caller decides whether to wait, not this function. */
|
|
16
|
+
export declare function githubFetch(url: string, init: RequestInit): Promise<Response>;
|
|
17
|
+
export declare function paginate<T>(request: (endpoint: string) => Promise<T[]>, endpoint: string, limit?: number, progress?: (page: number, fetched: number) => void): Promise<T[]>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Shared by every `GitHubClient` implementation that talks over `fetch`
|
|
2
|
+
* (pat.ts, and app.ts in P5). `GhClient` shells out to the `gh` binary,
|
|
3
|
+
* which already retries on its own, so it never calls `githubFetch` — it
|
|
4
|
+
* still shares `paginate`, the one 20-line loop every client used to carry
|
|
5
|
+
* its own copy of. */
|
|
6
|
+
export class GitHubHttpError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
constructor(status, body) {
|
|
9
|
+
super(`GitHub API ${status}: ${body}`);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.name = "GitHubHttpError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isAccessDenied(error) {
|
|
15
|
+
return (error instanceof GitHubHttpError &&
|
|
16
|
+
(error.status === 403 || error.status === 404));
|
|
17
|
+
}
|
|
18
|
+
function rateLimit(headers) {
|
|
19
|
+
const remaining = headers.get("x-ratelimit-remaining");
|
|
20
|
+
const reset = headers.get("x-ratelimit-reset");
|
|
21
|
+
if (remaining === null || reset === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
return {
|
|
24
|
+
remaining: Number(remaining),
|
|
25
|
+
resetAt: new Date(Number(reset) * 1000),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** A secondary (abuse-detection) rate limit carries a short `retry-after`
|
|
29
|
+
* and is retried in place, up to three attempts total. A primary limit at
|
|
30
|
+
* zero remaining fails the request right away, with the reset time in the
|
|
31
|
+
* message, rather than pausing the caller for however long that reset
|
|
32
|
+
* takes — the caller decides whether to wait, not this function. */
|
|
33
|
+
export async function githubFetch(url, init) {
|
|
34
|
+
let response;
|
|
35
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
36
|
+
response = await fetch(url, init);
|
|
37
|
+
if (response.status !== 403 && response.status !== 429)
|
|
38
|
+
return response;
|
|
39
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
40
|
+
const retryAfter = retryAfterHeader === null ? NaN : Number(retryAfterHeader);
|
|
41
|
+
if (Number.isFinite(retryAfter) && retryAfter >= 0 && attempt < 3) {
|
|
42
|
+
console.log(`[github] secondary rate limit, waiting ${retryAfter}s (attempt ${attempt} of 3)`);
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const limit = rateLimit(response.headers);
|
|
47
|
+
if (limit && limit.remaining === 0) {
|
|
48
|
+
throw new Error(`GitHub rate limit exhausted, resets at ${limit.resetAt.toISOString()}`);
|
|
49
|
+
}
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
return response;
|
|
53
|
+
}
|
|
54
|
+
export async function paginate(request, endpoint, limit, progress) {
|
|
55
|
+
const items = [];
|
|
56
|
+
for (let page = 1;; page++) {
|
|
57
|
+
const separator = endpoint.includes("?") ? "&" : "?";
|
|
58
|
+
const pageItems = await request(`${endpoint}${separator}per_page=100&page=${page}`);
|
|
59
|
+
items.push(...pageItems);
|
|
60
|
+
progress?.(page, items.length);
|
|
61
|
+
if (pageItems.length < 100 ||
|
|
62
|
+
(limit !== undefined && items.length >= limit))
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
return limit === undefined ? items : items.slice(0, limit);
|
|
66
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GitHubClient, Options } from "../types.ts";
|
|
2
|
+
import type { PullRequest, Source, State } from "../knowledge/types.ts";
|
|
3
|
+
export declare function collectSource(client: GitHubClient, options: Options, previous?: State, progress?: (items: PullRequest[]) => Promise<void>, codebaseProgress?: (data: {
|
|
4
|
+
tree: string[];
|
|
5
|
+
treeSha: Record<string, string>;
|
|
6
|
+
files: Record<string, string>;
|
|
7
|
+
}) => Promise<void>): Promise<Source>;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { log, startHeartbeat } from "../util/log.js";
|
|
2
|
+
import { cacheGet, cacheSet } from "../store/cache_db.js";
|
|
3
|
+
function decodeBase64(value) {
|
|
4
|
+
const binary = atob(value.replaceAll("\n", ""));
|
|
5
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
6
|
+
return new TextDecoder().decode(bytes);
|
|
7
|
+
}
|
|
8
|
+
function percent(done, total) {
|
|
9
|
+
if (total <= 0)
|
|
10
|
+
return "0%";
|
|
11
|
+
return `${Math.round((done / total) * 100)}%`;
|
|
12
|
+
}
|
|
13
|
+
function withinPrWindow(updatedAt, months) {
|
|
14
|
+
if (months === undefined || months === 0)
|
|
15
|
+
return true;
|
|
16
|
+
const cutoff = new Date();
|
|
17
|
+
cutoff.setMonth(cutoff.getMonth() - months);
|
|
18
|
+
return new Date(updatedAt) >= cutoff;
|
|
19
|
+
}
|
|
20
|
+
function listingCovers(cachedMonths, current) {
|
|
21
|
+
if (cachedMonths === 0)
|
|
22
|
+
return true;
|
|
23
|
+
if (current === undefined || current === 0)
|
|
24
|
+
return false;
|
|
25
|
+
return cachedMonths >= current;
|
|
26
|
+
}
|
|
27
|
+
async function loadListing(repo) {
|
|
28
|
+
const raw = await cacheGet("pr-listing", repo);
|
|
29
|
+
if (!raw)
|
|
30
|
+
return undefined;
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
if (!Array.isArray(parsed.items))
|
|
33
|
+
return undefined;
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
async function saveListing(repo, maxPrMonths, items) {
|
|
37
|
+
await cacheSet("pr-listing", repo, JSON.stringify({ maxPrMonths: maxPrMonths ?? 0, items }));
|
|
38
|
+
}
|
|
39
|
+
async function listPullRequestPages(client, options, phase) {
|
|
40
|
+
const cached = await loadListing(options.repo);
|
|
41
|
+
const canCatchUp = cached !== undefined &&
|
|
42
|
+
listingCovers(cached.maxPrMonths, options.maxPrMonths);
|
|
43
|
+
const cachedByNumber = new Map((cached?.items ?? []).map((pr) => [Number(pr.number), pr]));
|
|
44
|
+
const selected = [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const concurrency = Math.max(1, options.ghConcurrent);
|
|
47
|
+
log("fetch", `pull request listing · concurrency=${concurrency}`);
|
|
48
|
+
const fetchPage = async (page) => {
|
|
49
|
+
const pageItems = await client.request(`repos/${options.repo}/pulls?state=all&sort=updated&direction=desc&per_page=100&page=${page}`);
|
|
50
|
+
return Array.isArray(pageItems) ? pageItems : [];
|
|
51
|
+
};
|
|
52
|
+
const ingest = (page, pageItems) => {
|
|
53
|
+
if (pageItems.length === 0) {
|
|
54
|
+
const message = `pull request listing · page ${page} · fetched ${selected.length} · done`;
|
|
55
|
+
if (phase)
|
|
56
|
+
phase.text = message;
|
|
57
|
+
log("fetch", message);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
let stop = false;
|
|
61
|
+
let reason = "total unknown";
|
|
62
|
+
for (const pr of pageItems) {
|
|
63
|
+
const updatedAt = String(pr.updated_at ?? "");
|
|
64
|
+
if (!withinPrWindow(updatedAt, options.maxPrMonths)) {
|
|
65
|
+
stop = true;
|
|
66
|
+
reason = "window reached";
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const number = Number(pr.number);
|
|
70
|
+
selected.push(pr);
|
|
71
|
+
seen.add(number);
|
|
72
|
+
const prior = cachedByNumber.get(number);
|
|
73
|
+
if (canCatchUp && String(prior?.updated_at ?? "") === updatedAt) {
|
|
74
|
+
stop = true;
|
|
75
|
+
reason = "cache catch-up";
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const message = `pull request listing · page ${page} · fetched ${selected.length} · ${reason}`;
|
|
80
|
+
if (phase)
|
|
81
|
+
phase.text = message;
|
|
82
|
+
log("fetch", message);
|
|
83
|
+
return stop || pageItems.length < 100;
|
|
84
|
+
};
|
|
85
|
+
const first = await fetchPage(1);
|
|
86
|
+
let done = ingest(1, first);
|
|
87
|
+
let page = 2;
|
|
88
|
+
while (!done) {
|
|
89
|
+
const batch = Array.from({ length: concurrency }, (_, index) => page + index);
|
|
90
|
+
const pages = await mapPool(batch, concurrency, (item) => fetchPage(item));
|
|
91
|
+
for (let index = 0; index < pages.length; index++) {
|
|
92
|
+
if (ingest(batch[index], pages[index])) {
|
|
93
|
+
done = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
page += concurrency;
|
|
98
|
+
}
|
|
99
|
+
if (canCatchUp) {
|
|
100
|
+
let reused = 0;
|
|
101
|
+
for (const pr of cached?.items ?? []) {
|
|
102
|
+
const number = Number(pr.number);
|
|
103
|
+
if (seen.has(number))
|
|
104
|
+
continue;
|
|
105
|
+
if (!withinPrWindow(String(pr.updated_at ?? ""), options.maxPrMonths)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
selected.push(pr);
|
|
109
|
+
seen.add(number);
|
|
110
|
+
reused++;
|
|
111
|
+
}
|
|
112
|
+
if (reused > 0) {
|
|
113
|
+
log("fetch", `pull request listing · reused ${reused} cached PRs · ${selected.length} total`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await saveListing(options.repo, options.maxPrMonths, selected);
|
|
117
|
+
return selected;
|
|
118
|
+
}
|
|
119
|
+
async function mapPool(items, concurrency, fn) {
|
|
120
|
+
const results = new Array(items.length);
|
|
121
|
+
if (items.length === 0)
|
|
122
|
+
return results;
|
|
123
|
+
let cursor = 0;
|
|
124
|
+
const worker = async () => {
|
|
125
|
+
while (cursor < items.length) {
|
|
126
|
+
const index = cursor++;
|
|
127
|
+
results[index] = await fn(items[index], index);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
await Promise.all(Array.from({ length: Math.min(Math.max(1, concurrency), items.length) }, worker));
|
|
131
|
+
return results;
|
|
132
|
+
}
|
|
133
|
+
async function file(client, repo, path, branch) {
|
|
134
|
+
try {
|
|
135
|
+
const encodedPath = path.split("/").map(encodeURIComponent).join("/");
|
|
136
|
+
const result = await client.request(`repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
|
|
137
|
+
if (typeof result.content !== "string")
|
|
138
|
+
return undefined;
|
|
139
|
+
return decodeBase64(result.content);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function codebase(client, repo, meta, concurrency, previous, phase, progress) {
|
|
146
|
+
const branch = String(meta.default_branch ?? "main");
|
|
147
|
+
const response = await client.request(`repos/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1`);
|
|
148
|
+
const blobs = Array.isArray(response.tree)
|
|
149
|
+
? response.tree.filter((item) => item.type === "blob")
|
|
150
|
+
: [];
|
|
151
|
+
const tree = blobs.map((item) => String(item.path));
|
|
152
|
+
const treeSha = Object.fromEntries(blobs.map((item) => {
|
|
153
|
+
const value = item;
|
|
154
|
+
return [String(value.path), String(value.sha ?? "")];
|
|
155
|
+
}));
|
|
156
|
+
const canonical = tree.filter((path) => /(^|\/)(README|CONTRIBUTING|CODE_OF_CONDUCT|SECURITY|CHANGELOG)(\.[^/]*)?$|(^|\/)(package\.json|deno\.json|Cargo\.toml|Makefile|justfile|CODEOWNERS|build\.rs)$|^\.github\/(workflows\/.+\.(yml|yaml)|PULL_REQUEST_TEMPLATE.*)$/i.test(path));
|
|
157
|
+
const representative = tree
|
|
158
|
+
.filter((path) => /\.(rs|ts|tsx|js|jsx|py|go|java|cs|cpp|c|swift)$/i.test(path) &&
|
|
159
|
+
!/(^|\/)(test|tests|vendor|dist|target)\//i.test(path))
|
|
160
|
+
.slice(0, 20);
|
|
161
|
+
const important = [...new Set([...canonical, ...representative])].slice(0, 80);
|
|
162
|
+
const files = {};
|
|
163
|
+
let completed = 0;
|
|
164
|
+
let save = Promise.resolve();
|
|
165
|
+
if (phase) {
|
|
166
|
+
phase.text = `codebase files 0/${important.length} · 0% · concurrency=${concurrency}`;
|
|
167
|
+
}
|
|
168
|
+
log("fetch", `codebase · ${important.length} files · concurrency=${concurrency}`);
|
|
169
|
+
const checkpoint = () => {
|
|
170
|
+
if (!progress)
|
|
171
|
+
return Promise.resolve();
|
|
172
|
+
save = save.then(() => progress({ tree, treeSha, files: { ...files } }));
|
|
173
|
+
return save;
|
|
174
|
+
};
|
|
175
|
+
await mapPool(important, concurrency, async (path) => {
|
|
176
|
+
if (previous?.treeSha?.[path] &&
|
|
177
|
+
previous.treeSha[path] === treeSha[path] &&
|
|
178
|
+
previous.files[path] !== undefined) {
|
|
179
|
+
files[path] = previous.files[path];
|
|
180
|
+
completed++;
|
|
181
|
+
if (phase) {
|
|
182
|
+
phase.text = `codebase files ${completed}/${important.length} · ${percent(completed, important.length)}`;
|
|
183
|
+
}
|
|
184
|
+
log("fetch", `codebase cache ${completed}/${important.length} · ${percent(completed, important.length)}: ${path}`);
|
|
185
|
+
await checkpoint();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const content = await file(client, repo, path, branch);
|
|
189
|
+
if (content !== undefined && content.length <= 200_000) {
|
|
190
|
+
files[path] = content;
|
|
191
|
+
}
|
|
192
|
+
completed++;
|
|
193
|
+
if (phase) {
|
|
194
|
+
phase.text = `codebase files ${completed}/${important.length} · ${percent(completed, important.length)}`;
|
|
195
|
+
}
|
|
196
|
+
log("fetch", `codebase file ${completed}/${important.length} · ${percent(completed, important.length)}: ${path}`);
|
|
197
|
+
await checkpoint();
|
|
198
|
+
});
|
|
199
|
+
return { tree, treeSha, files };
|
|
200
|
+
}
|
|
201
|
+
async function pullRequests(client, options, previous, phase, progress) {
|
|
202
|
+
if (phase)
|
|
203
|
+
phase.text = "listing pull requests · page 0 · fetched 0";
|
|
204
|
+
const selected = await listPullRequestPages(client, options, phase);
|
|
205
|
+
const previousByNumber = new Map((previous?.source.pullRequests ?? []).map((pr) => [pr.number, pr]));
|
|
206
|
+
const result = new Array(selected.length);
|
|
207
|
+
let completed = 0;
|
|
208
|
+
let save = Promise.resolve();
|
|
209
|
+
if (phase) {
|
|
210
|
+
phase.text = `pull requests 0/${selected.length} · 0% · concurrency=${options.ghConcurrent}`;
|
|
211
|
+
}
|
|
212
|
+
log("fetch", `${selected.length} pull requests · concurrency=${options.ghConcurrent}`);
|
|
213
|
+
await mapPool(selected, options.ghConcurrent, async (pr, index) => {
|
|
214
|
+
const number = Number(pr.number);
|
|
215
|
+
const cached = previousByNumber.get(number);
|
|
216
|
+
const listedAdditions = Number(pr.additions);
|
|
217
|
+
const listedDeletions = Number(pr.deletions);
|
|
218
|
+
const listedStatsAvailable = Number.isFinite(listedAdditions) &&
|
|
219
|
+
Number.isFinite(listedDeletions) &&
|
|
220
|
+
pr.additions !== undefined &&
|
|
221
|
+
pr.deletions !== undefined;
|
|
222
|
+
const cachedStatsAvailable = cached &&
|
|
223
|
+
cached.updatedAt === String(pr.updated_at ?? "") &&
|
|
224
|
+
cached.headSha === String(pr.head?.sha ?? "") &&
|
|
225
|
+
Number.isFinite(cached.additions) &&
|
|
226
|
+
Number.isFinite(cached.deletions);
|
|
227
|
+
let detail = listedStatsAvailable
|
|
228
|
+
? pr
|
|
229
|
+
: cachedStatsAvailable
|
|
230
|
+
? {
|
|
231
|
+
additions: cached.additions,
|
|
232
|
+
deletions: cached.deletions,
|
|
233
|
+
}
|
|
234
|
+
: undefined;
|
|
235
|
+
if (!detail) {
|
|
236
|
+
try {
|
|
237
|
+
detail = await client.request(`repos/${options.repo}/pulls/${number}`);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
log("fetch", `PR #${number} detail unavailable; diff skipped: ${String(error)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const current = {
|
|
244
|
+
number,
|
|
245
|
+
title: String(pr.title ?? ""),
|
|
246
|
+
body: String(pr.body ?? ""),
|
|
247
|
+
state: String(pr.state ?? ""),
|
|
248
|
+
merged: Boolean(pr.merged_at),
|
|
249
|
+
updatedAt: String(pr.updated_at ?? ""),
|
|
250
|
+
headSha: String(pr.head?.sha ?? ""),
|
|
251
|
+
labels: Array.isArray(pr.labels)
|
|
252
|
+
? pr.labels.map((label) => String(label.name))
|
|
253
|
+
: [],
|
|
254
|
+
additions: Number(detail?.additions ?? cached?.additions ?? 0),
|
|
255
|
+
deletions: Number(detail?.deletions ?? cached?.deletions ?? 0),
|
|
256
|
+
comments: cached?.comments ?? [],
|
|
257
|
+
reviews: cached?.reviews ?? [],
|
|
258
|
+
changedFiles: cached?.changedFiles ?? [],
|
|
259
|
+
diff: cached?.diff ?? "",
|
|
260
|
+
};
|
|
261
|
+
const discussionUnchanged = cached?.updatedAt === current.updatedAt;
|
|
262
|
+
const diffUnchanged = cached?.headSha === current.headSha && Boolean(cached?.diff);
|
|
263
|
+
const discussionStatus = discussionUnchanged
|
|
264
|
+
? "comments/reviews cache"
|
|
265
|
+
: "download comments/reviews";
|
|
266
|
+
let diffStatus = "diff disabled";
|
|
267
|
+
if (options.includePullRequestChanges) {
|
|
268
|
+
const lines = current.additions + current.deletions;
|
|
269
|
+
diffStatus = diffUnchanged
|
|
270
|
+
? "diff cache"
|
|
271
|
+
: !detail
|
|
272
|
+
? "diff unavailable"
|
|
273
|
+
: options.maxPullRequestChangeLines !== undefined &&
|
|
274
|
+
lines > options.maxPullRequestChangeLines
|
|
275
|
+
? `diff skipped (${lines} lines > limit)`
|
|
276
|
+
: "download diff";
|
|
277
|
+
}
|
|
278
|
+
if (phase) {
|
|
279
|
+
phase.text = `PR #${number} · ${discussionStatus} · ${diffStatus} · ${completed}/${selected.length} · ${percent(completed, selected.length)}`;
|
|
280
|
+
}
|
|
281
|
+
if (!discussionUnchanged) {
|
|
282
|
+
const comments = await client.pages(`repos/${options.repo}/issues/${number}/comments`);
|
|
283
|
+
const reviews = await client.pages(`repos/${options.repo}/pulls/${number}/reviews`);
|
|
284
|
+
current.comments = comments
|
|
285
|
+
.map((comment) => String(comment.body ?? ""))
|
|
286
|
+
.filter(Boolean)
|
|
287
|
+
.slice(0, options.maxComments);
|
|
288
|
+
current.reviews = reviews
|
|
289
|
+
.map((review) => String(review.body ?? ""))
|
|
290
|
+
.filter(Boolean);
|
|
291
|
+
}
|
|
292
|
+
if (options.includePullRequestChanges && !diffUnchanged && detail) {
|
|
293
|
+
const lines = current.additions + current.deletions;
|
|
294
|
+
if (options.maxPullRequestChangeLines === undefined ||
|
|
295
|
+
lines <= options.maxPullRequestChangeLines) {
|
|
296
|
+
const files = await client.pages(`repos/${options.repo}/pulls/${number}/files`);
|
|
297
|
+
current.changedFiles = files
|
|
298
|
+
.map((item) => String(item.filename))
|
|
299
|
+
.filter(Boolean);
|
|
300
|
+
current.diff = files
|
|
301
|
+
.map((item) => {
|
|
302
|
+
const path = String(item.filename ?? "");
|
|
303
|
+
const patch = String(item.patch ?? "");
|
|
304
|
+
return patch ? `FILE: ${path}\n${patch}` : "";
|
|
305
|
+
})
|
|
306
|
+
.filter(Boolean)
|
|
307
|
+
.join("\n\n");
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
current.changedFiles = [];
|
|
311
|
+
current.diff = "";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
result[index] = current;
|
|
315
|
+
completed++;
|
|
316
|
+
if (phase) {
|
|
317
|
+
phase.text = `pull requests ${completed}/${selected.length} · ${percent(completed, selected.length)}`;
|
|
318
|
+
}
|
|
319
|
+
log("fetch", `PR ${completed}/${selected.length} · ${percent(completed, selected.length)} #${number} · ${discussionStatus} · ${diffStatus}`);
|
|
320
|
+
if (progress) {
|
|
321
|
+
save = save.then(() => progress(result.filter((item) => !!item)));
|
|
322
|
+
await save;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
async function commits(client, options, meta, phase) {
|
|
328
|
+
const branch = String(meta.default_branch ?? "main");
|
|
329
|
+
const limit = options.maxCommits && options.maxCommits > 0
|
|
330
|
+
? options.maxCommits
|
|
331
|
+
: undefined;
|
|
332
|
+
if (phase)
|
|
333
|
+
phase.text = "commit history · page 0 · fetched 0";
|
|
334
|
+
return client.pages(`repos/${options.repo}/commits?sha=${encodeURIComponent(branch)}`, limit, (page, fetched) => {
|
|
335
|
+
const message = `commit history · page ${page} · fetched ${fetched} · total unknown`;
|
|
336
|
+
if (phase)
|
|
337
|
+
phase.text = message;
|
|
338
|
+
log("fetch", message);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
export async function collectSource(client, options, previous, progress, codebaseProgress) {
|
|
342
|
+
const phase = { text: "reading repository metadata" };
|
|
343
|
+
const stopHeartbeat = startHeartbeat(() => phase.text);
|
|
344
|
+
try {
|
|
345
|
+
const repo = await client.request(`repos/${options.repo}`);
|
|
346
|
+
const includeCodebase = options.includeCodebase || options.includeHowRepoWorks;
|
|
347
|
+
const files = includeCodebase
|
|
348
|
+
? await codebase(client, options.repo, repo, options.ghConcurrent, previous?.source, phase, codebaseProgress)
|
|
349
|
+
: { tree: [], treeSha: {}, files: {} };
|
|
350
|
+
const pullRequestData = options.includePullRequests
|
|
351
|
+
? await pullRequests(client, options, previous, phase, progress)
|
|
352
|
+
: [];
|
|
353
|
+
const commitData = options.includeCommitHistory
|
|
354
|
+
? await commits(client, options, repo, phase)
|
|
355
|
+
: [];
|
|
356
|
+
return {
|
|
357
|
+
repo,
|
|
358
|
+
tree: files.tree,
|
|
359
|
+
treeSha: files.treeSha,
|
|
360
|
+
files: files.files,
|
|
361
|
+
pullRequests: pullRequestData,
|
|
362
|
+
commits: commitData,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
stopHeartbeat();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { GitHubClient } from "../types.ts";
|
|
2
|
+
export declare class GhClient implements GitHubClient {
|
|
3
|
+
request<T>(endpoint: string): Promise<T>;
|
|
4
|
+
write<T>(endpoint: string, body: unknown): Promise<T>;
|
|
5
|
+
createCheckRun<T>(endpoint: string, body: unknown): Promise<T>;
|
|
6
|
+
updateCheckRun<T>(endpoint: string, body: unknown): Promise<T>;
|
|
7
|
+
pages<T>(endpoint: string, limit?: number, progress?: (page: number, fetched: number) => void): Promise<T[]>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { paginate } from "./client.js";
|
|
2
|
+
import { commandOutput, commandWithInput } from "../util/runtime.js";
|
|
3
|
+
export class GhClient {
|
|
4
|
+
async request(endpoint) {
|
|
5
|
+
const result = await commandOutput("gh", {
|
|
6
|
+
args: ["api", endpoint],
|
|
7
|
+
stdout: "piped",
|
|
8
|
+
stderr: "piped",
|
|
9
|
+
});
|
|
10
|
+
if (!result.success) {
|
|
11
|
+
const error = new TextDecoder().decode(result.stderr).trim();
|
|
12
|
+
throw new Error(`gh api failed: ${error || endpoint}`);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(new TextDecoder().decode(result.stdout));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error(`gh returned invalid JSON for ${endpoint}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async write(endpoint, body) {
|
|
22
|
+
const result = await commandWithInput("gh", {
|
|
23
|
+
args: ["api", "-X", "POST", endpoint, "--input", "-"],
|
|
24
|
+
stdin: "piped",
|
|
25
|
+
stdout: "piped",
|
|
26
|
+
stderr: "piped",
|
|
27
|
+
}, JSON.stringify(body));
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
const error = new TextDecoder().decode(result.stderr).trim();
|
|
30
|
+
throw new Error(`gh api failed: ${error || endpoint}`);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(new TextDecoder().decode(result.stdout));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`gh returned invalid JSON for ${endpoint}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async createCheckRun(endpoint, body) {
|
|
40
|
+
return await this.write(endpoint, body);
|
|
41
|
+
}
|
|
42
|
+
async updateCheckRun(endpoint, body) {
|
|
43
|
+
const result = await commandWithInput("gh", {
|
|
44
|
+
args: ["api", "-X", "PATCH", endpoint, "--input", "-"],
|
|
45
|
+
stdin: "piped",
|
|
46
|
+
stdout: "piped",
|
|
47
|
+
stderr: "piped",
|
|
48
|
+
}, JSON.stringify(body));
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
const error = new TextDecoder().decode(result.stderr).trim();
|
|
51
|
+
throw new Error(`gh api failed: ${error || endpoint}`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(new TextDecoder().decode(result.stdout));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error(`gh returned invalid JSON for ${endpoint}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
pages(endpoint, limit, progress) {
|
|
61
|
+
return paginate((e) => this.request(e), endpoint, limit, progress);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** GitHub hands out App private keys as PKCS#1 PEM ("BEGIN RSA PRIVATE
|
|
2
|
+
* KEY"). Web Crypto's `importKey("pkcs8", ...)` only takes PKCS#8. The two
|
|
3
|
+
* differ by a fixed 26-byte wrapper around the same PKCS#1 bytes: a version
|
|
4
|
+
* INTEGER, the rsaEncryption AlgorithmIdentifier, and an OCTET STRING tag
|
|
5
|
+
* around the PKCS#1 payload — this file builds that wrapper by hand rather
|
|
6
|
+
* than pulling in an ASN.1 library for one fixed structure. */
|
|
7
|
+
export declare function pkcs1ToPkcs8(pkcs1: Uint8Array): Uint8Array;
|
|
8
|
+
export declare function importAppPrivateKey(pem: string): Promise<CryptoKey>;
|
|
9
|
+
/** GitHub requires `exp` no more than 10 minutes out and tolerates `iat` a
|
|
10
|
+
* little in the past to absorb clock drift between this machine and
|
|
11
|
+
* GitHub's. */
|
|
12
|
+
export declare function signAppJwt(appId: string, key: CryptoKey): Promise<string>;
|