@ricsam/r5d-worker 0.0.99 → 0.0.101
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/main.cjs +37 -5
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/working-tree-mirror.cjs +347 -99
- package/dist/cjs/workspace-git-sync.cjs +198 -60
- package/dist/cjs/workspace-merge-projection.cjs +5 -19
- package/dist/mjs/main.mjs +37 -5
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/working-tree-mirror.mjs +347 -99
- package/dist/mjs/workspace-git-sync.mjs +198 -60
- package/dist/mjs/workspace-merge-projection.mjs +5 -19
- package/dist/types/main.d.ts +181 -0
- package/dist/types/working-tree-mirror.d.ts +56 -11
- package/dist/types/workspace-git-sync.d.ts +42 -2
- package/package.json +1 -1
|
@@ -4,6 +4,16 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
5
5
|
const HFS_IGNORED_CODE_POINTS = /[\u200c-\u200f\u202a-\u202e\u206a-\u206f\ufeff]/gu;
|
|
6
6
|
const MIRROR_TEMPORARY_FILE_PREFIX = ".r5d-working-tree-mirror-";
|
|
7
|
+
const OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u;
|
|
8
|
+
const GITLINK_STALE_HEADS_FILE = "gitlink-heads.json";
|
|
9
|
+
const NON_RECURSIVE_GIT_CONFIG = [
|
|
10
|
+
"-c",
|
|
11
|
+
"submodule.recurse=false",
|
|
12
|
+
"-c",
|
|
13
|
+
"fetch.recurseSubmodules=false",
|
|
14
|
+
"-c",
|
|
15
|
+
"push.recurseSubmodules=false"
|
|
16
|
+
];
|
|
7
17
|
function isPortableGitMetadataSegment(segment) {
|
|
8
18
|
const hfsFolded = segment.replace(HFS_IGNORED_CODE_POINTS, "").toLowerCase();
|
|
9
19
|
if (hfsFolded === ".git") return true;
|
|
@@ -79,42 +89,145 @@ function inspectDirectoryRoot(root, options) {
|
|
|
79
89
|
}
|
|
80
90
|
return true;
|
|
81
91
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
92
|
+
function lstatIfExists(targetPath) {
|
|
93
|
+
try {
|
|
94
|
+
return fs.lstatSync(targetPath);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error.code === "ENOENT") return null;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function nulRecords(content) {
|
|
101
|
+
const records = [];
|
|
102
|
+
let start = 0;
|
|
103
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
104
|
+
if (content[index] !== 0) continue;
|
|
105
|
+
if (index > start) records.push(content.subarray(start, index));
|
|
106
|
+
start = index + 1;
|
|
107
|
+
}
|
|
108
|
+
if (start < content.length) records.push(content.subarray(start));
|
|
109
|
+
return records;
|
|
110
|
+
}
|
|
111
|
+
function gitOutput(cwd, args, action, options = {}) {
|
|
112
|
+
const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, ...args], {
|
|
113
|
+
cwd,
|
|
114
|
+
...options.stdin ? { stdin: options.stdin } : {},
|
|
115
|
+
stdout: "pipe",
|
|
116
|
+
stderr: "pipe",
|
|
117
|
+
env: options.environment ?? workerGitProcessEnvironment()
|
|
118
|
+
});
|
|
107
119
|
if (result.exitCode !== 0) {
|
|
108
|
-
throw new Error(
|
|
120
|
+
throw new Error(`${action}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
|
|
109
121
|
}
|
|
110
|
-
return result.stdout
|
|
111
|
-
const normalized = normalizeRelativePath(entry);
|
|
112
|
-
return normalized ? [normalized] : [];
|
|
113
|
-
}).sort();
|
|
122
|
+
return Buffer.from(result.stdout);
|
|
114
123
|
}
|
|
115
|
-
function
|
|
124
|
+
function listGitCheckout(root) {
|
|
125
|
+
const output = gitOutput(
|
|
126
|
+
root,
|
|
127
|
+
["ls-files", "-z", "-t", "--stage", "--cached", "--others", "--exclude-standard", "--", "."],
|
|
128
|
+
"Inspect Git working tree"
|
|
129
|
+
);
|
|
130
|
+
const roots = /* @__PURE__ */ new Map();
|
|
131
|
+
const indexRecords = [];
|
|
132
|
+
for (const record of nulRecords(output)) {
|
|
133
|
+
const text = record.toString();
|
|
134
|
+
const other = /^\? (.*)$/su.exec(text);
|
|
135
|
+
if (other) {
|
|
136
|
+
const normalized2 = normalizeRelativePath(other[1]);
|
|
137
|
+
if (normalized2 && !roots.has(normalized2)) roots.set(normalized2, other[1].endsWith("/"));
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const staged = /^[A-Z] ([0-7]{6}) ([0-9a-f]{40,64}) ([0-3])\t(.*)$/su.exec(text);
|
|
141
|
+
if (!staged) continue;
|
|
142
|
+
const normalized = normalizeRelativePath(staged[4]);
|
|
143
|
+
if (!normalized) continue;
|
|
144
|
+
indexRecords.push({ mode: staged[1], objectId: staged[2], stage: Number(staged[3]), relativePath: normalized });
|
|
145
|
+
if (!roots.has(normalized)) roots.set(normalized, false);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
roots: [...roots].map(([relativePath, nestedRepository]) => ({ relativePath, nestedRepository })).sort((left, right) => left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0),
|
|
149
|
+
indexRecords
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function populatedCheckoutHead(root, absolutePath) {
|
|
153
|
+
if (!lstatIfExists(path.join(absolutePath, ".git"))) return null;
|
|
154
|
+
const result = Bun.spawnSync(["git", ...NON_RECURSIVE_GIT_CONFIG, "rev-parse", "--verify", "HEAD^{commit}"], {
|
|
155
|
+
cwd: absolutePath,
|
|
156
|
+
stdout: "pipe",
|
|
157
|
+
stderr: "pipe",
|
|
158
|
+
env: { ...workerGitProcessEnvironment(), GIT_CEILING_DIRECTORIES: root }
|
|
159
|
+
});
|
|
160
|
+
if (result.exitCode !== 0) return null;
|
|
161
|
+
const head = result.stdout.toString().trim();
|
|
162
|
+
return OBJECT_ID_PATTERN.test(head) ? head : null;
|
|
163
|
+
}
|
|
164
|
+
function checkoutGitDirectory(root) {
|
|
165
|
+
const dotGit = path.join(root, ".git");
|
|
166
|
+
const status = lstatIfExists(dotGit);
|
|
167
|
+
if (!status) return null;
|
|
168
|
+
if (status.isDirectory()) return dotGit;
|
|
169
|
+
if (!status.isFile()) return null;
|
|
170
|
+
const match = /^gitdir:[ \t]*(.+?)\s*$/u.exec(fs.readFileSync(dotGit, "utf8").split(/\r?\n/u)[0] ?? "");
|
|
171
|
+
return match ? path.resolve(root, match[1]) : null;
|
|
172
|
+
}
|
|
173
|
+
function gitlinkStaleHeadsPath(root) {
|
|
174
|
+
const gitDirectory = checkoutGitDirectory(root);
|
|
175
|
+
return gitDirectory ? path.join(gitDirectory, "r5d", GITLINK_STALE_HEADS_FILE) : null;
|
|
176
|
+
}
|
|
177
|
+
function readGitlinkStaleHeads(root) {
|
|
178
|
+
const heads = /* @__PURE__ */ new Map();
|
|
179
|
+
const markerPath = gitlinkStaleHeadsPath(root);
|
|
180
|
+
if (!markerPath || !lstatIfExists(markerPath)?.isFile()) return heads;
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = JSON.parse(fs.readFileSync(markerPath, "utf8"));
|
|
184
|
+
} catch {
|
|
185
|
+
return heads;
|
|
186
|
+
}
|
|
187
|
+
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) return heads;
|
|
188
|
+
const recorded = parsed.heads;
|
|
189
|
+
if (!recorded || typeof recorded !== "object") return heads;
|
|
190
|
+
for (const [relativePath, objectId] of Object.entries(recorded)) {
|
|
191
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
192
|
+
if (normalized && typeof objectId === "string" && OBJECT_ID_PATTERN.test(objectId)) heads.set(normalized, objectId);
|
|
193
|
+
}
|
|
194
|
+
return heads;
|
|
195
|
+
}
|
|
196
|
+
function writeGitlinkStaleHeads(root, heads) {
|
|
197
|
+
const markerPath = gitlinkStaleHeadsPath(root);
|
|
198
|
+
if (!markerPath) return;
|
|
199
|
+
const directory = path.dirname(markerPath);
|
|
200
|
+
if (heads.size === 0) {
|
|
201
|
+
if (!lstatIfExists(markerPath)) return;
|
|
202
|
+
fs.rmSync(markerPath, { force: true });
|
|
203
|
+
fsyncPath(directory, fs.constants.O_RDONLY);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const directoryStatus = lstatIfExists(directory);
|
|
207
|
+
if (!directoryStatus) fs.mkdirSync(directory, { mode: 448 });
|
|
208
|
+
else if (!directoryStatus.isDirectory() || directoryStatus.isSymbolicLink()) {
|
|
209
|
+
throw new Error(`Checkout private state is not a regular directory: ${directory}`);
|
|
210
|
+
}
|
|
211
|
+
const temporaryPath = path.join(directory, `.${GITLINK_STALE_HEADS_FILE}.${randomUUID()}.tmp`);
|
|
212
|
+
const descriptor = fs.openSync(temporaryPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
|
|
213
|
+
try {
|
|
214
|
+
fs.writeFileSync(descriptor, `${JSON.stringify({ version: 1, heads: Object.fromEntries([...heads].sort()) })}
|
|
215
|
+
`);
|
|
216
|
+
fs.fsyncSync(descriptor);
|
|
217
|
+
} finally {
|
|
218
|
+
fs.closeSync(descriptor);
|
|
219
|
+
}
|
|
220
|
+
fs.renameSync(temporaryPath, markerPath);
|
|
221
|
+
fsyncPath(directory, fs.constants.O_RDONLY);
|
|
222
|
+
}
|
|
223
|
+
function addEntry(entries, root, relativePath, recurseDirectories, gitlinks) {
|
|
116
224
|
const normalized = normalizeRelativePath(relativePath);
|
|
117
225
|
if (!normalized || entries.has(normalized)) return;
|
|
226
|
+
const gitlink = gitlinks?.get(normalized);
|
|
227
|
+
if (gitlink !== void 0) {
|
|
228
|
+
entries.set(normalized, { kind: "gitlink", objectId: gitlink });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
118
231
|
const inspected = inspectRelativePathWithoutFollowingAncestors(root, normalized);
|
|
119
232
|
if (inspected.kind !== "entry") return;
|
|
120
233
|
const { absolutePath, stat } = inspected;
|
|
@@ -133,7 +246,7 @@ function addEntry(entries, root, relativePath, recurseDirectories) {
|
|
|
133
246
|
entries.set(normalized, { kind: "directory", mode: stat.mode });
|
|
134
247
|
if (!recurseDirectories) return;
|
|
135
248
|
for (const child of fs.readdirSync(absolutePath).sort()) {
|
|
136
|
-
addEntry(entries, root, path.posix.join(normalized, child), true);
|
|
249
|
+
addEntry(entries, root, path.posix.join(normalized, child), true, gitlinks);
|
|
137
250
|
}
|
|
138
251
|
}
|
|
139
252
|
function addParentDirectories(entries, root) {
|
|
@@ -151,57 +264,59 @@ function addParentDirectories(entries, root) {
|
|
|
151
264
|
}
|
|
152
265
|
}
|
|
153
266
|
}
|
|
154
|
-
function
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
],
|
|
170
|
-
{
|
|
171
|
-
cwd: root,
|
|
172
|
-
stdout: "pipe",
|
|
173
|
-
stderr: "pipe",
|
|
174
|
-
env: workerGitProcessEnvironment()
|
|
267
|
+
function inspectGitWorkingTree(root) {
|
|
268
|
+
const entries = /* @__PURE__ */ new Map();
|
|
269
|
+
const listing = listGitCheckout(root);
|
|
270
|
+
const indexGitlinks = /* @__PURE__ */ new Map();
|
|
271
|
+
for (const record of listing.indexRecords) {
|
|
272
|
+
if (record.mode === "160000" && !indexGitlinks.has(record.relativePath)) indexGitlinks.set(record.relativePath, record.objectId);
|
|
273
|
+
}
|
|
274
|
+
const submodules = /* @__PURE__ */ new Map();
|
|
275
|
+
let staleHeads = null;
|
|
276
|
+
for (const { relativePath, nestedRepository } of listing.roots) {
|
|
277
|
+
if (entries.has(relativePath)) continue;
|
|
278
|
+
const indexObjectId = indexGitlinks.get(relativePath);
|
|
279
|
+
if (indexObjectId === void 0 && !nestedRepository) {
|
|
280
|
+
addEntry(entries, root, relativePath, true);
|
|
281
|
+
continue;
|
|
175
282
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (
|
|
283
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(root, relativePath);
|
|
284
|
+
if (inspected.kind !== "entry") continue;
|
|
285
|
+
if (!inspected.stat.isDirectory() || inspected.stat.isSymbolicLink()) {
|
|
286
|
+
addEntry(entries, root, relativePath, true);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const head = populatedCheckoutHead(root, inspected.absolutePath);
|
|
290
|
+
submodules.set(relativePath, head);
|
|
291
|
+
let objectId = indexObjectId ?? head;
|
|
292
|
+
if (head && indexObjectId !== void 0 && head !== indexObjectId) {
|
|
293
|
+
staleHeads ??= readGitlinkStaleHeads(root);
|
|
294
|
+
objectId = staleHeads.get(relativePath) === head ? indexObjectId : head;
|
|
295
|
+
}
|
|
296
|
+
if (objectId) entries.set(relativePath, { kind: "gitlink", objectId });
|
|
186
297
|
}
|
|
187
|
-
|
|
298
|
+
addParentDirectories(entries, root);
|
|
299
|
+
return { entries, indexRecords: listing.indexRecords, submodules };
|
|
188
300
|
}
|
|
189
|
-
function
|
|
190
|
-
const
|
|
191
|
-
|
|
301
|
+
function normalizedGitlinks(gitlinks) {
|
|
302
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
303
|
+
for (const [relativePath, objectId] of gitlinks ?? []) {
|
|
304
|
+
const relative = normalizeRelativePath(relativePath);
|
|
305
|
+
if (!relative) throw new Error(`Invalid working-tree gitlink path: ${relativePath}`);
|
|
306
|
+
if (!OBJECT_ID_PATTERN.test(objectId)) throw new Error(`Invalid working-tree gitlink object id for ${relativePath}: ${objectId}`);
|
|
307
|
+
normalized.set(relative, objectId);
|
|
308
|
+
}
|
|
309
|
+
return normalized;
|
|
192
310
|
}
|
|
193
311
|
function inspectWorkingTree(root, mode, options = {}) {
|
|
194
312
|
const entries = /* @__PURE__ */ new Map();
|
|
195
313
|
root = path.resolve(root);
|
|
196
314
|
if (!inspectDirectoryRoot(root, { allowMissing: true, label: "Working-tree root" })) return entries;
|
|
197
|
-
if (mode === "
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (omittedGitlinks?.has(relativePath) && directoryIsEmpty(root, relativePath)) continue;
|
|
203
|
-
addEntry(entries, root, relativePath, true);
|
|
204
|
-
}
|
|
315
|
+
if (mode === "git") return inspectGitWorkingTree(root).entries;
|
|
316
|
+
const gitlinks = normalizedGitlinks(options.gitlinks);
|
|
317
|
+
for (const child of fs.readdirSync(root).sort()) addEntry(entries, root, child, true, gitlinks);
|
|
318
|
+
for (const [relativePath, objectId] of gitlinks) {
|
|
319
|
+
if (entries.get(relativePath)?.kind !== "gitlink") entries.set(relativePath, { kind: "gitlink", objectId });
|
|
205
320
|
}
|
|
206
321
|
addParentDirectories(entries, root);
|
|
207
322
|
return entries;
|
|
@@ -351,6 +466,9 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
|
|
|
351
466
|
}
|
|
352
467
|
return;
|
|
353
468
|
}
|
|
469
|
+
if (entry.kind === "gitlink") {
|
|
470
|
+
throw new Error(`Working-tree gitlink cannot be copied as content: ${relativePath}`);
|
|
471
|
+
}
|
|
354
472
|
if (!source.stat.isFile() || source.stat.isSymbolicLink() || source.stat.size !== entry.size) {
|
|
355
473
|
throw new Error(`Working-tree source changed while it was being mirrored: ${sourcePath}`);
|
|
356
474
|
}
|
|
@@ -364,6 +482,110 @@ function copyEntry(sourceRoot, targetRoot, relativePath, entry, durability) {
|
|
|
364
482
|
copyRegularFile(sourcePath, targetPath, entry, durability);
|
|
365
483
|
}
|
|
366
484
|
}
|
|
485
|
+
function inspectMirrorTarget(targetRoot, deletionMode) {
|
|
486
|
+
if (deletionMode !== "git") return { existing: inspectWorkingTree(targetRoot, "all"), git: null };
|
|
487
|
+
const inspection = inspectGitWorkingTree(targetRoot);
|
|
488
|
+
const occupied = /* @__PURE__ */ new Set();
|
|
489
|
+
for (const relativePath of inspection.submodules.keys()) {
|
|
490
|
+
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
491
|
+
if (inspected.kind === "entry" && inspected.stat.isDirectory() && fs.readdirSync(inspected.absolutePath).length > 0) {
|
|
492
|
+
occupied.add(relativePath);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return { existing: inspection.entries, git: { indexRecords: inspection.indexRecords, submodules: inspection.submodules, occupied } };
|
|
496
|
+
}
|
|
497
|
+
function shieldedDesiredPaths(desired, git) {
|
|
498
|
+
const shielded = /* @__PURE__ */ new Set();
|
|
499
|
+
if (git.submodules.size === 0) return shielded;
|
|
500
|
+
const roots = new Set(git.submodules.keys());
|
|
501
|
+
for (const [relativePath, entry] of desired) {
|
|
502
|
+
if (hasAncestorIn(relativePath, roots)) {
|
|
503
|
+
shielded.add(relativePath);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (!roots.has(relativePath) || entry.kind === "gitlink") continue;
|
|
507
|
+
if (entry.kind === "directory" || git.occupied.has(relativePath)) shielded.add(relativePath);
|
|
508
|
+
}
|
|
509
|
+
return shielded;
|
|
510
|
+
}
|
|
511
|
+
function directoryShieldsSubmodule(relativePath, entry, git) {
|
|
512
|
+
if (!git || entry.kind !== "directory") return false;
|
|
513
|
+
const prefix = `${relativePath}/`;
|
|
514
|
+
for (const root of git.submodules.keys()) if (root.startsWith(prefix)) return true;
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
function planIndexChanges(desired, shielded, git) {
|
|
518
|
+
const removed = [];
|
|
519
|
+
const written = [];
|
|
520
|
+
const desiredGitlinks = /* @__PURE__ */ new Map();
|
|
521
|
+
for (const [relativePath, entry] of desired) {
|
|
522
|
+
if (entry.kind === "gitlink" && !shielded.has(relativePath)) desiredGitlinks.set(relativePath, entry.objectId);
|
|
523
|
+
}
|
|
524
|
+
const desiredGitlinkRoots = new Set(desiredGitlinks.keys());
|
|
525
|
+
const currentGitlinks = /* @__PURE__ */ new Map();
|
|
526
|
+
for (const record of git.indexRecords) {
|
|
527
|
+
const desiredObjectId = desiredGitlinks.get(record.relativePath);
|
|
528
|
+
if (record.mode === "160000") {
|
|
529
|
+
if (record.stage === 0) currentGitlinks.set(record.relativePath, record.objectId);
|
|
530
|
+
if (desiredObjectId !== void 0) {
|
|
531
|
+
if (desiredObjectId !== record.objectId) removed.push(record);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const desiredEntry = desired.get(record.relativePath);
|
|
535
|
+
if (desiredEntry?.kind !== "directory" && desiredEntry?.kind !== "gitlink" && !git.occupied.has(record.relativePath)) {
|
|
536
|
+
removed.push(record);
|
|
537
|
+
}
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (desiredObjectId !== void 0 || hasAncestorIn(record.relativePath, desiredGitlinkRoots)) removed.push(record);
|
|
541
|
+
}
|
|
542
|
+
for (const [relativePath, objectId] of desiredGitlinks) {
|
|
543
|
+
if (currentGitlinks.get(relativePath) !== objectId) written.push({ relativePath, objectId });
|
|
544
|
+
}
|
|
545
|
+
return { removed, written };
|
|
546
|
+
}
|
|
547
|
+
function indexInfoRecord(record) {
|
|
548
|
+
const stage = record.stage === 0 ? "" : ` ${record.stage}`;
|
|
549
|
+
return Buffer.concat([Buffer.from(`${record.mode} ${record.objectId}${stage} `), Buffer.from(record.relativePath), Buffer.from([0])]);
|
|
550
|
+
}
|
|
551
|
+
function updateIndex(targetRoot, changes) {
|
|
552
|
+
if (changes.drop.length === 0 && changes.put.length === 0) return;
|
|
553
|
+
const feed = Buffer.concat([
|
|
554
|
+
...changes.drop.map(
|
|
555
|
+
({ relativePath, objectId }) => indexInfoRecord({ mode: "0", objectId: "0".repeat(objectId.length), stage: 0, relativePath })
|
|
556
|
+
),
|
|
557
|
+
...changes.put.map(indexInfoRecord)
|
|
558
|
+
]);
|
|
559
|
+
gitOutput(
|
|
560
|
+
targetRoot,
|
|
561
|
+
["-c", "core.fsync=index", "-c", "core.fsyncMethod=fsync", "update-index", "-z", "--index-info"],
|
|
562
|
+
"Update Git working-tree index",
|
|
563
|
+
{ stdin: feed }
|
|
564
|
+
);
|
|
565
|
+
const gitDirectory = checkoutGitDirectory(targetRoot);
|
|
566
|
+
if (!gitDirectory) return;
|
|
567
|
+
const indexPath = path.join(gitDirectory, "index");
|
|
568
|
+
if (lstatIfExists(indexPath)?.isFile()) fsyncPath(indexPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
569
|
+
fsyncPath(gitDirectory, fs.constants.O_RDONLY);
|
|
570
|
+
}
|
|
571
|
+
function recordGitlinkStaleHeads(targetRoot, desired, shielded, git) {
|
|
572
|
+
const heads = readGitlinkStaleHeads(targetRoot);
|
|
573
|
+
let changed = false;
|
|
574
|
+
for (const [relativePath, head] of git.submodules) {
|
|
575
|
+
const entry = desired.get(relativePath);
|
|
576
|
+
const next = entry?.kind === "gitlink" && !shielded.has(relativePath) && head && head !== entry.objectId ? head : null;
|
|
577
|
+
if ((heads.get(relativePath) ?? null) === next) continue;
|
|
578
|
+
if (next) heads.set(relativePath, next);
|
|
579
|
+
else heads.delete(relativePath);
|
|
580
|
+
changed = true;
|
|
581
|
+
}
|
|
582
|
+
for (const relativePath of [...heads.keys()]) {
|
|
583
|
+
if (git.submodules.has(relativePath)) continue;
|
|
584
|
+
heads.delete(relativePath);
|
|
585
|
+
changed = true;
|
|
586
|
+
}
|
|
587
|
+
if (changed) writeGitlinkStaleHeads(targetRoot, heads);
|
|
588
|
+
}
|
|
367
589
|
function mirrorWorkingTree(input) {
|
|
368
590
|
const sourceRoot = path.resolve(input.sourceRoot);
|
|
369
591
|
const targetRoot = path.resolve(input.targetRoot);
|
|
@@ -372,19 +594,34 @@ function mirrorWorkingTree(input) {
|
|
|
372
594
|
fs.mkdirSync(targetRoot, { recursive: true });
|
|
373
595
|
}
|
|
374
596
|
inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
|
|
375
|
-
const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
for (const relativePath of [...existing.keys()].sort(
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
(
|
|
384
|
-
|
|
597
|
+
const desired = inspectWorkingTree(sourceRoot, input.sourceMode, { gitlinks: input.sourceGitlinks });
|
|
598
|
+
const target = inspectMirrorTarget(targetRoot, input.deletionMode);
|
|
599
|
+
const shielded = target.git ? shieldedDesiredPaths(desired, target.git) : /* @__PURE__ */ new Set();
|
|
600
|
+
const index = target.git ? planIndexChanges(desired, shielded, target.git) : null;
|
|
601
|
+
for (const relativePath of [...target.existing.keys()].sort(deepestFirst)) {
|
|
602
|
+
if (desired.has(relativePath)) continue;
|
|
603
|
+
const entry = target.existing.get(relativePath);
|
|
604
|
+
if (entry.kind === "gitlink" || directoryShieldsSubmodule(relativePath, entry, target.git)) continue;
|
|
605
|
+
removeEntry(targetRoot, relativePath);
|
|
606
|
+
}
|
|
607
|
+
const gitlinks = [];
|
|
608
|
+
for (const [relativePath, entry] of [...desired.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
609
|
+
if (shielded.has(relativePath)) continue;
|
|
610
|
+
if (entry.kind === "gitlink") {
|
|
611
|
+
ensureDirectory(targetRoot, relativePath, 493);
|
|
612
|
+
gitlinks.push({ relativePath, objectId: entry.objectId });
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
385
615
|
copyEntry(sourceRoot, targetRoot, relativePath, entry, durability);
|
|
386
616
|
}
|
|
387
|
-
|
|
617
|
+
if (index && target.git) {
|
|
618
|
+
updateIndex(targetRoot, { drop: index.removed, put: index.written.map((gitlink) => ({ mode: "160000", stage: 0, ...gitlink })) });
|
|
619
|
+
recordGitlinkStaleHeads(targetRoot, desired, shielded, target.git);
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
paths: [...desired.keys()].filter((relativePath) => !shielded.has(relativePath) && desired.get(relativePath)?.kind !== "directory").sort(),
|
|
623
|
+
gitlinks
|
|
624
|
+
};
|
|
388
625
|
}
|
|
389
626
|
function normalizeWorkingTreeRelativePath(value) {
|
|
390
627
|
return normalizeRelativePath(value);
|
|
@@ -424,23 +661,25 @@ function fsyncPath(targetPath, flags) {
|
|
|
424
661
|
function planWorkingTreeMirror(input) {
|
|
425
662
|
const sourceRoot = path.resolve(input.sourceRoot);
|
|
426
663
|
const targetRoot = path.resolve(input.targetRoot);
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
664
|
+
const inspectedDesired = inspectWorkingTree(sourceRoot, input.sourceMode, { gitlinks: input.sourceGitlinks });
|
|
665
|
+
const target = inspectMirrorTarget(targetRoot, input.deletionMode);
|
|
666
|
+
const shielded = target.git ? shieldedDesiredPaths(inspectedDesired, target.git) : /* @__PURE__ */ new Set();
|
|
667
|
+
const desired = new Map([...inspectedDesired].filter(([relativePath]) => !shielded.has(relativePath)));
|
|
431
668
|
const replaced = /* @__PURE__ */ new Map();
|
|
432
669
|
const absent = [];
|
|
433
|
-
for (const [relativePath, entry] of existing) {
|
|
434
|
-
if (
|
|
670
|
+
for (const [relativePath, entry] of target.existing) {
|
|
671
|
+
if (inspectedDesired.has(relativePath) || entry.kind === "gitlink") continue;
|
|
672
|
+
if (directoryShieldsSubmodule(relativePath, entry, target.git)) continue;
|
|
673
|
+
replaced.set(relativePath, entry);
|
|
435
674
|
}
|
|
436
675
|
for (const [relativePath, entry] of desired) {
|
|
437
|
-
const
|
|
438
|
-
if (
|
|
676
|
+
const targetEntry = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
677
|
+
if (targetEntry.kind !== "entry") {
|
|
439
678
|
absent.push(relativePath);
|
|
440
679
|
continue;
|
|
441
680
|
}
|
|
442
|
-
const { absolutePath, stat } =
|
|
443
|
-
if (entry.kind === "directory") {
|
|
681
|
+
const { absolutePath, stat } = targetEntry;
|
|
682
|
+
if (entry.kind === "directory" || entry.kind === "gitlink") {
|
|
444
683
|
if (stat.isDirectory()) continue;
|
|
445
684
|
} else if (entry.kind === "symlink") {
|
|
446
685
|
if (stat.isSymbolicLink() && fs.readlinkSync(absolutePath) === entry.target) continue;
|
|
@@ -460,7 +699,8 @@ function planWorkingTreeMirror(input) {
|
|
|
460
699
|
return {
|
|
461
700
|
desired,
|
|
462
701
|
replaced,
|
|
463
|
-
absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst)
|
|
702
|
+
absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst),
|
|
703
|
+
index: target.git ? planIndexChanges(inspectedDesired, shielded, target.git) : { removed: [], written: [] }
|
|
464
704
|
};
|
|
465
705
|
}
|
|
466
706
|
function captureWorkingTreeMirrorPlan(input) {
|
|
@@ -473,6 +713,7 @@ function captureWorkingTreeMirrorPlan(input) {
|
|
|
473
713
|
}
|
|
474
714
|
const trees = [];
|
|
475
715
|
for (const [relativePath, entry] of [...input.plan.replaced.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
|
|
716
|
+
if (entry.kind === "gitlink") continue;
|
|
476
717
|
if (entry.kind !== "directory") {
|
|
477
718
|
copyEntry(targetRoot, snapshotRoot, relativePath, entry, durability);
|
|
478
719
|
continue;
|
|
@@ -491,7 +732,12 @@ function captureWorkingTreeMirrorPlan(input) {
|
|
|
491
732
|
});
|
|
492
733
|
trees.push(relativePath);
|
|
493
734
|
}
|
|
494
|
-
|
|
735
|
+
const { index } = input.plan;
|
|
736
|
+
return {
|
|
737
|
+
absent: [...input.plan.absent].sort(shallowestFirst),
|
|
738
|
+
trees: trees.sort(shallowestFirst),
|
|
739
|
+
...index.removed.length > 0 || index.written.length > 0 ? { index } : {}
|
|
740
|
+
};
|
|
495
741
|
}
|
|
496
742
|
function restoreWorkingTreeMirrorScope(input) {
|
|
497
743
|
const snapshotRoot = path.resolve(input.snapshotRoot);
|
|
@@ -516,6 +762,7 @@ function restoreWorkingTreeMirrorScope(input) {
|
|
|
516
762
|
const touched = /* @__PURE__ */ new Set();
|
|
517
763
|
for (const relativePath of [...absent].sort(deepestFirst)) {
|
|
518
764
|
const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
|
|
765
|
+
if (inspected.kind === "entry" && inspected.stat.isDirectory() && lstatIfExists(path.join(inspected.absolutePath, ".git"))) continue;
|
|
519
766
|
if (inspected.kind === "entry") fs.rmSync(inspected.absolutePath, { recursive: true, force: true });
|
|
520
767
|
touched.add(relativePath);
|
|
521
768
|
}
|
|
@@ -553,7 +800,7 @@ function restoreWorkingTreeMirrorScope(input) {
|
|
|
553
800
|
if (current.kind === "entry" && !current.stat.isDirectory()) {
|
|
554
801
|
fs.rmSync(current.absolutePath, { recursive: true, force: true });
|
|
555
802
|
}
|
|
556
|
-
ensureDirectory(targetRoot, relativePath, entry.mode);
|
|
803
|
+
ensureDirectory(targetRoot, relativePath, entry.kind === "directory" ? entry.mode : 493);
|
|
557
804
|
mirrorWorkingTree({
|
|
558
805
|
sourceRoot: path.join(snapshotRoot, ...relativePath.split("/")),
|
|
559
806
|
targetRoot: path.join(targetRoot, ...relativePath.split("/")),
|
|
@@ -561,6 +808,7 @@ function restoreWorkingTreeMirrorScope(input) {
|
|
|
561
808
|
deletionMode: "all"
|
|
562
809
|
});
|
|
563
810
|
}
|
|
811
|
+
if (input.scope.index) updateIndex(targetRoot, { drop: input.scope.index.written, put: input.scope.index.removed });
|
|
564
812
|
return { paths: [...touched].sort(shallowestFirst) };
|
|
565
813
|
}
|
|
566
814
|
function fsyncWorkingTreePaths(root, relativePaths) {
|