@volter/twin-github 0.1.2 → 2.0.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 (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 +232 -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 +104 -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 +170 -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 +225 -95
  161. package/src/github-git-http.ts +48 -84
  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 +50 -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 +193 -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,86 @@
1
+ // GitHub's gists family: create, list, read, edit and delete a gist. A gist's id is minted from a
2
+ // sequence (`gist:<hash>`), its files are stored whole and merged on edit, and every create and edit of its files
3
+ // saves a version the gist's history lists and a revision read returns.
4
+ import { createHash } from 'node:crypto';
5
+ import { GITHUB_WORLD_USER, githubNotFound, paginate, toGithubGist } from "../github-twin.js";
6
+ import { gh, put, send } from "./shared.js";
7
+ const gist = async (ctx) => {
8
+ const { seg, state } = gh(ctx);
9
+ const g = state().gists.find((x) => x.id === decodeURIComponent(seg[1]));
10
+ return send(ctx, g ? { status: 200, body: toGithubGist(g) } : githubNotFound());
11
+ };
12
+ // a gist as one of its versions stood: its files then (docs.github.com/rest/gists/gists#get-a-gist-revision)
13
+ const revision = async (ctx) => {
14
+ const { seg, state } = gh(ctx);
15
+ const g = state().gists.find((x) => x.id === decodeURIComponent(seg[1]));
16
+ const v = g?.history?.find((h) => h.version === decodeURIComponent(seg[2]));
17
+ return send(ctx, g && v ? { status: 200, body: toGithubGist({ ...g, files: v.files, updated_at: v.committed_at }) } : githubNotFound());
18
+ };
19
+ /** A saved version of a gist's files: its sha, and the lines added and removed since the one before. */
20
+ function version(gistId, files, before, at, login, n) {
21
+ const lines = (fs) => new Set(fs.flatMap((f) => (f.content ?? '').split('\n').map((l) => `${f.filename}:${l}`)));
22
+ const [now, then] = [lines(files), lines(before)];
23
+ return {
24
+ version: createHash('sha1').update(`${gistId}:${n}:${JSON.stringify(files)}`).digest('hex'), committed_at: at, user_login: login, files,
25
+ additions: [...now].filter((l) => !then.has(l)).length, deletions: [...then].filter((l) => !now.has(l)).length,
26
+ };
27
+ }
28
+ export function gistsSemantics() {
29
+ const gistsList = async (ctx) => {
30
+ const { query, state } = gh(ctx);
31
+ return ctx.reply(paginate(state().gists, query).map(toGithubGist));
32
+ };
33
+ const gistsCreate = async (ctx) => {
34
+ const { body, occurredAt, state } = gh(ctx);
35
+ const filesIn = (body.files ?? {});
36
+ const files = Object.entries(filesIn).map(([filename, v]) => ({ filename, content: v?.content ?? '', size: (v?.content ?? '').length }));
37
+ if (!files.length)
38
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Gist', field: 'files', code: 'missing_field' }] }, 422);
39
+ // one past every gist stored, tombstones included: each create mints its own id
40
+ const seq = ctx.rowsRaw('gist', { withDeleted: true }).length + 1;
41
+ const gistId = createHash('sha256').update(`gist:${seq}`).digest('hex').slice(0, 20);
42
+ const fields = { id: gistId, owner: gh(ctx).caller ?? GITHUB_WORLD_USER, description: body.description ?? null, public: body.public === true, files, created_at: occurredAt, updated_at: occurredAt, history: [version(gistId, files, [], occurredAt, gh(ctx).caller ?? GITHUB_WORLD_USER, 1)] };
43
+ await put(ctx, { operation: 'gist.create', subjectType: 'gist', subjectId: `gist:${gistId}`, fields });
44
+ return ctx.reply(toGithubGist(state().gists.find((x) => x.id === gistId)), 201);
45
+ };
46
+ const gistsUpdate = async (ctx) => {
47
+ const { seg, body, occurredAt, state } = gh(ctx);
48
+ const gistId = decodeURIComponent(seg[1]);
49
+ const existing = state().gists.find((g) => g.id === gistId);
50
+ if (!existing)
51
+ return send(ctx, githubNotFound());
52
+ const fields = { id: gistId, updated_at: occurredAt };
53
+ if (body.description !== undefined)
54
+ fields.description = body.description;
55
+ if (body.files !== undefined) {
56
+ const merged = new Map(existing.files.map((f) => [f.filename, f]));
57
+ for (const [filename, v] of Object.entries(body.files)) {
58
+ if (v === null)
59
+ merged.delete(filename);
60
+ else
61
+ merged.set(filename, { filename: v.filename ?? filename, content: v.content ?? merged.get(filename)?.content ?? '', size: (v.content ?? '').length });
62
+ }
63
+ fields.files = [...merged.values()];
64
+ const history = existing.history ?? [];
65
+ fields.history = [version(gistId, fields.files, existing.files, occurredAt, gh(ctx).caller ?? GITHUB_WORLD_USER, history.length + 1), ...history];
66
+ }
67
+ await put(ctx, { operation: 'gist.update', subjectType: 'gist', subjectId: `gist:${gistId}`, fields });
68
+ return ctx.reply(toGithubGist(state().gists.find((x) => x.id === gistId)));
69
+ };
70
+ const gistsDelete = async (ctx) => {
71
+ const { seg, state } = gh(ctx);
72
+ const gistId = decodeURIComponent(seg[1]);
73
+ if (!state().gists.find((g) => g.id === gistId))
74
+ return send(ctx, githubNotFound());
75
+ await put(ctx, { operation: 'gist.delete', subjectType: 'gist', subjectId: `gist:${gistId}`, fields: { deleted: true, id: gistId } });
76
+ return send(ctx, { status: 204, body: undefined });
77
+ };
78
+ return {
79
+ 'gists/list': gistsList,
80
+ 'gists/get': gist,
81
+ 'gists/get-revision': revision,
82
+ 'gists/create': gistsCreate,
83
+ 'gists/update': gistsUpdate,
84
+ 'gists/delete': gistsDelete,
85
+ };
86
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare function gitSemantics(): Record<string, Semantics>;
@@ -0,0 +1,235 @@
1
+ // The Git Data API: blobs, trees, commits, annotated tags and refs, backed by the git plane (the
2
+ // engine slot): each create writes a REAL git object into the repository's store and answers its
3
+ // real sha, so a REST-authored chain (blob → tree → commit → ref) is fetchable by an unmodified git
4
+ // client. The kernel records every write as well (the ledger the projection, mirror and webhooks
5
+ // fold). A ref write is a push, as on GitHub: it moves the branch, re-points or closes the open pull
6
+ // requests riding it, and announces push (and pull_request synchronize/closed).
7
+ import { Buffer } from 'node:buffer';
8
+ import { closePrsForDeletedBranch, deleteGitRefInPlane, githubNotFound, isAncestorInPlane, listGitPlaneRefs, objectExistsInPlane, objectTypeInPlane, readGitBlobFromPlane, readGitCommitFromPlane, readGitTagFromPlane, readGitTreeFromPlane, syncPrHeadsToBranchTip, toGithubGitBlob, toGithubGitCommit, toGithubGitRef, toGithubGitTagObject, toGithubGitTree, updateGitRefInPlane, writeGitBlobToPlane, writeGitCommitToPlane, writeGitTagToPlane, writeGitTreeToPlane, } from "../github-twin.js";
9
+ import { announce, atomic, gh, identityOf, put, send } from "./shared.js";
10
+ /** An owner/name the plane's path validation rejects can never name a twin repository: GitHub's 404,
11
+ * rather than a store error. */
12
+ function unnamed(ctx) {
13
+ const { seg } = gh(ctx);
14
+ return !/^[A-Za-z0-9_.-]+$/.test(seg[1]) || !/^[A-Za-z0-9_.-]+$/.test(seg[2]) || ['.', '..'].includes(seg[1]) || ['.', '..'].includes(seg[2]);
15
+ }
16
+ /** A Git Data write: Not Found for a repository name the plane cannot hold. */
17
+ const gitWrite = (serve) => async (ctx) => (unnamed(ctx) ? send(ctx, githubNotFound()) : serve(ctx));
18
+ const invalid = (ctx, message) => ctx.reply({ message }, 422);
19
+ const missing = (ctx, resource, fields) => ctx.reply({ message: 'Validation Failed', errors: fields.map((field) => ({ resource, field, code: 'missing_field' })) }, 422);
20
+ /** The ref a path names under git/refs/ or git/ref/ (heads/x, tags/x; a ref may hold slashes). */
21
+ const refName = (ctx) => `refs/${decodeURIComponent(gh(ctx).seg.slice(5).join('/'))}`;
22
+ /** A branch ref's move: the branch row follows, and the open pull requests riding it are re-pointed. */
23
+ async function followBranch(ctx, ref, sha, events) {
24
+ if (ref.startsWith('refs/heads/'))
25
+ await branchMoved(ctx, ref, sha, events);
26
+ }
27
+ /** A branch ref written through the Git Data API: its branch row and the pull requests riding it follow. */
28
+ async function branchMoved(ctx, ref, sha, events) {
29
+ const { repo, root, occurredAt } = gh(ctx);
30
+ const branch = ref.slice('refs/heads/'.length);
31
+ await put(ctx, { operation: 'branch.create', subjectType: 'branch', subjectId: `branch:${repo}#${branch}`, fields: { repository: repo, name: branch, commit_sha: sha } });
32
+ await syncPrHeadsToBranchTip(repo, branch, sha, root, occurredAt, (pr, base) => events.push({ event: 'pull_request', action: 'synchronize', repository: base, number: Number(pr.number), pr }), atomic(ctx));
33
+ }
34
+ export function gitSemantics() {
35
+ const gitGetRef = async (ctx) => {
36
+ const { repo, root, state } = gh(ctx);
37
+ const current = new Map(state().gitRefs.filter((r) => r.repository === repo).map((r) => [r.ref, r]));
38
+ for (const r of await listGitPlaneRefs(repo, root))
39
+ current.set(r.ref, { ...r, repository: repo });
40
+ const r = current.get(refName(ctx));
41
+ return send(ctx, r ? { status: 200, body: toGithubGitRef(r) } : githubNotFound());
42
+ };
43
+ const gitGetCommit = async (ctx) => {
44
+ const { repo, seg, root, state } = gh(ctx);
45
+ const sha = decodeURIComponent(seg[5]);
46
+ const c = state().gitCommits.find((x) => x.repository === repo && x.sha === sha)
47
+ ?? await (async () => { const p = await readGitCommitFromPlane(repo, sha, root); return p ? { repository: repo, ...p } : undefined; })();
48
+ return send(ctx, c ? { status: 200, body: toGithubGitCommit(c) } : githubNotFound());
49
+ };
50
+ const gitGetTree = async (ctx) => {
51
+ const { repo, seg, root, state } = gh(ctx);
52
+ const sha = decodeURIComponent(seg[5]);
53
+ const t = state().gitTrees.find((x) => x.repository === repo && x.sha === sha)
54
+ ?? await (async () => { const p = await readGitTreeFromPlane(repo, sha, root); return p ? { repository: repo, sha, tree: p, truncated: false } : undefined; })();
55
+ return send(ctx, t ? { status: 200, body: toGithubGitTree(t) } : githubNotFound());
56
+ };
57
+ const gitGetBlob = async (ctx) => {
58
+ const { repo, seg, root, state } = gh(ctx);
59
+ const sha = decodeURIComponent(seg[5]);
60
+ const b = state().gitBlobs.find((x) => x.repository === repo && x.sha === sha)
61
+ ?? await (async () => { const p = await readGitBlobFromPlane(repo, sha, root); return p ? { repository: repo, sha, content_b64: p.content_b64, size: p.size, encoding: 'base64' } : undefined; })();
62
+ return send(ctx, b ? { status: 200, body: toGithubGitBlob(b) } : githubNotFound());
63
+ };
64
+ const gitGetTag = async (ctx) => {
65
+ const { repo, seg, root, state } = gh(ctx);
66
+ const sha = decodeURIComponent(seg[5]);
67
+ const t = state().gitTagObjects.find((x) => x.repository === repo && x.sha === sha)
68
+ ?? await (async () => { const p = await readGitTagFromPlane(repo, sha, root); return p ? { repository: repo, ...p } : undefined; })();
69
+ return send(ctx, t ? { status: 200, body: toGithubGitTagObject(t) } : githubNotFound());
70
+ };
71
+ const gitCreateBlob = (ctx) => (gitWrite(async (ctx) => {
72
+ const { repo, body, root } = gh(ctx);
73
+ if (body.content === undefined)
74
+ return missing(ctx, 'Blob', ['content']);
75
+ const encoding = String(body.encoding ?? 'utf-8');
76
+ const content_b64 = encoding === 'base64' ? String(body.content) : Buffer.from(String(body.content), 'utf-8').toString('base64');
77
+ let size = 0;
78
+ try {
79
+ size = Buffer.from(content_b64, 'base64').length;
80
+ }
81
+ catch {
82
+ size = content_b64.length;
83
+ }
84
+ const sha = await writeGitBlobToPlane(repo, content_b64, root);
85
+ await put(ctx, { operation: 'git_blob.create', subjectType: 'git_blob', subjectId: `gitblob:${repo}#${sha}`, fields: { repository: repo, sha, content_b64, size, encoding: 'base64' } });
86
+ return ctx.reply({ sha, url: `https://api.github.com/repos/${repo}/git/blobs/${sha}` }, 201);
87
+ }))(ctx);
88
+ const gitCreateTree = (ctx) => (gitWrite(async (ctx) => {
89
+ const { repo, body, root, state } = gh(ctx);
90
+ if (!Array.isArray(body.tree))
91
+ return missing(ctx, 'Tree', ['tree']);
92
+ const entries = body.tree.map((e) => ({
93
+ path: String(e.path ?? ''), mode: String(e.mode ?? '100644'), type: String(e.type ?? 'blob'), sha: e.sha === null ? '' : String(e.sha ?? ''),
94
+ ...(e.size !== undefined ? { size: Number(e.size) } : {}),
95
+ }));
96
+ if (entries.some((e) => e.sha !== '' && !/^[0-9a-f]{40}$/i.test(e.sha)))
97
+ return invalid(ctx, 'The sha parameter must be exactly 40 characters and contain only [0-9a-f]');
98
+ // a base_tree the store does not hold is a 422: dropping it would lose the base's entries
99
+ if (body.base_tree !== undefined && (!/^[0-9a-f]{40}$/i.test(String(body.base_tree)) || await objectTypeInPlane(repo, String(body.base_tree), root) !== 'tree')) {
100
+ return invalid(ctx, `The base tree parameter (${String(body.base_tree)}) is not a valid tree`);
101
+ }
102
+ const sha = await writeGitTreeToPlane(repo, entries, body.base_tree !== undefined ? String(body.base_tree) : undefined, root);
103
+ // the kernel row records the tree the plane now holds (after the overlay)
104
+ const planeTree = await readGitTreeFromPlane(repo, sha, root) ?? entries.filter((e) => e.sha);
105
+ await put(ctx, { operation: 'git_tree.create', subjectType: 'git_tree', subjectId: `gittree:${repo}#${sha}`, fields: { repository: repo, sha, tree: planeTree, truncated: false } });
106
+ return ctx.reply(toGithubGitTree(state().gitTrees.find((x) => x.repository === repo && x.sha === sha)), 201);
107
+ }))(ctx);
108
+ const gitCreateCommit = (ctx) => (gitWrite(async (ctx) => {
109
+ const { repo, body, occurredAt, root, state } = gh(ctx);
110
+ const absent = ['message', 'tree'].filter((k) => body[k] === undefined);
111
+ if (absent.length)
112
+ return missing(ctx, 'Commit', absent);
113
+ const parents = Array.isArray(body.parents) ? body.parents.map(String) : [];
114
+ if (!/^[0-9a-f]{40}$/i.test(String(body.tree)) || parents.some((p) => !/^[0-9a-f]{40}$/i.test(p)))
115
+ return invalid(ctx, 'The tree and parent parameters must be exactly 40 characters and contain only [0-9a-f]');
116
+ // GitHub defaults the author to the signed-in person and the committer to the author
117
+ const me = identityOf(gh(ctx));
118
+ const author = { name: String(body.author?.name ?? me.name), email: String(body.author?.email ?? me.email) };
119
+ const committer = body.committer?.name !== undefined || body.committer?.email !== undefined
120
+ ? { name: String(body.committer?.name ?? author.name), email: String(body.committer?.email ?? author.email) }
121
+ : author;
122
+ const sha = await writeGitCommitToPlane(repo, {
123
+ message: String(body.message), treeSha: String(body.tree), parents,
124
+ author, committer,
125
+ occurredAt: String(body.author?.date ?? body.committer?.date ?? occurredAt),
126
+ }, root);
127
+ // the kernel row records the identity the object's bytes carry
128
+ const fields = { repository: repo, sha, message: String(body.message), tree_sha: String(body.tree), parents, author_name: author.name, author_email: author.email, created_at: occurredAt };
129
+ await put(ctx, { operation: 'git_commit.create', subjectType: 'git_commit', subjectId: `gitcommit:${repo}#${sha}`, fields });
130
+ return ctx.reply(toGithubGitCommit(state().gitCommits.find((x) => x.repository === repo && x.sha === sha)), 201);
131
+ }))(ctx);
132
+ const gitCreateTag = (ctx) => (gitWrite(async (ctx) => {
133
+ const { repo, body, occurredAt, root, state } = gh(ctx);
134
+ const absent = ['tag', 'message', 'object'].filter((k) => body[k] === undefined);
135
+ if (absent.length)
136
+ return missing(ctx, 'Tag', absent);
137
+ if (!/^[0-9a-f]{40}$/i.test(String(body.object)))
138
+ return invalid(ctx, 'The object parameter must be exactly 40 characters and contain only [0-9a-f]');
139
+ const me = identityOf(gh(ctx));
140
+ const tagger = { name: String(body.tagger?.name ?? me.name), email: String(body.tagger?.email ?? me.email) };
141
+ const sha = await writeGitTagToPlane(repo, {
142
+ tag: String(body.tag), message: String(body.message), objectSha: String(body.object), objectType: String(body.type ?? 'commit'),
143
+ ...(tagger !== undefined ? { tagger } : {}), occurredAt: String(body.tagger?.date ?? occurredAt),
144
+ }, root);
145
+ const fields = { repository: repo, sha, tag: String(body.tag), message: String(body.message), object_sha: String(body.object), object_type: String(body.type ?? 'commit'), tagger_name: body.tagger?.name, created_at: occurredAt };
146
+ await put(ctx, { operation: 'git_tag_object.create', subjectType: 'git_tag_object', subjectId: `gittag:${repo}#${sha}`, fields });
147
+ return ctx.reply(toGithubGitTagObject(state().gitTagObjects.find((x) => x.repository === repo && x.sha === sha)), 201);
148
+ }))(ctx);
149
+ const gitUpdateRef = (ctx) => (gitWrite(async (ctx) => {
150
+ const { repo, body, root, state } = gh(ctx);
151
+ const ref = refName(ctx);
152
+ const existing = state().gitRefs.find((x) => x.repository === repo && x.ref === ref);
153
+ if (!existing)
154
+ return send(ctx, githubNotFound());
155
+ if (body.sha === undefined)
156
+ return missing(ctx, 'Reference', ['sha']);
157
+ const force = body.force === true || body.force === 'true';
158
+ if (!force
159
+ && await objectExistsInPlane(repo, existing.object_sha, root) && await objectExistsInPlane(repo, String(body.sha), root)
160
+ && !await isAncestorInPlane(repo, existing.object_sha, String(body.sha), root)) {
161
+ return invalid(ctx, 'Update is not a fast forward');
162
+ }
163
+ await put(ctx, { operation: 'git_ref.update', subjectType: 'git_ref', subjectId: `gitref:${repo}#${ref}`, fields: { repository: repo, ref, object_sha: String(body.sha), object_type: existing.object_type } });
164
+ await updateGitRefInPlane(repo, ref, String(body.sha), root);
165
+ const events = [{ event: 'push', action: '', repository: repo, number: 0, ref, before: existing.object_sha, after: String(body.sha) }];
166
+ await followBranch(ctx, ref, String(body.sha), events);
167
+ const answer = toGithubGitRef(state().gitRefs.find((x) => x.repository === repo && x.ref === ref));
168
+ await announce(ctx, events);
169
+ return ctx.reply(answer);
170
+ }))(ctx);
171
+ const gitDeleteRef = (ctx) => (gitWrite(async (ctx) => {
172
+ const { repo, root, occurredAt, state } = gh(ctx);
173
+ const ref = refName(ctx);
174
+ const existing = state().gitRefs.find((x) => x.repository === repo && x.ref === ref);
175
+ if (!existing)
176
+ return send(ctx, githubNotFound());
177
+ await put(ctx, { operation: 'git_ref.delete', subjectType: 'git_ref', subjectId: `gitref:${repo}#${ref}`, fields: { deleted: true, repository: repo, ref } });
178
+ await deleteGitRefInPlane(repo, ref, root);
179
+ const events = [{ event: 'push', action: '', repository: repo, number: 0, ref, before: existing.object_sha, after: '0'.repeat(40) }];
180
+ if (ref.startsWith('refs/heads/')) {
181
+ const branch = ref.slice('refs/heads/'.length);
182
+ if (state().branches.some((b) => b.repository === repo && b.name === branch)) {
183
+ await put(ctx, { operation: 'branch.delete', subjectType: 'branch', subjectId: `branch:${repo}#${branch}`, fields: { deleted: true, repository: repo, name: branch } });
184
+ }
185
+ await closePrsForDeletedBranch(repo, branch, root, occurredAt, (pr, base) => events.push({ event: 'pull_request', action: 'closed', repository: base, number: Number(pr.number), pr }), atomic(ctx), { operation: 'git/delete-ref', actor: 'api' });
186
+ }
187
+ await announce(ctx, events);
188
+ return send(ctx, { status: 204, body: undefined });
189
+ }))(ctx);
190
+ return {
191
+ // exactly one ref (heads/x or tags/x), the kernel's refs overlaid by the plane's
192
+ 'git/get-ref': gitGetRef,
193
+ // Object reads consult the kernel row first (REST-authored objects land in both with the same real
194
+ // sha), then the plane, so an object that arrived by `git push` is visible here too.
195
+ 'git/get-commit': gitGetCommit,
196
+ 'git/get-tree': gitGetTree,
197
+ 'git/get-blob': gitGetBlob,
198
+ 'git/get-tag': gitGetTag,
199
+ // content in base64 or utf-8; answers {sha, url}
200
+ 'git/create-blob': gitCreateBlob,
201
+ // {tree:[{path,mode,type,sha}]} with base_tree overlay semantics (sha:null deletes a path), as GitHub
202
+ 'git/create-tree': gitCreateTree,
203
+ // {message, tree, parents}; an author or committer date pins the sha as GitHub honors it
204
+ 'git/create-commit': gitCreateCommit,
205
+ // an ANNOTATED tag object {tag, message, object, type}
206
+ 'git/create-tag': gitCreateTag,
207
+ // {ref:'refs/heads/x', sha}; 422 when either is missing or the ref exists. A ref minted against a
208
+ // sha the plane has never seen stays control-plane-only.
209
+ 'git/create-ref': gitWrite(async (ctx) => {
210
+ const { repo, body, root, state } = gh(ctx);
211
+ const absent = ['ref', 'sha'].filter((k) => body[k] === undefined);
212
+ if (absent.length)
213
+ return missing(ctx, 'Reference', absent);
214
+ const ref = String(body.ref);
215
+ if (!ref.startsWith('refs/'))
216
+ return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Reference', field: 'ref', code: 'invalid' }] }, 422);
217
+ if (state().gitRefs.some((x) => x.repository === repo && x.ref === ref))
218
+ return invalid(ctx, 'Reference already exists');
219
+ const planeType = await objectTypeInPlane(repo, String(body.sha), root);
220
+ const object_type = planeType === 'tag' || (ref.startsWith('refs/tags/') && state().gitTagObjects.some((t) => t.repository === repo && t.sha === String(body.sha))) ? 'tag' : 'commit';
221
+ await put(ctx, { operation: 'git_ref.create', subjectType: 'git_ref', subjectId: `gitref:${repo}#${ref}`, fields: { repository: repo, ref, object_sha: String(body.sha), object_type } });
222
+ await updateGitRefInPlane(repo, ref, String(body.sha), root);
223
+ const events = [{ event: 'push', action: '', repository: repo, number: 0, ref, before: '0'.repeat(40), after: String(body.sha) }];
224
+ await followBranch(ctx, ref, String(body.sha), events);
225
+ const answer = toGithubGitRef(state().gitRefs.find((x) => x.repository === repo && x.ref === ref));
226
+ await announce(ctx, events);
227
+ return ctx.reply(answer, 201);
228
+ }),
229
+ // Fast-forward, or force-update, a ref. A non-fast-forward needs force (true or "true", as gh's -F
230
+ // and -f send it); decidable only when both commits are in the plane.
231
+ 'git/update-ref': gitUpdateRef,
232
+ // deleting a branch ref removes the branch and closes the open pull requests riding it
233
+ 'git/delete-ref': gitDeleteRef,
234
+ };
235
+ }
@@ -0,0 +1,7 @@
1
+ import type { FieldResolver } from '../graphql-wire.js';
2
+ /** The closing-keyword issue linkage (Closes/Fixes/Resolves #n) real GitHub derives from the PR body. */
3
+ export declare function closingIssueNumbersFromBody(body: string | undefined): number[];
4
+ export declare function githubGraphql(): {
5
+ resolvers: Record<string, FieldResolver>;
6
+ stored: Record<string, string[]>;
7
+ };