@coderook/cli 0.46.1 → 0.46.3
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 +583 -70
- package/dist/cbx/src/local/history.js +27 -4
- package/dist/cbx/src/local/remote.js +15 -0
- package/dist/cbx/src/local/repository.js +90 -11
- package/dist/cbx/src/local/snapshot.js +110 -9
- package/dist/cbx/src/local/store.js +365 -9
- package/dist/cbx/src/local/store_v2.js +269 -0
- package/dist/cbx/src/shared/delta.js +109 -4
- package/dist/cbx/src/shared/native_engine.js +23 -0
- package/dist/cli/src/bin.js +21 -0
- package/dist/cli/src/cli.js +13 -0
- package/dist/cli/src/local_status.js +18 -33
- package/dist/cli/src/paint.js +16 -0
- package/dist/cli/src/status_lines.js +26 -0
- 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 +1 -1
|
@@ -126,7 +126,7 @@ async function save(repository, options) {
|
|
|
126
126
|
previous: before,
|
|
127
127
|
progress: options.progress,
|
|
128
128
|
});
|
|
129
|
-
const tree = await repository.writeTree([...reading.files.values()]);
|
|
129
|
+
const tree = await repository.writeTree([...reading.files.values()], parent?.tree);
|
|
130
130
|
await repository.writeIndex(reading.index);
|
|
131
131
|
if (!merging && parent && parent.tree === tree) {
|
|
132
132
|
return { saved: false, reason: "unchanged" };
|
|
@@ -148,6 +148,14 @@ async function save(repository, options) {
|
|
|
148
148
|
await repository.setLineTip(line, made.id);
|
|
149
149
|
if (merging)
|
|
150
150
|
await (0, merge_js_1.clearMergeState)(repository);
|
|
151
|
+
/* The folder is what was just saved; said so, for the next quiet status. */
|
|
152
|
+
if (repository.objects.v2) {
|
|
153
|
+
await repository.writeIndex(reading.index, {
|
|
154
|
+
head: made.id,
|
|
155
|
+
clean: true,
|
|
156
|
+
complete: reading.index.size === reading.files.size,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
151
159
|
let bytes = 0;
|
|
152
160
|
for (const file of reading.files.values())
|
|
153
161
|
bytes += file.size;
|
|
@@ -190,13 +198,28 @@ async function status(repository) {
|
|
|
190
198
|
that file once more, never trust something it has not stored. Written
|
|
191
199
|
only when it changed; rewriting it whole was a good part of a quiet status.
|
|
192
200
|
*/
|
|
193
|
-
if (reading.indexChanged)
|
|
194
|
-
await repository.writeIndex(reading.index);
|
|
195
201
|
const merging = await (0, merge_js_1.readMergeState)(repository);
|
|
202
|
+
const changes = compareTrees(byPath(tree), reading.files);
|
|
203
|
+
/*
|
|
204
|
+
And whether the folder is exactly the head, so the next quiet status can
|
|
205
|
+
be answered without Node (engine/src/quick.rs). Rewritten when what it
|
|
206
|
+
says changes, as well as when the index does.
|
|
207
|
+
*/
|
|
208
|
+
const stamp = {
|
|
209
|
+
head: head?.id ?? null,
|
|
210
|
+
clean: !changeCount(changes) && !merging,
|
|
211
|
+
complete: reading.index.size === reading.files.size,
|
|
212
|
+
};
|
|
213
|
+
const told = repository.indexStamp;
|
|
214
|
+
if (reading.indexChanged ||
|
|
215
|
+
(repository.objects.v2 &&
|
|
216
|
+
(told?.head !== stamp.head || told?.clean !== stamp.clean || (told?.complete === true) !== stamp.complete))) {
|
|
217
|
+
await repository.writeIndex(reading.index, stamp);
|
|
218
|
+
}
|
|
196
219
|
return {
|
|
197
220
|
line: await repository.currentLine(),
|
|
198
221
|
head,
|
|
199
|
-
changes
|
|
222
|
+
changes,
|
|
200
223
|
...(merging
|
|
201
224
|
? {
|
|
202
225
|
merging: {
|
|
@@ -210,7 +210,22 @@ async function push(repository, options) {
|
|
|
210
210
|
}
|
|
211
211
|
await (0, promises_1.rm)(scratch, { recursive: true, force: true });
|
|
212
212
|
await (0, snapshot_js_1.writeEntriesInto)(repository, scratch, changed.map((file) => after.get(file)));
|
|
213
|
+
/*
|
|
214
|
+
This history holds the base of every file it changes, as Git's sender
|
|
215
|
+
does, so changed files go up as exact patches with no signatures
|
|
216
|
+
fetched first.
|
|
217
|
+
*/
|
|
218
|
+
const baseTree = before;
|
|
213
219
|
const request = {
|
|
220
|
+
/* The listing of each version pushed is kept in the history, for the next push. */
|
|
221
|
+
listingRoot: repository.directory,
|
|
222
|
+
localBase: async (file, sha256) => {
|
|
223
|
+
const entry = baseTree.get(file);
|
|
224
|
+
if (!entry || entry.sha256 !== sha256)
|
|
225
|
+
return null;
|
|
226
|
+
const parts = await Promise.all(entry.chunks.map((name) => repository.objects.get(name)));
|
|
227
|
+
return new Uint8Array(Buffer.concat(parts));
|
|
228
|
+
},
|
|
214
229
|
localPath: scratch,
|
|
215
230
|
include: [...changed, ...changes.deleted],
|
|
216
231
|
deletions: changes.deleted,
|
|
@@ -3,12 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.Repository = exports.DEFAULT_LINE = exports.FORMAT_LINE = exports.STORE_DIRECTORY = void 0;
|
|
6
|
+
exports.Repository = exports.DEFAULT_LINE = exports.FORMAT_LINE_V2 = exports.FORMAT_LINE = exports.STORE_DIRECTORY = void 0;
|
|
7
7
|
exports.lineNameProblem = lineNameProblem;
|
|
8
8
|
/**
|
|
9
9
|
* A local history: the `.cbx/` folder at the top of a project.
|
|
10
10
|
*
|
|
11
|
-
* .cbx/format "cbx-local 1", so a later layout can tell an older one
|
|
11
|
+
* .cbx/format "cbx-local 1" or "cbx-local 2", so a later layout can tell an older one
|
|
12
12
|
* .cbx/objects/ the object store (see store.ts)
|
|
13
13
|
* .cbx/lines/<name> the id of the newest save on each line of work
|
|
14
14
|
* .cbx/state.json which line the folder is on
|
|
@@ -28,6 +28,15 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
28
28
|
const store_js_1 = require("./store.js");
|
|
29
29
|
exports.STORE_DIRECTORY = ".cbx";
|
|
30
30
|
exports.FORMAT_LINE = "cbx-local 1";
|
|
31
|
+
/*
|
|
32
|
+
The same objects, saves and trees, kept as solid blocks and deltas
|
|
33
|
+
(store_v2.ts, engine/V2-STORE.md). Read by every cbx that knows it; an
|
|
34
|
+
older one refuses the history and asks to be updated rather than misread it.
|
|
35
|
+
New histories are made in it only when CBX_LOCAL_FORMAT=2, until it has
|
|
36
|
+
proved itself on the benchmark suite.
|
|
37
|
+
*/
|
|
38
|
+
exports.FORMAT_LINE_V2 = "cbx-local 2";
|
|
39
|
+
const FORMATS = new Set([exports.FORMAT_LINE, exports.FORMAT_LINE_V2]);
|
|
31
40
|
exports.DEFAULT_LINE = "main";
|
|
32
41
|
/**
|
|
33
42
|
* Line names are file names under `.cbx/lines`, so they are held to what is
|
|
@@ -69,10 +78,10 @@ class Repository {
|
|
|
69
78
|
root;
|
|
70
79
|
directory;
|
|
71
80
|
objects;
|
|
72
|
-
constructor(root) {
|
|
81
|
+
constructor(root, format = exports.FORMAT_LINE) {
|
|
73
82
|
this.root = root;
|
|
74
83
|
this.directory = node_path_1.default.join(root, exports.STORE_DIRECTORY);
|
|
75
|
-
this.objects = new store_js_1.ObjectStore(node_path_1.default.join(this.directory, "objects"));
|
|
84
|
+
this.objects = new store_js_1.ObjectStore(node_path_1.default.join(this.directory, "objects"), { v2: format === exports.FORMAT_LINE_V2 });
|
|
76
85
|
}
|
|
77
86
|
/** Start a history in `root`. Refuses a folder that already has one. */
|
|
78
87
|
static async init(root, line = exports.DEFAULT_LINE) {
|
|
@@ -86,10 +95,11 @@ class Repository {
|
|
|
86
95
|
}
|
|
87
96
|
await (0, promises_1.mkdir)(node_path_1.default.join(directory, "objects"), { recursive: true });
|
|
88
97
|
await (0, promises_1.mkdir)(node_path_1.default.join(directory, "lines"), { recursive: true });
|
|
89
|
-
|
|
98
|
+
const format = process.env.CBX_LOCAL_FORMAT === "2" ? exports.FORMAT_LINE_V2 : exports.FORMAT_LINE;
|
|
99
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(directory, "format"), `${format}\n`);
|
|
90
100
|
await (0, promises_1.writeFile)(node_path_1.default.join(directory, "state.json"), JSON.stringify({ line }));
|
|
91
101
|
await (0, promises_1.writeFile)(node_path_1.default.join(directory, "config.json"), "{}");
|
|
92
|
-
return new Repository(full);
|
|
102
|
+
return new Repository(full, format);
|
|
93
103
|
}
|
|
94
104
|
/** The history `from` is inside, looking upward; null when there is none. */
|
|
95
105
|
static async find(from) {
|
|
@@ -98,10 +108,10 @@ class Repository {
|
|
|
98
108
|
const directory = node_path_1.default.join(at, exports.STORE_DIRECTORY);
|
|
99
109
|
if (await exists(node_path_1.default.join(directory, "format"))) {
|
|
100
110
|
const format = (await (0, promises_1.readFile)(node_path_1.default.join(directory, "format"), "utf8")).trim();
|
|
101
|
-
if (format
|
|
111
|
+
if (!FORMATS.has(format)) {
|
|
102
112
|
throw new Error(`${directory} is in a format this cbx does not read (${format}). Update cbx.`);
|
|
103
113
|
}
|
|
104
|
-
return new Repository(at);
|
|
114
|
+
return new Repository(at, format);
|
|
105
115
|
}
|
|
106
116
|
const parent = node_path_1.default.dirname(at);
|
|
107
117
|
if (parent === at)
|
|
@@ -218,7 +228,13 @@ class Repository {
|
|
|
218
228
|
const id = await this.objects.putJson(canonical);
|
|
219
229
|
return { ...canonical, id };
|
|
220
230
|
}
|
|
221
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Keep a tree. In a v2 history it goes into a pack as a delta against
|
|
233
|
+
* `base`, the tree it was made from: a tree names every file, so twenty
|
|
234
|
+
* saves of an 8,500-file project kept twenty megabyte-sized lists, where
|
|
235
|
+
* what changed between them was a few lines.
|
|
236
|
+
*/
|
|
237
|
+
async writeTree(files, base) {
|
|
222
238
|
const sorted = [...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
223
239
|
const tree = {
|
|
224
240
|
format: "cbx-tree",
|
|
@@ -231,6 +247,20 @@ class Repository {
|
|
|
231
247
|
chunks: file.chunks,
|
|
232
248
|
})),
|
|
233
249
|
};
|
|
250
|
+
if (this.objects.v2) {
|
|
251
|
+
const id = await this.objects.put(Buffer.from(JSON.stringify(tree), "utf8"), {
|
|
252
|
+
...(base ? { base } : {}),
|
|
253
|
+
path: ".cbx-tree",
|
|
254
|
+
/*
|
|
255
|
+
Every status rebuilds the head tree, so its chain is kept short: four
|
|
256
|
+
deltas of a megabyte-sized list is a few milliseconds, twelve was not.
|
|
257
|
+
*/
|
|
258
|
+
maxChain: 4,
|
|
259
|
+
});
|
|
260
|
+
/* On disk before any save can name it: a save is written on its own, at once. */
|
|
261
|
+
await this.objects.flush();
|
|
262
|
+
return id;
|
|
263
|
+
}
|
|
234
264
|
return this.objects.putJson(tree);
|
|
235
265
|
}
|
|
236
266
|
/**
|
|
@@ -281,17 +311,66 @@ class Repository {
|
|
|
281
311
|
return save;
|
|
282
312
|
}
|
|
283
313
|
/* ---- the index ------------------------------------------------------ */
|
|
314
|
+
/*
|
|
315
|
+
What a compact index last said about the folder as a whole: the save it
|
|
316
|
+
matched and whether it matched it exactly. Written by status, read by the
|
|
317
|
+
engine's quiet status (engine/src/quick.rs), which answers "nothing has
|
|
318
|
+
changed" from it after a walk, without Node. Any other write leaves it out.
|
|
319
|
+
*/
|
|
320
|
+
stamp = null;
|
|
321
|
+
get indexStamp() {
|
|
322
|
+
return this.stamp;
|
|
323
|
+
}
|
|
284
324
|
async readIndex() {
|
|
325
|
+
this.stamp = null;
|
|
285
326
|
try {
|
|
286
327
|
const raw = JSON.parse(await (0, promises_1.readFile)(node_path_1.default.join(this.directory, "index.json"), "utf8"));
|
|
328
|
+
if (raw.compact === 1) {
|
|
329
|
+
const told = raw.stamp;
|
|
330
|
+
if (told && (typeof told.head === "string" || told.head === null) && typeof told.clean === "boolean") {
|
|
331
|
+
this.stamp = { head: told.head, clean: told.clean, ...(told.complete === true ? { complete: true } : {}) };
|
|
332
|
+
}
|
|
333
|
+
const index = new Map();
|
|
334
|
+
for (const [file, size, mtimeMs, sha256, chunks, runs] of raw.files) {
|
|
335
|
+
index.set(file, {
|
|
336
|
+
size,
|
|
337
|
+
mtimeMs,
|
|
338
|
+
sha256,
|
|
339
|
+
chunks: chunks === 0 ? [sha256] : chunks,
|
|
340
|
+
...(runs === undefined ? {} : { executable: runs === 1 }),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return index;
|
|
344
|
+
}
|
|
287
345
|
return new Map(Object.entries(raw));
|
|
288
346
|
}
|
|
289
347
|
catch {
|
|
290
348
|
return new Map();
|
|
291
349
|
}
|
|
292
350
|
}
|
|
293
|
-
|
|
294
|
-
|
|
351
|
+
/*
|
|
352
|
+
A v2 history keeps the index compact: one array per file, and no list of
|
|
353
|
+
objects for a file that is one object named by its own digest, which is
|
|
354
|
+
nearly every file. It was 2.1 MB for 8,500 files, read and parsed on
|
|
355
|
+
every status. A v1 history keeps the old shape, which an older cbx reads.
|
|
356
|
+
*/
|
|
357
|
+
async writeIndex(index, stamp) {
|
|
358
|
+
this.stamp = stamp && this.objects.v2 ? stamp : null;
|
|
359
|
+
const body = this.objects.v2
|
|
360
|
+
? JSON.stringify({
|
|
361
|
+
compact: 1,
|
|
362
|
+
...(stamp ? { stamp } : {}),
|
|
363
|
+
files: [...index].map(([file, entry]) => [
|
|
364
|
+
file,
|
|
365
|
+
entry.size,
|
|
366
|
+
entry.mtimeMs,
|
|
367
|
+
entry.sha256,
|
|
368
|
+
entry.chunks.length === 1 && entry.chunks[0] === entry.sha256 ? 0 : entry.chunks,
|
|
369
|
+
...(entry.executable === undefined ? [] : [entry.executable ? 1 : 0]),
|
|
370
|
+
]),
|
|
371
|
+
})
|
|
372
|
+
: JSON.stringify(Object.fromEntries(index));
|
|
373
|
+
await writeAtomically(node_path_1.default.join(this.directory, "index.json"), body);
|
|
295
374
|
}
|
|
296
375
|
/* ---- the lock ------------------------------------------------------- */
|
|
297
376
|
/**
|
|
@@ -110,13 +110,65 @@ async function digestFile(full, size) {
|
|
|
110
110
|
* that size, so an edit in the middle of a large asset stores the pieces
|
|
111
111
|
* around the edit and reuses the rest.
|
|
112
112
|
*/
|
|
113
|
-
async function storeFile(repository, full, size
|
|
113
|
+
async function storeFile(repository, full, size,
|
|
114
|
+
/*
|
|
115
|
+
For a v2 history: where the file sits, and what it was last time (or,
|
|
116
|
+
for a new file, a file like it), so its objects can be kept as deltas.
|
|
117
|
+
*/
|
|
118
|
+
hints) {
|
|
114
119
|
const profile = (0, chunking_js_1.profileForFileSize)(size);
|
|
115
120
|
if (!profile) {
|
|
116
121
|
const bytes = await (0, promises_1.readFile)(full);
|
|
117
|
-
const
|
|
122
|
+
const was = hints?.previous?.chunks.length === 1 ? hints.previous : hints?.similar;
|
|
123
|
+
const base = was?.chunks.length === 1 ? was.chunks[0] : undefined;
|
|
124
|
+
const digest = await repository.objects.put(bytes, {
|
|
125
|
+
...(base ? { base } : {}),
|
|
126
|
+
...(hints ? { path: hints.path } : {}),
|
|
127
|
+
});
|
|
118
128
|
return { sha256: digest, chunks: bytes.length ? [digest] : [] };
|
|
119
129
|
}
|
|
130
|
+
/*
|
|
131
|
+
The old pieces by where they started, so each new piece is kept as a
|
|
132
|
+
delta against the old piece at the same place: an edit inside a big file
|
|
133
|
+
changes the pieces around it, and those resemble what they replaced.
|
|
134
|
+
*/
|
|
135
|
+
const before = hints?.previous?.chunks ?? [];
|
|
136
|
+
const starts = [];
|
|
137
|
+
if (before.length && repository.objects.v2) {
|
|
138
|
+
let at = 0;
|
|
139
|
+
for (const digest of before) {
|
|
140
|
+
starts.push(at);
|
|
141
|
+
const length = await repository.objects.rawSize(digest);
|
|
142
|
+
if (length === null) {
|
|
143
|
+
starts.length = 0;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
at += length;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const baseAt = (offset) => {
|
|
150
|
+
if (!starts.length)
|
|
151
|
+
return undefined;
|
|
152
|
+
let low = 0;
|
|
153
|
+
let high = starts.length - 1;
|
|
154
|
+
while (low < high) {
|
|
155
|
+
const middle = (low + high + 1) >> 1;
|
|
156
|
+
if (starts[middle] <= offset)
|
|
157
|
+
low = middle;
|
|
158
|
+
else
|
|
159
|
+
high = middle - 1;
|
|
160
|
+
}
|
|
161
|
+
return before[low];
|
|
162
|
+
};
|
|
163
|
+
let written = 0;
|
|
164
|
+
const keep = async (piece) => {
|
|
165
|
+
const base = baseAt(written);
|
|
166
|
+
written += piece.length;
|
|
167
|
+
return repository.objects.put(piece, {
|
|
168
|
+
...(base ? { base } : {}),
|
|
169
|
+
...(hints ? { path: hints.path } : {}),
|
|
170
|
+
});
|
|
171
|
+
};
|
|
120
172
|
const whole = (0, node_crypto_1.createHash)("sha256");
|
|
121
173
|
const chunks = [];
|
|
122
174
|
let pending = Buffer.alloc(0);
|
|
@@ -126,13 +178,13 @@ async function storeFile(repository, full, size) {
|
|
|
126
178
|
pending = pending.length ? Buffer.concat([pending, block]) : Buffer.from(block);
|
|
127
179
|
let from = 0;
|
|
128
180
|
for (const cut of (0, chunking_js_1.cutPoints)(pending, profile)) {
|
|
129
|
-
chunks.push(await
|
|
181
|
+
chunks.push(await keep(Buffer.from(pending.subarray(from, cut))));
|
|
130
182
|
from = cut;
|
|
131
183
|
}
|
|
132
184
|
pending = pending.subarray(from);
|
|
133
185
|
}
|
|
134
186
|
if (pending.length)
|
|
135
|
-
chunks.push(await
|
|
187
|
+
chunks.push(await keep(Buffer.from(pending)));
|
|
136
188
|
return { sha256: whole.digest("hex"), chunks };
|
|
137
189
|
}
|
|
138
190
|
/**
|
|
@@ -169,7 +221,11 @@ async function readFolder(repository, options) {
|
|
|
169
221
|
let native = null;
|
|
170
222
|
let done = listed.length - toRead.length;
|
|
171
223
|
const readBytes = toRead.reduce((sum, file) => sum + file.size, 0);
|
|
172
|
-
|
|
224
|
+
/*
|
|
225
|
+
Not to store into a v2 history: the engine writes v1 packs, so what it
|
|
226
|
+
stored would miss the solid blocks and deltas. It still measures there.
|
|
227
|
+
*/
|
|
228
|
+
if ((0, native_engine_js_1.worthTheEngine)(toRead.length, readBytes) && !(options.store && repository.objects.v2)) {
|
|
173
229
|
const report = options.progress
|
|
174
230
|
? (count, _total, file) => options.progress(done + count, listed.length, file)
|
|
175
231
|
: undefined;
|
|
@@ -201,15 +257,16 @@ async function readFolder(repository, options) {
|
|
|
201
257
|
return null;
|
|
202
258
|
};
|
|
203
259
|
const record = (file, sha256, chunks, settled) => {
|
|
260
|
+
const executable = executableFrom(file.mode, previous.get(file.path));
|
|
204
261
|
files.set(file.path, {
|
|
205
262
|
path: file.path,
|
|
206
263
|
size: file.size,
|
|
207
264
|
sha256,
|
|
208
|
-
executable
|
|
265
|
+
executable,
|
|
209
266
|
chunks,
|
|
210
267
|
});
|
|
211
268
|
if (file.mtimeMs < racyBefore && settled) {
|
|
212
|
-
next.set(file.path, { size: file.size, mtimeMs: file.mtimeMs, sha256, chunks });
|
|
269
|
+
next.set(file.path, { size: file.size, mtimeMs: file.mtimeMs, sha256, chunks, executable });
|
|
213
270
|
}
|
|
214
271
|
};
|
|
215
272
|
/*
|
|
@@ -231,12 +288,50 @@ async function readFolder(repository, options) {
|
|
|
231
288
|
options.progress?.(done, listed.length, file.path);
|
|
232
289
|
}
|
|
233
290
|
}
|
|
291
|
+
/*
|
|
292
|
+
A new file is kept against the file most like it the last save held:
|
|
293
|
+
one with the same extension in the same folder, nearest in size, and
|
|
294
|
+
within a factor of two. A copy made and edited is then a few bytes.
|
|
295
|
+
*/
|
|
296
|
+
let alike = null;
|
|
297
|
+
const kindOf = (filePath) => {
|
|
298
|
+
const slash = filePath.lastIndexOf("/");
|
|
299
|
+
const name = filePath.slice(slash + 1);
|
|
300
|
+
const dot = name.lastIndexOf(".");
|
|
301
|
+
return `${filePath.slice(0, slash + 1)}\u0000${dot > 0 ? name.slice(dot).toLowerCase() : ""}`;
|
|
302
|
+
};
|
|
303
|
+
const hintsFor = (file) => {
|
|
304
|
+
const was = previous.get(file.path);
|
|
305
|
+
if (was)
|
|
306
|
+
return { previous: was };
|
|
307
|
+
if (!alike) {
|
|
308
|
+
alike = new Map();
|
|
309
|
+
for (const entry of previous.values()) {
|
|
310
|
+
if (entry.chunks.length !== 1)
|
|
311
|
+
continue;
|
|
312
|
+
const key = kindOf(entry.path);
|
|
313
|
+
const list = alike.get(key);
|
|
314
|
+
if (list)
|
|
315
|
+
list.push(entry);
|
|
316
|
+
else
|
|
317
|
+
alike.set(key, [entry]);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
let best;
|
|
321
|
+
for (const entry of alike.get(kindOf(file.path)) ?? []) {
|
|
322
|
+
if (entry.size * 2 < file.size || entry.size > file.size * 2)
|
|
323
|
+
continue;
|
|
324
|
+
if (!best || Math.abs(entry.size - file.size) < Math.abs(best.size - file.size))
|
|
325
|
+
best = entry;
|
|
326
|
+
}
|
|
327
|
+
return best ? { similar: best } : {};
|
|
328
|
+
};
|
|
234
329
|
const one = async (file) => {
|
|
235
330
|
const full = node_path_1.default.join(repository.root, file.path);
|
|
236
331
|
let sha256;
|
|
237
332
|
let chunks = [];
|
|
238
333
|
if (options.store) {
|
|
239
|
-
({ sha256, chunks } = await storeFile(repository, full, file.size));
|
|
334
|
+
({ sha256, chunks } = await storeFile(repository, full, file.size, repository.objects.v2 ? { path: file.path, ...hintsFor(file) } : undefined));
|
|
240
335
|
}
|
|
241
336
|
else {
|
|
242
337
|
sha256 = await digestFile(full, file.size);
|
|
@@ -251,13 +346,19 @@ async function readFolder(repository, options) {
|
|
|
251
346
|
inLanes(toReadHere.filter(large), LARGE_LANES, one),
|
|
252
347
|
]);
|
|
253
348
|
/* Nothing may name an object that is not on disk yet. */
|
|
254
|
-
|
|
349
|
+
/*
|
|
350
|
+
A v2 history writes the files with the tree that names them, in one pack:
|
|
351
|
+
the save flushes after its tree (writeTree), so a pack is not spent on
|
|
352
|
+
the tree alone.
|
|
353
|
+
*/
|
|
354
|
+
if (options.store && !repository.objects.v2)
|
|
255
355
|
await repository.objects.flush();
|
|
256
356
|
options.progress?.(listed.length, listed.length, "");
|
|
257
357
|
const sameEntry = (left, right) => Boolean(right) &&
|
|
258
358
|
left.size === right.size &&
|
|
259
359
|
left.mtimeMs === right.mtimeMs &&
|
|
260
360
|
left.sha256 === right.sha256 &&
|
|
361
|
+
left.executable === right.executable &&
|
|
261
362
|
left.chunks.length === right.chunks.length &&
|
|
262
363
|
left.chunks.every((chunk, at) => chunk === right.chunks[at]);
|
|
263
364
|
const indexChanged = next.size !== index.size || [...next].some(([file, entry]) => !sameEntry(entry, index.get(file)));
|