@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.
Files changed (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. package/package.json +71 -0
@@ -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
+ }