@preventive/upstream 1.0.0-alpha.2 → 1.0.0-alpha.3
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/github.d.ts +19 -0
- package/package.json +1 -1
- package/src/github/read.js +36 -4
- package/src/tree.js +16 -2
package/github.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ export interface ClientOptions {
|
|
|
25
25
|
|
|
26
26
|
export type PullRequestStatus = 'open' | 'draft' | 'closed' | 'merged'
|
|
27
27
|
|
|
28
|
+
// An entry of a directory, as git has it, `path` its name: a file
|
|
29
|
+
// (`100644`, `100755` executable) or a symlink (`120000`) is a blob, a
|
|
30
|
+
// directory (`040000`) a tree, a submodule (`160000`) the commit it is at.
|
|
31
|
+
export interface RepoDirEntry {
|
|
32
|
+
path: string
|
|
33
|
+
mode: '100644' | '100755' | '120000' | '040000' | '160000'
|
|
34
|
+
type: 'blob' | 'tree' | 'commit'
|
|
35
|
+
sha: string
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
// A failed request: `status` is the HTTP status GitHub answered with.
|
|
29
39
|
export class HttpError extends Error {
|
|
30
40
|
name: 'HttpError'
|
|
@@ -60,6 +70,11 @@ export interface Client {
|
|
|
60
70
|
// on every call, cached or not. So its top directory is named for the
|
|
61
71
|
// tree, and files marked `export-subst` are as committed, not rewritten.
|
|
62
72
|
getRepoTarball(options: { repo: RepoName; sha: string }): Promise<Uint8Array>
|
|
73
|
+
// The id of the tree at `path`, a directory, in the full commit `sha`, or
|
|
74
|
+
// of its root without one: from the tree GitHub names for the commit,
|
|
75
|
+
// down GitHub's listings a directory at a time, each hashed back to its
|
|
76
|
+
// id. Refused where `path` is no directory.
|
|
77
|
+
getRepoTreeId(options: { repo: RepoName; sha: string; path?: string }): Promise<string>
|
|
63
78
|
// The gzipped tarball of a tree, the repo's own or any subdirectory's, by
|
|
64
79
|
// its id, whole, in memory: GitHub's, its files under one top directory.
|
|
65
80
|
// Its files are hashed back into git's tree and the id must be `tree`,
|
|
@@ -78,6 +93,10 @@ export interface Client {
|
|
|
78
93
|
// repository advisory objects. One page of 100: GitHub pages this list
|
|
79
94
|
// by cursor, so a repository with a full page is refused.
|
|
80
95
|
listRepoAdvisories(options: { repo: RepoName }): Promise<any[]>
|
|
96
|
+
// The entries of the directory at `path` in the full commit `sha`, or of
|
|
97
|
+
// its root, found as getRepoTreeId finds it: GitHub's listing, hashed
|
|
98
|
+
// back to the tree's id, so none is left out or changed.
|
|
99
|
+
listRepoDir(options: { repo: RepoName; sha: string; path?: string }): Promise<RepoDirEntry[]>
|
|
81
100
|
// Every page of `GET /user/repos`, as GitHub's repository objects; more
|
|
82
101
|
// than `maxPages` (100 by default) pages of 100 is an error.
|
|
83
102
|
listUserRepos(options?: { maxPages?: number }): Promise<any[]>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preventive/upstream",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.3",
|
|
4
4
|
"description": "Minimal npm, crates.io and Soldeer registry clients, a GitHub client, and a git checkout reader: the calls we use, nothing more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/github/read.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createHash } from 'node:crypto'
|
|
|
5
5
|
import { assertArgs, assertGhsa, assertLogin, assertNumber, assertPath, assertRef, assertRepo, assertSha, assertTreeId, isSha, isSha1, optional, sameName, show } from '../args.js'
|
|
6
6
|
import { verifiedDownload } from '../download.js'
|
|
7
7
|
import { decode, encodeSegment } from '../http.js'
|
|
8
|
+
import { gitTreeOfListing } from '../tree.js'
|
|
8
9
|
import { api, bindMethods, call, clientHeaders, isGone, repoApi } from './client.js'
|
|
9
10
|
|
|
10
11
|
const PER_PAGE = 100
|
|
@@ -83,15 +84,20 @@ async function treeTarball(method, headers, repo, tree) {
|
|
|
83
84
|
return await verifiedDownload({ method, dir: 'github/trees', what: tree, ext: 'tgz', algorithm: 'tree', expected: tree, locate, options: { headers, redirect: 'follow' }, list })
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
async function commitTree(method, headers, repo, sha) {
|
|
88
|
+
const commit = await call(headers, repoApi(repo, ['git', 'commits', sha]))
|
|
89
|
+
assert.ok(commit?.sha === sha && isSha1(commit.tree?.sha), `${method}: GitHub names no tree for ${repo}@${sha}`)
|
|
90
|
+
return commit.tree.sha
|
|
91
|
+
}
|
|
92
|
+
|
|
86
93
|
// A full sha only, so the bytes are that commit's, not wherever a ref points
|
|
87
94
|
// now. Its tree's tarball, not its own, in which `git archive` rewrites the
|
|
88
95
|
// files marked `export-subst`.
|
|
89
96
|
async function getRepoTarball(headers, options) {
|
|
90
97
|
assertArgs('getRepoTarball', options, { repo: assertRepo, sha: assertSha })
|
|
91
98
|
const { repo, sha } = options
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
return await treeTarball('getRepoTarball', headers, repo, commit.tree.sha)
|
|
99
|
+
const tree = await commitTree('getRepoTarball', headers, repo, sha)
|
|
100
|
+
return await treeTarball('getRepoTarball', headers, repo, tree)
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
async function getRepoTreeTarball(headers, options) {
|
|
@@ -99,6 +105,32 @@ async function getRepoTreeTarball(headers, options) {
|
|
|
99
105
|
return await treeTarball('getRepoTreeTarball', headers, options.repo, options.tree)
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
// Not recursive: GitHub cuts short a recursive listing of a large tree.
|
|
109
|
+
async function listTree(method, headers, repo, tree) {
|
|
110
|
+
const entries = (await call(headers, repoApi(repo, ['git', 'trees', tree])))?.tree
|
|
111
|
+
assert.ok(Array.isArray(entries) && gitTreeOfListing(entries) === tree, `${method}: GitHub's listing of tree ${tree} in ${repo} is not that tree`)
|
|
112
|
+
return entries.map(({ path, mode, type, sha }) => ({ path, mode, type, sha }))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function treeAt(method, headers, options) {
|
|
116
|
+
assertArgs(method, options, { repo: assertRepo, sha: assertSha, path: optional(assertPath) })
|
|
117
|
+
const { repo, sha, path } = options
|
|
118
|
+
let tree = await commitTree(method, headers, repo, sha)
|
|
119
|
+
for (const name of path?.split('/') ?? []) {
|
|
120
|
+
const entry = (await listTree(method, headers, repo, tree)).find((candidate) => candidate.path === name)
|
|
121
|
+
assert.ok(entry?.type === 'tree', `${method}: ${repo}@${sha} has no directory at ${show(path)}`)
|
|
122
|
+
tree = entry.sha
|
|
123
|
+
}
|
|
124
|
+
return tree
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const getRepoTreeId = (headers, options) => treeAt('getRepoTreeId', headers, options)
|
|
128
|
+
|
|
129
|
+
async function listRepoDir(headers, options) {
|
|
130
|
+
const tree = await treeAt('listRepoDir', headers, options)
|
|
131
|
+
return await listTree('listRepoDir', headers, options.repo, tree)
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
async function getPullRequest(headers, options) {
|
|
103
135
|
assertArgs('getPullRequest', options, { repo: assertRepo, number: assertNumber })
|
|
104
136
|
const { repo, number } = options
|
|
@@ -146,5 +178,5 @@ async function listRepoAdvisories(headers, options) {
|
|
|
146
178
|
return list
|
|
147
179
|
}
|
|
148
180
|
|
|
149
|
-
export const readMethods = { getCurrentUser, listUserRepos, getRepo, getRepoHead, getRepoFile, getRepoTarball, getRepoTreeTarball, getPullRequest, getCollaboratorPermission, getAdvisory, listRepoAdvisories }
|
|
181
|
+
export const readMethods = { getCurrentUser, listUserRepos, getRepo, getRepoHead, getRepoFile, getRepoTarball, getRepoTreeTarball, getRepoTreeId, listRepoDir, getPullRequest, getCollaboratorPermission, getAdvisory, listRepoAdvisories }
|
|
150
182
|
export const createClient = (options) => bindMethods(clientHeaders('createClient', options, true), readMethods)
|
package/src/tree.js
CHANGED
|
@@ -71,6 +71,8 @@ function treeId(dir) {
|
|
|
71
71
|
|
|
72
72
|
const EMPTY_TREE = objectId('tree', Buffer.alloc(0)).toString('hex')
|
|
73
73
|
const isName = matches(/^[^\0/]+$/u)
|
|
74
|
+
const isEntry = (entry) => isName(entry?.path) && isSha1(entry.sha)
|
|
75
|
+
const nameOf = (path) => Buffer.from(path).toString('latin1')
|
|
74
76
|
const subtree = (sha) => ({ mode: '40000', id: Buffer.from(sha, 'hex') })
|
|
75
77
|
|
|
76
78
|
// A subtree with nothing in it but subtrees like it, down to the empty
|
|
@@ -163,8 +165,8 @@ export async function gitTreeOfTarball(gzipped, { expected, list } = {}) {
|
|
|
163
165
|
}
|
|
164
166
|
if (top === undefined) return 'no tree: an empty tarball'
|
|
165
167
|
const listed = async (sha) => (list && isSha1(sha) ? await list(sha) : [])
|
|
166
|
-
.filter(
|
|
167
|
-
.map((entry) => ({ ...entry, name:
|
|
168
|
+
.filter(isEntry)
|
|
169
|
+
.map((entry) => ({ ...entry, name: nameOf(entry.path) }))
|
|
168
170
|
for (const [dir, name, path] of emptyDirs(root)) {
|
|
169
171
|
let sha = expected
|
|
170
172
|
const parts = path.split('/')
|
|
@@ -178,3 +180,15 @@ export async function gitTreeOfTarball(gzipped, { expected, list } = {}) {
|
|
|
178
180
|
await putBackEmptyTrees(root, expected, listed)
|
|
179
181
|
return treeId(root).toString('hex')
|
|
180
182
|
}
|
|
183
|
+
|
|
184
|
+
// GitHub lists a subtree's mode as `040000`, which git writes `40000`.
|
|
185
|
+
const LISTED = new Set(['100644 blob', '100755 blob', '120000 blob', '040000 tree', '160000 commit'])
|
|
186
|
+
|
|
187
|
+
// The id of the tree a listing such as GitHub's names, its entries
|
|
188
|
+
// { path, mode, type, sha }, or null where one is not an entry a tree can
|
|
189
|
+
// hold, or a name is there twice.
|
|
190
|
+
export function gitTreeOfListing(entries) {
|
|
191
|
+
if (!entries.every((entry) => isEntry(entry) && LISTED.has(`${entry.mode} ${entry.type}`))) return null
|
|
192
|
+
const dir = new Map(entries.map(({ path, mode, sha }) => [nameOf(path), { mode: mode.replace(/^0/u, ''), id: Buffer.from(sha, 'hex') }]))
|
|
193
|
+
return dir.size === entries.length ? treeId(dir).toString('hex') : null
|
|
194
|
+
}
|