@volter/world-core 3.0.164 → 3.0.165
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.
- package/dist/generated/pack-facts.json +44 -3660
- package/dist/pack-facts.cjs +4 -4
- package/dist/src/browser.d.ts +4 -0
- package/dist/src/browser.js +10 -0
- package/dist/src/derived-core.d.ts +45 -5
- package/dist/src/derived-core.js +53 -8
- package/dist/src/derived-real.js +144 -2
- package/dist/src/derived.d.ts +1 -1
- package/dist/src/executor.d.ts +2 -2
- package/dist/src/executor.js +13 -0
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/smart-http-client.d.ts +28 -0
- package/dist/src/git/smart-http-client.js +95 -0
- package/dist/src/git/smart-http.d.ts +3 -0
- package/dist/src/git/smart-http.js +9 -0
- package/dist/src/head.d.ts +16 -1
- package/dist/src/head.js +52 -7
- package/dist/src/html.d.ts +29 -0
- package/dist/src/html.js +64 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -0
- package/dist/src/pack-fetch.js +41 -2
- package/dist/src/pack-paths.d.ts +2 -3
- package/dist/src/pack-paths.js +3 -13
- package/dist/src/packRegistry.d.ts +17 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/serve-http.d.ts +1 -1
- package/dist/src/serve-http.js +11 -2
- package/dist/src/smtp-server.d.ts +78 -0
- package/dist/src/smtp-server.js +160 -0
- package/dist/src/smtp.d.ts +29 -11
- package/dist/src/smtp.js +221 -52
- package/generated/pack-facts.json +44 -3660
- package/pack-facts.cjs +4 -4
- package/package.json +4 -2
- package/src/browser.ts +10 -0
- package/src/derived-core.ts +68 -12
- package/src/derived-real.ts +121 -2
- package/src/derived.ts +2 -2
- package/src/executor.ts +13 -2
- package/src/git/index.ts +1 -0
- package/src/git/smart-http-client.ts +99 -0
- package/src/git/smart-http.ts +7 -0
- package/src/head.ts +37 -6
- package/src/html.ts +72 -0
- package/src/index.ts +4 -1
- package/src/pack-fetch.ts +35 -2
- package/src/pack-paths.ts +3 -11
- package/src/packRegistry.ts +9 -1
- package/src/runtime.ts +3 -0
- package/src/serve-http.ts +11 -3
- package/src/smtp-server.ts +166 -0
- package/src/smtp.ts +199 -53
package/pack-facts.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// THE PACK FACTS a process routes by (docs/contributing/architecture.md, "The catalog: where twins come from"): the facts
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// the platform owns (generated/pack-facts.json: its server backings, scripts/pack-facts.ts), overlaid by the facts of the
|
|
3
|
+
// packs the World installs (ADR 0034, decision 2). Each pack carries its own (`generated/pack-facts.json`), and the
|
|
4
|
+
// World's instance file (VOLTER_WORLD_INSTANCE, which every World process already has) names them as `packFacts`; a
|
|
5
|
+
// pack the World has not installed is unknown to it. Plain CommonJS, no
|
|
6
6
|
// dependency: the injector preloads it. Read once per process.
|
|
7
7
|
'use strict';
|
|
8
8
|
const fs = require('fs');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.165",
|
|
4
4
|
"description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -169,6 +169,7 @@
|
|
|
169
169
|
"zod": "^4.4.2"
|
|
170
170
|
},
|
|
171
171
|
"devDependencies": {
|
|
172
|
+
"@volter/twin-stripe": "3.0.6",
|
|
172
173
|
"@types/bun": "^1.2.20",
|
|
173
174
|
"@types/node": "^24.0.0",
|
|
174
175
|
"typescript": "^5.9.0",
|
|
@@ -181,7 +182,8 @@
|
|
|
181
182
|
"node": ">=22.3"
|
|
182
183
|
},
|
|
183
184
|
"optionalDependencies": {
|
|
184
|
-
"@xmldom/xmldom": "0.9.12"
|
|
185
|
+
"@xmldom/xmldom": "0.9.12",
|
|
186
|
+
"parse5": "7.3.0"
|
|
185
187
|
},
|
|
186
188
|
"packageManager": "pnpm@12.15.0",
|
|
187
189
|
"devEngines": {
|
package/src/browser.ts
CHANGED
|
@@ -60,6 +60,16 @@ function vendorResponse(response: Response, vendor: URL, twin: URL, mount: strin
|
|
|
60
60
|
return changed ? new Response(response.body, { status: response.status, statusText: response.statusText, headers }) : response;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/** The one password of an address in this World (ADR 0033 §5): `worldSecret('_world', <data>/_world,
|
|
64
|
+
* 'password:<address>')`, retained World state, so a seed signs a person up with it and a twin keeps only its hash. A
|
|
65
|
+
* seed reads it beside `browser(name)`; it is never a literal in a committed file. */
|
|
66
|
+
export async function password(address: string): Promise<string> {
|
|
67
|
+
const data = process.env.VOLTER_WORLD_DATA;
|
|
68
|
+
if (!process.env.VOLTER_WORLD || !data) throw new Error('password(address) runs in a World seed, with its data directory');
|
|
69
|
+
const { worldSecret } = await import('./derived-core.ts');
|
|
70
|
+
return worldSecret('_world', nodeBuiltin<typeof import('node:path')>('path').join(data, '_world'), `password:${address}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
/** A seed's named browser. Every state write goes through the World's single HTTP loader. */
|
|
64
74
|
export async function browser(name: string): Promise<SeedBrowser> {
|
|
65
75
|
assertBrowserName(name);
|
package/src/derived-core.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBl
|
|
|
31
31
|
import { managedDatabase, unboundDatabase, type ManagedDatabase } from './managed-database.ts';
|
|
32
32
|
import { redisServiceTimes } from './redis/service-times.ts';
|
|
33
33
|
import { execRedisRun, type RedisDialect, type RunItem } from './redis/engine.ts';
|
|
34
|
-
import { sendMail, type Mail, type
|
|
34
|
+
import { sendMail, type Mail, type MailReply } from './smtp.ts';
|
|
35
35
|
import { ownField, parseBracketForm } from './form.ts';
|
|
36
36
|
export { ownField, parseBracketForm } from './form.ts';
|
|
37
37
|
import { multipartBoundary, multipartParts, type MultipartPart } from './multipart.ts';
|
|
@@ -340,6 +340,45 @@ export type PerformHookArgs = {
|
|
|
340
340
|
};
|
|
341
341
|
export type PerformHook = (args: PerformHookArgs) => Promise<import('./head.ts').PushOutcome>;
|
|
342
342
|
|
|
343
|
+
export type GitRepositoryDecl = { resource: string; name: string; remote: string };
|
|
344
|
+
export type GitPushDecl = { resource: string; repository: string; remote: string; ref: string; before: string; after: string };
|
|
345
|
+
|
|
346
|
+
/** A template over a row's fields (`{field}`, a dotted path read through objects), every name filled or refused. */
|
|
347
|
+
export function fillTemplate(template: string, row: Record<string, unknown>): string {
|
|
348
|
+
return template.replace(/\{([^}]+)\}/g, (_, name: string) => {
|
|
349
|
+
const value = name.split('.').reduce<unknown>((at, k) => (at !== null && typeof at === 'object' ? (at as Record<string, unknown>)[k] : undefined), row);
|
|
350
|
+
if (value === undefined || value === null || typeof value === 'object') throw new Error(`template ${template}: no value for ${name}`);
|
|
351
|
+
return String(value);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** A pack's git repository `name` in a World (the store and refs `ctx.git(name)` answers), under the twin's state and,
|
|
356
|
+
* for a namespaced account, that account's. */
|
|
357
|
+
export function gitRepositoryOf(m: DerivedManifest, name: string, root?: string): { store: GitObjectStore; refs: GitRefs } {
|
|
358
|
+
if (!/^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)*$/.test(name) || name.split('/').some((seg) => seg === '.' || seg === '..')) throw new Error(`semantics: ${name} is not a git repository name`);
|
|
359
|
+
const account = accountScope(m);
|
|
360
|
+
const plane = `${worldPaths(m.service, root).dir}/git${account && account !== 'default' ? `/accounts/${encodeURIComponent(account)}` : ''}`;
|
|
361
|
+
return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** The name a repository row's code is kept under: the manifest's `git.repository.name` (a template over `{id}` alone)
|
|
365
|
+
* filled with the row's first World id, the one an adoption rebound to the vendor's (walked back through the aliases),
|
|
366
|
+
* so the screen, a refresh, a deploy and a waiting push's protection name one repository before and after adoption. */
|
|
367
|
+
export function gitNameOf(m: DerivedManifest, id: string, root?: string): string {
|
|
368
|
+
const decl = m.git?.repository;
|
|
369
|
+
if (!decl) throw new Error(`${m.vendor}: the manifest declares no git repositories`);
|
|
370
|
+
if (decl.name.replace(/\{id\}/g, '').includes('{')) throw new Error(`${m.vendor}: git.repository.name is a template over {id} alone`);
|
|
371
|
+
const type = `${storedType(m, decl.resource)}:`;
|
|
372
|
+
const aliases = aliasesNow(m.service, root);
|
|
373
|
+
let first = id;
|
|
374
|
+
for (let hops = 0; hops < 8; hops++) {
|
|
375
|
+
const from = [...aliases].find(([local, vendor]) => vendor === first && local.startsWith(type))?.[0];
|
|
376
|
+
if (from === undefined) break;
|
|
377
|
+
first = from.slice(type.length);
|
|
378
|
+
}
|
|
379
|
+
return decl.name.replace(/\{id\}/g, first);
|
|
380
|
+
}
|
|
381
|
+
|
|
343
382
|
export type DerivedManifest = {
|
|
344
383
|
/** Vendor data-plane hosts mapped to an already-running customer guest. */
|
|
345
384
|
machineRoutes?: ReadonlyArray<{ host: string; resource: string; where: Record<string, string>; name: string; port: number | string }>;
|
|
@@ -409,6 +448,12 @@ export type DerivedManifest = {
|
|
|
409
448
|
ingest?: IngestDecl;
|
|
410
449
|
/** the vendor's documented limits, charged by the executor on every live call (D8) */
|
|
411
450
|
rateBudget?: RateBudgetDeclaration;
|
|
451
|
+
/** where the vendor's repositories' code lives, as templates over a row's fields (`{field}`) (architecture, "The
|
|
452
|
+
* real-system adapters"; ADR 0031): the resource whose rows are the vendor's repositories, the name each is kept under
|
|
453
|
+
* (over `{id}` alone: the row's first World id, `gitNameOf`) and its remote (the vendor's absolute git URL), which a
|
|
454
|
+
* refresh fetches; and the resource a push to a World repository records, one row per ref, with the World id of the
|
|
455
|
+
* repository row it pushed to, which a deploy pushes */
|
|
456
|
+
git?: { repository: GitRepositoryDecl; push: GitPushDecl };
|
|
412
457
|
/** a pack whose wire has no derived perform (a socket, a line protocol, a managed database): binding a root is refused
|
|
413
458
|
* with this reason */
|
|
414
459
|
vendorBacked?: { none: string };
|
|
@@ -1399,6 +1444,17 @@ const scopeOfCall = new WeakMap<DerivedCall, CoreScope>();
|
|
|
1399
1444
|
/** The intact copy contextFor already keeps for core(), before a handler can consume its request. */
|
|
1400
1445
|
const requestOfCall = new WeakMap<DerivedCall, Request>();
|
|
1401
1446
|
|
|
1447
|
+
/** A row of the manifest's `git.push.resource` a screen writes (a client's push to a World repository) records the push
|
|
1448
|
+
* it is, which a deploy sends to the vendor as a git push (head.ts settlingEntries offers it; derived-real.ts performs
|
|
1449
|
+
* it). One an operation of the surface writes (an API commit, a ref the API creates) is that operation's, which a
|
|
1450
|
+
* deploy sends again. */
|
|
1451
|
+
function pushedInput(m: DerivedManifest, resource: string, fields: Record<string, unknown>, input: Record<string, unknown>, root: string | undefined): Record<string, unknown> {
|
|
1452
|
+
const push = m.git?.push;
|
|
1453
|
+
if (!push || push.resource !== resource || typeof input.door !== 'string') return input;
|
|
1454
|
+
const fill = (template: string): string => fillTemplate(template, fields);
|
|
1455
|
+
return { ...input, git: { name: gitNameOf(m, fill(push.repository), root), remote: fill(push.remote), ref: fill(push.ref), before: fill(push.before), after: fill(push.after) } };
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1402
1458
|
async function writeDetailed(m: DerivedManifest, call: DerivedCall, resource: string, id: string, fields: Record<string, unknown>, operation: string, params: Record<string, unknown>, root: string | undefined, occurredAt: string, checks?: StateChecks): Promise<{ body: Record<string, unknown>; id: string; vendorData?: unknown }> {
|
|
1403
1459
|
const before = stateBeforeWrite(m, resource, id, root);
|
|
1404
1460
|
const prior = counted(m, resource) ? stored(m, resource, root).find((r) => r.id === id) as Record<string, unknown> | undefined : undefined;
|
|
@@ -1408,7 +1464,7 @@ async function writeDetailed(m: DerivedManifest, call: DerivedCall, resource: st
|
|
|
1408
1464
|
// a vendor's own move (ctx.asVendor, the clock) is the World's, as the clock's writes are: no API call a head performs
|
|
1409
1465
|
inVendorMove()
|
|
1410
1466
|
? { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { clock: true }, occurredAt, actor: { kind: 'system' } }
|
|
1411
|
-
: { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent' } },
|
|
1467
|
+
: { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: pushedInput(m, resource, fields, await withFiles(recordedInput(m, call, params)), root), occurredAt, actor: { kind: 'agent' } },
|
|
1412
1468
|
root,
|
|
1413
1469
|
);
|
|
1414
1470
|
observeStateWrite(m, result, before, root, checks);
|
|
@@ -1509,7 +1565,7 @@ function decodeOffset(cursor: unknown): number {
|
|
|
1509
1565
|
|
|
1510
1566
|
/** What a coverage measurement sees of a pack's declared state logic: each move a request made and each refusal a
|
|
1511
1567
|
* guard gave, by the transition that decided it. Nothing is observed unless a measurement installs an observer
|
|
1512
|
-
* (
|
|
1568
|
+
* (`twin-standard coverage`); serving never depends on it. */
|
|
1513
1569
|
export type TransitionEntry = { action: TwinAction; field: string; actor: Actor };
|
|
1514
1570
|
export type TransitionObserver = (decl: StateField, transition: Transition, outcome: 'moved' | 'refused', from: string, entry?: TransitionEntry) => void;
|
|
1515
1571
|
let transitionObserver: TransitionObserver | undefined;
|
|
@@ -1981,15 +2037,18 @@ export type SemanticsContext = {
|
|
|
1981
2037
|
* World's repository-scoped content-addressed object store, its refs in the World's store. Served over smart HTTP
|
|
1982
2038
|
* (`git.serveSmartHttp`), read and written through the library's codecs. */
|
|
1983
2039
|
git(name: string): { store: GitObjectStore; refs: GitRefs };
|
|
2040
|
+
/** The git repository of a row of the manifest's `git.repository.resource`, under the one name it keeps through
|
|
2041
|
+
* adoption (`gitNameOf`). */
|
|
2042
|
+
gitRepository(row: Record<string, unknown>): { store: GitObjectStore; refs: GitRefs };
|
|
1984
2043
|
/** A value the World set for its application (world-env.ts): what the app was given and the vendor must agree with (a
|
|
1985
2044
|
* webhook signing secret the app verifies with), never the caller's own shell; undefined when the World sets none. */
|
|
1986
2045
|
worldEnv(name: string): string | undefined;
|
|
1987
2046
|
/** Runs `fn` as the vendor's own move, not the caller's: what time makes due (a renewal a read catches up) is
|
|
1988
2047
|
* recorded with no caller, wherever a handler reaches it. */
|
|
1989
2048
|
asVendor<T>(fn: () => Promise<T>): Promise<T>;
|
|
1990
|
-
/** A mail the vendor
|
|
1991
|
-
*
|
|
1992
|
-
mail(
|
|
2049
|
+
/** A mail the vendor hands on, over SMTP to the World's twin that receives each recipient's domain (smtp.ts, ADR 0032
|
|
2050
|
+
* §3): each recipient's reply, `250` delivered and `5xx` refused; a domain no twin receives is refused by the kernel. */
|
|
2051
|
+
mail(mail: Mail): Promise<MailReply[]>;
|
|
1993
2052
|
now(): unknown;
|
|
1994
2053
|
/** Render a stored row or vendor view using its resource declaration, retaining the stored subject id fallback. */
|
|
1995
2054
|
render(row: Record<string, unknown>, resource?: string): Record<string, unknown>;
|
|
@@ -2254,12 +2313,9 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
|
|
|
2254
2313
|
ask: (url, init, within) => askApplication(url, init, within, scope.application),
|
|
2255
2314
|
worldEnv: (name) => worldEnvValue(name),
|
|
2256
2315
|
asVendor: (fn) => runAsVendorMove(fn),
|
|
2257
|
-
git: (name) =>
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
|
|
2261
|
-
},
|
|
2262
|
-
mail: (route, mail, headers) => sendMail(route, mail, headers),
|
|
2316
|
+
git: (name) => gitRepositoryOf(m, name, root),
|
|
2317
|
+
gitRepository: (row) => gitRepositoryOf(m, gitNameOf(m, String(row.id), root), root),
|
|
2318
|
+
mail: (mail) => sendMail(mail),
|
|
2263
2319
|
now: () => now(m, at),
|
|
2264
2320
|
render: (row, resource) => {
|
|
2265
2321
|
const raw = rawResources.has(row);
|
package/src/derived-real.ts
CHANGED
|
@@ -5,12 +5,13 @@ import { parseExactJson, exactInteger, compareExactNumbers } from './exact-json.
|
|
|
5
5
|
// reaches here, only its keyed fingerprint (`PerformContext.credential`, for the idempotency key).
|
|
6
6
|
import type { TwinAction } from './actions.ts';
|
|
7
7
|
import { aliasesFrom, parentEntries, readParentTreeMap, readTree, wholeLog } from './log.ts';
|
|
8
|
-
import { storedType, type DerivedManifest, type IngestDecl, type ResourceDecl, type VendorManifest } from './derived-core.ts';
|
|
8
|
+
import { fillTemplate, gitNameOf, gitRepositoryOf, storedType, type DerivedManifest, type IngestDecl, type ResourceDecl, type VendorManifest } from './derived-core.ts';
|
|
9
9
|
import type { DerivedOperation, DerivedSurface } from './derived.ts';
|
|
10
10
|
import { verifyEvent, type EventScheme } from './events.ts';
|
|
11
11
|
import { hashFieldValue } from './hash.ts';
|
|
12
12
|
import { readResourceBlob, writeResourceBlob } from './resource-blob.ts';
|
|
13
|
-
import { RefusedWriteError, type PerformAction, type PerformContext, type PushOutcome } from './head.ts';
|
|
13
|
+
import { RefusedWriteError, settlingEntries, type PerformAction, type PerformContext, type PushOutcome } from './head.ts';
|
|
14
|
+
import { ZERO_SHA, fetchPack, isAncestor, lsRemote, sendPack, type SmartHttpTransport } from './git/index.ts';
|
|
14
15
|
import { observeResources, type Completed, type ObservedResource } from './observe.ts';
|
|
15
16
|
import type { RemoteExecute, RemoteExecuteResponse } from './remote-execute.ts';
|
|
16
17
|
import { readRoot, type StateSystemAdapters } from './state-system.ts';
|
|
@@ -411,6 +412,7 @@ function performOf(units: Unit[]): PerformAction {
|
|
|
411
412
|
return async (execute: RemoteExecute, group: TwinAction[], ctx: PerformContext): Promise<PushOutcome> => {
|
|
412
413
|
await adoptAccount(units, execute, ctx);
|
|
413
414
|
const input = (group[0]!.input ?? {}) as Row;
|
|
415
|
+
if (object(input.git)) return pushOf(units, execute, group, ctx);
|
|
414
416
|
const unit = unitOf(units, input);
|
|
415
417
|
// a protocol one request cannot express is the pack's hook, which composes the derived perform ("Protocols a
|
|
416
418
|
// request cannot express")
|
|
@@ -426,6 +428,113 @@ function performOf(units: Unit[]): PerformAction {
|
|
|
426
428
|
};
|
|
427
429
|
}
|
|
428
430
|
|
|
431
|
+
// ── git: a root's repositories over git's own smart HTTP (architecture, "The real-system adapters"; ADR 0031) ──
|
|
432
|
+
|
|
433
|
+
type PushInput = { name: string; remote: string; ref: string; before: string; after: string };
|
|
434
|
+
const pushInputOf = (e: TwinAction): PushInput | undefined => object((e.input as Row | undefined)?.git) as PushInput | undefined;
|
|
435
|
+
|
|
436
|
+
/** A 4xx other than 408 and 429 from the vendor's git: a refusal, as a REST answer's is ("Perform", 3). */
|
|
437
|
+
class GitAnswer extends Error { constructor(readonly status: number, message: string) { super(message); } }
|
|
438
|
+
|
|
439
|
+
/** Git smart HTTP to a remote (the vendor's absolute git URL) over the root's executor, which applies the credential
|
|
440
|
+
* (HostRule.basic) and charges the vendor's budget. */
|
|
441
|
+
function gitTransport(execute: RemoteExecute, remote: string): SmartHttpTransport {
|
|
442
|
+
return async ({ service, body }) => {
|
|
443
|
+
const answer = await execute({
|
|
444
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
445
|
+
path: body === undefined ? `${remote}/info/refs?service=${service}` : `${remote}/${service}`,
|
|
446
|
+
headers: {
|
|
447
|
+
'user-agent': 'git/2.0 (volter-twin)',
|
|
448
|
+
accept: body === undefined ? `application/x-${service}-advertisement` : `application/x-${service}-result`,
|
|
449
|
+
...(body === undefined ? {} : { 'content-type': `application/x-${service}-request` }),
|
|
450
|
+
},
|
|
451
|
+
...(body === undefined ? {} : { body }),
|
|
452
|
+
responseType: 'bytes',
|
|
453
|
+
});
|
|
454
|
+
if (answer.status !== 200) throw new GitAnswer(answer.status, `${answer.status}: git ${service} at ${remote}${answer.body ? `: ${answer.body.slice(0, 300)}` : ''}`);
|
|
455
|
+
return answer.bodyBytes ?? new TextEncoder().encode(answer.body);
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** DEPLOY a receive-pack's group (its push rows, one per ref): each ref's latest target decided on its own, the rest
|
|
460
|
+
* sent in one push against the root's refs (atomic when the root offers it), never forced. Each entry settles as its
|
|
461
|
+
* ref did (`PushOutcome.refused`); every ref refused refuses the group. */
|
|
462
|
+
async function pushOf(units: Unit[], execute: RemoteExecute, group: TwinAction[], ctx: PerformContext): Promise<PushOutcome> {
|
|
463
|
+
const first = group[0]!;
|
|
464
|
+
const m = units.find((u) => u.manifest.git)?.manifest;
|
|
465
|
+
const pushes = group.map((e) => ({ entry: e, push: pushInputOf(e) }));
|
|
466
|
+
const refuseAll = (reason: string): never => { throw new RefusedWriteError('vendor', reason, first.id); };
|
|
467
|
+
if (!m || pushes.some((p) => p.push === undefined)) refuseAll('the push names no repository of the vendor\'s');
|
|
468
|
+
const { name, remote } = pushes[0]!.push!;
|
|
469
|
+
if (pushes.some((p) => p.push!.name !== name || p.push!.remote !== remote)) refuseAll('one push names one repository');
|
|
470
|
+
const { store } = gitRepositoryOf(m!, name, ctx.root);
|
|
471
|
+
const transport = gitTransport(execute, remote);
|
|
472
|
+
const answered = <T>(work: Promise<T>): Promise<T> => work.catch((error: unknown) => {
|
|
473
|
+
if (error instanceof GitAnswer && error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429) refuseAll(error.message);
|
|
474
|
+
throw error;
|
|
475
|
+
});
|
|
476
|
+
const root = await answered(lsRemote(transport, 'git-receive-pack'));
|
|
477
|
+
// each ref's latest target in the group, and why the root may not take it
|
|
478
|
+
const targets = new Map<string, string>();
|
|
479
|
+
for (const p of pushes) targets.set(p.push!.ref, p.push!.after);
|
|
480
|
+
const refused = new Map<string, string>();
|
|
481
|
+
const commands: Array<{ name: string; old: string; new: string }> = [];
|
|
482
|
+
for (const [ref, target] of targets) {
|
|
483
|
+
const old = root.refs.get(ref) ?? ZERO_SHA;
|
|
484
|
+
if (old === target) continue;
|
|
485
|
+
if (target !== ZERO_SHA && old !== ZERO_SHA) {
|
|
486
|
+
if (ref.startsWith('refs/tags/')) { refused.set(ref, 'the tag already exists on the root'); continue; }
|
|
487
|
+
if (!(await store.has(old))) { refused.set(ref, 'the root holds work this World has not fetched (fetch first)'); continue; }
|
|
488
|
+
if (!(await isAncestor(store, old, target))) { refused.set(ref, `not a fast-forward of the root's ${old.slice(0, 7)}`); continue; }
|
|
489
|
+
}
|
|
490
|
+
commands.push({ name: ref, old, new: target });
|
|
491
|
+
}
|
|
492
|
+
if (commands.length) {
|
|
493
|
+
const report = await answered(sendPack(transport, store, commands, [...new Set(root.refs.values())], root.caps.includes('atomic')));
|
|
494
|
+
for (const c of commands) { const reason = report.get(c.name); if (reason !== null) refused.set(c.name, `the root refused it: ${reason}`); }
|
|
495
|
+
}
|
|
496
|
+
const took = pushes.filter((p) => !refused.has(p.push!.ref));
|
|
497
|
+
if (!took.length) refuseAll([...refused].map(([ref, reason]) => `${ref}: ${reason}`).join('; '));
|
|
498
|
+
return {
|
|
499
|
+
externalId: '', actionId: took[0]!.entry.id,
|
|
500
|
+
...(refused.size ? { refused: pushes.filter((p) => refused.has(p.push!.ref)).map((p) => ({ actionId: p.entry.id, reason: `${p.push!.ref}: ${refused.get(p.push!.ref)}` })) } : {}),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** A repository's refs a refresh will publish: the root's branches and tags, its HEAD, and the World's refs as they
|
|
505
|
+
* stood when the refresh read them. */
|
|
506
|
+
type StagedMirror = { m: DerivedManifest; name: string; theirs: Map<string, string>; head: string | null; held: Map<string, string>; heldHead: string };
|
|
507
|
+
|
|
508
|
+
/** REFRESH a repository's code, read: the root's advertisement and the objects of its branches and tags the World's
|
|
509
|
+
* repository lacks (immutable: kept as they arrive). Its refs are staged, published by `publishMirror` once every read of
|
|
510
|
+
* the refresh succeeded. */
|
|
511
|
+
async function readMirror(m: DerivedManifest, execute: RemoteExecute, root: string | undefined, name: string, remote: string): Promise<StagedMirror> {
|
|
512
|
+
const { store, refs } = gitRepositoryOf(m, name, root);
|
|
513
|
+
const transport = gitTransport(execute, remote);
|
|
514
|
+
const advertised = await lsRemote(transport, 'git-upload-pack');
|
|
515
|
+
const theirs = new Map([...advertised.refs].filter(([ref]) => mirrored(ref)));
|
|
516
|
+
const held = new Map(refs.list().map((r) => [r.name, r.sha]));
|
|
517
|
+
const wants: string[] = [];
|
|
518
|
+
for (const sha of new Set(theirs.values())) if (!(await store.has(sha))) wants.push(sha);
|
|
519
|
+
await fetchPack(transport, store, wants, [...new Set(held.values())]);
|
|
520
|
+
return { m, name, theirs, head: advertised.head, held, heldHead: refs.head() };
|
|
521
|
+
}
|
|
522
|
+
const mirrored = (ref: string): boolean => ref.startsWith('refs/heads/') || ref.startsWith('refs/tags/');
|
|
523
|
+
|
|
524
|
+
/** REFRESH a repository's code, published: by compare-and-set under the repository's ref lock, the lock receive-pack
|
|
525
|
+
* holds. A ref takes the root's value, or is deleted when the root holds it no more, only while it still holds what the
|
|
526
|
+
* refresh read and no offered push names it (read inside the lock); HEAD follows the root's only while unchanged. */
|
|
527
|
+
function publishMirror(service: string, root: string | undefined, staged: StagedMirror): void {
|
|
528
|
+
const { refs } = gitRepositoryOf(staged.m, staged.name, root);
|
|
529
|
+
refs.withLock(() => {
|
|
530
|
+
const waiting = new Set(settlingEntries(service, root).offered.map(pushInputOf).filter((p): p is PushInput => p?.name === staged.name).map((p) => p.ref));
|
|
531
|
+
const untouched = (ref: string): boolean => !waiting.has(ref) && (refs.get(ref) ?? undefined) === staged.held.get(ref);
|
|
532
|
+
for (const [ref, sha] of staged.theirs) if (untouched(ref)) refs.set(ref, sha);
|
|
533
|
+
for (const ref of staged.held.keys()) if (mirrored(ref) && !staged.theirs.has(ref) && untouched(ref)) refs.delete(ref);
|
|
534
|
+
if (staged.head?.startsWith('refs/heads/') && refs.head() === staged.heldHead) refs.setHead(staged.head);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
429
538
|
/** Path parameters that name a subject by a field rather than its id, sent as the vendor's value of that field, read
|
|
430
539
|
* from the adopted landed copy ("Perform", step 1): a parent found by `parent.where` (a repository addressed as
|
|
431
540
|
* `{owner}/{repo}`), and a subject named by the number it counts per parent (`number`, an issue's `issue_number`).
|
|
@@ -861,7 +970,17 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
|
|
|
861
970
|
}
|
|
862
971
|
}
|
|
863
972
|
}
|
|
973
|
+
// the code of each repository observed, read over git; its refs are published with the blobs, once every read succeeded
|
|
974
|
+
const mirrors: StagedMirror[] = [];
|
|
975
|
+
for (const { manifest: m } of units) {
|
|
976
|
+
if (!m.git) continue;
|
|
977
|
+
const { resource, remote } = m.git.repository;
|
|
978
|
+
for (const row of observed.filter((o) => o.type === storedType(m, resource) && o.deleted !== true)) {
|
|
979
|
+
mirrors.push(await readMirror(m, execute, opts.root, gitNameOf(m, row.id, opts.root), fillTemplate(remote, { ...row.fields, id: row.id })));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
864
982
|
for (const download of downloads) await writeResourceBlob(service, download.key, download.bytes, opts.root);
|
|
983
|
+
for (const staged of mirrors) publishMirror(service, opts.root, staged);
|
|
865
984
|
adopt(service, opts.root, units, observed);
|
|
866
985
|
const report = observeResources(service, observed, { ...(opts.root !== undefined ? { root: opts.root } : {}), complete });
|
|
867
986
|
return unread.length ? { ...(report as object), unread } : report;
|
package/src/derived.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseExactJson } from './exact-json.ts';
|
|
2
2
|
// THE DERIVED DISPATCH — the generated wire's router (docs/contributing/architecture.md, "The derived
|
|
3
|
-
// pack"). A derived pack's surface (`src/generated/surface.gen.json`, written by
|
|
3
|
+
// pack"). A derived pack's surface (`src/generated/surface.gen.json`, written by `twin-standard derive`)
|
|
4
4
|
// names every operation the vendor publishes. This matches a request to one of them and hands it to its
|
|
5
5
|
// owner: the pack's semantics handler for that operationId when there is one, otherwise the pack's
|
|
6
6
|
// existing fetch, which keeps serving everything not yet moved (and every route outside the spec, such
|
|
@@ -24,7 +24,7 @@ export type DerivedOperation = {
|
|
|
24
24
|
headers?: Record<string, string | null>;
|
|
25
25
|
/** the status the vendor answers success with (201 for a create, 204 for an empty answer) */
|
|
26
26
|
successStatus?: number;
|
|
27
|
-
/** how the spec says the request body is encoded (
|
|
27
|
+
/** how the spec says the request body is encoded (the derivation: json, form, multipart, none) */
|
|
28
28
|
bodyEncoding?: string;
|
|
29
29
|
/** the success is documented with no content (a setting's update answered 200 and nothing else) */
|
|
30
30
|
emptySuccess?: boolean;
|
package/src/executor.ts
CHANGED
|
@@ -248,8 +248,8 @@ async function awsSigV4Headers(input: {
|
|
|
248
248
|
/** `vendorHosts`: the pack's declared vendor host rules (its descriptor's `hosts`). An absolute https URL
|
|
249
249
|
* on one of them — an exact `host` rule whose `pathPattern` the path matches — is a second host of the
|
|
250
250
|
* same vendor that takes the credential (LinkedIn's image uploads on www.linkedin.com/dms-uploads,
|
|
251
|
-
* Instagram's rupload host); it is sent credentialed like an anchored call
|
|
252
|
-
*
|
|
251
|
+
* Instagram's rupload host); it is sent credentialed like an anchored call, as HTTP Basic when a rule naming
|
|
252
|
+
* it names a `basic` user. Any other absolute URL is refused. */
|
|
253
253
|
/** Where a credential is kept (the head's and refresh's root: head.ts rootCustody): `key` names the credential stably
|
|
254
254
|
* across rotations and anew when another is sealed in its place (the token cache is keyed by it, not by the secret's value), `open` reads it as sealed now, `seal` keeps a rotated one. */
|
|
255
255
|
/** `unsealed`: where a rotation the seal failed to keep is recorded durably (the field and the cause, never a value), since
|
|
@@ -326,6 +326,8 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
|
|
|
326
326
|
let target: URL;
|
|
327
327
|
// the vendor's own URL, where a loopback root stands in for it: what a signature is made over
|
|
328
328
|
let vendorUrl: URL | undefined;
|
|
329
|
+
// the user a host that takes the token as HTTP Basic's password names it under (HostRule.basic)
|
|
330
|
+
let basicUser: string | undefined;
|
|
329
331
|
if (/^https:\/\//i.test(path) && vendorHosts?.length) {
|
|
330
332
|
// a second host the pack declares for this vendor, on a path its rule names: validated as the root is
|
|
331
333
|
target = new URL(path);
|
|
@@ -335,6 +337,7 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
|
|
|
335
337
|
&& (r.pathPattern === undefined || new RegExp(r.pathPattern).test(target.pathname));
|
|
336
338
|
const declared = vendorHosts.some((r) => !r.exclude && named(r)) && !vendorHosts.some((r) => r.exclude && named(r));
|
|
337
339
|
if (!declared) throw new Error(`remote execute: ${target.host}${target.pathname.slice(0, 64)} is not a host this vendor's pack declares for this path`);
|
|
340
|
+
basicUser = vendorHosts.find((r) => !r.exclude && r.basic !== undefined && named(r))?.basic;
|
|
338
341
|
// A loopback root is the vendor substitute. Preserve the vendor Host while retaining
|
|
339
342
|
// the already-validated root's mounted path; an absolute URL never escapes that root.
|
|
340
343
|
if (isLoopbackHost(origin.hostname)) {
|
|
@@ -352,6 +355,14 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
|
|
|
352
355
|
// a credential the vendor issued in this perform is the request's authorization; the sealed one stays home
|
|
353
356
|
if (issued !== undefined) outbound.set('authorization', issued);
|
|
354
357
|
else for (const [name, value] of Object.entries(credential.headers)) outbound.set(name, value);
|
|
358
|
+
// A HOST THAT TAKES THE TOKEN AS HTTP BASIC: the sealed authorization header's credentials, after its scheme, are
|
|
359
|
+
// Basic's password under the rule's user; one sealed as Basic goes as it is, and none sealed refuses before egress
|
|
360
|
+
if (issued === undefined && basicUser !== undefined) {
|
|
361
|
+
const sealed = outbound.get('authorization') ?? '';
|
|
362
|
+
const token = /^\S+\s+(\S.*)$/.exec(sealed)?.[1];
|
|
363
|
+
if (token === undefined) throw new Error(`remote execute: ${target.host} takes the credential as HTTP Basic, and the sealed credential carries no authorization header`);
|
|
364
|
+
if (!/^basic\s/i.test(sealed)) outbound.set('authorization', `Basic ${btoa(`${basicUser}:${token}`)}`);
|
|
365
|
+
}
|
|
355
366
|
// the vendor's host, kept over any the credential carries (a loopback twin routes by it, as the World's proxy's)
|
|
356
367
|
if (vendorUrl) { outbound.set('host', vendorUrl.host); outbound.set('x-volter-twin-original-host', vendorUrl.host); }
|
|
357
368
|
|
package/src/git/index.ts
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// THE CLIENT HALF OF SMART HTTP (gitprotocol-http, protocol v0): what `git fetch` and `git push` send to a server,
|
|
2
|
+
// over a transport the caller supplies. The library opens no socket, holds no credential and names no host; a root's
|
|
3
|
+
// refresh and deploy reach the vendor's git through it over the kernel executor (derived-real.ts, ADR 0031). It asks
|
|
4
|
+
// only for what it reads: no side-band (the pack arrives bare), no multi_ack (one answer), no thin pack (every delta
|
|
5
|
+
// base is in the pack or the store), ofs-delta (readPack resolves both delta kinds), and on a push report-status, with
|
|
6
|
+
// atomic when the server offers it.
|
|
7
|
+
import { concat, type GitObjectStore } from './objects.ts';
|
|
8
|
+
import { readPack } from './pack.ts';
|
|
9
|
+
import { FLUSH, ZERO_SHA, packObjects, pktLine, readPktLines, type Service } from './smart-http.ts';
|
|
10
|
+
|
|
11
|
+
const DEC = new TextDecoder();
|
|
12
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
13
|
+
|
|
14
|
+
/** One exchange: `GET info/refs?service=<service>` when there is no body, `POST <service>` when there is. It answers
|
|
15
|
+
* the response's bytes, and throws when the server did not answer 200. */
|
|
16
|
+
export type SmartHttpTransport = (request: { service: Service; body?: Uint8Array }) => Promise<Uint8Array>;
|
|
17
|
+
|
|
18
|
+
/** What a server advertises: its refs (peeled `^{}` lines dropped), the ref its HEAD names, and its capabilities. */
|
|
19
|
+
export type RemoteRefs = { refs: Map<string, string>; head: string | null; caps: string[] };
|
|
20
|
+
|
|
21
|
+
/** `GET info/refs?service=<service>`, read. */
|
|
22
|
+
export async function lsRemote(transport: SmartHttpTransport, service: Service): Promise<RemoteRefs> {
|
|
23
|
+
let { lines, rest } = readPktLines(await transport({ service }));
|
|
24
|
+
if (lines[0]?.startsWith('# service=')) ({ lines, rest } = readPktLines(rest));
|
|
25
|
+
const refs = new Map<string, string>(); let head: string | null = null; let caps: string[] = [];
|
|
26
|
+
for (const raw of lines) {
|
|
27
|
+
const [line, offered] = raw.replace(/\n$/, '').split('\0') as [string, string | undefined];
|
|
28
|
+
if (line.startsWith('ERR ')) throw new Error(`git ${service}: ${line.slice(4)}`);
|
|
29
|
+
if (offered !== undefined) {
|
|
30
|
+
caps = offered.split(' ').filter(Boolean);
|
|
31
|
+
const symref = caps.find((c) => c.startsWith('symref=HEAD:'));
|
|
32
|
+
if (symref) head = symref.slice('symref=HEAD:'.length);
|
|
33
|
+
}
|
|
34
|
+
const sha = line.slice(0, 40); const name = line.slice(41);
|
|
35
|
+
if (!SHA.test(sha) || sha === ZERO_SHA || name === 'HEAD' || name.endsWith('^{}')) continue;
|
|
36
|
+
refs.set(name, sha);
|
|
37
|
+
}
|
|
38
|
+
return { refs, head, caps };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isPack = (bytes: Uint8Array, at: number): boolean => DEC.decode(bytes.subarray(at, at + 4)) === 'PACK';
|
|
42
|
+
|
|
43
|
+
/** `POST git-upload-pack` with `done`: every object `wants` reach and `haves` do not, read into `store`. */
|
|
44
|
+
export async function fetchPack(transport: SmartHttpTransport, store: GitObjectStore, wants: string[], haves: string[]): Promise<void> {
|
|
45
|
+
if (wants.length === 0) return;
|
|
46
|
+
const answer = await transport({
|
|
47
|
+
service: 'git-upload-pack',
|
|
48
|
+
body: concat(
|
|
49
|
+
pktLine(`want ${wants[0]} ofs-delta no-progress agent=volter-twin\n`),
|
|
50
|
+
...wants.slice(1).map((w) => pktLine(`want ${w}\n`)),
|
|
51
|
+
FLUSH,
|
|
52
|
+
...haves.map((h) => pktLine(`have ${h}\n`)),
|
|
53
|
+
pktLine('done\n'),
|
|
54
|
+
),
|
|
55
|
+
});
|
|
56
|
+
// the negotiation's ACK / NAK lines, then the pack
|
|
57
|
+
let at = 0;
|
|
58
|
+
while (at + 4 <= answer.length && !isPack(answer, at)) {
|
|
59
|
+
const len = parseInt(DEC.decode(answer.subarray(at, at + 4)), 16);
|
|
60
|
+
if (Number.isNaN(len)) break;
|
|
61
|
+
if (len < 4) { at += 4; continue; }
|
|
62
|
+
const line = DEC.decode(answer.subarray(at + 4, at + len));
|
|
63
|
+
if (line.startsWith('ERR ')) throw new Error(`git upload-pack: ${line.slice(4).trim()}`);
|
|
64
|
+
at += len;
|
|
65
|
+
}
|
|
66
|
+
if (!isPack(answer, at)) throw new Error('git upload-pack: the server answered no pack');
|
|
67
|
+
await readPack(answer.subarray(at), store);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One ref update a push asks for: `old` is the server's value as advertised (ZERO_SHA to create), `new` the object
|
|
71
|
+
* to point it at (ZERO_SHA to delete). */
|
|
72
|
+
export type PushCommand = { name: string; old: string; new: string };
|
|
73
|
+
|
|
74
|
+
/** `POST git-receive-pack`: the commands and the pack of what they need beyond `haves`, atomic when `atomic` (the
|
|
75
|
+
* server offered it). Answers each command's outcome by ref name: null when the server took it, else its reason. */
|
|
76
|
+
export async function sendPack(transport: SmartHttpTransport, store: GitObjectStore, commands: PushCommand[], haves: string[], atomic = false): Promise<Map<string, string | null>> {
|
|
77
|
+
const updates = commands.filter((c) => c.new !== ZERO_SHA).map((c) => c.new);
|
|
78
|
+
const caps = `report-status${atomic ? ' atomic' : ''} agent=volter-twin`;
|
|
79
|
+
const answer = await transport({
|
|
80
|
+
service: 'git-receive-pack',
|
|
81
|
+
body: concat(
|
|
82
|
+
...commands.map((c, i) => pktLine(`${c.old} ${c.new} ${c.name}${i === 0 ? `\0${caps}` : ''}\n`)),
|
|
83
|
+
FLUSH,
|
|
84
|
+
// a push of deletions alone sends no pack (gitprotocol-pack, "Pushing data to a server")
|
|
85
|
+
...(updates.length ? [await packObjects(store, updates, haves)] : []),
|
|
86
|
+
),
|
|
87
|
+
});
|
|
88
|
+
const report = readPktLines(answer).lines.map((l) => l.replace(/\n$/, ''));
|
|
89
|
+
const failure = report.find((l) => l.startsWith('ERR '));
|
|
90
|
+
if (failure) throw new Error(`git receive-pack: ${failure.slice(4)}`);
|
|
91
|
+
const unpack = report.find((l) => l.startsWith('unpack '))?.slice('unpack '.length);
|
|
92
|
+
if (unpack === undefined) throw new Error('git receive-pack: the server sent no report');
|
|
93
|
+
const out = new Map<string, string | null>(commands.map((c) => [c.name, unpack === 'ok' ? 'the server reported nothing for it' : `unpacker error: ${unpack}`]));
|
|
94
|
+
for (const l of report) {
|
|
95
|
+
if (l.startsWith('ok ')) out.set(l.slice(3), null);
|
|
96
|
+
else if (l.startsWith('ng ')) { const rest = l.slice(3); const sp = rest.indexOf(' '); out.set(sp === -1 ? rest : rest.slice(0, sp), sp === -1 ? 'refused' : rest.slice(sp + 1)); }
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
package/src/git/smart-http.ts
CHANGED
|
@@ -68,6 +68,13 @@ async function closure(store: GitObjectStore, roots: string[], seen: Set<string>
|
|
|
68
68
|
}
|
|
69
69
|
async function reachable(store: GitObjectStore, roots: string[]): Promise<Set<string>> { const seen = new Set<string>(); await closure(store, roots, seen); return seen; }
|
|
70
70
|
|
|
71
|
+
/** The pack of every object `wants` reach that the `haves` the store holds do not: what a push sends (a client's
|
|
72
|
+
* send-pack, smart-http-client.ts). */
|
|
73
|
+
export async function packObjects(store: GitObjectStore, wants: string[], haves: string[]): Promise<Uint8Array> {
|
|
74
|
+
const known: string[] = []; for (const h of haves) if (await store.has(h)) known.push(h);
|
|
75
|
+
return writePack(await closure(store, wants, await reachable(store, known)));
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
/**
|
|
72
79
|
* A shallow request's commits (gitprotocol-pack, "Shallow requests"): every commit within `depth` of a want (a want is
|
|
73
80
|
* depth 1), and the boundary, the commits whose parents the client will not have. The client's own shallow commits it
|