@preventive/triage 1.0.0-alpha.47 → 1.0.0-alpha.49
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/client/dependency-paths.d.ts +2 -1
- package/client/dependency-paths.js +16 -4
- package/common/managed/agent-tasks.ts +12 -14
- package/common/managed/github-agents.ts +7 -6
- package/out/activity.js +4 -4
- package/out/{chunk-NOZJULWS.js → chunk-6LNQTKAV.js} +1 -1
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/{chunk-5AHIQFFG.js → chunk-C2AVODGZ.js} +1 -1
- package/out/{chunk-I2MDGSQX.js → chunk-IO7JTZ6O.js} +1 -1
- package/out/chunk-JEAUAJVF.js +232 -0
- package/out/chunk-O6M574JI.js +1 -0
- package/out/{chunk-VL43BTSO.js → chunk-PBSOL2SL.js} +1 -1
- package/out/{chunk-ZCS4UYHI.js → chunk-PBZPYIG7.js} +1 -1
- package/out/{chunk-6WF3KNTL.js → chunk-SZD5CW5R.js} +2 -2
- package/out/{chunk-SPEFPML6.js → chunk-TQJDYHVE.js} +1 -1
- package/out/{chunk-UENXT2HK.js → chunk-ULIF7A7N.js} +1 -1
- package/out/{chunk-FH24VHSJ.js → chunk-WYLQWYLA.js} +1 -1
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +26 -26
- package/out/compare.js +13 -13
- package/out/dialogs.js +17 -17
- package/out/graph.js +1 -1
- package/out/index.html +7 -7
- package/out/scan.js +1 -1
- package/out/terminal.js +214 -214
- package/out/view.css +1 -1
- package/out/view.js +15 -15
- package/package.json +8 -5
- package/risk/npm/score.js +1 -1
- package/server-managed/agent-merges.ts +38 -22
- package/server-managed/app-agents.ts +6 -8
- package/server-managed/cache-storage.ts +3 -2
- package/server-managed/commit-coauthors.ts +7 -2
- package/server-managed/commit-lockfiles.ts +22 -21
- package/server-managed/db-methods.ts +52 -38
- package/server-managed/db-neon.ts +21 -5
- package/server-managed/db.ts +2 -0
- package/server-managed/dependabot-access.ts +1 -1
- package/server-managed/dependency-review-comments.ts +7 -4
- package/server-managed/dependency-reviews.ts +71 -33
- package/server-managed/github-accounts.ts +28 -1
- package/server-managed/github-app.ts +27 -23
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +8 -1
- package/server-managed/github-metadata.ts +2 -2
- package/server-managed/http-response.ts +28 -10
- package/server-managed/http.ts +751 -273
- package/server-managed/index.ts +2 -2
- package/server-managed/managed-issues.ts +3 -0
- package/server-managed/npm-delta.ts +16 -0
- package/server-managed/npm-kept.ts +32 -5
- package/server-managed/npm-loads.ts +21 -13
- package/server-managed/npm-manifest.ts +1 -1
- package/server-managed/npm-packages.ts +13 -4
- package/server-managed/object-storage.ts +4 -3
- package/server-managed/public-workspace.ts +3 -3
- package/server-managed/pull-comment-reviews.ts +1 -1
- package/server-managed/pull-dependencies.ts +84 -36
- package/server-managed/pull-dependency-reviews.ts +28 -18
- package/server-managed/rename-diff-worker.js +1 -1
- package/server-managed/repository-agents.ts +9 -6
- package/server-managed/repository-browser.ts +13 -3
- package/server-managed/repository-code.ts +153 -7
- package/server-managed/repository-dependency-reviews.ts +12 -4
- package/server-managed/repository-history.ts +85 -152
- package/server-managed/repository-issues.ts +27 -13
- package/server-managed/repository-pulse.ts +31 -11
- package/server-managed/repository-squashes.ts +145 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +3 -2
- package/server-managed/session.ts +34 -12
- package/server-managed/sql.ts +16 -8
- package/server-managed/storage-encryption.ts +16 -8
- package/server-managed/storage-maintenance.ts +5 -2
- package/server-managed/storage-payload.ts +18 -0
- package/server-managed/storage-stores.ts +3 -2
- package/server-managed/team-feed.ts +16 -11
- package/server-managed/team-npm-scopes.ts +29 -0
- package/server-managed/team-reports.ts +68 -16
- package/out/chunk-6ANSHRFQ.js +0 -6
- package/out/chunk-S6U2J62U.js +0 -232
- package/out/chunk-ZGNMDGSB.js +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Compare's diff with renamed names hidden (rename-diff.ts), worked out off the
|
|
2
2
|
// server's event loop by @preventive/compare's renamedDiffs: the old side given
|
|
3
|
-
// the new side's names, formatted again where
|
|
3
|
+
// the new side's names, not formatted again (each line where the old side's was),
|
|
4
4
|
// diffed with whitespace changes shown and hidden, and the blocks checked to
|
|
5
5
|
// take it to the new side. `{ before, blocks, reason, reads, marks,
|
|
6
6
|
// ignoringWhitespace }`: the old side as diffed, the change blocks, why the
|
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
// an agent, and those alone are answered, never the texts. GraphQL alone
|
|
12
12
|
// gives them, so it needs a token.
|
|
13
13
|
import { type AgentAuthor, type AgentBranch, type AgentCommit, type AgentIssue, type AgentPull, type AppAgent, agentSignals } from '../common/managed/agent-tasks.ts'
|
|
14
|
+
import { GITHUB_AGENTS, type GithubAgentAccount, type GithubAgentId } from '../common/managed/github-agents.ts'
|
|
14
15
|
import { AGENT_BRANCH_PREFIXES, APP_AGENTS } from './app-agents.ts'
|
|
15
16
|
import { GithubApiError } from './github-app.ts'
|
|
16
|
-
import { ACTOR, type NamedAccount, actorOf, commitAccount, githubGraphql } from './repository-history.ts'
|
|
17
|
+
import { ACTOR, type NamedAccount, actorOf, commitAccount, githubGraphql, linkNoreplyUsers } from './repository-history.ts'
|
|
17
18
|
import { ISSUES_CURSOR } from './repository-issues.ts'
|
|
18
19
|
|
|
19
|
-
export { APP_AGENTS }
|
|
20
|
-
|
|
21
20
|
export const AGENT_PULLS_PAGE = 50
|
|
22
21
|
export const AGENT_COMMITS_PAGE = 100
|
|
23
22
|
const AGENT_ISSUES = 100
|
|
@@ -35,7 +34,8 @@ const SHA = /^[a-f\d]{40}$/u
|
|
|
35
34
|
const CLAUDE_EMAIL = 'noreply@anthropic.com'
|
|
36
35
|
|
|
37
36
|
// The app agents' accounts' no-reply addresses, by each login of theirs.
|
|
38
|
-
const AGENT_EMAILS = APP_AGENTS.flatMap(({
|
|
37
|
+
const AGENT_EMAILS = APP_AGENTS.flatMap(({ id }) => GITHUB_AGENTS[id as GithubAgentId].accounts
|
|
38
|
+
.flatMap(({ id: account, logins }: GithubAgentAccount) => account === undefined ? [] : logins.map(login => `${account}+${login}@users.noreply.github.com`)))
|
|
39
39
|
|
|
40
40
|
// An agent's branches, a page at a time, as GitHub lists them, by name;
|
|
41
41
|
// each its tip commit's time, message and author.
|
|
@@ -164,6 +164,7 @@ async function agentBranches(owner: string, name: string, prefix: string, token:
|
|
|
164
164
|
for (let page = 0; page < MAX_BRANCH_PAGES; page++) {
|
|
165
165
|
const refs: Connection<RefNode> | undefined = (await githubGraphql(BRANCHES_QUERY, { owner, name, prefix: `refs/heads/${prefix}`, after }, token, fetchImpl) as RefsPage)
|
|
166
166
|
?.repository?.refs
|
|
167
|
+
await linkNoreplyUsers((refs?.nodes ?? []).map(node => node?.target?.author), token, fetchImpl)
|
|
167
168
|
found.push(...(refs?.nodes ?? []).flatMap(node => branch(prefix)(node) ?? []))
|
|
168
169
|
const next: Connection<RefNode>['pageInfo'] = refs?.pageInfo
|
|
169
170
|
if (next?.hasNextPage !== true || typeof next.endCursor !== 'string') break
|
|
@@ -175,7 +176,7 @@ async function agentBranches(owner: string, name: string, prefix: string, token:
|
|
|
175
176
|
// What its answers are kept by, with their cursors (http.ts
|
|
176
177
|
// handleRepositoryAgents): bumped as what a page reads, or how it tells an
|
|
177
178
|
// agent, changes, so that no answer an older server kept is sent for a day.
|
|
178
|
-
export const AGENTS_READ_VERSION =
|
|
179
|
+
export const AGENTS_READ_VERSION = 7
|
|
179
180
|
|
|
180
181
|
// A cursor of the default branch's commits: GitHub names one by a commit
|
|
181
182
|
// and how many on from it, `<oid> <offset>`, a space between.
|
|
@@ -206,7 +207,9 @@ export async function repositoryAgents(fullName: string, token: string, cursors:
|
|
|
206
207
|
if (!repository) throw new GithubApiError(404, 'github-not-found')
|
|
207
208
|
const history = repository.defaultBranchRef?.target?.history
|
|
208
209
|
const after = { pulls: nextCursor(repository.pullRequests), commits: nextCursor(history, COMMITS_CURSOR) }
|
|
209
|
-
const
|
|
210
|
+
const nodes = [...history?.nodes ?? [], ...authored?.repository?.defaultBranchRef?.target?.history?.nodes ?? []]
|
|
211
|
+
await linkNoreplyUsers(nodes.map(node => node?.author), token, fetchImpl)
|
|
212
|
+
const commits = nodes.flatMap(node => directCommit(node) ?? [])
|
|
210
213
|
return {
|
|
211
214
|
pulls: (repository.pullRequests?.nodes ?? []).flatMap(node => pull(node) ?? []),
|
|
212
215
|
commits: commits.filter((commit, index) => commits.findIndex(other => other.oid === commit.oid) === index),
|
|
@@ -171,9 +171,16 @@ async function askAccess(repo: SelectedRepo, start: string | null, userToken: st
|
|
|
171
171
|
return githubJson(base, token, fetchImpl)
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
-
// A repository last known private asks who the user is
|
|
175
|
-
//
|
|
176
|
-
|
|
174
|
+
// A repository last known private asks who the user is, and mints the
|
|
175
|
+
// installation token its permission is asked with, as it asks for its
|
|
176
|
+
// metadata, not after. Each is asked once a request (the token once for
|
|
177
|
+
// concurrent requests too, github-app.ts), so a failure is answered where
|
|
178
|
+
// it is awaited. One made public since mints a token it never waits for,
|
|
179
|
+
// kept for later requests.
|
|
180
|
+
if (repo.private && userToken) {
|
|
181
|
+
getIdentity().catch(() => {})
|
|
182
|
+
if (repo.installationId != null) installationToken().catch(() => {})
|
|
183
|
+
}
|
|
177
184
|
// Stored visibility can be stale (or internal with private=false). Require
|
|
178
185
|
// current, explicit public visibility or the caller's effective permission.
|
|
179
186
|
const data = await metadata() as RepoMetadata
|
|
@@ -232,6 +239,9 @@ async function repositoryReader(repo: SelectedRepo, userToken: string | null, in
|
|
|
232
239
|
// The user's permission level on it where the gate asked GitHub of it, as
|
|
233
240
|
// for a private repository; null for a public one.
|
|
234
241
|
permission: () => level,
|
|
242
|
+
// Its default branch as GitHub named it when access was first checked,
|
|
243
|
+
// `''` where it named none: the one whose head `commit('')` reads.
|
|
244
|
+
defaultBranch,
|
|
235
245
|
async refs() {
|
|
236
246
|
// Suggestions are bounded; the input also accepts any branch or tag name.
|
|
237
247
|
const list = async (kind: string) => {
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
// bundle builds keep theirs (getRepoTreeTarball), and its files as the npm
|
|
5
5
|
// viewer reads a package's (npm-packages.ts npmFileRows). Readers asking for
|
|
6
6
|
// the same directory meanwhile share its body, a load among the npm viewer's
|
|
7
|
-
// active ones (npm-loads.ts sharedLoad).
|
|
7
|
+
// active ones (npm-loads.ts sharedLoad). The default branch's head's bodies
|
|
8
|
+
// are kept in byte storage, for the next to open them (repositoryCodeKept).
|
|
8
9
|
import { Buffer } from 'node:buffer'
|
|
10
|
+
import { createHash } from 'node:crypto'
|
|
9
11
|
import { encodeBrotli } from './brotli.ts'
|
|
12
|
+
import { CacheMissError, type CacheStorage } from './cache-storage.ts'
|
|
13
|
+
import type { KeptStore } from './npm-kept.ts'
|
|
14
|
+
import { plainObject } from './npm-manifest.ts'
|
|
10
15
|
import { MAX_NPM_JSON_LENGTH, NpmPackageError, npmFileRows } from './npm-packages.ts'
|
|
11
16
|
import { type NpmLoad, loadPlaces, readTarballFiles, sharedLoad } from './npm-loads.ts'
|
|
12
17
|
|
|
@@ -49,9 +54,14 @@ const directoryError = (err: unknown) => {
|
|
|
49
54
|
|
|
50
55
|
// The directory as the viewer reads it, as brotli-encoded JSON: `{ repoId,
|
|
51
56
|
// repo, commit, path, tree, files }`, its files as npmFileRows gives them,
|
|
52
|
-
// by their paths in the directory, from `tarball`, the tree's
|
|
53
|
-
|
|
57
|
+
// by their paths in the directory, from `tarball`, the tree's, as upstream
|
|
58
|
+
// verifies it with its symlinks let climb `symlinksAbove` levels.
|
|
59
|
+
export function loadRepositoryCodeBody(input: RepositoryCodeInput, tarball: (symlinksAbove: number) => Promise<Uint8Array>): NpmLoad<Buffer> {
|
|
54
60
|
const { repoId, repo, commit, path, tree, listed } = input
|
|
61
|
+
// A symlink may reach the repository's root, as a monorepo package's
|
|
62
|
+
// LICENSE links the root's, and no further: the directory's depth, less
|
|
63
|
+
// the level the archive's root already stands above its top directory.
|
|
64
|
+
const symlinksAbove = path.split('/').length - 1
|
|
55
65
|
// Its places among the active loads, by its files as listed, hold their
|
|
56
66
|
// JSON too, which the listing cannot tell, as escaping may grow a text
|
|
57
67
|
// sixfold: as much as a package's may take for each.
|
|
@@ -59,11 +69,147 @@ export function loadRepositoryCodeBody(input: RepositoryCodeInput, tarball: () =
|
|
|
59
69
|
const maxLength = Math.min(MAX_REPOSITORY_CODE_JSON_LENGTH, places * MAX_NPM_JSON_LENGTH)
|
|
60
70
|
return sharedLoad(`repository ${repoId} ${repo} ${commit} ${path} ${tree}`, async () => {
|
|
61
71
|
let files
|
|
62
|
-
|
|
63
|
-
// LICENSE links the root's, and no further: the directory's depth, less
|
|
64
|
-
// the level the archive's root already stands above its top directory.
|
|
65
|
-
try { files = npmFileRows(await readTarballFiles(await tarball(), { ...REPOSITORY_TAR, symlinksAbove: path.split('/').length - 1 }), maxLength) }
|
|
72
|
+
try { files = npmFileRows(await readTarballFiles(await tarball(symlinksAbove), { ...REPOSITORY_TAR, symlinksAbove }), maxLength) }
|
|
66
73
|
catch (err) { throw directoryError(err) }
|
|
67
74
|
return encodeBrotli(Buffer.from(JSON.stringify({ repoId, repo, commit, path, tree, files })))
|
|
68
75
|
}, 'repository-busy', { places })
|
|
69
76
|
}
|
|
77
|
+
|
|
78
|
+
// The form of the bodies kept, bumped as what a body holds changes
|
|
79
|
+
// (npmFileRows): one kept in another is none to read.
|
|
80
|
+
const KEPT_CODE_VERSION = 1
|
|
81
|
+
// The largest body kept. Source text runs, as brotli's JSON, to a third of
|
|
82
|
+
// its bytes or less, and a directory's binary files are named, not held, so
|
|
83
|
+
// this keeps one of some 200 MiB of text: nearly every directory that opens
|
|
84
|
+
// at all (MAX_REPOSITORY_CODE_BYTES). One past it would take as much byte
|
|
85
|
+
// storage as hundreds of ordinary directories, and hold a serverless
|
|
86
|
+
// invocation a while uploading it: it is sent as ever, and read again the
|
|
87
|
+
// next time.
|
|
88
|
+
export const MAX_KEPT_CODE_BYTES = 64 * 1024 * 1024
|
|
89
|
+
const COMMIT = /^[a-f\d]{40}$/u
|
|
90
|
+
|
|
91
|
+
// A directory as kept: the repository by its id and name, the commit and the
|
|
92
|
+
// directory, as its body names them; and the head they are kept at.
|
|
93
|
+
export type KeptCodeAt = Pick<RepositoryCodeInput, 'repoId' | 'repo' | 'commit' | 'path'>
|
|
94
|
+
type KeptCodeHead = Pick<KeptCodeAt, 'commit' | 'repo'>
|
|
95
|
+
// Under the repository, then the commit, so either goes by its prefix; the
|
|
96
|
+
// repository's name and the directory hashed, as a key's parts are names.
|
|
97
|
+
const keptKey = ({ repoId, repo, commit, path }: KeptCodeAt) =>
|
|
98
|
+
`${repoId}/${commit}/v${KEPT_CODE_VERSION}-${createHash('sha256').update(JSON.stringify([repo, path])).digest('hex')}.json.br`
|
|
99
|
+
|
|
100
|
+
// Bodies being kept, by their storage and key, each kept once; and each
|
|
101
|
+
// storage's own name among the active loads, as its reads are one of them.
|
|
102
|
+
const keeping = new WeakMap<CacheStorage, Set<string>>()
|
|
103
|
+
const storageNames = new WeakMap<CacheStorage, number>()
|
|
104
|
+
let storagesNamed = 0
|
|
105
|
+
|
|
106
|
+
// A body kept, read whole, where it is one MAX_KEPT_CODE_BYTES keeps.
|
|
107
|
+
async function readKept(storage: CacheStorage, key: string): Promise<Buffer | null> {
|
|
108
|
+
const { size, stream } = await storage.open(key)
|
|
109
|
+
try {
|
|
110
|
+
if (size !== null && size > MAX_KEPT_CODE_BYTES) return null
|
|
111
|
+
const parts: Buffer[] = []
|
|
112
|
+
let length = 0
|
|
113
|
+
for await (const part of stream) {
|
|
114
|
+
length += (part as Buffer).byteLength
|
|
115
|
+
if (length > MAX_KEPT_CODE_BYTES) return null
|
|
116
|
+
parts.push(Buffer.from(part as Buffer))
|
|
117
|
+
}
|
|
118
|
+
return Buffer.concat(parts, length)
|
|
119
|
+
} finally { stream.destroy() }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The directories of repositories' default branches' heads, as
|
|
123
|
+
// loadRepositoryCodeBody encodes them, kept in the managed byte storage
|
|
124
|
+
// (`storage`: Vercel Blob, else the filesystem, encrypted at rest where
|
|
125
|
+
// storage is: storage-payload.ts REPOSITORY_CODE_CACHE), so that one opened
|
|
126
|
+
// again, on any instance, asks GitHub for no tarball, and is neither unpacked
|
|
127
|
+
// nor encoded again; by the commit `heads` keeps for each repository, the
|
|
128
|
+
// head as last read (npm-kept.ts codeHeadStore). Most opens read the head:
|
|
129
|
+
// `/repo/<repo id>` opens it, and its route then names its commit, as do its
|
|
130
|
+
// directories opened from it and a reload. A read naming the default branch
|
|
131
|
+
// (`''`, or `heads/` and its name) takes its head as GitHub resolves it for
|
|
132
|
+
// the commit kept, the directories kept at any other removed (`own`ed,
|
|
133
|
+
// behind its answer); any other read, of a commit, a tag or another branch,
|
|
134
|
+
// reads and keeps a directory only where what it resolves to is the commit
|
|
135
|
+
// kept. Nothing kept is per reader, and each is read only once the reader's
|
|
136
|
+
// own access has named its commit. A body names what it was read of, so
|
|
137
|
+
// instances disagreeing on the head meanwhile, as at a push, only cost a
|
|
138
|
+
// directory read again: one kept for a head since moved on removes itself.
|
|
139
|
+
// The head is kept with the repository's name, which each body's key holds,
|
|
140
|
+
// so a repository renamed has what was kept under its name before removed
|
|
141
|
+
// as a read next names its head. A head is written only for a repository
|
|
142
|
+
// still `active` after it, as one removed meanwhile has its head removed
|
|
143
|
+
// after (remove). Byte storage, as the database, is an optimization: its
|
|
144
|
+
// failures read as a directory kept nowhere, and keep nothing.
|
|
145
|
+
export function repositoryCodeKept(storage: CacheStorage, heads: KeptStore<KeptCodeHead>, active: (repoId: number) => Promise<boolean>,
|
|
146
|
+
own: (work: Promise<unknown>) => void, debug = false) {
|
|
147
|
+
const warn = (what: string) => (error: unknown) => { if (debug) console.warn(`managed: kept repository code ${what} failed:`, error) }
|
|
148
|
+
const headOf = async (repoId: number): Promise<KeptCodeHead | null> => {
|
|
149
|
+
const kept: unknown = (await heads.read(String(repoId)))?.value
|
|
150
|
+
return plainObject(kept) && typeof kept['commit'] === 'string' && typeof kept['repo'] === 'string' ? { commit: kept['commit'], repo: kept['repo'] } : null
|
|
151
|
+
}
|
|
152
|
+
// What is kept of repository `repoId`: its head first, so that a body being
|
|
153
|
+
// kept meanwhile removes itself.
|
|
154
|
+
const remove = async (repoId: number) => {
|
|
155
|
+
await heads.remove(String(repoId))
|
|
156
|
+
await storage.delete(String(repoId))
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
// Whether a read of `ref` at repository `repoId`, by its name `repo`,
|
|
160
|
+
// resolved to `commit` with `defaultBranch` GitHub's, reads and keeps
|
|
161
|
+
// its directories.
|
|
162
|
+
async at({ repoId, repo, commit }: Omit<KeptCodeAt, 'path'>, ref: string, defaultBranch: string): Promise<boolean> {
|
|
163
|
+
if (!COMMIT.test(commit)) return false
|
|
164
|
+
const kept = await headOf(repoId)
|
|
165
|
+
const atKept = kept?.commit === commit && kept.repo === repo
|
|
166
|
+
if (ref !== '' && (defaultBranch === '' || ref !== `heads/${defaultBranch}`)) return atKept
|
|
167
|
+
if (!atKept) {
|
|
168
|
+
await heads.write(String(repoId), { commit, repo }, Date.now())
|
|
169
|
+
// One removed as its commit was read, its head removed before this
|
|
170
|
+
// wrote it again, keeps nothing, and has what is kept removed again.
|
|
171
|
+
if (!await active(repoId).catch(() => false)) {
|
|
172
|
+
await remove(repoId).catch(warn('removal'))
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
// Those kept at the head before, or, renamed, every one kept under
|
|
176
|
+
// its name before.
|
|
177
|
+
own(storage.delete(String(repoId), kept?.repo === repo ? commit : undefined).catch(warn('removal')))
|
|
178
|
+
}
|
|
179
|
+
return true
|
|
180
|
+
},
|
|
181
|
+
// The body kept of the directory `at` names, or null: a load among the
|
|
182
|
+
// active ones (npm-loads.ts sharedLoad), as one read from GitHub is, a
|
|
183
|
+
// place of them, as it holds no more than a package may
|
|
184
|
+
// (MAX_KEPT_CODE_BYTES), till each reader has released it; readers
|
|
185
|
+
// asking for it meanwhile share it, and none is read where every place
|
|
186
|
+
// is taken (`repository-busy`).
|
|
187
|
+
read(at: KeptCodeAt): NpmLoad<Buffer | null> {
|
|
188
|
+
const key = keptKey(at)
|
|
189
|
+
const name = storageNames.get(storage) ?? storageNames.set(storage, ++storagesNamed).get(storage)!
|
|
190
|
+
return sharedLoad(`repository kept ${name} ${key}`, () => readKept(storage, key).catch((error: unknown) => {
|
|
191
|
+
if (!(error instanceof CacheMissError)) warn('read')(error)
|
|
192
|
+
return null
|
|
193
|
+
}), 'repository-busy')
|
|
194
|
+
},
|
|
195
|
+
// The directory's body kept, behind its answer, where it is no larger
|
|
196
|
+
// than MAX_KEPT_CODE_BYTES; and removed again, with the rest of its
|
|
197
|
+
// commit's, where the head kept is another by then, as when it moved on
|
|
198
|
+
// or the repository was renamed or removed meanwhile.
|
|
199
|
+
keep(at: KeptCodeAt, body: Buffer): void {
|
|
200
|
+
const key = keptKey(at)
|
|
201
|
+
const kept = keeping.get(storage) ?? keeping.set(storage, new Set()).get(storage)!
|
|
202
|
+
if (body.byteLength > MAX_KEPT_CODE_BYTES || kept.has(key)) return
|
|
203
|
+
kept.add(key)
|
|
204
|
+
own((async () => {
|
|
205
|
+
await storage.put(key, body)
|
|
206
|
+
const head = await headOf(at.repoId)
|
|
207
|
+
if (head?.commit !== at.commit || head.repo !== at.repo) await storage.delete(`${at.repoId}/${at.commit}`)
|
|
208
|
+
})().catch(warn('write')).finally(() => kept.delete(key)))
|
|
209
|
+
},
|
|
210
|
+
// What is kept of repository `repoId`, removed once the repository is
|
|
211
|
+
// no longer active.
|
|
212
|
+
remove,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export type RepositoryCodeKept = ReturnType<typeof repositoryCodeKept>
|
|
@@ -25,13 +25,14 @@ export interface RepoDependencyReview { enabled: boolean; required: number; sinc
|
|
|
25
25
|
// A review published of the version it is of: its package, version and
|
|
26
26
|
// npm's integrity of it, the patch it is of (none here, the app reviewing no
|
|
27
27
|
// patch yet), the version it was compared with (none for a whole package's),
|
|
28
|
-
// who wrote it (`reviewer`, their id, else their login then),
|
|
29
|
-
//
|
|
28
|
+
// who wrote it (`reviewer`, their id, else their login then), their login
|
|
29
|
+
// as it was then (`login`), its verdict, whether it checked every file
|
|
30
|
+
// (`full`), and when it was published.
|
|
30
31
|
// One made in a pull request's comments (`comment`) counts for it alone;
|
|
31
32
|
// one published here has its `id`.
|
|
32
33
|
export interface PublishedReview {
|
|
33
34
|
id?: string; package: string; version: string; integrity: string; patchHash?: string; basePackage: string | null; baseVersion: string | null
|
|
34
|
-
reviewer: string; status: ReviewVerdict; full: boolean; time: number; comment?: true
|
|
35
|
+
reviewer: string; login: string; status: ReviewVerdict; full: boolean; time: number; comment?: true
|
|
35
36
|
}
|
|
36
37
|
// A repository's review in effect for a reader: its settings, whether they
|
|
37
38
|
// publish reviews to one of the teams that count (`canReview`), and the
|
|
@@ -45,6 +46,8 @@ export interface RepositoryDependencyReviewStore {
|
|
|
45
46
|
listRepoDependencyReviews(): Promise<Map<number, RepoDependencyReview>>
|
|
46
47
|
// One repository's, none where it isn't connected.
|
|
47
48
|
repoDependencyReview(repoId: number): Promise<RepoDependencyReview | null>
|
|
49
|
+
// Those of `repoIds` connected, by their ids, in one statement.
|
|
50
|
+
getRepoDependencyReviews(repoIds: number[]): Promise<Map<number, RepoDependencyReview>>
|
|
48
51
|
// The teams whose published reviews count for each repository (COUNTING_TEAM),
|
|
49
52
|
// by name.
|
|
50
53
|
listRepoReviewTeams(): Promise<Map<number, { id: string; name: string }[]>>
|
|
@@ -103,7 +106,7 @@ async function publishedReviews(db: ManagedSql, repoId: number, versions: Review
|
|
|
103
106
|
const nonText = Array.isArray(snapshot) ? null : snapshot?.nonTextFiles ?? []
|
|
104
107
|
const full = summarizeReview(entries.get(row.id) ?? [], files, nonText).coverage === 'full'
|
|
105
108
|
found.push({ id: row.id, package: row.package, version: row.version, integrity: row.integrity, basePackage: row.basePackage, baseVersion: row.baseVersion,
|
|
106
|
-
reviewer: row.userId ?? `login:${row.userLogin.toLowerCase()}`, status: row.status, full, time: Number(row.publishedAt) })
|
|
109
|
+
reviewer: row.userId ?? `login:${row.userLogin.toLowerCase()}`, login: row.userLogin, status: row.status, full, time: Number(row.publishedAt) })
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
return found
|
|
@@ -122,6 +125,11 @@ export function repositoryDependencyReviewMethods(db: ManagedSql): RepositoryDep
|
|
|
122
125
|
return new Map(rows.map(row => [row.repoId, setting(row)]))
|
|
123
126
|
},
|
|
124
127
|
repoDependencyReview: readSetting,
|
|
128
|
+
async getRepoDependencyReviews(repoIds) {
|
|
129
|
+
if (repoIds.length === 0) return new Map()
|
|
130
|
+
const rows = await db.prepare(`SELECT ${columns} FROM managed_selected_repo WHERE repo_id IN (${repoIds.map(() => '?').join(',')})`).all(...repoIds) as SettingRow[]
|
|
131
|
+
return new Map(rows.map(row => [row.repoId, setting(row)]))
|
|
132
|
+
},
|
|
125
133
|
async listRepoReviewTeams() {
|
|
126
134
|
const rows = await db.prepare(`SELECT DISTINCT tr.repo_id AS repoId, t.id AS id, t.name AS name FROM managed_team_repo tr
|
|
127
135
|
JOIN managed_team t ON t.id = tr.team_id WHERE t.hidden = 0 AND t.dependency_review = 1 ORDER BY t.name, t.id`).all() as { repoId: number; id: string; name: string }[]
|