@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,407 @@
1
+ // GITHUB GIT PLANE — the pack half of the kernel's git library (the engine slot,
2
+ // docs/contributing/architecture.md#the-engine-slot). Every repository's git objects live in ONE content-addressed
3
+ // object store per world over the blob seam (R11) — a fork copies refs, never objects; a deleted
4
+ // repository drops its refs — and every repository's refs live in the world store, carried by a
5
+ // snapshot like any other world state. Trees, commits and tags go through the library's codecs;
6
+ // ancestry, merge base and file diffs through its history. No `git` binary anywhere in this module.
7
+ //
8
+ // The names and semantics are the ones the twin's REST plumbing has always called; the calls are
9
+ // async now, because the store is.
10
+ import { join } from 'node:path';
11
+ import { getActiveBlobStore, getActiveWorldStore, git, worldPaths } from '@volter/world-core';
12
+ const { GitObjectStore, GitRefs, decodeCommit, decodeTag, decodeTree, diffFiles, encodeCommit, encodeTag, encodeTree, isAncestor, mergeBase, mergeText } = git;
13
+ const SEGMENT_RE = /^[A-Za-z0-9_.-]+$/;
14
+ const SHA_RE = /^[0-9a-f]{40}$/i;
15
+ /** The git plane root for a twin instance: `<state>/github/git`. */
16
+ export function githubGitPlaneDir(root) {
17
+ return join(worldPaths('github', root).dir, 'git');
18
+ }
19
+ /** A repository's ref prefix in the world store — `<plane>/<owner>/<name>.git` — after validating the name. */
20
+ export function bareRepoDir(repo, root) {
21
+ const [owner, name, extra] = repo.split('/');
22
+ if (!owner || !name || extra !== undefined || !SEGMENT_RE.test(owner) || !SEGMENT_RE.test(name) || owner === '..' || name === '..' || owner === '.' || name === '.') {
23
+ throw new Error(`github twin git plane: invalid repository name ${JSON.stringify(repo)}`);
24
+ }
25
+ return join(githubGitPlaneDir(root), owner, `${name}.git`);
26
+ }
27
+ /** ONE object store per world: content-addressed, shared by every repository of the world. */
28
+ function objectStore(root) {
29
+ return new GitObjectStore(getActiveBlobStore(), join(githubGitPlaneDir(root), 'objects'));
30
+ }
31
+ function refsOf(repo, root) {
32
+ return new GitRefs(getActiveWorldStore(), bareRepoDir(repo, root));
33
+ }
34
+ export function hasBareRepo(repo, root) {
35
+ return getActiveWorldStore().exists(join(bareRepoDir(repo, root), 'HEAD'));
36
+ }
37
+ /** Create the repository's refs (HEAD → the default branch) when absent; answers its prefix. */
38
+ export function ensureBareRepo(repo, root, defaultBranch = 'main') {
39
+ const dir = bareRepoDir(repo, root);
40
+ if (!hasBareRepo(repo, root))
41
+ refsOf(repo, root).setHead(`refs/heads/${defaultBranch}`);
42
+ return dir;
43
+ }
44
+ export async function objectExistsInPlane(repo, sha, root) {
45
+ if (!hasBareRepo(repo, root) || !SHA_RE.test(sha))
46
+ return false;
47
+ return objectStore(root).has(sha.toLowerCase());
48
+ }
49
+ export async function objectTypeInPlane(repo, sha, root) {
50
+ if (!hasBareRepo(repo, root) || !SHA_RE.test(sha))
51
+ return null;
52
+ const obj = await objectStore(root).read(sha.toLowerCase());
53
+ return obj?.type ?? null;
54
+ }
55
+ export async function writeGitBlobToPlane(repo, contentB64, root) {
56
+ ensureBareRepo(repo, root);
57
+ return objectStore(root).write('blob', new Uint8Array(Buffer.from(contentB64, 'base64')));
58
+ }
59
+ export async function readGitBlobFromPlane(repo, sha, root) {
60
+ if (!hasBareRepo(repo, root) || !SHA_RE.test(sha))
61
+ return null;
62
+ const obj = await objectStore(root).read(sha.toLowerCase());
63
+ if (!obj || obj.type !== 'blob')
64
+ return null;
65
+ return { content_b64: Buffer.from(obj.payload).toString('base64'), size: obj.payload.length };
66
+ }
67
+ const isTreeMode = (mode) => mode.replace(/^0+(?=\d)/, '') === '40000';
68
+ const modeOut = (mode) => (isTreeMode(mode) ? '040000' : mode);
69
+ const typeOfMode = (mode) => (isTreeMode(mode) ? 'tree' : mode.replace(/^0+/, '') === '160000' ? 'commit' : 'blob');
70
+ /** `ls-tree -r`: every blob under `sha`, keyed by its full path. */
71
+ async function lsTreeRecursive(store, sha, prefix = '') {
72
+ const out = new Map();
73
+ const obj = await store.read(sha);
74
+ if (!obj || obj.type !== 'tree')
75
+ return out;
76
+ for (const e of decodeTree(obj.payload)) {
77
+ const path = prefix ? `${prefix}/${e.name}` : e.name;
78
+ if (isTreeMode(e.mode)) {
79
+ for (const [p, v] of await lsTreeRecursive(store, e.sha, path))
80
+ out.set(p, v);
81
+ }
82
+ else
83
+ out.set(path, { mode: e.mode, type: typeOfMode(e.mode), sha: e.sha });
84
+ }
85
+ return out;
86
+ }
87
+ /** `mktree`, level by level, from a flat path → entry map. */
88
+ async function buildTreeFromFlat(store, flat) {
89
+ const rootNode = { entries: new Map(), children: new Map() };
90
+ for (const [path, e] of flat) {
91
+ const parts = path.split('/');
92
+ let node = rootNode;
93
+ for (let i = 0; i < parts.length - 1; i++) {
94
+ const seg = parts[i];
95
+ if (!node.children.has(seg))
96
+ node.children.set(seg, { entries: new Map(), children: new Map() });
97
+ node = node.children.get(seg);
98
+ }
99
+ node.entries.set(parts[parts.length - 1], { mode: e.mode, sha: e.sha });
100
+ }
101
+ const write = async (node) => {
102
+ const level = [];
103
+ for (const [name, child] of node.children)
104
+ level.push({ name, mode: '40000', sha: await write(child) });
105
+ for (const [name, e] of node.entries)
106
+ level.push({ name, mode: e.mode.replace(/^0+(?=\d)/, ''), sha: e.sha });
107
+ return store.write('tree', encodeTree(level));
108
+ };
109
+ return write(rootNode);
110
+ }
111
+ /** A tree from `entries` over `baseTreeSha` (an entry with no sha deletes its path). */
112
+ export async function writeGitTreeToPlane(repo, entries, baseTreeSha, root) {
113
+ ensureBareRepo(repo, root);
114
+ const store = objectStore(root);
115
+ const flat = baseTreeSha && (await objectTypeInPlane(repo, baseTreeSha, root)) === 'tree'
116
+ ? await lsTreeRecursive(store, baseTreeSha.toLowerCase())
117
+ : new Map();
118
+ for (const e of entries) {
119
+ if (!e.sha) {
120
+ flat.delete(e.path);
121
+ continue;
122
+ }
123
+ flat.set(e.path, { mode: e.mode, type: e.type, sha: e.sha });
124
+ }
125
+ return buildTreeFromFlat(store, flat);
126
+ }
127
+ /** `ls-tree -l`: one level, blob sizes measured. */
128
+ export async function readGitTreeFromPlane(repo, sha, root) {
129
+ if (!hasBareRepo(repo, root) || !SHA_RE.test(sha))
130
+ return null;
131
+ const store = objectStore(root);
132
+ const obj = await store.read(sha.toLowerCase());
133
+ if (!obj || obj.type !== 'tree')
134
+ return null;
135
+ const out = [];
136
+ for (const e of decodeTree(obj.payload)) {
137
+ const entry = { path: e.name, mode: modeOut(e.mode), type: typeOfMode(e.mode), sha: e.sha };
138
+ if (entry.type === 'blob') {
139
+ const b = await store.read(e.sha);
140
+ if (b)
141
+ entry.size = b.payload.length;
142
+ }
143
+ out.push(entry);
144
+ }
145
+ return out;
146
+ }
147
+ const TWIN_IDENT = { name: 'github-twin', email: 'github-twin@localhost' };
148
+ const identOf = (ident, occurredAt) => {
149
+ const who = ident ?? TWIN_IDENT;
150
+ return { name: who.name, email: who.email, time: Math.floor((Date.parse(who.date ?? occurredAt) || 0) / 1000), tz: '+0000' };
151
+ };
152
+ export async function writeGitCommitToPlane(repo, commit, root) {
153
+ ensureBareRepo(repo, root);
154
+ const c = { tree: commit.treeSha, parents: commit.parents, author: identOf(commit.author, commit.occurredAt), committer: identOf(commit.committer ?? commit.author, commit.occurredAt), message: commit.message };
155
+ return objectStore(root).write('commit', encodeCommit(c));
156
+ }
157
+ export async function readGitCommitFromPlane(repo, sha, root) {
158
+ if (!hasBareRepo(repo, root) || !SHA_RE.test(sha))
159
+ return null;
160
+ const obj = await objectStore(root).read(sha.toLowerCase());
161
+ if (!obj || obj.type !== 'commit')
162
+ return null;
163
+ const c = decodeCommit(obj.payload);
164
+ return { sha: sha.toLowerCase(), message: c.message.replace(/\n$/, ''), tree_sha: c.tree, parents: c.parents, author_name: c.author.name, author_email: c.author.email, created_at: new Date(c.author.time * 1000).toISOString() };
165
+ }
166
+ export async function writeGitTagToPlane(repo, tag, root) {
167
+ ensureBareRepo(repo, root);
168
+ return objectStore(root).write('tag', encodeTag({ object: tag.objectSha, type: tag.objectType, tag: tag.tag, tagger: identOf(tag.tagger, tag.occurredAt), message: tag.message }));
169
+ }
170
+ export async function readGitTagFromPlane(repo, sha, root) {
171
+ if (!hasBareRepo(repo, root) || !SHA_RE.test(sha))
172
+ return null;
173
+ const obj = await objectStore(root).read(sha.toLowerCase());
174
+ return !obj || obj.type !== 'tag' ? null : annotatedTag(sha, obj.payload);
175
+ }
176
+ /** An annotated tag object's fields. */
177
+ function annotatedTag(sha, payload) {
178
+ const t = decodeTag(payload);
179
+ const out = { sha: sha.toLowerCase(), tag: t.tag, message: t.message.replace(/\n$/, ''), object_sha: t.object, object_type: t.type };
180
+ if (t.tagger) {
181
+ out.tagger_name = t.tagger.name;
182
+ out.created_at = new Date(t.tagger.time * 1000).toISOString();
183
+ }
184
+ return out;
185
+ }
186
+ // Objects are immutable, so a chain once found whole stays whole while its objects are kept: the walk stops at
187
+ // an object an earlier walk proved (still present), as git's connectivity check stops at what it already has.
188
+ // Without it every ref update re-read the repository's whole history.
189
+ const provenWhole = new WeakMap();
190
+ /** Every object `sha` reaches (commits through parents and trees, trees through entries) is present. */
191
+ async function chainComplete(store, sha, root, limit = 20000) {
192
+ const blobs = getActiveBlobStore();
193
+ const planes = provenWhole.get(blobs) ?? provenWhole.set(blobs, new Map()).get(blobs);
194
+ const plane = githubGitPlaneDir(root);
195
+ const proven = planes.get(plane) ?? planes.set(plane, new Set()).get(plane);
196
+ const seen = new Set();
197
+ const stack = [sha];
198
+ while (stack.length) {
199
+ const s = stack.pop();
200
+ if (seen.has(s))
201
+ continue;
202
+ seen.add(s);
203
+ if (seen.size > limit)
204
+ return true; // bounded: a huge history is not a dangling one
205
+ if (proven.has(s)) {
206
+ if (await store.has(s))
207
+ continue;
208
+ proven.clear();
209
+ return false;
210
+ }
211
+ const obj = await store.read(s);
212
+ if (!obj)
213
+ return false;
214
+ if (obj.type === 'commit') {
215
+ const c = decodeCommit(obj.payload);
216
+ stack.push(c.tree, ...c.parents);
217
+ }
218
+ else if (obj.type === 'tree') {
219
+ for (const e of decodeTree(obj.payload))
220
+ if (!/^160000$/.test(e.mode.replace(/^0+/, '')))
221
+ stack.push(e.sha);
222
+ }
223
+ else if (obj.type === 'tag')
224
+ stack.push(decodeTag(obj.payload).object);
225
+ }
226
+ for (const s of seen)
227
+ proven.add(s);
228
+ return true;
229
+ }
230
+ /** Point `ref` at `sha` — only at an object the plane holds whole (a dangling chain is not clonable). */
231
+ export async function updateGitRefInPlane(repo, ref, sha, root, expected) {
232
+ if (!hasBareRepo(repo, root) || !SHA_RE.test(sha))
233
+ return false;
234
+ const store = objectStore(root);
235
+ if (!(await store.has(sha.toLowerCase())) || !(await chainComplete(store, sha.toLowerCase(), root)))
236
+ return false;
237
+ const refs = refsOf(repo, root);
238
+ return refs.withLock(() => {
239
+ if (expected !== undefined && refs.get(ref) !== expected)
240
+ return false;
241
+ refs.set(ref, sha.toLowerCase());
242
+ return true;
243
+ });
244
+ }
245
+ export async function deleteGitRefInPlane(repo, ref, root) {
246
+ if (!hasBareRepo(repo, root))
247
+ return;
248
+ refsOf(repo, root).delete(ref);
249
+ }
250
+ export function setBareRepoHead(repo, branch, root) {
251
+ if (!hasBareRepo(repo, root))
252
+ return;
253
+ refsOf(repo, root).setHead(`refs/heads/${branch}`);
254
+ }
255
+ /** Drop the repository's refs; its objects stay in the world's store (content-addressed, shared). */
256
+ export function deleteBareRepo(repo, root) {
257
+ if (!hasBareRepo(repo, root))
258
+ return;
259
+ const refs = refsOf(repo, root);
260
+ for (const r of refs.list())
261
+ refs.delete(r.name);
262
+ getActiveWorldStore().remove(join(bareRepoDir(repo, root), 'HEAD'));
263
+ }
264
+ /** A fork shares the world's object store: copy the refs and HEAD, nothing else. */
265
+ export function forkBareRepo(parentRepo, forkRepo, root, defaultBranch = 'main') {
266
+ bareRepoDir(forkRepo, root); // validates the fork name before any copy
267
+ if (hasBareRepo(forkRepo, root))
268
+ return;
269
+ if (!hasBareRepo(parentRepo, root)) {
270
+ ensureBareRepo(forkRepo, root, defaultBranch);
271
+ return;
272
+ }
273
+ const from = refsOf(parentRepo, root);
274
+ const to = refsOf(forkRepo, root);
275
+ to.setHead(from.head());
276
+ for (const r of from.list())
277
+ to.set(r.name, r.sha);
278
+ }
279
+ export async function isAncestorInPlane(repo, ancestor, descendant, root) {
280
+ if (!hasBareRepo(repo, root) || !SHA_RE.test(ancestor) || !SHA_RE.test(descendant))
281
+ return false;
282
+ return isAncestor(objectStore(root), ancestor.toLowerCase(), descendant.toLowerCase());
283
+ }
284
+ export async function mergeBaseInPlane(repo, a, b, root) {
285
+ if (!(await objectExistsInPlane(repo, a, root)) || !(await objectExistsInPlane(repo, b, root)))
286
+ return null;
287
+ return mergeBase(objectStore(root), a.toLowerCase(), b.toLowerCase());
288
+ }
289
+ /** GitHub's compare: merge-base(base, head)..head, as the library's `diffFiles` computes it. */
290
+ export async function diffFilesInPlane(repo, baseSha, headSha, root, opts = {}) {
291
+ if (!(await objectExistsInPlane(repo, baseSha, root)) || !(await objectExistsInPlane(repo, headSha, root)))
292
+ return null;
293
+ const files = await diffFiles(objectStore(root), baseSha.toLowerCase(), headSha.toLowerCase(), opts);
294
+ return files === null ? null : files.map((f) => ({ filename: f.filename, status: f.status, additions: f.additions, deletions: f.deletions, ...(f.sha !== undefined ? { sha: f.sha } : {}), ...(f.patch !== undefined ? { patch: f.patch } : {}) }));
295
+ }
296
+ export function isValidBranchName(name) {
297
+ if (!name || name.length > 250)
298
+ return false;
299
+ if (/[\s~^:?*[\\\x00-\x1f\x7f]/.test(name))
300
+ return false;
301
+ if (name.includes('..') || name.includes('@{') || name.includes('//'))
302
+ return false;
303
+ if (name.startsWith('/') || name.endsWith('/') || name.startsWith('.') || name.endsWith('.') || name.endsWith('.lock'))
304
+ return false;
305
+ return true;
306
+ }
307
+ /** `for-each-ref`: every ref with its object's sha and type. */
308
+ export async function listGitPlaneRefs(repo, root) {
309
+ if (!hasBareRepo(repo, root))
310
+ return [];
311
+ const store = objectStore(root);
312
+ const out = [];
313
+ for (const r of refsOf(repo, root).list()) {
314
+ const obj = await store.read(r.sha);
315
+ out.push({ ref: r.name, object_sha: r.sha, object_type: obj?.type ?? 'commit' });
316
+ }
317
+ return out;
318
+ }
319
+ /** The library's view of a repository, for smart HTTP. */
320
+ export function planeRepo(repo, root) {
321
+ return { store: objectStore(root), refs: refsOf(repo, root) };
322
+ }
323
+ /** A file both sides of a merge changed: its three texts merged into one blob, or nothing when they conflict (a side
324
+ * that is not a text blob, modes that disagree, or edits that overlap). */
325
+ async function mergeBothSides(store, original, ours, theirs) {
326
+ if (!ours || !theirs || !original || ours.type !== 'blob' || theirs.type !== 'blob' || original.type !== 'blob')
327
+ return null;
328
+ const mode = ours.mode === theirs.mode ? ours.mode : ours.mode === original.mode ? theirs.mode : theirs.mode === original.mode ? ours.mode : null;
329
+ if (!mode || !mode.startsWith('100'))
330
+ return null;
331
+ const objects = await Promise.all([original, ours, theirs].map(entry => store.read(entry.sha)));
332
+ if (objects.some(obj => !obj || obj.type !== 'blob' || obj.payload.includes(0)))
333
+ return null;
334
+ let contents;
335
+ try {
336
+ contents = objects.map(obj => new TextDecoder('utf-8', { fatal: true }).decode(obj.payload));
337
+ }
338
+ catch {
339
+ return null;
340
+ }
341
+ const content = mergeText(contents[0], contents[1], contents[2]);
342
+ return content === null ? null : { mode, type: 'blob', sha: await store.write('blob', new TextEncoder().encode(content)) };
343
+ }
344
+ /** Merge three trees, publishing the resulting ref only if its original tip still holds.
345
+ * Text edits merge against the common ancestor; no conflicted tree is published. A dry run answers
346
+ * the merged tree without writing a commit or moving the branch (a pull request's mergeability). */
347
+ export async function mergeIntoBranchInPlane(repo, baseBranch, headSha, message, method, occurredAt, root, merger, dryRun = false) {
348
+ if (!hasBareRepo(repo, root))
349
+ return null;
350
+ const refs = refsOf(repo, root);
351
+ const ref = `refs/heads/${baseBranch}`;
352
+ const baseSha = refs.get(ref);
353
+ if (!baseSha)
354
+ return null;
355
+ const store = objectStore(root);
356
+ const base = await readGitCommitFromPlane(repo, baseSha, root);
357
+ const head = await readGitCommitFromPlane(repo, headSha, root);
358
+ if (!base || !head)
359
+ return { conflict: 'missing commit' };
360
+ if (await isAncestor(store, headSha, baseSha))
361
+ return { sha: baseSha };
362
+ const ancestorSha = await mergeBase(store, baseSha, headSha);
363
+ if (!ancestorSha)
364
+ return { conflict: 'unrelated histories' };
365
+ const ancestor = ancestorSha ? await readGitCommitFromPlane(repo, ancestorSha, root) : null;
366
+ const before = ancestor ? await lsTreeRecursive(store, ancestor.tree_sha) : new Map();
367
+ const left = await lsTreeRecursive(store, base.tree_sha);
368
+ const right = await lsTreeRecursive(store, head.tree_sha);
369
+ const merged = new Map(left);
370
+ const same = (a, b) => a?.mode === b?.mode && a?.sha === b?.sha;
371
+ for (const path of new Set([...before.keys(), ...left.keys(), ...right.keys()])) {
372
+ const original = before.get(path), ours = left.get(path), theirs = right.get(path);
373
+ if (same(ours, theirs) || same(theirs, original))
374
+ continue;
375
+ // both sides changed the file: its texts merge against the ancestor's, or the merge conflicts
376
+ if (!same(ours, original)) {
377
+ const both = await mergeBothSides(store, original, ours, theirs);
378
+ if (!both)
379
+ return { conflict: path };
380
+ merged.set(path, both);
381
+ continue;
382
+ }
383
+ if (theirs)
384
+ merged.set(path, theirs);
385
+ else
386
+ merged.delete(path);
387
+ }
388
+ // A file/directory collision must not produce a tree containing both names.
389
+ for (const path of merged.keys()) {
390
+ const parts = path.split('/');
391
+ for (let i = 1; i < parts.length; i++)
392
+ if (merged.has(parts.slice(0, i).join('/')))
393
+ return { conflict: path };
394
+ }
395
+ const treeSha = await buildTreeFromFlat(store, merged);
396
+ if (dryRun)
397
+ return { sha: treeSha };
398
+ // GitHub authors a merge made through the API or the web as the person who merged, committed by GitHub itself.
399
+ const sha = await writeGitCommitToPlane(repo, { message, treeSha, parents: method === 'merge' ? [baseSha, headSha] : [baseSha], occurredAt,
400
+ ...(merger ? { author: merger, committer: { name: 'GitHub', email: 'noreply@github.com' } } : {}) }, root);
401
+ return refs.withLock(() => {
402
+ if (refs.get(ref) !== baseSha)
403
+ return { conflict: 'base moved' };
404
+ refs.set(ref, sha);
405
+ return { sha };
406
+ });
407
+ }
@@ -0,0 +1,2 @@
1
+ /** The `mirror` store: the whole console payload, projected from the twin's github world. */
2
+ export declare function githubMirrorState(root?: string): Record<string, unknown>;