@ricsam/r5d-worker 0.0.100 → 0.0.103
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 +390 -322
- 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 +49 -32
- package/dist/cjs/working-tree-mirror.cjs +347 -99
- package/dist/cjs/workspace-git-sync.cjs +189 -28
- package/dist/cjs/workspace-merge-projection.cjs +5 -19
- 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 +395 -324
- 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 +52 -33
- package/dist/mjs/working-tree-mirror.mjs +347 -99
- package/dist/mjs/workspace-git-sync.mjs +189 -28
- package/dist/mjs/workspace-merge-projection.mjs +5 -19
- 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 +29 -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 +26 -8
- package/dist/types/working-tree-mirror.d.ts +56 -11
- package/dist/types/workspace-git-sync.d.ts +41 -4
- 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
|
@@ -44,6 +44,16 @@ var import_node_crypto = require("node:crypto");
|
|
|
44
44
|
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
45
45
|
const HFS_IGNORED_CODE_POINTS = /[\u200c-\u200f\u202a-\u202e\u206a-\u206f\ufeff]/gu;
|
|
46
46
|
const MIRROR_TEMPORARY_FILE_PREFIX = ".r5d-working-tree-mirror-";
|
|
47
|
+
const OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
|
|
48
|
+
const GITLINK_STALE_HEADS_FILE = "gitlink-heads.json";
|
|
49
|
+
const NON_RECURSIVE_GIT_CONFIG = [
|
|
50
|
+
"-c",
|
|
51
|
+
"submodule.recurse=false",
|
|
52
|
+
"-c",
|
|
53
|
+
"fetch.recurseSubmodules=false",
|
|
54
|
+
"-c",
|
|
55
|
+
"push.recurseSubmodules=false"
|
|
56
|
+
];
|
|
47
57
|
function isPortableGitMetadataSegment(segment) {
|
|
48
58
|
const hfsFolded = segment.replace(HFS_IGNORED_CODE_POINTS, "").toLowerCase();
|
|
49
59
|
if (hfsFolded === ".git") return true;
|
|
@@ -119,42 +129,145 @@ function inspectDirectoryRoot(root, options) {
|
|
|
119
129
|
}
|
|
120
130
|
return true;
|
|
121
131
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
132
|
+
function lstatIfExists(targetPath) {
|
|
133
|
+
try {
|
|
134
|
+
return import_node_fs.default.lstatSync(targetPath);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error.code === "ENOENT") return null;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function nulRecords(content) {
|
|
141
|
+
const records = [];
|
|
142
|
+
let start = 0;
|
|
143
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
144
|
+
if (content[index] !== 0) continue;
|
|
145
|
+
if (index > start) records.push(content.subarray(start, index));
|
|
146
|
+
start = index + 1;
|
|
147
|
+
}
|
|
148
|
+
if (start < content.length) records.push(content.subarray(start));
|
|
149
|
+
return records;
|
|
150
|
+
}
|
|
151
|
+
function gitOutput(cwd, args, action, options = {}) {
|
|
152
|
+
const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, ...args], {
|
|
153
|
+
cwd,
|
|
154
|
+
...options.stdin ? { stdin: options.stdin } : {},
|
|
155
|
+
stdout: "pipe",
|
|
156
|
+
stderr: "pipe",
|
|
157
|
+
env: options.environment ?? (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
158
|
+
});
|
|
147
159
|
if (result.exitCode !== 0) {
|
|
148
|
-
throw new Error(
|
|
160
|
+
throw new Error(`${action}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
149
161
|
}
|
|
150
|
-
return result.stdout
|
|
151
|
-
const normalized = normalizeRelativePath(entry);
|
|
152
|
-
return normalized ? [normalized] : [];
|
|
153
|
-
}).sort();
|
|
162
|
+
return Buffer.from(result.stdout);
|
|
154
163
|
}
|
|
155
|
-
function
|
|
164
|
+
function listGitCheckout(root) {
|
|
165
|
+
const output = gitOutput(
|
|
166
|
+
root,
|
|
167
|
+
["ls-files", "-z", "-t", "--stage", "--cached", "--others", "--exclude-standard", "--", "."],
|
|
168
|
+
"Inspect Git working tree"
|
|
169
|
+
);
|
|
170
|
+
const roots = /* @__PURE__ */ new Map();
|
|
171
|
+
const indexRecords = [];
|
|
172
|
+
for (const record of nulRecords(output)) {
|
|
173
|
+
const text = record.toString();
|
|
174
|
+
const other = /^\? (.*)$/su.exec(text);
|
|
175
|
+
if (other) {
|
|
176
|
+
const normalized2 = normalizeRelativePath(other[1]);
|
|
177
|
+
if (normalized2 && !roots.has(normalized2)) roots.set(normalized2, other[1].endsWith("/"));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const staged = /^[A-Z] ([0-7]{6}) ([0-9a-f]{40,64}) ([0-3])\t(.*)$/su.exec(text);
|
|
181
|
+
if (!staged) continue;
|
|
182
|
+
const normalized = normalizeRelativePath(staged[4]);
|
|
183
|
+
if (!normalized) continue;
|
|
184
|
+
indexRecords.push({ mode: staged[1], objectId: staged[2], stage: Number(staged[3]), relativePath: normalized });
|
|
185
|
+
if (!roots.has(normalized)) roots.set(normalized, false);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
roots: [...roots].map(([relativePath, nestedRepository]) => ({ relativePath, nestedRepository })).sort((left, right) => left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0),
|
|
189
|
+
indexRecords
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function populatedCheckoutHead(root, absolutePath) {
|
|
193
|
+
if (!lstatIfExists(import_node_path.default.join(absolutePath, ".git"))) return null;
|
|
194
|
+
const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, "rev-parse", "--verify", "HEAD^{commit}"], {
|
|
195
|
+
cwd: absolutePath,
|
|
196
|
+
stdout: "pipe",
|
|
197
|
+
stderr: "pipe",
|
|
198
|
+
env: { ...(0, import_git_process_environment.workerGitProcessEnvironment)(), GIT_CEILING_DIRECTORIES: root }
|
|
199
|
+
});
|
|
200
|
+
if (result.exitCode !== 0) return null;
|
|
201
|
+
const head = result.stdout.toString().trim();
|
|
202
|
+
return OBJECT_ID_PATTERN.test(head) ? head : null;
|
|
203
|
+
}
|
|
204
|
+
function checkoutGitDirectory(root) {
|
|
205
|
+
const dotGit = import_node_path.default.join(root, ".git");
|
|
206
|
+
const status = lstatIfExists(dotGit);
|
|
207
|
+
if (!status) return null;
|
|
208
|
+
if (status.isDirectory()) return dotGit;
|
|
209
|
+
if (!status.isFile()) return null;
|
|
210
|
+
const match = /^gitdir:[ \t]*(.+?)\s*$/u.exec(import_node_fs.default.readFileSync(dotGit, "utf8").split(/\r?\n/u)[0] ?? "");
|
|
211
|
+
return match ? import_node_path.default.resolve(root, match[1]) : null;
|
|
212
|
+
}
|
|
213
|
+
function gitlinkStaleHeadsPath(root) {
|
|
214
|
+
const gitDirectory = checkoutGitDirectory(root);
|
|
215
|
+
return gitDirectory ? import_node_path.default.join(gitDirectory, "r5d", GITLINK_STALE_HEADS_FILE) : null;
|
|
216
|
+
}
|
|
217
|
+
function readGitlinkStaleHeads(root) {
|
|
218
|
+
const heads = /* @__PURE__ */ new Map();
|
|
219
|
+
const markerPath = gitlinkStaleHeadsPath(root);
|
|
220
|
+
if (!markerPath || !lstatIfExists(markerPath)?.isFile()) return heads;
|
|
221
|
+
let parsed;
|
|
222
|
+
try {
|
|
223
|
+
parsed = JSON.parse(import_node_fs.default.readFileSync(markerPath, "utf8"));
|
|
224
|
+
} catch {
|
|
225
|
+
return heads;
|
|
226
|
+
}
|
|
227
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) return heads;
|
|
228
|
+
const recorded = parsed.heads;
|
|
229
|
+
if (!recorded || typeof recorded !== "object") return heads;
|
|
230
|
+
for (const [relativePath, objectId] of Object.entries(recorded)) {
|
|
231
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
232
|
+
if (normalized && typeof objectId === "string" && OBJECT_ID_PATTERN.test(objectId)) heads.set(normalized, objectId);
|
|
233
|
+
}
|
|
234
|
+
return heads;
|
|
235
|
+
}
|
|
236
|
+
function writeGitlinkStaleHeads(root, heads) {
|
|
237
|
+
const markerPath = gitlinkStaleHeadsPath(root);
|
|
238
|
+
if (!markerPath) return;
|
|
239
|
+
const directory = import_node_path.default.dirname(markerPath);
|
|
240
|
+
if (heads.size === 0) {
|
|
241
|
+
if (!lstatIfExists(markerPath)) return;
|
|
242
|
+
import_node_fs.default.rmSync(markerPath, { force: true });
|
|
243
|
+
fsyncPath(directory, import_node_fs.default.constants.O_RDONLY);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const directoryStatus = lstatIfExists(directory);
|
|
247
|
+
if (!directoryStatus) import_node_fs.default.mkdirSync(directory, { mode: 448 });
|
|
248
|
+
else if (!directoryStatus.isDirectory() || directoryStatus.isSymbolicLink()) {
|
|
249
|
+
throw new Error(`Checkout private state is not a regular directory: ${directory}`);
|
|
250
|
+
}
|
|
251
|
+
const temporaryPath = import_node_path.default.join(directory, `.${GITLINK_STALE_HEADS_FILE}.${(0, import_node_crypto.randomUUID)()}.tmp`);
|
|
252
|
+
const descriptor = import_node_fs.default.openSync(temporaryPath, import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL, 384);
|
|
253
|
+
try {
|
|
254
|
+
import_node_fs.default.writeFileSync(descriptor, `${JSON.stringify({ version: 1, heads: Object.fromEntries([...heads].sort()) })}
|
|
255
|
+
`);
|
|
256
|
+
import_node_fs.default.fsyncSync(descriptor);
|
|
257
|
+
} finally {
|
|
258
|
+
import_node_fs.default.closeSync(descriptor);
|
|
259
|
+
}
|
|
260
|
+
import_node_fs.default.renameSync(temporaryPath, markerPath);
|
|
261
|
+
fsyncPath(directory, import_node_fs.default.constants.O_RDONLY);
|
|
262
|
+
}
|
|
263
|
+
function addEntry(entries, root, relativePath, recurseDirectories, gitlinks) {
|
|
156
264
|
const normalized = normalizeRelativePath(relativePath);
|
|
157
265
|
if (!normalized || entries.has(normalized)) return;
|
|
266
|
+
const gitlink = gitlinks?.get(normalized);
|
|
267
|
+
if (gitlink !== void 0) {
|
|
268
|
+
entries.set(normalized, { kind: "gitlink", objectId: gitlink });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
158
271
|
const inspected = inspectRelativePathWithoutFollowingAncestors(root, normalized);
|
|
159
272
|
if (inspected.kind !== "entry") return;
|
|
160
273
|
const { absolutePath, stat } = inspected;
|
|
@@ -173,7 +286,7 @@ function addEntry(entries, root, relativePath, recurseDirectories) {
|
|
|
173
286
|
entries.set(normalized, { kind: "directory", mode: stat.mode });
|
|
174
287
|
if (!recurseDirectories) return;
|
|
175
288
|
for (const child of import_node_fs.default.readdirSync(absolutePath).sort()) {
|
|
176
|
-
addEntry(entries, root, import_node_path.default.posix.join(normalized, child), true);
|
|
289
|
+
addEntry(entries, root, import_node_path.default.posix.join(normalized, child), true, gitlinks);
|
|
177
290
|
}
|
|
178
291
|
}
|
|
179
292
|
function addParentDirectories(entries, root) {
|
|
@@ -191,57 +304,59 @@ function addParentDirectories(entries, root) {
|
|
|
191
304
|
}
|
|
192
305
|
}
|
|
193
306
|
}
|
|
194
|
-
function
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
],
|
|
210
|
-
{
|
|
211
|
-
cwd: root,
|
|
212
|
-
stdout: "pipe",
|
|
213
|
-
stderr: "pipe",
|
|
214
|
-
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
307
|
+
function inspectGitWorkingTree(root) {
|
|
308
|
+
const entries = /* @__PURE__ */ new Map();
|
|
309
|
+
const listing = listGitCheckout(root);
|
|
310
|
+
const indexGitlinks = /* @__PURE__ */ new Map();
|
|
311
|
+
for (const record of listing.indexRecords) {
|
|
312
|
+
if (record.mode === "160000" && !indexGitlinks.has(record.relativePath)) indexGitlinks.set(record.relativePath, record.objectId);
|
|
313
|
+
}
|
|
314
|
+
const submodules = /* @__PURE__ */ new Map();
|
|
315
|
+
let staleHeads = null;
|
|
316
|
+
for (const { relativePath, nestedRepository } of listing.roots) {
|
|
317
|
+
if (entries.has(relativePath)) continue;
|
|
318
|
+
const indexObjectId = indexGitlinks.get(relativePath);
|
|
319
|
+
if (indexObjectId === void 0 && !nestedRepository) {
|
|
320
|
+
addEntry(entries, root, relativePath, true);
|
|
321
|
+
continue;
|
|
215
322
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
323
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(root, relativePath);
|
|
324
|
+
if (inspected.kind !== "entry") continue;
|
|
325
|
+
if (!inspected.stat.isDirectory() || inspected.stat.isSymbolicLink()) {
|
|
326
|
+
addEntry(entries, root, relativePath, true);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const head = populatedCheckoutHead(root, inspected.absolutePath);
|
|
330
|
+
submodules.set(relativePath, head);
|
|
331
|
+
let objectId = indexObjectId ?? head;
|
|
332
|
+
if (head && indexObjectId !== void 0 && head !== indexObjectId) {
|
|
333
|
+
staleHeads ??= readGitlinkStaleHeads(root);
|
|
334
|
+
objectId = staleHeads.get(relativePath) === head ? indexObjectId : head;
|
|
335
|
+
}
|
|
336
|
+
if (objectId) entries.set(relativePath, { kind: "gitlink", objectId });
|
|
226
337
|
}
|
|
227
|
-
|
|
338
|
+
addParentDirectories(entries, root);
|
|
339
|
+
return { entries, indexRecords: listing.indexRecords, submodules };
|
|
228
340
|
}
|
|
229
|
-
function
|
|
230
|
-
const
|
|
231
|
-
|
|
341
|
+
function normalizedGitlinks(gitlinks) {
|
|
342
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
343
|
+
for (const [relativePath, objectId] of gitlinks ?? []) {
|
|
344
|
+
const relative = normalizeRelativePath(relativePath);
|
|
345
|
+
if (!relative) throw new Error(`Invalid working-tree gitlink path: ${relativePath}`);
|
|
346
|
+
if (!OBJECT_ID_PATTERN.test(objectId)) throw new Error(`Invalid working-tree gitlink object id for ${relativePath}: ${objectId}`);
|
|
347
|
+
normalized.set(relative, objectId);
|
|
348
|
+
}
|
|
349
|
+
return normalized;
|
|
232
350
|
}
|
|
233
351
|
function inspectWorkingTree(root, mode, options = {}) {
|
|
234
352
|
const entries = /* @__PURE__ */ new Map();
|
|
235
353
|
root = import_node_path.default.resolve(root);
|
|
236
354
|
if (!inspectDirectoryRoot(root, { allowMissing: true, label: "Working-tree root" })) return entries;
|
|
237
|
-
if (mode === "
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (omittedGitlinks?.has(relativePath) && directoryIsEmpty(root, relativePath)) continue;
|
|
243
|
-
addEntry(entries, root, relativePath, true);
|
|
244
|
-
}
|
|
355
|
+
if (mode === "git") return inspectGitWorkingTree(root).entries;
|
|
356
|
+
const gitlinks = normalizedGitlinks(options.gitlinks);
|
|
357
|
+
for (const child of import_node_fs.default.readdirSync(root).sort()) addEntry(entries, root, child, true, gitlinks);
|
|
358
|
+
for (const [relativePath, objectId] of gitlinks) {
|
|
359
|
+
if (entries.get(relativePath)?.kind !== "gitlink") entries.set(relativePath, { kind: "gitlink", objectId });
|
|
245
360
|
}
|
|
246
361
|
addParentDirectories(entries, root);
|
|
247
362
|
return entries;
|
|
@@ -391,6 +506,9 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
|
|
|
391
506
|
}
|
|
392
507
|
return;
|
|
393
508
|
}
|
|
509
|
+
if (entry.kind === "gitlink") {
|
|
510
|
+
throw new Error(`Working-tree gitlink cannot be copied as content: ${relativePath}`);
|
|
511
|
+
}
|
|
394
512
|
if (!source.stat.isFile() || source.stat.isSymbolicLink() || source.stat.size !== entry.size) {
|
|
395
513
|
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
396
514
|
}
|
|
@@ -404,6 +522,110 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
|
|
|
404
522
|
copyRegularFile(sourcePath, targetPath, entry, durability);
|
|
405
523
|
}
|
|
406
524
|
}
|
|
525
|
+
function inspectMirrorTarget(targetRoot, deletionMode) {
|
|
526
|
+
if (deletionMode !== "git") return { existing: inspectWorkingTree(targetRoot, "all"), git: null };
|
|
527
|
+
const inspection = inspectGitWorkingTree(targetRoot);
|
|
528
|
+
const occupied = /* @__PURE__ */ new Set();
|
|
529
|
+
for (const relativePath of inspection.submodules.keys()) {
|
|
530
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
531
|
+
if (inspected.kind === "entry" && inspected.stat.isDirectory() && import_node_fs.default.readdirSync(inspected.absolutePath).length > 0) {
|
|
532
|
+
occupied.add(relativePath);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return { existing: inspection.entries, git: { indexRecords: inspection.indexRecords, submodules: inspection.submodules, occupied } };
|
|
536
|
+
}
|
|
537
|
+
function shieldedDesiredPaths(desired, git) {
|
|
538
|
+
const shielded = /* @__PURE__ */ new Set();
|
|
539
|
+
if (git.submodules.size === 0) return shielded;
|
|
540
|
+
const roots = new Set(git.submodules.keys());
|
|
541
|
+
for (const [relativePath, entry] of desired) {
|
|
542
|
+
if (hasAncestorIn(relativePath, roots)) {
|
|
543
|
+
shielded.add(relativePath);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (!roots.has(relativePath) || entry.kind === "gitlink") continue;
|
|
547
|
+
if (entry.kind === "directory" || git.occupied.has(relativePath)) shielded.add(relativePath);
|
|
548
|
+
}
|
|
549
|
+
return shielded;
|
|
550
|
+
}
|
|
551
|
+
function directoryShieldsSubmodule(relativePath, entry, git) {
|
|
552
|
+
if (!git || entry.kind !== "directory") return false;
|
|
553
|
+
const prefix = `${relativePath}/`;
|
|
554
|
+
for (const root of git.submodules.keys()) if (root.startsWith(prefix)) return true;
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
function planIndexChanges(desired, shielded, git) {
|
|
558
|
+
const removed = [];
|
|
559
|
+
const written = [];
|
|
560
|
+
const desiredGitlinks = /* @__PURE__ */ new Map();
|
|
561
|
+
for (const [relativePath, entry] of desired) {
|
|
562
|
+
if (entry.kind === "gitlink" && !shielded.has(relativePath)) desiredGitlinks.set(relativePath, entry.objectId);
|
|
563
|
+
}
|
|
564
|
+
const desiredGitlinkRoots = new Set(desiredGitlinks.keys());
|
|
565
|
+
const currentGitlinks = /* @__PURE__ */ new Map();
|
|
566
|
+
for (const record of git.indexRecords) {
|
|
567
|
+
const desiredObjectId = desiredGitlinks.get(record.relativePath);
|
|
568
|
+
if (record.mode === "160000") {
|
|
569
|
+
if (record.stage === 0) currentGitlinks.set(record.relativePath, record.objectId);
|
|
570
|
+
if (desiredObjectId !== void 0) {
|
|
571
|
+
if (desiredObjectId !== record.objectId) removed.push(record);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const desiredEntry = desired.get(record.relativePath);
|
|
575
|
+
if (desiredEntry?.kind !== "directory" && desiredEntry?.kind !== "gitlink" && !git.occupied.has(record.relativePath)) {
|
|
576
|
+
removed.push(record);
|
|
577
|
+
}
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (desiredObjectId !== void 0 || hasAncestorIn(record.relativePath, desiredGitlinkRoots)) removed.push(record);
|
|
581
|
+
}
|
|
582
|
+
for (const [relativePath, objectId] of desiredGitlinks) {
|
|
583
|
+
if (currentGitlinks.get(relativePath) !== objectId) written.push({ relativePath, objectId });
|
|
584
|
+
}
|
|
585
|
+
return { removed, written };
|
|
586
|
+
}
|
|
587
|
+
function indexInfoRecord(record) {
|
|
588
|
+
const stage = record.stage === 0 ? "" : ` ${record.stage}`;
|
|
589
|
+
return Buffer.concat([Buffer.from(`${record.mode} ${record.objectId}${stage} `), Buffer.from(record.relativePath), Buffer.from([0])]);
|
|
590
|
+
}
|
|
591
|
+
function updateIndex(targetRoot, changes) {
|
|
592
|
+
if (changes.drop.length === 0 && changes.put.length === 0) return;
|
|
593
|
+
const feed = Buffer.concat([
|
|
594
|
+
...changes.drop.map(
|
|
595
|
+
({ relativePath, objectId }) => indexInfoRecord({ mode: "0", objectId: "0".repeat(objectId.length), stage: 0, relativePath })
|
|
596
|
+
),
|
|
597
|
+
...changes.put.map(indexInfoRecord)
|
|
598
|
+
]);
|
|
599
|
+
gitOutput(
|
|
600
|
+
targetRoot,
|
|
601
|
+
["-c", "core.fsync=index", "-c", "core.fsyncMethod=fsync", "update-index", "-z", "--index-info"],
|
|
602
|
+
"Update Git working-tree index",
|
|
603
|
+
{ stdin: feed }
|
|
604
|
+
);
|
|
605
|
+
const gitDirectory = checkoutGitDirectory(targetRoot);
|
|
606
|
+
if (!gitDirectory) return;
|
|
607
|
+
const indexPath = import_node_path.default.join(gitDirectory, "index");
|
|
608
|
+
if (lstatIfExists(indexPath)?.isFile()) fsyncPath(indexPath, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
|
|
609
|
+
fsyncPath(gitDirectory, import_node_fs.default.constants.O_RDONLY);
|
|
610
|
+
}
|
|
611
|
+
function recordGitlinkStaleHeads(targetRoot, desired, shielded, git) {
|
|
612
|
+
const heads = readGitlinkStaleHeads(targetRoot);
|
|
613
|
+
let changed = false;
|
|
614
|
+
for (const [relativePath, head] of git.submodules) {
|
|
615
|
+
const entry = desired.get(relativePath);
|
|
616
|
+
const next = entry?.kind === "gitlink" && !shielded.has(relativePath) && head && head !== entry.objectId ? head : null;
|
|
617
|
+
if ((heads.get(relativePath) ?? null) === next) continue;
|
|
618
|
+
if (next) heads.set(relativePath, next);
|
|
619
|
+
else heads.delete(relativePath);
|
|
620
|
+
changed = true;
|
|
621
|
+
}
|
|
622
|
+
for (const relativePath of [...heads.keys()]) {
|
|
623
|
+
if (git.submodules.has(relativePath)) continue;
|
|
624
|
+
heads.delete(relativePath);
|
|
625
|
+
changed = true;
|
|
626
|
+
}
|
|
627
|
+
if (changed) writeGitlinkStaleHeads(targetRoot, heads);
|
|
628
|
+
}
|
|
407
629
|
function mirrorWorkingTree(input) {
|
|
408
630
|
const sourceRoot = import_node_path.default.resolve(input.sourceRoot);
|
|
409
631
|
const targetRoot = import_node_path.default.resolve(input.targetRoot);
|
|
@@ -412,19 +634,34 @@ function mirrorWorkingTree(input) {
|
|
|
412
634
|
import_node_fs.default.mkdirSync(targetRoot, { recursive: true });
|
|
413
635
|
}
|
|
414
636
|
inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
|
|
415
|
-
const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
for (const relativePath of [...existing.keys()].sort(
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
(
|
|
424
|
-
|
|
637
|
+
const desired = inspectWorkingTree(sourceRoot, input.sourceMode, { gitlinks: input.sourceGitlinks });
|
|
638
|
+
const target = inspectMirrorTarget(targetRoot, input.deletionMode);
|
|
639
|
+
const shielded = target.git ? shieldedDesiredPaths(desired, target.git) : /* @__PURE__ */ new Set();
|
|
640
|
+
const index = target.git ? planIndexChanges(desired, shielded, target.git) : null;
|
|
641
|
+
for (const relativePath of [...target.existing.keys()].sort(deepestFirst)) {
|
|
642
|
+
if (desired.has(relativePath)) continue;
|
|
643
|
+
const entry = target.existing.get(relativePath);
|
|
644
|
+
if (entry.kind === "gitlink" || directoryShieldsSubmodule(relativePath, entry, target.git)) continue;
|
|
645
|
+
removeEntry(targetRoot, relativePath);
|
|
646
|
+
}
|
|
647
|
+
const gitlinks = [];
|
|
648
|
+
for (const [relativePath, entry] of [...desired.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
649
|
+
if (shielded.has(relativePath)) continue;
|
|
650
|
+
if (entry.kind === "gitlink") {
|
|
651
|
+
ensureDirectory(targetRoot, relativePath, 493);
|
|
652
|
+
gitlinks.push({ relativePath, objectId: entry.objectId });
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
425
655
|
copyEntry(sourceRoot, targetRoot, relativePath, entry, durability);
|
|
426
656
|
}
|
|
427
|
-
|
|
657
|
+
if (index && target.git) {
|
|
658
|
+
updateIndex(targetRoot, { drop: index.removed, put: index.written.map((gitlink) => ({ mode: "160000", stage: 0, ...gitlink })) });
|
|
659
|
+
recordGitlinkStaleHeads(targetRoot, desired, shielded, target.git);
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
paths: [...desired.keys()].filter((relativePath) => !shielded.has(relativePath) && desired.get(relativePath)?.kind !== "directory").sort(),
|
|
663
|
+
gitlinks
|
|
664
|
+
};
|
|
428
665
|
}
|
|
429
666
|
function normalizeWorkingTreeRelativePath(value) {
|
|
430
667
|
return normalizeRelativePath(value);
|
|
@@ -464,23 +701,25 @@ function fsyncPath(targetPath, flags) {
|
|
|
464
701
|
function planWorkingTreeMirror(input) {
|
|
465
702
|
const sourceRoot = import_node_path.default.resolve(input.sourceRoot);
|
|
466
703
|
const targetRoot = import_node_path.default.resolve(input.targetRoot);
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
704
|
+
const inspectedDesired = inspectWorkingTree(sourceRoot, input.sourceMode, { gitlinks: input.sourceGitlinks });
|
|
705
|
+
const target = inspectMirrorTarget(targetRoot, input.deletionMode);
|
|
706
|
+
const shielded = target.git ? shieldedDesiredPaths(inspectedDesired, target.git) : /* @__PURE__ */ new Set();
|
|
707
|
+
const desired = new Map([...inspectedDesired].filter(([relativePath]) => !shielded.has(relativePath)));
|
|
471
708
|
const replaced = /* @__PURE__ */ new Map();
|
|
472
709
|
const absent = [];
|
|
473
|
-
for (const [relativePath, entry] of existing) {
|
|
474
|
-
if (
|
|
710
|
+
for (const [relativePath, entry] of target.existing) {
|
|
711
|
+
if (inspectedDesired.has(relativePath) || entry.kind === "gitlink") continue;
|
|
712
|
+
if (directoryShieldsSubmodule(relativePath, entry, target.git)) continue;
|
|
713
|
+
replaced.set(relativePath, entry);
|
|
475
714
|
}
|
|
476
715
|
for (const [relativePath, entry] of desired) {
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
716
|
+
const targetEntry = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
717
|
+
if (targetEntry.kind !== "entry") {
|
|
479
718
|
absent.push(relativePath);
|
|
480
719
|
continue;
|
|
481
720
|
}
|
|
482
|
-
const { absolutePath, stat } =
|
|
483
|
-
if (entry.kind === "directory") {
|
|
721
|
+
const { absolutePath, stat } = targetEntry;
|
|
722
|
+
if (entry.kind === "directory" || entry.kind === "gitlink") {
|
|
484
723
|
if (stat.isDirectory()) continue;
|
|
485
724
|
} else if (entry.kind === "symlink") {
|
|
486
725
|
if (stat.isSymbolicLink() && import_node_fs.default.readlinkSync(absolutePath) === entry.target) continue;
|
|
@@ -500,7 +739,8 @@ function planWorkingTreeMirror(input) {
|
|
|
500
739
|
return {
|
|
501
740
|
desired,
|
|
502
741
|
replaced,
|
|
503
|
-
absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst)
|
|
742
|
+
absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst),
|
|
743
|
+
index: target.git ? planIndexChanges(inspectedDesired, shielded, target.git) : { removed: [], written: [] }
|
|
504
744
|
};
|
|
505
745
|
}
|
|
506
746
|
function captureWorkingTreeMirrorPlan(input) {
|
|
@@ -513,6 +753,7 @@ function captureWorkingTreeMirrorPlan(input) {
|
|
|
513
753
|
}
|
|
514
754
|
const trees = [];
|
|
515
755
|
for (const [relativePath, entry] of [...input.plan.replaced.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
756
|
+
if (entry.kind === "gitlink") continue;
|
|
516
757
|
if (entry.kind !== "directory") {
|
|
517
758
|
copyEntry(targetRoot, snapshotRoot, relativePath, entry, durability);
|
|
518
759
|
continue;
|
|
@@ -531,7 +772,12 @@ function captureWorkingTreeMirrorPlan(input) {
|
|
|
531
772
|
});
|
|
532
773
|
trees.push(relativePath);
|
|
533
774
|
}
|
|
534
|
-
|
|
775
|
+
const { index } = input.plan;
|
|
776
|
+
return {
|
|
777
|
+
absent: [...input.plan.absent].sort(shallowestFirst),
|
|
778
|
+
trees: trees.sort(shallowestFirst),
|
|
779
|
+
...index.removed.length > 0 || index.written.length > 0 ? { index } : {}
|
|
780
|
+
};
|
|
535
781
|
}
|
|
536
782
|
function restoreWorkingTreeMirrorScope(input) {
|
|
537
783
|
const snapshotRoot = import_node_path.default.resolve(input.snapshotRoot);
|
|
@@ -556,6 +802,7 @@ function restoreWorkingTreeMirrorScope(input) {
|
|
|
556
802
|
const touched = /* @__PURE__ */ new Set();
|
|
557
803
|
for (const relativePath of [...absent].sort(deepestFirst)) {
|
|
558
804
|
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
805
|
+
if (inspected.kind === "entry" && inspected.stat.isDirectory() && lstatIfExists(import_node_path.default.join(inspected.absolutePath, ".git"))) continue;
|
|
559
806
|
if (inspected.kind === "entry") import_node_fs.default.rmSync(inspected.absolutePath, { recursive: true, force: true });
|
|
560
807
|
touched.add(relativePath);
|
|
561
808
|
}
|
|
@@ -593,7 +840,7 @@ function restoreWorkingTreeMirrorScope(input) {
|
|
|
593
840
|
if (current.kind === "entry" && !current.stat.isDirectory()) {
|
|
594
841
|
import_node_fs.default.rmSync(current.absolutePath, { recursive: true, force: true });
|
|
595
842
|
}
|
|
596
|
-
ensureDirectory(targetRoot, relativePath, entry.mode);
|
|
843
|
+
ensureDirectory(targetRoot, relativePath, entry.kind === "directory" ? entry.mode : 493);
|
|
597
844
|
mirrorWorkingTree({
|
|
598
845
|
sourceRoot: import_node_path.default.join(snapshotRoot, ...relativePath.split("/")),
|
|
599
846
|
targetRoot: import_node_path.default.join(targetRoot, ...relativePath.split("/")),
|
|
@@ -601,6 +848,7 @@ function restoreWorkingTreeMirrorScope(input) {
|
|
|
601
848
|
deletionMode: "all"
|
|
602
849
|
});
|
|
603
850
|
}
|
|
851
|
+
if (input.scope.index) updateIndex(targetRoot, { drop: input.scope.index.written, put: input.scope.index.removed });
|
|
604
852
|
return { paths: [...touched].sort(shallowestFirst) };
|
|
605
853
|
}
|
|
606
854
|
function fsyncWorkingTreePaths(root, relativePaths) {
|