@coderook/cli 0.29.5 → 0.30.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/cli/src/cli.js +93 -7
- package/dist/desktop-app/src/main/detect.js +102 -18
- package/dist/desktop-app/src/main/rules.js +48 -11
- package/dist/desktop-app/src/main/transfer_progress.js +109 -0
- package/dist/desktop-app/src/main/upload.js +617 -559
- package/dist/desktop-app/src/main/worktree.js +266 -5
- package/package.json +1 -1
|
@@ -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, from Claude Code.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.30.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -35,6 +35,7 @@ const skill_command_js_1 = require("./skill_command.js");
|
|
|
35
35
|
const licence_commands_js_1 = require("./licence_commands.js");
|
|
36
36
|
const service_commands_js_1 = require("./service_commands.js");
|
|
37
37
|
const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
|
|
38
|
+
const rules_js_1 = require("../../desktop-app/src/main/rules.js");
|
|
38
39
|
const upload_js_1 = require("../../desktop-app/src/main/upload.js");
|
|
39
40
|
const download_js_1 = require("../../desktop-app/src/main/download.js");
|
|
40
41
|
const faults_js_1 = require("../../desktop-app/src/main/faults.js");
|
|
@@ -349,7 +350,22 @@ async function commandStatus(parsed) {
|
|
|
349
350
|
console.log(dim("Offline — comparing with the last Version cached for this folder."));
|
|
350
351
|
}
|
|
351
352
|
const rules = await (0, worktree_js_1.readRules)(folder);
|
|
352
|
-
|
|
353
|
+
/*
|
|
354
|
+
Scanned the way `cbx diff` scans, and reported the way `cbx submit` acts.
|
|
355
|
+
|
|
356
|
+
Status used to ask only for additions and updates, which is exactly what a
|
|
357
|
+
plain save sends — so it was not wrong. But a deleted file then appeared
|
|
358
|
+
nowhere: `cbx diff` showed it as removed, `cbx status` said nothing, and
|
|
359
|
+
the two commands disagreed about the same folder. Somebody who had deleted
|
|
360
|
+
a file read that as CodeRook not having noticed it.
|
|
361
|
+
|
|
362
|
+
So deletions are found and named here, kept apart from what a save will
|
|
363
|
+
send, with the flag that would send them spelled out. The safe default
|
|
364
|
+
does not change: a save adds and updates.
|
|
365
|
+
*/
|
|
366
|
+
const found = await (0, worktree_js_1.changedFiles)(folder, rules, baseline, "synchronize");
|
|
367
|
+
const gone = found.filter((file) => file.deleted);
|
|
368
|
+
const files = found.filter((file) => !file.deleted);
|
|
353
369
|
console.log(bold(node_path_1.default.basename(folder)) + dim(` ${folder}`));
|
|
354
370
|
console.log(link
|
|
355
371
|
? `Account holds ${accent(`v${link.sequence}`)} · ${Object.keys(link.manifest).length} files ${dim(`(${link.slug})`)}`
|
|
@@ -370,18 +386,85 @@ async function commandStatus(parsed) {
|
|
|
370
386
|
for (const [path] of stale.slice(0, 10))
|
|
371
387
|
console.log(dim(` behind ${path}`));
|
|
372
388
|
}
|
|
389
|
+
/** The deletions, and what a plain save will do about them. */
|
|
390
|
+
/*
|
|
391
|
+
What this folder is, and what its own rules never heard about.
|
|
392
|
+
|
|
393
|
+
A real .gitignore always wins — that is not negotiable — but it also meant
|
|
394
|
+
that a Unity project whose file was written before the engine rules
|
|
395
|
+
existed went on sending its entire import cache, and nothing anywhere
|
|
396
|
+
said so. One real upload of forty-seven thousand files was forty-five
|
|
397
|
+
thousand of rebuildable cache, and it broke the project on the machine it
|
|
398
|
+
was downloaded to.
|
|
399
|
+
*/
|
|
400
|
+
const engine = async () => {
|
|
401
|
+
const { kind, missing } = await (0, worktree_js_1.missingEngineRules)(folder);
|
|
402
|
+
if (!kind.length || !missing.length)
|
|
403
|
+
return;
|
|
404
|
+
/*
|
|
405
|
+
Only the rules that would actually leave something out.
|
|
406
|
+
|
|
407
|
+
Naming every line the template has and this folder does not produced
|
|
408
|
+
sixty-odd of them, most matching nothing at all — a Visual Studio
|
|
409
|
+
template mentions `[Dd]ebug/x64/` whether or not you have one. What a
|
|
410
|
+
person needs is the two that are costing them forty thousand files, so
|
|
411
|
+
each candidate is measured against what this folder is currently
|
|
412
|
+
sending and the ones worth nothing are not mentioned.
|
|
413
|
+
*/
|
|
414
|
+
const sending = found.map((one) => one.path);
|
|
415
|
+
const worth = missing
|
|
416
|
+
.map((rule) => {
|
|
417
|
+
const layers = [{ base: "", rules: (0, rules_js_1.parseRules)(rule) }];
|
|
418
|
+
return {
|
|
419
|
+
rule,
|
|
420
|
+
files: sending.filter((one) => (0, rules_js_1.excludes)(one, false, layers)).length,
|
|
421
|
+
};
|
|
422
|
+
})
|
|
423
|
+
.filter((one) => one.files > 0)
|
|
424
|
+
.sort((left, right) => right.files - left.files);
|
|
425
|
+
if (!worth.length)
|
|
426
|
+
return;
|
|
427
|
+
const total = worth.reduce((sum, one) => sum + one.files, 0);
|
|
428
|
+
console.log(dim(`\nThis looks like a ${kind.join(" + ")} project, and its own rules are ` +
|
|
429
|
+
`missing ${worth.length} that would leave out ` +
|
|
430
|
+
`${total.toLocaleString()} file${total === 1 ? "" : "s"} you are sending — ` +
|
|
431
|
+
`${accent("cbx ignore --template " + kind[0])} adds them.`));
|
|
432
|
+
for (const one of worth.slice(0, 6)) {
|
|
433
|
+
console.log(dim(` ${String(one.files).padStart(7)} ${one.rule}`));
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const deletions = () => {
|
|
437
|
+
if (!gone.length)
|
|
438
|
+
return;
|
|
439
|
+
const many = gone.length === 1 ? "" : "s";
|
|
440
|
+
console.log(dim(`\n${gone.length} file${many} deleted here ${gone.length === 1 ? "is" : "are"} ` +
|
|
441
|
+
`still in the saved version. A save adds and updates, so ` +
|
|
442
|
+
`${gone.length === 1 ? "it stays" : "they stay"} until you pass ` +
|
|
443
|
+
`${accent("--sync")}.`));
|
|
444
|
+
for (const file of gone.slice(0, 10)) {
|
|
445
|
+
console.log(` ${red("deleted")} ${file.path}`);
|
|
446
|
+
}
|
|
447
|
+
if (gone.length > 10)
|
|
448
|
+
console.log(dim(` …and ${gone.length - 10} more`));
|
|
449
|
+
};
|
|
373
450
|
if (!files.length) {
|
|
374
451
|
console.log(stale.length
|
|
375
452
|
? "Nothing to submit; your own work is all saved."
|
|
376
|
-
:
|
|
453
|
+
: gone.length
|
|
454
|
+
? "Nothing here to add or update."
|
|
455
|
+
: "Nothing to submit; this folder matches the saved version.");
|
|
456
|
+
deletions();
|
|
457
|
+
await engine();
|
|
377
458
|
return 0;
|
|
378
459
|
}
|
|
379
460
|
console.log(`\n${files.length} file${files.length === 1 ? "" : "s"} to submit:`);
|
|
380
461
|
for (const file of files.slice(0, 50)) {
|
|
381
|
-
console.log(` ${
|
|
462
|
+
console.log(` ${accent("changed")} ${file.path}`);
|
|
382
463
|
}
|
|
383
464
|
if (files.length > 50)
|
|
384
465
|
console.log(dim(` …and ${files.length - 50} more`));
|
|
466
|
+
deletions();
|
|
467
|
+
await engine();
|
|
385
468
|
return 0;
|
|
386
469
|
}
|
|
387
470
|
/**
|
|
@@ -1311,11 +1394,14 @@ async function commandRules(parsed) {
|
|
|
1311
1394
|
return suggestRules(folder, hasFlag(parsed, "apply"));
|
|
1312
1395
|
}
|
|
1313
1396
|
if (hasFlag(parsed, "init")) {
|
|
1397
|
+
/*
|
|
1398
|
+
Whatever `readRules` decided, which is now the template that matches the
|
|
1399
|
+
folder rather than one generic list. Writing STARTER_IGNORE here would
|
|
1400
|
+
have handed a Unity project the JavaScript rules, which is how an upload
|
|
1401
|
+
came back missing things inside Assets.
|
|
1402
|
+
*/
|
|
1314
1403
|
const existing = await (0, worktree_js_1.readRules)(folder);
|
|
1315
|
-
await (0, worktree_js_1.writeRules)(folder,
|
|
1316
|
-
shared: existing.shared.trim() ? existing.shared : worktree_js_1.STARTER_IGNORE,
|
|
1317
|
-
local: existing.local,
|
|
1318
|
-
});
|
|
1404
|
+
await (0, worktree_js_1.writeRules)(folder, existing);
|
|
1319
1405
|
console.log(`Wrote ${node_path_1.default.join(folder, ".gitignore")}`);
|
|
1320
1406
|
return 0;
|
|
1321
1407
|
}
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.STARTER_TOPUP_HEADING = exports.SUGGESTED_HEADING = void 0;
|
|
7
|
+
exports.templatesFor = templatesFor;
|
|
7
8
|
exports.suggestExclusions = suggestExclusions;
|
|
8
9
|
exports.suggestBulkDirectories = suggestBulkDirectories;
|
|
9
10
|
exports.rulesFromSuggestions = rulesFromSuggestions;
|
|
@@ -59,24 +60,24 @@ const RULES = [
|
|
|
59
60
|
project where the question "what should I leave out?" has the largest
|
|
60
61
|
possible answer was the one the detector had nothing to say about.
|
|
61
62
|
*/
|
|
62
|
-
{ directory: "Library", reason: "Unity's import cache — rebuilt when the project next opens", recommended: true },
|
|
63
|
-
{ directory: "Temp", reason: "Unity's scratch folder for the running editor", recommended: true },
|
|
64
|
-
{ directory: "MemoryCaptures", reason: "Unity memory snapshots — large, and not the project", recommended: true },
|
|
65
|
-
{ directory: "Recordings", reason: "Unity recorder output", recommended: true },
|
|
66
|
-
{ directory: "UserSettings", reason: "Your own editor layout — not part of the project", recommended: true },
|
|
67
|
-
{ directory: "DerivedDataCache", reason: "Unreal's derived data cache — rebuilt on demand", recommended: true },
|
|
68
|
-
{ directory: "Intermediate", reason: "Unreal build intermediates — rebuilt when you build", recommended: true },
|
|
69
|
-
{ directory: "Binaries", reason: "Unreal compiled output — rebuilt when you build", recommended: true },
|
|
63
|
+
{ directory: "Library", atRoot: true, reason: "Unity's import cache — rebuilt when the project next opens", recommended: true },
|
|
64
|
+
{ directory: "Temp", atRoot: true, reason: "Unity's scratch folder for the running editor", recommended: true },
|
|
65
|
+
{ directory: "MemoryCaptures", atRoot: true, reason: "Unity memory snapshots — large, and not the project", recommended: true },
|
|
66
|
+
{ directory: "Recordings", atRoot: true, reason: "Unity recorder output", recommended: true },
|
|
67
|
+
{ directory: "UserSettings", atRoot: true, reason: "Your own editor layout — not part of the project", recommended: true },
|
|
68
|
+
{ directory: "DerivedDataCache", atRoot: true, reason: "Unreal's derived data cache — rebuilt on demand", recommended: true },
|
|
69
|
+
{ directory: "Intermediate", atRoot: true, reason: "Unreal build intermediates — rebuilt when you build", recommended: true },
|
|
70
|
+
{ directory: "Binaries", atRoot: true, reason: "Unreal compiled output — rebuilt when you build", recommended: true },
|
|
70
71
|
/*
|
|
71
72
|
Named like the work, and it is not: Unreal keeps logs, crash reports and
|
|
72
73
|
autosaves here. Offered rather than ticked, because a folder called
|
|
73
74
|
"Saved" is the one nobody should have excluded on our say-so.
|
|
74
75
|
*/
|
|
75
|
-
{ directory: "Saved", reason: "Unreal logs and autosaves — check before excluding", recommended: false },
|
|
76
|
-
{ directory: ".godot", reason: "Godot's import cache — rebuilt when the project next opens", recommended: true },
|
|
77
|
-
{ directory: ".import", reason: "Godot's import cache — rebuilt when the project next opens", recommended: true },
|
|
78
|
-
{ directory: "Logs", reason: "Editor logs", recommended: true },
|
|
79
|
-
{ directory: "obj", reason: "Compiler intermediates — rebuilt when you build", recommended: true },
|
|
76
|
+
{ directory: "Saved", atRoot: true, reason: "Unreal logs and autosaves — check before excluding", recommended: false },
|
|
77
|
+
{ directory: ".godot", atRoot: true, reason: "Godot's import cache — rebuilt when the project next opens", recommended: true },
|
|
78
|
+
{ directory: ".import", atRoot: true, reason: "Godot's import cache — rebuilt when the project next opens", recommended: true },
|
|
79
|
+
{ directory: "Logs", atRoot: true, reason: "Editor logs", recommended: true },
|
|
80
|
+
{ directory: "obj", atRoot: true, reason: "Compiler intermediates — rebuilt when you build", recommended: true },
|
|
80
81
|
{ directory: ".vs", reason: "Visual Studio's local cache", recommended: true },
|
|
81
82
|
/*
|
|
82
83
|
Offered, not ticked. JetBrains keeps run configurations in here that some
|
|
@@ -103,7 +104,7 @@ const RULES = [
|
|
|
103
104
|
{ directory: "dist", reason: "Usually build output — check before excluding", recommended: false },
|
|
104
105
|
{ directory: "build", reason: "Usually build output — check before excluding", recommended: false },
|
|
105
106
|
{ directory: "out", reason: "Usually build output — check before excluding", recommended: false },
|
|
106
|
-
{ directory: "bin", reason: "Usually build output — check before excluding", recommended: false },
|
|
107
|
+
{ directory: "bin", atRoot: true, reason: "Usually build output — check before excluding", recommended: false },
|
|
107
108
|
{ extension: ".safetensors", reason: "Model weights — large, and usually downloadable", recommended: false },
|
|
108
109
|
{ extension: ".ckpt", reason: "Model weights — large, and usually downloadable", recommended: false },
|
|
109
110
|
{ extension: ".pt", reason: "Model weights — large, and usually downloadable", recommended: false },
|
|
@@ -122,6 +123,83 @@ const norm = (value) => value.split("\\").join("/");
|
|
|
122
123
|
* their upload will take and the biggest item answers it. Anything matching
|
|
123
124
|
* nothing is simply absent — silence is the right output for a tidy folder.
|
|
124
125
|
*/
|
|
126
|
+
/**
|
|
127
|
+
* Which of github/gitignore's templates this folder is asking for.
|
|
128
|
+
*
|
|
129
|
+
* The templates have been vendored all along — three hundred of them, taken
|
|
130
|
+
* from github/gitignore — but nothing ever chose one. A folder with no rules
|
|
131
|
+
* of its own got the generic starter instead, and for a game project that is
|
|
132
|
+
* the wrong list: it carries `build/`, `dist/`, `out/` and `models/**` from
|
|
133
|
+
* the JavaScript and Python worlds, none of which are anchored, all of which
|
|
134
|
+
* can reach into somewhere like `Assets/Art/models`.
|
|
135
|
+
*
|
|
136
|
+
* Recognised from the files a project must have to be that kind of project,
|
|
137
|
+
* rather than from anything a person configures. Order matters: an engine is
|
|
138
|
+
* decided first, because a Unity project also has a package.json somewhere
|
|
139
|
+
* and the engine's own template is the one that knows about `Assets`.
|
|
140
|
+
*/
|
|
141
|
+
function templatesFor(entries) {
|
|
142
|
+
const here = new Set(entries.map((name) => name.toLowerCase()));
|
|
143
|
+
const any = (test) => entries.some(test);
|
|
144
|
+
const picked = [];
|
|
145
|
+
const take = (key) => {
|
|
146
|
+
if (!picked.includes(key))
|
|
147
|
+
picked.push(key);
|
|
148
|
+
};
|
|
149
|
+
/* An engine first, and at most one: their templates overlap and disagree. */
|
|
150
|
+
if (here.has("assets") && here.has("projectsettings"))
|
|
151
|
+
take("unity");
|
|
152
|
+
else if (any((name) => name.toLowerCase().endsWith(".uproject"))) {
|
|
153
|
+
take("unrealengine");
|
|
154
|
+
}
|
|
155
|
+
else if (here.has("project.godot"))
|
|
156
|
+
take("godot");
|
|
157
|
+
/*
|
|
158
|
+
Cocos before Node, and this order is the whole reason it is written down.
|
|
159
|
+
|
|
160
|
+
A Cocos project carries a package.json, so recognising Node first called
|
|
161
|
+
it a Node project and handed it Node's rules — which know nothing about
|
|
162
|
+
`library` or `temp`, the caches Cocos rebuilds. Measured on the soak
|
|
163
|
+
shape: eight and a half thousand cache files went from left out to sent.
|
|
164
|
+
*/
|
|
165
|
+
else if (here.has("project.json") && here.has("assets"))
|
|
166
|
+
take("cocos");
|
|
167
|
+
/* Then the languages, of which a folder may honestly be several. */
|
|
168
|
+
if (here.has("package.json"))
|
|
169
|
+
take("node");
|
|
170
|
+
if (here.has("pyproject.toml") ||
|
|
171
|
+
here.has("requirements.txt") ||
|
|
172
|
+
here.has("setup.py")) {
|
|
173
|
+
take("python");
|
|
174
|
+
}
|
|
175
|
+
if (here.has("cargo.toml"))
|
|
176
|
+
take("rust");
|
|
177
|
+
if (here.has("go.mod"))
|
|
178
|
+
take("go");
|
|
179
|
+
if (here.has("build.gradle") || here.has("build.gradle.kts"))
|
|
180
|
+
take("gradle");
|
|
181
|
+
/*
|
|
182
|
+
An engine's own project files are not a second kind of project.
|
|
183
|
+
|
|
184
|
+
Unity writes the .sln and the .csproj itself, and every engine here does
|
|
185
|
+
something similar. Reading that as "also a Visual Studio project" and
|
|
186
|
+
stacking GitHub's template on top is not a harmless extra: that template
|
|
187
|
+
ignores `*.meta`, which in the .NET world is build output and in Unity is
|
|
188
|
+
the sidecar carrying every asset's GUID. On a real project it would have
|
|
189
|
+
dropped 13,689 of them, and a Unity project without its metas is exactly
|
|
190
|
+
the broken-materials mess this was all started by.
|
|
191
|
+
*/
|
|
192
|
+
const engine = picked.length > 0;
|
|
193
|
+
if (!engine && any((name) => name.toLowerCase().endsWith(".sln"))) {
|
|
194
|
+
take("visualstudio");
|
|
195
|
+
}
|
|
196
|
+
if (!engine && any((name) => name.toLowerCase().endsWith(".xcodeproj"))) {
|
|
197
|
+
take("objective-c");
|
|
198
|
+
}
|
|
199
|
+
if (!engine && here.has("cmakelists.txt"))
|
|
200
|
+
take("cmake");
|
|
201
|
+
return picked;
|
|
202
|
+
}
|
|
125
203
|
function suggestExclusions(files) {
|
|
126
204
|
const totals = new Map();
|
|
127
205
|
for (const entry of files) {
|
|
@@ -144,11 +222,17 @@ function suggestExclusions(files) {
|
|
|
144
222
|
gets written matching the folder that is there.
|
|
145
223
|
*/
|
|
146
224
|
const wanted = rule.directory.toLowerCase();
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
225
|
+
/*
|
|
226
|
+
At the root, where the rule is about the project's own output, and
|
|
227
|
+
written anchored so it stays there. Unanchored, this offered to
|
|
228
|
+
leave out `Assets/Props/obj` — a folder of 3D models — because a
|
|
229
|
+
C# build also writes to a folder called obj. The file it proposed
|
|
230
|
+
removing was the one the rules had just been fixed to protect.
|
|
231
|
+
*/
|
|
232
|
+
const within = rule.atRoot ? segments.slice(0, 1) : segments.slice(0, -1);
|
|
233
|
+
const found = within.find((segment) => segment.toLowerCase() === wanted);
|
|
150
234
|
if (found)
|
|
151
|
-
pattern = `${found}/`;
|
|
235
|
+
pattern = rule.atRoot ? `/${found}/` : `${found}/`;
|
|
152
236
|
}
|
|
153
237
|
if (!pattern && rule.file && name === rule.file) {
|
|
154
238
|
pattern = rule.file;
|
|
@@ -7,6 +7,7 @@ exports.parseRules = parseRules;
|
|
|
7
7
|
exports.ruleMatches = ruleMatches;
|
|
8
8
|
exports.lastMatchingRule = lastMatchingRule;
|
|
9
9
|
exports.decide = decide;
|
|
10
|
+
exports.decidingRule = decidingRule;
|
|
10
11
|
exports.excludes = excludes;
|
|
11
12
|
exports.negationReachesInto = negationReachesInto;
|
|
12
13
|
exports.isIgnored = isIgnored;
|
|
@@ -131,17 +132,36 @@ function matcher(pattern) {
|
|
|
131
132
|
}
|
|
132
133
|
return expression;
|
|
133
134
|
}
|
|
135
|
+
/** A directory pattern with or without its trailing slash is the same rule. */
|
|
136
|
+
function trailingSlashOff(pattern) {
|
|
137
|
+
let end = pattern.length;
|
|
138
|
+
while (end > 0 && pattern[end - 1] === "/")
|
|
139
|
+
end -= 1;
|
|
140
|
+
return pattern.slice(0, end);
|
|
141
|
+
}
|
|
134
142
|
function ruleMatches(relativePath, isDirectory, rule) {
|
|
135
143
|
const expression = matcher(rule.pattern);
|
|
136
144
|
if (rule.directoryOnly) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
/*
|
|
146
|
+
A rule written `dir/` speaks about directories and about nothing else.
|
|
147
|
+
Everything under an excluded directory goes too, but that is git
|
|
148
|
+
declining to look inside it rather than the pattern reaching down, and
|
|
149
|
+
the difference shows the moment a later rule re-includes the directory:
|
|
150
|
+
`build/` then `!build/` leaves `build/keep.txt` travelling, which is the
|
|
151
|
+
escape hatch people are told to use. It shows again in reverse, which is
|
|
152
|
+
how this was found — asking `!Library/PackageCache/a.package/` about a
|
|
153
|
+
file let the whole cache out of a folder only meant to be opened.
|
|
154
|
+
|
|
155
|
+
So the pattern answers only for directories, and a file's ancestors are
|
|
156
|
+
asked separately, outermost in, by `decidingRule`.
|
|
157
|
+
*/
|
|
158
|
+
if (!isDirectory)
|
|
159
|
+
return false;
|
|
160
|
+
const bare = trailingSlashOff(rule.pattern);
|
|
161
|
+
if (rule.anchored || bare.includes("/"))
|
|
162
|
+
return expression.test(relativePath);
|
|
163
|
+
const own = relativePath.split("/").pop() ?? relativePath;
|
|
164
|
+
return expression.test(own);
|
|
145
165
|
}
|
|
146
166
|
if (rule.anchored || rule.pattern.includes("/")) {
|
|
147
167
|
return expression.test(relativePath);
|
|
@@ -198,7 +218,21 @@ function decide(relativePath, isDirectory, layers) {
|
|
|
198
218
|
* is the dangerous direction to differ in: it uploads what somebody thought
|
|
199
219
|
* they had excluded.
|
|
200
220
|
*/
|
|
201
|
-
|
|
221
|
+
/**
|
|
222
|
+
* The rule that settles this path, counting the directories above it.
|
|
223
|
+
*
|
|
224
|
+
* A directory rule is written about the directory — `/[Ll]ibrary/` matches
|
|
225
|
+
* `Library`, not `Library/Artifacts/f1.dat` — so anything asking only about
|
|
226
|
+
* the file finds no rule and calls it included. The upload has always walked
|
|
227
|
+
* the ancestors; the filter screen asked about the file alone, and so showed
|
|
228
|
+
* a Unity project's whole import cache as going up when the save would have
|
|
229
|
+
* left every byte of it behind.
|
|
230
|
+
*
|
|
231
|
+
* Returned as the rule rather than a yes or no, because the screen names the
|
|
232
|
+
* line responsible — "Library/ — 400 files" is the useful form, and a boolean
|
|
233
|
+
* cannot say it.
|
|
234
|
+
*/
|
|
235
|
+
function decidingRule(relativePath, isDirectory, layers) {
|
|
202
236
|
/*
|
|
203
237
|
Ancestors first, outermost in. The moment one is excluded the answer is
|
|
204
238
|
settled and nothing below it can appeal.
|
|
@@ -208,9 +242,12 @@ function excludes(relativePath, isDirectory, layers) {
|
|
|
208
242
|
const ancestor = parts.slice(0, depth).join("/");
|
|
209
243
|
const above = decide(ancestor, true, layers);
|
|
210
244
|
if (above && !above.negated)
|
|
211
|
-
return
|
|
245
|
+
return above;
|
|
212
246
|
}
|
|
213
|
-
|
|
247
|
+
return decide(relativePath, isDirectory, layers);
|
|
248
|
+
}
|
|
249
|
+
function excludes(relativePath, isDirectory, layers) {
|
|
250
|
+
const rule = decidingRule(relativePath, isDirectory, layers);
|
|
214
251
|
return Boolean(rule && !rule.negated);
|
|
215
252
|
}
|
|
216
253
|
/**
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TransferProgress = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* How much of an upload has happened, and how fast it is going.
|
|
6
|
+
*
|
|
7
|
+
* Lifted out of the upload pass because nearly every number a person watches
|
|
8
|
+
* during a save comes from here, and three separate faults have come out of it
|
|
9
|
+
* being a handful of loose counters in the middle of a very long function:
|
|
10
|
+
*
|
|
11
|
+
* - Six lanes each added their own offset to one shared total, so the figure
|
|
12
|
+
* jumped between six different answers and the bar ran backwards.
|
|
13
|
+
* - The rate was `transferred` over the whole elapsed time while the bar
|
|
14
|
+
* beside it showed something else, so one real upload sat at
|
|
15
|
+
* "152 MB of 1.1 GB · 10 B/s" — not a slow upload, a number describing a
|
|
16
|
+
* different thing.
|
|
17
|
+
* - A lifetime average meant a slow first minute went on dragging the figure
|
|
18
|
+
* down long after the link had recovered, which is the opposite of what
|
|
19
|
+
* somebody watching it wants to know.
|
|
20
|
+
*
|
|
21
|
+
* So the counters and the arithmetic over them live together, with names, and
|
|
22
|
+
* can be tested without starting an upload.
|
|
23
|
+
*/
|
|
24
|
+
class TransferProgress {
|
|
25
|
+
windowMs;
|
|
26
|
+
now;
|
|
27
|
+
/** Bytes of files that have finished, whether they travelled or not. */
|
|
28
|
+
settled = 0;
|
|
29
|
+
/** Bytes that actually crossed the wire. */
|
|
30
|
+
moved = 0;
|
|
31
|
+
/** Files the service already held, so nothing crossed the wire for them. */
|
|
32
|
+
reused = 0;
|
|
33
|
+
/*
|
|
34
|
+
How far each lane is through the file it currently holds, kept apart so the
|
|
35
|
+
total is the sum of real work rather than six lanes' guesses about one
|
|
36
|
+
shared number.
|
|
37
|
+
*/
|
|
38
|
+
inFlight = new Map();
|
|
39
|
+
samples = [];
|
|
40
|
+
constructor(
|
|
41
|
+
/** How long a window the rate is measured over. */
|
|
42
|
+
windowMs = 5000,
|
|
43
|
+
/** Injected so a test can move time without waiting for it. */
|
|
44
|
+
now = Date.now) {
|
|
45
|
+
this.windowMs = windowMs;
|
|
46
|
+
this.now = now;
|
|
47
|
+
}
|
|
48
|
+
/** A file finished: its own size, and what it cost to send. */
|
|
49
|
+
finished(sourceBytes, wireBytes) {
|
|
50
|
+
this.settled += sourceBytes;
|
|
51
|
+
this.moved += wireBytes;
|
|
52
|
+
}
|
|
53
|
+
/** A file the service already had. Counted apart so "sent" stays honest. */
|
|
54
|
+
alreadyHeld() {
|
|
55
|
+
this.reused += 1;
|
|
56
|
+
}
|
|
57
|
+
/** Where one lane has got to inside the file it is sending. */
|
|
58
|
+
lane(index, offset) {
|
|
59
|
+
this.inFlight.set(index, offset);
|
|
60
|
+
}
|
|
61
|
+
/** That lane has finished, so its offset is now part of the settled total. */
|
|
62
|
+
laneDone(index) {
|
|
63
|
+
this.inFlight.delete(index);
|
|
64
|
+
}
|
|
65
|
+
/** Everything settled, plus how far the open lanes have got. */
|
|
66
|
+
bytes() {
|
|
67
|
+
let total = this.settled;
|
|
68
|
+
for (const offset of this.inFlight.values())
|
|
69
|
+
total += offset;
|
|
70
|
+
return total;
|
|
71
|
+
}
|
|
72
|
+
get sentBytes() {
|
|
73
|
+
return this.settled;
|
|
74
|
+
}
|
|
75
|
+
get transferred() {
|
|
76
|
+
return this.moved;
|
|
77
|
+
}
|
|
78
|
+
get alreadyOnAccount() {
|
|
79
|
+
return this.reused;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* How fast the thing on screen is moving, over a short trailing window.
|
|
83
|
+
*
|
|
84
|
+
* The same counter the bar uses, so the two agree. Sampled rather than
|
|
85
|
+
* recorded per call: this is asked once per report, and reports arrive per
|
|
86
|
+
* file, which for small files is thousands a second.
|
|
87
|
+
*/
|
|
88
|
+
rate() {
|
|
89
|
+
const now = this.now();
|
|
90
|
+
const bytes = this.bytes();
|
|
91
|
+
const last = this.samples[this.samples.length - 1];
|
|
92
|
+
if (!last || now - last.at >= 250)
|
|
93
|
+
this.samples.push({ at: now, bytes });
|
|
94
|
+
while (this.samples.length > 2 && now - this.samples[0].at > this.windowMs) {
|
|
95
|
+
this.samples.shift();
|
|
96
|
+
}
|
|
97
|
+
const oldest = this.samples[0];
|
|
98
|
+
const seconds = (now - oldest.at) / 1000;
|
|
99
|
+
/*
|
|
100
|
+
Nothing until there is something to divide. Answering zero keeps the
|
|
101
|
+
window quiet rather than showing a figure computed from a fraction of a
|
|
102
|
+
second, which reads as a stall on an upload that has barely begun.
|
|
103
|
+
*/
|
|
104
|
+
if (seconds < 0.5)
|
|
105
|
+
return 0;
|
|
106
|
+
return Math.max(0, Math.round((bytes - oldest.bytes) / seconds));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.TransferProgress = TransferProgress;
|