dsh-mnemon 0.5.27 → 0.5.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/lib/bundle.js +1 -1
- package/lib/client.js +2000 -1257
- package/lib/index.js +606 -180
- package/lib/{starter-resolution-C2z8b9AZ.js → starter-resolution-CLmHa3uV.js} +1 -1
- package/lib/starter.js +1 -1
- package/lib/types/src/client/locales.d.ts +153 -100
- package/lib/types/src/host/protocol.d.ts +57 -2
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -541,6 +541,7 @@ var MnemonAutoBackupScheduler = class {
|
|
|
541
541
|
lastAt;
|
|
542
542
|
lastAttemptAt;
|
|
543
543
|
lastError;
|
|
544
|
+
lastFailure;
|
|
544
545
|
lastCommit;
|
|
545
546
|
lastPushed;
|
|
546
547
|
constructor(runtime, now = () => /* @__PURE__ */ new Date()) {
|
|
@@ -574,6 +575,7 @@ var MnemonAutoBackupScheduler = class {
|
|
|
574
575
|
...this.lastAt === void 0 ? {} : { lastAt: this.lastAt },
|
|
575
576
|
...this.lastAttemptAt === void 0 || !available ? {} : { nextAt: new Date(this.lastAttemptAt + minutes * MINUTE_MS).toISOString() },
|
|
576
577
|
...this.lastError === void 0 ? {} : { lastError: this.lastError },
|
|
578
|
+
...this.lastError === void 0 || this.lastFailure === void 0 ? {} : { lastFailure: this.lastFailure },
|
|
577
579
|
...this.lastCommit === void 0 ? {} : { lastCommit: this.lastCommit },
|
|
578
580
|
...this.lastPushed === void 0 ? {} : { lastPushed: this.lastPushed }
|
|
579
581
|
};
|
|
@@ -619,19 +621,26 @@ var MnemonAutoBackupScheduler = class {
|
|
|
619
621
|
const controller = new AbortController();
|
|
620
622
|
this.controller = controller;
|
|
621
623
|
try {
|
|
622
|
-
if (sync.settings().read().repoUrl === void 0)
|
|
623
|
-
|
|
624
|
-
|
|
624
|
+
if (sync.settings().read().repoUrl === void 0) {
|
|
625
|
+
this.lastError = void 0;
|
|
626
|
+
this.lastFailure = void 0;
|
|
627
|
+
} else if (!this.runtime.config.writeEnabled) {
|
|
628
|
+
this.lastError = "dsh-mnemon is configured read-only";
|
|
629
|
+
this.lastFailure = void 0;
|
|
630
|
+
} else {
|
|
625
631
|
const result = await sync.push({ signal: controller.signal });
|
|
626
632
|
this.lastAt = this.now().toISOString();
|
|
627
633
|
this.lastCommit = result.commit;
|
|
628
634
|
this.lastPushed = result.pushed;
|
|
629
635
|
this.lastError = result.pushed ? void 0 : result.committed ? result.reason : void 0;
|
|
636
|
+
this.lastFailure = this.lastError === void 0 ? void 0 : result.failure;
|
|
630
637
|
}
|
|
631
638
|
} catch (error) {
|
|
632
639
|
const message = error instanceof Error ? error.message : String(error);
|
|
633
|
-
|
|
640
|
+
const reason = error instanceof Error ? error.reason : void 0;
|
|
641
|
+
if (!this.stopped && reason !== "busy" && !message.includes(BUSY_MESSAGE)) {
|
|
634
642
|
this.lastError = message;
|
|
643
|
+
this.lastFailure = typeof reason === "string" ? reason : void 0;
|
|
635
644
|
console.warn("dsh-mnemon: automatic Git backup failed", error);
|
|
636
645
|
}
|
|
637
646
|
} finally {
|
|
@@ -707,7 +716,7 @@ function createStorageRoot(config, workspaceRoot) {
|
|
|
707
716
|
const MNEMON_MACHINE_DIRECTORY = "state";
|
|
708
717
|
const MNEMON_MACHINE_FILE = "machine.json";
|
|
709
718
|
const MNEMON_MACHINE_VERSION = 1;
|
|
710
|
-
const
|
|
719
|
+
const CONTROL = /[\p{Cc}\u2028\u2029]/u;
|
|
711
720
|
function record$8(value) {
|
|
712
721
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
713
722
|
}
|
|
@@ -715,16 +724,26 @@ function record$8(value) {
|
|
|
715
724
|
function defaultMachineLabel() {
|
|
716
725
|
const name = hostname().trim();
|
|
717
726
|
const label = name === "" ? "this machine" : name;
|
|
718
|
-
return label.length >
|
|
727
|
+
return label.length > 120 ? label.slice(0, 120) : label;
|
|
728
|
+
}
|
|
729
|
+
/** A name a person gives this machine, as it is stored and shown. */
|
|
730
|
+
function machineLabel(value) {
|
|
731
|
+
const label = typeof value === "string" ? value.trim() : "";
|
|
732
|
+
if (label === "" || label.length > 120 || CONTROL.test(label)) throw new Error("machine name must be 1 to " + String(120) + " characters on one line");
|
|
733
|
+
return label;
|
|
719
734
|
}
|
|
720
735
|
/**
|
|
721
736
|
* The machine identity store. One file, one identity, created on first read and
|
|
722
737
|
* then stable for the life of the data directory: an id that changed would
|
|
723
738
|
* make every earlier entry look like it came from another machine.
|
|
739
|
+
*
|
|
740
|
+
* The name is not part of that promise. A host-name label follows the computer's
|
|
741
|
+
* host name, and a person can name the machine instead. Every read goes to the
|
|
742
|
+
* file, because the pack manager and the sync channel each hold a store and a
|
|
743
|
+
* rename through one has to reach what the other stamps.
|
|
724
744
|
*/
|
|
725
745
|
var MnemonMachineStore = class {
|
|
726
746
|
root;
|
|
727
|
-
cached;
|
|
728
747
|
constructor(runner) {
|
|
729
748
|
this.root = resolve(runner.effectiveDataDir());
|
|
730
749
|
}
|
|
@@ -736,10 +755,42 @@ var MnemonMachineStore = class {
|
|
|
736
755
|
}
|
|
737
756
|
/** This installation's identity, creating and persisting one when none exists yet. */
|
|
738
757
|
read() {
|
|
739
|
-
|
|
758
|
+
return this.current().identity;
|
|
759
|
+
}
|
|
760
|
+
/** The identity with the source of its name, bringing the stored record up to date. */
|
|
761
|
+
current() {
|
|
740
762
|
const stored = this.stored();
|
|
741
|
-
|
|
742
|
-
|
|
763
|
+
if (stored === void 0) return this.write({
|
|
764
|
+
id: randomUUID(),
|
|
765
|
+
label: defaultMachineLabel(),
|
|
766
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
767
|
+
}, "hostname");
|
|
768
|
+
if (stored.source === "hostname") {
|
|
769
|
+
const label = defaultMachineLabel();
|
|
770
|
+
if (stored.identity.label !== label || !stored.marked) return this.write({
|
|
771
|
+
...stored.identity,
|
|
772
|
+
label
|
|
773
|
+
}, "hostname");
|
|
774
|
+
} else if (!stored.marked) return this.write(stored.identity, "custom");
|
|
775
|
+
return {
|
|
776
|
+
identity: stored.identity,
|
|
777
|
+
source: stored.source
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Name this machine. A typed name stays as given; null or an empty name goes back
|
|
782
|
+
* to the host name. The id never changes, so earlier entries keep pointing here.
|
|
783
|
+
*/
|
|
784
|
+
rename(value) {
|
|
785
|
+
const label = value === null || value === "" ? void 0 : machineLabel(value);
|
|
786
|
+
const identity = this.current().identity;
|
|
787
|
+
return label === void 0 ? this.write({
|
|
788
|
+
...identity,
|
|
789
|
+
label: defaultMachineLabel()
|
|
790
|
+
}, "hostname") : this.write({
|
|
791
|
+
...identity,
|
|
792
|
+
label
|
|
793
|
+
}, "custom");
|
|
743
794
|
}
|
|
744
795
|
stored() {
|
|
745
796
|
let value;
|
|
@@ -753,29 +804,42 @@ var MnemonMachineStore = class {
|
|
|
753
804
|
if (typeof parsed.id !== "string" || parsed.id.trim() === "") return void 0;
|
|
754
805
|
if (typeof parsed.label !== "string" || parsed.label.trim() === "") return void 0;
|
|
755
806
|
if (typeof parsed.createdAt !== "string") return void 0;
|
|
756
|
-
|
|
807
|
+
const identity = {
|
|
757
808
|
id: parsed.id,
|
|
758
809
|
label: parsed.label,
|
|
759
810
|
createdAt: parsed.createdAt
|
|
760
811
|
};
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
812
|
+
if (parsed.labelSource === "hostname" || parsed.labelSource === "custom") return {
|
|
813
|
+
identity,
|
|
814
|
+
source: parsed.labelSource,
|
|
815
|
+
marked: true
|
|
816
|
+
};
|
|
817
|
+
return {
|
|
818
|
+
identity,
|
|
819
|
+
source: parsed.label === defaultMachineLabel() ? "hostname" : "custom",
|
|
820
|
+
marked: false
|
|
767
821
|
};
|
|
822
|
+
}
|
|
823
|
+
write(identity, source) {
|
|
768
824
|
mkdirSync(this.directory(), {
|
|
769
825
|
recursive: true,
|
|
770
826
|
mode: 448
|
|
771
827
|
});
|
|
772
828
|
const temporary = `${this.path()}.${randomUUID()}.tmp`;
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
829
|
+
try {
|
|
830
|
+
writeFileSync(temporary, `${JSON.stringify({
|
|
831
|
+
version: MNEMON_MACHINE_VERSION,
|
|
832
|
+
...identity,
|
|
833
|
+
labelSource: source
|
|
834
|
+
}, null, 2)}\n`, { mode: 384 });
|
|
835
|
+
renameSync(temporary, this.path());
|
|
836
|
+
} finally {
|
|
837
|
+
rmSync(temporary, { force: true });
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
identity,
|
|
841
|
+
source
|
|
842
|
+
};
|
|
779
843
|
}
|
|
780
844
|
};
|
|
781
845
|
//#endregion
|
|
@@ -1573,6 +1637,7 @@ function mergeDocuments(root, pack) {
|
|
|
1573
1637
|
for (const source of incoming.index.documents) {
|
|
1574
1638
|
const existing = ids.get(source.id);
|
|
1575
1639
|
if (existing !== void 0 && existing.contentHash === source.contentHash) continue;
|
|
1640
|
+
if (existing !== void 0 && current.index.documents.some((document) => document.contentHash === source.contentHash)) continue;
|
|
1576
1641
|
let document = source;
|
|
1577
1642
|
let bytes = incoming.files.get(source.id);
|
|
1578
1643
|
if (existing !== void 0) {
|
|
@@ -1595,7 +1660,10 @@ function mergeRegistry(root, pack) {
|
|
|
1595
1660
|
let body = source;
|
|
1596
1661
|
const sourceDb = incoming.databases.get(source.id);
|
|
1597
1662
|
if (ids.has(source.id)) {
|
|
1598
|
-
|
|
1663
|
+
const currentDb = current.databases.get(source.id);
|
|
1664
|
+
const incomingHash = sha256$1(sourceDb);
|
|
1665
|
+
if (sha256$1(currentDb) === incomingHash) continue;
|
|
1666
|
+
if ([...current.databases.values()].some((database) => sha256$1(database) === incomingHash)) continue;
|
|
1599
1667
|
const id = randomUUID();
|
|
1600
1668
|
body = {
|
|
1601
1669
|
...source,
|
|
@@ -2591,6 +2659,20 @@ function createGitHubTransport(options = {}) {
|
|
|
2591
2659
|
});
|
|
2592
2660
|
}
|
|
2593
2661
|
//#endregion
|
|
2662
|
+
//#region src/host/sync-error.ts
|
|
2663
|
+
/**
|
|
2664
|
+
* A sync failure the settings page can name in its own language. The message
|
|
2665
|
+
* stays the Host's own words for logs and for anything the page cannot name.
|
|
2666
|
+
*/
|
|
2667
|
+
var MnemonSyncError = class extends Error {
|
|
2668
|
+
reason;
|
|
2669
|
+
constructor(reason, message) {
|
|
2670
|
+
super(message);
|
|
2671
|
+
this.reason = reason;
|
|
2672
|
+
this.name = "MnemonSyncError";
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
//#endregion
|
|
2594
2676
|
//#region src/host/github-auth.ts
|
|
2595
2677
|
/**
|
|
2596
2678
|
* GitHub sign-in for the repository sync channel: the OAuth device flow, the
|
|
@@ -2641,7 +2723,8 @@ function codeOf(body) {
|
|
|
2641
2723
|
}
|
|
2642
2724
|
function failure$2(action, status, body) {
|
|
2643
2725
|
const code = codeOf(body);
|
|
2644
|
-
|
|
2726
|
+
const message = code === void 0 ? action + " failed with HTTP " + String(status) : action + " failed: " + code;
|
|
2727
|
+
return status === 401 ? new MnemonSyncError("github-unauthorized", message) : new Error(message);
|
|
2645
2728
|
}
|
|
2646
2729
|
/** Read one grant out of a stored record, refusing anything else. */
|
|
2647
2730
|
function githubGrantFromRecord(record_) {
|
|
@@ -2920,17 +3003,20 @@ var MnemonGitHubAuth = class {
|
|
|
2920
3003
|
/** The repositories the account may write to, most recently pushed first. */
|
|
2921
3004
|
async repositories(signal) {
|
|
2922
3005
|
const token = await this.requireToken();
|
|
2923
|
-
const url = "https://api.github.com/user/repos?per_page=" + String(100) + "&sort=updated&affiliation=owner%2Ccollaborator%2Corganization_member";
|
|
2924
|
-
const { status, body } = await this.ask(url, {
|
|
2925
|
-
headers: this.authorization(token),
|
|
2926
|
-
...signal === void 0 ? {} : { signal }
|
|
2927
|
-
}, "listing GitHub repositories");
|
|
2928
|
-
if (status !== 200) throw failure$2("listing GitHub repositories", status, body);
|
|
2929
|
-
if (!Array.isArray(body)) throw new Error("the GitHub repository answer was not a list");
|
|
2930
3006
|
const repositories = [];
|
|
2931
|
-
for (
|
|
2932
|
-
const
|
|
2933
|
-
|
|
3007
|
+
for (let page = 1; page <= 5; page += 1) {
|
|
3008
|
+
const url = "https://api.github.com/user/repos?per_page=" + String(100) + "&page=" + String(page) + "&sort=updated&affiliation=owner%2Ccollaborator%2Corganization_member";
|
|
3009
|
+
const { status, body } = await this.ask(url, {
|
|
3010
|
+
headers: this.authorization(token),
|
|
3011
|
+
...signal === void 0 ? {} : { signal }
|
|
3012
|
+
}, "listing GitHub repositories");
|
|
3013
|
+
if (status !== 200) throw failure$2("listing GitHub repositories", status, body);
|
|
3014
|
+
if (!Array.isArray(body)) throw new Error("the GitHub repository answer was not a list");
|
|
3015
|
+
for (const entry of body) {
|
|
3016
|
+
const repository = repositoryOf(entry);
|
|
3017
|
+
if (repository !== void 0 && !repositories.some((known) => known.fullName === repository.fullName)) repositories.push(repository);
|
|
3018
|
+
}
|
|
3019
|
+
if (body.length < 100) break;
|
|
2934
3020
|
}
|
|
2935
3021
|
return {
|
|
2936
3022
|
login: await this.account(token, signal) ?? "",
|
|
@@ -3234,7 +3320,28 @@ var MnemonSyncSettingsStore = class {
|
|
|
3234
3320
|
//#region src/host/git-sync.ts
|
|
3235
3321
|
/** The oldest Git the channel drives: `archive`, `ls-remote`, `-C`, and `credential` all predate it. */
|
|
3236
3322
|
const MNEMON_SYNC_MINIMUM_GIT = "2.20";
|
|
3237
|
-
|
|
3323
|
+
/**
|
|
3324
|
+
* How long the settings page waits to learn whether the remote answers. A page
|
|
3325
|
+
* that opens should not hang on a host that drops packets, and anything slower
|
|
3326
|
+
* than this is reported as a timeout rather than awaited.
|
|
3327
|
+
*/
|
|
3328
|
+
const MNEMON_SYNC_STATUS_TIMEOUT_MS = 2e4;
|
|
3329
|
+
/** A push Git refused because the branch moved is folded in and tried once more. */
|
|
3330
|
+
const PUSH_ATTEMPTS = 2;
|
|
3331
|
+
/**
|
|
3332
|
+
* No sync runs this long, so a lock older than this is abandoned whoever holds it.
|
|
3333
|
+
* It also covers a process id that a restart handed to an unrelated process.
|
|
3334
|
+
*/
|
|
3335
|
+
const STALE_LOCK_MS = 36e5;
|
|
3336
|
+
const LOCK_OWNER = "owner.json";
|
|
3337
|
+
/**
|
|
3338
|
+
* A lock without an owner record was taken by a version that kept none, and may
|
|
3339
|
+
* still be in use by it; it is taken over only once no such run could still last.
|
|
3340
|
+
*/
|
|
3341
|
+
const OWNERLESS_LOCK_MS = 9e5;
|
|
3342
|
+
/** The mirror's own Git config names the repository it was filled from. */
|
|
3343
|
+
const ORIGIN_KEY = "mnemon.origin";
|
|
3344
|
+
const LAST_RUN_FILE = "last-run.json";
|
|
3238
3345
|
const GIT_LABEL = "git";
|
|
3239
3346
|
const MANIFEST = "manifest.json";
|
|
3240
3347
|
const CHECKSUMS = "checksums.json";
|
|
@@ -3295,6 +3402,16 @@ function gitEnvironment(environment = process.env) {
|
|
|
3295
3402
|
sanitized.GCM_INTERACTIVE = "never";
|
|
3296
3403
|
return sanitized;
|
|
3297
3404
|
}
|
|
3405
|
+
/** The host an HTTPS address hands a token to; any other address takes none. */
|
|
3406
|
+
function tokenHost(repoUrl) {
|
|
3407
|
+
if (repoUrl === void 0) return void 0;
|
|
3408
|
+
try {
|
|
3409
|
+
const url = new URL(repoUrl);
|
|
3410
|
+
return url.protocol === "https:" ? url.host.toLowerCase() : void 0;
|
|
3411
|
+
} catch {
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3298
3415
|
/** Whether a repository address is HTTPS on github.com, the only host a GitHub grant is for. */
|
|
3299
3416
|
function isGitHubHttps(repoUrl) {
|
|
3300
3417
|
try {
|
|
@@ -3304,6 +3421,37 @@ function isGitHubHttps(repoUrl) {
|
|
|
3304
3421
|
return false;
|
|
3305
3422
|
}
|
|
3306
3423
|
}
|
|
3424
|
+
/**
|
|
3425
|
+
* What kind of failure one Git error describes. Git words these differently per
|
|
3426
|
+
* transport and per host, so the patterns are the phrases each transport uses:
|
|
3427
|
+
* a refused or missing credential, an address that leads nowhere (GitHub answers
|
|
3428
|
+
* "not found" for a private repository it will not show without credentials), a
|
|
3429
|
+
* network that does not reach the host, and a host that never answered.
|
|
3430
|
+
*/
|
|
3431
|
+
function remoteIssueOf(message) {
|
|
3432
|
+
const text = message.toLowerCase();
|
|
3433
|
+
if (/did not respond within|timed out|timeout was reached/u.test(text)) return "timeout";
|
|
3434
|
+
if (/authentication failed|could not read (username|password)|terminal prompts disabled|permission denied|invalid username or password|bad credentials|returned error: 40[13]|host key verification failed|access denied|denied to /u.test(text)) return "auth";
|
|
3435
|
+
if (/repository not found|not found|does not appear to be a git repository|no such file or directory|does not exist|returned error: 404/u.test(text)) return "not-found";
|
|
3436
|
+
if (/could not resolve|failed to connect|couldn't connect|connection (refused|reset|closed|timed out)|network is unreachable|no route to host|unable to access|ssl|tls|proxy|empty reply/u.test(text)) return "network";
|
|
3437
|
+
return "unknown";
|
|
3438
|
+
}
|
|
3439
|
+
const REMOTE_FAILURE = {
|
|
3440
|
+
auth: "remote-auth",
|
|
3441
|
+
"not-found": "remote-not-found",
|
|
3442
|
+
network: "remote-network",
|
|
3443
|
+
timeout: "remote-timeout",
|
|
3444
|
+
unknown: void 0
|
|
3445
|
+
};
|
|
3446
|
+
/** One failed remote command as the error a caller reports: named when it can be. */
|
|
3447
|
+
function remoteFailure(message) {
|
|
3448
|
+
const reason = REMOTE_FAILURE[remoteIssueOf(message)];
|
|
3449
|
+
return reason === void 0 ? new Error(message) : new MnemonSyncError(reason, message);
|
|
3450
|
+
}
|
|
3451
|
+
/** Whether Git refused a push because the branch moved on since this run read it. */
|
|
3452
|
+
function rejectedPush(output) {
|
|
3453
|
+
return /\[rejected\]|fetch first|non-fast-forward/iu.test(output);
|
|
3454
|
+
}
|
|
3307
3455
|
function record$3(value) {
|
|
3308
3456
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
3309
3457
|
}
|
|
@@ -3450,9 +3598,9 @@ function objectCount(output) {
|
|
|
3450
3598
|
if (loose === null || bytes === null || packed === null || packedBytes === null) return void 0;
|
|
3451
3599
|
return {
|
|
3452
3600
|
loose: Number(loose[1]),
|
|
3453
|
-
bytes: Number(bytes[1]),
|
|
3601
|
+
bytes: Number(bytes[1]) * 1024,
|
|
3454
3602
|
packed: Number(packed[1]),
|
|
3455
|
-
packedBytes: Number(packedBytes[1])
|
|
3603
|
+
packedBytes: Number(packedBytes[1]) * 1024
|
|
3456
3604
|
};
|
|
3457
3605
|
}
|
|
3458
3606
|
/** How many commits one page asks for: the caller's window, clamped to what one listing should carry. */
|
|
@@ -3718,6 +3866,8 @@ var MnemonGitSync = class {
|
|
|
3718
3866
|
};
|
|
3719
3867
|
const commit = existsSync(this.store.mirror()) && git.available ? await this.lastCommit(signal) : void 0;
|
|
3720
3868
|
const autoBackup = this.autoBackupView?.();
|
|
3869
|
+
const lastRun = repoUrl === void 0 ? void 0 : this.lastRun();
|
|
3870
|
+
const machine = this.machine.current();
|
|
3721
3871
|
return {
|
|
3722
3872
|
enabled: true,
|
|
3723
3873
|
configured: repoUrl !== void 0,
|
|
@@ -3726,14 +3876,27 @@ var MnemonGitSync = class {
|
|
|
3726
3876
|
mirrorPath: this.store.mirror(),
|
|
3727
3877
|
git,
|
|
3728
3878
|
remote,
|
|
3729
|
-
machine:
|
|
3879
|
+
machine: machine.identity,
|
|
3880
|
+
machineLabel: {
|
|
3881
|
+
source: machine.source,
|
|
3882
|
+
hostname: defaultMachineLabel()
|
|
3883
|
+
},
|
|
3730
3884
|
...commit === void 0 ? {} : { lastCommit: commit },
|
|
3885
|
+
...lastRun === void 0 ? {} : { lastRun },
|
|
3731
3886
|
...autoBackup === void 0 ? {} : { autoBackup }
|
|
3732
3887
|
};
|
|
3733
3888
|
}
|
|
3734
3889
|
async configure(patch) {
|
|
3735
|
-
const
|
|
3890
|
+
const previous = this.store.read();
|
|
3891
|
+
const fields = record$3(patch);
|
|
3892
|
+
const { machineLabel: name, ...settings } = fields ?? {};
|
|
3893
|
+
const naming = fields !== void 0 && "machineLabel" in fields;
|
|
3894
|
+
if (naming && name !== null && name !== "") machineLabel(name);
|
|
3895
|
+
const next = this.store.patch(fields === void 0 ? patch : settings);
|
|
3896
|
+
if (next.token !== void 0 && previous.repoUrl !== void 0 && fields !== void 0 && !("token" in fields) && tokenHost(previous.repoUrl) !== tokenHost(next.repoUrl)) delete next.token;
|
|
3736
3897
|
this.store.write(next);
|
|
3898
|
+
if (naming) this.machine.rename(name);
|
|
3899
|
+
if (previous.repoUrl !== next.repoUrl) rmSync(this.lastRunPath(), { force: true });
|
|
3737
3900
|
return this.credentialView(next);
|
|
3738
3901
|
}
|
|
3739
3902
|
/**
|
|
@@ -3747,106 +3910,202 @@ var MnemonGitSync = class {
|
|
|
3747
3910
|
await this.requireGit(input.signal);
|
|
3748
3911
|
const message = commitMessage(input.message, this.now());
|
|
3749
3912
|
return this.lock(async () => {
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
let
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
tombstones: {
|
|
3758
|
-
version: 1,
|
|
3759
|
-
tombstones: remote.tombstones
|
|
3760
|
-
}
|
|
3761
|
-
});
|
|
3762
|
-
merged = {
|
|
3763
|
-
commit: remote.commit,
|
|
3764
|
-
...remote.machine === void 0 ? {} : { machine: {
|
|
3765
|
-
id: remote.machine.id,
|
|
3766
|
-
label: remote.machine.label
|
|
3767
|
-
} },
|
|
3768
|
-
components: imported.components,
|
|
3769
|
-
summary: imported.summary,
|
|
3770
|
-
tombstones: remote.tombstones.length
|
|
3771
|
-
};
|
|
3913
|
+
let result = await this.pushOnce(settings, message, input.signal);
|
|
3914
|
+
let added = result.merged?.runtime?.added ?? 0;
|
|
3915
|
+
let held = result.merged?.runtime?.held ?? 0;
|
|
3916
|
+
for (let attempt = 1; attempt < PUSH_ATTEMPTS && result.failure === "push-rejected"; attempt += 1) {
|
|
3917
|
+
result = await this.pushOnce(settings, message, input.signal);
|
|
3918
|
+
added += result.merged?.runtime?.added ?? 0;
|
|
3919
|
+
held = result.merged?.runtime?.held ?? held;
|
|
3772
3920
|
}
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3921
|
+
if (result.merged !== void 0 && (added > 0 || result.merged.runtime !== void 0)) result = {
|
|
3922
|
+
...result,
|
|
3923
|
+
merged: {
|
|
3924
|
+
...result.merged,
|
|
3925
|
+
runtime: {
|
|
3926
|
+
added,
|
|
3927
|
+
held
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3782
3930
|
};
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
if (prepared.tip === void 0 || !holdsPayload(before, entries)) this.writePayload(settings.subdir, entries);
|
|
3789
|
-
const staged = await this.git([
|
|
3790
|
-
"add",
|
|
3791
|
-
"--all",
|
|
3792
|
-
"--",
|
|
3793
|
-
settings.subdir
|
|
3794
|
-
], {
|
|
3795
|
-
cwd: this.store.mirror(),
|
|
3796
|
-
signal: input.signal
|
|
3931
|
+
if (result.pushed || !result.committed) this.recordRun({
|
|
3932
|
+
kind: "sync",
|
|
3933
|
+
commit: result.commit,
|
|
3934
|
+
pushed: result.pushed,
|
|
3935
|
+
...result.merged?.runtime === void 0 ? {} : { added: result.merged.runtime.added }
|
|
3797
3936
|
});
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3937
|
+
return result;
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3940
|
+
async pushOnce(settings, message, signal) {
|
|
3941
|
+
const prepared = await this.ensureMirror(settings, signal);
|
|
3942
|
+
await this.packs.recordDeletions();
|
|
3943
|
+
const remote = await this.readPrepared(settings, prepared);
|
|
3944
|
+
let merged;
|
|
3945
|
+
if (remote !== void 0) {
|
|
3946
|
+
const imported = await this.packs.importPack(Buffer.from(remote.archive).toString("base64"), {
|
|
3947
|
+
mode: "merge",
|
|
3948
|
+
tombstones: {
|
|
3949
|
+
version: 1,
|
|
3950
|
+
tombstones: remote.tombstones
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3953
|
+
merged = {
|
|
3954
|
+
commit: remote.commit,
|
|
3955
|
+
...remote.machine === void 0 ? {} : { machine: {
|
|
3956
|
+
id: remote.machine.id,
|
|
3957
|
+
label: remote.machine.label
|
|
3958
|
+
} },
|
|
3959
|
+
components: imported.components,
|
|
3960
|
+
summary: imported.summary,
|
|
3961
|
+
tombstones: remote.tombstones.length,
|
|
3962
|
+
...imported.runtime === void 0 ? {} : { runtime: imported.runtime }
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
const exported = await this.packs.exportPack("full", { stamp: true });
|
|
3966
|
+
const entries = unpack(Buffer.from(exported.base64, "base64"));
|
|
3967
|
+
const extension = {
|
|
3968
|
+
channel: "git",
|
|
3969
|
+
branch: settings.branch,
|
|
3970
|
+
subdir: settings.subdir,
|
|
3971
|
+
pushedAt: exported.manifest.exportedAt,
|
|
3972
|
+
machine: this.machine.read(),
|
|
3973
|
+
tombstones: this.packs.tombstones().tombstones
|
|
3974
|
+
};
|
|
3975
|
+
entries[MANIFEST] = new TextEncoder().encode(JSON.stringify({
|
|
3976
|
+
...exported.manifest,
|
|
3977
|
+
[SYNC_EXTENSION]: extension
|
|
3978
|
+
}, null, 2) + "\n");
|
|
3979
|
+
const before = readFiles(this.payloadRoot(settings.subdir));
|
|
3980
|
+
if (prepared.tip === void 0 || !holdsPayload(before, entries)) this.writePayload(settings.subdir, entries);
|
|
3981
|
+
const staged = await this.git([
|
|
3982
|
+
"add",
|
|
3983
|
+
"--all",
|
|
3984
|
+
"--",
|
|
3985
|
+
settings.subdir
|
|
3986
|
+
], {
|
|
3987
|
+
cwd: this.store.mirror(),
|
|
3988
|
+
signal
|
|
3989
|
+
});
|
|
3990
|
+
if (staged.exitCode !== 0) throw new Error("git add failed: " + tail(staged.stderr || staged.stdout));
|
|
3991
|
+
const committed = (await this.git([
|
|
3992
|
+
"status",
|
|
3993
|
+
"--porcelain",
|
|
3994
|
+
"--",
|
|
3995
|
+
settings.subdir
|
|
3996
|
+
], {
|
|
3997
|
+
cwd: this.store.mirror(),
|
|
3998
|
+
signal
|
|
3999
|
+
})).stdout.trim() !== "";
|
|
4000
|
+
if (committed) {
|
|
4001
|
+
const identity = await this.commitIdentity(settings, signal);
|
|
4002
|
+
const result = await this.git([
|
|
4003
|
+
...identity,
|
|
4004
|
+
"commit",
|
|
4005
|
+
"--quiet",
|
|
4006
|
+
"-m",
|
|
4007
|
+
message,
|
|
3802
4008
|
"--",
|
|
3803
4009
|
settings.subdir
|
|
3804
4010
|
], {
|
|
3805
4011
|
cwd: this.store.mirror(),
|
|
3806
|
-
signal
|
|
3807
|
-
})
|
|
3808
|
-
if (
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
}
|
|
4012
|
+
signal
|
|
4013
|
+
});
|
|
4014
|
+
if (result.exitCode !== 0) throw new Error("git commit failed: " + tail(result.stderr || result.stdout));
|
|
4015
|
+
}
|
|
4016
|
+
const head = await this.head(signal);
|
|
4017
|
+
const published = committed ? await this.publish(settings, await this.credential(settings), signal) : {
|
|
4018
|
+
pushed: false,
|
|
4019
|
+
reason: prepared.tip === void 0 ? "nothing to publish" : "the branch already holds this payload"
|
|
4020
|
+
};
|
|
4021
|
+
const compaction = await this.compact(signal);
|
|
4022
|
+
const summary = exported.manifest.summary.map((entry) => ({
|
|
4023
|
+
...entry,
|
|
4024
|
+
changed: componentChanged(before, entries, entry.component)
|
|
4025
|
+
}));
|
|
4026
|
+
return {
|
|
4027
|
+
repoUrl: settings.repoUrl,
|
|
4028
|
+
branch: settings.branch,
|
|
4029
|
+
subdir: settings.subdir,
|
|
4030
|
+
commit: head,
|
|
4031
|
+
committed,
|
|
4032
|
+
message,
|
|
4033
|
+
files: Object.keys(entries).length,
|
|
4034
|
+
bytes: Object.values(entries).reduce((total, bytes) => total + bytes.byteLength, 0),
|
|
4035
|
+
summary,
|
|
4036
|
+
pushed: published.pushed,
|
|
4037
|
+
...merged === void 0 ? {} : { merged },
|
|
4038
|
+
...published.reason === void 0 ? {} : { reason: published.reason },
|
|
4039
|
+
...published.failure === void 0 ? {} : { failure: published.failure },
|
|
4040
|
+
...compaction === void 0 ? {} : { compaction }
|
|
4041
|
+
};
|
|
4042
|
+
}
|
|
4043
|
+
/**
|
|
4044
|
+
* The identity a sync commit is recorded under: the one configured here, else the
|
|
4045
|
+
* Git identity this machine has. A machine without one gets the channel's own
|
|
4046
|
+
* rather than a failed commit, and rather than the account and host names Git
|
|
4047
|
+
* would otherwise guess and publish.
|
|
4048
|
+
*/
|
|
4049
|
+
async commitIdentity(settings, signal) {
|
|
4050
|
+
const name = settings.authorName !== "" ? settings.authorName : await this.configured("user.name", signal) ?? "dsh-mnemon sync";
|
|
4051
|
+
const email = settings.authorEmail !== "" ? settings.authorEmail : await this.configured("user.email", signal) ?? "mnemon@localhost";
|
|
4052
|
+
return [
|
|
4053
|
+
"-c",
|
|
4054
|
+
"user.name=" + name,
|
|
4055
|
+
"-c",
|
|
4056
|
+
"user.email=" + email
|
|
4057
|
+
];
|
|
4058
|
+
}
|
|
4059
|
+
/** One Git setting as this machine resolves it, or nothing when no config sets it. */
|
|
4060
|
+
async configured(key, signal) {
|
|
4061
|
+
const result = await this.git([
|
|
4062
|
+
"config",
|
|
4063
|
+
"--get",
|
|
4064
|
+
key
|
|
4065
|
+
], {
|
|
4066
|
+
cwd: this.store.mirror(),
|
|
4067
|
+
signal
|
|
4068
|
+
});
|
|
4069
|
+
const value = result.exitCode === 0 ? result.stdout.trim() : "";
|
|
4070
|
+
return value === "" ? void 0 : value;
|
|
4071
|
+
}
|
|
4072
|
+
/** When memory last travelled through this channel, or nothing when it never did. */
|
|
4073
|
+
lastRun() {
|
|
4074
|
+
try {
|
|
4075
|
+
const value = record$3(JSON.parse(readFileSync(this.lastRunPath(), "utf8")));
|
|
4076
|
+
if (value === void 0 || typeof value.at !== "string" || typeof value.commit !== "string") return void 0;
|
|
4077
|
+
if (value.kind !== "sync" && value.kind !== "merge") return void 0;
|
|
3834
4078
|
return {
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
message,
|
|
3841
|
-
files: Object.keys(entries).length,
|
|
3842
|
-
bytes: Object.values(entries).reduce((total, bytes) => total + bytes.byteLength, 0),
|
|
3843
|
-
summary,
|
|
3844
|
-
pushed: published.pushed,
|
|
3845
|
-
...merged === void 0 ? {} : { merged },
|
|
3846
|
-
...published.reason === void 0 ? {} : { reason: published.reason },
|
|
3847
|
-
...compaction === void 0 ? {} : { compaction }
|
|
4079
|
+
at: value.at,
|
|
4080
|
+
kind: value.kind,
|
|
4081
|
+
commit: value.commit,
|
|
4082
|
+
pushed: value.pushed === true,
|
|
4083
|
+
...typeof value.added === "number" && Number.isInteger(value.added) && value.added >= 0 ? { added: value.added } : {}
|
|
3848
4084
|
};
|
|
3849
|
-
}
|
|
4085
|
+
} catch {
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
lastRunPath() {
|
|
4090
|
+
return join(this.store.directory(), "sync", LAST_RUN_FILE);
|
|
4091
|
+
}
|
|
4092
|
+
/** Remember one finished run. Bookkeeping never fails the run it describes. */
|
|
4093
|
+
recordRun(run) {
|
|
4094
|
+
const path = this.lastRunPath();
|
|
4095
|
+
const temporary = path + "." + String(process.pid) + ".tmp";
|
|
4096
|
+
try {
|
|
4097
|
+
mkdirSync(join(path, ".."), {
|
|
4098
|
+
recursive: true,
|
|
4099
|
+
mode: 448
|
|
4100
|
+
});
|
|
4101
|
+
writeFileSync(temporary, JSON.stringify({
|
|
4102
|
+
at: this.now().toISOString(),
|
|
4103
|
+
...run
|
|
4104
|
+
}, null, 2) + "\n", { mode: 384 });
|
|
4105
|
+
renameSync(temporary, path);
|
|
4106
|
+
} catch {
|
|
4107
|
+
rmSync(temporary, { force: true });
|
|
4108
|
+
}
|
|
3850
4109
|
}
|
|
3851
4110
|
/** Read the remote payload and report what it would change, without importing anything. */
|
|
3852
4111
|
async preview(signal) {
|
|
@@ -3855,7 +4114,7 @@ var MnemonGitSync = class {
|
|
|
3855
4114
|
const exported = await this.packs.exportPack("full");
|
|
3856
4115
|
return this.lock(async () => {
|
|
3857
4116
|
const remote = await this.readRemote(settings, signal);
|
|
3858
|
-
if (remote === void 0) throw
|
|
4117
|
+
if (remote === void 0) throw this.absentPayload(settings);
|
|
3859
4118
|
const local = unpack(Buffer.from(exported.base64, "base64"));
|
|
3860
4119
|
const components = MNEMON_PACK_COMPONENTS.map((component) => {
|
|
3861
4120
|
const published = remote.manifest.summary.find((entry) => entry.component === component);
|
|
@@ -3930,7 +4189,7 @@ var MnemonGitSync = class {
|
|
|
3930
4189
|
const exported = await this.packs.exportPack("full");
|
|
3931
4190
|
return this.lock(async () => {
|
|
3932
4191
|
const remote = await this.readRemote(settings, signal);
|
|
3933
|
-
if (remote === void 0) throw
|
|
4192
|
+
if (remote === void 0) throw this.absentPayload(settings);
|
|
3934
4193
|
const local = unpack(Buffer.from(exported.base64, "base64"));
|
|
3935
4194
|
const limits = this.runtimeLimits();
|
|
3936
4195
|
const mine = runtimeOf(local, limits, "the local");
|
|
@@ -4000,7 +4259,7 @@ var MnemonGitSync = class {
|
|
|
4000
4259
|
};
|
|
4001
4260
|
return this.lock(async () => {
|
|
4002
4261
|
const remote = await this.readRemote(settings, input.signal);
|
|
4003
|
-
if (remote === void 0) throw
|
|
4262
|
+
if (remote === void 0) throw this.absentPayload(settings);
|
|
4004
4263
|
const imported = await this.packs.importPack(Buffer.from(remote.archive).toString("base64"), {
|
|
4005
4264
|
...options,
|
|
4006
4265
|
tombstones: {
|
|
@@ -4008,6 +4267,12 @@ var MnemonGitSync = class {
|
|
|
4008
4267
|
tombstones: remote.tombstones
|
|
4009
4268
|
}
|
|
4010
4269
|
});
|
|
4270
|
+
this.recordRun({
|
|
4271
|
+
kind: "merge",
|
|
4272
|
+
commit: remote.commit,
|
|
4273
|
+
pushed: false,
|
|
4274
|
+
...imported.runtime === void 0 ? {} : { added: imported.runtime.added }
|
|
4275
|
+
});
|
|
4011
4276
|
return {
|
|
4012
4277
|
imported: true,
|
|
4013
4278
|
mode: imported.mode,
|
|
@@ -4025,11 +4290,11 @@ var MnemonGitSync = class {
|
|
|
4025
4290
|
});
|
|
4026
4291
|
}
|
|
4027
4292
|
absentPayload(settings) {
|
|
4028
|
-
return "the branch " + settings.branch + " holds no Mnemon payload at " + settings.subdir + " yet; push one first";
|
|
4293
|
+
return new MnemonSyncError("remote-empty", "the branch " + settings.branch + " holds no Mnemon payload at " + settings.subdir + " yet; push one first");
|
|
4029
4294
|
}
|
|
4030
4295
|
requireRepository() {
|
|
4031
4296
|
const settings = this.store.read();
|
|
4032
|
-
if (settings.repoUrl === void 0) throw new
|
|
4297
|
+
if (settings.repoUrl === void 0) throw new MnemonSyncError("no-repository", "no sync repository is configured; set repoUrl in " + this.store.path() + " first");
|
|
4033
4298
|
return {
|
|
4034
4299
|
...settings,
|
|
4035
4300
|
repoUrl: settings.repoUrl
|
|
@@ -4067,12 +4332,12 @@ var MnemonGitSync = class {
|
|
|
4067
4332
|
}
|
|
4068
4333
|
async requireGit(signal) {
|
|
4069
4334
|
const status = await this.gitStatus(signal);
|
|
4070
|
-
if (!status.available) throw new
|
|
4335
|
+
if (!status.available) throw new MnemonSyncError("git-missing", "Git is not available on this Host: " + (status.issue ?? "unknown reason"));
|
|
4071
4336
|
}
|
|
4072
4337
|
async remoteStatus(settings, signal) {
|
|
4073
4338
|
const credential = await this.credential(settings);
|
|
4074
4339
|
try {
|
|
4075
|
-
const tip = await this.lsRemote(settings, credential, signal);
|
|
4340
|
+
const tip = await this.lsRemote(settings, credential, signal, MNEMON_SYNC_STATUS_TIMEOUT_MS);
|
|
4076
4341
|
return tip === void 0 ? {
|
|
4077
4342
|
reachable: true,
|
|
4078
4343
|
branchExists: false
|
|
@@ -4082,26 +4347,40 @@ var MnemonGitSync = class {
|
|
|
4082
4347
|
commit: tip
|
|
4083
4348
|
};
|
|
4084
4349
|
} catch (error) {
|
|
4350
|
+
const message = this.mask(error instanceof Error ? error.message : String(error), credential);
|
|
4085
4351
|
return {
|
|
4086
4352
|
reachable: false,
|
|
4087
4353
|
branchExists: false,
|
|
4088
|
-
error:
|
|
4354
|
+
error: message,
|
|
4355
|
+
issue: remoteIssueOf(message)
|
|
4089
4356
|
};
|
|
4090
4357
|
}
|
|
4091
4358
|
}
|
|
4092
|
-
|
|
4359
|
+
/**
|
|
4360
|
+
* The branch tip the remote holds, or nothing when it holds no such branch. A
|
|
4361
|
+
* remote that cannot be read fails with the kind of failure it was, the token
|
|
4362
|
+
* masked, so the page can say "sign in" rather than print Git's trace.
|
|
4363
|
+
*/
|
|
4364
|
+
async lsRemote(settings, credential, signal, timeoutMs) {
|
|
4093
4365
|
const reference = "refs/heads/" + settings.branch;
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4366
|
+
let result;
|
|
4367
|
+
try {
|
|
4368
|
+
result = await this.git([
|
|
4369
|
+
"ls-remote",
|
|
4370
|
+
"--heads",
|
|
4371
|
+
settings.repoUrl,
|
|
4372
|
+
reference
|
|
4373
|
+
], {
|
|
4374
|
+
signal,
|
|
4375
|
+
token: credential.token,
|
|
4376
|
+
authenticated: true,
|
|
4377
|
+
...timeoutMs === void 0 ? {} : { timeoutMs }
|
|
4378
|
+
});
|
|
4379
|
+
} catch (error) {
|
|
4380
|
+
if (signal?.aborted === true) throw error;
|
|
4381
|
+
throw remoteFailure(this.mask(error instanceof Error ? error.message : String(error), credential));
|
|
4382
|
+
}
|
|
4383
|
+
if (result.exitCode !== 0) throw remoteFailure(this.mask(tail(result.stderr || result.stdout) || "git ls-remote failed", credential));
|
|
4105
4384
|
for (const line of result.stdout.split("\n")) {
|
|
4106
4385
|
const [commit, name] = line.trim().split(/\s+/u);
|
|
4107
4386
|
if (name === reference && commit !== void 0) return commit;
|
|
@@ -4218,6 +4497,20 @@ var MnemonGitSync = class {
|
|
|
4218
4497
|
recursive: true,
|
|
4219
4498
|
mode: 448
|
|
4220
4499
|
});
|
|
4500
|
+
if (existsSync(join(mirror, ".git"))) {
|
|
4501
|
+
const origin = await this.git([
|
|
4502
|
+
"config",
|
|
4503
|
+
"--get",
|
|
4504
|
+
ORIGIN_KEY
|
|
4505
|
+
], {
|
|
4506
|
+
cwd: mirror,
|
|
4507
|
+
signal
|
|
4508
|
+
});
|
|
4509
|
+
if (origin.exitCode !== 0 || origin.stdout.trim() !== settings.repoUrl) rmSync(mirror, {
|
|
4510
|
+
recursive: true,
|
|
4511
|
+
force: true
|
|
4512
|
+
});
|
|
4513
|
+
}
|
|
4221
4514
|
if (!existsSync(join(mirror, ".git"))) {
|
|
4222
4515
|
rmSync(mirror, {
|
|
4223
4516
|
recursive: true,
|
|
@@ -4226,10 +4519,18 @@ var MnemonGitSync = class {
|
|
|
4226
4519
|
const initialized = await this.git([
|
|
4227
4520
|
"init",
|
|
4228
4521
|
"--quiet",
|
|
4229
|
-
"--initial-branch=" + settings.branch,
|
|
4230
4522
|
mirror
|
|
4231
4523
|
], { signal });
|
|
4232
4524
|
if (initialized.exitCode !== 0) throw new Error("git init failed: " + tail(initialized.stderr || initialized.stdout));
|
|
4525
|
+
const marked = await this.git([
|
|
4526
|
+
"config",
|
|
4527
|
+
ORIGIN_KEY,
|
|
4528
|
+
settings.repoUrl
|
|
4529
|
+
], {
|
|
4530
|
+
cwd: mirror,
|
|
4531
|
+
signal
|
|
4532
|
+
});
|
|
4533
|
+
if (marked.exitCode !== 0) throw new Error("git config failed: " + tail(marked.stderr || marked.stdout));
|
|
4233
4534
|
}
|
|
4234
4535
|
const current = await this.git([
|
|
4235
4536
|
"symbolic-ref",
|
|
@@ -4251,7 +4552,10 @@ var MnemonGitSync = class {
|
|
|
4251
4552
|
if (renamed.exitCode !== 0) throw new Error("git symbolic-ref failed: " + tail(renamed.stderr || renamed.stdout));
|
|
4252
4553
|
}
|
|
4253
4554
|
const tip = await this.lsRemote(settings, credential, signal);
|
|
4254
|
-
if (tip === void 0)
|
|
4555
|
+
if (tip === void 0) {
|
|
4556
|
+
await this.startHistory(settings.branch, signal);
|
|
4557
|
+
return {};
|
|
4558
|
+
}
|
|
4255
4559
|
const fetched = await this.git([
|
|
4256
4560
|
"fetch",
|
|
4257
4561
|
"--no-tags",
|
|
@@ -4264,7 +4568,7 @@ var MnemonGitSync = class {
|
|
|
4264
4568
|
token: credential.token,
|
|
4265
4569
|
authenticated: true
|
|
4266
4570
|
});
|
|
4267
|
-
if (fetched.exitCode !== 0) throw
|
|
4571
|
+
if (fetched.exitCode !== 0) throw remoteFailure("git fetch failed: " + this.mask(tail(fetched.stderr || fetched.stdout), credential));
|
|
4268
4572
|
const reset = await this.git([
|
|
4269
4573
|
"reset",
|
|
4270
4574
|
"--hard",
|
|
@@ -4278,6 +4582,32 @@ var MnemonGitSync = class {
|
|
|
4278
4582
|
return { tip };
|
|
4279
4583
|
}
|
|
4280
4584
|
/**
|
|
4585
|
+
* Point the mirror at a branch the remote does not hold. The next commit starts a
|
|
4586
|
+
* history of its own: continuing the one this mirror kept, from a branch the remote
|
|
4587
|
+
* has since deleted, would publish every earlier payload all over again.
|
|
4588
|
+
*/
|
|
4589
|
+
async startHistory(branch, signal) {
|
|
4590
|
+
const mirror = this.store.mirror();
|
|
4591
|
+
await this.git([
|
|
4592
|
+
"update-ref",
|
|
4593
|
+
"-d",
|
|
4594
|
+
"refs/heads/" + branch
|
|
4595
|
+
], {
|
|
4596
|
+
cwd: mirror,
|
|
4597
|
+
signal
|
|
4598
|
+
});
|
|
4599
|
+
const emptied = await this.git(["read-tree", "--empty"], {
|
|
4600
|
+
cwd: mirror,
|
|
4601
|
+
signal
|
|
4602
|
+
});
|
|
4603
|
+
if (emptied.exitCode !== 0) throw new Error("git read-tree failed: " + tail(emptied.stderr || emptied.stdout));
|
|
4604
|
+
const cleaned = await this.git(["clean", "-fdxq"], {
|
|
4605
|
+
cwd: mirror,
|
|
4606
|
+
signal
|
|
4607
|
+
});
|
|
4608
|
+
if (cleaned.exitCode !== 0) throw new Error("git clean failed: " + tail(cleaned.stderr || cleaned.stdout));
|
|
4609
|
+
}
|
|
4610
|
+
/**
|
|
4281
4611
|
* The payload directory inside the mirror. A link where that directory or one of
|
|
4282
4612
|
* its parents should be would lead every read, removal and write out of the mirror,
|
|
4283
4613
|
* so a path that crosses one is refused before anything touches it.
|
|
@@ -4318,26 +4648,37 @@ var MnemonGitSync = class {
|
|
|
4318
4648
|
}
|
|
4319
4649
|
async publish(settings, credential, signal) {
|
|
4320
4650
|
const reference = "refs/heads/" + settings.branch + ":refs/heads/" + settings.branch;
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4651
|
+
let result;
|
|
4652
|
+
try {
|
|
4653
|
+
result = await this.git([
|
|
4654
|
+
"push",
|
|
4655
|
+
"--porcelain",
|
|
4656
|
+
settings.repoUrl,
|
|
4657
|
+
reference
|
|
4658
|
+
], {
|
|
4659
|
+
cwd: this.store.mirror(),
|
|
4660
|
+
signal,
|
|
4661
|
+
token: credential.token,
|
|
4662
|
+
authenticated: true
|
|
4663
|
+
});
|
|
4664
|
+
} catch (error) {
|
|
4665
|
+
if (signal?.aborted === true) throw error;
|
|
4666
|
+
const reason = this.mask(error instanceof Error ? error.message : String(error), credential);
|
|
4667
|
+
const failure = REMOTE_FAILURE[remoteIssueOf(reason)];
|
|
4668
|
+
return {
|
|
4669
|
+
pushed: false,
|
|
4670
|
+
reason,
|
|
4671
|
+
...failure === void 0 ? {} : { failure }
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4332
4674
|
if (result.exitCode === 0) return { pushed: true };
|
|
4333
|
-
const
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
reason: "the commit stays local because no token is available: " + (reason || "git push failed")
|
|
4337
|
-
};
|
|
4675
|
+
const output = this.mask(result.stdout + "\n" + result.stderr, credential);
|
|
4676
|
+
const reason = this.mask(tail(result.stderr || result.stdout), credential) || "git push failed";
|
|
4677
|
+
const failure = rejectedPush(output) ? "push-rejected" : REMOTE_FAILURE[remoteIssueOf(output)];
|
|
4338
4678
|
return {
|
|
4339
4679
|
pushed: false,
|
|
4340
|
-
reason:
|
|
4680
|
+
reason: credential.token === void 0 && failure !== "push-rejected" ? "the commit stays local because no token is available: " + reason : reason,
|
|
4681
|
+
...failure === void 0 ? {} : { failure }
|
|
4341
4682
|
};
|
|
4342
4683
|
}
|
|
4343
4684
|
/**
|
|
@@ -4410,9 +4751,12 @@ var MnemonGitSync = class {
|
|
|
4410
4751
|
};
|
|
4411
4752
|
}
|
|
4412
4753
|
}
|
|
4413
|
-
/**
|
|
4754
|
+
/** Refresh the mirror, then read the payload the branch holds. */
|
|
4414
4755
|
async readRemote(settings, signal) {
|
|
4415
|
-
|
|
4756
|
+
return this.readPrepared(settings, await this.ensureMirror(settings, signal));
|
|
4757
|
+
}
|
|
4758
|
+
/** Validate the payload a refreshed mirror holds and rebuild the exact pack archive the ZIP path would have produced. */
|
|
4759
|
+
async readPrepared(settings, prepared) {
|
|
4416
4760
|
if (prepared.tip === void 0) return void 0;
|
|
4417
4761
|
const files = readFiles(this.payloadRoot(settings.subdir));
|
|
4418
4762
|
const manifestBytes = files[MANIFEST];
|
|
@@ -4465,13 +4809,36 @@ var MnemonGitSync = class {
|
|
|
4465
4809
|
recursive: true,
|
|
4466
4810
|
mode: 448
|
|
4467
4811
|
});
|
|
4468
|
-
return withDirectoryLock(join(this.store.directory(), ".sync.lock"),
|
|
4812
|
+
return withDirectoryLock(join(this.store.directory(), ".sync.lock"), async () => {
|
|
4813
|
+
this.sweepCredentials();
|
|
4814
|
+
return operation();
|
|
4815
|
+
});
|
|
4816
|
+
}
|
|
4817
|
+
/**
|
|
4818
|
+
* Remove the one-command credential files a Host that stopped mid-command left
|
|
4819
|
+
* behind. Each holds a token, so one whose process is gone is deleted rather
|
|
4820
|
+
* than kept until the next restart of that process id.
|
|
4821
|
+
*/
|
|
4822
|
+
sweepCredentials() {
|
|
4823
|
+
let names;
|
|
4824
|
+
try {
|
|
4825
|
+
names = readdirSync(this.store.directory());
|
|
4826
|
+
} catch {
|
|
4827
|
+
return;
|
|
4828
|
+
}
|
|
4829
|
+
for (const name of names) {
|
|
4830
|
+
const match = /^\.credentials\.(\d+)\.\d+\.tmp$/u.exec(name);
|
|
4831
|
+
if (match === null) continue;
|
|
4832
|
+
const pid = Number(match[1]);
|
|
4833
|
+
if (pid === process.pid || processAlive(pid)) continue;
|
|
4834
|
+
rmSync(join(this.store.directory(), name), { force: true });
|
|
4835
|
+
}
|
|
4469
4836
|
}
|
|
4470
4837
|
async git(args, options) {
|
|
4471
4838
|
const prepared = this.credentials(args, options);
|
|
4472
4839
|
try {
|
|
4473
4840
|
return await this.run(GIT_LABEL, prepared.args, {
|
|
4474
|
-
timeoutMs:
|
|
4841
|
+
timeoutMs: options.timeoutMs ?? 12e4,
|
|
4475
4842
|
env: gitEnvironment(),
|
|
4476
4843
|
...options.cwd === void 0 ? {} : { cwd: options.cwd },
|
|
4477
4844
|
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
@@ -4520,11 +4887,17 @@ var MnemonGitSync = class {
|
|
|
4520
4887
|
};
|
|
4521
4888
|
}
|
|
4522
4889
|
};
|
|
4890
|
+
/**
|
|
4891
|
+
* How the payload files differ. The manifest and the checksum inventory describe a
|
|
4892
|
+
* payload rather than hold memory, and the manifest's export time differs on every
|
|
4893
|
+
* read, so counting them would report a difference where every component agrees.
|
|
4894
|
+
*/
|
|
4523
4895
|
function compareFiles(published, local) {
|
|
4524
4896
|
let changed = 0;
|
|
4525
4897
|
let added = 0;
|
|
4526
4898
|
let removed = 0;
|
|
4527
|
-
|
|
4899
|
+
const paths = new Set([...Object.keys(published), ...Object.keys(local)].filter((path) => path !== MANIFEST && path !== CHECKSUMS));
|
|
4900
|
+
for (const path of paths) {
|
|
4528
4901
|
const remote = published[path];
|
|
4529
4902
|
const mine = local[path];
|
|
4530
4903
|
if (remote === void 0) added += 1;
|
|
@@ -4532,13 +4905,46 @@ function compareFiles(published, local) {
|
|
|
4532
4905
|
else if (sha256Hex(remote) !== sha256Hex(mine)) changed += 1;
|
|
4533
4906
|
}
|
|
4534
4907
|
return {
|
|
4535
|
-
total:
|
|
4908
|
+
total: paths.size,
|
|
4536
4909
|
changed,
|
|
4537
4910
|
added,
|
|
4538
4911
|
removed
|
|
4539
4912
|
};
|
|
4540
4913
|
}
|
|
4541
|
-
/**
|
|
4914
|
+
/** Whether a process of this machine still runs; one the Host may not signal is assumed alive. */
|
|
4915
|
+
function processAlive(pid) {
|
|
4916
|
+
try {
|
|
4917
|
+
process.kill(pid, 0);
|
|
4918
|
+
return true;
|
|
4919
|
+
} catch (error) {
|
|
4920
|
+
return error.code === "EPERM";
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
/**
|
|
4924
|
+
* Whether a lock was left by an operation that can no longer release it: its owner
|
|
4925
|
+
* process is gone, or it is older than any sync takes. The age also covers a
|
|
4926
|
+
* process id that a restart handed to an unrelated process.
|
|
4927
|
+
*/
|
|
4928
|
+
function abandoned(path, now) {
|
|
4929
|
+
let age;
|
|
4930
|
+
try {
|
|
4931
|
+
age = now - statSync(path).mtimeMs;
|
|
4932
|
+
} catch {
|
|
4933
|
+
return false;
|
|
4934
|
+
}
|
|
4935
|
+
if (age > STALE_LOCK_MS) return true;
|
|
4936
|
+
try {
|
|
4937
|
+
const pid = record$3(JSON.parse(readFileSync(join(path, LOCK_OWNER), "utf8")))?.pid;
|
|
4938
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && pid !== process.pid && !processAlive(pid);
|
|
4939
|
+
} catch {
|
|
4940
|
+
return age > OWNERLESS_LOCK_MS;
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
/**
|
|
4944
|
+
* A single-writer lock for one storage root's sync work; the pack locks cover the
|
|
4945
|
+
* shared data. The lock records its owner, so a Host that stopped mid-operation
|
|
4946
|
+
* does not leave every later sync refused until someone deletes the directory.
|
|
4947
|
+
*/
|
|
4542
4948
|
async function withDirectoryLock(path, operation) {
|
|
4543
4949
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
4544
4950
|
let held = false;
|
|
@@ -4549,9 +4955,28 @@ async function withDirectoryLock(path, operation) {
|
|
|
4549
4955
|
});
|
|
4550
4956
|
held = true;
|
|
4551
4957
|
} catch {
|
|
4958
|
+
if (abandoned(path, Date.now())) {
|
|
4959
|
+
const aside = path + ".abandoned." + String(process.pid) + "." + String(Date.now());
|
|
4960
|
+
try {
|
|
4961
|
+
if (abandoned(path, Date.now())) renameSync(path, aside);
|
|
4962
|
+
} catch {
|
|
4963
|
+
continue;
|
|
4964
|
+
}
|
|
4965
|
+
rmSync(aside, {
|
|
4966
|
+
recursive: true,
|
|
4967
|
+
force: true
|
|
4968
|
+
});
|
|
4969
|
+
continue;
|
|
4970
|
+
}
|
|
4552
4971
|
await new Promise((resolveReady) => setTimeout(resolveReady, 25));
|
|
4553
4972
|
}
|
|
4554
|
-
if (!held) throw new
|
|
4973
|
+
if (!held) throw new MnemonSyncError("busy", "another Mnemon sync operation is still running");
|
|
4974
|
+
try {
|
|
4975
|
+
writeFileSync(join(path, LOCK_OWNER), JSON.stringify({
|
|
4976
|
+
pid: process.pid,
|
|
4977
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4978
|
+
}) + "\n", { mode: 384 });
|
|
4979
|
+
} catch {}
|
|
4555
4980
|
try {
|
|
4556
4981
|
return await operation();
|
|
4557
4982
|
} finally {
|
|
@@ -11056,12 +11481,13 @@ function success$1(value) {
|
|
|
11056
11481
|
};
|
|
11057
11482
|
}
|
|
11058
11483
|
function failure$1(error) {
|
|
11484
|
+
const reason = error instanceof Error ? error.reason : void 0;
|
|
11059
11485
|
return {
|
|
11060
11486
|
ok: false,
|
|
11061
11487
|
error: {
|
|
11062
11488
|
code: "internal",
|
|
11063
11489
|
message: error instanceof Error ? error.message : String(error),
|
|
11064
|
-
details: {}
|
|
11490
|
+
details: typeof reason === "string" ? { reason } : {}
|
|
11065
11491
|
}
|
|
11066
11492
|
};
|
|
11067
11493
|
}
|