@magland/mochi 0.3.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 +201 -0
- package/README.md +108 -0
- package/dist/ansi.js +174 -0
- package/dist/api/admin.js +416 -0
- package/dist/api/auth.js +166 -0
- package/dist/api/backup.js +598 -0
- package/dist/api/ci.js +336 -0
- package/dist/api/contents.js +339 -0
- package/dist/api/issues.js +165 -0
- package/dist/api/pulls.js +244 -0
- package/dist/api/releases.js +83 -0
- package/dist/api/repos.js +156 -0
- package/dist/api/write.js +518 -0
- package/dist/api.js +326 -0
- package/dist/assets.js +29 -0
- package/dist/atom.js +32 -0
- package/dist/atomic.js +171 -0
- package/dist/avatar.js +81 -0
- package/dist/browse.js +630 -0
- package/dist/build-info.json +4 -0
- package/dist/ci/actionref.js +86 -0
- package/dist/ci/api.js +829 -0
- package/dist/ci/artifacts.js +201 -0
- package/dist/ci/dispatch.js +30 -0
- package/dist/ci/engine.js +1321 -0
- package/dist/ci/expr.js +526 -0
- package/dist/ci/manual.js +199 -0
- package/dist/ci/present.js +82 -0
- package/dist/ci/protocol.js +6 -0
- package/dist/ci/runners.js +256 -0
- package/dist/ci/runs.js +208 -0
- package/dist/ci/trigger.js +28 -0
- package/dist/ci/views.js +441 -0
- package/dist/ci/wake.js +194 -0
- package/dist/ci/web.js +617 -0
- package/dist/ci/workflow.js +436 -0
- package/dist/cli/admin-cmd.js +324 -0
- package/dist/cli/api-cmd.js +128 -0
- package/dist/cli/backup-cmd.js +1500 -0
- package/dist/cli/exit.js +69 -0
- package/dist/cli/input.js +64 -0
- package/dist/cli/issue-cmd.js +243 -0
- package/dist/cli/output.js +93 -0
- package/dist/cli/parse.js +317 -0
- package/dist/cli/pr-cmd.js +289 -0
- package/dist/cli/release-cmd.js +171 -0
- package/dist/cli/repo-cmd.js +763 -0
- package/dist/cli/repo.js +101 -0
- package/dist/cli/run-cmd.js +438 -0
- package/dist/cli/target.js +54 -0
- package/dist/cli-api.js +84 -0
- package/dist/compare.js +111 -0
- package/dist/config.js +212 -0
- package/dist/credentials.js +235 -0
- package/dist/deploy-cli.js +859 -0
- package/dist/deploy-runner-cli.js +592 -0
- package/dist/diff.js +171 -0
- package/dist/discussion.js +253 -0
- package/dist/egress.js +559 -0
- package/dist/filecache.js +68 -0
- package/dist/find.js +162 -0
- package/dist/forms.js +737 -0
- package/dist/git.js +547 -0
- package/dist/githttp.js +428 -0
- package/dist/html.js +87 -0
- package/dist/icons.js +101 -0
- package/dist/import-cli.js +316 -0
- package/dist/index.js +752 -0
- package/dist/issues.js +308 -0
- package/dist/issueweb.js +447 -0
- package/dist/job-cli.js +197 -0
- package/dist/jobtoken.js +96 -0
- package/dist/languages.js +383 -0
- package/dist/layout.js +100 -0
- package/dist/lfs.js +438 -0
- package/dist/lfsstore.js +425 -0
- package/dist/limit.js +259 -0
- package/dist/logo.js +61 -0
- package/dist/markdown.js +382 -0
- package/dist/migrate.js +334 -0
- package/dist/multipart.js +90 -0
- package/dist/ops.js +869 -0
- package/dist/pagescript.js +465 -0
- package/dist/perms.js +370 -0
- package/dist/pointer.js +55 -0
- package/dist/profile.js +106 -0
- package/dist/pulls.js +320 -0
- package/dist/pullweb.js +461 -0
- package/dist/redirects.js +455 -0
- package/dist/releases.js +435 -0
- package/dist/render.js +233 -0
- package/dist/runner/actions.js +448 -0
- package/dist/runner/client.js +428 -0
- package/dist/runner/context.js +247 -0
- package/dist/runner/docker.js +197 -0
- package/dist/runner/externals.js +175 -0
- package/dist/runner/job.js +290 -0
- package/dist/runner/manual-run.js +272 -0
- package/dist/runner/overrides.js +554 -0
- package/dist/runner/steps.js +571 -0
- package/dist/runner/wake.js +84 -0
- package/dist/runner-cli.js +405 -0
- package/dist/scan.js +231 -0
- package/dist/server.js +424 -0
- package/dist/session.js +267 -0
- package/dist/site.js +259 -0
- package/dist/siteshost.js +94 -0
- package/dist/source.js +90 -0
- package/dist/style.js +1295 -0
- package/dist/themes.js +369 -0
- package/dist/vault.js +442 -0
- package/dist/version.js +88 -0
- package/dist/views.js +1007 -0
- package/dist/web.js +182 -0
- package/dist/webops.js +1402 -0
- package/package.json +71 -0
package/dist/jobtoken.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.mintJobToken = mintJobToken;
|
|
37
|
+
exports.verifyJobToken = verifyJobToken;
|
|
38
|
+
exports.grantCovers = grantCovers;
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
const session_1 = require("./session");
|
|
41
|
+
// Ephemeral per-job credentials, so a workflow job can clone the repository
|
|
42
|
+
// it is running for even when that repository is private. The runner is a
|
|
43
|
+
// separate process on a separate machine and holds no vault token; the
|
|
44
|
+
// engine mints one of these into the job payload, and the runner presents it
|
|
45
|
+
// as the Basic-auth password on the clone, exactly as a user token would be
|
|
46
|
+
// presented.
|
|
47
|
+
//
|
|
48
|
+
// The token is an HMAC under the vault's session secret (<vault>/.secret)
|
|
49
|
+
// over what it grants: one repository, read only, until an expiry. Nothing
|
|
50
|
+
// is stored: verification is recomputing the signature, the way sessions and
|
|
51
|
+
// LFS transfer URLs already work, so a job token cannot be enumerated or
|
|
52
|
+
// revoked singly, and rotating .secret revokes them all at once. The grant
|
|
53
|
+
// is read-only by construction: checkReadAuth accepts these and the push
|
|
54
|
+
// path does not.
|
|
55
|
+
const PREFIX = 'mochijob_';
|
|
56
|
+
function sign(root, body) {
|
|
57
|
+
return crypto.createHmac('sha256', (0, session_1.getSecret)(root)).update(`job:${body}`).digest('base64url');
|
|
58
|
+
}
|
|
59
|
+
function mintJobToken(root, grant, ttlMs) {
|
|
60
|
+
const payload = { c: grant.collection, r: grant.repo, run: grant.run, exp: Date.now() + ttlMs };
|
|
61
|
+
const body = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
|
|
62
|
+
return `${PREFIX}${body}.${sign(root, body)}`;
|
|
63
|
+
}
|
|
64
|
+
/** The grant a token carries, or null for anything expired, forged, or not a job token at all. */
|
|
65
|
+
function verifyJobToken(root, token) {
|
|
66
|
+
if (!token.startsWith(PREFIX))
|
|
67
|
+
return null;
|
|
68
|
+
const rest = token.slice(PREFIX.length);
|
|
69
|
+
const dot = rest.lastIndexOf('.');
|
|
70
|
+
if (dot === -1)
|
|
71
|
+
return null;
|
|
72
|
+
const body = rest.slice(0, dot);
|
|
73
|
+
const sig = Buffer.from(rest.slice(dot + 1));
|
|
74
|
+
const expected = Buffer.from(sign(root, body));
|
|
75
|
+
if (sig.length !== expected.length || !crypto.timingSafeEqual(sig, expected))
|
|
76
|
+
return null;
|
|
77
|
+
let payload;
|
|
78
|
+
try {
|
|
79
|
+
payload = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (typeof payload !== 'object' || payload === null)
|
|
85
|
+
return null;
|
|
86
|
+
const p = payload;
|
|
87
|
+
if (typeof p.c !== 'string' || typeof p.r !== 'string' || typeof p.run !== 'string')
|
|
88
|
+
return null;
|
|
89
|
+
if (typeof p.exp !== 'number' || p.exp < Date.now())
|
|
90
|
+
return null;
|
|
91
|
+
return { collection: p.c, repo: p.r, run: p.run };
|
|
92
|
+
}
|
|
93
|
+
/** Whether a verified grant covers the repository being read. */
|
|
94
|
+
function grantCovers(grant, collection, repo) {
|
|
95
|
+
return grant.collection === collection && grant.repo === repo;
|
|
96
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// What a repository is written in, as GitHub's Languages bar says it: the
|
|
3
|
+
// share of the source at a ref that belongs to each language, measured in
|
|
4
|
+
// bytes.
|
|
5
|
+
//
|
|
6
|
+
// The measurement is deliberately simple. GitHub runs Linguist, which reads
|
|
7
|
+
// file contents, resolves ambiguous extensions with heuristics, and consults
|
|
8
|
+
// .gitattributes; we look at the tree once and go by extension (or by
|
|
9
|
+
// filename, for the files that have none). That is a good deal cheaper — one
|
|
10
|
+
// `git ls-tree`, since the tree already carries every blob's size — and it
|
|
11
|
+
// agrees with Linguist on the ordinary repository. Two exceptions read
|
|
12
|
+
// contents, both bounded: `.m` files, whose extension names two languages
|
|
13
|
+
// (see decideMFiles), and a root .gitattributes carrying linguist-language
|
|
14
|
+
// overrides. Where the rules still cannot agree with Linguist is noted at
|
|
15
|
+
// each one below.
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.languageBreakdown = languageBreakdown;
|
|
18
|
+
const git_1 = require("./git");
|
|
19
|
+
// Names and colours are Linguist's (github-linguist/linguist, MIT), copied in
|
|
20
|
+
// rather than depended on, since mochi ships no static files. A language
|
|
21
|
+
// absent from this table is simply not counted, which is the same treatment
|
|
22
|
+
// Linguist gives a file it cannot classify.
|
|
23
|
+
const LANGUAGES = [
|
|
24
|
+
{ name: 'TypeScript', color: '#3178c6', type: 'programming', ext: ['ts', 'mts', 'cts'] },
|
|
25
|
+
{ name: 'TSX', color: '#3178c6', type: 'programming', ext: ['tsx'] },
|
|
26
|
+
{ name: 'JavaScript', color: '#f1e05a', type: 'programming', ext: ['js', 'mjs', 'cjs', 'jsx'] },
|
|
27
|
+
{ name: 'Python', color: '#3572A5', type: 'programming', ext: ['py', 'pyw', 'pyi'] },
|
|
28
|
+
{ name: 'Ruby', color: '#701516', type: 'programming', ext: ['rb'], names: ['Rakefile', 'Gemfile'] },
|
|
29
|
+
{ name: 'Go', color: '#00ADD8', type: 'programming', ext: ['go'] },
|
|
30
|
+
{ name: 'Rust', color: '#dea584', type: 'programming', ext: ['rs'] },
|
|
31
|
+
{ name: 'C', color: '#555555', type: 'programming', ext: ['c', 'h'] },
|
|
32
|
+
{ name: 'C++', color: '#f34b7d', type: 'programming', ext: ['cc', 'cpp', 'cxx', 'hpp', 'hh', 'hxx'] },
|
|
33
|
+
{ name: 'C#', color: '#178600', type: 'programming', ext: ['cs'] },
|
|
34
|
+
{ name: 'Java', color: '#b07219', type: 'programming', ext: ['java'] },
|
|
35
|
+
{ name: 'Kotlin', color: '#A97BFF', type: 'programming', ext: ['kt', 'kts'] },
|
|
36
|
+
{ name: 'Swift', color: '#F05138', type: 'programming', ext: ['swift'] },
|
|
37
|
+
{ name: 'Objective-C', color: '#438eff', type: 'programming', ext: ['mm'] },
|
|
38
|
+
{ name: 'PHP', color: '#4F5D95', type: 'programming', ext: ['php'] },
|
|
39
|
+
{ name: 'Perl', color: '#0298c3', type: 'programming', ext: ['pl', 'pm'] },
|
|
40
|
+
{ name: 'Lua', color: '#000080', type: 'programming', ext: ['lua'] },
|
|
41
|
+
{ name: 'R', color: '#198CE7', type: 'programming', ext: ['r'] },
|
|
42
|
+
{ name: 'Julia', color: '#a270ba', type: 'programming', ext: ['jl'] },
|
|
43
|
+
{ name: 'MATLAB', color: '#e16737', type: 'programming' },
|
|
44
|
+
{ name: 'Scala', color: '#c22d40', type: 'programming', ext: ['scala', 'sc'] },
|
|
45
|
+
{ name: 'Clojure', color: '#db5855', type: 'programming', ext: ['clj', 'cljs', 'cljc', 'edn'] },
|
|
46
|
+
{ name: 'Elixir', color: '#6e4a7e', type: 'programming', ext: ['ex', 'exs'] },
|
|
47
|
+
{ name: 'Erlang', color: '#B83998', type: 'programming', ext: ['erl', 'hrl'] },
|
|
48
|
+
{ name: 'Haskell', color: '#5e5086', type: 'programming', ext: ['hs', 'lhs'] },
|
|
49
|
+
{ name: 'OCaml', color: '#ef7a08', type: 'programming', ext: ['ml', 'mli'] },
|
|
50
|
+
{ name: 'F#', color: '#b845fc', type: 'programming', ext: ['fs', 'fsi', 'fsx'] },
|
|
51
|
+
{ name: 'Dart', color: '#00B4AB', type: 'programming', ext: ['dart'] },
|
|
52
|
+
{ name: 'Zig', color: '#ec915c', type: 'programming', ext: ['zig'] },
|
|
53
|
+
{ name: 'Nim', color: '#ffc200', type: 'programming', ext: ['nim', 'nims'] },
|
|
54
|
+
{ name: 'Crystal', color: '#000100', type: 'programming', ext: ['cr'] },
|
|
55
|
+
{ name: 'D', color: '#ba595e', type: 'programming', ext: ['d'] },
|
|
56
|
+
{ name: 'Groovy', color: '#4298b8', type: 'programming', ext: ['groovy', 'gradle'] },
|
|
57
|
+
{ name: 'Fortran', color: '#4d41b1', type: 'programming', ext: ['f', 'f90', 'f95', 'for'] },
|
|
58
|
+
{ name: 'Assembly', color: '#6E4C13', type: 'programming', ext: ['asm', 's'] },
|
|
59
|
+
{ name: 'Verilog', color: '#b2b7f8', type: 'programming', ext: ['v', 'vh'] },
|
|
60
|
+
{ name: 'VHDL', color: '#adb2cb', type: 'programming', ext: ['vhd', 'vhdl'] },
|
|
61
|
+
{ name: 'Solidity', color: '#AA6746', type: 'programming', ext: ['sol'] },
|
|
62
|
+
{ name: 'Emacs Lisp', color: '#c065db', type: 'programming', ext: ['el'] },
|
|
63
|
+
{ name: 'Common Lisp', color: '#3fb68b', type: 'programming', ext: ['lisp', 'lsp'] },
|
|
64
|
+
{ name: 'Scheme', color: '#1e4aec', type: 'programming', ext: ['scm', 'ss'] },
|
|
65
|
+
{ name: 'Racket', color: '#3c5caa', type: 'programming', ext: ['rkt'] },
|
|
66
|
+
{ name: 'Prolog', color: '#74283c', type: 'programming', ext: ['pro'] },
|
|
67
|
+
{ name: 'Ada', color: '#02f88c', type: 'programming', ext: ['adb', 'ads'] },
|
|
68
|
+
{ name: 'Vim Script', color: '#199f4b', type: 'programming', ext: ['vim'], names: ['.vimrc', 'vimrc'] },
|
|
69
|
+
{ name: 'Shell', color: '#89e051', type: 'programming', ext: ['sh', 'bash', 'zsh', 'fish'] },
|
|
70
|
+
{ name: 'PowerShell', color: '#012456', type: 'programming', ext: ['ps1', 'psm1', 'psd1'] },
|
|
71
|
+
{ name: 'Batchfile', color: '#C1F12E', type: 'programming', ext: ['bat', 'cmd'] },
|
|
72
|
+
{ name: 'Awk', color: '#c30e9b', type: 'programming', ext: ['awk'] },
|
|
73
|
+
{ name: 'Makefile', color: '#427819', type: 'programming', ext: ['mk'], names: ['Makefile', 'GNUmakefile', 'makefile'] },
|
|
74
|
+
{ name: 'CMake', color: '#DA3434', type: 'programming', ext: ['cmake'], names: ['CMakeLists.txt'] },
|
|
75
|
+
{ name: 'Dockerfile', color: '#384d54', type: 'programming', ext: ['dockerfile'], names: ['Dockerfile'] },
|
|
76
|
+
{ name: 'Nix', color: '#7e7eff', type: 'programming', ext: ['nix'] },
|
|
77
|
+
{ name: 'HCL', color: '#844FBA', type: 'programming', ext: ['tf', 'tfvars', 'hcl'] },
|
|
78
|
+
{ name: 'SQL', color: '#e38c00', type: 'programming', ext: ['sql'] },
|
|
79
|
+
{ name: 'GraphQL', color: '#e10098', type: 'programming', ext: ['graphql', 'gql'] },
|
|
80
|
+
{ name: 'Jupyter Notebook', color: '#DA5B0B', type: 'programming', ext: ['ipynb'] },
|
|
81
|
+
{ name: 'TeX', color: '#3D6117', type: 'programming', ext: ['tex', 'sty', 'cls'] },
|
|
82
|
+
{ name: 'HTML', color: '#e34c26', type: 'markup', ext: ['html', 'htm', 'xhtml'] },
|
|
83
|
+
{ name: 'CSS', color: '#563d7c', type: 'markup', ext: ['css'] },
|
|
84
|
+
{ name: 'SCSS', color: '#c6538c', type: 'markup', ext: ['scss'] },
|
|
85
|
+
{ name: 'Sass', color: '#a53b70', type: 'markup', ext: ['sass'] },
|
|
86
|
+
{ name: 'Less', color: '#1d365d', type: 'markup', ext: ['less'] },
|
|
87
|
+
{ name: 'Vue', color: '#41b883', type: 'markup', ext: ['vue'] },
|
|
88
|
+
{ name: 'Svelte', color: '#ff3e00', type: 'markup', ext: ['svelte'] },
|
|
89
|
+
{ name: 'Astro', color: '#ff5a03', type: 'markup', ext: ['astro'] },
|
|
90
|
+
{ name: 'Handlebars', color: '#f7931e', type: 'markup', ext: ['hbs', 'handlebars'] },
|
|
91
|
+
{ name: 'Pug', color: '#a86454', type: 'markup', ext: ['pug', 'jade'] },
|
|
92
|
+
{ name: 'Haml', color: '#ece2a9', type: 'markup', ext: ['haml'] },
|
|
93
|
+
{ name: 'Twig', color: '#c1d026', type: 'markup', ext: ['twig'] },
|
|
94
|
+
{ name: 'Blade', color: '#f7523f', type: 'markup', ext: ['blade'] },
|
|
95
|
+
{ name: 'EJS', color: '#a91e50', type: 'markup', ext: ['ejs'] },
|
|
96
|
+
{ name: 'Liquid', color: '#67b8de', type: 'markup', ext: ['liquid'] },
|
|
97
|
+
{ name: 'Svg', color: '#ff9900', type: 'data', ext: ['svg'] },
|
|
98
|
+
{ name: 'JSON', color: '#292929', type: 'data', ext: ['json', 'jsonc'] },
|
|
99
|
+
{ name: 'YAML', color: '#cb171e', type: 'data', ext: ['yml', 'yaml'] },
|
|
100
|
+
{ name: 'TOML', color: '#9c4221', type: 'data', ext: ['toml'] },
|
|
101
|
+
{ name: 'XML', color: '#0060ac', type: 'data', ext: ['xml', 'xsd', 'xsl'] },
|
|
102
|
+
{ name: 'CSV', color: '#237346', type: 'data', ext: ['csv', 'tsv'] },
|
|
103
|
+
{ name: 'INI', color: '#d1dbe0', type: 'data', ext: ['ini', 'cfg'] },
|
|
104
|
+
{ name: 'Markdown', color: '#083fa1', type: 'prose', ext: ['md', 'markdown'] },
|
|
105
|
+
{ name: 'reStructuredText', color: '#141414', type: 'prose', ext: ['rst'] },
|
|
106
|
+
{ name: 'Text', color: '#8f8f8f', type: 'prose', ext: ['txt'] },
|
|
107
|
+
];
|
|
108
|
+
const BY_EXT = new Map();
|
|
109
|
+
const BY_NAME = new Map();
|
|
110
|
+
for (const lang of LANGUAGES) {
|
|
111
|
+
for (const e of lang.ext ?? [])
|
|
112
|
+
BY_EXT.set(e, lang);
|
|
113
|
+
for (const n of lang.names ?? [])
|
|
114
|
+
BY_NAME.set(n, lang);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Paths whose contents someone else wrote, or a tool generated. Linguist has
|
|
118
|
+
* a hundred or so of these rules; this is the handful that covers what a
|
|
119
|
+
* repository in a vault is actually likely to contain, and being a subset it
|
|
120
|
+
* errs towards counting something it should have skipped rather than the
|
|
121
|
+
* reverse.
|
|
122
|
+
*/
|
|
123
|
+
const VENDORED = /(^|\/)(node_modules|bower_components|vendor|third_party|thirdparty|Godeps|\.yarn|dist|build|out|target|__pycache__|\.venv|venv)\//;
|
|
124
|
+
const GENERATED = /(\.min\.(js|css)|\.map|-lock\.json|\.lock|\.pb\.go|_pb2\.py)$/;
|
|
125
|
+
/**
|
|
126
|
+
* Everything smaller than this disappears into "Other", so that a repository
|
|
127
|
+
* with one stray file of forty languages still gets a legible bar.
|
|
128
|
+
*/
|
|
129
|
+
const MIN_SHARE = 0.5;
|
|
130
|
+
const OTHER_COLOR = '#8f8f8f';
|
|
131
|
+
/**
|
|
132
|
+
* A tree wider than this is not measured. The parse is cheap, but a bar built
|
|
133
|
+
* from a million files is neither more informative than one built from twenty
|
|
134
|
+
* thousand nor worth the memory, and the root page has to stay quick.
|
|
135
|
+
*/
|
|
136
|
+
const MAX_FILES = 20000;
|
|
137
|
+
const BY_LANG_NAME = new Map(LANGUAGES.map((l) => [l.name.toLowerCase(), l]));
|
|
138
|
+
function classify(filePath) {
|
|
139
|
+
const name = filePath.slice(filePath.lastIndexOf('/') + 1);
|
|
140
|
+
const byName = BY_NAME.get(name);
|
|
141
|
+
if (byName)
|
|
142
|
+
return byName;
|
|
143
|
+
const dot = name.lastIndexOf('.');
|
|
144
|
+
// A leading dot is the start of a dotfile's name, not an extension.
|
|
145
|
+
if (dot <= 0)
|
|
146
|
+
return null;
|
|
147
|
+
return BY_EXT.get(name.slice(dot + 1).toLowerCase()) ?? null;
|
|
148
|
+
}
|
|
149
|
+
// ---- the .m problem ----
|
|
150
|
+
//
|
|
151
|
+
// One extension, two languages: `.m` is Objective-C and it is MATLAB, and an
|
|
152
|
+
// extension table cannot tell them apart. Going by extension alone called a
|
|
153
|
+
// vault of MATLAB code Objective-C across the board. So `.m` is the one place
|
|
154
|
+
// contents are read: the files are sampled with a single `git cat-file
|
|
155
|
+
// --batch` and told apart by markers no editor of the other language writes.
|
|
156
|
+
// A file the markers cannot decide is MATLAB, the likelier answer for what a
|
|
157
|
+
// research vault holds.
|
|
158
|
+
const MATLAB = BY_LANG_NAME.get('matlab');
|
|
159
|
+
const OBJC = BY_LANG_NAME.get('objective-c');
|
|
160
|
+
/** The most `.m` files read per breakdown; the rest take the sampled majority. */
|
|
161
|
+
const MAX_SNIFF = 20;
|
|
162
|
+
/** A `.m` file bigger than this is not read; the first bytes of a smaller one decide it. */
|
|
163
|
+
const MAX_SNIFF_BYTES = 100 * 1024;
|
|
164
|
+
/** How much of each sampled file the markers are scored over. */
|
|
165
|
+
const SNIFF_WINDOW = 4096;
|
|
166
|
+
function sniffM(text) {
|
|
167
|
+
let objc = 0;
|
|
168
|
+
let matlab = 0;
|
|
169
|
+
for (const line of text.split('\n', 200)) {
|
|
170
|
+
const t = line.trimStart();
|
|
171
|
+
if (/^#(import|include)\b/.test(t))
|
|
172
|
+
objc += 2;
|
|
173
|
+
else if (/^@(interface|implementation|protocol|property|synthesize|end)\b/.test(t))
|
|
174
|
+
objc += 2;
|
|
175
|
+
else if (/^[-+]\s*\(/.test(t))
|
|
176
|
+
objc += 2;
|
|
177
|
+
else if (t.startsWith('//') || t.startsWith('/*'))
|
|
178
|
+
objc += 1;
|
|
179
|
+
else if (/^(function|classdef)\b/.test(t))
|
|
180
|
+
matlab += 2;
|
|
181
|
+
else if (t.startsWith('%'))
|
|
182
|
+
matlab += 1;
|
|
183
|
+
else if (/^end\s*;?\s*$/.test(t))
|
|
184
|
+
matlab += 1;
|
|
185
|
+
}
|
|
186
|
+
return objc > matlab ? OBJC : MATLAB;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Decide each `.m` file, reading up to MAX_SNIFF of them in one batched git
|
|
190
|
+
* call. Files beyond the sample, and files too large to sample, follow the
|
|
191
|
+
* majority of what was read; a repository that genuinely mixes the two
|
|
192
|
+
* languages gets its sampled files right and its tail approximated, which is
|
|
193
|
+
* the honest limit of sampling.
|
|
194
|
+
*/
|
|
195
|
+
async function decideMFiles(dir, ref, files) {
|
|
196
|
+
const out = new Map();
|
|
197
|
+
const sample = files.filter((f) => f.size <= MAX_SNIFF_BYTES).slice(0, MAX_SNIFF);
|
|
198
|
+
let objcBytes = 0;
|
|
199
|
+
let matlabBytes = 0;
|
|
200
|
+
if (sample.length > 0) {
|
|
201
|
+
let batch;
|
|
202
|
+
try {
|
|
203
|
+
batch = await (0, git_1.execGit)(dir, ['cat-file', '--batch'], {
|
|
204
|
+
input: sample.map((f) => `${ref}:${f.path}`).join('\n') + '\n',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
batch = Buffer.alloc(0);
|
|
209
|
+
}
|
|
210
|
+
// <oid> <type> <size>\n<contents>\n per object, "<input> missing" for one
|
|
211
|
+
// that is not there. Walked by size rather than split, since contents can
|
|
212
|
+
// hold anything.
|
|
213
|
+
let pos = 0;
|
|
214
|
+
let i = 0;
|
|
215
|
+
while (pos < batch.length && i < sample.length) {
|
|
216
|
+
const nl = batch.indexOf(0x0a, pos);
|
|
217
|
+
if (nl === -1)
|
|
218
|
+
break;
|
|
219
|
+
const header = batch.toString('utf8', pos, nl);
|
|
220
|
+
pos = nl + 1;
|
|
221
|
+
const m = header.match(/^\S+ (\S+) (\d+)$/);
|
|
222
|
+
if (!m || m[1] !== 'blob') {
|
|
223
|
+
i++;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const size = parseInt(m[2], 10);
|
|
227
|
+
const text = batch.toString('utf8', pos, Math.min(pos + SNIFF_WINDOW, pos + size));
|
|
228
|
+
pos += size + 1;
|
|
229
|
+
const def = sniffM(text);
|
|
230
|
+
out.set(sample[i].path, def);
|
|
231
|
+
if (def === OBJC)
|
|
232
|
+
objcBytes += sample[i].size;
|
|
233
|
+
else
|
|
234
|
+
matlabBytes += sample[i].size;
|
|
235
|
+
i++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const rest = objcBytes > matlabBytes ? OBJC : MATLAB;
|
|
239
|
+
for (const f of files)
|
|
240
|
+
if (!out.has(f.path))
|
|
241
|
+
out.set(f.path, rest);
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
function attrPatternToRegex(pattern) {
|
|
245
|
+
let p = pattern;
|
|
246
|
+
if (p.startsWith('/'))
|
|
247
|
+
p = p.slice(1);
|
|
248
|
+
let rx = '';
|
|
249
|
+
for (let i = 0; i < p.length; i++) {
|
|
250
|
+
const c = p[i];
|
|
251
|
+
if (c === '*') {
|
|
252
|
+
if (p[i + 1] === '*') {
|
|
253
|
+
rx += '.*';
|
|
254
|
+
i++;
|
|
255
|
+
}
|
|
256
|
+
else
|
|
257
|
+
rx += '[^/]*';
|
|
258
|
+
}
|
|
259
|
+
else if (c === '?')
|
|
260
|
+
rx += '[^/]';
|
|
261
|
+
else
|
|
262
|
+
rx += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
return new RegExp(`^${rx}$`);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function parseOverrides(text) {
|
|
272
|
+
const out = [];
|
|
273
|
+
for (const raw of text.split('\n')) {
|
|
274
|
+
const line = raw.trim();
|
|
275
|
+
if (line === '' || line.startsWith('#'))
|
|
276
|
+
continue;
|
|
277
|
+
const m = line.match(/^(\S+)\s+.*\blinguist-language=(\S+)/);
|
|
278
|
+
if (!m)
|
|
279
|
+
continue;
|
|
280
|
+
const rx = attrPatternToRegex(m[1]);
|
|
281
|
+
if (!rx)
|
|
282
|
+
continue;
|
|
283
|
+
out.push({
|
|
284
|
+
pattern: rx,
|
|
285
|
+
basename: !m[1].includes('/'),
|
|
286
|
+
lang: BY_LANG_NAME.get(m[2].toLowerCase().replace(/-/g, ' ')) ?? BY_LANG_NAME.get(m[2].toLowerCase()) ?? null,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
/** The last matching override wins, as in gitattributes; null means no override. */
|
|
292
|
+
function overrideFor(overrides, filePath) {
|
|
293
|
+
const base = filePath.slice(filePath.lastIndexOf('/') + 1);
|
|
294
|
+
let hit = null;
|
|
295
|
+
for (const o of overrides) {
|
|
296
|
+
if (o.pattern.test(o.basename ? base : filePath))
|
|
297
|
+
hit = o;
|
|
298
|
+
}
|
|
299
|
+
return hit;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The language breakdown of a ref, biggest share first, or an empty list when
|
|
303
|
+
* nothing at the ref is countable (an empty repository, a tree of documents,
|
|
304
|
+
* or one wider than MAX_FILES).
|
|
305
|
+
*
|
|
306
|
+
* Sizes come from `git ls-tree -l`, which is the size of the blob as stored:
|
|
307
|
+
* for a file kept in LFS that is the size of its pointer, so an LFS-heavy
|
|
308
|
+
* repository reports the language of its pointers' neighbours. Symlinks and
|
|
309
|
+
* submodule gitlinks are not files here and are skipped.
|
|
310
|
+
*/
|
|
311
|
+
async function languageBreakdown(dir, ref) {
|
|
312
|
+
let out;
|
|
313
|
+
try {
|
|
314
|
+
out = (await (0, git_1.execGit)(dir, ['ls-tree', '-r', '-l', '-z', ref])).toString('utf8');
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
const counted = [];
|
|
320
|
+
const mFiles = [];
|
|
321
|
+
let hasAttributes = false;
|
|
322
|
+
let files = 0;
|
|
323
|
+
for (const item of out.split('\0')) {
|
|
324
|
+
if (!item)
|
|
325
|
+
continue;
|
|
326
|
+
if (++files > MAX_FILES)
|
|
327
|
+
return [];
|
|
328
|
+
// <mode> <type> <sha> <size>\t<path>, as listTree() parses it.
|
|
329
|
+
const m = item.match(/^(\S+) (\S+) (\S+) +(\S+)\t([\s\S]*)$/);
|
|
330
|
+
if (!m)
|
|
331
|
+
continue;
|
|
332
|
+
const [, mode, type, , sizeText, filePath] = m;
|
|
333
|
+
if (type !== 'blob' || mode === '120000')
|
|
334
|
+
continue;
|
|
335
|
+
if (filePath === '.gitattributes')
|
|
336
|
+
hasAttributes = true;
|
|
337
|
+
const size = parseInt(sizeText, 10);
|
|
338
|
+
if (!Number.isFinite(size) || size === 0)
|
|
339
|
+
continue;
|
|
340
|
+
if (VENDORED.test(filePath) || GENERATED.test(filePath))
|
|
341
|
+
continue;
|
|
342
|
+
counted.push({ path: filePath, size });
|
|
343
|
+
if (/\.m$/i.test(filePath))
|
|
344
|
+
mFiles.push({ path: filePath, size });
|
|
345
|
+
}
|
|
346
|
+
const overrides = hasAttributes
|
|
347
|
+
? parseOverrides(await (0, git_1.execGit)(dir, ['cat-file', 'blob', `${ref}:.gitattributes`]).then((b) => b.toString('utf8'), () => ''))
|
|
348
|
+
: [];
|
|
349
|
+
const mLang = mFiles.length > 0 ? await decideMFiles(dir, ref, mFiles) : new Map();
|
|
350
|
+
const bytes = new Map();
|
|
351
|
+
for (const f of counted) {
|
|
352
|
+
let def;
|
|
353
|
+
const o = overrideFor(overrides, f.path);
|
|
354
|
+
if (o)
|
|
355
|
+
def = o.lang;
|
|
356
|
+
else
|
|
357
|
+
def = mLang.get(f.path) ?? classify(f.path);
|
|
358
|
+
if (!def || (def.type !== 'programming' && def.type !== 'markup'))
|
|
359
|
+
continue;
|
|
360
|
+
const entry = bytes.get(def.name);
|
|
361
|
+
if (entry)
|
|
362
|
+
entry.total += f.size;
|
|
363
|
+
else
|
|
364
|
+
bytes.set(def.name, { def, total: f.size });
|
|
365
|
+
}
|
|
366
|
+
const total = [...bytes.values()].reduce((sum, e) => sum + e.total, 0);
|
|
367
|
+
if (total === 0)
|
|
368
|
+
return [];
|
|
369
|
+
const all = [...bytes.values()]
|
|
370
|
+
.map((e) => ({ name: e.def.name, color: e.def.color, bytes: e.total, share: (e.total / total) * 100 }))
|
|
371
|
+
.sort((a, b) => b.bytes - a.bytes);
|
|
372
|
+
const kept = all.filter((l) => l.share >= MIN_SHARE);
|
|
373
|
+
const rest = all.filter((l) => l.share < MIN_SHARE);
|
|
374
|
+
if (rest.length > 0) {
|
|
375
|
+
kept.push({
|
|
376
|
+
name: 'Other',
|
|
377
|
+
color: OTHER_COLOR,
|
|
378
|
+
bytes: rest.reduce((sum, l) => sum + l.bytes, 0),
|
|
379
|
+
share: rest.reduce((sum, l) => sum + l.share, 0),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return kept;
|
|
383
|
+
}
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.REPOS_DIR = exports.COLLECTIONS_DIR = void 0;
|
|
37
|
+
exports.collectionsDir = collectionsDir;
|
|
38
|
+
exports.collectionDir = collectionDir;
|
|
39
|
+
exports.reposDir = reposDir;
|
|
40
|
+
exports.repoPath = repoPath;
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
/**
|
|
43
|
+
* Where a vault keeps the things a user named.
|
|
44
|
+
*
|
|
45
|
+
* Collections do not sit directly in the vault directory, and repositories do
|
|
46
|
+
* not sit directly in a collection's: each is held one level down, in a fixed
|
|
47
|
+
* directory. The reason is the same at both levels. A vault has files of its
|
|
48
|
+
* own (vault.json, config.json, .secret, runners.json) and will have more of
|
|
49
|
+
* them, and a collection will want its own metadata too; with user-chosen
|
|
50
|
+
* names in the same directory, every such file is a name a collection or a
|
|
51
|
+
* repository may then never be called, and adding one later takes a name away
|
|
52
|
+
* from vaults that already exist. Holding the named things apart keeps the two
|
|
53
|
+
* namespaces from ever meeting, so `collections/` may gain neighbours and a
|
|
54
|
+
* collection may gain files without any of it constraining what a user may
|
|
55
|
+
* name a thing.
|
|
56
|
+
*
|
|
57
|
+
* ```
|
|
58
|
+
* <vault>/
|
|
59
|
+
* vault.json
|
|
60
|
+
* config.json
|
|
61
|
+
* .secret
|
|
62
|
+
* runners.json
|
|
63
|
+
* collections/
|
|
64
|
+
* alice/
|
|
65
|
+
* repos/
|
|
66
|
+
* webapp.git/
|
|
67
|
+
* webapp.issues/
|
|
68
|
+
* ...
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* Every path into a vault is built here rather than joined at its use, so the
|
|
72
|
+
* layout is one file's business. A vault written under the older layout, with
|
|
73
|
+
* collections directly in the root, is moved to this one on startup; see
|
|
74
|
+
* src/migrate.ts.
|
|
75
|
+
*/
|
|
76
|
+
exports.COLLECTIONS_DIR = 'collections';
|
|
77
|
+
exports.REPOS_DIR = 'repos';
|
|
78
|
+
/** The directory holding every collection in the vault. */
|
|
79
|
+
function collectionsDir(root) {
|
|
80
|
+
return path.join(root, exports.COLLECTIONS_DIR);
|
|
81
|
+
}
|
|
82
|
+
/** One collection's directory, whether or not it exists. */
|
|
83
|
+
function collectionDir(root, collection) {
|
|
84
|
+
return path.join(root, exports.COLLECTIONS_DIR, collection);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Where a collection's repositories live, along with the directories each one
|
|
88
|
+
* keeps beside it (`<repo>.issues`, `<repo>.site`, and the rest).
|
|
89
|
+
*/
|
|
90
|
+
function reposDir(root, collection) {
|
|
91
|
+
return path.join(root, exports.COLLECTIONS_DIR, collection, exports.REPOS_DIR);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A named directory inside a collection's repos directory: a bare repository,
|
|
95
|
+
* or one of the directories a repository keeps beside it. The name is used as
|
|
96
|
+
* given, so callers pass `webapp.git` or `webapp.issues` rather than `webapp`.
|
|
97
|
+
*/
|
|
98
|
+
function repoPath(root, collection, dirName) {
|
|
99
|
+
return path.join(root, exports.COLLECTIONS_DIR, collection, exports.REPOS_DIR, dirName);
|
|
100
|
+
}
|