-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathcompute-repository.js
220 lines (193 loc) · 6.77 KB
/
compute-repository.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/**
* Module that exports a function that takes a list of specifications as input
* and computes, for each of them, the URL of the repository that contains the
* source code for this, as well as the source file of the specification at the
* HEAD of default branch in the repository.
*
* The function needs an authentication token for the GitHub API.
*/
import Octokit from "./octokit.js";
import parseSpecUrl from "./parse-spec-url.js";
/**
* Returns the first item in the list found in the Git tree, or null if none of
* the items exists in the array.
*/
function getFirstFoundInTree(paths, ...items) {
for (const item of items) {
const path = paths.find(p => p.path === item);
if (path) {
return path;
}
}
return null;
}
/**
* Exports main function that takes a list of specs (with a nighly.url property)
* as input, completes entries with a nightly.repository property when possible
* and returns the list.
*
* The options parameter is used to specify the GitHub API authentication token.
* In the absence of it, the function does not go through the GitHub API and
* thus cannot set most of the information. This is useful to run tests without
* an authentication token (but obviously means that the owner name returned
* by the function will remain the lowercased version, and that the returned
* info won't include the source file).
*/
export default async function (specs, options) {
if (!specs) {
throw "Invalid list of specifications passed as parameter";
}
options = options || {};
const octokit = new Octokit({ auth: options.githubToken });
const repoCache = new Map();
const repoPathCache = new Map();
const userCache = new Map();
/**
* Take a GitHub repo owner name (lowercase version) and retrieve the real
* owner name (with possible uppercase characters) from the GitHub API.
*/
async function fetchRealGitHubOwnerName(username) {
if (!userCache.has(username)) {
const { data } = await octokit.users.getByUsername({ username });
if (data.message) {
// Alert when user does not exist
throw res.message;
}
userCache.set(username, data.login);
}
return userCache.get(username);
}
/**
* Determine the name of the file that contains the source of the spec in the
* default branch of the GitHub repository associated with the specification.
*/
async function determineSourcePath(spec, repo) {
// Retrieve all paths of the GitHub repository
const cacheKey = `${repo.owner}/${repo.name}`;
if (!repoPathCache.has(cacheKey)) {
const { data } = await octokit.git.getTree({
owner: repo.owner,
repo: repo.name,
tree_sha: "HEAD",
recursive: true
});
const paths = data.tree;
repoPathCache.set(cacheKey, paths);
}
const paths = repoPathCache.get(cacheKey);
// Extract filename from nightly URL when there is one
const match = spec.nightly.url.match(/\/([\w\-]+)\.html$/);
const nightlyFilename = match ? match[1] : "";
const sourcePath = getFirstFoundInTree(paths,
// Common paths for CSS specs
`${spec.shortname}.bs`,
`${spec.shortname}/Overview.bs`,
`${spec.shortname}/Overview.src.html`,
`${spec.series.shortname}/Overview.bs`,
`${spec.series.shortname}/Overview.src.html`,
// Used for SHACL specs
`${spec.shortname}/index.html`,
// Used for ARIA specs
`${spec.series.shortname}/index.html`,
// Named after the nightly filename
`${nightlyFilename}.bs`,
`${nightlyFilename}.html`,
`${nightlyFilename}.src.html`,
`${nightlyFilename}.md`,
// WebGL extensions
`extensions/${spec.shortname}/extension.xml`,
// WebAssembly specs
`document/${spec.series.shortname.replace(/^wasm-/, '')}/index.bs`,
// SVG specs
`specs/${spec.shortname.replace(/^svg-/, '')}/master/Overview.html`,
`master/Overview.html`,
// HTTPWG specs
`specs/${spec.shortname}.xml`,
// Following patterns are used in a small number of cases, but could
// perhaps appear again in the future, so worth handling here.
"spec/index.bs",
"spec/index.html", // Only one TC39 spec
"spec/Overview.html", // Only WebCrypto
"docs/index.bs", // Only ServiceWorker
"spec.html", // Most TC39 specs
"spec.emu", // Some TC39 specs
`${spec.shortname}/Overview.html`, // css-color-3, mediaqueries-3
// Most common patterns, checking on "index.html" last as some repos
// include such a file to store the generated spec from the source.
"index.src.html",
"index.bs",
"spec.bs",
"index.md",
"index.html"
);
if (!sourcePath) {
return null;
}
// Fetch target file for symlinks
if (sourcePath.mode === "120000") {
const { data } = await octokit.git.getBlob({
owner: repo.owner,
repo: repo.name,
file_sha: sourcePath.sha
});
return Buffer.from(data.content, "base64").toString("utf8");
}
return sourcePath.path;
}
async function isRealRepo(repo) {
if (!options.githubToken) {
// Assume the repo exists if we can't check
return true;
}
const cacheKey = `${repo.owner}/${repo.name}`;
if (!repoCache.has(cacheKey)) {
try {
await octokit.repos.get({
owner: repo.owner,
repo: repo.name
});
repoCache.set(cacheKey, true);
}
catch (err) {
if (err.status === 404) {
repoCache.set(cacheKey, false);
}
else {
throw err;
}
}
}
return repoCache.get(cacheKey);
}
// Compute GitHub repositories with lowercase owner names
const repos = specs.map(spec => spec.nightly ?
parseSpecUrl(spec.nightly.repository ?? spec.nightly.url) :
null);
if (options.githubToken) {
// Fetch the real name of repository owners (preserving case)
for (const repo of repos) {
if (repo) {
repo.owner = await fetchRealGitHubOwnerName(repo.owner);
}
}
}
// Compute final repo URL and add source file if possible
for (const spec of specs) {
const repo = repos.shift();
if (repo && await isRealRepo(repo)) {
spec.nightly.repository = `https://github.com/${repo.owner}/${repo.name}`;
if (options.githubToken && !spec.nightly.sourcePath) {
const sourcePath = await determineSourcePath(spec, repo);
if (sourcePath) {
spec.nightly.sourcePath = sourcePath;
}
}
}
else if (spec.nightly?.url.match(/\/httpwg\.org\//)) {
const draftName = spec.nightly.url.match(/\/(draft-ietf-(.+))\.html$/);
spec.nightly.repository = 'https://github.com/httpwg/http-extensions';
spec.nightly.sourcePath = `${draftName[1]}.md`;
}
}
return specs;
};