@coderook/cli 0.45.0 → 0.46.1
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/.claude-plugin/plugin.json +1 -1
- package/dist/cbx/src/core/upload.js +40 -0
- package/dist/cbx/src/core/worktree.js +77 -10
- package/dist/cbx/src/local/history.js +6 -3
- package/dist/cbx/src/local/snapshot.js +99 -22
- package/dist/cbx/src/local/store.js +26 -0
- package/dist/cbx/src/shared/native_engine.js +289 -0
- package/dist/cli/src/bin.js +86 -0
- package/dist/cli/src/cli.js +22 -2
- package/dist/cli/src/local_commands.js +68 -116
- package/dist/cli/src/local_status.js +72 -0
- package/dist/cli/src/network.js +21 -7
- package/dist/cli/src/reminder_gate.js +37 -0
- package/dist/cli/src/update_command.js +2 -1
- package/dist/cli/src/workspace_threads.js +3 -13
- package/engine-bin/darwin-arm64/cbx-engine +0 -0
- package/engine-bin/darwin-x64/cbx-engine +0 -0
- package/engine-bin/linux-arm64/cbx-engine +0 -0
- package/engine-bin/linux-x64/cbx-engine +0 -0
- package/engine-bin/win32-x64/cbx-engine.exe +0 -0
- package/package.json +5 -4
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "coderook",
|
|
3
3
|
"displayName": "CodeRook",
|
|
4
4
|
"description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, with cbx or plain git, from Claude Code.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.46.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
|
@@ -20,6 +20,7 @@ const faults_js_1 = require("./faults.js");
|
|
|
20
20
|
const identify_js_1 = require("./identify.js");
|
|
21
21
|
const worktree_js_1 = require("./worktree.js");
|
|
22
22
|
const rules_js_1 = require("./rules.js");
|
|
23
|
+
const native_engine_js_1 = require("../shared/native_engine.js");
|
|
23
24
|
const publish_name_js_1 = require("./publish_name.js");
|
|
24
25
|
const compress_js_1 = require("./compress.js");
|
|
25
26
|
const compression_policy_js_1 = require("../shared/compression_policy.js");
|
|
@@ -696,6 +697,33 @@ class Uploader {
|
|
|
696
697
|
bytesPerSecond: 0,
|
|
697
698
|
});
|
|
698
699
|
};
|
|
700
|
+
/*
|
|
701
|
+
Hashed ahead by the native engine, on every core, when there is enough
|
|
702
|
+
to read to be worth a process. Each answer carries the size and time the
|
|
703
|
+
file had while it was read, and is used below only when a fresh stat
|
|
704
|
+
still says the same — so it never vouches for bytes that have moved.
|
|
705
|
+
Anything it does not answer is hashed here as before.
|
|
706
|
+
*/
|
|
707
|
+
const ahead = new Map();
|
|
708
|
+
{
|
|
709
|
+
const unknown = sending.filter((file) => {
|
|
710
|
+
const remembered = this.plannedDigests.get(file.path) ?? request.knownDigests?.get(file.path);
|
|
711
|
+
return !(remembered && remembered.size === file.size);
|
|
712
|
+
});
|
|
713
|
+
const bytes = unknown.reduce((sum, file) => sum + file.size, 0);
|
|
714
|
+
if ((0, native_engine_js_1.worthTheEngine)(unknown.length, bytes)) {
|
|
715
|
+
const answered = await (0, profile_js_1.timed)("hash files", () => (0, native_engine_js_1.engineMeasure)(request.localPath, unknown.map((file) => ({ path: file.path, count: false })), (done, _total, file) => {
|
|
716
|
+
hashed = done;
|
|
717
|
+
tell(file);
|
|
718
|
+
}, this.controller.signal));
|
|
719
|
+
this.check();
|
|
720
|
+
hashed = 0;
|
|
721
|
+
for (const [file, one] of answered ?? []) {
|
|
722
|
+
if (one.sha256 && !one.error && typeof one.mtimeMs === "number")
|
|
723
|
+
ahead.set(file, one);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
699
727
|
const lane = async () => {
|
|
700
728
|
for (;;) {
|
|
701
729
|
const index = cursor;
|
|
@@ -757,6 +785,18 @@ class Uploader {
|
|
|
757
785
|
continue;
|
|
758
786
|
}
|
|
759
787
|
let digest;
|
|
788
|
+
const read = ahead.get(file.path);
|
|
789
|
+
if (read && read.size === size && read.mtimeMs === mtimeMs) {
|
|
790
|
+
digest = read.sha256;
|
|
791
|
+
if (fromPlan && fromPlan.sha256 !== digest)
|
|
792
|
+
changedSincePlan.push(file.path);
|
|
793
|
+
this.plannedDigests.set(file.path, { size, mtimeMs, sha256: digest });
|
|
794
|
+
measured[index] = { path: file.path, full, size, sha256: digest, mediaType: mediaTypeOf(file.path) };
|
|
795
|
+
hashed += 1;
|
|
796
|
+
readDone += file.size;
|
|
797
|
+
tell(file.path, hashed === sending.length);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
760
800
|
try {
|
|
761
801
|
digest = await (0, profile_js_1.timed)("hash files", () => digestOf(full, (read) => {
|
|
762
802
|
readingNow.set(index, read);
|
|
@@ -46,6 +46,7 @@ const detect_js_1 = require("./detect.js");
|
|
|
46
46
|
const ignore_templates_js_1 = require("./ignore_templates.js");
|
|
47
47
|
const rules_js_1 = require("./rules.js");
|
|
48
48
|
const secret_patterns_js_1 = require("./secret_patterns.js");
|
|
49
|
+
const native_engine_js_1 = require("../shared/native_engine.js");
|
|
49
50
|
const run = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
50
51
|
/* A code point, so nothing between here and disk can eat the escape. */
|
|
51
52
|
const NEWLINE = String.fromCharCode(10);
|
|
@@ -799,6 +800,8 @@ async function branchName(root) {
|
|
|
799
800
|
const LINE_COUNT_LIMIT = 4 * 1024 * 1024;
|
|
800
801
|
/** Enough rows for any real project; past it the rail would be unusable. */
|
|
801
802
|
const LISTED_FILE_LIMIT = 20_000;
|
|
803
|
+
/** How many files the native engine is handed at once while a scan reads ahead. */
|
|
804
|
+
const READ_AHEAD_FILES = 4_096;
|
|
802
805
|
/**
|
|
803
806
|
* The digest of a file, without holding it.
|
|
804
807
|
*
|
|
@@ -927,10 +930,18 @@ async function changedFiles(root, rules, baseline = null, mode = "add-and-update
|
|
|
927
930
|
*/
|
|
928
931
|
stats,
|
|
929
932
|
/** Filled with how much of the folder the cap allowed through. */
|
|
930
|
-
outcome
|
|
931
|
-
|
|
933
|
+
outcome,
|
|
934
|
+
/** False for a read-only report: the listing's sizes and times may stand in for a stat (see engineScan). */
|
|
935
|
+
exact = true) {
|
|
932
936
|
/* What git already tracks goes up whatever a rule says; see gitTracked. */
|
|
933
937
|
const tracked = await gitTracked(root);
|
|
938
|
+
/*
|
|
939
|
+
The native engine walks the folder itself when there is one, deciding every
|
|
940
|
+
rule as this file does and in the same order (engine/compat/rules-cross.mjs
|
|
941
|
+
compares the two), with one stat per file. Otherwise both passes run here.
|
|
942
|
+
*/
|
|
943
|
+
const native = await (0, native_engine_js_1.engineScan)(root, rules, tracked, baseline?.size ?? stats?.size, exact);
|
|
944
|
+
const layers = native ? [] : await collectLayers(root, rules);
|
|
934
945
|
const files = [];
|
|
935
946
|
const present = new Set();
|
|
936
947
|
const pending = [root];
|
|
@@ -951,7 +962,12 @@ outcome) {
|
|
|
951
962
|
costs, so reading is what the cap should govern, and it can only be spent
|
|
952
963
|
well once the whole folder is known.
|
|
953
964
|
*/
|
|
954
|
-
for (
|
|
965
|
+
for (const one of native ?? []) {
|
|
966
|
+
present.add(one.path);
|
|
967
|
+
outcome?.sizes.push({ path: one.path, size: one.size });
|
|
968
|
+
found.push({ full: node_path_1.default.join(root, ...one.path.split("/")), relative: one.path, size: one.size, mtimeMs: one.mtimeMs });
|
|
969
|
+
}
|
|
970
|
+
for (let next = 0; !native && next < pending.length; next++) {
|
|
955
971
|
const directory = pending[next];
|
|
956
972
|
let entries;
|
|
957
973
|
try {
|
|
@@ -1024,15 +1040,58 @@ outcome) {
|
|
|
1024
1040
|
the list is full rather than over a fixed slice — a folder where nothing
|
|
1025
1041
|
has changed still has to be walked through to find that out.
|
|
1026
1042
|
*/
|
|
1027
|
-
|
|
1043
|
+
const order = spreadAcrossDirectories(found);
|
|
1044
|
+
/*
|
|
1045
|
+
Read ahead by the native engine, a slice at a time in the same order, when
|
|
1046
|
+
there is enough to read to be worth a process. A slice rather than all of
|
|
1047
|
+
it, so the cap still stops the reading where it stops the list. Whatever
|
|
1048
|
+
the engine does not answer is measured here as before, with the same rules:
|
|
1049
|
+
an unchanged file is not read, nor a large one nothing could match.
|
|
1050
|
+
*/
|
|
1051
|
+
const readAhead = new Map();
|
|
1052
|
+
const needsReading = (one) => {
|
|
1053
|
+
const cached = stats?.get(one.relative);
|
|
1054
|
+
if (cached && cached.size === one.size && cached.mtimeMs === one.mtimeMs)
|
|
1055
|
+
return false;
|
|
1056
|
+
return one.size <= LINE_COUNT_LIMIT || (baseline?.has(one.relative) ?? false);
|
|
1057
|
+
};
|
|
1058
|
+
let readAheadTo = 0;
|
|
1059
|
+
for (let at = 0; at < order.length; at += 1) {
|
|
1028
1060
|
if (files.length >= limit)
|
|
1029
1061
|
break;
|
|
1062
|
+
if (at >= readAheadTo) {
|
|
1063
|
+
const slice = [];
|
|
1064
|
+
let bytes = 0;
|
|
1065
|
+
let next = at;
|
|
1066
|
+
while (next < order.length && slice.length < READ_AHEAD_FILES) {
|
|
1067
|
+
const candidate = order[next];
|
|
1068
|
+
if (needsReading(candidate)) {
|
|
1069
|
+
slice.push(candidate);
|
|
1070
|
+
bytes += candidate.size;
|
|
1071
|
+
}
|
|
1072
|
+
next += 1;
|
|
1073
|
+
}
|
|
1074
|
+
readAheadTo = next;
|
|
1075
|
+
if ((0, native_engine_js_1.worthTheEngine)(slice.length, bytes)) {
|
|
1076
|
+
const answered = await (0, native_engine_js_1.engineMeasure)(root, slice.map((one) => ({ path: one.relative, count: one.size <= LINE_COUNT_LIMIT })));
|
|
1077
|
+
for (const one of slice) {
|
|
1078
|
+
const got = answered?.get(one.relative);
|
|
1079
|
+
/* Not as listed means it is being written; measured again below. */
|
|
1080
|
+
if (!got?.sha256 || got.error || got.size !== one.size || got.mtimeMs !== one.mtimeMs)
|
|
1081
|
+
continue;
|
|
1082
|
+
readAhead.set(one.relative, { lines: got.lines ?? 0, binary: got.binary ?? true, hash: got.sha256 });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
const one = order[at];
|
|
1030
1087
|
const { full, relative, size, mtimeMs } = one;
|
|
1031
|
-
const measured =
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1088
|
+
const measured = readAhead.get(relative) ??
|
|
1089
|
+
(await measure(full, size, {
|
|
1090
|
+
mtimeMs,
|
|
1091
|
+
cached: stats?.get(relative),
|
|
1092
|
+
recorded: baseline?.has(relative) ?? false,
|
|
1093
|
+
}));
|
|
1094
|
+
readAhead.delete(relative);
|
|
1036
1095
|
/*
|
|
1037
1096
|
Remembered whether or not it changed: the next scan wants to skip
|
|
1038
1097
|
reading this file again, and that is just as true of one that was
|
|
@@ -1107,8 +1166,16 @@ async function surveyFiles(root, rules,
|
|
|
1107
1166
|
difference between waiting and wondering whether it has hung.
|
|
1108
1167
|
*/
|
|
1109
1168
|
onFound) {
|
|
1110
|
-
const layers = await collectLayers(root, rules);
|
|
1111
1169
|
const tracked = await gitTracked(root);
|
|
1170
|
+
/* The native engine's walk, when there is one: the same files, decided the same way. */
|
|
1171
|
+
const native = await (0, native_engine_js_1.engineScan)(root, rules, tracked);
|
|
1172
|
+
if (native) {
|
|
1173
|
+
onFound?.(native.length);
|
|
1174
|
+
return native
|
|
1175
|
+
.map((one) => ({ path: one.path, size: one.size }))
|
|
1176
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
1177
|
+
}
|
|
1178
|
+
const layers = await collectLayers(root, rules);
|
|
1112
1179
|
const found = [];
|
|
1113
1180
|
/*
|
|
1114
1181
|
Every entry asks about each folder above it; those answers do not change
|
|
@@ -182,13 +182,16 @@ async function log(repository, options = {}) {
|
|
|
182
182
|
}
|
|
183
183
|
async function status(repository) {
|
|
184
184
|
const { save: head, tree } = await headTree(repository);
|
|
185
|
-
|
|
185
|
+
/* A report, not a save: the listing's sizes and times may stand in for a stat each. */
|
|
186
|
+
const reading = await (0, snapshot_js_1.readFolder)(repository, { store: false, previous: tree, exact: false });
|
|
186
187
|
/*
|
|
187
188
|
Kept, so the next status does not hash the same files again. The index
|
|
188
189
|
is only a cache: an entry this writes without objects makes a save read
|
|
189
|
-
that file once more, never trust something it has not stored.
|
|
190
|
+
that file once more, never trust something it has not stored. Written
|
|
191
|
+
only when it changed; rewriting it whole was a good part of a quiet status.
|
|
190
192
|
*/
|
|
191
|
-
|
|
193
|
+
if (reading.indexChanged)
|
|
194
|
+
await repository.writeIndex(reading.index);
|
|
192
195
|
const merging = await (0, merge_js_1.readMergeState)(repository);
|
|
193
196
|
return {
|
|
194
197
|
line: await repository.currentLine(),
|
|
@@ -24,6 +24,7 @@ const promises_1 = require("node:fs/promises");
|
|
|
24
24
|
const node_path_1 = __importDefault(require("node:path"));
|
|
25
25
|
const worktree_js_1 = require("../core/worktree.js");
|
|
26
26
|
const chunking_js_1 = require("../shared/chunking.js");
|
|
27
|
+
const native_engine_js_1 = require("../shared/native_engine.js");
|
|
27
28
|
const safe_path_js_1 = require("../shared/safe_path.js");
|
|
28
29
|
const READ_SIZE = 8 * 1024 * 1024;
|
|
29
30
|
/*
|
|
@@ -58,8 +59,15 @@ async function inLanes(items, count, work) {
|
|
|
58
59
|
await Promise.all(Array.from({ length: Math.min(count, items.length) }, () => lane()));
|
|
59
60
|
}
|
|
60
61
|
/** The files the rules would save, with what `stat` says about each. */
|
|
61
|
-
async function listFolder(root) {
|
|
62
|
+
async function listFolder(root, expectedFiles, exact = true) {
|
|
62
63
|
const rules = await (0, worktree_js_1.readRules)(root);
|
|
64
|
+
/* The native engine walks and stats in one pass when there is one; otherwise the walk below and a stat each. */
|
|
65
|
+
const native = await (0, native_engine_js_1.engineScan)(root, rules, await (0, worktree_js_1.gitTracked)(root), expectedFiles, exact);
|
|
66
|
+
if (native) {
|
|
67
|
+
return native
|
|
68
|
+
.map((one) => ({ path: one.path, size: one.size, mtimeMs: one.mtimeMs, mode: one.mode }))
|
|
69
|
+
.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
70
|
+
}
|
|
63
71
|
const found = await (0, worktree_js_1.surveyFiles)(root, rules);
|
|
64
72
|
const files = [];
|
|
65
73
|
await inLanes(found, SMALL_LANES, async (one) => {
|
|
@@ -138,30 +146,61 @@ async function storeFile(repository, full, size) {
|
|
|
138
146
|
async function readFolder(repository, options) {
|
|
139
147
|
const index = await repository.readIndex();
|
|
140
148
|
const previous = new Map(options.previous?.files.map((file) => [file.path, file]) ?? []);
|
|
141
|
-
const listed = await listFolder(repository.root);
|
|
149
|
+
const listed = await listFolder(repository.root, index.size || undefined, options.store || options.exact !== false);
|
|
142
150
|
const files = new Map();
|
|
143
151
|
const next = new Map();
|
|
144
152
|
const racyBefore = Date.now() - RACY_WINDOW_MS;
|
|
145
|
-
|
|
146
|
-
const one = async (file) => {
|
|
147
|
-
const full = node_path_1.default.join(repository.root, file.path);
|
|
153
|
+
const unchangedSince = (file) => {
|
|
148
154
|
const known = index.get(file.path);
|
|
149
|
-
|
|
150
|
-
let chunks = [];
|
|
151
|
-
const unchanged = known &&
|
|
155
|
+
return known &&
|
|
152
156
|
known.size === file.size &&
|
|
153
157
|
known.mtimeMs === file.mtimeMs &&
|
|
154
|
-
(!options.store || known.chunks.length > 0 || file.size === 0)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
(!options.store || known.chunks.length > 0 || file.size === 0)
|
|
159
|
+
? known
|
|
160
|
+
: null;
|
|
161
|
+
};
|
|
162
|
+
/*
|
|
163
|
+
The files that have to be read, handed to the native engine in one go when
|
|
164
|
+
there are enough of them to be worth a process. It reads, hashes, cuts and
|
|
165
|
+
compresses them on every core and gives the same names and chunks storeFile
|
|
166
|
+
would; anything it does not answer is read here as before.
|
|
167
|
+
*/
|
|
168
|
+
const toRead = listed.filter((file) => !unchangedSince(file));
|
|
169
|
+
let native = null;
|
|
170
|
+
let done = listed.length - toRead.length;
|
|
171
|
+
const readBytes = toRead.reduce((sum, file) => sum + file.size, 0);
|
|
172
|
+
if ((0, native_engine_js_1.worthTheEngine)(toRead.length, readBytes)) {
|
|
173
|
+
const report = options.progress
|
|
174
|
+
? (count, _total, file) => options.progress(done + count, listed.length, file)
|
|
175
|
+
: undefined;
|
|
176
|
+
const wanted = toRead.map((file) => ({ path: file.path, size: file.size }));
|
|
177
|
+
native = options.store
|
|
178
|
+
? await (0, native_engine_js_1.engineStore)(repository.objects.directory, repository.root, wanted, report)
|
|
179
|
+
: await (0, native_engine_js_1.engineMeasure)(repository.root, wanted.map((file) => ({ path: file.path, count: false })), report);
|
|
180
|
+
/* The engine wrote packs this store has not read yet. */
|
|
181
|
+
if (native && options.store)
|
|
182
|
+
repository.objects.rescan();
|
|
183
|
+
if (native)
|
|
184
|
+
done += toRead.filter((file) => native.get(file.path)?.sha256).length;
|
|
185
|
+
}
|
|
186
|
+
/* What is already known about a file without reading it: from the index, or from the engine. */
|
|
187
|
+
const answerFor = (file) => {
|
|
188
|
+
const known = unchangedSince(file);
|
|
189
|
+
if (known)
|
|
190
|
+
return { sha256: known.sha256, chunks: known.chunks, settled: true, fromEngine: false };
|
|
191
|
+
const answered = native?.get(file.path);
|
|
192
|
+
if (answered && !answered.error && answered.sha256 && (!options.store || answered.chunks)) {
|
|
193
|
+
return {
|
|
194
|
+
sha256: answered.sha256,
|
|
195
|
+
chunks: options.store ? answered.chunks : [],
|
|
196
|
+
/* Whether what was read is what was listed; a file still being written is not trusted next time. */
|
|
197
|
+
settled: answered.size === file.size && (options.store || answered.mtimeMs === file.mtimeMs),
|
|
198
|
+
fromEngine: true,
|
|
199
|
+
};
|
|
164
200
|
}
|
|
201
|
+
return null;
|
|
202
|
+
};
|
|
203
|
+
const record = (file, sha256, chunks, settled) => {
|
|
165
204
|
files.set(file.path, {
|
|
166
205
|
path: file.path,
|
|
167
206
|
size: file.size,
|
|
@@ -169,22 +208,60 @@ async function readFolder(repository, options) {
|
|
|
169
208
|
executable: executableFrom(file.mode, previous.get(file.path)),
|
|
170
209
|
chunks,
|
|
171
210
|
});
|
|
172
|
-
if (file.mtimeMs < racyBefore) {
|
|
211
|
+
if (file.mtimeMs < racyBefore && settled) {
|
|
173
212
|
next.set(file.path, { size: file.size, mtimeMs: file.mtimeMs, sha256, chunks });
|
|
174
213
|
}
|
|
214
|
+
};
|
|
215
|
+
/*
|
|
216
|
+
Everything already answered is recorded straight away; only files that
|
|
217
|
+
still have to be read go through the lanes. A quiet status on a folder of
|
|
218
|
+
eight thousand files spent fifty milliseconds queueing files that needed
|
|
219
|
+
nothing done to them.
|
|
220
|
+
*/
|
|
221
|
+
const toReadHere = [];
|
|
222
|
+
for (const file of listed) {
|
|
223
|
+
const answer = answerFor(file);
|
|
224
|
+
if (!answer) {
|
|
225
|
+
toReadHere.push(file);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
record(file, answer.sha256, answer.chunks, answer.settled);
|
|
229
|
+
if (!answer.fromEngine) {
|
|
230
|
+
done += 1;
|
|
231
|
+
options.progress?.(done, listed.length, file.path);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const one = async (file) => {
|
|
235
|
+
const full = node_path_1.default.join(repository.root, file.path);
|
|
236
|
+
let sha256;
|
|
237
|
+
let chunks = [];
|
|
238
|
+
if (options.store) {
|
|
239
|
+
({ sha256, chunks } = await storeFile(repository, full, file.size));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
sha256 = await digestFile(full, file.size);
|
|
243
|
+
}
|
|
244
|
+
record(file, sha256, chunks, true);
|
|
175
245
|
done += 1;
|
|
176
246
|
options.progress?.(done, listed.length, file.path);
|
|
177
247
|
};
|
|
178
248
|
const large = (file) => (0, chunking_js_1.profileForFileSize)(file.size) !== null;
|
|
179
249
|
await Promise.all([
|
|
180
|
-
inLanes(
|
|
181
|
-
inLanes(
|
|
250
|
+
inLanes(toReadHere.filter((file) => !large(file)), SMALL_LANES, one),
|
|
251
|
+
inLanes(toReadHere.filter(large), LARGE_LANES, one),
|
|
182
252
|
]);
|
|
183
253
|
/* Nothing may name an object that is not on disk yet. */
|
|
184
254
|
if (options.store)
|
|
185
255
|
await repository.objects.flush();
|
|
186
256
|
options.progress?.(listed.length, listed.length, "");
|
|
187
|
-
|
|
257
|
+
const sameEntry = (left, right) => Boolean(right) &&
|
|
258
|
+
left.size === right.size &&
|
|
259
|
+
left.mtimeMs === right.mtimeMs &&
|
|
260
|
+
left.sha256 === right.sha256 &&
|
|
261
|
+
left.chunks.length === right.chunks.length &&
|
|
262
|
+
left.chunks.every((chunk, at) => chunk === right.chunks[at]);
|
|
263
|
+
const indexChanged = next.size !== index.size || [...next].some(([file, entry]) => !sameEntry(entry, index.get(file)));
|
|
264
|
+
return { files, index: next, indexChanged };
|
|
188
265
|
}
|
|
189
266
|
/** A tree entry's bytes, put back together from the store and checked. */
|
|
190
267
|
async function contentsOf(repository, entry) {
|
|
@@ -213,6 +213,15 @@ class ObjectStore {
|
|
|
213
213
|
}
|
|
214
214
|
return (0, cbx_js_1.openChunkFrame)(frame, digest);
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Read the pack indexes again on next use.
|
|
218
|
+
*
|
|
219
|
+
* For after something else has written packs into this store — the native
|
|
220
|
+
* engine does, during a save — since the list is read once and then kept.
|
|
221
|
+
*/
|
|
222
|
+
rescan() {
|
|
223
|
+
this.packed = null;
|
|
224
|
+
}
|
|
216
225
|
/** Close any packs held open for reading. Safe to call at any time. */
|
|
217
226
|
async release() {
|
|
218
227
|
const open = [...this.handles.values()];
|
|
@@ -231,6 +240,23 @@ class ObjectStore {
|
|
|
231
240
|
return this.put(Buffer.from(JSON.stringify(value), "utf8"), { loose: true });
|
|
232
241
|
}
|
|
233
242
|
async getJson(digest) {
|
|
243
|
+
/*
|
|
244
|
+
Saves and trees are written on their own (putJson), so they are looked
|
|
245
|
+
for there first. Going through get() read every pack's index just to
|
|
246
|
+
find them, which grew with the history: the same object either way,
|
|
247
|
+
checked the same way.
|
|
248
|
+
*/
|
|
249
|
+
if (!this.pending.has(digest) && !this.writing.has(digest)) {
|
|
250
|
+
let frame = null;
|
|
251
|
+
try {
|
|
252
|
+
frame = await (0, promises_1.readFile)(this.where(digest));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
/* Not on its own; packed, perhaps. */
|
|
256
|
+
}
|
|
257
|
+
if (frame)
|
|
258
|
+
return JSON.parse((0, cbx_js_1.openChunkFrame)(frame, digest).toString("utf8"));
|
|
259
|
+
}
|
|
234
260
|
return JSON.parse((await this.get(digest)).toString("utf8"));
|
|
235
261
|
}
|
|
236
262
|
/** Every object whose name starts with `prefix` (at least four characters). */
|