@ricsam/r5d-worker 0.0.101 → 0.0.104
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/README.md +16 -3
- package/dist/cjs/main.cjs +467 -336
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-mirror-fetch-policy.cjs +48 -7
- package/dist/cjs/project-mirror-refs-token.cjs +36 -2
- package/dist/cjs/project-worktrees.cjs +172 -63
- package/dist/cjs/workspace-git-sync.cjs +45 -6
- package/dist/cjs/workspace-mutation-gate.cjs +35 -0
- package/dist/cjs/workspace-projection-ledger.cjs +81 -0
- package/dist/cjs/workspace-sync-coalescer.cjs +58 -0
- package/dist/mjs/main.mjs +473 -338
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-mirror-fetch-policy.mjs +46 -6
- package/dist/mjs/project-mirror-refs-token.mjs +30 -1
- package/dist/mjs/project-worktrees.mjs +172 -64
- package/dist/mjs/workspace-git-sync.mjs +45 -6
- package/dist/mjs/workspace-mutation-gate.mjs +35 -0
- package/dist/mjs/workspace-projection-ledger.mjs +57 -0
- package/dist/mjs/workspace-sync-coalescer.mjs +34 -0
- package/dist/types/main.d.ts +35 -17
- package/dist/types/project-mirror-fetch-policy.d.ts +70 -15
- package/dist/types/project-mirror-refs-token.d.ts +34 -0
- package/dist/types/project-worktrees.d.ts +73 -8
- package/dist/types/workspace-git-sync.d.ts +24 -2
- package/dist/types/workspace-mutation-gate.d.ts +24 -3
- package/dist/types/workspace-projection-ledger.d.ts +59 -0
- package/dist/types/workspace-sync-coalescer.d.ts +28 -0
- package/package.json +1 -1
package/dist/mjs/package.json
CHANGED
|
@@ -1,16 +1,56 @@
|
|
|
1
|
-
const PROJECT_MIRROR_FETCH_BACKSTOP_MS = 10 * 6e4;
|
|
2
1
|
function projectMirrorFetchDecision(input) {
|
|
3
2
|
if (!input.skippingEnabled) return { fetch: true, reason: "skipping_disabled" };
|
|
4
3
|
if (input.advertisedToken === void 0) return { fetch: true, reason: "no_advertised_token" };
|
|
5
4
|
if (input.fetchedToken === void 0) return { fetch: true, reason: "never_fetched" };
|
|
6
5
|
if (input.advertisedToken !== input.fetchedToken) return { fetch: true, reason: "token_changed" };
|
|
7
|
-
const backstopMs = input.backstopMs ?? PROJECT_MIRROR_FETCH_BACKSTOP_MS;
|
|
8
|
-
if (input.lastFetchAtMs === void 0 || input.nowMs - input.lastFetchAtMs >= backstopMs) {
|
|
9
|
-
return { fetch: true, reason: "backstop_elapsed" };
|
|
10
|
-
}
|
|
11
6
|
return { fetch: false, reason: "token_match" };
|
|
12
7
|
}
|
|
8
|
+
function planProjectMirrorObservationFetch(input) {
|
|
9
|
+
if (!input.skippingEnabled) return { scope: "project", reason: "skipping_disabled" };
|
|
10
|
+
if (input.advertisedMountTokens === void 0) {
|
|
11
|
+
const decision = projectMirrorFetchDecision({
|
|
12
|
+
advertisedToken: input.advertisedProjectToken,
|
|
13
|
+
fetchedToken: input.fetchedProjectToken,
|
|
14
|
+
skippingEnabled: true
|
|
15
|
+
});
|
|
16
|
+
return decision.fetch ? { scope: "project", reason: decision.reason } : { scope: "none" };
|
|
17
|
+
}
|
|
18
|
+
const mounts = [];
|
|
19
|
+
for (const branchName of [...input.branchNames].sort()) {
|
|
20
|
+
const decision = projectMirrorFetchDecision({
|
|
21
|
+
advertisedToken: input.advertisedMountTokens.get(branchName),
|
|
22
|
+
fetchedToken: input.fetchedMountTokens.get(branchName),
|
|
23
|
+
skippingEnabled: true
|
|
24
|
+
});
|
|
25
|
+
if (decision.fetch) mounts.push({ branchName, reason: decision.reason });
|
|
26
|
+
}
|
|
27
|
+
return mounts.length === 0 ? { scope: "none" } : { scope: "mounts", mounts };
|
|
28
|
+
}
|
|
29
|
+
const TOKEN_RE = /^[0-9a-f]{64}$/;
|
|
30
|
+
const GIT_OBJECT_NAME_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
31
|
+
function parseProjectMirrorRefsTokensMessage(message) {
|
|
32
|
+
const projectTokens = /* @__PURE__ */ new Map();
|
|
33
|
+
if (message.tokens && typeof message.tokens === "object") {
|
|
34
|
+
for (const [projectId, token] of Object.entries(message.tokens)) {
|
|
35
|
+
if (typeof token === "string" && TOKEN_RE.test(token)) projectTokens.set(projectId, token);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const mountTokens = /* @__PURE__ */ new Map();
|
|
39
|
+
if (message.mountTokens && typeof message.mountTokens === "object") {
|
|
40
|
+
for (const [projectId, tokensByBranch] of Object.entries(message.mountTokens)) {
|
|
41
|
+
if (!tokensByBranch || typeof tokensByBranch !== "object") continue;
|
|
42
|
+
const perMount = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const [branchName, token] of Object.entries(tokensByBranch)) {
|
|
44
|
+
if (typeof token === "string" && TOKEN_RE.test(token)) perMount.set(branchName, token);
|
|
45
|
+
}
|
|
46
|
+
mountTokens.set(projectId, perMount);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const workspaceHead = typeof message.workspaceHead === "string" && GIT_OBJECT_NAME_RE.test(message.workspaceHead) ? message.workspaceHead : void 0;
|
|
50
|
+
return { projectTokens, mountTokens, workspaceHead };
|
|
51
|
+
}
|
|
13
52
|
export {
|
|
14
|
-
|
|
53
|
+
parseProjectMirrorRefsTokensMessage,
|
|
54
|
+
planProjectMirrorObservationFetch,
|
|
15
55
|
projectMirrorFetchDecision
|
|
16
56
|
};
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
const PROJECT_MIRROR_REFS_TOKEN_NAMESPACES = ["refs/heads/", "refs/r5d/server-synced/"];
|
|
2
3
|
const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
|
|
3
4
|
const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
|
|
5
|
+
const PROJECT_MIRROR_REF_LISTING_FORMAT = "--format=%(objectname) %(refname)";
|
|
6
|
+
const GIT_OBJECT_NAME_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
4
7
|
function hashProjectMirrorRefs(refs) {
|
|
5
8
|
const lines = refs.map(({ objectName, refname }) => `${objectName} ${refname}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
6
9
|
return createHash("sha256").update(lines.join("\n")).digest("hex");
|
|
7
10
|
}
|
|
11
|
+
function parseProjectMirrorRefListing(output, options = {}) {
|
|
12
|
+
return output.split(/\r?\n/).filter(Boolean).map((row) => {
|
|
13
|
+
const separator = row.indexOf(" ");
|
|
14
|
+
const objectName = separator < 0 ? "" : row.slice(0, separator);
|
|
15
|
+
const refname = separator < 0 ? "" : row.slice(separator + 1);
|
|
16
|
+
if (!GIT_OBJECT_NAME_RE.test(objectName) || refname.length === 0 || options.namespaces !== void 0 && !options.namespaces.some((prefix) => refname.startsWith(prefix))) {
|
|
17
|
+
throw new Error(`Invalid ref listing row: ${row}`);
|
|
18
|
+
}
|
|
19
|
+
return { refname, objectName };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
8
22
|
function canonicalizeLocalProjectMirrorRefs(refs) {
|
|
9
23
|
const canonical = [];
|
|
10
24
|
for (const { refname, objectName } of refs) {
|
|
@@ -16,9 +30,24 @@ function canonicalizeLocalProjectMirrorRefs(refs) {
|
|
|
16
30
|
}
|
|
17
31
|
return canonical;
|
|
18
32
|
}
|
|
33
|
+
function projectMirrorMountRefs(refs, mount) {
|
|
34
|
+
const headRef = `refs/heads/${mount.branchName}`;
|
|
35
|
+
const handoffPrefix = `${PROJECT_SERVER_SYNCED_REF_PREFIX}${mount.branchId}/`;
|
|
36
|
+
return refs.filter(({ refname }) => refname === headRef || refname.startsWith(handoffPrefix));
|
|
37
|
+
}
|
|
38
|
+
function projectMirrorRefsTokens(refs, mounts) {
|
|
39
|
+
const mountTokens = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const mount of mounts) mountTokens.set(mount.branchName, hashProjectMirrorRefs(projectMirrorMountRefs(refs, mount)));
|
|
41
|
+
return { project: hashProjectMirrorRefs(refs), mounts: mountTokens };
|
|
42
|
+
}
|
|
19
43
|
export {
|
|
44
|
+
PROJECT_MIRROR_REFS_TOKEN_NAMESPACES,
|
|
45
|
+
PROJECT_MIRROR_REF_LISTING_FORMAT,
|
|
20
46
|
PROJECT_MIRROR_REF_PREFIX,
|
|
21
47
|
PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
22
48
|
canonicalizeLocalProjectMirrorRefs,
|
|
23
|
-
hashProjectMirrorRefs
|
|
49
|
+
hashProjectMirrorRefs,
|
|
50
|
+
parseProjectMirrorRefListing,
|
|
51
|
+
projectMirrorMountRefs,
|
|
52
|
+
projectMirrorRefsTokens
|
|
24
53
|
};
|
|
@@ -6,9 +6,11 @@ import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProc
|
|
|
6
6
|
import { validateManagedBranchName } from "./managed-paths.mjs";
|
|
7
7
|
import {
|
|
8
8
|
canonicalizeLocalProjectMirrorRefs,
|
|
9
|
-
|
|
9
|
+
parseProjectMirrorRefListing,
|
|
10
|
+
PROJECT_MIRROR_REF_LISTING_FORMAT,
|
|
10
11
|
PROJECT_MIRROR_REF_PREFIX,
|
|
11
|
-
PROJECT_SERVER_SYNCED_REF_PREFIX
|
|
12
|
+
PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
13
|
+
projectMirrorRefsTokens
|
|
12
14
|
} from "./project-mirror-refs-token.mjs";
|
|
13
15
|
import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
|
|
14
16
|
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
@@ -285,6 +287,8 @@ function normalizeSnapshotManifest(value) {
|
|
|
285
287
|
if (record.state !== "building" && record.state !== "prepared" && record.state !== "restoring" && record.state !== "consumed") {
|
|
286
288
|
throw new Error("Project worktree snapshot manifest has an invalid state");
|
|
287
289
|
}
|
|
290
|
+
const scope = record.scope === void 0 ? "project" : record.scope;
|
|
291
|
+
if (scope !== "project" && scope !== "branches") throw new Error("Project worktree snapshot manifest has an invalid scope");
|
|
288
292
|
if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
|
|
289
293
|
throw new Error("Project worktree snapshot manifest has an invalid owner process");
|
|
290
294
|
}
|
|
@@ -320,6 +324,7 @@ function normalizeSnapshotManifest(value) {
|
|
|
320
324
|
return {
|
|
321
325
|
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
322
326
|
state: record.state,
|
|
327
|
+
scope,
|
|
323
328
|
ownerProcessId: record.ownerProcessId,
|
|
324
329
|
ownerSessionId: record.ownerSessionId,
|
|
325
330
|
projectRoot: record.projectRoot,
|
|
@@ -356,7 +361,7 @@ function writeSnapshotManifest(snapshotRoot, manifest) {
|
|
|
356
361
|
return readSnapshotManifest(snapshotRoot);
|
|
357
362
|
}
|
|
358
363
|
function assertSameSnapshotIdentity(expected, actual) {
|
|
359
|
-
if (expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
|
|
364
|
+
if (expected.scope !== actual.scope || expected.ownerProcessId !== actual.ownerProcessId || expected.ownerSessionId !== actual.ownerSessionId || expected.projectRoot !== actual.projectRoot || expected.createdAt !== actual.createdAt || JSON.stringify(expected.branches) !== JSON.stringify(actual.branches)) {
|
|
360
365
|
throw new Error("Project worktree snapshot identity changed");
|
|
361
366
|
}
|
|
362
367
|
}
|
|
@@ -423,7 +428,7 @@ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
|
|
|
423
428
|
}
|
|
424
429
|
}
|
|
425
430
|
}
|
|
426
|
-
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
|
|
431
|
+
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope = "project") {
|
|
427
432
|
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
428
433
|
const resolvedTemporaryRoot = path.resolve(temporaryRoot ?? os.tmpdir());
|
|
429
434
|
assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
|
|
@@ -444,6 +449,7 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
|
|
|
444
449
|
let manifest = writeSnapshotManifest(root, {
|
|
445
450
|
version: PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION,
|
|
446
451
|
state: "building",
|
|
452
|
+
scope,
|
|
447
453
|
ownerProcessId: process.pid,
|
|
448
454
|
ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
|
|
449
455
|
projectRoot: resolvedProjectRoot,
|
|
@@ -550,7 +556,7 @@ function restoreProjectWorktreeSnapshot(snapshotRoot, manifest, onProgress) {
|
|
|
550
556
|
const presentBranches = current.branches.filter(({ snapshotPresent }) => snapshotPresent);
|
|
551
557
|
const destinationParent = path.dirname(current.projectRoot);
|
|
552
558
|
fs.mkdirSync(destinationParent, { recursive: true });
|
|
553
|
-
const canMoveWholeSnapshot = presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === fs.lstatSync(destinationParent).dev);
|
|
559
|
+
const canMoveWholeSnapshot = current.scope === "project" && presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === fs.lstatSync(destinationParent).dev);
|
|
554
560
|
if (canMoveWholeSnapshot) {
|
|
555
561
|
if (current.state === "prepared") current = advanceSnapshotState(snapshotRoot, current, "restoring");
|
|
556
562
|
onProgress?.({
|
|
@@ -641,8 +647,12 @@ function consumeAndRemoveProjectWorktreeSnapshot(snapshotRoot, manifest) {
|
|
|
641
647
|
} catch {
|
|
642
648
|
}
|
|
643
649
|
}
|
|
644
|
-
function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync) {
|
|
645
|
-
|
|
650
|
+
function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFsync, durabilityPaths) {
|
|
651
|
+
if (durabilityPaths) {
|
|
652
|
+
for (const durabilityPath of durabilityPaths) if (fs.existsSync(durabilityPath)) fsyncTree(durabilityPath);
|
|
653
|
+
} else {
|
|
654
|
+
fsyncTree(snapshot.manifest.projectRoot);
|
|
655
|
+
}
|
|
646
656
|
fsyncManagedProjectRootEntry(snapshot.manifest.projectRoot);
|
|
647
657
|
afterProjectTreeFsync?.();
|
|
648
658
|
consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, snapshot.manifest);
|
|
@@ -940,8 +950,10 @@ async function reconcileLinkedLayout(input) {
|
|
|
940
950
|
await configureRepository({ ...input, primaryPath });
|
|
941
951
|
await tryGitAsync(primaryPath, ["worktree", "prune"]);
|
|
942
952
|
await fetchProjectHeads({ ...input, primaryPath });
|
|
953
|
+
const primaryCommonDir = await commonGitDirectoryAsync(primaryPath);
|
|
954
|
+
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
943
955
|
const mirrorHeads = /* @__PURE__ */ new Map();
|
|
944
|
-
const
|
|
956
|
+
const plan = [];
|
|
945
957
|
for (const branch of input.branches) {
|
|
946
958
|
const checkoutPath = branchPath(input.projectRoot, branch.branchName);
|
|
947
959
|
const resolved = await seedForBranch({
|
|
@@ -951,24 +963,30 @@ async function reconcileLinkedLayout(input) {
|
|
|
951
963
|
requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
|
|
952
964
|
});
|
|
953
965
|
mirrorHeads.set(branch.branchName, resolved.mirrorHead);
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
input.beforeDestructiveMutation();
|
|
958
|
-
mutatedBranches.add(branch.branchName);
|
|
959
|
-
await preserveHead(checkoutPath);
|
|
960
|
-
await gitAsync(checkoutPath, ["reset", "--hard", resolved.seed], `anchor linked worktree ${branch.branchName}`);
|
|
966
|
+
const linked = branch.branchName === input.primaryBranchName || gitDirectoryKind(checkoutPath) !== null && await commonGitDirectoryAsync(checkoutPath) === primaryCommonDir;
|
|
967
|
+
if (!linked) {
|
|
968
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: "recreate" });
|
|
961
969
|
continue;
|
|
962
970
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
971
|
+
const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
|
|
972
|
+
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: currentHead === resolved.seed ? "keep" : "reset" });
|
|
973
|
+
}
|
|
974
|
+
const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
|
|
975
|
+
if (mutatedBranches.size > 0) input.beforeDestructiveMutation(mutatedBranches);
|
|
976
|
+
for (const entry of plan) {
|
|
977
|
+
if (entry.action === "keep") continue;
|
|
978
|
+
if (entry.action === "reset") {
|
|
979
|
+
await preserveHead(entry.checkoutPath);
|
|
980
|
+
await gitAsync(entry.checkoutPath, ["reset", "--hard", entry.seed], `anchor linked worktree ${entry.branchName}`);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
fs.rmSync(entry.checkoutPath, { recursive: true, force: true });
|
|
984
|
+
await gitAsync(primaryPath, ["branch", "-f", entry.branchName, entry.seed], `anchor project branch ${entry.branchName}`);
|
|
985
|
+
fs.mkdirSync(path.dirname(entry.checkoutPath), { recursive: true });
|
|
968
986
|
await gitAsync(
|
|
969
987
|
primaryPath,
|
|
970
|
-
["worktree", "add", "--force", checkoutPath,
|
|
971
|
-
`create linked worktree ${
|
|
988
|
+
["worktree", "add", "--force", entry.checkoutPath, entry.branchName],
|
|
989
|
+
`create linked worktree ${entry.branchName}`
|
|
972
990
|
);
|
|
973
991
|
}
|
|
974
992
|
return { mirrorHeads, mutatedBranches };
|
|
@@ -995,8 +1013,14 @@ async function ensureProjectWorktrees(input) {
|
|
|
995
1013
|
const snapshotStorageRoot = path.resolve(input.snapshotRoot ?? os.tmpdir());
|
|
996
1014
|
assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
|
|
997
1015
|
let snapshot;
|
|
998
|
-
const ensureSnapshot = () => {
|
|
999
|
-
snapshot ??= snapshotBranchTrees(
|
|
1016
|
+
const ensureSnapshot = (branchNames) => {
|
|
1017
|
+
snapshot ??= snapshotBranchTrees(
|
|
1018
|
+
projectRoot,
|
|
1019
|
+
branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches,
|
|
1020
|
+
snapshotStorageRoot,
|
|
1021
|
+
input.onSnapshotProgress,
|
|
1022
|
+
branchNames ? "branches" : "project"
|
|
1023
|
+
);
|
|
1000
1024
|
return snapshot;
|
|
1001
1025
|
};
|
|
1002
1026
|
try {
|
|
@@ -1048,7 +1072,21 @@ async function ensureProjectWorktrees(input) {
|
|
|
1048
1072
|
});
|
|
1049
1073
|
}
|
|
1050
1074
|
states.sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
1051
|
-
if (snapshot)
|
|
1075
|
+
if (snapshot) {
|
|
1076
|
+
const primaryGitDirectory = path.join(branchPath(projectRoot, input.primaryBranchName), ".git");
|
|
1077
|
+
persistReconciledProjectAndConsumeSnapshot(
|
|
1078
|
+
snapshot,
|
|
1079
|
+
void 0,
|
|
1080
|
+
snapshot.manifest.scope === "branches" ? [
|
|
1081
|
+
...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
|
|
1082
|
+
// Branch refs and worktree registrations moved in the shared
|
|
1083
|
+
// administrative directory; objects are fsynced by git itself.
|
|
1084
|
+
path.join(primaryGitDirectory, "refs"),
|
|
1085
|
+
path.join(primaryGitDirectory, "packed-refs"),
|
|
1086
|
+
path.join(primaryGitDirectory, "worktrees")
|
|
1087
|
+
] : void 0
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1052
1090
|
return states;
|
|
1053
1091
|
} catch (error) {
|
|
1054
1092
|
if (!snapshot) throw error;
|
|
@@ -1125,8 +1163,12 @@ function createLinkedProjectBranch(input) {
|
|
|
1125
1163
|
);
|
|
1126
1164
|
configureProjectBranchTracking(targetPath, input.branchName, input.branchName);
|
|
1127
1165
|
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
1128
|
-
|
|
1129
|
-
|
|
1166
|
+
if (input.workingTree === "carry") {
|
|
1167
|
+
git(targetPath, ["read-tree", sourceHead], `initialize linked worktree index ${input.branchName}`);
|
|
1168
|
+
mirrorWorkingTree({ sourceRoot: sourcePath, targetRoot: targetPath, sourceMode: "git", deletionMode: "git" });
|
|
1169
|
+
} else {
|
|
1170
|
+
git(targetPath, ["reset", "--hard", sourceHead], `check out linked worktree ${input.branchName}`);
|
|
1171
|
+
}
|
|
1130
1172
|
return { branchPath: targetPath, baseCommitHash: sourceHead };
|
|
1131
1173
|
} catch (error) {
|
|
1132
1174
|
if (branchCreated) {
|
|
@@ -1190,6 +1232,53 @@ function createOrRetryLinkedProjectBranch(input) {
|
|
|
1190
1232
|
}
|
|
1191
1233
|
return createLinkedProjectBranch(input);
|
|
1192
1234
|
}
|
|
1235
|
+
function classifyProjectCheckout(input) {
|
|
1236
|
+
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1237
|
+
let stat;
|
|
1238
|
+
try {
|
|
1239
|
+
stat = fs.lstatSync(checkoutPath);
|
|
1240
|
+
} catch {
|
|
1241
|
+
return "missing";
|
|
1242
|
+
}
|
|
1243
|
+
if (!stat.isDirectory()) return "not_a_worktree";
|
|
1244
|
+
if (input.branchName === input.primaryBranchName) return gitDirectoryKind(checkoutPath) === "directory" ? "linked" : "not_a_worktree";
|
|
1245
|
+
if (gitDirectoryKind(checkoutPath) !== "file") return "not_a_worktree";
|
|
1246
|
+
const gitDirectory = perWorktreeGitDirectory(checkoutPath);
|
|
1247
|
+
if (!gitDirectory) return "not_a_worktree";
|
|
1248
|
+
let registry;
|
|
1249
|
+
let registered;
|
|
1250
|
+
try {
|
|
1251
|
+
registry = fs.realpathSync(path.join(branchPath(input.projectRoot, input.primaryBranchName), ".git", "worktrees"));
|
|
1252
|
+
registered = fs.realpathSync(gitDirectory);
|
|
1253
|
+
} catch {
|
|
1254
|
+
return "not_a_worktree";
|
|
1255
|
+
}
|
|
1256
|
+
const relative = path.relative(registry, registered);
|
|
1257
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return "not_a_worktree";
|
|
1258
|
+
return "linked";
|
|
1259
|
+
}
|
|
1260
|
+
function inventoryProjectCheckouts(input) {
|
|
1261
|
+
const states = input.branchNames.map((branchName) => ({
|
|
1262
|
+
branchName,
|
|
1263
|
+
checkoutPath: branchPath(input.projectRoot, branchName),
|
|
1264
|
+
state: classifyProjectCheckout({ ...input, branchName })
|
|
1265
|
+
}));
|
|
1266
|
+
const primaryLinked = states.some(({ branchName, state }) => branchName === input.primaryBranchName && state === "linked");
|
|
1267
|
+
const inventory = { linkedBranchNames: [], missingCheckouts: [] };
|
|
1268
|
+
for (const { branchName, checkoutPath, state } of states) {
|
|
1269
|
+
if (primaryLinked && state === "linked") inventory.linkedBranchNames.push(branchName);
|
|
1270
|
+
else inventory.missingCheckouts.push({ branchName, checkoutPath, reason: state === "linked" ? "not_a_worktree" : state });
|
|
1271
|
+
}
|
|
1272
|
+
return inventory;
|
|
1273
|
+
}
|
|
1274
|
+
const PROJECT_REMOVED_CHECKOUTS_DIRECTORY = ".r5d-removed";
|
|
1275
|
+
function moveProjectCheckoutAside(projectRoot, branchName, checkoutPath) {
|
|
1276
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1277
|
+
const target = path.join(projectRoot, PROJECT_REMOVED_CHECKOUTS_DIRECTORY, ...`${branchName}-${timestamp}`.split("/"));
|
|
1278
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1279
|
+
fs.renameSync(checkoutPath, target);
|
|
1280
|
+
return target;
|
|
1281
|
+
}
|
|
1193
1282
|
function deleteLinkedProjectBranch(input) {
|
|
1194
1283
|
validateManagedBranchName(input.primaryBranchName);
|
|
1195
1284
|
validateManagedBranchName(input.branchName);
|
|
@@ -1198,15 +1287,18 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1198
1287
|
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
1199
1288
|
const primaryCommonDir = commonGitDirectory(primaryPath);
|
|
1200
1289
|
if (!primaryCommonDir) throw new Error("Primary project checkout is unavailable");
|
|
1290
|
+
let movedAsidePath;
|
|
1201
1291
|
if (fs.existsSync(checkoutPath)) {
|
|
1202
|
-
const checkoutCommonDir = commonGitDirectory(checkoutPath);
|
|
1292
|
+
const checkoutCommonDir = fs.lstatSync(checkoutPath).isDirectory() ? commonGitDirectory(checkoutPath) : null;
|
|
1203
1293
|
if (checkoutCommonDir !== primaryCommonDir) {
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1294
|
+
movedAsidePath = moveProjectCheckoutAside(input.projectRoot, input.branchName, checkoutPath);
|
|
1295
|
+
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1296
|
+
} else {
|
|
1297
|
+
if (projectWorktreeOperationInProgress(checkoutPath)) {
|
|
1298
|
+
throw new Error(`Project branch ${input.branchName} has an in-progress Git operation`);
|
|
1299
|
+
}
|
|
1300
|
+
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1208
1301
|
}
|
|
1209
|
-
git(primaryPath, ["worktree", "remove", "--force", checkoutPath], `remove linked worktree ${input.branchName}`);
|
|
1210
1302
|
} else {
|
|
1211
1303
|
tryGit(primaryPath, ["worktree", "prune"]);
|
|
1212
1304
|
}
|
|
@@ -1214,7 +1306,7 @@ function deleteLinkedProjectBranch(input) {
|
|
|
1214
1306
|
if (tryGit(primaryPath, ["show-ref", "--verify", "--quiet", branchRef])) {
|
|
1215
1307
|
git(primaryPath, ["branch", "-D", input.branchName], `delete project branch ${input.branchName}`);
|
|
1216
1308
|
}
|
|
1217
|
-
return { branchName: input.branchName };
|
|
1309
|
+
return { branchName: input.branchName, ...movedAsidePath ? { movedAsidePath } : {} };
|
|
1218
1310
|
}
|
|
1219
1311
|
function removeProjectWorktrees(input) {
|
|
1220
1312
|
const projectRoot = path.resolve(input.projectRoot);
|
|
@@ -1316,24 +1408,46 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1316
1408
|
throw new Error(`Hidden project mirror publication skipped ${skipped.branchName}${skipped.reason ? ` (${skipped.reason})` : ""}`);
|
|
1317
1409
|
}
|
|
1318
1410
|
}
|
|
1411
|
+
const PROJECT_MIRROR_FETCH_REFSPECS = ["+refs/heads/*:refs/r5d/mirror/*", "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"];
|
|
1412
|
+
function projectMirrorMountFetchRefspecs(branchName, branchId) {
|
|
1413
|
+
validateManagedBranchName(branchName);
|
|
1414
|
+
if (branchId !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
|
|
1415
|
+
return [
|
|
1416
|
+
`+refs/heads/${branchName}:refs/r5d/mirror/${branchName}`,
|
|
1417
|
+
branchId === void 0 ? "+refs/r5d/server-synced/*:refs/r5d/server-synced/*" : `+refs/r5d/server-synced/${branchId}/*:refs/r5d/server-synced/${branchId}/*`
|
|
1418
|
+
];
|
|
1419
|
+
}
|
|
1420
|
+
async function fetchProjectMirrorRefs(input) {
|
|
1421
|
+
const fetch = (refspecs) => gitResultAsync(
|
|
1422
|
+
input.primaryPath,
|
|
1423
|
+
[
|
|
1424
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1425
|
+
"fetch",
|
|
1426
|
+
"--no-recurse-submodules",
|
|
1427
|
+
"--prune",
|
|
1428
|
+
input.mirrorUrl,
|
|
1429
|
+
...refspecs
|
|
1430
|
+
],
|
|
1431
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1432
|
+
);
|
|
1433
|
+
if (input.fetchOnlyBranches && input.fetchOnlyBranches.size > 0) {
|
|
1434
|
+
const refspecs = [...input.fetchOnlyBranches].sort().flatMap((branchName) => projectMirrorMountFetchRefspecs(branchName, input.branchIds?.get(branchName)));
|
|
1435
|
+
const scoped = await fetch(refspecs);
|
|
1436
|
+
if (scoped.exitCode === 0) return;
|
|
1437
|
+
if (!/(couldn't find remote ref|no such ref)/i.test(`${scoped.stderr}
|
|
1438
|
+
${scoped.stdout}`)) {
|
|
1439
|
+
throw new Error(`Refresh project head mirror: ${scoped.stderr || scoped.stdout || `git exited ${scoped.exitCode}`}`);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const whole = await fetch(PROJECT_MIRROR_FETCH_REFSPECS);
|
|
1443
|
+
if (whole.exitCode !== 0) {
|
|
1444
|
+
throw new Error(`Refresh project head mirror: ${whole.stderr || whole.stdout || `git exited ${whole.exitCode}`}`);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1319
1447
|
async function observeProjectMirrorHeads(input) {
|
|
1320
1448
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1321
|
-
if (input.refreshRemote !== false) {
|
|
1322
|
-
|
|
1323
|
-
primaryPath,
|
|
1324
|
-
[
|
|
1325
|
-
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1326
|
-
"fetch",
|
|
1327
|
-
"--no-recurse-submodules",
|
|
1328
|
-
"--prune",
|
|
1329
|
-
input.mirrorUrl,
|
|
1330
|
-
"+refs/heads/*:refs/r5d/mirror/*",
|
|
1331
|
-
"+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
|
|
1332
|
-
],
|
|
1333
|
-
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1334
|
-
);
|
|
1335
|
-
if (fetch.exitCode !== 0)
|
|
1336
|
-
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
1449
|
+
if (input.refreshRemote !== false && !(input.fetchOnlyBranches && input.fetchOnlyBranches.size === 0)) {
|
|
1450
|
+
await fetchProjectMirrorRefs({ ...input, primaryPath });
|
|
1337
1451
|
}
|
|
1338
1452
|
const observations = [];
|
|
1339
1453
|
for (const branchName of [...input.branchNames].sort()) {
|
|
@@ -1380,23 +1494,14 @@ async function observeProjectMirrorHeads(input) {
|
|
|
1380
1494
|
}
|
|
1381
1495
|
return observations;
|
|
1382
1496
|
}
|
|
1383
|
-
async function
|
|
1497
|
+
async function computeLocalProjectMirrorRefsTokens(input) {
|
|
1384
1498
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1385
1499
|
const rows = await gitAsync(
|
|
1386
1500
|
primaryPath,
|
|
1387
|
-
["for-each-ref",
|
|
1501
|
+
["for-each-ref", PROJECT_MIRROR_REF_LISTING_FORMAT, PROJECT_MIRROR_REF_PREFIX, PROJECT_SERVER_SYNCED_REF_PREFIX],
|
|
1388
1502
|
"list cached project mirror refs"
|
|
1389
1503
|
);
|
|
1390
|
-
|
|
1391
|
-
const separator = row.indexOf(" ");
|
|
1392
|
-
const objectName = separator < 0 ? "" : row.slice(0, separator);
|
|
1393
|
-
const refname = separator < 0 ? "" : row.slice(separator + 1);
|
|
1394
|
-
if (!/^[0-9a-f]{40,64}$/.test(objectName) || refname.length === 0) {
|
|
1395
|
-
throw new Error(`Invalid cached mirror ref listing row: ${row}`);
|
|
1396
|
-
}
|
|
1397
|
-
return { refname, objectName };
|
|
1398
|
-
});
|
|
1399
|
-
return hashProjectMirrorRefs(canonicalizeLocalProjectMirrorRefs(refs));
|
|
1504
|
+
return projectMirrorRefsTokens(canonicalizeLocalProjectMirrorRefs(parseProjectMirrorRefListing(rows)), input.mounts);
|
|
1400
1505
|
}
|
|
1401
1506
|
async function applyObservedProjectMirrorHeads(input) {
|
|
1402
1507
|
const results = [];
|
|
@@ -1460,7 +1565,7 @@ const projectWorktreesTestHarness = {
|
|
|
1460
1565
|
commandArgs: gitCommandArgs,
|
|
1461
1566
|
configureRepository,
|
|
1462
1567
|
createPreparedSnapshot(input) {
|
|
1463
|
-
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot).root };
|
|
1568
|
+
return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, void 0, input.scope).root };
|
|
1464
1569
|
},
|
|
1465
1570
|
consumeSnapshot(snapshotRoot) {
|
|
1466
1571
|
const manifest = readSnapshotManifest(snapshotRoot);
|
|
@@ -1475,11 +1580,13 @@ const projectWorktreesTestHarness = {
|
|
|
1475
1580
|
}
|
|
1476
1581
|
};
|
|
1477
1582
|
export {
|
|
1583
|
+
PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
1478
1584
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
1479
1585
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1480
1586
|
applyObservedProjectMirrorHeads,
|
|
1481
1587
|
assertProjectMirrorHeadsPushed,
|
|
1482
|
-
|
|
1588
|
+
classifyProjectCheckout,
|
|
1589
|
+
computeLocalProjectMirrorRefsTokens,
|
|
1483
1590
|
createLinkedProjectBranch,
|
|
1484
1591
|
createOrRetryLinkedProjectBranch,
|
|
1485
1592
|
deleteLinkedProjectBranch,
|
|
@@ -1487,6 +1594,7 @@ export {
|
|
|
1487
1594
|
ensureProjectWorktrees,
|
|
1488
1595
|
fastForwardProjectHeadsFromMirror,
|
|
1489
1596
|
hasLinkedProjectWorktreeLayout,
|
|
1597
|
+
inventoryProjectCheckouts,
|
|
1490
1598
|
observeProjectMirrorHeads,
|
|
1491
1599
|
projectBranchMayExistAfterCreateFailure,
|
|
1492
1600
|
projectOriginBranchName,
|
|
@@ -1352,6 +1352,19 @@ ${result.stdout}`;
|
|
|
1352
1352
|
}
|
|
1353
1353
|
return revParse(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
1354
1354
|
}
|
|
1355
|
+
async function resolveWorkspaceRemoteHead(workspacePath, input) {
|
|
1356
|
+
const advertised = input.advertisedRemoteHead;
|
|
1357
|
+
if (advertised && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(advertised)) {
|
|
1358
|
+
const tracked = await revParseAsync(workspacePath, `refs/remotes/origin/${WORKSPACE_GIT_BRANCH}`);
|
|
1359
|
+
if (tracked === advertised && await tryGitAsync(workspacePath, ["cat-file", "-e", `${tracked}^{commit}`])) {
|
|
1360
|
+
return { head: tracked, fetched: false };
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return {
|
|
1364
|
+
head: await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername),
|
|
1365
|
+
fetched: true
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1355
1368
|
async function ensureWorkspaceGitClone(input) {
|
|
1356
1369
|
const workspacePath = path.resolve(input.workspacePath);
|
|
1357
1370
|
const workspaceParent = path.dirname(workspacePath);
|
|
@@ -1399,7 +1412,8 @@ async function ensureWorkspaceGitClone(input) {
|
|
|
1399
1412
|
if (!await revParseAsync(workspacePath, WORKSPACE_GIT_INTEGRATED_REF) && previousRemoteHead && localHeadBeforeFetch && await tryGitAsync(workspacePath, ["merge-base", "--is-ancestor", previousRemoteHead, localHeadBeforeFetch])) {
|
|
1400
1413
|
await updateIntegratedWorkspaceHead(workspacePath, previousRemoteHead);
|
|
1401
1414
|
}
|
|
1402
|
-
const
|
|
1415
|
+
const resolvedRemoteHead = await resolveWorkspaceRemoteHead(workspacePath, input);
|
|
1416
|
+
const remoteHead = resolvedRemoteHead.head;
|
|
1403
1417
|
let localHead = await revParseAsync(workspacePath, "HEAD");
|
|
1404
1418
|
if (!localHead && remoteHead) {
|
|
1405
1419
|
await runWorkspaceCheckoutTransition(workspacePath, async () => {
|
|
@@ -1420,7 +1434,7 @@ async function ensureWorkspaceGitClone(input) {
|
|
|
1420
1434
|
await updateIntegratedWorkspaceHead(workspacePath, remoteHead);
|
|
1421
1435
|
localHead = remoteHead;
|
|
1422
1436
|
}
|
|
1423
|
-
return { localHead, remoteHead };
|
|
1437
|
+
return { localHead, remoteHead, remoteHeadFetched: resolvedRemoteHead.fetched };
|
|
1424
1438
|
}
|
|
1425
1439
|
function activeMounts(mounts) {
|
|
1426
1440
|
const active = [];
|
|
@@ -2069,6 +2083,11 @@ async function synchronizeWithFetchedHead(input) {
|
|
|
2069
2083
|
return rebased.ok ? { kind: "ready", updated: false, rebased: true } : { kind: "conflict", conflictPaths: rebased.conflictPaths, refs: rebased.refs, error: rebased.error };
|
|
2070
2084
|
}
|
|
2071
2085
|
async function synchronizeWorkspaceGit(input) {
|
|
2086
|
+
const remoteHeadFetches = { count: 0 };
|
|
2087
|
+
const result = await runWorkspaceGitSynchronization(input, remoteHeadFetches);
|
|
2088
|
+
return { ...result, remoteHeadFetches: remoteHeadFetches.count };
|
|
2089
|
+
}
|
|
2090
|
+
async function runWorkspaceGitSynchronization(input, remoteHeadFetches) {
|
|
2072
2091
|
const workspacePath = path.resolve(input.workspacePath);
|
|
2073
2092
|
const attemptId = input.attemptId ?? crypto.randomUUID();
|
|
2074
2093
|
const maxDiffBytes = input.maxDiffBytes ?? MAX_WORKSPACE_GIT_DIFF_BYTES;
|
|
@@ -2077,6 +2096,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2077
2096
|
const requiredAncestorHeads = requiredWorkspaceAncestorHeads(input.requiredAncestorHeads);
|
|
2078
2097
|
const preserveResolutionInProgress = input.skipMountMirror === true;
|
|
2079
2098
|
const initial = await ensureWorkspaceGitClone({ ...input, workspacePath, preserveResolutionInProgress });
|
|
2099
|
+
if (initial.remoteHeadFetched) remoteHeadFetches.count += 1;
|
|
2080
2100
|
assertWorkspaceRemediationAncestry({
|
|
2081
2101
|
workspacePath,
|
|
2082
2102
|
currentHead: initial.localHead,
|
|
@@ -2124,11 +2144,22 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2124
2144
|
const classifyMounts = (activeCandidates, additionalSkippedIds = []) => {
|
|
2125
2145
|
refreshCycleSkippedMounts();
|
|
2126
2146
|
const skippedIds = /* @__PURE__ */ new Set([...cycleSkippedMountIds, ...projectionSkippedIds, ...additionalSkippedIds]);
|
|
2147
|
+
const activeMountIds = [...new Set(activeCandidates.filter(({ id }) => !skippedIds.has(id)).map(({ id }) => id))].sort();
|
|
2127
2148
|
return {
|
|
2128
|
-
activeMountIds
|
|
2129
|
-
skippedMountIds: [...skippedIds].sort()
|
|
2149
|
+
activeMountIds,
|
|
2150
|
+
skippedMountIds: [...skippedIds].sort(),
|
|
2151
|
+
// An active mount's token is unchanged since capture (a moved token
|
|
2152
|
+
// lands it in cycleSkippedMountIds above), so this is the token its
|
|
2153
|
+
// projected bytes belong to.
|
|
2154
|
+
projectedMutationTokens: Object.fromEntries(
|
|
2155
|
+
activeMountIds.flatMap((id) => {
|
|
2156
|
+
const token = projectionMutationTokens.get(id);
|
|
2157
|
+
return token === void 0 ? [] : [[id, token]];
|
|
2158
|
+
})
|
|
2159
|
+
)
|
|
2130
2160
|
};
|
|
2131
2161
|
};
|
|
2162
|
+
const mountProjectionIsCurrent = (mount) => mount.lastProjectedMutationToken !== void 0 && !mount.deleteWhenSourceMissing && mount.preserveLocalOnHydrationBasisChange !== true && projectionBasisHead !== null && workspaceHydrationReceiptMountBasisHead(projectionBasisReceipt, mount) === projectionBasisHead && projectionMutationTokens.get(mount.id) === mount.lastProjectedMutationToken && mount.mutationToken?.() === mount.lastProjectedMutationToken;
|
|
2132
2163
|
if (!input.skipMountMirror) {
|
|
2133
2164
|
const mergeProjectionEnabled = process.env.R5D_WORKSPACE_MERGE_PROJECTION !== "0";
|
|
2134
2165
|
const mergeSupport = mergeProjectionEnabled ? workspaceMergeProjectionSupport() : { supported: false, error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0" };
|
|
@@ -2267,6 +2298,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2267
2298
|
}
|
|
2268
2299
|
};
|
|
2269
2300
|
const projectedFastMounts = [];
|
|
2301
|
+
const currentProjectionMounts = [];
|
|
2270
2302
|
if (!input.skipMountMirror) {
|
|
2271
2303
|
beginWorkspaceCheckoutTransition(workspacePath);
|
|
2272
2304
|
try {
|
|
@@ -2278,6 +2310,10 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2278
2310
|
cycleSkippedMountIds.add(mount.id);
|
|
2279
2311
|
continue;
|
|
2280
2312
|
}
|
|
2313
|
+
if (mountProjectionIsCurrent(mount)) {
|
|
2314
|
+
currentProjectionMounts.push(mount);
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2281
2317
|
projectedGitlinks.push(...mirrorMountsToWorkspace(workspacePath, [mount]));
|
|
2282
2318
|
projectedFastMounts.push(mount);
|
|
2283
2319
|
await yieldToWorkerEventLoop();
|
|
@@ -2311,7 +2347,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2311
2347
|
const targetProjectionReceipt = receiptWithMountsAtHead(
|
|
2312
2348
|
projectionBasisReceipt,
|
|
2313
2349
|
projectedHead,
|
|
2314
|
-
projectedFastMounts,
|
|
2350
|
+
[...projectedFastMounts, ...currentProjectionMounts],
|
|
2315
2351
|
allProjectionReceiptMounts
|
|
2316
2352
|
);
|
|
2317
2353
|
const currentProjectionReceipt = await readHydratedWorkspaceReceiptAsync(workspacePath);
|
|
@@ -2324,7 +2360,9 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2324
2360
|
}
|
|
2325
2361
|
await completeWorkspaceCheckoutTransition(workspacePath);
|
|
2326
2362
|
}
|
|
2327
|
-
|
|
2363
|
+
const prePublishRemoteHead = await resolveWorkspaceRemoteHead(workspacePath, input);
|
|
2364
|
+
if (prePublishRemoteHead.fetched) remoteHeadFetches.count += 1;
|
|
2365
|
+
let remoteHead = prePublishRemoteHead.head;
|
|
2328
2366
|
let rebaseCount = 0;
|
|
2329
2367
|
let updated = false;
|
|
2330
2368
|
for (let pushAttempt = 0; pushAttempt < maxPushAttempts; pushAttempt += 1) {
|
|
@@ -2481,6 +2519,7 @@ ${push.stdout}`)) {
|
|
|
2481
2519
|
throw new Error(`Push workspace main: ${push.stderr || push.stdout || `git exited ${push.exitCode}`}`);
|
|
2482
2520
|
}
|
|
2483
2521
|
remoteHead = await fetchWorkspaceHead(workspacePath, input.remoteUrl, input.credentialHelper, input.credentialUsername);
|
|
2522
|
+
remoteHeadFetches.count += 1;
|
|
2484
2523
|
}
|
|
2485
2524
|
throw new Error(`Workspace push did not converge after ${maxPushAttempts} attempts`);
|
|
2486
2525
|
}
|