@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,41 @@
1
+ import type { GithubWriteEvent } from './github-twin.js';
2
+ export type GithubWebhook = Record<string, unknown>;
3
+ export type GithubWebhookResponse = {
4
+ status: number;
5
+ headers?: Record<string, string>;
6
+ body?: string;
7
+ };
8
+ export type GithubWebhookDelivery = (url: string, event: string, payload: GithubWebhook, headers: Record<string, string>, body: string) => Promise<GithubWebhookResponse> | GithubWebhookResponse;
9
+ type Options = {
10
+ root?: string;
11
+ occurredAt?: string;
12
+ deliver?: GithubWebhookDelivery;
13
+ };
14
+ export declare function signGithubPayload(secret: string, body: string): string;
15
+ /** Current persisted hook settings are resolved per event and World, including after restart. */
16
+ export declare function emitGithubEvent(write: GithubWriteEvent, opts?: Options): Promise<GithubWebhook[]>;
17
+ /** A repository's event, delivered as well to each GitHub App installed on the repository whose app subscribes to it,
18
+ * with the installation it is for, so the app knows which installation token to mint
19
+ * (docs.github.com/apps/creating-github-apps/registering-a-github-app/using-webhooks-with-github-apps,
20
+ * docs.github.com/webhooks/webhook-events-and-payloads: `installation` is present when the event is for an app's
21
+ * installation). Answers how many apps it was delivered to. */
22
+ export declare function emitGithubInstallationEvents(write: GithubWriteEvent, opts?: Options): Promise<number>;
23
+ /** An event for a GitHub App's own webhook (installation, …), delivered to the URL its registration names
24
+ * and recorded as any delivery is; an app with no webhook URL gets none, as on GitHub. */
25
+ export declare function emitGithubAppEvent(app: {
26
+ id: number;
27
+ hook_url?: string;
28
+ webhook_secret?: string;
29
+ }, event: string, payload: GithubWebhook, opts?: Options): Promise<boolean>;
30
+ export declare function pingGithubHook(repository: string, hookId: number, opts?: Options): Promise<boolean>;
31
+ /** A hook's last delivery as GitHub reports it on the hook (`last_response`): unused until one is made. */
32
+ export declare function hookLastResponse(repository: string, hookId: number, root?: string): Record<string, unknown>;
33
+ /** A GitHub App's own webhook deliveries, newest first (docs.github.com/rest/apps/webhooks#list-deliveries-for-an-app-webhook):
34
+ * each names the installation its payload was for; one delivery in detail carries its request and response. */
35
+ export declare function githubAppDeliveries(appId: number, root?: string): Record<string, unknown>[];
36
+ export declare function githubAppDelivery(appId: number, id: number, root?: string): Record<string, unknown> | undefined;
37
+ /** Native delivery/ping routes, shared by CLI serve and in-process callers. */
38
+ export declare function handleGithubWebhookRequest(request: Request, opts: Options & {
39
+ readOnly?: boolean;
40
+ }): Promise<Response | undefined>;
41
+ export {};
@@ -0,0 +1,232 @@
1
+ // Native repository hooks and delivery history live in the World's existing state.
2
+ // Callback HTTP is the caller's webhook sink, never a credentialed GitHub API call.
3
+ import { createHash, createHmac } from 'node:crypto';
4
+ import { applyTwinWrite, applyTwinWriteAtomic, ownFields, twinResources, worldNow } from '@volter/world-core';
5
+ import { GITHUB_WORLD_USER, githubApps, githubState, handleGithubRequest, toGithubWebhook } from "./github-twin.js";
6
+ import { appDestination, appFetch } from '@volter/world-core/app-route';
7
+ import { worldEgressRefusal } from '@volter/world-core/network-policy';
8
+ const TYPE = 'webhook_delivery';
9
+ export function signGithubPayload(secret, body) {
10
+ return `sha256=${createHmac('sha256', secret).update(body).digest('hex')}`;
11
+ }
12
+ const httpDelivery = async (url, _event, _payload, headers, body) => {
13
+ // the World's egress rule: a refused hook URL fails as a transport failure, recorded on the delivery; the
14
+ // application's own hostnames reach it inside the World. GitHub follows no redirect: the 3xx is the delivery's answer.
15
+ const refusal = appDestination(url) ? null : worldEgressRefusal(url);
16
+ if (refusal !== null)
17
+ throw new Error(refusal);
18
+ const response = await appFetch(url, { method: 'POST', headers, body, redirect: 'manual', signal: AbortSignal.timeout(10_000) });
19
+ return { status: response.status, headers: Object.fromEntries(response.headers), body: await response.text() };
20
+ };
21
+ async function nativeObject(path, root) {
22
+ const response = await handleGithubRequest({ method: 'GET', path, root });
23
+ return response.status === 200 && response.body && !Array.isArray(response.body)
24
+ ? response.body : undefined;
25
+ }
26
+ async function buildPayload(write, root) {
27
+ const payload = { action: write.action, repository: { full_name: write.repository }, sender: { login: GITHUB_WORLD_USER, type: 'User' } };
28
+ const path = `/repos/${write.repository}`;
29
+ if (['pull_request', 'pull_request_review', 'pull_request_review_comment'].includes(write.event)) {
30
+ payload.pull_request = write.pr ?? await nativeObject(`${path}/pulls/${write.number}`, root) ?? { number: write.number };
31
+ }
32
+ if (write.event === 'pull_request')
33
+ payload.number = write.number;
34
+ if (write.event === 'issues' || write.event === 'issue_comment')
35
+ payload.issue = await issuePayload(write, path, root);
36
+ if (write.event === 'pull_request_review')
37
+ payload.review = reviewPayload(write);
38
+ if (write.event === 'issue_comment' || write.event === 'pull_request_review_comment')
39
+ payload.comment = write.comment ?? { body: write.body ?? '' };
40
+ if (write.event === 'discussion' || write.event === 'discussion_comment')
41
+ payload.number = write.number;
42
+ if (write.event === 'push' && write.ref !== undefined)
43
+ pushPayload(payload, write);
44
+ return payload;
45
+ }
46
+ /** An issue event's issue: the one the write carried, or as the API reads it. */
47
+ async function issuePayload(write, path, root) {
48
+ return write.issue ?? await nativeObject(`${path}/issues/${write.number}`, root) ?? { number: write.number };
49
+ }
50
+ /** A review event's review, its state lower-cased as webhooks send it. */
51
+ function reviewPayload(write) {
52
+ const review = write.review ?? { body: write.body ?? '' };
53
+ return { ...review, ...(typeof review.state === 'string' ? { state: review.state.toLowerCase() } : {}) };
54
+ }
55
+ /** A push event names its ref and the commits before and after it, and carries no action. */
56
+ function pushPayload(payload, write) {
57
+ delete payload.action;
58
+ Object.assign(payload, { ref: write.ref, before: write.before ?? '', after: write.after ?? '', created: write.before === '0'.repeat(40), deleted: write.after === '0'.repeat(40) });
59
+ }
60
+ function records(root) {
61
+ return twinResources('github', root).filter(r => r.type === TYPE).map(r => ownFields(r));
62
+ }
63
+ function hookFor(repository, id, root) {
64
+ return githubState(root).webhooks.find(h => h.repository === repository && h.id === id);
65
+ }
66
+ function headersFor(hook, event, guid, body) {
67
+ const headers = {
68
+ 'content-type': hook.content_type !== 'json' ? 'application/x-www-form-urlencoded' : 'application/json',
69
+ 'x-github-event': event, 'x-github-delivery': guid, 'x-github-hook-id': String(hook.id),
70
+ };
71
+ if (hook.secret)
72
+ headers['x-hub-signature-256'] = signGithubPayload(hook.secret, body);
73
+ return headers;
74
+ }
75
+ async function dispatch(hook, event, payload, opts, original) {
76
+ const payloadJson = original?.payloadJson ?? JSON.stringify(payload);
77
+ const requestBody = hook.content_type !== 'json' ? new URLSearchParams({ payload: payloadJson }).toString() : payloadJson;
78
+ const occurredAt = opts.occurredAt ?? worldNow();
79
+ // Allocation and append share the kernel lock; two callbacks cannot claim the same id.
80
+ const { value: pending } = await applyTwinWriteAtomic('github', resources => {
81
+ const id = resources.filter(r => r.type === TYPE).reduce((max, r) => Math.max(max, Number(ownFields(r).id) || 0), 0) + 1;
82
+ // from the World's history alone (never its directory): a replay of the same history delivers the same ids
83
+ const hex = createHash('sha256').update(`${occurredAt}\0${hook.repository}\0${hook.id}\0${id}\0${payloadJson}`).digest('hex');
84
+ const guid = original?.guid ?? `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
85
+ const value = {
86
+ id, guid, repository: hook.repository, hookId: hook.id, event,
87
+ ...(typeof payload.action === 'string' ? { action: payload.action } : {}),
88
+ url: hook.url, payloadJson, requestBody, requestHeaders: headersFor(hook, event, guid, requestBody),
89
+ deliveredAt: occurredAt, redelivery: original !== undefined, statusCode: null,
90
+ };
91
+ return { kind: 'write', value, write: { operation: 'webhook_delivery.create', subjectType: TYPE, subjectId: `webhook-delivery:${id}`, fields: value, occurredAt, actor: { kind: 'system' } } };
92
+ }, opts.root);
93
+ let result;
94
+ try {
95
+ const response = await (opts.deliver ?? httpDelivery)(pending.url, event, JSON.parse(payloadJson), pending.requestHeaders, requestBody);
96
+ result = { statusCode: response.status, responseHeaders: response.headers ?? {}, responseBody: response.body ?? '' };
97
+ }
98
+ catch {
99
+ // Persist failure without exposing signing keys, URL credentials or a thrown transport stack.
100
+ result = { statusCode: 0, error: 'Webhook transport failed' };
101
+ }
102
+ await applyTwinWrite('github', { operation: 'webhook_delivery.update', subjectType: TYPE, subjectId: `webhook-delivery:${pending.id}`, fields: result, occurredAt, actor: { kind: 'system' } }, opts.root);
103
+ return { ...pending, ...result };
104
+ }
105
+ /** Current persisted hook settings are resolved per event and World, including after restart. */
106
+ export async function emitGithubEvent(write, opts = {}) {
107
+ const hooks = githubState(opts.root).webhooks.filter(h => h.repository === write.repository && h.active && h.url && (h.events.includes('*') || h.events.includes(write.event)));
108
+ return hooks.length ? deliverToHooks(hooks, write, opts) : [];
109
+ }
110
+ /** Each subscribed repository hook gets the event's payload. */
111
+ async function deliverToHooks(hooks, write, opts) {
112
+ const payload = await buildPayload(write, opts.root);
113
+ for (const hook of hooks)
114
+ await dispatch(hook, write.event, payload, opts);
115
+ return hooks.map(() => payload);
116
+ }
117
+ /** A repository's event, delivered as well to each GitHub App installed on the repository whose app subscribes to it,
118
+ * with the installation it is for, so the app knows which installation token to mint
119
+ * (docs.github.com/apps/creating-github-apps/registering-a-github-app/using-webhooks-with-github-apps,
120
+ * docs.github.com/webhooks/webhook-events-and-payloads: `installation` is present when the event is for an app's
121
+ * installation). Answers how many apps it was delivered to. */
122
+ export async function emitGithubInstallationEvents(write, opts = {}) {
123
+ const st = githubState(opts.root);
124
+ const [owner] = write.repository.split('/');
125
+ const covering = st.appInstallations.filter((i) => i.account_login === owner && (i.repository_selection === 'all' || (i.repositories ?? []).includes(write.repository)));
126
+ let delivered = 0;
127
+ for (const installation of covering) {
128
+ const app = githubApps(opts.root).find((a) => a.slug === installation.app_slug);
129
+ if (!app?.hook_url || !app.events.includes(write.event))
130
+ continue;
131
+ const payload = { ...(await buildPayload(write, opts.root)), installation: { id: installation.id, node_id: nodeIdOf('Integration::Installation', installation.id) } };
132
+ if (await emitGithubAppEvent(app, write.event, payload, opts))
133
+ delivered += 1;
134
+ }
135
+ return delivered;
136
+ }
137
+ const nodeIdOf = (type, id) => Buffer.from(`${String(type.length).padStart(2, '0')}:${type}${id}`).toString('base64');
138
+ /** An event for a GitHub App's own webhook (installation, …), delivered to the URL its registration names
139
+ * and recorded as any delivery is; an app with no webhook URL gets none, as on GitHub. */
140
+ export async function emitGithubAppEvent(app, event, payload, opts = {}) {
141
+ if (!app.hook_url)
142
+ return false;
143
+ const hook = { id: app.id, repository: '', name: 'web', active: true, url: app.hook_url, content_type: 'json', secret: app.webhook_secret ?? '', events: [event] };
144
+ await dispatch(hook, event, payload, opts);
145
+ return true;
146
+ }
147
+ export async function pingGithubHook(repository, hookId, opts = {}) {
148
+ const hook = hookFor(repository, hookId, opts.root);
149
+ if (!hook?.url)
150
+ return false;
151
+ await dispatch(hook, 'ping', { hook_id: hook.id, hook: toGithubWebhook(hook), repository: { full_name: repository } }, opts);
152
+ return true;
153
+ }
154
+ /** A hook's last delivery as GitHub reports it on the hook (`last_response`): unused until one is made. */
155
+ export function hookLastResponse(repository, hookId, root) {
156
+ const last = records(root).filter((d) => d.repository === repository && d.hookId === hookId).sort((a, b) => b.id - a.id)[0];
157
+ if (!last || last.statusCode === null)
158
+ return { code: null, status: 'unused', message: null };
159
+ const ok = last.statusCode >= 200 && last.statusCode < 300;
160
+ return { code: last.statusCode || null, status: ok ? 'active' : 'failed', message: ok ? 'OK' : (last.error ?? `Invalid HTTP Response: ${last.statusCode}`) };
161
+ }
162
+ // a delivery takes no World time, so its duration is 0; a repository's hook belongs to no installation
163
+ function deliveryJson(d, detail, repositoryId) {
164
+ return {
165
+ id: d.id, guid: d.guid, delivered_at: d.deliveredAt, redelivery: d.redelivery, duration: 0, installation_id: null, repository_id: repositoryId,
166
+ status: d.statusCode === null ? 'Pending' : d.statusCode >= 200 && d.statusCode < 300 ? 'OK' : d.error ?? 'Failed',
167
+ status_code: d.statusCode, event: d.event, action: d.action ?? null,
168
+ ...(detail ? { url: d.url, request: { headers: d.requestHeaders, payload: JSON.parse(d.payloadJson) }, response: { headers: d.responseHeaders ?? {}, payload: d.responseBody ?? null } } : {}),
169
+ };
170
+ }
171
+ /** A GitHub App's own webhook deliveries, newest first (docs.github.com/rest/apps/webhooks#list-deliveries-for-an-app-webhook):
172
+ * each names the installation its payload was for; one delivery in detail carries its request and response. */
173
+ export function githubAppDeliveries(appId, root) {
174
+ return records(root).filter((d) => d.repository === '' && d.hookId === appId).sort((a, b) => b.id - a.id).map((d) => appDeliveryJson(d, false));
175
+ }
176
+ export function githubAppDelivery(appId, id, root) {
177
+ const d = records(root).find((x) => x.repository === '' && x.hookId === appId && x.id === id);
178
+ return d && appDeliveryJson(d, true);
179
+ }
180
+ function appDeliveryJson(d, detail) {
181
+ const payload = JSON.parse(d.payloadJson);
182
+ return { ...deliveryJson(d, detail, typeof payload.repository?.id === 'number' ? payload.repository.id : null), installation_id: payload.installation?.id ?? null };
183
+ }
184
+ /** Native delivery/ping routes, shared by CLI serve and in-process callers. */
185
+ export async function handleGithubWebhookRequest(request, opts) {
186
+ const url = new URL(request.url);
187
+ const match = /^\/repos\/([^/]+)\/([^/]+)\/hooks\/(\d+)\/(deliveries(?:\/(\d+)(?:\/(attempts))?)?|pings)\/?$/.exec(url.pathname);
188
+ if (!match)
189
+ return;
190
+ const repository = `${decodeURIComponent(match[1])}/${decodeURIComponent(match[2])}`;
191
+ const hook = hookFor(repository, Number(match[3]), opts.root);
192
+ const missing = () => Response.json({ message: 'Not Found' }, { status: 404 });
193
+ if (!hook)
194
+ return missing();
195
+ if (request.method === 'POST' && opts.readOnly)
196
+ return Response.json({ message: 'Read-only mirror' }, { status: 403 });
197
+ if (match[4] === 'pings') {
198
+ if (request.method !== 'POST')
199
+ return missing();
200
+ await pingGithubHook(repository, hook.id, opts);
201
+ return new Response(null, { status: 204 });
202
+ }
203
+ const all = records(opts.root).filter(d => d.repository === repository && d.hookId === hook.id).sort((a, b) => b.id - a.id);
204
+ const repoId = githubState(opts.root).repos.find((r) => r.full_name === repository)?.id ?? null;
205
+ if (!match[5]) {
206
+ if (request.method !== 'GET')
207
+ return missing();
208
+ const perPage = Number(url.searchParams.get('per_page') ?? 30);
209
+ const cursor = url.searchParams.get('cursor');
210
+ const status = url.searchParams.get('status');
211
+ if (!Number.isInteger(perPage) || perPage < 1 || perPage > 100 || cursor !== null && !/^\d+$/.test(cursor) || status !== null && !['success', 'failure'].includes(status))
212
+ return Response.json({ message: 'Validation Failed' }, { status: 422 });
213
+ const eligible = all.filter(d => (cursor === null || d.id < Number(cursor)) && (status === null || (status === 'success'
214
+ ? d.statusCode !== null && d.statusCode >= 200 && d.statusCode <= 399
215
+ : d.statusCode !== null && d.statusCode >= 400 && d.statusCode <= 599)));
216
+ const page = eligible.slice(0, perPage);
217
+ const headers = {};
218
+ if (eligible.length > page.length) {
219
+ url.searchParams.set('cursor', String(page.at(-1).id));
220
+ headers.link = `<${url.href}>; rel="next"`;
221
+ }
222
+ return Response.json(page.map(d => deliveryJson(d, false, repoId)), { headers });
223
+ }
224
+ const delivery = all.find(d => d.id === Number(match[5]));
225
+ if (!delivery)
226
+ return missing();
227
+ if (match[6] === 'attempts' && request.method === 'POST') {
228
+ await dispatch(hook, delivery.event, JSON.parse(delivery.payloadJson), opts, delivery);
229
+ return new Response(null, { status: 202 });
230
+ }
231
+ return !match[6] && request.method === 'GET' ? Response.json(deliveryJson(delivery, true, repoId)) : missing();
232
+ }
@@ -0,0 +1,49 @@
1
+ import { type PlaneRef } from './github-git-plane.js';
2
+ import { type GithubWriteEvent } from './github-twin.js';
3
+ export type GitSmartHttpRoute = {
4
+ kind: 'info-refs';
5
+ repo: string;
6
+ } | {
7
+ kind: 'upload-pack';
8
+ repo: string;
9
+ } | {
10
+ kind: 'receive-pack';
11
+ repo: string;
12
+ };
13
+ /** Parse a smart-HTTP pathname; null when the path is not a git-protocol route. */
14
+ export declare function parseGitSmartHttpPath(method: string, pathname: string): GitSmartHttpRoute | null;
15
+ export type GitSmartHttpResponse = {
16
+ status: number;
17
+ contentType: string;
18
+ body: Uint8Array;
19
+ /** push webhook events (receive-pack only) — the server announces them. */
20
+ webhooks: GithubWriteEvent[];
21
+ };
22
+ /**
23
+ * Smart HTTP over the kernel library (`serveSmartHttp`): the advertisement, upload-pack and
24
+ * receive-pack — no `git` binary. A push's applied ref commands reach the twin through the
25
+ * reconciliation below (git_ref / branch rows, PR heads, the push webhooks).
26
+ */
27
+ export declare function handleGithubGitSmartHttp(req: {
28
+ method: string;
29
+ pathname: string;
30
+ query?: URLSearchParams;
31
+ body?: Uint8Array;
32
+ root?: string;
33
+ readOnly?: boolean;
34
+ /** request Content-Encoding (git clients gzip large POST bodies) */
35
+ contentEncoding?: string;
36
+ /** request Git-Protocol header (protocol v2 negotiation — the library answers v0, as git falls back to) */
37
+ gitProtocol?: string;
38
+ occurredAt?: string;
39
+ }): Promise<GitSmartHttpResponse | null>;
40
+ /**
41
+ * Fold the bare repo's ref changes since `before` into the kernel event log — the SAME
42
+ * git_ref.* / branch.* operations the Git Data REST writes append, so the control plane and
43
+ * the git plane converge on one ref record. Returns the `push` webhook events (one per
44
+ * changed branch/tag ref, GitHub-style ref/before/after).
45
+ */
46
+ export declare function reconcileGitPlaneRefs(repo: string, before: PlaneRef[], opts?: {
47
+ root?: string;
48
+ occurredAt?: string;
49
+ }): Promise<GithubWriteEvent[]>;
@@ -0,0 +1,185 @@
1
+ // GitHub twin git smart-HTTP — serves the REAL git wire protocol over the pack's REAL bare
2
+ // repos (github-git-plane.ts), so an UNMODIFIED `git` CLI can clone from and push to the twin:
3
+ //
4
+ // GET /:owner/:repo[.git]/info/refs?service=git-upload-pack|git-receive-pack
5
+ // POST /:owner/:repo[.git]/git-upload-pack
6
+ // POST /:owner/:repo[.git]/git-receive-pack
7
+ //
8
+ // Implementation choice — `git upload-pack|receive-pack --stateless-rpc` SPAWNED DIRECTLY,
9
+ // not `git http-backend` (CGI). Both are first-party; direct spawn is the simpler one to
10
+ // GATE: http-backend adds a CGI translation layer (GIT_PROJECT_ROOT/PATH_INFO/QUERY_STRING/
11
+ // REMOTE_USER env plumbing + CGI response-header parsing) whose plumbing would live in the
12
+ // twin and need its own tests, while contributing no protocol behavior of its own —
13
+ // http-backend itself execs exactly these two commands with exactly these flags
14
+ // (gitprotocol-http(5), "Smart Service git-upload-pack"). Direct spawn leaves ONE seam
15
+ // (this module) a mutation saboteur can kill and the fidelity oracle (a real `git` client)
16
+ // can prove end-to-end. The only protocol framing the twin adds itself is the documented
17
+ // service-advertisement prefix — one pkt-line (`# service=$servicename` LF) plus a
18
+ // flush-pkt, per gitprotocol-http(5) "smart_reply" — everything else on the wire is
19
+ // produced by real git.
20
+ //
21
+ // Reconciliation (the heart of the upgrade): after a receive-pack POST, ref changes in the
22
+ // bare repo are FOLDED into the kernel event log (git_ref.* + branch.* actions — the same
23
+ // operations the Git Data REST writes append), so a pushed branch is immediately visible to
24
+ // the REST plane: GET git/refs, GET branches, PR head/base resolution. Objects are NOT
25
+ // copied into kernel rows — the bare repo IS the object store (one store, not two); the Git
26
+ // Data REST reads fall back to it (github-twin.ts).
27
+ //
28
+ // Each branch-ref change also yields a `push` webhook event (ref/before/after), which the
29
+ // server announces as a REST contents write's is — GitHub's push reactions and the webhook.
30
+ //
31
+ // Offline + deterministic: everything here is the kernel's git library over the World's
32
+ // blob store (no network, no rate budget, no `git` binary).
33
+ import { applyTwinWrite, git, worldNow } from '@volter/world-core';
34
+ const { serveSmartHttp, serveSmartHttpPost } = git;
35
+ import { ensureBareRepo, hasBareRepo, listGitPlaneRefs, planeRepo } from "./github-git-plane.js";
36
+ import { closePrsForDeletedBranch, githubState, kernelAtomic, syncPrHeadsToBranchTip } from "./github-twin.js";
37
+ import { directPushRules } from "./semantics/push-reactions.js";
38
+ const SERVICE = 'github';
39
+ /** Parse a smart-HTTP pathname; null when the path is not a git-protocol route. */
40
+ export function parseGitSmartHttpPath(method, pathname) {
41
+ const m = /^\/([^/]+)\/([^/]+?)(?:\.git)?\/(info\/refs|git-upload-pack|git-receive-pack)$/.exec(pathname);
42
+ if (!m)
43
+ return null;
44
+ const owner = decodeURIComponent(m[1]);
45
+ const name = decodeURIComponent(m[2]);
46
+ // A name the plane's path validation would reject can never be a twin repo — fall through
47
+ // to the REST handler's vendor-shaped 404 instead of throwing a 500 out of bareRepoDir.
48
+ const SEG = /^[A-Za-z0-9_.-]+$/;
49
+ if (!SEG.test(owner) || !SEG.test(name) || owner === '.' || owner === '..' || name === '.' || name === '..')
50
+ return null;
51
+ const repo = `${owner}/${name}`;
52
+ if (m[3] === 'info/refs')
53
+ return method === 'GET' ? { kind: 'info-refs', repo } : null;
54
+ if (method !== 'POST')
55
+ return null;
56
+ return m[3] === 'git-upload-pack' ? { kind: 'upload-pack', repo } : { kind: 'receive-pack', repo };
57
+ }
58
+ function plain(status, message) {
59
+ return { status, contentType: 'text/plain; charset=utf-8', body: Buffer.from(`${message}\n`, 'utf8'), webhooks: [] };
60
+ }
61
+ /** The vendor-shaped miss: git clients render a 404 here as "repository not found". */
62
+ function repoNotFound() {
63
+ return plain(404, 'Repository not found.');
64
+ }
65
+ // The bare repo must exist to be served. A repo the CONTROL plane knows (created via REST
66
+ // before the git plane existed, or pulled from real GitHub) is materialized on first
67
+ // protocol contact — creating a repo via REST initializes its bare repo either way.
68
+ /** The repository's refs exist, or are created for a repository the twin knows (its default branch). */
69
+ function resolvePlaneRepo(repo, root) {
70
+ if (hasBareRepo(repo, root))
71
+ return true;
72
+ const known = githubState(root).repos.find((r) => r.full_name === repo);
73
+ if (!known)
74
+ return false;
75
+ ensureBareRepo(repo, root, known.default_branch ?? 'main');
76
+ return true;
77
+ }
78
+ /**
79
+ * Smart HTTP over the kernel library (`serveSmartHttp`): the advertisement, upload-pack and
80
+ * receive-pack — no `git` binary. A push's applied ref commands reach the twin through the
81
+ * reconciliation below (git_ref / branch rows, PR heads, the push webhooks).
82
+ */
83
+ export async function handleGithubGitSmartHttp(req) {
84
+ const route = parseGitSmartHttpPath(req.method, req.pathname);
85
+ if (!route)
86
+ return null;
87
+ const tail = route.kind === 'info-refs' ? 'info/refs' : route.kind === 'upload-pack' ? 'git-upload-pack' : 'git-receive-pack';
88
+ if (route.kind === 'info-refs') {
89
+ const service = req.query?.get('service') ?? '';
90
+ if (service !== 'git-upload-pack' && service !== 'git-receive-pack') {
91
+ return plain(403, 'Dumb HTTP protocol is not supported: pass ?service=git-upload-pack or ?service=git-receive-pack.');
92
+ }
93
+ if (service === 'git-receive-pack' && req.readOnly)
94
+ return plain(403, 'twin is read-only (mirror mode); pushes are rejected');
95
+ }
96
+ if (route.kind === 'receive-pack' && req.readOnly)
97
+ return plain(403, 'twin is read-only (mirror mode); pushes are rejected');
98
+ if (!resolvePlaneRepo(route.repo, req.root))
99
+ return repoNotFound();
100
+ const before = route.kind === 'receive-pack' ? await listGitPlaneRefs(route.repo, req.root) : [];
101
+ // POST hands the kernel the body already in hand; a Request round-trip would copy the whole pack.
102
+ const answer = route.kind === 'info-refs'
103
+ ? await serveSmartHttp(new Request(`http://twin/${route.repo}.git/info/refs?service=${req.query?.get('service') ?? ''}`), planeRepo(route.repo, req.root), tail)
104
+ : await serveSmartHttpPost(req.body ?? new Uint8Array(), req.contentEncoding ?? '', { ...planeRepo(route.repo, req.root), policy: rulesPolicy(route.repo, req.root) }, tail);
105
+ if (!answer)
106
+ return null;
107
+ const body = new Uint8Array(await answer.arrayBuffer());
108
+ const webhooks = route.kind === 'receive-pack' && answer.status === 200
109
+ ? await reconcileGitPlaneRefs(route.repo, before, { ...(req.root !== undefined ? { root: req.root } : {}), ...(req.occurredAt !== undefined ? { occurredAt: req.occurredAt } : {}) })
110
+ : [];
111
+ return { status: answer.status, contentType: answer.headers.get('content-type') ?? 'application/octet-stream', body, webhooks };
112
+ }
113
+ const ZERO_SHA = '0'.repeat(40);
114
+ /** An active ruleset requiring pull requests refuses a direct push to its branch, as it refuses an API commit
115
+ * (semantics/push-reactions.ts directPushRules); git reports the ref `ng` with GitHub's reason. */
116
+ function rulesPolicy(repo, root) {
117
+ return (cmd) => {
118
+ if (!cmd.name.startsWith('refs/heads/') || cmd.new === ZERO_SHA)
119
+ return null;
120
+ return directPushRules(githubState(root), repo, cmd.name.slice('refs/heads/'.length)).refused ? 'push declined due to repository rule violations' : null;
121
+ };
122
+ }
123
+ // Per-write ordinal folded into every reconcile write's fields: the kernel dedupes actions
124
+ // by content + occurredAt millisecond, so a ref REVISITING a value under a pinned clock
125
+ // (force-push A→B→A replayed in one ms) would silently collide with its own earlier action
126
+ // and leave the projection at B while the bare repo says A (docs/contributing/adding-a-twin.md §5 — the
127
+ // upstash `rev` pattern). Strictly increasing within the process; harmless extra field.
128
+ let reconcileSeq = 0; // counter: strictly increasing within the process; never state
129
+ /**
130
+ * Fold the bare repo's ref changes since `before` into the kernel event log — the SAME
131
+ * git_ref.* / branch.* operations the Git Data REST writes append, so the control plane and
132
+ * the git plane converge on one ref record. Returns the `push` webhook events (one per
133
+ * changed branch/tag ref, GitHub-style ref/before/after).
134
+ */
135
+ export async function reconcileGitPlaneRefs(repo, before, opts = {}) {
136
+ const root = opts.root;
137
+ const occurredAt = opts.occurredAt ?? worldNow();
138
+ const after = await listGitPlaneRefs(repo, root);
139
+ const beforeBy = new Map(before.map((r) => [r.ref, r]));
140
+ const afterBy = new Map(after.map((r) => [r.ref, r]));
141
+ const events = [];
142
+ const branchName = (ref) => (ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : null);
143
+ for (const r of after) {
144
+ const prev = beforeBy.get(r.ref);
145
+ if (prev && prev.object_sha === r.object_sha)
146
+ continue;
147
+ await applyTwinWrite(SERVICE, {
148
+ operation: prev ? 'git_ref.update' : 'git_ref.create', subjectType: 'git_ref', subjectId: `gitref:${repo}#${r.ref}`,
149
+ fields: { repository: repo, ref: r.ref, object_sha: r.object_sha, object_type: r.object_type === 'tag' ? 'tag' : 'commit', rev: ++reconcileSeq },
150
+ occurredAt, actor: { kind: 'agent' },
151
+ }, root);
152
+ const name = branchName(r.ref);
153
+ if (name) {
154
+ await applyTwinWrite(SERVICE, {
155
+ operation: 'branch.create', subjectType: 'branch', subjectId: `branch:${repo}#${name}`,
156
+ fields: { repository: repo, name, commit_sha: r.object_sha, rev: ++reconcileSeq },
157
+ occurredAt, actor: { kind: 'agent' },
158
+ }, root);
159
+ // GitHub's `synchronize`: a push to a PR's head branch moves the PR head — the old
160
+ // head's statuses/reviews stop matching (exact-head re-earn).
161
+ await syncPrHeadsToBranchTip(repo, name, r.object_sha, root, occurredAt, (pr, base) => events.push({ event: 'pull_request', action: 'synchronize', repository: base, number: Number(pr.number), pr }));
162
+ }
163
+ events.push({ event: 'push', action: '', repository: repo, number: 0, ref: r.ref, before: prev?.object_sha ?? ZERO_SHA, after: r.object_sha });
164
+ }
165
+ for (const r of before) {
166
+ if (afterBy.has(r.ref))
167
+ continue;
168
+ await applyTwinWrite(SERVICE, {
169
+ operation: 'git_ref.delete', subjectType: 'git_ref', subjectId: `gitref:${repo}#${r.ref}`,
170
+ fields: { deleted: true, repository: repo, ref: r.ref, rev: ++reconcileSeq }, occurredAt, actor: { kind: 'agent' },
171
+ }, root);
172
+ const name = branchName(r.ref);
173
+ if (name) {
174
+ await applyTwinWrite(SERVICE, {
175
+ operation: 'branch.delete', subjectType: 'branch', subjectId: `branch:${repo}#${name}`,
176
+ fields: { deleted: true, repository: repo, name, rev: ++reconcileSeq }, occurredAt, actor: { kind: 'agent' },
177
+ }, root);
178
+ // Real GitHub CLOSES an open PR whose head branch is deleted (shared with the REST
179
+ // DELETE .../git/refs path — both converge on the same helper).
180
+ await closePrsForDeletedBranch(repo, name, root, occurredAt, (pr, base) => events.push({ event: 'pull_request', action: 'closed', repository: base, number: Number(pr.number), pr }), kernelAtomic(occurredAt, root), { operation: 'git-receive-pack', actor: 'external' });
181
+ }
182
+ events.push({ event: 'push', action: '', repository: repo, number: 0, ref: r.ref, before: r.object_sha, after: ZERO_SHA });
183
+ }
184
+ return events;
185
+ }
@@ -0,0 +1,114 @@
1
+ import { git } from '@volter/world-core';
2
+ declare const GitObjectStore: typeof git.GitObjectStore, GitRefs: typeof git.GitRefs;
3
+ type GitObjectStore = InstanceType<typeof git.GitObjectStore>;
4
+ type GitRefs = InstanceType<typeof git.GitRefs>;
5
+ /** The git plane root for a twin instance: `<state>/github/git`. */
6
+ export declare function githubGitPlaneDir(root?: string): string;
7
+ /** A repository's ref prefix in the world store — `<plane>/<owner>/<name>.git` — after validating the name. */
8
+ export declare function bareRepoDir(repo: string, root?: string): string;
9
+ export declare function hasBareRepo(repo: string, root?: string): boolean;
10
+ /** Create the repository's refs (HEAD → the default branch) when absent; answers its prefix. */
11
+ export declare function ensureBareRepo(repo: string, root?: string, defaultBranch?: string): string;
12
+ export declare function objectExistsInPlane(repo: string, sha: string, root?: string): Promise<boolean>;
13
+ export declare function objectTypeInPlane(repo: string, sha: string, root?: string): Promise<string | null>;
14
+ export declare function writeGitBlobToPlane(repo: string, contentB64: string, root?: string): Promise<string>;
15
+ export declare function readGitBlobFromPlane(repo: string, sha: string, root?: string): Promise<{
16
+ content_b64: string;
17
+ size: number;
18
+ } | null>;
19
+ export type PlaneTreeEntry = {
20
+ path: string;
21
+ mode: string;
22
+ type: string;
23
+ sha: string;
24
+ size?: number;
25
+ };
26
+ /** A tree from `entries` over `baseTreeSha` (an entry with no sha deletes its path). */
27
+ export declare function writeGitTreeToPlane(repo: string, entries: PlaneTreeEntry[], baseTreeSha: string | undefined, root?: string): Promise<string>;
28
+ /** `ls-tree -l`: one level, blob sizes measured. */
29
+ export declare function readGitTreeFromPlane(repo: string, sha: string, root?: string): Promise<PlaneTreeEntry[] | null>;
30
+ export type PlaneIdent = {
31
+ name: string;
32
+ email: string;
33
+ date?: string;
34
+ };
35
+ export declare function writeGitCommitToPlane(repo: string, commit: {
36
+ message: string;
37
+ treeSha: string;
38
+ parents: string[];
39
+ author?: PlaneIdent;
40
+ committer?: PlaneIdent;
41
+ occurredAt: string;
42
+ }, root?: string): Promise<string>;
43
+ export type PlaneCommit = {
44
+ sha: string;
45
+ message: string;
46
+ tree_sha: string;
47
+ parents: string[];
48
+ author_name?: string;
49
+ author_email?: string;
50
+ created_at?: string;
51
+ };
52
+ export declare function readGitCommitFromPlane(repo: string, sha: string, root?: string): Promise<PlaneCommit | null>;
53
+ export declare function writeGitTagToPlane(repo: string, tag: {
54
+ tag: string;
55
+ message: string;
56
+ objectSha: string;
57
+ objectType: string;
58
+ tagger?: PlaneIdent;
59
+ occurredAt: string;
60
+ }, root?: string): Promise<string>;
61
+ export type PlaneTag = {
62
+ sha: string;
63
+ tag: string;
64
+ message: string;
65
+ object_sha: string;
66
+ object_type: string;
67
+ tagger_name?: string;
68
+ created_at?: string;
69
+ };
70
+ export declare function readGitTagFromPlane(repo: string, sha: string, root?: string): Promise<PlaneTag | null>;
71
+ /** Point `ref` at `sha` — only at an object the plane holds whole (a dangling chain is not clonable). */
72
+ export declare function updateGitRefInPlane(repo: string, ref: string, sha: string, root?: string, expected?: string | null): Promise<boolean>;
73
+ export declare function deleteGitRefInPlane(repo: string, ref: string, root?: string): Promise<void>;
74
+ export type PlaneRef = {
75
+ ref: string;
76
+ object_sha: string;
77
+ object_type: string;
78
+ };
79
+ export declare function setBareRepoHead(repo: string, branch: string, root?: string): void;
80
+ /** Drop the repository's refs; its objects stay in the world's store (content-addressed, shared). */
81
+ export declare function deleteBareRepo(repo: string, root?: string): void;
82
+ /** A fork shares the world's object store: copy the refs and HEAD, nothing else. */
83
+ export declare function forkBareRepo(parentRepo: string, forkRepo: string, root?: string, defaultBranch?: string): void;
84
+ export declare function isAncestorInPlane(repo: string, ancestor: string, descendant: string, root?: string): Promise<boolean>;
85
+ export type PlaneDiffFile = {
86
+ filename: string;
87
+ status: string;
88
+ additions: number;
89
+ deletions: number;
90
+ sha?: string;
91
+ patch?: string;
92
+ };
93
+ export declare function mergeBaseInPlane(repo: string, a: string, b: string, root?: string): Promise<string | null>;
94
+ /** GitHub's compare: merge-base(base, head)..head, as the library's `diffFiles` computes it. */
95
+ export declare function diffFilesInPlane(repo: string, baseSha: string, headSha: string, root?: string, opts?: {
96
+ patch?: boolean;
97
+ }): Promise<PlaneDiffFile[] | null>;
98
+ export declare function isValidBranchName(name: string): boolean;
99
+ /** `for-each-ref`: every ref with its object's sha and type. */
100
+ export declare function listGitPlaneRefs(repo: string, root?: string): Promise<PlaneRef[]>;
101
+ /** The library's view of a repository, for smart HTTP. */
102
+ export declare function planeRepo(repo: string, root?: string): {
103
+ store: GitObjectStore;
104
+ refs: GitRefs;
105
+ };
106
+ /** Merge three trees, publishing the resulting ref only if its original tip still holds.
107
+ * Text edits merge against the common ancestor; no conflicted tree is published. A dry run answers
108
+ * the merged tree without writing a commit or moving the branch (a pull request's mergeability). */
109
+ export declare function mergeIntoBranchInPlane(repo: string, baseBranch: string, headSha: string, message: string, method: 'merge' | 'squash' | 'rebase', occurredAt: string, root?: string, merger?: PlaneIdent, dryRun?: boolean): Promise<{
110
+ sha: string;
111
+ } | {
112
+ conflict: string;
113
+ } | null>;
114
+ export {};