@volter/twin-github 0.1.2 → 2.0.0

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 (215) hide show
  1. package/README.md +88 -36
  2. package/client/github-mirror.css +277 -319
  3. package/client/github-mirror.d.ts +418 -0
  4. package/client/github-mirror.js +485 -0
  5. package/client/github-mirror.tsx +153 -357
  6. package/client/pulls-rest.ts +159 -0
  7. package/client/pulls-workspace.tsx +841 -0
  8. package/dist/client/github-mirror.bundle.js +239 -0
  9. package/dist/client/github-mirror.css +916 -0
  10. package/dist/client/github-mirror.d.ts +418 -0
  11. package/dist/client/github-mirror.js +485 -0
  12. package/dist/client/github-mirror.tsx +1315 -0
  13. package/dist/client/pulls-rest.d.ts +42 -0
  14. package/dist/client/pulls-rest.js +140 -0
  15. package/dist/client/pulls-rest.ts +159 -0
  16. package/dist/client/pulls-workspace.bundle.js +22 -0
  17. package/dist/client/pulls-workspace.d.ts +114 -0
  18. package/dist/client/pulls-workspace.js +418 -0
  19. package/dist/client/pulls-workspace.tsx +841 -0
  20. package/dist/src/cli.d.ts +2 -0
  21. package/dist/src/cli.js +40 -0
  22. package/dist/src/generated/graphql-sdl.gen.json +1 -0
  23. package/dist/src/generated/graphql.gen.json +1 -0
  24. package/dist/src/generated/surface.gen.json +1 -0
  25. package/dist/src/generated/ui.gen.json +1 -0
  26. package/dist/src/github-budget.d.ts +69 -0
  27. package/dist/src/github-budget.js +172 -0
  28. package/dist/src/github-capabilities.d.ts +5 -0
  29. package/dist/src/github-capabilities.js +4468 -0
  30. package/dist/src/github-conformance.d.ts +43 -0
  31. package/dist/src/github-conformance.js +76 -0
  32. package/dist/src/github-connector.d.ts +307 -0
  33. package/dist/src/github-connector.js +1398 -0
  34. package/dist/src/github-events.d.ts +41 -0
  35. package/dist/src/github-events.js +230 -0
  36. package/dist/src/github-git-http.d.ts +49 -0
  37. package/dist/src/github-git-http.js +185 -0
  38. package/dist/src/github-git-plane.d.ts +114 -0
  39. package/dist/src/github-git-plane.js +407 -0
  40. package/dist/src/github-mirror-state.d.ts +2 -0
  41. package/dist/src/github-mirror-state.js +335 -0
  42. package/dist/src/github-mirror-ui.d.ts +20 -0
  43. package/dist/src/github-mirror-ui.js +101 -0
  44. package/dist/src/github-server.d.ts +14 -0
  45. package/dist/src/github-server.js +240 -0
  46. package/dist/src/github-shared.d.ts +12 -0
  47. package/dist/src/github-shared.js +21 -0
  48. package/dist/src/github-twin.d.ts +1411 -0
  49. package/dist/src/github-twin.js +4084 -0
  50. package/dist/src/github-ui-conformance.d.ts +4 -0
  51. package/dist/src/github-ui-conformance.js +105 -0
  52. package/dist/src/github-ui-structure.d.ts +18 -0
  53. package/dist/src/github-ui-structure.js +251 -0
  54. package/dist/src/graphql-wire.d.ts +22 -0
  55. package/dist/src/graphql-wire.js +89 -0
  56. package/dist/src/index.d.ts +17 -0
  57. package/dist/src/index.js +101 -0
  58. package/dist/src/manifest.d.ts +8 -0
  59. package/dist/src/manifest.js +597 -0
  60. package/dist/src/npm-registry.d.ts +7 -0
  61. package/dist/src/npm-registry.js +47 -0
  62. package/dist/src/screens/app-installation.d.ts +3 -0
  63. package/dist/src/screens/app-installation.js +166 -0
  64. package/dist/src/screens/app-manifest.d.ts +3 -0
  65. package/dist/src/screens/app-manifest.js +81 -0
  66. package/dist/src/screens/oauth.d.ts +15 -0
  67. package/dist/src/screens/oauth.js +257 -0
  68. package/dist/src/screens/session.d.ts +8 -0
  69. package/dist/src/screens/session.js +159 -0
  70. package/dist/src/semantics/actions.d.ts +2 -0
  71. package/dist/src/semantics/actions.js +413 -0
  72. package/dist/src/semantics/activity.d.ts +4 -0
  73. package/dist/src/semantics/activity.js +161 -0
  74. package/dist/src/semantics/apps.d.ts +2 -0
  75. package/dist/src/semantics/apps.js +144 -0
  76. package/dist/src/semantics/branches.d.ts +2 -0
  77. package/dist/src/semantics/branches.js +136 -0
  78. package/dist/src/semantics/checks.d.ts +2 -0
  79. package/dist/src/semantics/checks.js +176 -0
  80. package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
  81. package/dist/src/semantics/code-scanning-upload.js +97 -0
  82. package/dist/src/semantics/codespaces.d.ts +2 -0
  83. package/dist/src/semantics/codespaces.js +58 -0
  84. package/dist/src/semantics/commits.d.ts +2 -0
  85. package/dist/src/semantics/commits.js +109 -0
  86. package/dist/src/semantics/contents.d.ts +2 -0
  87. package/dist/src/semantics/contents.js +131 -0
  88. package/dist/src/semantics/deployments.d.ts +2 -0
  89. package/dist/src/semantics/deployments.js +127 -0
  90. package/dist/src/semantics/gists.d.ts +2 -0
  91. package/dist/src/semantics/gists.js +86 -0
  92. package/dist/src/semantics/git.d.ts +2 -0
  93. package/dist/src/semantics/git.js +235 -0
  94. package/dist/src/semantics/graphql.d.ts +7 -0
  95. package/dist/src/semantics/graphql.js +512 -0
  96. package/dist/src/semantics/index.d.ts +5 -0
  97. package/dist/src/semantics/index.js +62 -0
  98. package/dist/src/semantics/issues.d.ts +2 -0
  99. package/dist/src/semantics/issues.js +456 -0
  100. package/dist/src/semantics/keys.d.ts +2 -0
  101. package/dist/src/semantics/keys.js +66 -0
  102. package/dist/src/semantics/labels.d.ts +2 -0
  103. package/dist/src/semantics/labels.js +100 -0
  104. package/dist/src/semantics/meta.d.ts +12 -0
  105. package/dist/src/semantics/meta.js +144 -0
  106. package/dist/src/semantics/notifications.d.ts +4 -0
  107. package/dist/src/semantics/notifications.js +62 -0
  108. package/dist/src/semantics/orgs.d.ts +2 -0
  109. package/dist/src/semantics/orgs.js +420 -0
  110. package/dist/src/semantics/packages.d.ts +2 -0
  111. package/dist/src/semantics/packages.js +55 -0
  112. package/dist/src/semantics/pages.d.ts +2 -0
  113. package/dist/src/semantics/pages.js +176 -0
  114. package/dist/src/semantics/projects.d.ts +2 -0
  115. package/dist/src/semantics/projects.js +239 -0
  116. package/dist/src/semantics/pulls.d.ts +2 -0
  117. package/dist/src/semantics/pulls.js +462 -0
  118. package/dist/src/semantics/push-reactions.d.ts +101 -0
  119. package/dist/src/semantics/push-reactions.js +513 -0
  120. package/dist/src/semantics/releases.d.ts +19 -0
  121. package/dist/src/semantics/releases.js +231 -0
  122. package/dist/src/semantics/repo-invitations.d.ts +2 -0
  123. package/dist/src/semantics/repo-invitations.js +98 -0
  124. package/dist/src/semantics/repos.d.ts +17 -0
  125. package/dist/src/semantics/repos.js +390 -0
  126. package/dist/src/semantics/rulesets.d.ts +2 -0
  127. package/dist/src/semantics/rulesets.js +111 -0
  128. package/dist/src/semantics/search.d.ts +2 -0
  129. package/dist/src/semantics/search.js +84 -0
  130. package/dist/src/semantics/security.d.ts +2 -0
  131. package/dist/src/semantics/security.js +177 -0
  132. package/dist/src/semantics/shared.d.ts +61 -0
  133. package/dist/src/semantics/shared.js +151 -0
  134. package/dist/src/semantics/users.d.ts +2 -0
  135. package/dist/src/semantics/users.js +255 -0
  136. package/dist/src/semantics/webhooks.d.ts +2 -0
  137. package/dist/src/semantics/webhooks.js +107 -0
  138. package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
  139. package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
  140. package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
  141. package/dist/test-fixtures/github-comment-schema.json +702 -0
  142. package/dist/test-fixtures/github-known-deviations.json +94 -0
  143. package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
  144. package/dist/test-fixtures/github-openapi-operations.json +362 -0
  145. package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
  146. package/dist/test-fixtures/github-pull-schema.json +3601 -0
  147. package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
  148. package/dist/test-fixtures/github-review-schema.json +236 -0
  149. package/package.json +20 -11
  150. package/src/cli.ts +7 -6
  151. package/src/generated/graphql-sdl.gen.json +1 -0
  152. package/src/generated/graphql.gen.json +1 -0
  153. package/src/generated/surface.gen.json +1 -0
  154. package/src/generated/ui.gen.json +1 -0
  155. package/src/github-a11y-snapshot.uitest.ts +5 -5
  156. package/src/github-budget.ts +4 -4
  157. package/src/github-capabilities.ts +1993 -556
  158. package/src/github-conformance.ts +12 -7
  159. package/src/github-connector.ts +108 -96
  160. package/src/github-events.ts +223 -95
  161. package/src/github-git-http.ts +47 -83
  162. package/src/github-git-plane.ts +247 -385
  163. package/src/github-journey.uitest.ts +28 -44
  164. package/src/github-mirror-state.ts +23 -61
  165. package/src/github-mirror-ui.ts +16 -10
  166. package/src/github-server.ts +158 -60
  167. package/src/github-shared.ts +1 -1
  168. package/src/github-twin.ts +1525 -4494
  169. package/src/github-ui-conformance.ts +7 -8
  170. package/src/github-ui-structure.ts +19 -18
  171. package/src/graphql-wire.ts +115 -0
  172. package/src/index.ts +47 -7
  173. package/src/manifest.ts +605 -0
  174. package/src/npm-registry.ts +43 -0
  175. package/src/screens/app-installation.tsx +217 -0
  176. package/src/screens/app-manifest.tsx +97 -0
  177. package/src/screens/oauth.tsx +258 -0
  178. package/src/screens/session.tsx +186 -0
  179. package/src/semantics/actions.ts +395 -0
  180. package/src/semantics/activity.ts +171 -0
  181. package/src/semantics/apps.ts +133 -0
  182. package/src/semantics/branches.ts +133 -0
  183. package/src/semantics/checks.ts +163 -0
  184. package/src/semantics/code-scanning-upload.ts +92 -0
  185. package/src/semantics/codespaces.ts +58 -0
  186. package/src/semantics/commits.ts +109 -0
  187. package/src/semantics/contents.ts +112 -0
  188. package/src/semantics/deployments.ts +116 -0
  189. package/src/semantics/gists.ts +85 -0
  190. package/src/semantics/git.ts +226 -0
  191. package/src/semantics/graphql.ts +505 -0
  192. package/src/semantics/index.ts +68 -0
  193. package/src/semantics/issues.ts +434 -0
  194. package/src/semantics/keys.ts +66 -0
  195. package/src/semantics/labels.ts +91 -0
  196. package/src/semantics/meta.ts +139 -0
  197. package/src/semantics/notifications.ts +58 -0
  198. package/src/semantics/orgs.ts +383 -0
  199. package/src/semantics/packages.ts +59 -0
  200. package/src/semantics/pages.ts +154 -0
  201. package/src/semantics/projects.ts +246 -0
  202. package/src/semantics/pulls.ts +421 -0
  203. package/src/semantics/push-reactions.ts +471 -0
  204. package/src/semantics/releases.ts +213 -0
  205. package/src/semantics/repo-invitations.ts +112 -0
  206. package/src/semantics/repos.ts +373 -0
  207. package/src/semantics/rulesets.ts +107 -0
  208. package/src/semantics/search.ts +83 -0
  209. package/src/semantics/security.ts +153 -0
  210. package/src/semantics/shared.ts +181 -0
  211. package/src/semantics/users.ts +252 -0
  212. package/src/semantics/webhooks.ts +101 -0
  213. package/test-fixtures/github-known-deviations.json +7 -8
  214. package/test-fixtures/github-openapi-operations.json +46 -227
  215. package/src/github-graphql.ts +0 -398
@@ -0,0 +1,390 @@
1
+ // A repository itself: create (with its git plane and default branch), read, edit, delete, fork; its
2
+ // topics, forks, languages, contributors, traffic, archive redirects, the comparison of two refs, and
3
+ // its collaborators and assignable users. Repository writes announce the repository, fork and
4
+ // member webhooks.
5
+ import { Buffer } from 'node:buffer';
6
+ import { GITHUB_WORLD_USER, defaultBranchCommits, mergeBaseInPlane, planeAncestors, planeCommitBody, resolvePlaneRef, deleteBareRepo, diffFilesInPlane, hasBareRepo, ensureBareRepo, forkBareRepo, githubNotFound, isValidBranchName, listGitPlaneRefs, paginate, seedUser, setBareRepoHead, githubApps, toGithubAppInstallation, toGithubCollaborator, toGithubRepo, toGithubUser, updateGitRefInPlane, writeGitBlobToPlane, writeGitCommitToPlane, writeGitTreeToPlane, } from "../github-twin.js";
7
+ import { announce, asView, gh, identityOf, nextSeq, put, send } from "./shared.js";
8
+ import { gitignoreTemplate, licenseTemplate } from "./meta.js";
9
+ import { emitGithubAppEvent } from "../github-events.js";
10
+ const repoOf = (ctx) => {
11
+ const { repo, state } = gh(ctx);
12
+ return state().repos.find((x) => x.full_name === repo);
13
+ };
14
+ /** The name a license's copyright line gives an account: an organization's name, a person's profile name, else the login. */
15
+ function holderOf(g, owner) {
16
+ const org = g.state().orgs.find((o) => o.login === owner);
17
+ if (org)
18
+ return String(org.name ?? org.login);
19
+ return owner === (g.caller ?? 'octocat') ? identityOf(g).name : owner;
20
+ }
21
+ /** Create a repository for `owner` (+ its git plane and default branch). 422 for a name GitHub
22
+ * refuses (it would be unservable as a plane path) or one that exists. */
23
+ async function createRepo(ctx, owner) {
24
+ const { body, occurredAt, root, state } = gh(ctx);
25
+ if (!body.name)
26
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Repository', field: 'name', code: 'missing_field' }] }, 422);
27
+ const name = String(body.name);
28
+ if (!/^[A-Za-z0-9_.-]+$/.test(name) || name === '.' || name === '..' || !/^[A-Za-z0-9_.-]+$/.test(owner))
29
+ return invalidRepository(ctx, 'name');
30
+ const full = `${owner}/${name}`;
31
+ if (state().repos.some((r) => r.full_name === full))
32
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Repository', code: 'already_exists', field: 'name' }] }, 422);
33
+ const seq = nextSeq(ctx, 'repo');
34
+ const fields = { owner, name, description: body.description ?? null, private: body.private === true, default_branch: body.default_branch ?? 'main', has_issues: body.has_issues ?? true, has_projects: body.has_projects ?? true, has_wiki: body.has_wiki ?? true, has_discussions: body.has_discussions ?? false, homepage: body.homepage ?? null, created_at: occurredAt, updated_at: occurredAt };
35
+ await put(ctx, { operation: 'repository.create', subjectType: 'repository', subjectId: `repo:${seq}`, fields });
36
+ // the new repository is at once clonable (empty) and pushable by an unmodified git client; it has no
37
+ // branch until something is committed, unless auto_init or a template makes the initial commit on the default
38
+ // branch as the signed-in person: a README for auto_init, the .gitignore and LICENSE templates named
39
+ // (docs.github.com/rest/repos/repos#create-a-repository-for-the-authenticated-user). That a template alone makes
40
+ // the commit, with no README, is github.com's behaviour as its new-repository page shows it; the REST reference
41
+ // does not say.
42
+ const branch = String(fields.default_branch);
43
+ ensureBareRepo(full, root, branch);
44
+ const files = [
45
+ ['README.md', body.auto_init === true ? `# ${name}\n${body.description ? `\n${String(body.description)}\n` : ''}` : undefined],
46
+ ['.gitignore', typeof body.gitignore_template === 'string' ? gitignoreTemplate(body.gitignore_template) : undefined],
47
+ // the license with its year and holder filled, as GitHub commits it: the year made, and the owning account's name
48
+ // (the organization's, or the person's) (docs.github.com/rest/licenses/licenses#get-a-license, its implementation)
49
+ ['LICENSE', typeof body.license_template === 'string' ? licenseTemplate(body.license_template, { year: String(occurredAt).slice(0, 4), fullname: holderOf(gh(ctx), owner) }) : undefined],
50
+ ];
51
+ const initial = files.filter((f) => f[1] !== undefined);
52
+ if (initial.length) {
53
+ const entries = await Promise.all(initial.map(async ([path, text]) => ({ path, mode: '100644', type: 'blob', sha: await writeGitBlobToPlane(full, Buffer.from(text).toString('base64'), root) })));
54
+ const tree = await writeGitTreeToPlane(full, entries, undefined, root);
55
+ const me = identityOf(gh(ctx));
56
+ const commit = await writeGitCommitToPlane(full, { message: 'Initial commit', treeSha: tree, parents: [], author: me, committer: me, occurredAt }, root);
57
+ await updateGitRefInPlane(full, `refs/heads/${branch}`, commit, root, undefined);
58
+ await put(ctx, { operation: 'git_ref.update', subjectType: 'git_ref', subjectId: `gitref:${full}#refs/heads/${branch}`, fields: { repository: full, ref: `refs/heads/${branch}`, object_sha: commit, object_type: 'commit' } });
59
+ await put(ctx, { operation: 'branch.create', subjectType: 'branch', subjectId: `branch:${full}#${branch}`, fields: { repository: full, name: branch, commit_sha: commit } });
60
+ // each file's receipt, as a Contents write keeps one (semantics/contents.ts): the repository's facts read them
61
+ for (const [i, [path, text]] of initial.entries()) {
62
+ const content_b64 = Buffer.from(text).toString('base64');
63
+ await put(ctx, { operation: 'content_file.create', subjectType: 'content_file', subjectId: `content:${full}#${branch}:${path}`, fields: { repository: full, path, branch, sha: entries[i].sha, content_b64, size: text.length, deleted: false } });
64
+ }
65
+ }
66
+ // every new repository has GitHub's default labels (docs.github.com/issues/using-labels-and-milestones-to-track-work/
67
+ // managing-labels#about-default-labels)
68
+ for (const [label, color, description] of DEFAULT_LABELS) {
69
+ await put(ctx, { operation: 'label.create', subjectType: 'label', subjectId: `label:${nextSeq(ctx, 'label')}`, fields: { repository: full, name: label, color, description, default: true } });
70
+ }
71
+ const answer = toGithubRepo(state().repos.find((x) => x.full_name === full));
72
+ await announce(ctx, [{ event: 'repository', action: 'created', repository: full, number: 0 }]);
73
+ return ctx.reply(answer, 201);
74
+ }
75
+ /** GitHub's default labels, with their colours and descriptions as github.com shows them. */
76
+ const DEFAULT_LABELS = [
77
+ ['bug', 'd73a4a', "Something isn't working"], ['documentation', '0075ca', 'Improvements or additions to documentation'],
78
+ ['duplicate', 'cfd3d7', 'This issue or pull request already exists'], ['enhancement', 'a2eeef', 'New feature or request'],
79
+ ['good first issue', '7057ff', 'Good for newcomers'], ['help wanted', '008672', 'Extra attention is needed'],
80
+ ['invalid', 'e4e669', "This doesn't seem right"], ['question', 'd876e3', 'Further information is requested'],
81
+ ['wontfix', 'ffffff', 'This will not be worked on'],
82
+ ];
83
+ // The fields a subject names its repository by: its own, a fork's parent, a project card's content, the container
84
+ // of an Actions secret or variable, a notification's subject, a pull request's or a run's head repository.
85
+ const NAMED_BY = ['repository', 'parent_full_name', 'content_repository', 'container', 'subject_url', 'head_repo', 'head_repository'];
86
+ /** `text` with the repository `from` named `to` wherever it stands as a whole name (`octocat/tessellate`, never the
87
+ * start of `octocat/tessellate.dev`). */
88
+ const renamed = (text, from, to) => text.replace(new RegExp(`(?<![A-Za-z0-9_.-])${from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_.-])`, 'g'), to);
89
+ /** Everything a repository holds moves with it to its new name (docs.github.com/rest/repos/repos#transfer-a-repository:
90
+ * a transfer keeps the issues, pull requests, releases, stars and history). A subject whose id names the repository
91
+ * is written anew under the new name and the old id points there (`moved_to`, which the projection follows); any
92
+ * other subject has its naming fields rewritten in place. The git plane's refs move to the new name (the objects are
93
+ * the World's, shared). The previous owner's teams lose their access. */
94
+ async function moveRepository(ctx, from, to) {
95
+ const { root } = gh(ctx);
96
+ for (const row of ctx.tree()) {
97
+ if (row.deleted === true || typeof row.moved_to === 'string' || row.type === 'repository' || row.type === 'webhook_delivery')
98
+ continue;
99
+ const type = String(row.type);
100
+ const id = String(row.id);
101
+ if (type === 'team_repo' && row.repository === from) {
102
+ await put(ctx, { operation: 'repository.transfer', subjectType: type, subjectId: id, fields: { deleted: true } });
103
+ continue;
104
+ }
105
+ const fields = ctx.own(row);
106
+ delete fields.id;
107
+ delete fields.type;
108
+ delete fields.updatedAt;
109
+ const changed = Object.fromEntries(NAMED_BY.filter((k) => typeof fields[k] === 'string' && renamed(String(fields[k]), from, to) !== fields[k]).map((k) => [k, renamed(String(fields[k]), from, to)]));
110
+ const newId = renamed(id, from, to);
111
+ if (newId !== id) {
112
+ // an issue, pull request or environment keeps the id it was given where it was made
113
+ const born = ['issue', 'pull_request', 'environment'].includes(type) ? { born_in: fields.born_in ?? from } : {};
114
+ await put(ctx, { operation: 'repository.transfer', subjectType: type, subjectId: newId, fields: { ...fields, ...changed, ...born } });
115
+ await put(ctx, { operation: 'repository.transfer', subjectType: type, subjectId: id, fields: { deleted: true, moved_to: newId } });
116
+ }
117
+ else if (Object.keys(changed).length) {
118
+ await put(ctx, { operation: 'repository.transfer', subjectType: type, subjectId: id, fields: changed });
119
+ }
120
+ }
121
+ forkBareRepo(from, to, root);
122
+ deleteBareRepo(from, root);
123
+ }
124
+ /** A transfer to a person waits for them to accept it from an email GitHub sends them; until then the repository
125
+ * stays where it is and the answer names its original owner (docs.github.com/rest/repos/repos#transfer-a-repository). */
126
+ const transferAwaitingAcceptance = (ctx, r) => ctx.reply(toGithubRepo(r), 202);
127
+ /** GitHub's refusal of a repository name or default branch that is not one. */
128
+ function invalidRepository(ctx, field) {
129
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Repository', field, code: 'invalid' }] }, 422);
130
+ }
131
+ /** GET .../tarball|zipball/:ref → 302 to codeload (the bytes are not modeled). */
132
+ const archive = (fmt) => async (ctx) => {
133
+ const { repo, seg } = gh(ctx);
134
+ if (!repoOf(ctx))
135
+ return send(ctx, githubNotFound());
136
+ return ctx.reply({ location: `https://codeload.github.com/${repo}/legacy.${fmt}/${decodeURIComponent(seg[4])}` }, 302);
137
+ };
138
+ /** Traffic is not observable offline: an honest zero-history answer for a repository that exists. */
139
+ const traffic = (body) => async (ctx) => (repoOf(ctx) ? ctx.reply(body) : send(ctx, githubNotFound()));
140
+ /** The people who can be assigned in a repository (docs.github.com/rest/issues/assignees). */
141
+ export function assignable(st, repo) {
142
+ const owner = repo.split('/')[0];
143
+ const isOrg = st.orgs.some((o) => o.login === owner);
144
+ const people = new Set([...(isOrg ? st.orgMembers.filter((m) => m.org === owner && m.state === 'active').map((m) => m.login) : [owner]), ...st.collaborators.filter((c) => c.repository === repo).map((c) => c.login)]);
145
+ return [...people].sort();
146
+ }
147
+ export function reposSemantics() {
148
+ const reposGet = async (ctx) => {
149
+ const r = repoOf(ctx);
150
+ return send(ctx, r ? { status: 200, body: toGithubRepo(r) } : githubNotFound());
151
+ };
152
+ const reposDelete = async (ctx) => {
153
+ const { repo, root } = gh(ctx);
154
+ const r0 = repoOf(ctx);
155
+ if (!r0)
156
+ return send(ctx, githubNotFound());
157
+ await put(ctx, { operation: 'repository.delete', subjectType: 'repository', subjectId: `repo:${r0.id}`, fields: { deleted: true, owner: r0.owner, name: r0.name } });
158
+ deleteBareRepo(repo, root);
159
+ await announce(ctx, [{ event: 'repository', action: 'deleted', repository: repo, number: 0 }]);
160
+ return send(ctx, { status: 204, body: undefined });
161
+ };
162
+ const reposListForks = async (ctx) => {
163
+ const { repo, query, state } = gh(ctx);
164
+ return ctx.reply(paginate(state().repos.filter((x) => x.fork && x.parent_full_name === repo), query).map((r) => asView('minimal-repository', toGithubRepo(r))));
165
+ };
166
+ const reposReplaceAllTopics = async (ctx) => {
167
+ const { body, occurredAt } = gh(ctx);
168
+ const r0 = repoOf(ctx);
169
+ if (!r0)
170
+ return send(ctx, githubNotFound());
171
+ const names = Array.isArray(body.names) ? body.names.map(String) : [];
172
+ await put(ctx, { operation: 'repository.set_topics', subjectType: 'repository', subjectId: `repo:${r0.id}`, fields: { owner: r0.owner, name: r0.name, topics: names, updated_at: occurredAt } });
173
+ return ctx.reply({ names });
174
+ };
175
+ const reposListLanguages = async (ctx) => {
176
+ const { repo, state } = gh(ctx);
177
+ const defaultBranch = repoOf(ctx)?.default_branch ?? 'main';
178
+ const extLang = { ts: 'TypeScript', tsx: 'TypeScript', js: 'JavaScript', jsx: 'JavaScript', py: 'Python', go: 'Go', rs: 'Rust', java: 'Java', rb: 'Ruby', css: 'CSS', html: 'HTML', md: 'Markdown', json: 'JSON', sh: 'Shell' };
179
+ const totals = {};
180
+ for (const c of state().contents.filter((x) => x.repository === repo && (x.branch ?? defaultBranch) === defaultBranch)) {
181
+ const lang = extLang[c.path.split('.').pop()?.toLowerCase() ?? ''];
182
+ if (lang)
183
+ totals[lang] = (totals[lang] ?? 0) + (c.size ?? 0);
184
+ }
185
+ return ctx.reply(totals);
186
+ };
187
+ const reposListContributors = async (ctx) => {
188
+ const { repo, query, state, root } = gh(ctx);
189
+ const counts = new Map();
190
+ for (const c of await defaultBranchCommits(state(), repo, root))
191
+ if (c.author)
192
+ counts.set(c.author, (counts.get(c.author) ?? 0) + 1);
193
+ const items = [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([login, contributions]) => asView('contributor', { ...toGithubUser(seedUser(login)), contributions }));
194
+ return ctx.reply(paginate(items, query));
195
+ };
196
+ const reposCompareCommits = async (ctx) => {
197
+ const { repo, seg, root, state } = gh(ctx);
198
+ const basehead = decodeURIComponent(seg.slice(4).join('/'));
199
+ const [base, head] = basehead.split('...');
200
+ const branch = state().repos.find((r) => r.full_name === repo)?.default_branch ?? 'main';
201
+ const baseSha = base ? await resolvePlaneRef(repo, base, branch, root) : null;
202
+ const headSha = head ? await resolvePlaneRef(repo, head, branch, root) : null;
203
+ if (!baseSha || !headSha)
204
+ return send(ctx, githubNotFound());
205
+ const [ofBase, ofHead] = [await planeAncestors(repo, baseSha, root), await planeAncestors(repo, headSha, root)];
206
+ const ahead = [...ofHead.keys()].filter((c) => !ofBase.has(c)).reverse();
207
+ const behind = [...ofBase.keys()].filter((c) => !ofHead.has(c));
208
+ const mergeBase = (await mergeBaseInPlane(repo, baseSha, headSha, root)) ?? baseSha;
209
+ const diff = (await diffFilesInPlane(repo, mergeBase, headSha, root, { patch: true })) ?? [];
210
+ const bodies = [];
211
+ for (const c of ahead) {
212
+ const b = await planeCommitBody(repo, c, root);
213
+ if (b)
214
+ bodies.push(b);
215
+ }
216
+ return ctx.reply({
217
+ url: `https://api.github.com/repos/${repo}/compare/${basehead}`, html_url: `https://github.com/${repo}/compare/${basehead}`,
218
+ status: ahead.length && behind.length ? 'diverged' : ahead.length ? 'ahead' : behind.length ? 'behind' : 'identical',
219
+ ahead_by: ahead.length, behind_by: behind.length, total_commits: ahead.length,
220
+ base_commit: await planeCommitBody(repo, baseSha, root), merge_base_commit: await planeCommitBody(repo, mergeBase, root),
221
+ commits: bodies,
222
+ files: diff.map((f) => ({ sha: f.sha ?? null, filename: f.filename, status: f.status, additions: f.additions, deletions: f.deletions, changes: f.additions + f.deletions,
223
+ blob_url: `https://github.com/${repo}/blob/${headSha}/${f.filename}`, raw_url: `https://github.com/${repo}/raw/${headSha}/${f.filename}`, contents_url: `https://api.github.com/repos/${repo}/contents/${f.filename}?ref=${headSha}`, ...(f.patch !== undefined ? { patch: f.patch } : {}) })),
224
+ });
225
+ };
226
+ const reposListCollaborators = async (ctx) => {
227
+ const { repo, query, state } = gh(ctx);
228
+ return ctx.reply(paginate(state().collaborators.filter((c) => c.repository === repo), query).map(toGithubCollaborator));
229
+ };
230
+ const reposCheckCollaborator = async (ctx) => {
231
+ const { repo, seg, state } = gh(ctx);
232
+ const login = decodeURIComponent(seg[4]);
233
+ return send(ctx, state().collaborators.some((x) => x.repository === repo && x.login === login) ? { status: 204, body: undefined } : githubNotFound());
234
+ };
235
+ const reposGetCollaboratorPermissionLevel = async (ctx) => {
236
+ const { repo, seg, state } = gh(ctx);
237
+ const login = decodeURIComponent(seg[4]);
238
+ // `permission` is the legacy base role (maintain maps to write, triage to read), `role_name` the role itself
239
+ // (docs.github.com/rest/collaborators/collaborators#get-repository-permissions-for-a-user)
240
+ const c = state().collaborators.find((x) => x.repository === repo && x.login === login);
241
+ const role = { push: 'write', pull: 'read' }[c?.permission ?? ''] ?? c?.permission ?? 'none';
242
+ const permission = { maintain: 'write', triage: 'read' }[role] ?? role;
243
+ return ctx.reply({ permission, role_name: role, user: c ? toGithubCollaborator(c) : null });
244
+ };
245
+ const reposRemoveCollaborator = async (ctx) => {
246
+ const { repo, seg, state } = gh(ctx);
247
+ const login = decodeURIComponent(seg[4]);
248
+ if (!state().collaborators.find((c) => c.repository === repo && c.login === login))
249
+ return send(ctx, githubNotFound());
250
+ await put(ctx, { operation: 'collaborator.remove', subjectType: 'collaborator', subjectId: `collab:${repo}#${login}`, fields: { deleted: true, repository: repo, login } });
251
+ await announce(ctx, [{ event: 'member', action: 'removed', repository: repo, number: 0 }]);
252
+ return send(ctx, { status: 204, body: undefined });
253
+ };
254
+ const issuesListAssignees = async (ctx) => {
255
+ const { repo, query, state } = gh(ctx);
256
+ return ctx.reply(paginate(assignable(state(), repo).map((l) => asView('simple-user', toGithubUser(seedUser(l)))), query));
257
+ };
258
+ const issuesCheckUserCanBeAssigned = async (ctx) => {
259
+ const { repo, seg, state } = gh(ctx);
260
+ return send(ctx, assignable(state(), repo).includes(decodeURIComponent(seg[4])) ? { status: 204, body: undefined } : githubNotFound());
261
+ };
262
+ return {
263
+ 'repos/get': reposGet,
264
+ 'repos/create-for-authenticated-user': async (ctx) => createRepo(ctx, 'octocat'),
265
+ 'repos/create-in-org': async (ctx) => createRepo(ctx, String(gh(ctx).seg[1])),
266
+ // A default_branch change re-points the plane's HEAD too (a clone checks out the new one); a name
267
+ // git would refuse is GitHub's 422, checked before anything is written.
268
+ 'repos/update': async (ctx) => {
269
+ const { repo, body, occurredAt, root, state } = gh(ctx);
270
+ const r0 = repoOf(ctx);
271
+ if (!r0)
272
+ return send(ctx, githubNotFound());
273
+ const fields = { owner: r0.owner, name: r0.name, updated_at: occurredAt };
274
+ for (const k of ['description', 'private', 'default_branch', 'homepage', 'has_issues', 'has_projects', 'has_wiki', 'has_discussions', 'archived']) {
275
+ if (body[k] !== undefined)
276
+ fields[k] = body[k];
277
+ }
278
+ if (body.name !== undefined)
279
+ fields.name = String(body.name);
280
+ // a new name follows creation's rule; the default branch must be one the repository has
281
+ if (body.name !== undefined && (!/^[A-Za-z0-9_.-]+$/.test(String(body.name)) || ['.', '..'].includes(String(body.name))))
282
+ return invalidRepository(ctx, 'name');
283
+ if (body.default_branch !== undefined && (!isValidBranchName(String(body.default_branch)) || (hasBareRepo(repo, root) && !(await listGitPlaneRefs(repo, root)).some((r) => r.ref === `refs/heads/${String(body.default_branch)}`))))
284
+ return invalidRepository(ctx, 'default_branch');
285
+ await put(ctx, { operation: 'repository.update', subjectType: 'repository', subjectId: `repo:${r0.id}`, fields });
286
+ if (body.default_branch !== undefined)
287
+ setBareRepoHead(repo, String(body.default_branch), root);
288
+ const answer = toGithubRepo(state().repos.find((x) => x.id === r0.id));
289
+ await announce(ctx, [{ event: 'repository', action: 'edited', repository: repo, number: 0 }]);
290
+ return ctx.reply(answer);
291
+ },
292
+ // the plane dies with the repository: a later re-create must not inherit its history
293
+ 'repos/delete': reposDelete,
294
+ // A fork copies the parent's object store and refs (what GitHub does server-side), and the copied
295
+ // refs get their rows, so REST and a clone agree. 202.
296
+ 'repos/create-fork': async (ctx) => {
297
+ const { repo, seg, body, occurredAt, root, state, caller } = gh(ctx);
298
+ const parent = repoOf(ctx);
299
+ // the fork is the forker's, or the organization's they name
300
+ const owner = String(body.organization ?? caller ?? GITHUB_WORLD_USER);
301
+ const name = String(body.name ?? seg[2]);
302
+ if (!/^[A-Za-z0-9_.-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(name) || ['.', '..'].includes(owner) || ['.', '..'].includes(name))
303
+ return invalidRepository(ctx, 'name');
304
+ const full = `${owner}/${name}`;
305
+ const seq = nextSeq(ctx, 'repo');
306
+ const fields = { owner, name, fork: true, parent_full_name: repo, description: parent?.description ?? null, default_branch: parent?.default_branch ?? 'main', created_at: occurredAt, updated_at: occurredAt };
307
+ await put(ctx, { operation: 'repository.fork', subjectType: 'repository', subjectId: `repo:${seq}`, fields });
308
+ forkBareRepo(repo, full, root, String(fields.default_branch));
309
+ for (const planeRef of await listGitPlaneRefs(full, root)) {
310
+ await put(ctx, { operation: 'git_ref.create', subjectType: 'git_ref', subjectId: `gitref:${full}#${planeRef.ref}`, fields: { repository: full, ref: planeRef.ref, object_sha: planeRef.object_sha, object_type: planeRef.object_type === 'tag' ? 'tag' : 'commit' } });
311
+ if (planeRef.ref.startsWith('refs/heads/')) {
312
+ const bn = planeRef.ref.slice('refs/heads/'.length);
313
+ await put(ctx, { operation: 'branch.create', subjectType: 'branch', subjectId: `branch:${full}#${bn}`, fields: { repository: full, name: bn, commit_sha: planeRef.object_sha } });
314
+ }
315
+ }
316
+ const answer = toGithubRepo(state().repos.find((x) => x.full_name === full));
317
+ await announce(ctx, [{ event: 'fork', action: 'created', repository: repo, number: 0 }]);
318
+ return ctx.reply(answer, 202);
319
+ },
320
+ // Transfer to an organization the caller may create repositories in: at once, under the organization, with
321
+ // everything the repository holds (docs.github.com/rest/repos/repos#transfer-a-repository). Only an admin of the
322
+ // repository transfers it: its owner, or an owner of the organization that owns it.
323
+ 'repos/transfer': async (ctx) => {
324
+ const { repo, body, caller, occurredAt, state } = gh(ctx);
325
+ const r0 = repoOf(ctx);
326
+ if (!r0)
327
+ return send(ctx, githubNotFound());
328
+ const me = caller ?? GITHUB_WORLD_USER;
329
+ const st = state();
330
+ const admin = r0.owner === me || st.orgMembers.some((m) => m.org === r0.owner && m.login === me && m.role === 'admin' && m.state === 'active');
331
+ const newOwner = String(body.new_owner ?? '');
332
+ const name = String(body.new_name ?? r0.name);
333
+ if (!admin)
334
+ return ctx.reply({ message: 'You must be an admin to transfer a repository.', documentation_url: 'https://docs.github.com/rest/repos/repos#transfer-a-repository' }, 403);
335
+ if (!st.orgs.some((o) => o.login === newOwner))
336
+ return transferAwaitingAcceptance(ctx, r0);
337
+ if (!st.orgMembers.some((m) => m.org === newOwner && m.login === me && m.state === 'active') || st.repos.some((x) => x.full_name === `${newOwner}/${name}`) || !/^[A-Za-z0-9_.-]+$/.test(name))
338
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Repository', field: 'new_owner', code: 'invalid' }] }, 422);
339
+ const to = `${newOwner}/${name}`;
340
+ await moveRepository(ctx, repo, to);
341
+ await put(ctx, { operation: 'repository.transfer', subjectType: 'repository', subjectId: `repo:${r0.id}`, fields: { owner: newOwner, name, updated_at: occurredAt } });
342
+ for (const fork of state().repos.filter((x) => x.parent_full_name === repo))
343
+ await put(ctx, { operation: 'repository.transfer', subjectType: 'repository', subjectId: `repo:${fork.id}`, fields: { parent_full_name: to } });
344
+ await announce(ctx, [{ event: 'repository', action: 'transferred', repository: to, number: 0 }]);
345
+ // the repository leaves the previous owner's app installations that had it selected, and each app hears so
346
+ // (docs.github.com/webhooks/webhook-events-and-payloads#installation_repositories)
347
+ for (const i of state().appInstallations.filter((x) => x.account_login === r0.owner && x.repository_selection === 'selected' && (x.repositories ?? []).includes(repo))) {
348
+ await put(ctx, { operation: 'app_installation.remove_repository', subjectType: 'app_installation', subjectId: `installation:${i.id}`, fields: { repositories: (i.repositories ?? []).filter((n) => n !== repo), updated_at: occurredAt } });
349
+ const app = githubApps(ctx.root).find((a) => a.slug === i.app_slug);
350
+ const now = state().appInstallations.find((x) => x.id === i.id);
351
+ if (app) {
352
+ try {
353
+ await emitGithubAppEvent(app, 'installation_repositories', { action: 'removed', installation: toGithubAppInstallation(now), repository_selection: 'selected', repositories_added: [], repositories_removed: [{ id: r0.id, name: r0.name, full_name: repo, private: r0.private ?? false }], sender: { login: caller ?? GITHUB_WORLD_USER, type: 'User' } }, { occurredAt, ...(ctx.root !== undefined ? { root: ctx.root } : {}) });
354
+ }
355
+ catch { /* a delivery failure never fails the transfer */ }
356
+ }
357
+ }
358
+ return ctx.reply(toGithubRepo(state().repos.find((x) => x.id === r0.id)), 202);
359
+ },
360
+ // repos with fork:true whose parent is this one
361
+ 'repos/list-forks': reposListForks,
362
+ 'repos/get-all-topics': async (ctx) => ctx.reply({ names: repoOf(ctx)?.topics ?? [] }),
363
+ 'repos/replace-all-topics': reposReplaceAllTopics,
364
+ // bytes per language, from the default branch's files (their extensions and sizes); {} when none
365
+ 'repos/list-languages': reposListLanguages,
366
+ // commit counts per author on the default branch's history
367
+ 'repos/list-contributors': reposListContributors,
368
+ // Two refs compared on the repository's history (three-dot: from their merge base): the commits the head has that
369
+ // the base lacks (oldest first), how far each is ahead and behind, and the files changed since the merge base
370
+ // (docs.github.com/rest/commits/commits#compare-two-commits). A pair that does not resolve is not found.
371
+ 'repos/compare-commits': reposCompareCommits,
372
+ 'repos/get-views': traffic({ count: 0, uniques: 0, views: [] }),
373
+ 'repos/get-clones': traffic({ count: 0, uniques: 0, clones: [] }),
374
+ 'repos/get-top-referrers': traffic([]),
375
+ 'repos/get-top-paths': traffic([]),
376
+ 'repos/download-tarball-archive': archive('tar.gz'),
377
+ 'repos/download-zipball-archive': archive('zip'),
378
+ 'repos/list-collaborators': reposListCollaborators,
379
+ // 204 when a collaborator, else 404
380
+ 'repos/check-collaborator': reposCheckCollaborator,
381
+ 'repos/get-collaborator-permission-level': reposGetCollaboratorPermissionLevel,
382
+ // GitHub answers a new collaborator with an invitation (201)
383
+ // adding a collaborator is semantics/repo-invitations.ts: it invites them
384
+ 'repos/remove-collaborator': reposRemoveCollaborator,
385
+ // assignable users are the repository's collaborators
386
+ // who can be assigned: the repository's owner when a person, its collaborators, and its organization's members
387
+ 'issues/list-assignees': issuesListAssignees,
388
+ 'issues/check-user-can-be-assigned': issuesCheckUserCanBeAssigned,
389
+ };
390
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare function rulesetsSemantics(): Record<string, Semantics>;
@@ -0,0 +1,111 @@
1
+ import { githubNotFound, observedId, paginate, recordRulesetVersion, toGithubRuleset, twinUserId } from "../github-twin.js";
2
+ import { gh, nextSeq, put, send } from "./shared.js";
3
+ const suiteOf = (repo, x, full) => ({ id: x.id, actor_id: twinUserId(x.actor_login), actor_name: x.actor_login, before_sha: x.before_sha, after_sha: x.after_sha, ref: x.ref,
4
+ repository_id: observedId(`repository:${repo}`), repository_name: repo.split('/')[1], pushed_at: x.pushed_at, result: x.result, evaluation_result: x.result === 'fail' ? 'fail' : 'pass',
5
+ ...(full ? { rule_evaluations: x.rules.map((r) => ({ rule_source: { type: 'ruleset', id: r.ruleset_id, name: r.ruleset_name }, enforcement: r.enforcement, result: r.result, rule_type: r.rule_type, details: r.details })) } : {}) });
6
+ // GitHub names each version's actor by account id and type
7
+ const versionOf = (v, full) => ({ version_id: v.version_id, actor: { id: v.actor_login ? twinUserId(v.actor_login) : null, type: 'User' }, updated_at: v.updated_at, ...(full ? { state: v.state } : {}) });
8
+ const rulesetOf = (ctx) => {
9
+ const { repo, seg, state } = gh(ctx);
10
+ return state().rulesets.find((x) => x.repository === repo && x.id === Number(decodeURIComponent(seg[4])));
11
+ };
12
+ /** Record the version a ruleset write made, through this request's context. */
13
+ const version = (ctx, id) => {
14
+ const { repo, caller, occurredAt, root } = gh(ctx);
15
+ return recordRulesetVersion(repo, id, caller ?? '', occurredAt, root, (w) => put(ctx, w));
16
+ };
17
+ export function rulesetsSemantics() {
18
+ const reposGetRepoRulesets = async (ctx) => {
19
+ const { repo, query, state } = gh(ctx);
20
+ return ctx.reply(paginate(state().rulesets.filter((r) => r.repository === repo), query).map(toGithubRuleset));
21
+ };
22
+ const reposGetRepoRuleset = async (ctx) => {
23
+ const r = rulesetOf(ctx);
24
+ return send(ctx, r ? { status: 200, body: toGithubRuleset(r) } : githubNotFound());
25
+ };
26
+ const reposGetRepoRulesetHistory = async (ctx) => {
27
+ const r = rulesetOf(ctx);
28
+ if (!r)
29
+ return send(ctx, githubNotFound());
30
+ return ctx.reply(paginate([...(r.versions ?? [])].reverse(), gh(ctx).query).map((v) => versionOf(v, false)));
31
+ };
32
+ const reposGetRepoRulesetVersion = async (ctx) => {
33
+ const r = rulesetOf(ctx);
34
+ if (!r)
35
+ return send(ctx, githubNotFound());
36
+ const v = [...(r.versions ?? [])].reverse().find((x) => x.version_id === Number(decodeURIComponent(gh(ctx).seg[6])));
37
+ return send(ctx, v ? { status: 200, body: versionOf(v, true) } : githubNotFound());
38
+ };
39
+ const reposCreateRepoRuleset = async (ctx) => {
40
+ const { repo, body, occurredAt, state } = gh(ctx);
41
+ if (!body.name)
42
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Ruleset', field: 'name', code: 'missing_field' }] }, 422);
43
+ const id = nextSeq(ctx, 'ruleset');
44
+ const fields = { repository: repo, name: String(body.name), target: String(body.target ?? 'branch'), enforcement: String(body.enforcement ?? 'active'), rules: Array.isArray(body.rules) ? body.rules : [], conditions: body.conditions, bypass_actors: Array.isArray(body.bypass_actors) ? body.bypass_actors : [], created_at: occurredAt, updated_at: occurredAt };
45
+ await put(ctx, { operation: 'ruleset.create', subjectType: 'ruleset', subjectId: `ruleset:${id}`, fields });
46
+ await version(ctx, id);
47
+ return ctx.reply(toGithubRuleset(state().rulesets.find((x) => x.id === id)), 201);
48
+ };
49
+ const reposUpdateRepoRuleset = async (ctx) => {
50
+ const { repo, body, occurredAt, state } = gh(ctx);
51
+ const existing = rulesetOf(ctx);
52
+ if (!existing)
53
+ return send(ctx, githubNotFound());
54
+ const id = existing.id;
55
+ const fields = { repository: repo, updated_at: occurredAt };
56
+ if (body.name !== undefined)
57
+ fields.name = String(body.name);
58
+ if (body.target !== undefined)
59
+ fields.target = String(body.target);
60
+ if (body.enforcement !== undefined)
61
+ fields.enforcement = String(body.enforcement);
62
+ if (body.rules !== undefined)
63
+ fields.rules = Array.isArray(body.rules) ? body.rules : [];
64
+ if (body.conditions !== undefined)
65
+ fields.conditions = body.conditions;
66
+ if (body.bypass_actors !== undefined)
67
+ fields.bypass_actors = Array.isArray(body.bypass_actors) ? body.bypass_actors : [];
68
+ await put(ctx, { operation: 'ruleset.update', subjectType: 'ruleset', subjectId: `ruleset:${id}`, fields });
69
+ await version(ctx, id);
70
+ return ctx.reply(toGithubRuleset(state().rulesets.find((x) => x.id === id)));
71
+ };
72
+ const reposDeleteRepoRuleset = async (ctx) => {
73
+ const { repo } = gh(ctx);
74
+ const existing = rulesetOf(ctx);
75
+ if (!existing)
76
+ return send(ctx, githubNotFound());
77
+ await put(ctx, { operation: 'ruleset.delete', subjectType: 'ruleset', subjectId: `ruleset:${existing.id}`, fields: { deleted: true, repository: repo } });
78
+ return send(ctx, { status: 204, body: undefined });
79
+ };
80
+ const reposGetRepoRuleSuites = async (ctx) => {
81
+ const { repo, query, occurredAt, state } = gh(ctx);
82
+ const q = new URLSearchParams(query?.startsWith('?') ? query.slice(1) : (query ?? ''));
83
+ const span = { hour: 36e5, day: 864e5, week: 6048e5, month: 2592e6 }[q.get('time_period') ?? 'day'];
84
+ if (!span)
85
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'RuleSuite', field: 'time_period', code: 'invalid' }] }, 422);
86
+ const since = Date.parse(occurredAt) - span;
87
+ const ref = q.get('ref'), actor = q.get('actor_name'), result = q.get('rule_suite_result');
88
+ const hits = state().ruleSuites.filter((x) => x.repository === repo && Date.parse(x.pushed_at) >= since && (!ref || x.ref === ref || x.ref === `refs/heads/${ref}`) && (!actor || x.actor_login === actor) && (!result || result === 'all' || x.result === result)).reverse();
89
+ return ctx.reply(paginate(hits, query).map((x) => suiteOf(repo, x, false)));
90
+ };
91
+ const reposGetRepoRuleSuite = async (ctx) => {
92
+ const { repo, seg, state } = gh(ctx);
93
+ const x = state().ruleSuites.find((y) => y.repository === repo && y.id === Number(decodeURIComponent(seg[5])));
94
+ return send(ctx, x ? { status: 200, body: suiteOf(repo, x, true) } : githubNotFound());
95
+ };
96
+ return {
97
+ 'repos/get-repo-rulesets': reposGetRepoRulesets,
98
+ 'repos/get-repo-ruleset': reposGetRepoRuleset,
99
+ // newest first
100
+ 'repos/get-repo-ruleset-history': reposGetRepoRulesetHistory,
101
+ 'repos/get-repo-ruleset-version': reposGetRepoRulesetVersion,
102
+ 'repos/create-repo-ruleset': reposCreateRepoRuleset,
103
+ // GitHub's PUT (the hand-written route keeps the PATCH it also answered)
104
+ 'repos/update-repo-ruleset': reposUpdateRepoRuleset,
105
+ 'repos/delete-repo-ruleset': reposDeleteRepoRuleset,
106
+ // Rule insights, newest first, filtered by ref, actor_name, rule_suite_result and a time_period
107
+ // (hour, day, week, month) reaching back from now; GitHub keeps no longer window than a month.
108
+ 'repos/get-repo-rule-suites': reposGetRepoRuleSuites,
109
+ 'repos/get-repo-rule-suite': reposGetRepoRuleSuite,
110
+ };
111
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare function searchSemantics(): Record<string, Semantics>;
@@ -0,0 +1,84 @@
1
+ import { defaultBranchCommits, paginate, planeCommitBody, pullRequestAsIssue, seedUser, toGithubIssue, toGithubRepo, toGithubUser } from "../github-twin.js";
2
+ import { asView, gh } from "./shared.js";
3
+ function read(ctx) {
4
+ const { query, state } = gh(ctx);
5
+ const q = (new URLSearchParams((query ?? '').replace(/^\?/, '')).get('q') ?? '').toLowerCase();
6
+ const terms = q.replace(/\b\w+:\S+/g, '').trim().split(/\s+/).filter(Boolean); // strip qualifiers like repo:/is:
7
+ const matchText = (...vals) => terms.length === 0 || terms.every((t) => vals.some((v) => (v ?? '').toLowerCase().includes(t)));
8
+ return { q, repoQual: /repo:(\S+)/.exec(q)?.[1], matchText, query, state: state() };
9
+ }
10
+ const found = (ctx, total, items) => ctx.reply({ total_count: total, incomplete_results: false, items });
11
+ export function searchSemantics() {
12
+ const searchIssuesAndPullRequests = async (ctx) => {
13
+ const { q, repoQual, matchText, query, state } = read(ctx);
14
+ const wantPr = /\bis:pr\b/.test(q);
15
+ const wantIssue = /\bis:issue\b/.test(q);
16
+ const wantOpen = /\bis:open\b/.test(q);
17
+ const wantClosed = /\bis:closed\b/.test(q);
18
+ const issueItems = state.issues.map((i) => ({ obj: toGithubIssue(i, state.milestones, undefined, state.comments), repo: i.repository, isPr: false, st: i.state ?? 'open', t: i.title, b: i.body }));
19
+ // Same marker, same reason as the pulls list and the activity feed: a counter shell is
20
+ // not a pull request, and search must not answer for one (it would also DOUBLE the
21
+ // issue it shadows — once as itself, once as a phantom PR at the same number).
22
+ // a pull request is found as the issue it also is, carrying its pull_request key
23
+ const prItems = state.prs.filter((p) => p.is_pull_request).map((p) => ({ obj: toGithubIssue(pullRequestAsIssue(p), state.milestones, p, state.comments), repo: p.repository, isPr: true, st: p.state ?? 'open', t: p.title, b: p.body }));
24
+ let pool = [...issueItems, ...prItems];
25
+ if (wantPr && !wantIssue)
26
+ pool = pool.filter((x) => x.isPr);
27
+ if (wantIssue && !wantPr)
28
+ pool = pool.filter((x) => !x.isPr);
29
+ if (wantOpen && !wantClosed)
30
+ pool = pool.filter((x) => x.st === 'open');
31
+ if (wantClosed && !wantOpen)
32
+ pool = pool.filter((x) => x.st === 'closed');
33
+ if (repoQual)
34
+ pool = pool.filter((x) => x.repo === repoQual);
35
+ pool = pool.filter((x) => matchText(x.t, x.b));
36
+ return found(ctx, pool.length, paginate(pool.map((x) => asView('issue-search-result-item', { ...x.obj, score: 1 })), query));
37
+ };
38
+ const searchRepos = async (ctx) => {
39
+ const { matchText, query, state } = read(ctx);
40
+ const pool = state.repos.filter((r) => matchText(r.name, r.full_name, r.description ?? undefined));
41
+ return found(ctx, pool.length, paginate(pool.map((r) => asView('repo-search-result-item', { ...toGithubRepo(r), score: 1 })), query));
42
+ };
43
+ const searchUsers = async (ctx) => {
44
+ const { matchText, query, state } = read(ctx);
45
+ const logins = new Set();
46
+ for (const c of state.collaborators)
47
+ logins.add(c.login);
48
+ for (const r of state.repos)
49
+ logins.add(r.owner);
50
+ const pool = [...logins].filter((l) => matchText(l)).map(seedUser);
51
+ return found(ctx, pool.length, paginate(pool.map((u) => asView('user-search-result-item', { ...toGithubUser(u), score: 1 })), query));
52
+ };
53
+ const searchCode = async (ctx) => {
54
+ const { repoQual, matchText, query, state } = read(ctx);
55
+ // GitHub searches the default branch's code (docs.github.com/search-github/searching-on-github/searching-code)
56
+ const onDefault = (c) => (c.branch ?? 'main') === (state.repos.find((r) => r.full_name === c.repository)?.default_branch ?? 'main');
57
+ const pool = state.contents.filter((c) => (!repoQual || c.repository === repoQual) && onDefault(c) && matchText(c.path));
58
+ const items = paginate(pool, query).map((c) => ({ name: c.path.split('/').pop(), path: c.path, sha: c.sha, url: `https://api.github.com/repos/${c.repository}/contents/${c.path}?ref=${c.sha}`, git_url: `https://api.github.com/repos/${c.repository}/git/blobs/${c.sha}`, repository: { full_name: c.repository }, html_url: `https://github.com/${c.repository}/blob/${c.branch ?? 'main'}/${c.path}`, score: 1 }));
59
+ return found(ctx, pool.length, items);
60
+ };
61
+ const searchCommits = async (ctx) => {
62
+ const { repoQual, matchText, query, state } = read(ctx);
63
+ const all = [];
64
+ for (const r of state.repos.filter((x) => !repoQual || x.full_name === repoQual)) {
65
+ for (const c of await defaultBranchCommits(state, r.full_name, gh(ctx).root)) {
66
+ const body = matchText(c.message) ? await planeCommitBody(r.full_name, c.sha, gh(ctx).root) : null;
67
+ if (body)
68
+ all.push(asView('commit-search-result-item', { ...body, repository: asView('minimal-repository', toGithubRepo(r)), score: 1 }));
69
+ }
70
+ }
71
+ return found(ctx, all.length, paginate(all, query));
72
+ };
73
+ return {
74
+ // Issues + PRs (real GitHub's issue search spans both). Honor is:issue / is:pr / is:open|closed.
75
+ 'search/issues-and-pull-requests': searchIssuesAndPullRequests,
76
+ 'search/repos': searchRepos,
77
+ // The set of known logins = collaborators + repo owners. Faithful local search domain.
78
+ 'search/users': searchUsers,
79
+ 'search/code': searchCode,
80
+ // Commits on each repository's default branch whose message matches (GitHub searches default branches only,
81
+ // docs.github.com/search-github/searching-on-github/searching-commits)
82
+ 'search/commits': searchCommits,
83
+ };
84
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare function securitySemantics(): Record<string, Semantics>;