@ricsam/r5d-worker 0.0.161 → 0.0.163
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/cjs/package.json +1 -1
- package/dist/mjs/internal-r5dctl.cjs +1 -1
- package/dist/mjs/main.mjs +2 -2
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal/client.mjs +2 -2
- package/dist/mjs/personal/publication-refusal.mjs +47 -0
- package/dist/mjs/personal/runtime.mjs +47 -46
- package/dist/mjs/runtime/workspace/authority.mjs +521 -76
- package/dist/mjs/runtime/workspace/files.mjs +44 -7
- package/dist/mjs/runtime/workspace/outer.mjs +286 -0
- package/dist/mjs/runtime/workspace/storage-wire.mjs +5 -2
- package/dist/types/personal/publication-refusal.d.ts +35 -0
- package/dist/types/personal/runtime.d.ts +3 -2
- package/dist/types/runtime/workspace/authority.d.ts +86 -4
- package/dist/types/runtime/workspace/contracts.d.ts +26 -0
- package/dist/types/runtime/workspace/files.d.ts +30 -1
- package/dist/types/runtime/workspace/outer.d.ts +105 -0
- package/dist/types/runtime/workspace/storage-wire.d.ts +2 -1
- package/package.json +2 -2
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
WorkspaceConfig,
|
|
12
12
|
WorkspaceError
|
|
13
13
|
} from "./contracts.mjs";
|
|
14
|
+
import { OuterRepository, OuterSnapshotRefusal, OUTER_BRANCH, OUTER_CONFLICT_CODE } from "./outer.mjs";
|
|
14
15
|
import { SessionArtifactStore, SessionArtifactChunk, RESERVED_ARTIFACT_ENV } from "./artifacts.mjs";
|
|
15
16
|
import { WorkspaceFileWrite, WorkspaceFileWriteLookup } from "./file-write.mjs";
|
|
16
17
|
import { WorkspaceStorageClient } from "./storage-client.mjs";
|
|
@@ -18,15 +19,19 @@ import {
|
|
|
18
19
|
durableJson,
|
|
19
20
|
ensureAuthorityGitRepositoryLayout,
|
|
20
21
|
git,
|
|
22
|
+
gitResult,
|
|
21
23
|
materializeTree,
|
|
22
24
|
noSymlinkAncestors,
|
|
25
|
+
PLATFORM_COMMIT_EMAIL,
|
|
23
26
|
privateRoot,
|
|
27
|
+
PROJECT_COMMIT_IDENTITY,
|
|
24
28
|
readHostRegular,
|
|
25
29
|
readRegular,
|
|
26
30
|
selectedTree,
|
|
27
31
|
sha256,
|
|
28
32
|
snapshotTree,
|
|
29
|
-
sourceBytes
|
|
33
|
+
sourceBytes,
|
|
34
|
+
WORKBENCH_CONFLICT_CODES
|
|
30
35
|
} from "./files.mjs";
|
|
31
36
|
const GITHUB_CREDENTIAL_HELPER = '!f() { if [ "$1" = get ] && [ -n "$R5D_GIT_CREDENTIAL" ]; then git credential-store --file="$R5D_GIT_CREDENTIAL" get; else return 0; fi; }; f';
|
|
32
37
|
class WorkspaceAuthority {
|
|
@@ -44,6 +49,7 @@ class WorkspaceAuthority {
|
|
|
44
49
|
closeTask;
|
|
45
50
|
actions = /* @__PURE__ */ new Set();
|
|
46
51
|
repositoryInitializations = /* @__PURE__ */ new Map();
|
|
52
|
+
outerRepository;
|
|
47
53
|
pending = 0;
|
|
48
54
|
expectedCwd(config) {
|
|
49
55
|
const workspaceRoot = this.config.workspaceRoot ?? this.config.root;
|
|
@@ -226,7 +232,10 @@ class WorkspaceAuthority {
|
|
|
226
232
|
hydrations = /* @__PURE__ */ new Map();
|
|
227
233
|
async ensureHydrated(identity) {
|
|
228
234
|
const b = await this.bench(identity);
|
|
229
|
-
if (b.config.rootProfile === "account")
|
|
235
|
+
if (b.config.rootProfile === "account") {
|
|
236
|
+
if (this.outerEnabled()) await this.synchronizeOuter(b.config.userId, { bootstrapOnly: true });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
230
239
|
if (b.state.initialized) return;
|
|
231
240
|
let task = this.hydrations.get(b.config.id);
|
|
232
241
|
if (!task) {
|
|
@@ -311,10 +320,12 @@ class WorkspaceAuthority {
|
|
|
311
320
|
async inspectGit(identity, command) {
|
|
312
321
|
const b = await this.bench(identity);
|
|
313
322
|
return this.serial(b, async () => {
|
|
314
|
-
if (!b.state.initialized
|
|
315
|
-
const
|
|
323
|
+
if (!b.state.initialized) throw new WorkspaceError("not_initialized", "Initialize workspace first");
|
|
324
|
+
const innerHead = this.outerEnabled(b) && this.linkedWorkbench(b) ? await this.innerHead(b) : void 0;
|
|
325
|
+
const head = innerHead === void 0 ? b.state.head : innerHead ?? (await git(b.repo, ["hash-object", "-t", "tree", "--stdin"], "")).toString().trim();
|
|
326
|
+
if (!head) throw new WorkspaceError("not_initialized", "Initialize workspace first");
|
|
316
327
|
const base = command.base ? GitOid.parse(command.base) : head;
|
|
317
|
-
const treeHash = await snapshotTree(b.repo, b.config.cwd, head);
|
|
328
|
+
const treeHash = await snapshotTree(b.repo, b.config.cwd, innerHead === void 0 ? head : innerHead);
|
|
318
329
|
if (command.method === "history") {
|
|
319
330
|
const history = (await git(b.repo, ["log", "-100", "--format=%H%x00%P%x00%s%x00%an%x00%ae%x00%aI%x00%cI", head])).toString();
|
|
320
331
|
return { head, treeHash, commits: history.trimEnd().split("\n").filter(Boolean).map((line) => {
|
|
@@ -362,10 +373,12 @@ class WorkspaceAuthority {
|
|
|
362
373
|
if (owner.lockId !== this.lockId) throw new WorkspaceError("authority_lost", "Workspace authority lock changed");
|
|
363
374
|
}
|
|
364
375
|
serial(b, fn, admitted = false) {
|
|
376
|
+
return this.serialKey(this.linkedWorkbench(b) ? b.repo : b.config.id, fn, admitted);
|
|
377
|
+
}
|
|
378
|
+
serialKey(queueKey, fn, admitted = false) {
|
|
365
379
|
if (this.closing && !admitted) return Promise.reject(new WorkspaceError("authority_closed", "Workspace authority is closing"));
|
|
366
380
|
if (this.pending >= 32) return Promise.reject(new WorkspaceError("busy", "Workspace request budget reached"));
|
|
367
381
|
this.pending++;
|
|
368
|
-
const queueKey = this.linkedWorkbench(b) ? b.repo : b.config.id;
|
|
369
382
|
const task = (this.queues.get(queueKey) ?? Promise.resolve()).catch(() => {
|
|
370
383
|
}).then(async () => {
|
|
371
384
|
await this.owned(admitted);
|
|
@@ -417,6 +430,13 @@ class WorkspaceAuthority {
|
|
|
417
430
|
async assertAvailable(b, allowBlocked = false) {
|
|
418
431
|
if (b.state.blocked && !allowBlocked) throw new WorkspaceError(b.state.blocked.code, b.state.blocked.message);
|
|
419
432
|
}
|
|
433
|
+
/** Whether any run on this physical workbench is still live, across every
|
|
434
|
+
* session sharing it. An in-progress Git operation marker belongs to such a
|
|
435
|
+
* run, so publication defers to it instead of electing an incident over it. */
|
|
436
|
+
async workbenchHasLiveRun(identity) {
|
|
437
|
+
const b = await this.bench(identity);
|
|
438
|
+
return Object.values(b.state.runs).some((run) => !["completed", "cancelled", "rejected_capacity"].includes(run.state));
|
|
439
|
+
}
|
|
420
440
|
async status(identity) {
|
|
421
441
|
const b = await this.bench(identity);
|
|
422
442
|
return this.serial(b, async () => {
|
|
@@ -434,7 +454,14 @@ class WorkspaceAuthority {
|
|
|
434
454
|
state.runs = Object.fromEntries(
|
|
435
455
|
Object.entries(state.runs).filter(([, run]) => (run.sessionId ?? b.config.sessionId) === identity.sessionId)
|
|
436
456
|
);
|
|
437
|
-
|
|
457
|
+
let outer;
|
|
458
|
+
if (b.config.rootProfile === "account" && this.outerEnabled()) {
|
|
459
|
+
outer = structuredClone((await this.outer(b.config.userId)).state);
|
|
460
|
+
state.initialized = outer.initialized;
|
|
461
|
+
state.head = outer.head;
|
|
462
|
+
state.blocked = outer.blocked;
|
|
463
|
+
}
|
|
464
|
+
return { workbench: { ...b.config, sessionId: identity.sessionId }, ...state, files, ...outer ? { outer } : {} };
|
|
438
465
|
});
|
|
439
466
|
}
|
|
440
467
|
async cleanRefreshBase(b, expectedBase) {
|
|
@@ -468,7 +495,18 @@ class WorkspaceAuthority {
|
|
|
468
495
|
* clean workbench refresh; process/PTY liveness never gates synchronization. */
|
|
469
496
|
async hydrate(identity, expectedBase) {
|
|
470
497
|
const b = await this.bench(identity);
|
|
471
|
-
if (b.config.rootProfile === "account")
|
|
498
|
+
if (b.config.rootProfile === "account") {
|
|
499
|
+
if (!this.outerEnabled()) return { head: "", unchanged: true };
|
|
500
|
+
return this.synchronizeOuter(b.config.userId, expectedBase === void 0 ? { bootstrapOnly: true } : { requireHead: GitOid.parse(expectedBase) });
|
|
501
|
+
}
|
|
502
|
+
if (this.outerEnabled(b)) {
|
|
503
|
+
if (!b.state.initialized) {
|
|
504
|
+
await this.synchronizeOuter(b.config.userId, { bootstrapOnly: true });
|
|
505
|
+
await this.serial(b, () => this.hydrateLinkedIdle(b, identity));
|
|
506
|
+
}
|
|
507
|
+
if (expectedBase !== void 0) return this.synchronizeOuter(b.config.userId, { requireHead: GitOid.parse(expectedBase) });
|
|
508
|
+
return { head: b.state.head ?? "" };
|
|
509
|
+
}
|
|
472
510
|
return this.serial(b, () => this.hydrateIdle(b, identity, expectedBase));
|
|
473
511
|
}
|
|
474
512
|
async hydrateIdle(b, identity, expectedBase) {
|
|
@@ -524,27 +562,7 @@ class WorkspaceAuthority {
|
|
|
524
562
|
message: "Source synchronization interrupted; preserve workbench and inspect staged tree/retained originals before maintenance"
|
|
525
563
|
};
|
|
526
564
|
await this.save(b);
|
|
527
|
-
const
|
|
528
|
-
let offset = 0, snapshot = null, size = -1, hash = "";
|
|
529
|
-
do {
|
|
530
|
-
const chunk = await storage.read({
|
|
531
|
-
method: "repository.bundle",
|
|
532
|
-
repositoryId: b.config.repositoryId,
|
|
533
|
-
snapshot,
|
|
534
|
-
offset,
|
|
535
|
-
limit: STORAGE_LIMITS.chunkBytes
|
|
536
|
-
});
|
|
537
|
-
const bytes = Buffer.from(chunk.data, "base64");
|
|
538
|
-
if (chunk.data !== bytes.toString("base64") || bytes.length > STORAGE_LIMITS.chunkBytes || chunk.offset !== offset || chunk.nextOffset !== offset + bytes.length || !bytes.length || chunk.size > STORAGE_LIMITS.blobBytes || chunk.nextOffset > chunk.size || snapshot && (snapshot !== chunk.snapshot || size !== chunk.size || hash !== chunk.sha256))
|
|
539
|
-
throw new WorkspaceError("invalid_bundle", "Chunk identity/size/offset changed; no checkout performed");
|
|
540
|
-
snapshot = chunk.snapshot;
|
|
541
|
-
size = chunk.size;
|
|
542
|
-
hash = chunk.sha256;
|
|
543
|
-
offset = chunk.nextOffset;
|
|
544
|
-
chunks.push(bytes);
|
|
545
|
-
} while (offset < size);
|
|
546
|
-
const bundle = Buffer.concat(chunks);
|
|
547
|
-
if (sha256(bundle) !== hash) throw new WorkspaceError("invalid_bundle", "Bundle SHA256 mismatch");
|
|
565
|
+
const bundle = await this.downloadBundle(storage, b.config.repositoryId);
|
|
548
566
|
const file = path.join(b.directory, `${randomUUID()}.bundle`);
|
|
549
567
|
await fs.writeFile(file, bundle, { mode: 384, flag: "wx" });
|
|
550
568
|
await git(b.repo, ["bundle", "unbundle", file]);
|
|
@@ -768,9 +786,11 @@ class WorkspaceAuthority {
|
|
|
768
786
|
b.state.initialized = true;
|
|
769
787
|
b.state.blocked = null;
|
|
770
788
|
await this.save(b);
|
|
771
|
-
const result = await this.publishIdle(b, identity);
|
|
789
|
+
const result = await this.publishIdle(b, identity, this.linkedWorkbench(b) ? "Initialize project" : void 0, false, this.linkedWorkbench(b));
|
|
772
790
|
if (this.linkedWorkbench(b)) {
|
|
773
791
|
await fs.unlink(path.join(b.config.cwd, "README.md"));
|
|
792
|
+
b.state.mirroredHead = result.head;
|
|
793
|
+
await this.save(b);
|
|
774
794
|
}
|
|
775
795
|
await this.installGitPolicy(b, result.head);
|
|
776
796
|
return result;
|
|
@@ -796,22 +816,7 @@ class WorkspaceAuthority {
|
|
|
796
816
|
await git(b.config.cwd, ["reset", "--mixed", visibleHead]);
|
|
797
817
|
}
|
|
798
818
|
}
|
|
799
|
-
|
|
800
|
-
await git(b.repo, ["config", "--local", "--replace-all", "remote.origin.url", origin]);
|
|
801
|
-
await git(b.repo, ["config", "--local", "--replace-all", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"]);
|
|
802
|
-
const localConfig = (await readRegular(path.join(b.repo, "config"), 128 * 1024)).toString("utf8");
|
|
803
|
-
if (/^\[remote "canonical"\]\s*$/m.test(localConfig))
|
|
804
|
-
await git(b.repo, ["config", "--local", "--remove-section", "remote.canonical"]);
|
|
805
|
-
await git(b.repo, ["config", "--local", "--replace-all", `branch.${b.config.branch}.remote`, "origin"]);
|
|
806
|
-
await git(b.repo, ["config", "--local", "--replace-all", `branch.${b.config.branch}.merge`, `refs/heads/${b.config.branch}`]);
|
|
807
|
-
if (remoteTrackingHead)
|
|
808
|
-
await git(b.repo, ["update-ref", `refs/remotes/origin/${b.config.branch}`, remoteTrackingHead]);
|
|
809
|
-
await git(b.repo, ["config", "--local", "--replace-all", "push.default", "upstream"]);
|
|
810
|
-
await git(b.repo, ["config", "--local", "--replace-all", "credential.helper", ""]);
|
|
811
|
-
await git(b.repo, ["config", "--local", "--add", "credential.helper", GITHUB_CREDENTIAL_HELPER]);
|
|
812
|
-
await git(b.repo, ["config", "--local", "--replace-all", "credential.useHttpPath", "false"]);
|
|
813
|
-
await git(b.repo, ["config", "protocol.allow", "never"]);
|
|
814
|
-
await git(b.repo, ["config", "protocol.https.allow", "always"]);
|
|
819
|
+
await this.configureLinkedRemote(b, remoteTrackingHead);
|
|
815
820
|
return;
|
|
816
821
|
}
|
|
817
822
|
const destination = path.join(b.config.cwd, ".git");
|
|
@@ -840,6 +845,28 @@ class WorkspaceAuthority {
|
|
|
840
845
|
await fs.writeFile(ref, `${head}
|
|
841
846
|
`, { mode: 384 });
|
|
842
847
|
}
|
|
848
|
+
/** This repository is also the common Git directory for the visible project
|
|
849
|
+
* worktrees. Keep their ordinary Git topology pointed at GitHub; mirroring is
|
|
850
|
+
* performed by WorkspaceAuthority directly and must never be exposed as the
|
|
851
|
+
* checkout's origin. */
|
|
852
|
+
async configureLinkedRemote(b, remoteTrackingHead = b.config.baseCommitHash) {
|
|
853
|
+
const origin = this.githubOrigin(b);
|
|
854
|
+
await git(b.repo, ["config", "--local", "--replace-all", "remote.origin.url", origin]);
|
|
855
|
+
await git(b.repo, ["config", "--local", "--replace-all", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"]);
|
|
856
|
+
const localConfig = (await readRegular(path.join(b.repo, "config"), 128 * 1024)).toString("utf8");
|
|
857
|
+
if (/^\[remote "canonical"\]\s*$/m.test(localConfig))
|
|
858
|
+
await git(b.repo, ["config", "--local", "--remove-section", "remote.canonical"]);
|
|
859
|
+
await git(b.repo, ["config", "--local", "--replace-all", `branch.${b.config.branch}.remote`, "origin"]);
|
|
860
|
+
await git(b.repo, ["config", "--local", "--replace-all", `branch.${b.config.branch}.merge`, `refs/heads/${b.config.branch}`]);
|
|
861
|
+
if (remoteTrackingHead)
|
|
862
|
+
await git(b.repo, ["update-ref", `refs/remotes/origin/${b.config.branch}`, remoteTrackingHead]);
|
|
863
|
+
await git(b.repo, ["config", "--local", "--replace-all", "push.default", "upstream"]);
|
|
864
|
+
await git(b.repo, ["config", "--local", "--replace-all", "credential.helper", ""]);
|
|
865
|
+
await git(b.repo, ["config", "--local", "--add", "credential.helper", GITHUB_CREDENTIAL_HELPER]);
|
|
866
|
+
await git(b.repo, ["config", "--local", "--replace-all", "credential.useHttpPath", "false"]);
|
|
867
|
+
await git(b.repo, ["config", "protocol.allow", "never"]);
|
|
868
|
+
await git(b.repo, ["config", "protocol.https.allow", "always"]);
|
|
869
|
+
}
|
|
843
870
|
/** Scoped receipt lookup never repeats a file effect; completion can clear its own crash marker. */
|
|
844
871
|
async fileWriteResult(identity, input) {
|
|
845
872
|
WorkspaceFileWriteLookup.parse({ method: "fileWriteResult", ...input });
|
|
@@ -870,7 +897,7 @@ class WorkspaceAuthority {
|
|
|
870
897
|
let mode = 420;
|
|
871
898
|
let retainedBlock = null;
|
|
872
899
|
const check = async () => {
|
|
873
|
-
if (b.state.blocked && !(allowBlockedConflict && (b.state.blocked.code
|
|
900
|
+
if (b.state.blocked && !(allowBlockedConflict && WORKBENCH_CONFLICT_CODES.has(b.state.blocked.code)))
|
|
874
901
|
throw new WorkspaceError(b.state.blocked.code, b.state.blocked.message);
|
|
875
902
|
retainedBlock = b.state.blocked;
|
|
876
903
|
if (!b.state.initialized) throw new WorkspaceError("uninitialized", "Hydrate this workspace before writing files");
|
|
@@ -1048,10 +1075,7 @@ class WorkspaceAuthority {
|
|
|
1048
1075
|
const bytes = await readRegular(file, STORAGE_LIMITS.blobBytes), blobId = operationId;
|
|
1049
1076
|
b.state.blocked = { code: "publication_unknown", operationId, commit: head, message: "Inspect the original import publication receipt" };
|
|
1050
1077
|
await this.save(b);
|
|
1051
|
-
|
|
1052
|
-
const end = Math.min(bytes.length, offset + STORAGE_LIMITS.chunkBytes);
|
|
1053
|
-
await storage.mutate({ method: "blob.append", operationId: `${operationId}-chunk-${offset}`, blobId, kind: "blob", offset, data: bytes.subarray(offset, end).toString("base64"), seal: end === bytes.length });
|
|
1054
|
-
}
|
|
1078
|
+
await this.uploadBundle(storage, bytes, blobId, operationId);
|
|
1055
1079
|
const result = await storage.mutate({ method: "repository.publish", operationId, repositoryId: b.config.repositoryId, branch: b.config.branch, expectedHead: null, commit: head, bundleId: blobId });
|
|
1056
1080
|
if (result.head !== head) throw new WorkspaceError("invalid_receipt", "Import receipt does not match selected commit");
|
|
1057
1081
|
if (this.linkedWorkbench(b)) await fs.rm(staged, { recursive: true });
|
|
@@ -1060,6 +1084,7 @@ class WorkspaceAuthority {
|
|
|
1060
1084
|
b.state.initialized = true;
|
|
1061
1085
|
b.state.head = head;
|
|
1062
1086
|
b.state.blocked = null;
|
|
1087
|
+
b.state.mirroredHead = head;
|
|
1063
1088
|
await this.save(b);
|
|
1064
1089
|
return { head, workspaceHead: head };
|
|
1065
1090
|
}
|
|
@@ -1067,6 +1092,10 @@ class WorkspaceAuthority {
|
|
|
1067
1092
|
const b = await this.bench(identity);
|
|
1068
1093
|
if (!input.message.trim() || input.message.length > 1e4) throw new WorkspaceError("invalid_input", "Commit message is required");
|
|
1069
1094
|
return this.serial(b, () => this.productAction(b, input.id, { method: "commit", expectedHead: input.expectedHead, message: input.message }, async () => {
|
|
1095
|
+
if (this.outerEnabled(b)) {
|
|
1096
|
+
const result2 = await this.commitInnerIdle(b, identity, input.message);
|
|
1097
|
+
return { ...result2, treeHash: (await git(b.repo, ["rev-parse", `${result2.head}^{tree}`])).toString().trim() };
|
|
1098
|
+
}
|
|
1070
1099
|
const result = await this.publishIdle(b, identity, input.message);
|
|
1071
1100
|
if (this.linkedWorkbench(b)) {
|
|
1072
1101
|
return { ...result, treeHash: (await git(b.repo, ["rev-parse", `${result.head}^{tree}`])).toString().trim() };
|
|
@@ -1080,22 +1109,24 @@ class WorkspaceAuthority {
|
|
|
1080
1109
|
return { ...result, treeHash: (await git(b.repo, ["rev-parse", `${result.head}^{tree}`])).toString().trim() };
|
|
1081
1110
|
}, async () => {
|
|
1082
1111
|
await this.assertAvailable(b);
|
|
1083
|
-
if (input.expectedHead !== void 0 && input.expectedHead
|
|
1112
|
+
if (input.expectedHead !== void 0 && !await this.headMatches(b, input.expectedHead)) throw new WorkspaceError("conflict", "Workbench head changed");
|
|
1084
1113
|
}));
|
|
1085
1114
|
}
|
|
1086
1115
|
async push(identity, input) {
|
|
1087
1116
|
const b = await this.bench(identity);
|
|
1088
1117
|
this.validateGithubRemote(input.remote);
|
|
1089
1118
|
return this.serial(b, () => this.productAction(b, input.id, { method: "push", expectedHead: input.expectedHead, url: input.remote.url, branch: input.remote.branch, expectedRemoteHead: input.remote.expectedHead }, async () => {
|
|
1119
|
+
const head = this.outerEnabled(b) ? await this.innerHead(b) : b.state.head;
|
|
1120
|
+
if (!head) throw new WorkspaceError("conflict", "The checkout has no commit to push");
|
|
1090
1121
|
if (input.remote.expectedHead) {
|
|
1091
1122
|
GitOid.parse(input.remote.expectedHead);
|
|
1092
|
-
await git(b.repo, ["merge-base", "--is-ancestor", input.remote.expectedHead,
|
|
1123
|
+
await git(b.repo, ["merge-base", "--is-ancestor", input.remote.expectedHead, head]);
|
|
1093
1124
|
}
|
|
1094
|
-
await git(b.repo, ["-c", "protocol.https.allow=always", "push", "--porcelain", ...input.remote.expectedHead !== void 0 ? [`--force-with-lease=refs/heads/${input.remote.branch}:${input.remote.expectedHead ?? ""}`] : [], "--", input.remote.url, `${
|
|
1095
|
-
return { head
|
|
1125
|
+
await git(b.repo, ["-c", "protocol.https.allow=always", "push", "--porcelain", ...input.remote.expectedHead !== void 0 ? [`--force-with-lease=refs/heads/${input.remote.branch}:${input.remote.expectedHead ?? ""}`] : [], "--", input.remote.url, `${head}:refs/heads/${input.remote.branch}`], void 0, void 0, { token: input.remote.token });
|
|
1126
|
+
return { head, pushed: true };
|
|
1096
1127
|
}, async () => {
|
|
1097
1128
|
await this.assertAvailable(b);
|
|
1098
|
-
if (
|
|
1129
|
+
if (input.expectedHead !== void 0 && !await this.headMatches(b, input.expectedHead)) throw new WorkspaceError("conflict", "Workbench head changed");
|
|
1099
1130
|
}));
|
|
1100
1131
|
}
|
|
1101
1132
|
async archiveBranch(identity, input) {
|
|
@@ -1128,8 +1159,11 @@ class WorkspaceAuthority {
|
|
|
1128
1159
|
const receipt = await storage.read({ method: "operation.get", lookupId: b.state.blocked.operationId });
|
|
1129
1160
|
if (receipt.state !== "completed" || b.state.blocked.commit && receipt.result?.head !== b.state.blocked.commit) throw new WorkspaceError("publication_unknown", "Reconcile the original publication before resetting");
|
|
1130
1161
|
}
|
|
1131
|
-
const canonical = await storage.read({ method: "repository.get", repositoryId: b.config.repositoryId, branch: b.config.branch })
|
|
1132
|
-
|
|
1162
|
+
const canonical = await storage.read({ method: "repository.get", repositoryId: b.config.repositoryId, branch: b.config.branch }).catch((error) => {
|
|
1163
|
+
if (this.outerEnabled(b) && error instanceof WorkspaceError && error.code === "not_found") return { head: null };
|
|
1164
|
+
throw error;
|
|
1165
|
+
});
|
|
1166
|
+
if (!canonical.head && !this.outerEnabled(b)) throw new WorkspaceError("empty_repository", "No canonical source exists to restore");
|
|
1133
1167
|
const retained = path.join(b.directory, `reset-retained-${randomUUID()}`);
|
|
1134
1168
|
await fs.mkdir(retained, { mode: 448 });
|
|
1135
1169
|
await durableJson(path.join(retained, "recovery.json"), { workbench: b.config, previousState: b.state, resetId: input.id, targetHead: canonical.head });
|
|
@@ -1145,21 +1179,443 @@ class WorkspaceAuthority {
|
|
|
1145
1179
|
b.state.initialized = false;
|
|
1146
1180
|
b.state.head = null;
|
|
1147
1181
|
b.state.blocked = null;
|
|
1182
|
+
b.state.mirroredHead = null;
|
|
1148
1183
|
await this.save(b);
|
|
1149
|
-
const result = await this.hydrateIdle(b, identity);
|
|
1184
|
+
const result = this.outerEnabled(b) ? await this.hydrateLinkedIdle(b, identity) : await this.hydrateIdle(b, identity);
|
|
1150
1185
|
return { ...result, reset: true, retained: true };
|
|
1151
1186
|
}));
|
|
1152
1187
|
}
|
|
1188
|
+
// ---- Outer repository ---------------------------------------------------
|
|
1189
|
+
outerEnabled(b) {
|
|
1190
|
+
return !!this.options.accountStorage && (!b || b.config.rootProfile === "account" || this.linkedWorkbench(b));
|
|
1191
|
+
}
|
|
1192
|
+
outerStateFile() {
|
|
1193
|
+
return path.join(this.config.root, "state", "outer.json");
|
|
1194
|
+
}
|
|
1195
|
+
async outer(userId) {
|
|
1196
|
+
if (!this.options.accountStorage) throw new WorkspaceError("outer_unavailable", "This authority has no account storage");
|
|
1197
|
+
const workTree = this.config.workspaceRoot;
|
|
1198
|
+
if (!workTree) throw new WorkspaceError("invalid_config", "The outer repository requires a workspace root");
|
|
1199
|
+
if (!this.outerRepository)
|
|
1200
|
+
this.outerRepository = (async () => {
|
|
1201
|
+
const repo = await OuterRepository.open(path.join(this.config.root, "outer.git"), workTree);
|
|
1202
|
+
const stored = await readRegular(this.outerStateFile(), 1024 * 1024).catch((error) => {
|
|
1203
|
+
if (error.code === "ENOENT") return null;
|
|
1204
|
+
throw error;
|
|
1205
|
+
});
|
|
1206
|
+
const state = stored ? JSON.parse(stored.toString("utf8")) : { userId, initialized: false, publishedHead: null, head: null, blocked: null, conflict: null };
|
|
1207
|
+
if (typeof state.initialized !== "boolean" || !(state.head === null || GitOid.safeParse(state.head).success))
|
|
1208
|
+
throw new WorkspaceError("invalid_state", "Invalid durable outer repository state; maintenance required");
|
|
1209
|
+
if (!stored) await durableJson(this.outerStateFile(), state);
|
|
1210
|
+
return { repo, state };
|
|
1211
|
+
})();
|
|
1212
|
+
const outer = await this.outerRepository.catch((error) => {
|
|
1213
|
+
this.outerRepository = void 0;
|
|
1214
|
+
throw error;
|
|
1215
|
+
});
|
|
1216
|
+
if (outer.state.userId !== userId) throw new WorkspaceError("wrong_binding", "The outer repository belongs to another account");
|
|
1217
|
+
return outer;
|
|
1218
|
+
}
|
|
1219
|
+
saveOuter(state) {
|
|
1220
|
+
return durableJson(this.outerStateFile(), state);
|
|
1221
|
+
}
|
|
1222
|
+
async outerStatus(userId) {
|
|
1223
|
+
return structuredClone((await this.outer(userId)).state);
|
|
1224
|
+
}
|
|
1225
|
+
accountRepositoryId(userId) {
|
|
1226
|
+
return `user-workspace-${sha256(userId).slice(0, 40)}`;
|
|
1227
|
+
}
|
|
1228
|
+
provisionId(repositoryId) {
|
|
1229
|
+
return `provision-${sha256(repositoryId).slice(0, 48)}`;
|
|
1230
|
+
}
|
|
1231
|
+
async hasCommit(repo, commit) {
|
|
1232
|
+
return (await gitResult(repo, ["cat-file", "-e", `${GitOid.parse(commit)}^{commit}`])).code === 0;
|
|
1233
|
+
}
|
|
1234
|
+
async innerHead(b) {
|
|
1235
|
+
const { code, stdout } = await gitResult(b.config.cwd, ["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
1236
|
+
if (code === 1) return null;
|
|
1237
|
+
if (code !== 0) throw new WorkspaceError("unsafe_git", "Project checkout HEAD is unreadable");
|
|
1238
|
+
return GitOid.parse(stdout.toString("utf8").trim());
|
|
1239
|
+
}
|
|
1240
|
+
async headMatches(b, expected) {
|
|
1241
|
+
if (!this.outerEnabled(b)) return expected === b.state.head;
|
|
1242
|
+
return expected === b.state.mirroredHead || expected === await this.innerHead(b);
|
|
1243
|
+
}
|
|
1244
|
+
async innerTracked(b) {
|
|
1245
|
+
const head = await this.innerHead(b);
|
|
1246
|
+
if (!head) return /* @__PURE__ */ new Set();
|
|
1247
|
+
const listing = await git(b.repo, ["ls-tree", "-r", "--name-only", "-z", head]);
|
|
1248
|
+
return new Set(listing.toString("utf8").split("\0").filter(Boolean));
|
|
1249
|
+
}
|
|
1250
|
+
/** Platform snapshot commits from the per-project model sit on top of the
|
|
1251
|
+
* GitHub tip. Hydration peels them so a checkout's history is its own. */
|
|
1252
|
+
async platformSnapshot(repo, commit) {
|
|
1253
|
+
const header = (await git(repo, ["cat-file", "commit", commit])).toString("utf8").split("\n\n")[0] ?? "";
|
|
1254
|
+
return /^committer .*<workspace@invalid>/m.test(header.replace(PLATFORM_COMMIT_EMAIL, "workspace@invalid"));
|
|
1255
|
+
}
|
|
1256
|
+
async firstParent(repo, commit) {
|
|
1257
|
+
const parents = (await git(repo, ["rev-list", "--parents", "-n", "1", commit])).toString("utf8").trim().split(/\s+/).slice(1);
|
|
1258
|
+
return parents[0] ? GitOid.parse(parents[0]) : null;
|
|
1259
|
+
}
|
|
1260
|
+
/** Every initialized project checkout beneath the workspace root, for the
|
|
1261
|
+
* outer walk and the measure. */
|
|
1262
|
+
async innerCheckouts(userId) {
|
|
1263
|
+
const workTree = this.config.workspaceRoot;
|
|
1264
|
+
const result = [];
|
|
1265
|
+
for (const b of this.benches.values()) {
|
|
1266
|
+
if (b.config.userId !== userId || !this.linkedWorkbench(b) || !b.state.initialized || b.state.blocked?.code === "archived") continue;
|
|
1267
|
+
const relative = path.relative(workTree, b.config.cwd);
|
|
1268
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) continue;
|
|
1269
|
+
if (!await fs.lstat(b.config.cwd).then((stat) => stat.isDirectory(), () => false)) continue;
|
|
1270
|
+
result.push({ bench: b, root: relative.split(path.sep).join("/"), tracked: () => this.innerTracked(b) });
|
|
1271
|
+
}
|
|
1272
|
+
return result;
|
|
1273
|
+
}
|
|
1274
|
+
/** The mirror's head for a branch, or null when it has none. Only a caller
|
|
1275
|
+
* that owns the repository's existence provisions it: a project repository
|
|
1276
|
+
* is provisioned by its seed or import under that action's own identity. */
|
|
1277
|
+
async remoteHead(storage, repositoryId, branch, provision = false) {
|
|
1278
|
+
try {
|
|
1279
|
+
return (await storage.read({ method: "repository.get", repositoryId, branch })).head;
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
if (!(error instanceof WorkspaceError) || error.code !== "not_found") throw error;
|
|
1282
|
+
if (provision) await storage.mutate({ method: "repository.provision", operationId: this.provisionId(repositoryId), repositoryId });
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
async downloadBundle(storage, repositoryId) {
|
|
1287
|
+
const chunks = [];
|
|
1288
|
+
let offset = 0, snapshot = null, size = -1, hash = "";
|
|
1289
|
+
do {
|
|
1290
|
+
const chunk = await storage.read({
|
|
1291
|
+
method: "repository.bundle",
|
|
1292
|
+
repositoryId,
|
|
1293
|
+
snapshot,
|
|
1294
|
+
offset,
|
|
1295
|
+
limit: STORAGE_LIMITS.chunkBytes
|
|
1296
|
+
});
|
|
1297
|
+
const bytes = Buffer.from(chunk.data, "base64");
|
|
1298
|
+
if (chunk.data !== bytes.toString("base64") || bytes.length > STORAGE_LIMITS.chunkBytes || chunk.offset !== offset || chunk.nextOffset !== offset + bytes.length || !bytes.length || chunk.size > STORAGE_LIMITS.blobBytes || chunk.nextOffset > chunk.size || snapshot && (snapshot !== chunk.snapshot || size !== chunk.size || hash !== chunk.sha256))
|
|
1299
|
+
throw new WorkspaceError("invalid_bundle", "Chunk identity/size/offset changed; no checkout performed");
|
|
1300
|
+
snapshot = chunk.snapshot;
|
|
1301
|
+
size = chunk.size;
|
|
1302
|
+
hash = chunk.sha256;
|
|
1303
|
+
offset = chunk.nextOffset;
|
|
1304
|
+
chunks.push(bytes);
|
|
1305
|
+
} while (offset < size);
|
|
1306
|
+
const bundle = Buffer.concat(chunks);
|
|
1307
|
+
if (sha256(bundle) !== hash) throw new WorkspaceError("invalid_bundle", "Bundle SHA256 mismatch");
|
|
1308
|
+
return bundle;
|
|
1309
|
+
}
|
|
1310
|
+
async uploadBundle(storage, bytes, blobId, operationId) {
|
|
1311
|
+
for (let offset = 0; offset < bytes.length; offset += STORAGE_LIMITS.chunkBytes) {
|
|
1312
|
+
const end = Math.min(bytes.length, offset + STORAGE_LIMITS.chunkBytes);
|
|
1313
|
+
await storage.mutate({
|
|
1314
|
+
method: "blob.append",
|
|
1315
|
+
operationId: `${operationId}-chunk-${offset}`,
|
|
1316
|
+
blobId,
|
|
1317
|
+
kind: "blob",
|
|
1318
|
+
offset,
|
|
1319
|
+
data: bytes.subarray(offset, end).toString("base64"),
|
|
1320
|
+
seal: end === bytes.length
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
async unbundleInto(b, bytes) {
|
|
1325
|
+
const file = path.join(b.directory, `${randomUUID()}.bundle`);
|
|
1326
|
+
await fs.writeFile(file, bytes, { mode: 384, flag: "wx" });
|
|
1327
|
+
try {
|
|
1328
|
+
await git(b.repo, ["bundle", "unbundle", file]);
|
|
1329
|
+
} finally {
|
|
1330
|
+
await fs.rm(file, { force: true });
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
async forcePublishSupported(storage) {
|
|
1334
|
+
const hello = await storage.read({ method: "hello" });
|
|
1335
|
+
return hello.capabilities?.includes("storage-force-publish-v1") === true;
|
|
1336
|
+
}
|
|
1337
|
+
/** One synchronization cycle of the outer repository: snapshot the workspace,
|
|
1338
|
+
* integrate what the mirror gained, publish what changed. Serialized on its
|
|
1339
|
+
* own queue; project checkouts stay ordinary concurrent writers whose later
|
|
1340
|
+
* edits the next cycle observes. */
|
|
1341
|
+
async synchronizeOuter(userId, options = {}) {
|
|
1342
|
+
return this.serialKey("outer", () => this.synchronizeOuterIdle(userId, options));
|
|
1343
|
+
}
|
|
1344
|
+
async synchronizeOuterIdle(userId, options) {
|
|
1345
|
+
await this.owned();
|
|
1346
|
+
const { repo, state } = await this.outer(userId);
|
|
1347
|
+
const storage = await this.options.accountStorage(userId);
|
|
1348
|
+
const repositoryId = this.accountRepositoryId(userId);
|
|
1349
|
+
if (state.blocked?.code === "publication_unknown") {
|
|
1350
|
+
const receipt = await storage.read({ method: "operation.get", lookupId: state.blocked.operationId });
|
|
1351
|
+
if (receipt.state !== "completed" || receipt.result?.head !== state.blocked.commit)
|
|
1352
|
+
throw new WorkspaceError("publication_unknown", "Original workspace publication not proven completed; preserve state for operator review");
|
|
1353
|
+
state.publishedHead = state.blocked.commit;
|
|
1354
|
+
state.head = state.blocked.commit;
|
|
1355
|
+
state.blocked = null;
|
|
1356
|
+
await this.saveOuter(state);
|
|
1357
|
+
}
|
|
1358
|
+
if (!state.initialized) {
|
|
1359
|
+
const remote2 = await this.remoteHead(storage, repositoryId, OUTER_BRANCH, true);
|
|
1360
|
+
if (remote2) {
|
|
1361
|
+
if (!await repo.has(remote2)) await repo.unbundle(await this.downloadBundle(storage, repositoryId));
|
|
1362
|
+
if (!await repo.isLegacyBootstrap(remote2)) await repo.checkout(remote2);
|
|
1363
|
+
await repo.setHead(remote2);
|
|
1364
|
+
}
|
|
1365
|
+
state.initialized = true;
|
|
1366
|
+
state.publishedHead = remote2;
|
|
1367
|
+
state.head = remote2;
|
|
1368
|
+
await this.saveOuter(state);
|
|
1369
|
+
if (options.bootstrapOnly) return { head: remote2, unchanged: true };
|
|
1370
|
+
} else if (options.bootstrapOnly) return { head: state.head, unchanged: true };
|
|
1371
|
+
if (state.conflict && !options.resolveConflict)
|
|
1372
|
+
throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, "Workspace synchronization conflict awaits resolution", state.conflict.paths);
|
|
1373
|
+
const inners = await this.innerCheckouts(userId);
|
|
1374
|
+
let snapshot;
|
|
1375
|
+
try {
|
|
1376
|
+
snapshot = await repo.snapshot(inners, { allowLargeDiff: options.allowLargeDiff });
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
if (error instanceof OuterSnapshotRefusal) {
|
|
1379
|
+
state.blocked = { code: error.code, message: error.message, paths: error.paths };
|
|
1380
|
+
await this.saveOuter(state);
|
|
1381
|
+
}
|
|
1382
|
+
throw error;
|
|
1383
|
+
}
|
|
1384
|
+
if (state.conflict) {
|
|
1385
|
+
const marker = /^(<{7}|={7}|>{7})( |$)/m;
|
|
1386
|
+
for (const file of state.conflict.paths) {
|
|
1387
|
+
const bytes2 = await readRegular(path.join(repo.workTree, file)).catch((error) => {
|
|
1388
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
|
|
1389
|
+
throw error;
|
|
1390
|
+
});
|
|
1391
|
+
if (bytes2 && marker.test(bytes2.toString("utf8")))
|
|
1392
|
+
throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, "Conflict markers remain; resolve them before confirming", [file]);
|
|
1393
|
+
}
|
|
1394
|
+
} else if (state.blocked && state.blocked.code !== "publication_unknown") {
|
|
1395
|
+
state.blocked = null;
|
|
1396
|
+
await this.saveOuter(state);
|
|
1397
|
+
}
|
|
1398
|
+
const base = state.publishedHead;
|
|
1399
|
+
const baseTree = base ? await repo.treeOf(base) : await repo.emptyTree();
|
|
1400
|
+
let local;
|
|
1401
|
+
if (state.conflict) {
|
|
1402
|
+
local = await repo.commit(snapshot.tree, [state.conflict.ours, state.conflict.theirs], "Resolve workspace synchronization conflict");
|
|
1403
|
+
state.conflict = null;
|
|
1404
|
+
state.blocked = null;
|
|
1405
|
+
} else if (base && snapshot.tree === baseTree) local = base;
|
|
1406
|
+
else local = await repo.commit(snapshot.tree, base ? [base] : [], "Workspace snapshot");
|
|
1407
|
+
if (state.head !== local) {
|
|
1408
|
+
state.head = local;
|
|
1409
|
+
await repo.setHead(local);
|
|
1410
|
+
await this.saveOuter(state);
|
|
1411
|
+
}
|
|
1412
|
+
const integrated = async (head) => {
|
|
1413
|
+
if (options.requireHead && !await repo.isAncestor(options.requireHead, head))
|
|
1414
|
+
throw new WorkspaceError("conflict", "The required workspace head is not integrated; retry the transfer");
|
|
1415
|
+
return options.requireHead ? { integrated: options.requireHead } : {};
|
|
1416
|
+
};
|
|
1417
|
+
const remote = await this.remoteHead(storage, repositoryId, OUTER_BRANCH, true);
|
|
1418
|
+
let currentTree = snapshot.tree;
|
|
1419
|
+
if (remote && remote !== base) {
|
|
1420
|
+
if (!await repo.has(remote)) await repo.unbundle(await this.downloadBundle(storage, repositoryId));
|
|
1421
|
+
if (local === base) {
|
|
1422
|
+
await repo.apply(currentTree, await repo.treeOf(remote));
|
|
1423
|
+
state.publishedHead = remote;
|
|
1424
|
+
state.head = remote;
|
|
1425
|
+
await repo.setHead(remote);
|
|
1426
|
+
await this.saveOuter(state);
|
|
1427
|
+
return { head: remote, unchanged: false, ...await integrated(remote) };
|
|
1428
|
+
}
|
|
1429
|
+
const merge = await repo.merge(local, remote);
|
|
1430
|
+
if (merge.conflicts.length) {
|
|
1431
|
+
await repo.apply(currentTree, merge.tree);
|
|
1432
|
+
state.conflict = { ours: local, theirs: remote, tree: merge.tree, paths: merge.conflicts };
|
|
1433
|
+
state.blocked = { code: OUTER_CONFLICT_CODE, message: "The workspace diverged from another worker; conflict markers were written to the paths named", paths: merge.conflicts };
|
|
1434
|
+
await this.saveOuter(state);
|
|
1435
|
+
throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, state.blocked.message, merge.conflicts);
|
|
1436
|
+
}
|
|
1437
|
+
await repo.apply(currentTree, merge.tree);
|
|
1438
|
+
currentTree = merge.tree;
|
|
1439
|
+
local = await repo.commit(merge.tree, [local, remote], "Merge workspace");
|
|
1440
|
+
state.head = local;
|
|
1441
|
+
await repo.setHead(local);
|
|
1442
|
+
await this.saveOuter(state);
|
|
1443
|
+
}
|
|
1444
|
+
if (local === remote) {
|
|
1445
|
+
state.publishedHead = local;
|
|
1446
|
+
await this.saveOuter(state);
|
|
1447
|
+
return { head: local, unchanged: true, ...await integrated(local) };
|
|
1448
|
+
}
|
|
1449
|
+
const operationId = `ws-${randomUUID()}`, blobId = `ws-${randomUUID()}`;
|
|
1450
|
+
const bytes = await repo.bundle(remote && await repo.has(remote) ? remote : null);
|
|
1451
|
+
state.blocked = { code: "publication_unknown", operationId, commit: local, message: "Workspace publication is in progress/unknown. Inspect the original storage operation; never retry under a new ID." };
|
|
1452
|
+
await this.saveOuter(state);
|
|
1453
|
+
try {
|
|
1454
|
+
await this.uploadBundle(storage, bytes, blobId, operationId);
|
|
1455
|
+
const result = await storage.mutate({
|
|
1456
|
+
method: "repository.publish",
|
|
1457
|
+
operationId,
|
|
1458
|
+
repositoryId,
|
|
1459
|
+
branch: OUTER_BRANCH,
|
|
1460
|
+
expectedHead: remote,
|
|
1461
|
+
commit: local,
|
|
1462
|
+
bundleId: blobId
|
|
1463
|
+
});
|
|
1464
|
+
if (result.head !== local) throw new WorkspaceError("invalid_response", "Workspace publication receipt head mismatch; inspect original operation");
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
if (error instanceof WorkspaceError && error.rejectedBeforeAdmission) {
|
|
1467
|
+
state.blocked = null;
|
|
1468
|
+
await this.saveOuter(state);
|
|
1469
|
+
throw new WorkspaceError("mirror_advanced", "The workspace mirror advanced during publication; retried next cycle", true);
|
|
1470
|
+
}
|
|
1471
|
+
throw error;
|
|
1472
|
+
}
|
|
1473
|
+
state.publishedHead = local;
|
|
1474
|
+
state.blocked = null;
|
|
1475
|
+
await this.saveOuter(state);
|
|
1476
|
+
return { head: local, unchanged: false, ...await integrated(local) };
|
|
1477
|
+
}
|
|
1478
|
+
/** Push a project checkout's own HEAD to its mirror. Last push wins: no merge
|
|
1479
|
+
* is attempted, and a rewind or rewrite replaces the mirror as readily as a
|
|
1480
|
+
* fast-forward. A lost acknowledgement needs no reconciliation because the
|
|
1481
|
+
* next cycle simply observes the mirror already holds the head. */
|
|
1482
|
+
async mirrorInnerIdle(b, identity) {
|
|
1483
|
+
if (!b.state.initialized || !this.linkedWorkbench(b) || b.state.blocked?.code === "archived") return { head: b.state.head, unchanged: true };
|
|
1484
|
+
const head = await this.innerHead(b);
|
|
1485
|
+
if (!head || head === b.state.mirroredHead) return { head, unchanged: true };
|
|
1486
|
+
const storage = await this.storage(b, identity);
|
|
1487
|
+
const remote = await this.remoteHead(storage, b.config.repositoryId, b.config.branch, true);
|
|
1488
|
+
if (remote !== head) {
|
|
1489
|
+
const ref = `refs/r5d/mirror/${b.config.branch}`;
|
|
1490
|
+
await git(b.repo, ["update-ref", ref, head]);
|
|
1491
|
+
const file = path.join(b.directory, `mirror-${randomUUID()}.bundle`);
|
|
1492
|
+
const known = remote !== null && await this.hasCommit(b.repo, remote);
|
|
1493
|
+
const rewound = known && (await gitResult(b.repo, ["merge-base", "--is-ancestor", head, remote])).code === 0;
|
|
1494
|
+
const exclude = known && !rewound ? [`^${remote}`] : (await gitResult(b.repo, ["rev-parse", "--verify", "--quiet", `${head}~1`])).code === 0 ? [`^${head}~1`] : [];
|
|
1495
|
+
await git(b.repo, ["bundle", "create", file, ref, ...exclude]);
|
|
1496
|
+
const bytes = await readRegular(file, STORAGE_LIMITS.blobBytes);
|
|
1497
|
+
await fs.rm(file, { force: true });
|
|
1498
|
+
const operationId = `mirror-${randomUUID()}`;
|
|
1499
|
+
const force = await this.forcePublishSupported(storage);
|
|
1500
|
+
try {
|
|
1501
|
+
await this.uploadBundle(storage, bytes, operationId, operationId);
|
|
1502
|
+
const result = await storage.mutate({
|
|
1503
|
+
method: "repository.publish",
|
|
1504
|
+
operationId,
|
|
1505
|
+
repositoryId: b.config.repositoryId,
|
|
1506
|
+
branch: b.config.branch,
|
|
1507
|
+
expectedHead: remote,
|
|
1508
|
+
commit: head,
|
|
1509
|
+
bundleId: operationId,
|
|
1510
|
+
...force ? { force: true } : {}
|
|
1511
|
+
});
|
|
1512
|
+
if (result.head !== head) throw new WorkspaceError("invalid_response", "Mirror receipt head mismatch");
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
if (error instanceof WorkspaceError && error.rejectedBeforeAdmission) return { head, unchanged: true };
|
|
1515
|
+
throw error;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
b.state.mirroredHead = head;
|
|
1519
|
+
b.state.head = head;
|
|
1520
|
+
await this.save(b);
|
|
1521
|
+
return { head, unchanged: false };
|
|
1522
|
+
}
|
|
1523
|
+
/** Mirror one project checkout now, outside the periodic cycle. */
|
|
1524
|
+
async mirror(identity) {
|
|
1525
|
+
const b = await this.bench(identity);
|
|
1526
|
+
if (!this.outerEnabled(b) || b.config.rootProfile === "account") return { head: b.state.head, unchanged: true };
|
|
1527
|
+
return this.serial(b, () => this.mirrorInnerIdle(b, identity));
|
|
1528
|
+
}
|
|
1529
|
+
/** Commit the checkout's working tree to its own history on the user's behalf. */
|
|
1530
|
+
async commitInnerIdle(b, identity, message) {
|
|
1531
|
+
if (!b.state.initialized) throw new WorkspaceError("not_initialized", "Hydrate first");
|
|
1532
|
+
await git(b.config.cwd, ["add", "-A"]);
|
|
1533
|
+
const dirty = (await git(b.config.cwd, ["status", "--porcelain", "-z"])).length > 0;
|
|
1534
|
+
const before = await this.innerHead(b);
|
|
1535
|
+
if (!dirty && before) return { head: before, unchanged: true };
|
|
1536
|
+
await git(b.config.cwd, ["commit", "--quiet", "--allow-empty", "-m", message], void 0, void 0, void 0, { env: PROJECT_COMMIT_IDENTITY });
|
|
1537
|
+
const head = await this.innerHead(b);
|
|
1538
|
+
await this.mirrorInnerIdle(b, identity);
|
|
1539
|
+
return { head };
|
|
1540
|
+
}
|
|
1541
|
+
/** Install a project checkout from its mirror. Files may already be present
|
|
1542
|
+
* because the outer repository delivered them first; the worktree is then
|
|
1543
|
+
* created around them and they show as ordinary dirty content. */
|
|
1544
|
+
async hydrateLinkedIdle(b, identity) {
|
|
1545
|
+
await this.assertAvailable(b);
|
|
1546
|
+
if (b.state.initialized) return { head: b.state.head ?? "" };
|
|
1547
|
+
const storage = await this.storage(b, identity);
|
|
1548
|
+
const mirror = await this.remoteHead(storage, b.config.repositoryId, b.config.branch);
|
|
1549
|
+
if (mirror && !await this.hasCommit(b.repo, mirror)) await this.unbundleInto(b, await this.downloadBundle(storage, b.config.repositoryId));
|
|
1550
|
+
let visible = mirror;
|
|
1551
|
+
while (visible && await this.platformSnapshot(b.repo, visible)) {
|
|
1552
|
+
const parent = await this.firstParent(b.repo, visible);
|
|
1553
|
+
if (!parent) break;
|
|
1554
|
+
visible = parent;
|
|
1555
|
+
}
|
|
1556
|
+
if (!visible && b.config.baseCommitHash && await this.hasCommit(b.repo, b.config.baseCommitHash)) visible = b.config.baseCommitHash;
|
|
1557
|
+
if (!visible) throw new WorkspaceError("empty_repository", "Seed README explicitly for an empty mirrored branch");
|
|
1558
|
+
b.state.blocked = { code: "hydration_incomplete", message: "Checkout installation interrupted; preserve the checkout and inspect before maintenance" };
|
|
1559
|
+
await this.save(b);
|
|
1560
|
+
await this.installLinkedWorktree(b, visible, mirror && visible !== mirror ? mirror : null);
|
|
1561
|
+
await this.configureLinkedRemote(b);
|
|
1562
|
+
b.state.initialized = true;
|
|
1563
|
+
b.state.head = mirror ?? visible;
|
|
1564
|
+
b.state.mirroredHead = mirror;
|
|
1565
|
+
b.state.blocked = null;
|
|
1566
|
+
delete b.state.publishedMetadata;
|
|
1567
|
+
await this.save(b);
|
|
1568
|
+
return { head: b.state.head ?? "" };
|
|
1569
|
+
}
|
|
1570
|
+
async installLinkedWorktree(b, visible, legacySnapshot) {
|
|
1571
|
+
const destination = path.join(b.config.cwd, ".git");
|
|
1572
|
+
if (await fs.lstat(destination).then(() => true, (error) => {
|
|
1573
|
+
if (error.code === "ENOENT") return false;
|
|
1574
|
+
throw error;
|
|
1575
|
+
})) return;
|
|
1576
|
+
await fs.mkdir(b.config.cwd, { recursive: true, mode: 448 });
|
|
1577
|
+
await noSymlinkAncestors(b.config.cwd);
|
|
1578
|
+
await git(b.repo, ["update-ref", `refs/heads/${b.config.branch}`, visible]);
|
|
1579
|
+
if (!(await fs.readdir(b.config.cwd)).length) {
|
|
1580
|
+
await fs.rmdir(b.config.cwd);
|
|
1581
|
+
await git(b.repo, ["worktree", "add", "--force", b.config.cwd, b.config.branch]);
|
|
1582
|
+
if (legacySnapshot) {
|
|
1583
|
+
await git(b.config.cwd, ["reset", "--hard", legacySnapshot]);
|
|
1584
|
+
await git(b.config.cwd, ["reset", "--mixed", visible]);
|
|
1585
|
+
}
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
const staging = path.join(b.directory, `worktree-${randomUUID()}`);
|
|
1589
|
+
await git(b.repo, ["worktree", "add", "--no-checkout", "--force", staging, b.config.branch]);
|
|
1590
|
+
const pointer = /^gitdir: (.+?)\s*$/.exec((await readRegular(path.join(staging, ".git"), 4096)).toString("utf8"));
|
|
1591
|
+
if (!pointer || !path.isAbsolute(pointer[1])) throw new WorkspaceError("unsafe_git", "Invalid linked worktree pointer");
|
|
1592
|
+
const gitDirectory = path.normalize(pointer[1]);
|
|
1593
|
+
if (!gitDirectory.startsWith(path.join(b.repo, "worktrees") + path.sep)) throw new WorkspaceError("unsafe_git", "Linked worktree belongs to another repository");
|
|
1594
|
+
await fs.writeFile(destination, `gitdir: ${gitDirectory}
|
|
1595
|
+
`, { mode: 384, flag: "wx" });
|
|
1596
|
+
await fs.writeFile(path.join(gitDirectory, "gitdir"), `${destination}
|
|
1597
|
+
`, { mode: 384 });
|
|
1598
|
+
await fs.rm(staging, { recursive: true });
|
|
1599
|
+
await git(b.config.cwd, ["reset", "--mixed", visible]);
|
|
1600
|
+
}
|
|
1153
1601
|
async publish(identity, options = {}) {
|
|
1154
1602
|
const b = await this.bench(identity);
|
|
1155
|
-
if (b.config.rootProfile === "account")
|
|
1603
|
+
if (b.config.rootProfile === "account") {
|
|
1604
|
+
if (!this.outerEnabled()) return { head: "", unchanged: true };
|
|
1605
|
+
return this.synchronizeOuter(b.config.userId, { allowLargeDiff: options.allowLargeDiff, resolveConflict: options.allowBlockedConflict });
|
|
1606
|
+
}
|
|
1607
|
+
if (this.outerEnabled(b)) {
|
|
1608
|
+
const outer = await this.synchronizeOuter(b.config.userId);
|
|
1609
|
+
await this.serial(b, () => this.mirrorInnerIdle(b, identity));
|
|
1610
|
+
return outer;
|
|
1611
|
+
}
|
|
1156
1612
|
return this.serial(b, async () => {
|
|
1157
|
-
const conflictBlocked = b.state.blocked
|
|
1613
|
+
const conflictBlocked = !!b.state.blocked && WORKBENCH_CONFLICT_CODES.has(b.state.blocked.code);
|
|
1158
1614
|
await this.assertAvailable(b, options.allowBlockedConflict === true && conflictBlocked);
|
|
1159
1615
|
return this.publishIdle(b, identity, "Destination workspace snapshot", options.allowLargeDiff === true);
|
|
1160
1616
|
});
|
|
1161
1617
|
}
|
|
1162
|
-
async publishIdle(b, identity, message = "Destination workspace snapshot", allowLargeDiff = false) {
|
|
1618
|
+
async publishIdle(b, identity, message = "Destination workspace snapshot", allowLargeDiff = false, projectIdentity = false) {
|
|
1163
1619
|
if (!b.state.initialized) throw new WorkspaceError("not_initialized", "Hydrate or explicitly seed first");
|
|
1164
1620
|
const storage = await this.storage(b, identity);
|
|
1165
1621
|
const latest = await storage.read({
|
|
@@ -1192,7 +1648,7 @@ class WorkspaceAuthority {
|
|
|
1192
1648
|
throw new WorkspaceError("too_large", "Source diff exceeds the 5242880-byte automatic publication limit");
|
|
1193
1649
|
}
|
|
1194
1650
|
const commit = (await git(b.repo, ["commit-tree", tree, ...b.state.head ? ["-p", b.state.head] : []], `${message}
|
|
1195
|
-
|
|
1651
|
+
`, void 0, void 0, projectIdentity ? { env: PROJECT_COMMIT_IDENTITY } : {})).toString().trim();
|
|
1196
1652
|
await git(b.repo, ["update-ref", this.canonicalRef(b), commit]);
|
|
1197
1653
|
const bundleFile = path.join(b.directory, `${randomUUID()}.bundle`);
|
|
1198
1654
|
await git(b.repo, ["bundle", "create", bundleFile, this.canonicalRef(b)]);
|
|
@@ -1226,18 +1682,7 @@ class WorkspaceAuthority {
|
|
|
1226
1682
|
message: "Publication attempt is in progress/unknown. Inspect original storage operation; NEVER retry under a new ID. Workbench and prepared commit retained."
|
|
1227
1683
|
};
|
|
1228
1684
|
await this.save(b);
|
|
1229
|
-
|
|
1230
|
-
const end = Math.min(bytes.length, offset + STORAGE_LIMITS.chunkBytes);
|
|
1231
|
-
await storage.mutate({
|
|
1232
|
-
method: "blob.append",
|
|
1233
|
-
operationId: `${operationId}-chunk-${offset}`,
|
|
1234
|
-
blobId,
|
|
1235
|
-
kind: "blob",
|
|
1236
|
-
offset,
|
|
1237
|
-
data: bytes.subarray(offset, end).toString("base64"),
|
|
1238
|
-
seal: end === bytes.length
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1685
|
+
await this.uploadBundle(storage, bytes, blobId, operationId);
|
|
1241
1686
|
const result = await storage.mutate({
|
|
1242
1687
|
method: "repository.publish",
|
|
1243
1688
|
operationId,
|
|
@@ -1313,7 +1758,7 @@ class WorkspaceAuthority {
|
|
|
1313
1758
|
const b = await this.bench(identity);
|
|
1314
1759
|
const operation = OperationEnvelope.parse(JSON.parse(canonicalJson(input)));
|
|
1315
1760
|
return this.serial(b, async () => {
|
|
1316
|
-
const conflictBlocked = b.state.blocked
|
|
1761
|
+
const conflictBlocked = !!b.state.blocked && WORKBENCH_CONFLICT_CODES.has(b.state.blocked.code);
|
|
1317
1762
|
if (!b.state.initialized || b.state.blocked && !(allowBlockedConflict && conflictBlocked))
|
|
1318
1763
|
throw new WorkspaceError("workbench_blocked", b.state.blocked?.message ?? "Initialize workbench first");
|
|
1319
1764
|
if (operation.installationId !== this.config.installationId || operation.userId !== identity.userId || operation.sessionId !== identity.sessionId || operation.kind !== "host.shell")
|