@amalgm/shell 0.1.164-preview.36072132286 → 0.1.164-preview.36809057014
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/PURPOSE.md +642 -128
- package/README.md +10 -8
- package/dist/accepted-tail.d.ts +26 -0
- package/dist/accepted-tail.js +37 -0
- package/dist/accepted-tail.js.map +1 -0
- package/dist/agent-execution.d.ts +16 -1
- package/dist/agent-execution.js +13 -5
- package/dist/agent-execution.js.map +1 -1
- package/dist/apply-cleanup.d.ts +28 -0
- package/dist/apply-cleanup.js +166 -0
- package/dist/apply-cleanup.js.map +1 -0
- package/dist/apply-recovery.d.ts +47 -0
- package/dist/apply-recovery.js +99 -0
- package/dist/apply-recovery.js.map +1 -0
- package/dist/apply-rows.d.ts +72 -0
- package/dist/apply-rows.js +150 -0
- package/dist/apply-rows.js.map +1 -0
- package/dist/apply-saves.d.ts +85 -0
- package/dist/apply-saves.js +238 -0
- package/dist/apply-saves.js.map +1 -0
- package/dist/boot.js +2 -1
- package/dist/boot.js.map +1 -1
- package/dist/capture-work.d.ts +8 -0
- package/dist/capture-work.js +67 -0
- package/dist/capture-work.js.map +1 -0
- package/dist/cargo-group.d.ts +45 -0
- package/dist/cargo-group.js +88 -0
- package/dist/cargo-group.js.map +1 -0
- package/dist/channel-session.d.ts +4 -2
- package/dist/channel-session.js +4 -3
- package/dist/channel-session.js.map +1 -1
- package/dist/chat-composition.d.ts +2 -0
- package/dist/chat-composition.js +1 -0
- package/dist/chat-composition.js.map +1 -1
- package/dist/cli.js +20 -1
- package/dist/cli.js.map +1 -1
- package/dist/construct-apply-host.d.ts +4 -2
- package/dist/construct-apply-host.js +8 -3
- package/dist/construct-apply-host.js.map +1 -1
- package/dist/construct-door.js +1 -1
- package/dist/construct-door.js.map +1 -1
- package/dist/construct-ledger.d.ts +6 -1
- package/dist/construct-ledger.js +21 -33
- package/dist/construct-ledger.js.map +1 -1
- package/dist/content-cache-collection.d.ts +18 -0
- package/dist/content-cache-collection.js +99 -0
- package/dist/content-cache-collection.js.map +1 -0
- package/dist/content-cache-host.d.ts +83 -17
- package/dist/content-cache-host.js +335 -95
- package/dist/content-cache-host.js.map +1 -1
- package/dist/content-cache-namespace.d.ts +59 -0
- package/dist/content-cache-namespace.js +210 -0
- package/dist/content-cache-namespace.js.map +1 -0
- package/dist/content-cache-ownership.d.ts +13 -0
- package/dist/content-cache-ownership.js +75 -0
- package/dist/content-cache-ownership.js.map +1 -0
- package/dist/content-cache-retention.d.ts +23 -0
- package/dist/content-cache-retention.js +195 -0
- package/dist/content-cache-retention.js.map +1 -0
- package/dist/content-capture-pool.d.ts +287 -0
- package/dist/content-capture-pool.js +461 -0
- package/dist/content-capture-pool.js.map +1 -0
- package/dist/content-capture-worker.d.ts +8 -0
- package/dist/content-capture-worker.js +109 -0
- package/dist/content-capture-worker.js.map +1 -0
- package/dist/content-clock.d.ts +42 -0
- package/dist/content-clock.js +166 -0
- package/dist/content-clock.js.map +1 -0
- package/dist/content-direct.d.ts +2 -0
- package/dist/content-direct.js +12 -0
- package/dist/content-direct.js.map +1 -1
- package/dist/content-failure.d.ts +18 -0
- package/dist/content-failure.js +38 -0
- package/dist/content-failure.js.map +1 -0
- package/dist/content-stream-thread.d.ts +60 -0
- package/dist/content-stream-thread.js +95 -0
- package/dist/content-stream-thread.js.map +1 -0
- package/dist/content-stream-worker.d.ts +7 -0
- package/dist/content-stream-worker.js +52 -0
- package/dist/content-stream-worker.js.map +1 -0
- package/dist/declaration-absorption.d.ts +37 -0
- package/dist/declaration-absorption.js +119 -0
- package/dist/declaration-absorption.js.map +1 -0
- package/dist/declaration-chain.d.ts +36 -0
- package/dist/declaration-chain.js +96 -0
- package/dist/declaration-chain.js.map +1 -0
- package/dist/declaration-commit.d.ts +31 -0
- package/dist/declaration-commit.js +49 -0
- package/dist/declaration-commit.js.map +1 -0
- package/dist/declaration-hold.d.ts +22 -0
- package/dist/declaration-hold.js +24 -0
- package/dist/declaration-hold.js.map +1 -0
- package/dist/declaration-journal.d.ts +62 -0
- package/dist/declaration-journal.js +99 -0
- package/dist/declaration-journal.js.map +1 -0
- package/dist/declaration-save.d.ts +31 -0
- package/dist/declaration-save.js +134 -0
- package/dist/declaration-save.js.map +1 -0
- package/dist/detection/accepted-result.d.ts +19 -1
- package/dist/detection/accepted-result.js +36 -5
- package/dist/detection/accepted-result.js.map +1 -1
- package/dist/detection/admission-save.d.ts +55 -0
- package/dist/detection/admission-save.js +48 -0
- package/dist/detection/admission-save.js.map +1 -0
- package/dist/detection/admission-units.d.ts +66 -0
- package/dist/detection/admission-units.js +322 -0
- package/dist/detection/admission-units.js.map +1 -0
- package/dist/detection/alias-discovery.d.ts +12 -0
- package/dist/detection/alias-discovery.js +80 -0
- package/dist/detection/alias-discovery.js.map +1 -0
- package/dist/detection/alias-examination.d.ts +30 -0
- package/dist/detection/alias-examination.js +49 -0
- package/dist/detection/alias-examination.js.map +1 -0
- package/dist/detection/apply-ownership.d.ts +81 -0
- package/dist/detection/apply-ownership.js +232 -0
- package/dist/detection/apply-ownership.js.map +1 -0
- package/dist/detection/basis-advanced.d.ts +11 -0
- package/dist/detection/basis-advanced.js +26 -0
- package/dist/detection/basis-advanced.js.map +1 -0
- package/dist/detection/capture-outcome.d.ts +16 -0
- package/dist/detection/capture-outcome.js +92 -0
- package/dist/detection/capture-outcome.js.map +1 -0
- package/dist/detection/container-membership.d.ts +32 -0
- package/dist/detection/container-membership.js +49 -0
- package/dist/detection/container-membership.js.map +1 -0
- package/dist/detection/detect-cancelled.d.ts +6 -0
- package/dist/detection/detect-cancelled.js +10 -0
- package/dist/detection/detect-cancelled.js.map +1 -0
- package/dist/detection/directory-proof.d.ts +47 -0
- package/dist/detection/directory-proof.js +77 -0
- package/dist/detection/directory-proof.js.map +1 -0
- package/dist/detection/disposition-rows.d.ts +18 -0
- package/dist/detection/disposition-rows.js +29 -0
- package/dist/detection/disposition-rows.js.map +1 -0
- package/dist/detection/dispositions.d.ts +130 -0
- package/dist/detection/dispositions.js +248 -0
- package/dist/detection/dispositions.js.map +1 -0
- package/dist/detection/git-notice.d.ts +88 -0
- package/dist/detection/git-notice.js +444 -0
- package/dist/detection/git-notice.js.map +1 -0
- package/dist/detection/ground-proof.d.ts +130 -0
- package/dist/detection/ground-proof.js +274 -0
- package/dist/detection/ground-proof.js.map +1 -0
- package/dist/detection/ground-reader.d.ts +29 -0
- package/dist/detection/ground-reader.js +102 -0
- package/dist/detection/ground-reader.js.map +1 -0
- package/dist/detection/held-territory.d.ts +51 -0
- package/dist/detection/held-territory.js +205 -0
- package/dist/detection/held-territory.js.map +1 -0
- package/dist/detection/identity-discovery.d.ts +45 -0
- package/dist/detection/identity-discovery.js +287 -0
- package/dist/detection/identity-discovery.js.map +1 -0
- package/dist/detection/maintenance-sweep.d.ts +108 -0
- package/dist/detection/maintenance-sweep.js +325 -0
- package/dist/detection/maintenance-sweep.js.map +1 -0
- package/dist/detection/name-reach.d.ts +12 -0
- package/dist/detection/name-reach.js +28 -0
- package/dist/detection/name-reach.js.map +1 -0
- package/dist/detection/notebook-basis.d.ts +5 -7
- package/dist/detection/notebook-basis.js +6 -7
- package/dist/detection/notebook-basis.js.map +1 -1
- package/dist/detection/owed-selection.d.ts +51 -0
- package/dist/detection/owed-selection.js +169 -0
- package/dist/detection/owed-selection.js.map +1 -0
- package/dist/detection/portable.d.ts +34 -34
- package/dist/detection/portable.js +67 -93
- package/dist/detection/portable.js.map +1 -1
- package/dist/detection/reconciliation-coverage.d.ts +7 -0
- package/dist/detection/reconciliation-coverage.js +16 -0
- package/dist/detection/reconciliation-coverage.js.map +1 -0
- package/dist/detection/repository-publication.d.ts +20 -0
- package/dist/detection/repository-publication.js +79 -0
- package/dist/detection/repository-publication.js.map +1 -0
- package/dist/detection/repository-routes.d.ts +43 -0
- package/dist/detection/repository-routes.js +97 -0
- package/dist/detection/repository-routes.js.map +1 -0
- package/dist/detection/root-examination.d.ts +94 -0
- package/dist/detection/root-examination.js +523 -0
- package/dist/detection/root-examination.js.map +1 -0
- package/dist/detection/runtime.d.ts +131 -64
- package/dist/detection/runtime.js +808 -1103
- package/dist/detection/runtime.js.map +1 -1
- package/dist/detection/scoped-identity-port.d.ts +34 -0
- package/dist/detection/scoped-identity-port.js +81 -0
- package/dist/detection/scoped-identity-port.js.map +1 -0
- package/dist/detection/seal-law.d.ts +66 -0
- package/dist/detection/seal-law.js +116 -0
- package/dist/detection/seal-law.js.map +1 -0
- package/dist/detection/sharing-policy.d.ts +11 -0
- package/dist/detection/sharing-policy.js +32 -0
- package/dist/detection/sharing-policy.js.map +1 -0
- package/dist/detection/thread-database.d.ts +4 -0
- package/dist/detection/thread-database.js +30 -0
- package/dist/detection/thread-database.js.map +1 -0
- package/dist/detection/unreadable.d.ts +11 -0
- package/dist/detection/unreadable.js +9 -0
- package/dist/detection/unreadable.js.map +1 -0
- package/dist/detection/verified-content.d.ts +6 -0
- package/dist/detection/verified-content.js +12 -0
- package/dist/detection/verified-content.js.map +1 -0
- package/dist/detection/walk-law.d.ts +34 -0
- package/dist/detection/walk-law.js +55 -0
- package/dist/detection/walk-law.js.map +1 -0
- package/dist/development/fly-guest.js +2 -1
- package/dist/development/fly-guest.js.map +1 -1
- package/dist/entity-apply-host.d.ts +29 -13
- package/dist/entity-apply-host.js +1426 -461
- package/dist/entity-apply-host.js.map +1 -1
- package/dist/entity-apply-store.d.ts +77 -30
- package/dist/entity-apply-store.js +221 -516
- package/dist/entity-apply-store.js.map +1 -1
- package/dist/entity-record-observability.js +2 -0
- package/dist/entity-record-observability.js.map +1 -1
- package/dist/entity-record-saves.d.ts +20 -0
- package/dist/entity-record-saves.js +91 -0
- package/dist/entity-record-saves.js.map +1 -0
- package/dist/entity-record-store.d.ts +15 -7
- package/dist/entity-record-store.js +57 -117
- package/dist/entity-record-store.js.map +1 -1
- package/dist/files-register-host.d.ts +22 -3
- package/dist/files-register-host.js +70 -31
- package/dist/files-register-host.js.map +1 -1
- package/dist/filesystem.js +3 -2
- package/dist/filesystem.js.map +1 -1
- package/dist/git-binding.d.ts +65 -0
- package/dist/git-binding.js +220 -0
- package/dist/git-binding.js.map +1 -0
- package/dist/git-capture.d.ts +39 -0
- package/dist/git-capture.js +86 -0
- package/dist/git-capture.js.map +1 -0
- package/dist/git-card.d.ts +12 -0
- package/dist/git-card.js +123 -0
- package/dist/git-card.js.map +1 -0
- package/dist/git-checkpoint-construction.d.ts +41 -0
- package/dist/git-checkpoint-construction.js +96 -0
- package/dist/git-checkpoint-construction.js.map +1 -0
- package/dist/git-checkpoint-preparation.d.ts +37 -0
- package/dist/git-checkpoint-preparation.js +145 -0
- package/dist/git-checkpoint-preparation.js.map +1 -0
- package/dist/git-command.d.ts +61 -8
- package/dist/git-command.js +217 -63
- package/dist/git-command.js.map +1 -1
- package/dist/git-configuration.d.ts +40 -0
- package/dist/git-configuration.js +217 -0
- package/dist/git-configuration.js.map +1 -0
- package/dist/git-evidence.d.ts +61 -0
- package/dist/git-evidence.js +181 -0
- package/dist/git-evidence.js.map +1 -0
- package/dist/git-index-entries.d.ts +15 -0
- package/dist/git-index-entries.js +44 -0
- package/dist/git-index-entries.js.map +1 -0
- package/dist/git-index-format.d.ts +35 -0
- package/dist/git-index-format.js +293 -0
- package/dist/git-index-format.js.map +1 -0
- package/dist/git-index-witness.d.ts +33 -0
- package/dist/git-index-witness.js +69 -0
- package/dist/git-index-witness.js.map +1 -0
- package/dist/git-literal-host.d.ts +82 -0
- package/dist/git-literal-host.js +172 -0
- package/dist/git-literal-host.js.map +1 -0
- package/dist/git-literal-read.d.ts +88 -0
- package/dist/git-literal-read.js +170 -0
- package/dist/git-literal-read.js.map +1 -0
- package/dist/git-literal-replay.d.ts +33 -0
- package/dist/git-literal-replay.js +268 -0
- package/dist/git-literal-replay.js.map +1 -0
- package/dist/git-native-file.d.ts +42 -0
- package/dist/git-native-file.js +145 -0
- package/dist/git-native-file.js.map +1 -0
- package/dist/git-native-index.d.ts +21 -0
- package/dist/git-native-index.js +44 -0
- package/dist/git-native-index.js.map +1 -0
- package/dist/git-operation-host.d.ts +48 -0
- package/dist/git-operation-host.js +127 -0
- package/dist/git-operation-host.js.map +1 -0
- package/dist/git-private-index.d.ts +11 -0
- package/dist/git-private-index.js +42 -0
- package/dist/git-private-index.js.map +1 -0
- package/dist/git-private-repository.d.ts +4 -0
- package/dist/git-private-repository.js +69 -0
- package/dist/git-private-repository.js.map +1 -0
- package/dist/git-private-view.d.ts +39 -0
- package/dist/git-private-view.js +162 -0
- package/dist/git-private-view.js.map +1 -0
- package/dist/git-ref-store.d.ts +12 -0
- package/dist/git-ref-store.js +68 -0
- package/dist/git-ref-store.js.map +1 -0
- package/dist/git-registration-host.d.ts +20 -30
- package/dist/git-registration-host.js +86 -249
- package/dist/git-registration-host.js.map +1 -1
- package/dist/git-repository-host.d.ts +148 -22
- package/dist/git-repository-host.js +633 -650
- package/dist/git-repository-host.js.map +1 -1
- package/dist/git-worktree-host.d.ts +2 -0
- package/dist/git-worktree-host.js +16 -0
- package/dist/git-worktree-host.js.map +1 -0
- package/dist/graph-publication.d.ts +75 -0
- package/dist/graph-publication.js +203 -0
- package/dist/graph-publication.js.map +1 -0
- package/dist/ground-basis.d.ts +242 -0
- package/dist/ground-basis.js +1023 -0
- package/dist/ground-basis.js.map +1 -0
- package/dist/ground-checkpoint-worker.d.ts +1 -0
- package/dist/ground-checkpoint-worker.js +84 -0
- package/dist/ground-checkpoint-worker.js.map +1 -0
- package/dist/ground-checkpoints.d.ts +16 -0
- package/dist/ground-checkpoints.js +45 -0
- package/dist/ground-checkpoints.js.map +1 -0
- package/dist/ground-coordination.d.ts +107 -3
- package/dist/ground-coordination.js +249 -24
- package/dist/ground-coordination.js.map +1 -1
- package/dist/ground-database.d.ts +42 -0
- package/dist/ground-database.js +290 -0
- package/dist/ground-database.js.map +1 -0
- package/dist/ground-differences.d.ts +23 -0
- package/dist/ground-differences.js +47 -0
- package/dist/ground-differences.js.map +1 -0
- package/dist/ground-holders.d.ts +4 -0
- package/dist/ground-holders.js +15 -0
- package/dist/ground-holders.js.map +1 -0
- package/dist/ground-initialization-worker.d.ts +1 -0
- package/dist/ground-initialization-worker.js +20 -0
- package/dist/ground-initialization-worker.js.map +1 -0
- package/dist/ground-initialization.d.ts +35 -0
- package/dist/ground-initialization.js +102 -0
- package/dist/ground-initialization.js.map +1 -0
- package/dist/ground-install-saves.d.ts +63 -0
- package/dist/ground-install-saves.js +139 -0
- package/dist/ground-install-saves.js.map +1 -0
- package/dist/ground-inventory-schema.d.ts +18 -0
- package/dist/ground-inventory-schema.js +168 -0
- package/dist/ground-inventory-schema.js.map +1 -0
- package/dist/ground-profile.d.ts +7 -0
- package/dist/ground-profile.js +52 -0
- package/dist/ground-profile.js.map +1 -0
- package/dist/ground-root-index.d.ts +2 -1
- package/dist/ground-root-index.js +16 -14
- package/dist/ground-root-index.js.map +1 -1
- package/dist/ground-row.d.ts +62 -0
- package/dist/ground-row.js +70 -0
- package/dist/ground-row.js.map +1 -0
- package/dist/ground-save-ops.d.ts +44 -0
- package/dist/ground-save-ops.js +41 -0
- package/dist/ground-save-ops.js.map +1 -0
- package/dist/ground-saver-worker.d.ts +1 -0
- package/dist/ground-saver-worker.js +156 -0
- package/dist/ground-saver-worker.js.map +1 -0
- package/dist/ground-saver.d.ts +75 -0
- package/dist/ground-saver.js +216 -0
- package/dist/ground-saver.js.map +1 -0
- package/dist/ground-store.d.ts +84 -0
- package/dist/ground-store.js +473 -0
- package/dist/ground-store.js.map +1 -0
- package/dist/ground-territory.d.ts +130 -0
- package/dist/ground-territory.js +511 -0
- package/dist/ground-territory.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/installation/ownership.d.ts +0 -2
- package/dist/installation/ownership.js +3 -15
- package/dist/installation/ownership.js.map +1 -1
- package/dist/machine-host.d.ts +3 -2
- package/dist/machine-host.js +28 -16
- package/dist/machine-host.js.map +1 -1
- package/dist/materialized-graph.d.ts +7 -7
- package/dist/materialized-graph.js +11 -23
- package/dist/materialized-graph.js.map +1 -1
- package/dist/module-thread.d.ts +9 -0
- package/dist/module-thread.js +10 -0
- package/dist/module-thread.js.map +1 -0
- package/dist/native/fsevents-stream.node +0 -0
- package/dist/physical-path-witness.d.ts +64 -0
- package/dist/physical-path-witness.js +168 -0
- package/dist/physical-path-witness.js.map +1 -0
- package/dist/register-upload-fusion.d.ts +5 -4
- package/dist/register-upload-fusion.js +9 -6
- package/dist/register-upload-fusion.js.map +1 -1
- package/dist/registry-graph.d.ts +3 -2
- package/dist/registry-graph.js +9 -6
- package/dist/registry-graph.js.map +1 -1
- package/dist/registry-page-cache.d.ts +5 -2
- package/dist/registry-page-cache.js.map +1 -1
- package/dist/repository-identity-steps.d.ts +5 -0
- package/dist/repository-identity-steps.js +33 -0
- package/dist/repository-identity-steps.js.map +1 -0
- package/dist/runtime-node-flags.d.ts +10 -0
- package/dist/runtime-node-flags.js +11 -0
- package/dist/runtime-node-flags.js.map +1 -0
- package/dist/service/adapter.d.ts +1 -1
- package/dist/service/adapter.js +1 -1
- package/dist/service/adapter.js.map +1 -1
- package/dist/service/cli.js +16 -20
- package/dist/service/cli.js.map +1 -1
- package/dist/service/control.d.ts +2 -2
- package/dist/service/keepalive.d.ts +4 -11
- package/dist/service/keepalive.js +65 -118
- package/dist/service/keepalive.js.map +1 -1
- package/dist/service/policy.d.ts +36 -0
- package/dist/service/policy.js +25 -0
- package/dist/service/policy.js.map +1 -0
- package/dist/service/portable.d.ts +1 -1
- package/dist/service/portable.js +1 -1
- package/dist/service/portable.js.map +1 -1
- package/dist/service/recovery.d.ts +10 -9
- package/dist/service/recovery.js +29 -19
- package/dist/service/recovery.js.map +1 -1
- package/dist/service/windows-action.d.ts +5 -3
- package/dist/service/windows-action.js +6 -9
- package/dist/service/windows-action.js.map +1 -1
- package/dist/service/windows-task.js +5 -5
- package/dist/service/windows-task.js.map +1 -1
- package/dist/sharing-admission.d.ts +17 -0
- package/dist/sharing-admission.js +128 -0
- package/dist/sharing-admission.js.map +1 -0
- package/dist/snapshot-delivery-baseline.d.ts +12 -3
- package/dist/snapshot-delivery-baseline.js +41 -42
- package/dist/snapshot-delivery-baseline.js.map +1 -1
- package/dist/source-proof.d.ts +36 -0
- package/dist/source-proof.js +70 -0
- package/dist/source-proof.js.map +1 -0
- package/dist/source-signature.d.ts +56 -0
- package/dist/source-signature.js +56 -0
- package/dist/source-signature.js.map +1 -0
- package/dist/source-witness.d.ts +15 -0
- package/dist/source-witness.js +29 -0
- package/dist/source-witness.js.map +1 -0
- package/dist/sqlite-durability.d.ts +31 -0
- package/dist/sqlite-durability.js +28 -0
- package/dist/sqlite-durability.js.map +1 -0
- package/dist/sqlite.d.ts +8 -2
- package/dist/sqlite.js +27 -4
- package/dist/sqlite.js.map +1 -1
- package/dist/sync-address.d.ts +19 -0
- package/dist/sync-address.js +52 -0
- package/dist/sync-address.js.map +1 -0
- package/dist/sync-setting.d.ts +15 -0
- package/dist/sync-setting.js +13 -0
- package/dist/sync-setting.js.map +1 -1
- package/dist/turns.d.ts +42 -0
- package/dist/turns.js +112 -0
- package/dist/turns.js.map +1 -0
- package/dist/updates/bootstrap.d.ts +53 -7
- package/dist/updates/bootstrap.js +224 -137
- package/dist/updates/bootstrap.js.map +1 -1
- package/dist/updates/controller.d.ts +8 -0
- package/dist/updates/controller.js +27 -3
- package/dist/updates/controller.js.map +1 -1
- package/dist/updates/store.d.ts +3 -0
- package/dist/updates/store.js +12 -0
- package/dist/updates/store.js.map +1 -1
- package/dist/user-ground-host.d.ts +364 -69
- package/dist/user-ground-host.js +5231 -3366
- package/dist/user-ground-host.js.map +1 -1
- package/dist/watching/fsevents.d.ts +52 -1
- package/dist/watching/fsevents.js +60 -25
- package/dist/watching/fsevents.js.map +1 -1
- package/dist/watching/host.d.ts +129 -5
- package/dist/watching/host.js +433 -80
- package/dist/watching/host.js.map +1 -1
- package/dist/watching/index.d.ts +1 -1
- package/dist/wire-binary.d.ts +3 -2
- package/dist/wire-binary.js +10 -8
- package/dist/wire-binary.js.map +1 -1
- package/dist/wire-client.d.ts +3 -4
- package/dist/wire-client.js +18 -44
- package/dist/wire-client.js.map +1 -1
- package/dist/wire-outbox.d.ts +34 -0
- package/dist/wire-outbox.js +127 -0
- package/dist/wire-outbox.js.map +1 -0
- package/dist/workspace-host.d.ts +4 -0
- package/dist/workspace-host.js +12 -1
- package/dist/workspace-host.js.map +1 -1
- package/examinations/fixtures/private-wire-authority.js +133 -10
- package/npm-shrinkwrap.json +43 -56
- package/package.json +26 -13
|
@@ -1,343 +1,213 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { mkdtemp, open, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { gitText, runGit, runGitToFile, tryGit, tryGitText, } from "./git-command.js";
|
|
6
|
+
import { applyRepositoryIdentity, assertRepositoryTransportTruth, deriveCardId, deriveCheckpointId, deriveStateId, encodeRepositoryTransport, inspectRepositoryTransport, ITEMS_PER_BOUNDARY, repositoryIdentityDelta, repositoryIdentityHash, repositoryIdentityMap, repositoryPayloadVersion, repositoryTransportMetadataBytes, } from "@amalgm/live";
|
|
7
|
+
import { gitHost, gitText, runGit } from "./git-command.js";
|
|
8
|
+
import { busyMarker, gitOperationWitness, mergeOperation, REVEAL_MARKER } from "./git-operation-host.js";
|
|
10
9
|
import { atomicWrite } from "./filesystem.js";
|
|
10
|
+
import { repositoryIdentityMapSteps } from "./repository-identity-steps.js";
|
|
11
|
+
import { CaptureSourceChangedError, hashContentFile, writeCacheHint } from "./content-cache-host.js";
|
|
12
|
+
import { literalRepositoryIdentity, literalRepositoryIdentityMapSteps, collectLiteralRepositoryView, observeLiteralPaths, } from "./git-literal-host.js";
|
|
13
|
+
import { leafPayload } from "./git-literal-read.js";
|
|
14
|
+
import { PrivateGitView } from "./git-private-view.js";
|
|
15
|
+
import { installLiteralCheckpoint } from "./git-literal-replay.js";
|
|
16
|
+
import { prepareLiteralCheckpoint } from "./git-checkpoint-preparation.js";
|
|
17
|
+
import { checkpointReads, constructLiteralCheckpoint } from "./git-checkpoint-construction.js";
|
|
18
|
+
import { PhysicalPathWitnesses } from "./physical-path-witness.js";
|
|
11
19
|
import { recordFileDurability } from "./durability-barrier.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
import { GitCapture } from "./git-capture.js";
|
|
21
|
+
import { stashList } from "./git-card.js";
|
|
22
|
+
import { eachInTurns, finishStepsInTurns, turnBoundary } from "./turns.js";
|
|
23
|
+
import { looseRefs } from "./git-ref-store.js";
|
|
24
|
+
import { gitStorageDirectory } from "./git-binding.js";
|
|
25
|
+
import { admissionProof, closeSource, sourceProofs, witnessProof } from "./source-proof.js";
|
|
26
|
+
import { physicalKey, sourceSignature } from "./source-signature.js";
|
|
19
27
|
const sha256Hex = (input) => createHash("sha256").update(input).digest("hex");
|
|
20
28
|
const bufferView = (bytes) => Buffer.isBuffer(bytes)
|
|
21
29
|
? bytes
|
|
22
30
|
: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
/** What one lstat of a `.git` entry says. */
|
|
32
|
+
export const gitMarker = (marker) => ({ physicalKey: physicalKey(marker),
|
|
33
|
+
kind: marker.isDirectory() ? "directory" : marker.isFile() ? "file" : "other" });
|
|
34
|
+
/** The `.git` entry as it stands now; null when absent. */
|
|
35
|
+
export function observeGitMarker(directory) {
|
|
26
36
|
try {
|
|
27
|
-
|
|
28
|
-
return marker.isDirectory() || marker.isFile();
|
|
37
|
+
return gitMarker(lstatSync(join(directory, ".git"), { bigint: true }));
|
|
29
38
|
}
|
|
30
39
|
catch (error) {
|
|
31
40
|
const code = error.code;
|
|
32
41
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
33
|
-
return
|
|
42
|
+
return null;
|
|
34
43
|
throw error;
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!output)
|
|
43
|
-
return [];
|
|
44
|
-
return output.split("\n").map((line) => {
|
|
45
|
-
const separator = line.lastIndexOf(" ");
|
|
46
|
-
return { name: line.slice(0, separator), commit: line.slice(separator + 1) };
|
|
47
|
-
}).filter((ref) => ref.name !== "refs/stash" && !ref.name.startsWith("refs/amalgm-import/"))
|
|
48
|
-
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
49
|
-
}
|
|
50
|
-
/** The stash stack newest first, exactly as Git's reflog holds it. `%gs` is
|
|
51
|
-
* the reflog subject after Git's own whitespace normalization, so storing a
|
|
52
|
-
* captured message reads back unchanged. */
|
|
53
|
-
function stashList(repository) {
|
|
54
|
-
const output = gitText(repository, ["stash", "list", "--format=%H%x00%gs"]);
|
|
55
|
-
if (!output)
|
|
56
|
-
return [];
|
|
57
|
-
return output.split("\n").map((line) => {
|
|
58
|
-
const separator = line.indexOf("\0");
|
|
59
|
-
return { commit: line.slice(0, separator), message: line.slice(separator + 1) };
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
function remoteList(repository) {
|
|
63
|
-
const output = tryGitText(repository, ["config", "--get-regexp", "^remote\\..*\\.url$"]);
|
|
64
|
-
if (!output)
|
|
65
|
-
return [];
|
|
66
|
-
return output.split("\n").map((line) => {
|
|
67
|
-
const separator = line.indexOf(" ");
|
|
68
|
-
return { name: line.slice(7, separator - 4), url: line.slice(separator + 1) };
|
|
69
|
-
}).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
70
|
-
}
|
|
71
|
-
/** A shallow boundary is required to interpret the repository's object graph.
|
|
72
|
-
* It travels with the Git parcel, but does not change the semantic Card id. */
|
|
73
|
-
function shallowBoundary(repository) {
|
|
74
|
-
const file = resolve(repository, gitText(repository, ["rev-parse", "--git-path", "shallow"]));
|
|
75
|
-
if (!existsSync(file))
|
|
76
|
-
return [];
|
|
77
|
-
const commits = readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean).sort();
|
|
78
|
-
if (commits.some((commit) => !/^[0-9a-f]{40,64}$/.test(commit))) {
|
|
79
|
-
throw new Error("repository has an invalid shallow boundary");
|
|
80
|
-
}
|
|
81
|
-
return commits;
|
|
82
|
-
}
|
|
46
|
+
/** Classification observes only the filesystem marker. Git command health
|
|
47
|
+
* belongs to Card + Checkpoint capture and cannot demote repository ground. */
|
|
48
|
+
export const marksRepository = (marker) => marker !== null && marker.kind !== "other";
|
|
49
|
+
export const hasGitMarker = (directory) => marksRepository(observeGitMarker(directory));
|
|
50
|
+
/** The nested repositories a reading of `identity`'s repository must not enter. */
|
|
83
51
|
export function nestedRepositoryPaths(identity) {
|
|
84
52
|
return identity
|
|
85
53
|
.filter((entry) => entry.type === "repo.git")
|
|
86
54
|
.map((entry) => entry.path)
|
|
87
55
|
.sort();
|
|
88
56
|
}
|
|
89
|
-
function pathWithinRepositoryBoundary(path, boundaries) {
|
|
90
|
-
const candidate = path.endsWith("/") ? path.slice(0, -1) : path;
|
|
91
|
-
return boundaries.some((boundary) => candidate === boundary || candidate.startsWith(`${boundary}/`));
|
|
92
|
-
}
|
|
93
|
-
function worktreePathspec(boundaries, excludedPaths = []) {
|
|
94
|
-
return boundaries.length === 0 && excludedPaths.length === 0
|
|
95
|
-
? []
|
|
96
|
-
: [
|
|
97
|
-
"--", ".",
|
|
98
|
-
...boundaries.flatMap((path) => [`:(exclude)${path}`, `:(exclude)${path}/**`]),
|
|
99
|
-
...excludedPaths.map((path) => `:(exclude,literal)${path}`),
|
|
100
|
-
];
|
|
101
|
-
}
|
|
102
|
-
function portableWorktreePath(path) {
|
|
103
|
-
return path.length > 0 && !path.startsWith("/") && !path.includes("\0")
|
|
104
|
-
&& path.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
105
|
-
}
|
|
106
|
-
function worktreeLeaf(repository, path) {
|
|
107
|
-
if (!portableWorktreePath(path))
|
|
108
|
-
throw new Error(`repository has an unsafe conflicted path: ${path}`);
|
|
109
|
-
const absolutePath = join(repository, path);
|
|
110
|
-
try {
|
|
111
|
-
const stats = lstatSync(absolutePath);
|
|
112
|
-
if (stats.isSymbolicLink()) {
|
|
113
|
-
const content = Buffer.from(readlinkSync(absolutePath), "utf8");
|
|
114
|
-
return { mode: "120000", content, sha256: sha256Hex(content) };
|
|
115
|
-
}
|
|
116
|
-
if (stats.isDirectory())
|
|
117
|
-
return null;
|
|
118
|
-
const content = readFileSync(absolutePath);
|
|
119
|
-
return {
|
|
120
|
-
mode: stats.mode & 0o111 ? "100755" : "100644",
|
|
121
|
-
content,
|
|
122
|
-
sha256: sha256Hex(content),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
if (error.code === "ENOENT")
|
|
127
|
-
return null;
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function unmergedPaths(repository) {
|
|
132
|
-
const fields = runGit(repository, ["ls-files", "--unmerged", "-z"])
|
|
133
|
-
.toString("utf8").split("\0").filter(Boolean);
|
|
134
|
-
const grouped = new Map();
|
|
135
|
-
for (const field of fields) {
|
|
136
|
-
const separator = field.indexOf("\t");
|
|
137
|
-
const [mode, objectId, rawStage] = field.slice(0, separator).split(" ");
|
|
138
|
-
const path = field.slice(separator + 1);
|
|
139
|
-
const stage = Number(rawStage);
|
|
140
|
-
if (separator < 0 || !mode || !/^[0-9a-f]{40}([0-9a-f]{24})?$/.test(objectId ?? "")
|
|
141
|
-
|| ![1, 2, 3].includes(stage) || !portableWorktreePath(path)) {
|
|
142
|
-
throw new Error("Git returned an invalid unmerged index entry");
|
|
143
|
-
}
|
|
144
|
-
const entries = grouped.get(path) ?? [];
|
|
145
|
-
if (entries.some((entry) => entry.stage === stage)) {
|
|
146
|
-
throw new Error(`Git returned a duplicate unmerged index stage: ${path}`);
|
|
147
|
-
}
|
|
148
|
-
entries.push({ mode, objectId: objectId, stage: stage });
|
|
149
|
-
grouped.set(path, entries);
|
|
150
|
-
}
|
|
151
|
-
return [...grouped.entries()]
|
|
152
|
-
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
153
|
-
.map(([path, stages]) => ({
|
|
154
|
-
path,
|
|
155
|
-
stages: stages.sort((left, right) => left.stage - right.stage),
|
|
156
|
-
worktree: worktreeLeaf(repository, path),
|
|
157
|
-
}));
|
|
158
|
-
}
|
|
159
|
-
function optionalGitFile(repository, name) {
|
|
160
|
-
try {
|
|
161
|
-
return readFileSync(join(gitDirectory(repository), name));
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
if (error.code === "ENOENT")
|
|
165
|
-
return Buffer.alloc(0);
|
|
166
|
-
throw error;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function mergeOperation(repository) {
|
|
170
|
-
const heads = optionalGitFile(repository, "MERGE_HEAD").toString("utf8")
|
|
171
|
-
.split(/\r?\n/).filter(Boolean);
|
|
172
|
-
if (heads.length === 0)
|
|
173
|
-
return null;
|
|
174
|
-
if (heads.some((head) => !/^[0-9a-f]{40}([0-9a-f]{24})?$/.test(head))) {
|
|
175
|
-
throw new Error("repository has an invalid MERGE_HEAD");
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
kind: "merge",
|
|
179
|
-
heads,
|
|
180
|
-
message: optionalGitFile(repository, "MERGE_MSG"),
|
|
181
|
-
mode: optionalGitFile(repository, "MERGE_MODE"),
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
function untrackedFiles(repository, nestedRepositories) {
|
|
185
|
-
// Git ignore is not Amalgm enrollment policy. Every on-disk worktree leaf
|
|
186
|
-
// travels unless the registered root's .amalgmignore excludes it.
|
|
187
|
-
const output = runGit(repository, ["ls-files", "--others", "-z"])
|
|
188
|
-
.toString("utf8");
|
|
189
|
-
return output.split("\0").filter(Boolean).sort().flatMap((relativePath) => {
|
|
190
|
-
if (pathWithinRepositoryBoundary(relativePath, nestedRepositories))
|
|
191
|
-
return [];
|
|
192
|
-
if (relativePath.endsWith("/"))
|
|
193
|
-
return [];
|
|
194
|
-
const absolutePath = join(repository, relativePath);
|
|
195
|
-
try {
|
|
196
|
-
const stats = lstatSync(absolutePath);
|
|
197
|
-
if (stats.isSymbolicLink()) {
|
|
198
|
-
const content = Buffer.from(readlinkSync(absolutePath), "utf8");
|
|
199
|
-
return [{ path: relativePath, mode: "120000", content, sha256: sha256Hex(content) }];
|
|
200
|
-
}
|
|
201
|
-
if (stats.isDirectory())
|
|
202
|
-
return [];
|
|
203
|
-
const content = readFileSync(absolutePath);
|
|
204
|
-
return [{
|
|
205
|
-
path: relativePath,
|
|
206
|
-
mode: stats.mode & 0o111 ? "100755" : "100644",
|
|
207
|
-
content,
|
|
208
|
-
sha256: sha256Hex(content),
|
|
209
|
-
}];
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
if (error.code === "ENOENT")
|
|
213
|
-
return [];
|
|
214
|
-
throw error;
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
57
|
/** The common `.git` directory needs no Git invocation; a `.git` file
|
|
219
58
|
* (worktree, submodule) is resolved by Git itself. */
|
|
220
|
-
|
|
221
|
-
const common = join(repository, ".git");
|
|
222
|
-
try {
|
|
223
|
-
if (lstatSync(common).isDirectory())
|
|
224
|
-
return common;
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
// Resolved below.
|
|
228
|
-
}
|
|
229
|
-
return resolve(repository, gitText(repository, ["rev-parse", "--git-dir"]));
|
|
230
|
-
}
|
|
231
|
-
/** An Amalgm reveal rewrites a repository in place over several Git
|
|
232
|
-
* invocations. Like MERGE_HEAD, this file in the Git directory says the
|
|
233
|
-
* repository is mid-operation: capture refuses it (so Detect never records a
|
|
234
|
-
* torn moment as truth, and retries once it settles), while the reveal that
|
|
235
|
-
* owns it resumes from whatever state an interruption left behind. */
|
|
236
|
-
const REVEAL_MARKER = "amalgm-reveal";
|
|
59
|
+
const gitDirectory = (repository) => gitStorageDirectory(repository).directory;
|
|
237
60
|
export function markRepositoryReveal(repository) {
|
|
238
61
|
writeFileSync(join(gitDirectory(repository), REVEAL_MARKER), "");
|
|
239
62
|
}
|
|
240
63
|
export function repositoryRevealInProgress(repository) {
|
|
241
64
|
return hasGitMarker(repository) && existsSync(join(gitDirectory(repository), REVEAL_MARKER));
|
|
242
65
|
}
|
|
243
|
-
function busyMarker(repository, ownReveal) {
|
|
244
|
-
const directory = gitDirectory(repository);
|
|
245
|
-
return [...BUSY_MARKERS, ...(ownReveal ? [] : [REVEAL_MARKER])]
|
|
246
|
-
.find((marker) => existsSync(join(directory, marker))) ?? null;
|
|
247
|
-
}
|
|
248
66
|
const CAPTURE_ATTEMPTS = 3;
|
|
249
|
-
/**
|
|
250
|
-
*
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
67
|
+
/** The first attempt uses the caller's capture; a source that moved while
|
|
68
|
+
* it was read is asked again from a fresh one. */
|
|
69
|
+
async function retrySourceChange(capture, operation) {
|
|
70
|
+
for (let attempt = 0;; attempt += 1) {
|
|
71
|
+
try {
|
|
72
|
+
return await operation(attempt === 0 ? capture : new GitCapture(capture.repository));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!(error instanceof CaptureSourceChangedError) || attempt + 1 >= CAPTURE_ATTEMPTS)
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** One collected repository moment with its unclosed source. Detect retains
|
|
81
|
+
* the proof and closes it once, at the admission that trusts it; Apply's
|
|
82
|
+
* readback closes it itself. */
|
|
83
|
+
async function collectState(capture, nestedRepositories, lane, proven, ownReveal = false) {
|
|
84
|
+
const directory = gitDirectory(capture.repository);
|
|
85
|
+
// A busy repository is unsettled, never a fault: it reads again once the
|
|
86
|
+
// operation that owns the marker ends.
|
|
87
|
+
const busy = busyMarker(directory, ownReveal);
|
|
268
88
|
if (busy)
|
|
269
|
-
throw new
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
const operation = mergeOperation(repository);
|
|
275
|
-
const checkpointPathspec = worktreePathspec(nestedRepositories, unmerged.map((entry) => entry.path));
|
|
276
|
-
const card = {
|
|
277
|
-
version: REPOSITORY_CARD_VERSION,
|
|
278
|
-
head: { branch, commit, detached: branch === null && commit !== null },
|
|
279
|
-
refs: refList(repository),
|
|
280
|
-
stash: stashList(repository),
|
|
281
|
-
remotes: remoteList(repository),
|
|
282
|
-
...(shallow.length > 0 ? { shallow } : {}),
|
|
283
|
-
};
|
|
89
|
+
throw new CaptureSourceChangedError(`repository is busy: ${busy}`);
|
|
90
|
+
const configuration = await capture.configuration();
|
|
91
|
+
const { value: card, proof: cardProof } = await capture.card();
|
|
92
|
+
const operation = mergeOperation(directory);
|
|
93
|
+
const view = await collectLiteralRepositoryView(capture, card.head.commit, nestedRepositories, lane, proven);
|
|
284
94
|
const checkpoint = {
|
|
285
|
-
baseCommit: commit,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
"diff", "--cached", "--binary", "--full-index", commit ?? EMPTY_TREE,
|
|
289
|
-
...checkpointPathspec,
|
|
290
|
-
]),
|
|
291
|
-
unstagedPatch: runGit(repository, [
|
|
292
|
-
"diff", "--binary", "--full-index", ...checkpointPathspec,
|
|
293
|
-
]),
|
|
294
|
-
unmerged,
|
|
295
|
-
operation,
|
|
296
|
-
untracked: untrackedFiles(repository, nestedRepositories),
|
|
95
|
+
baseCommit: card.head.commit, branch: card.head.branch,
|
|
96
|
+
stagedPatch: view.stagedPatch, unstagedPatch: view.unstagedPatch,
|
|
97
|
+
unmerged: view.unmerged, untracked: view.untracked, operation,
|
|
297
98
|
};
|
|
298
99
|
const cardId = deriveCardId(card, sha256Hex);
|
|
299
100
|
const checkpointId = deriveCheckpointId(checkpoint, sha256Hex);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
101
|
+
// Operation and busy markers are native files in the observed Git
|
|
102
|
+
// directory, so admission re-reads them with the other physical witnesses.
|
|
103
|
+
const markers = witnessProof([gitOperationWitness(directory, ownReveal, operation)]);
|
|
104
|
+
const proof = sourceProofs([cardProof, configuration, markers, view.proof]);
|
|
105
|
+
return { capture, state: { stateId: deriveStateId(cardId, checkpointId, sha256Hex),
|
|
106
|
+
cardId, checkpointId, card, checkpoint, pack: null }, view, configuration, proof };
|
|
107
|
+
}
|
|
108
|
+
/** Physical observation for publication. Selection and package sealing have
|
|
109
|
+
* not happened; the caller retains this source proof, unclosed, and its
|
|
110
|
+
* admission closes it once. */
|
|
111
|
+
export function observeRepositoryForPublication(capture, identity, lane, proven) {
|
|
112
|
+
return retrySourceChange(capture, attempt => collectState(attempt, nestedRepositoryPaths(identity), lane, proven));
|
|
113
|
+
}
|
|
114
|
+
/** Seal an observation when publication selected every observed member. No
|
|
115
|
+
* second Git/worktree observation is necessary; the original proof still
|
|
116
|
+
* closes once, at admission. */
|
|
117
|
+
export async function captureObservedRepository(observed, identity, observedIdentity, prior, previousIdentity) {
|
|
118
|
+
return sealRepository(observed, await selectedLiteralIdentity(identity, observed.view, observedIdentity), prior, previousIdentity);
|
|
119
|
+
}
|
|
120
|
+
/** The literal identity of the selected members. Publication usually selects
|
|
121
|
+
* exactly the observed members, whose literal map the observation already
|
|
122
|
+
* derived: equal paths and UUIDs, where a leaf's type and payload come from
|
|
123
|
+
* the same view and every other entry must match as recorded. */
|
|
124
|
+
async function selectedLiteralIdentity(selected, view, observed) {
|
|
125
|
+
const derived = () => finishStepsInTurns(literalRepositoryIdentityMapSteps(selected, view));
|
|
126
|
+
if (selected.length !== observed.entries.length)
|
|
127
|
+
return derived();
|
|
128
|
+
const byPath = new Map();
|
|
129
|
+
await eachInTurns(observed.entries, entry => byPath.set(entry.path, entry));
|
|
130
|
+
let visited = 0;
|
|
131
|
+
for (const entry of selected) {
|
|
132
|
+
const other = byPath.get(entry.path);
|
|
133
|
+
if (other?.uuid !== entry.uuid || !(view.leaves.has(entry.path)
|
|
134
|
+
|| (other.type === entry.type && (other.payloadVersion ?? null) === (entry.payloadVersion ?? null))))
|
|
135
|
+
return derived();
|
|
136
|
+
if (++visited % ITEMS_PER_BOUNDARY === 0)
|
|
137
|
+
await turnBoundary();
|
|
138
|
+
}
|
|
139
|
+
return observed;
|
|
140
|
+
}
|
|
141
|
+
/** The same Card + Checkpoint without the untracked names a caller accounts
|
|
142
|
+
* for separately. */
|
|
143
|
+
function withoutUntracked(state, omitted) {
|
|
144
|
+
const checkpoint = { ...state.checkpoint, untracked: state.checkpoint.untracked.filter(file => !omitted(file.path)) };
|
|
145
|
+
const checkpointId = deriveCheckpointId(checkpoint, sha256Hex);
|
|
146
|
+
return { ...state, checkpoint, checkpointId, stateId: deriveStateId(state.cardId, checkpointId, sha256Hex) };
|
|
308
147
|
}
|
|
309
148
|
const cardTips = (card) => [...new Set([
|
|
310
149
|
...(card.head.commit ? [card.head.commit] : []),
|
|
311
|
-
...card.refs.
|
|
150
|
+
...card.refs.flatMap((ref) => ref.kind === "direct" ? [ref.commit] : []),
|
|
312
151
|
...card.stash.map((entry) => entry.commit),
|
|
313
152
|
])];
|
|
314
|
-
/**
|
|
153
|
+
/** The commits a state names: its Card tips and an unfinished merge's heads. */
|
|
154
|
+
const stateCommits = (state) => [...new Set([...cardTips(state.card), ...(state.checkpoint.operation?.heads ?? [])])];
|
|
155
|
+
/** Every object a state's pack delivers: its commits and each conflict
|
|
156
|
+
* stage. A gitlink records a commit owned by the nested repository, which
|
|
157
|
+
* the outer repository preserves as an opaque index value but never packs. */
|
|
158
|
+
const stateWants = (state) => [...new Set([...stateCommits(state),
|
|
159
|
+
...state.checkpoint.unmerged.flatMap((entry) => entry.stages
|
|
160
|
+
.filter((stage) => stage.mode !== "160000").map((stage) => stage.objectId))])];
|
|
161
|
+
/** The previous boundaries the receiver holds: those its tips reach under
|
|
162
|
+
* the horizon it received. */
|
|
163
|
+
async function receiverHolds(binding, horizon, tips, boundaries) {
|
|
164
|
+
const receiver = await PrivateGitView.open(binding, horizon);
|
|
165
|
+
try {
|
|
166
|
+
return [...(await receiver.reached(tips, boundaries)).keys()];
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
await receiver.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Produce the same native thin-pack work as upload-pack: current wants minus
|
|
315
173
|
* receiver haves. Ref updates and Amalgm metadata remain outside the pack. */
|
|
316
|
-
function nativePack(
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
// A gitlink records a commit owned by the nested repository. The outer
|
|
320
|
-
// repository preserves that opaque index value but can never pack it.
|
|
321
|
-
...current.checkpoint.unmerged.flatMap((entry) => entry.stages
|
|
322
|
-
.filter((stage) => stage.mode !== "160000")
|
|
323
|
-
.map((stage) => stage.objectId)),
|
|
324
|
-
...(current.checkpoint.operation?.heads ?? []),
|
|
325
|
-
])];
|
|
174
|
+
async function nativePack(binding, current, previous) {
|
|
175
|
+
const commits = stateCommits(current);
|
|
176
|
+
const wants = stateWants(current);
|
|
326
177
|
if (wants.length === 0)
|
|
327
178
|
return null;
|
|
328
|
-
const
|
|
329
|
-
const revisions = Buffer.from([
|
|
330
|
-
...wants,
|
|
331
|
-
...haves.map((commit) => `^${commit}`),
|
|
332
|
-
"",
|
|
333
|
-
].join("\n"), "utf8");
|
|
334
|
-
const scratch = mkdtempSync(join(tmpdir(), "amalgm-repo-pack-"));
|
|
179
|
+
const scratch = await mkdtemp(join(tmpdir(), "amalgm-repo-pack-"));
|
|
335
180
|
const file = join(scratch, "repository.pack");
|
|
336
181
|
try {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
182
|
+
const view = await PrivateGitView.open(binding, current.card.shallow);
|
|
183
|
+
try {
|
|
184
|
+
// The receiver holds each previous tip's closure under the previous
|
|
185
|
+
// horizon. A previous boundary this horizon moved past (Git's
|
|
186
|
+
// unshallow) is a commit it holds, if at all, without its parents. As
|
|
187
|
+
// upload-pack does, every traversal stops at those boundaries, so the
|
|
188
|
+
// haves subtract only what the receiver holds; each one this state
|
|
189
|
+
// reaches wants its parents, so the history newly exposed beneath it
|
|
190
|
+
// travels; and each one the receiver holds is an edge, so its tree
|
|
191
|
+
// never travels again.
|
|
192
|
+
const boundary = new Set(current.card.shallow ?? []);
|
|
193
|
+
const received = previous?.shallow ?? [];
|
|
194
|
+
const unshallowed = received.filter(commit => !boundary.has(commit));
|
|
195
|
+
const haves = previous ? await view.present(cardTips(previous)) : [];
|
|
196
|
+
const [exposed, held] = unshallowed.length ? await Promise.all([
|
|
197
|
+
view.reached(commits, unshallowed).then(found => [...found.values()].flat()),
|
|
198
|
+
receiverHolds(binding, received, haves, unshallowed),
|
|
199
|
+
]) : [[], []];
|
|
200
|
+
const revisions = Buffer.from([
|
|
201
|
+
...unshallowed.map(commit => `--shallow ${commit}`),
|
|
202
|
+
...new Set([...wants, ...exposed]),
|
|
203
|
+
...[...haves, ...held].map(commit => `^${commit}`), "",
|
|
204
|
+
].join("\n"), "utf8");
|
|
205
|
+
await view.exportPack(revisions, file, received.length > 0);
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
await view.close();
|
|
209
|
+
}
|
|
210
|
+
const bytes = await readFile(file);
|
|
341
211
|
if (bytes.byteLength < 12 || bytes.subarray(0, 4).toString("ascii") !== "PACK") {
|
|
342
212
|
throw new Error("Git produced an invalid repository pack");
|
|
343
213
|
}
|
|
@@ -346,73 +216,248 @@ function nativePack(repository, current, previous) {
|
|
|
346
216
|
return { bytes, sha256: sha256Hex(bytes) };
|
|
347
217
|
}
|
|
348
218
|
finally {
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
function identityFromCheckpoint(identity, state) {
|
|
353
|
-
const untracked = new Map(state.checkpoint.untracked.map((entry) => [entry.path, entry]));
|
|
354
|
-
return identity.map((entry) => {
|
|
355
|
-
const current = untracked.get(entry.path);
|
|
356
|
-
if (!current)
|
|
357
|
-
return entry;
|
|
358
|
-
const type = current.mode === "120000"
|
|
359
|
-
? "link"
|
|
360
|
-
: classifyFile({
|
|
361
|
-
binary: current.content === null
|
|
362
|
-
? entry.type === "file.binary"
|
|
363
|
-
: Buffer.from(current.content).includes(0),
|
|
364
|
-
});
|
|
365
|
-
return { ...entry, type, payloadVersion: current.sha256 };
|
|
366
|
-
});
|
|
219
|
+
await rm(scratch, { recursive: true, force: true });
|
|
220
|
+
}
|
|
367
221
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
222
|
+
/** Content-addressed cache bytes are evidence, not ground. A cached head that
|
|
223
|
+
* cannot prove the row's declared state is a cache miss, never a Detect fault. */
|
|
224
|
+
export async function readPriorRepositoryTransport(cacheDir, row) {
|
|
225
|
+
if (row?.type !== "repo.git" || !row.payloadVersion || !row.transportVersion)
|
|
226
|
+
return null;
|
|
227
|
+
const file = join(cacheDir, `${row.transportVersion}.bin`);
|
|
228
|
+
try {
|
|
229
|
+
const layout = await inspectRepositoryTransportFile(file);
|
|
230
|
+
if (repositoryPayloadVersion(layout, sha256Hex) === row.payloadVersion) {
|
|
231
|
+
return { stateId: layout.stateId, transportVersion: row.transportVersion, card: layout.card, identityHash: layout.identityHash };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error.code === "ENOENT")
|
|
236
|
+
return null;
|
|
237
|
+
// An unreadable envelope is the same miss.
|
|
238
|
+
}
|
|
239
|
+
await rm(file, { force: true });
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
export function pairRepositoryObservation(uuid, directory, observed, identityHash) {
|
|
243
|
+
const payloadVersion = repositoryPayloadVersion({ stateId: observed.state.stateId, identityHash }, sha256Hex);
|
|
244
|
+
return observationPair(uuid, directory, payloadVersion, observed.configuration, observed.view.sourceBinding, observed.proof);
|
|
245
|
+
}
|
|
246
|
+
/** The one pairing formula: a configuration stamp names the complete observed
|
|
247
|
+
* payload and the repository directory the Git binding resolved. */
|
|
248
|
+
function observationPair(uuid, directory, payloadVersion, configuration, binding, proof) {
|
|
249
|
+
// The binding's first identity is the checkout's exact `device:inode:mode`.
|
|
250
|
+
const [device, inode, mode] = binding.identities[0].split(":");
|
|
251
|
+
return Object.freeze({ uuid, absolutePath: directory, payloadVersion,
|
|
252
|
+
gitConfigurationObservation: configuration.stamp(uuid, payloadVersion, binding),
|
|
253
|
+
physicalKey: `${device}:${inode}`, filesystemMode: Number(mode), proof });
|
|
254
|
+
}
|
|
255
|
+
export function readRepository(repository, boundaries, lane, ownReveal = false) {
|
|
256
|
+
return retrySourceChange(new GitCapture(repository), attempt => readRepositoryAttempt(attempt, boundaries, lane, ownReveal));
|
|
257
|
+
}
|
|
258
|
+
const repositoryDirectoryStats = (path) => {
|
|
259
|
+
const stat = lstatSync(path, { bigint: true });
|
|
260
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
261
|
+
throw new CaptureSourceChangedError("repository directory changed kind");
|
|
262
|
+
return stat;
|
|
263
|
+
};
|
|
264
|
+
const repositoryDirectoryWitness = (path) => {
|
|
265
|
+
return sourceSignature(repositoryDirectoryStats(path));
|
|
266
|
+
};
|
|
267
|
+
/** Keep only witnesses; a cached Apply comparison must not retain patches,
|
|
268
|
+
* untracked contents, conflict contents or the construction view's closure. */
|
|
269
|
+
function repositoryNamespaceProof(source, witnesses) {
|
|
270
|
+
const ordered = [...witnesses].sort(([a], [b]) => a.length - b.length);
|
|
271
|
+
const check = (directory, witness) => {
|
|
272
|
+
if (repositoryDirectoryWitness(directory) !== witness) {
|
|
273
|
+
throw new CaptureSourceChangedError("repository namespace changed before reuse");
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
return sourceProofs([source, admissionProof(() => {
|
|
277
|
+
for (const [directory, witness] of ordered)
|
|
278
|
+
check(directory, witness);
|
|
279
|
+
for (let index = ordered.length - 1; index >= 0; index -= 1)
|
|
280
|
+
check(...ordered[index]);
|
|
281
|
+
})]);
|
|
282
|
+
}
|
|
283
|
+
async function readRepositoryAttempt(capture, boundaries, lane, ownReveal) {
|
|
284
|
+
const repository = capture.repository;
|
|
285
|
+
const { state, view, configuration, proof: source } = await collectState(capture, boundaries, lane, null, ownReveal);
|
|
286
|
+
const members = new Map();
|
|
287
|
+
const directoryWitnesses = new Map();
|
|
288
|
+
const walk = (directory, stats, prefix) => {
|
|
289
|
+
directoryWitnesses.set(directory, sourceSignature(stats));
|
|
290
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
291
|
+
if ([".git", ".amalgm"].includes(entry.name.toLowerCase()))
|
|
292
|
+
continue;
|
|
293
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
294
|
+
const absolute = join(directory, entry.name);
|
|
295
|
+
if (boundaries.includes(path))
|
|
296
|
+
members.set(path, { kind: "boundary", directory: entry.isDirectory() });
|
|
297
|
+
else if (!entry.isDirectory())
|
|
298
|
+
members.set(path, { kind: "leaf" });
|
|
299
|
+
else if (hasGitMarker(absolute))
|
|
300
|
+
members.set(path, { kind: "repository" });
|
|
301
|
+
else {
|
|
302
|
+
const child = repositoryDirectoryStats(absolute);
|
|
303
|
+
members.set(path, { kind: "directory", stats: child });
|
|
304
|
+
walk(absolute, child, path);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
walk(repository, repositoryDirectoryStats(repository), "");
|
|
309
|
+
const proof = repositoryNamespaceProof(source, directoryWitnesses);
|
|
310
|
+
await closeSource(proof);
|
|
311
|
+
return { repository, boundaries: [...boundaries], state, view, configuration, members, proof };
|
|
312
|
+
}
|
|
313
|
+
/** Judge one reading against one identity and the remainder it may keep.
|
|
314
|
+
* Remainder is accounted one member at a time, never by an excluded
|
|
315
|
+
* ancestor: it must keep its type and stay outside Git's index. */
|
|
316
|
+
export function judgeRepository(reading, identity, remainder) {
|
|
317
|
+
if (reading.boundaries.join("\0") !== nestedRepositoryPaths(identity).join("\0")) {
|
|
318
|
+
throw new Error("repository reading is judged only under the boundaries it read");
|
|
319
|
+
}
|
|
320
|
+
const { state, view, configuration, members, proof } = reading;
|
|
321
|
+
const expected = new Map(identity.map(entry => [entry.path, entry]));
|
|
322
|
+
const kept = new Map(remainder.map(entry => [entry.path, entry]));
|
|
323
|
+
const indexed = new Set(view.index.map(entry => entry.path));
|
|
324
|
+
const keptStats = new Map();
|
|
325
|
+
let identityMatches = true;
|
|
326
|
+
for (const [path, member] of members) {
|
|
327
|
+
if (member.kind === "boundary") {
|
|
328
|
+
if (!member.directory)
|
|
329
|
+
identityMatches = false;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const wanted = expected.get(path) ?? kept.get(path);
|
|
333
|
+
if (!wanted || (kept.has(path) && indexed.has(path)))
|
|
334
|
+
identityMatches = false;
|
|
335
|
+
if (member.kind === "repository")
|
|
336
|
+
identityMatches = false;
|
|
337
|
+
else if (member.kind === "directory") {
|
|
338
|
+
if (wanted?.type !== "folder")
|
|
339
|
+
identityMatches = false;
|
|
340
|
+
else if (kept.has(path))
|
|
341
|
+
keptStats.set(path, member.stats);
|
|
342
|
+
}
|
|
343
|
+
else if (!view.leaves.has(path))
|
|
344
|
+
identityMatches = false;
|
|
345
|
+
else if (kept.has(path)) {
|
|
346
|
+
const leaf = view.leaves.get(path);
|
|
347
|
+
if (leaf.type !== wanted.type)
|
|
348
|
+
identityMatches = false;
|
|
349
|
+
else
|
|
350
|
+
keptStats.set(path, leaf.stats);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (identity.some(entry => entry.type !== "repo.git" && !members.has(entry.path)))
|
|
354
|
+
identityMatches = false;
|
|
355
|
+
const observedIdentity = literalRepositoryIdentity(identity, view);
|
|
356
|
+
const expectedState = remainder.length === 0 ? state : withoutUntracked(state, path => !expected.has(path));
|
|
357
|
+
const observedRemainder = literalRepositoryIdentity(remainder.filter(entry => keptStats.has(entry.path)), view);
|
|
358
|
+
const observedPayload = repositoryPayloadVersion({ stateId: state.stateId,
|
|
359
|
+
identityHash: repositoryIdentityHash([...observedIdentity, ...observedRemainder], sha256Hex) }, sha256Hex);
|
|
360
|
+
const binding = view.sourceBinding;
|
|
361
|
+
return { stateId: expectedState.stateId, identityHash: repositoryIdentityHash(observedIdentity, sha256Hex),
|
|
362
|
+
identityMatches, observedPayload,
|
|
363
|
+
remainder: observedRemainder.map(entry => ({ entry, stats: keptStats.get(entry.path) })),
|
|
364
|
+
proof,
|
|
365
|
+
pair: (uuid, directory) => observationPair(uuid, directory, observedPayload, configuration, binding, proof) };
|
|
371
366
|
}
|
|
372
367
|
/** One observed repository moment reproduces one immutable transport exactly
|
|
373
|
-
* when its state
|
|
368
|
+
* when its complete Git state and identity map agree with it. */
|
|
374
369
|
export function repositoryObservationMatches(observed, identity, prior) {
|
|
375
|
-
return repositoryObservationMatchesState(observed, prior)
|
|
376
|
-
&& prior.identityHash === planRepositoryIdentity(identity, null, sha256Hex).identityHash;
|
|
377
|
-
}
|
|
378
|
-
/** Card + Checkpoint are the complete physical Git state. Callers that must
|
|
379
|
-
* also prove the transport's entity frontier use repositoryObservationMatches. */
|
|
380
|
-
export function repositoryObservationMatchesState(observed, prior) {
|
|
381
370
|
return observed.stateId === prior.stateId
|
|
382
|
-
&&
|
|
371
|
+
&& observed.identityMatches
|
|
372
|
+
&& observed.identityHash === prior.identityHash
|
|
373
|
+
&& prior.identityHash === repositoryIdentityMap(identity, sha256Hex).identityHash;
|
|
374
|
+
}
|
|
375
|
+
const ordinaryMember = (entry) => ["file.text", "file.binary", "link"].includes(entry.type);
|
|
376
|
+
/** Compile the selected working files over the observed native Git history,
|
|
377
|
+
* index and continuation. Retained members keep their advertised content
|
|
378
|
+
* witnesses; immutable byte sources never become physical stat evidence.
|
|
379
|
+
* Every other selected member is read once, on a content thread, and must
|
|
380
|
+
* reproduce the type and payload its entry declares. */
|
|
381
|
+
export async function captureSelectedRepository(observed, identity, retained, lane, prior, previousIdentity) {
|
|
382
|
+
const repository = observed.capture.repository;
|
|
383
|
+
const selected = new Map(identity.map(entry => [entry.path, entry]));
|
|
384
|
+
if (selected.size !== identity.length)
|
|
385
|
+
throw new Error("selected repository contains duplicate paths");
|
|
386
|
+
const examined = new Set();
|
|
387
|
+
const indexed = new Set(observed.view.index.map(entry => entry.path));
|
|
388
|
+
const boundaries = nestedRepositoryPaths(identity);
|
|
389
|
+
const additionalPaths = identity.filter(entry => ordinaryMember(entry) && !indexed.has(entry.path)).map(entry => entry.path);
|
|
390
|
+
const reads = (await checkpointReads(observed.view.index, boundaries, additionalPaths)).filter(({ path }) => {
|
|
391
|
+
const entry = selected.get(path);
|
|
392
|
+
return entry !== undefined && ordinaryMember(entry) && !retained.has(path);
|
|
393
|
+
});
|
|
394
|
+
const current = await observeLiteralPaths(repository, observed.view.format, reads, new PhysicalPathWitnesses(), lane);
|
|
395
|
+
const view = await PrivateGitView.open(observed.view.sourceBinding, observed.state.card.shallow);
|
|
396
|
+
let constructed;
|
|
397
|
+
try {
|
|
398
|
+
constructed = await constructLiteralCheckpoint({
|
|
399
|
+
view, index: observed.view.index, commit: observed.state.card.head.commit, boundaries, additionalPaths,
|
|
400
|
+
read: path => {
|
|
401
|
+
const entry = selected.get(path);
|
|
402
|
+
if (!entry || !ordinaryMember(entry))
|
|
403
|
+
return null;
|
|
404
|
+
const observation = current.get(path);
|
|
405
|
+
const leaf = retained.get(path) ?? (observation?.kind === "leaf" ? observation.leaf : null);
|
|
406
|
+
if (!leaf)
|
|
407
|
+
throw new Error(`selected observed repository member is unavailable: ${path}`);
|
|
408
|
+
const payload = leafPayload(entry.payloadVersion, view.format, leaf);
|
|
409
|
+
if (entry.type !== leaf.type || entry.payloadVersion !== payload) {
|
|
410
|
+
throw new Error(`selected repository source does not reproduce its member: ${path}`);
|
|
411
|
+
}
|
|
412
|
+
examined.add(path);
|
|
413
|
+
return leaf;
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
if (identity.some(entry => ordinaryMember(entry) && !examined.has(entry.path))) {
|
|
417
|
+
throw new Error("selected ordinary member overlaps an opaque Git owner");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
await view.close();
|
|
422
|
+
}
|
|
423
|
+
const checkpoint = { ...observed.state.checkpoint, ...constructed };
|
|
424
|
+
const checkpointId = deriveCheckpointId(checkpoint, sha256Hex);
|
|
425
|
+
const state = { ...observed.state, checkpoint, checkpointId,
|
|
426
|
+
stateId: deriveStateId(observed.state.cardId, checkpointId, sha256Hex) };
|
|
427
|
+
return sealRepository({ ...observed, state }, await finishStepsInTurns(repositoryIdentityMapSteps(identity)), prior, previousIdentity);
|
|
383
428
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const capturedIdentity = identityFromCheckpoint(identity, state);
|
|
389
|
-
if (prior && repositoryObservationMatches(state, capturedIdentity, prior)) {
|
|
390
|
-
return { ...prior, state, identity: capturedIdentity, bytes: null };
|
|
429
|
+
async function sealRepository(observed, identity, prior, previousIdentity) {
|
|
430
|
+
const { capture, state, view, configuration, proof } = observed;
|
|
431
|
+
if (prior && state.stateId === prior.stateId && identity.identityHash === prior.identityHash) {
|
|
432
|
+
return { ...prior, capture, state, view, configuration, identity, bytes: null, proof };
|
|
391
433
|
}
|
|
392
434
|
// A repository delta extends the immutable parent transport, never the
|
|
393
435
|
// machine's mutable SQLite projection. Detect may already have recorded a
|
|
394
436
|
// child move before the enclosing repository capture runs. If the proposed
|
|
395
437
|
// base does not reproduce the parent's declared identity map, seal one
|
|
396
438
|
// self-contained transport instead of emitting a lying delta.
|
|
397
|
-
const
|
|
398
|
-
&& repositoryIdentityHash(previousIdentity, sha256Hex) === prior.identityHash;
|
|
399
|
-
const identityPlan = planRepositoryIdentity(capturedIdentity, chainable ? previousIdentity : null, sha256Hex);
|
|
439
|
+
const previous = prior !== null && previousIdentity?.identityHash === prior.identityHash ? previousIdentity : null;
|
|
400
440
|
const captured = {
|
|
401
441
|
...state,
|
|
402
|
-
pack: nativePack(
|
|
442
|
+
pack: await nativePack(view.sourceBinding, state, previous ? prior.card : null),
|
|
403
443
|
};
|
|
404
444
|
const bytes = bufferView(encodeRepositoryTransport(captured, {
|
|
405
|
-
parentTransportVersion:
|
|
406
|
-
|
|
445
|
+
parentTransportVersion: previous ? prior.transportVersion : null,
|
|
446
|
+
identityHash: identity.identityHash,
|
|
447
|
+
identityDelta: repositoryIdentityDelta(identity, previous?.entries ?? null),
|
|
407
448
|
}));
|
|
408
449
|
return {
|
|
450
|
+
capture,
|
|
409
451
|
state: captured,
|
|
410
|
-
|
|
452
|
+
view,
|
|
453
|
+
configuration,
|
|
454
|
+
identity,
|
|
411
455
|
stateId: captured.stateId,
|
|
412
456
|
transportVersion: sha256Hex(bytes),
|
|
413
457
|
card: captured.card,
|
|
414
|
-
identityHash:
|
|
458
|
+
identityHash: identity.identityHash,
|
|
415
459
|
bytes,
|
|
460
|
+
proof,
|
|
416
461
|
};
|
|
417
462
|
}
|
|
418
463
|
// Git closes immutable objects after making them read-only. Its own writer
|
|
@@ -422,15 +467,16 @@ export function captureRepository(repository, identity, prior, previousIdentity
|
|
|
422
467
|
// user configuration. The final staging barrier still owns every other file.
|
|
423
468
|
const DURABLE_GIT_ARGS = ["-c", "core.fsync=all", "-c", "core.fsyncMethod=fsync"];
|
|
424
469
|
function runDurableGit(repository, args, input) {
|
|
425
|
-
return runGit(repository, [...DURABLE_GIT_ARGS, ...args], input);
|
|
470
|
+
return runGit(repository, [...DURABLE_GIT_ARGS, ...args], { input, purpose: "apply" });
|
|
426
471
|
}
|
|
427
472
|
/** Only these immutable object components are flushed by Git's writer
|
|
428
473
|
* contract. Other files, including pack retention markers, remain the final
|
|
429
474
|
* barrier's responsibility. The object directory comes from its owner, Git.
|
|
430
475
|
* Scan only fresh materialization; incremental Apply never walks old packs. */
|
|
431
|
-
function gitObjectFiles(repository) {
|
|
432
|
-
const
|
|
433
|
-
const
|
|
476
|
+
async function gitObjectFiles(repository) {
|
|
477
|
+
const binding = { purpose: "binding" };
|
|
478
|
+
const objects = resolve(repository, await gitText(repository, ["rev-parse", "--git-path", "objects"], binding));
|
|
479
|
+
const hashLength = await gitText(repository, ["rev-parse", "--show-object-format"], binding) === "sha256" ? 64 : 40;
|
|
434
480
|
const loose = new RegExp(`^[0-9a-f]{${hashLength - 2}}$`);
|
|
435
481
|
const packed = new RegExp(`^pack-[0-9a-f]{${hashLength}}\\.(pack|idx|rev)$`);
|
|
436
482
|
const files = [];
|
|
@@ -445,105 +491,77 @@ function gitObjectFiles(repository) {
|
|
|
445
491
|
}
|
|
446
492
|
return files;
|
|
447
493
|
}
|
|
448
|
-
function newlyDurableGitObjects(repository, priorFiles) {
|
|
449
|
-
return priorFiles === null ? [] : gitObjectFiles(repository)
|
|
494
|
+
async function newlyDurableGitObjects(repository, priorFiles) {
|
|
495
|
+
return priorFiles === null ? [] : (await gitObjectFiles(repository))
|
|
450
496
|
.filter((file) => !priorFiles.has(file)).map(recordFileDurability);
|
|
451
497
|
}
|
|
452
|
-
function installShallowBoundary(repository, commits) {
|
|
453
|
-
|
|
498
|
+
async function installShallowBoundary(repository, commits) {
|
|
499
|
+
const file = resolve(repository, await gitText(repository, ["rev-parse", "--git-path", "shallow"], { purpose: "binding" }));
|
|
500
|
+
if (commits.length === 0) {
|
|
501
|
+
rmSync(file, { force: true });
|
|
454
502
|
return;
|
|
455
|
-
|
|
503
|
+
}
|
|
456
504
|
mkdirSync(dirname(file), { recursive: true });
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
* `update-ref`
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
505
|
+
// Renamed over as a new inode, never written in place.
|
|
506
|
+
atomicWrite(file, `${[...commits].sort().join("\n")}\n`, 0o644);
|
|
507
|
+
}
|
|
508
|
+
/** Card refs replace the stored ref set in two steps, because Git 2.39's
|
|
509
|
+
* `update-ref --stdin` has no symbolic verb. One transaction writes every
|
|
510
|
+
* direct value and deletes every stored ref the Card lacks. Card names are
|
|
511
|
+
* literal, so no update or delete may follow a symbolic ref (a clone's
|
|
512
|
+
* `refs/remotes/origin/HEAD` would otherwise write `origin/main` twice), and
|
|
513
|
+
* only the command-line flag covers every command; `option no-deref` covers
|
|
514
|
+
* one. Each differing symbolic relationship is then set by name. The stored
|
|
515
|
+
* set includes the loose store, since for-each-ref omits a dangling or cyclic
|
|
516
|
+
* symbolic ref that must still go. Rerunning both steps is idempotent. */
|
|
517
|
+
async function forceRefs(repository, state) {
|
|
518
|
+
const wanted = new Set(state.card.refs.map((ref) => ref.name));
|
|
519
|
+
const listed = await gitText(repository, ["for-each-ref", "--format=%(refname)", "refs"], { purpose: "apply" });
|
|
520
|
+
const stored = looseRefs(repository);
|
|
467
521
|
const commands = [
|
|
468
|
-
...state.card.refs.
|
|
469
|
-
...(
|
|
470
|
-
.filter((
|
|
471
|
-
.map((
|
|
522
|
+
...state.card.refs.flatMap((ref) => ref.kind === "direct" ? [`update ${ref.name} ${ref.commit}`] : []),
|
|
523
|
+
...[...new Set([...(listed ? listed.split("\n") : []), ...stored.keys()])]
|
|
524
|
+
.filter((name) => name !== "refs/stash" && !wanted.has(name))
|
|
525
|
+
.map((name) => `delete ${name}`),
|
|
472
526
|
"",
|
|
473
527
|
];
|
|
474
|
-
runDurableGit(repository, ["update-ref", "--stdin"], Buffer.from(commands.join("\n"), "utf8"));
|
|
528
|
+
await runDurableGit(repository, ["update-ref", "--no-deref", "--stdin"], Buffer.from(commands.join("\n"), "utf8"));
|
|
529
|
+
for (const ref of state.card.refs) {
|
|
530
|
+
if (ref.kind !== "symbolic")
|
|
531
|
+
continue;
|
|
532
|
+
const current = stored.get(ref.name)?.();
|
|
533
|
+
if (current?.kind === "symbolic" && current.target === ref.target)
|
|
534
|
+
continue;
|
|
535
|
+
await runDurableGit(repository, ["symbolic-ref", ref.name, ref.target]);
|
|
536
|
+
}
|
|
475
537
|
}
|
|
476
538
|
/** Git keeps the stash stack only in the `refs/stash` reflog, so a differing
|
|
477
539
|
* stack is rebuilt by value: drop the ref and its log, then store every entry
|
|
478
540
|
* oldest first. An equal stack is left untouched. */
|
|
479
|
-
function forceStash(repository, state) {
|
|
541
|
+
async function forceStash(repository, state) {
|
|
480
542
|
const wanted = state.card.stash;
|
|
481
|
-
const current = stashList(repository);
|
|
543
|
+
const current = await stashList(repository);
|
|
482
544
|
if (current.length === wanted.length && current.every((entry, index) => entry.commit === wanted[index].commit && entry.message === wanted[index].message)) {
|
|
483
545
|
return;
|
|
484
546
|
}
|
|
485
|
-
runDurableGit(repository, ["update-ref", "-d", "refs/stash"]);
|
|
547
|
+
await runDurableGit(repository, ["update-ref", "-d", "refs/stash"]);
|
|
486
548
|
for (let index = wanted.length - 1; index >= 0; index -= 1) {
|
|
487
549
|
const entry = wanted[index];
|
|
488
|
-
runDurableGit(repository, ["stash", "store", `--message=${entry.message}`, entry.commit]);
|
|
550
|
+
await runDurableGit(repository, ["stash", "store", `--message=${entry.message}`, entry.commit]);
|
|
489
551
|
}
|
|
490
552
|
}
|
|
491
|
-
function forceHead(repository, state) {
|
|
553
|
+
async function forceHead(repository, state) {
|
|
492
554
|
const { head } = state.card;
|
|
493
|
-
if (head.
|
|
494
|
-
|
|
495
|
-
throw new Error("unborn repository state has no branch");
|
|
496
|
-
runDurableGit(repository, ["symbolic-ref", "HEAD", `refs/heads/${head.branch}`]);
|
|
497
|
-
runDurableGit(repository, ["read-tree", "--empty"]);
|
|
498
|
-
runDurableGit(repository, ["clean", "-fdq"]);
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
if (head.branch === null) {
|
|
502
|
-
runDurableGit(repository, ["-c", "advice.detachedHead=false", "checkout", "--force", "--quiet", "--detach", head.commit]);
|
|
555
|
+
if (head.branch !== null) {
|
|
556
|
+
await runDurableGit(repository, ["symbolic-ref", "HEAD", `refs/heads/${head.branch}`]);
|
|
503
557
|
}
|
|
504
|
-
else {
|
|
505
|
-
runDurableGit(repository, ["
|
|
506
|
-
runDurableGit(repository, ["reset", "--hard", "--quiet"]);
|
|
507
|
-
}
|
|
508
|
-
runDurableGit(repository, ["clean", "-fdq"]);
|
|
509
|
-
}
|
|
510
|
-
function writeCheckpointLeaf(repository, path, leaf) {
|
|
511
|
-
const absolutePath = join(repository, path);
|
|
512
|
-
if (leaf === null) {
|
|
513
|
-
rmSync(absolutePath, { recursive: true, force: true });
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
if (leaf.content === null)
|
|
517
|
-
throw new Error(`repository transport omitted conflict content for ${path}`);
|
|
518
|
-
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
519
|
-
if (leaf.mode === "120000") {
|
|
520
|
-
rmSync(absolutePath, { force: true });
|
|
521
|
-
symlinkSync(Buffer.from(leaf.content).toString("utf8"), absolutePath);
|
|
522
|
-
return;
|
|
558
|
+
else if (head.commit !== null) {
|
|
559
|
+
await runDurableGit(repository, ["update-ref", "--no-deref", "HEAD", head.commit]);
|
|
523
560
|
}
|
|
524
|
-
|
|
525
|
-
|
|
561
|
+
else
|
|
562
|
+
throw new Error("unborn repository state has no branch");
|
|
526
563
|
}
|
|
527
|
-
function
|
|
528
|
-
if (entries.length === 0)
|
|
529
|
-
return;
|
|
530
|
-
const objectFormat = gitText(repository, ["rev-parse", "--show-object-format"]);
|
|
531
|
-
const zero = "0".repeat(objectFormat === "sha256" ? 64 : 40);
|
|
532
|
-
const records = entries.flatMap((entry) => [
|
|
533
|
-
`0 ${zero}\t${entry.path}`,
|
|
534
|
-
...entry.stages.map((stage) => `${stage.mode} ${stage.objectId} ${stage.stage}\t${entry.path}`),
|
|
535
|
-
]);
|
|
536
|
-
runDurableGit(repository, ["update-index", "-z", "--index-info"], Buffer.from(`${records.join("\0")}\0`, "utf8"));
|
|
537
|
-
for (const entry of entries) {
|
|
538
|
-
// A conflict made only of gitlink stages belongs to the nested repository
|
|
539
|
-
// rail. The parent owns the opaque index entries, but never creates,
|
|
540
|
-
// replaces, or removes that nested worktree.
|
|
541
|
-
if (entry.stages.every((stage) => stage.mode === "160000"))
|
|
542
|
-
continue;
|
|
543
|
-
writeCheckpointLeaf(repository, entry.path, entry.worktree);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
function installMergeOperation(repository, operation) {
|
|
564
|
+
async function installMergeOperation(repository, operation) {
|
|
547
565
|
const directory = gitDirectory(repository);
|
|
548
566
|
for (const file of ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE"]) {
|
|
549
567
|
rmSync(join(directory, file), { force: true });
|
|
@@ -554,120 +572,34 @@ function installMergeOperation(repository, operation) {
|
|
|
554
572
|
writeFileSync(join(directory, "MERGE_MSG"), operation.message);
|
|
555
573
|
writeFileSync(join(directory, "MERGE_MODE"), operation.mode);
|
|
556
574
|
}
|
|
557
|
-
function
|
|
558
|
-
if (state.checkpoint.stagedPatch.length > 0) {
|
|
559
|
-
runDurableGit(repository, ["apply", "--index", "--binary", "--whitespace=nowarn"], state.checkpoint.stagedPatch);
|
|
560
|
-
}
|
|
561
|
-
if (state.checkpoint.unstagedPatch.length > 0) {
|
|
562
|
-
runDurableGit(repository, ["apply", "--binary", "--whitespace=nowarn"], state.checkpoint.unstagedPatch);
|
|
563
|
-
}
|
|
564
|
-
installUnmergedIndex(repository, state.checkpoint.unmerged);
|
|
565
|
-
installMergeOperation(repository, state.checkpoint.operation);
|
|
566
|
-
for (const file of state.checkpoint.untracked) {
|
|
567
|
-
if (file.content === null)
|
|
568
|
-
throw new Error(`repository transport omitted untracked content for ${file.path}`);
|
|
569
|
-
const absolutePath = join(repository, file.path);
|
|
570
|
-
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
571
|
-
if (file.mode === "120000") {
|
|
572
|
-
rmSync(absolutePath, { force: true });
|
|
573
|
-
symlinkSync(Buffer.from(file.content).toString("utf8"), absolutePath);
|
|
574
|
-
}
|
|
575
|
-
else {
|
|
576
|
-
writeFileSync(absolutePath, file.content);
|
|
577
|
-
chmodSync(absolutePath, file.mode === "100755" ? 0o755 : 0o644);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
function importPack(repository, state) {
|
|
582
|
-
if (!state.pack)
|
|
583
|
-
return;
|
|
584
|
-
if (sha256Hex(state.pack.bytes) !== state.pack.sha256) {
|
|
585
|
-
throw new Error("repository Git pack checksum mismatch");
|
|
586
|
-
}
|
|
587
|
-
runDurableGit(repository, [
|
|
588
|
-
"index-pack", "--stdin", "--fix-thin", "--keep=amalgm repository transport",
|
|
589
|
-
], state.pack.bytes);
|
|
590
|
-
}
|
|
591
|
-
function initializeMaterializedRepository(repository) {
|
|
575
|
+
async function initializeMaterializedRepository(repository, format) {
|
|
592
576
|
// Older Git accepts unknown -c keys without implementing their contract.
|
|
593
577
|
// The writer acknowledgement requires core.fsync/core.fsyncMethod, which
|
|
594
578
|
// became available in Git 2.36; admit that capability before any Git writes.
|
|
595
|
-
const version =
|
|
579
|
+
const { version } = gitHost();
|
|
596
580
|
const parsed = /^git version (\d+)\.(\d+)\.(\d+)(?:[.\s-]|$)/.exec(version);
|
|
597
581
|
if (!parsed || Number(parsed[1]) < 2 || (Number(parsed[1]) === 2 && Number(parsed[2]) < 36)) {
|
|
598
582
|
throw new Error(`Git 2.36 or newer is required for durable repository materialization; found ${version}`);
|
|
599
583
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
runDurableGit(repository, ["
|
|
584
|
+
if (hasGitMarker(repository) && await gitText(repository, ["rev-parse", "--show-object-format"], { purpose: "binding" }) !== format) {
|
|
585
|
+
throw new Error("repository object format differs from accepted state");
|
|
586
|
+
}
|
|
587
|
+
await runDurableGit(repository, ["init", "--quiet", `--object-format=${format}`, "--template="]);
|
|
604
588
|
}
|
|
605
|
-
function forceRemotes(repository, remotes) {
|
|
606
|
-
const existing = (
|
|
589
|
+
async function forceRemotes(repository, remotes) {
|
|
590
|
+
const existing = (await gitText(repository, ["remote"], { purpose: "apply" })).split("\n").filter(Boolean);
|
|
607
591
|
const wanted = new Map(remotes.map((remote) => [remote.name, remote.url]));
|
|
608
592
|
for (const name of existing) {
|
|
609
593
|
if (!wanted.has(name))
|
|
610
|
-
runDurableGit(repository, ["remote", "remove", name]);
|
|
594
|
+
await runDurableGit(repository, ["remote", "remove", name]);
|
|
611
595
|
}
|
|
612
596
|
for (const [name, url] of wanted) {
|
|
613
597
|
if (existing.includes(name))
|
|
614
|
-
runDurableGit(repository, ["remote", "set-url", name, url]);
|
|
598
|
+
await runDurableGit(repository, ["remote", "set-url", name, url]);
|
|
615
599
|
else
|
|
616
|
-
runDurableGit(repository, ["remote", "add", name, url]);
|
|
600
|
+
await runDurableGit(repository, ["remote", "add", name, url]);
|
|
617
601
|
}
|
|
618
602
|
}
|
|
619
|
-
/** Materialize a complete transport chain and then re-derive its semantic
|
|
620
|
-
* state. The oldest node must be self-contained; every later node must name
|
|
621
|
-
* the exact predecessor whose Git objects and identity map it extends. */
|
|
622
|
-
export function applyRepository(repository, chain) {
|
|
623
|
-
if (chain.length === 0)
|
|
624
|
-
throw new Error("repository transport chain is empty");
|
|
625
|
-
const transports = [];
|
|
626
|
-
let priorVersion = null;
|
|
627
|
-
let identity = null;
|
|
628
|
-
for (const bytes of chain) {
|
|
629
|
-
const state = decodeRepositoryTransport(bytes);
|
|
630
|
-
if (state.parentTransportVersion !== priorVersion) {
|
|
631
|
-
throw new Error("repository transport chain is discontinuous");
|
|
632
|
-
}
|
|
633
|
-
identity = applyRepositoryIdentity(identity, state, sha256Hex);
|
|
634
|
-
transports.push(state);
|
|
635
|
-
priorVersion = sha256Hex(bytes);
|
|
636
|
-
}
|
|
637
|
-
const state = transports.at(-1);
|
|
638
|
-
mkdirSync(repository, { recursive: true });
|
|
639
|
-
const fresh = !hasGitMarker(repository);
|
|
640
|
-
initializeMaterializedRepository(repository);
|
|
641
|
-
// An init template may carry pre-existing objects. Git has not necessarily
|
|
642
|
-
// written them, so their presence cannot become a durability acknowledgement.
|
|
643
|
-
const priorFiles = fresh ? new Set(gitObjectFiles(repository)) : null;
|
|
644
|
-
markRepositoryReveal(repository);
|
|
645
|
-
forceRemotes(repository, state.card.remotes);
|
|
646
|
-
for (const transport of transports)
|
|
647
|
-
importPack(repository, transport);
|
|
648
|
-
installShallowBoundary(repository, state.card.shallow ?? []);
|
|
649
|
-
forceRefs(repository, state);
|
|
650
|
-
forceStash(repository, state);
|
|
651
|
-
forceHead(repository, state);
|
|
652
|
-
applyCheckpoint(repository, state);
|
|
653
|
-
const proof = captureState(repository, nestedRepositoryPaths(identity), true);
|
|
654
|
-
if (proof.cardId !== state.cardId || proof.checkpointId !== state.checkpointId) {
|
|
655
|
-
throw new Error("materialized repository does not reproduce its declared state");
|
|
656
|
-
}
|
|
657
|
-
if (JSON.stringify(proof.card.shallow ?? []) !== JSON.stringify(state.card.shallow ?? [])) {
|
|
658
|
-
throw new Error("materialized repository does not reproduce its shallow boundary");
|
|
659
|
-
}
|
|
660
|
-
rmSync(join(gitDirectory(repository), REVEAL_MARKER), { force: true });
|
|
661
|
-
return {
|
|
662
|
-
stateId: state.stateId,
|
|
663
|
-
cardId: state.cardId,
|
|
664
|
-
checkpointId: state.checkpointId,
|
|
665
|
-
card: state.card,
|
|
666
|
-
transportVersion: priorVersion,
|
|
667
|
-
identity: identity,
|
|
668
|
-
durableFiles: newlyDurableGitObjects(repository, priorFiles),
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
603
|
function readExact(file, offset, bytes) {
|
|
672
604
|
const result = Buffer.allocUnsafe(bytes);
|
|
673
605
|
const descriptor = openSync(file, "r");
|
|
@@ -685,40 +617,75 @@ function readExact(file, offset, bytes) {
|
|
|
685
617
|
closeSync(descriptor);
|
|
686
618
|
}
|
|
687
619
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const
|
|
692
|
-
|
|
620
|
+
/** A transport's layout from its header alone; the pack and Checkpoint
|
|
621
|
+
* bytes it locates are never read. */
|
|
622
|
+
export async function inspectRepositoryTransportFile(file) {
|
|
623
|
+
const handle = await open(file, "r");
|
|
624
|
+
try {
|
|
625
|
+
const { size } = await handle.stat();
|
|
626
|
+
const head = async (bytes) => {
|
|
627
|
+
const result = Buffer.allocUnsafe(bytes);
|
|
628
|
+
for (let cursor = 0; cursor < bytes;) {
|
|
629
|
+
const { bytesRead } = await handle.read(result, cursor, bytes - cursor, cursor);
|
|
630
|
+
if (bytesRead === 0)
|
|
631
|
+
throw new Error("repository transport file is truncated");
|
|
632
|
+
cursor += bytesRead;
|
|
633
|
+
}
|
|
634
|
+
return result;
|
|
635
|
+
};
|
|
636
|
+
return inspectRepositoryTransport(await head(repositoryTransportMetadataBytes(await head(12))), size);
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
await handle.close();
|
|
640
|
+
}
|
|
693
641
|
}
|
|
694
642
|
const REPOSITORY_IDENTITY_CACHE_CONTRACT = "amalgm.repository-identity-cache.v1";
|
|
695
643
|
const repositoryIdentityCacheFile = (cacheDir, transportVersion) => join(cacheDir, "repository-identities", `${transportVersion}.json`);
|
|
644
|
+
/** Identity frontiers this process has proven, by immutable transport. A
|
|
645
|
+
* transport names exactly one map, so a proven frontier never goes stale; the
|
|
646
|
+
* bound, in entries, only limits memory. The newest frontier always stays. */
|
|
647
|
+
const provenFrontiers = new Map();
|
|
648
|
+
const PROVEN_FRONTIER_ENTRIES = 100_000;
|
|
649
|
+
let provenFrontierEntries = 0;
|
|
650
|
+
function rememberFrontier(transportVersion, identity) {
|
|
651
|
+
const known = provenFrontiers.get(transportVersion);
|
|
652
|
+
if (known)
|
|
653
|
+
provenFrontierEntries -= known.entries.length;
|
|
654
|
+
provenFrontiers.delete(transportVersion);
|
|
655
|
+
provenFrontiers.set(transportVersion, identity);
|
|
656
|
+
provenFrontierEntries += identity.entries.length;
|
|
657
|
+
for (const [version, frontier] of provenFrontiers) {
|
|
658
|
+
if (provenFrontierEntries <= PROVEN_FRONTIER_ENTRIES || version === transportVersion)
|
|
659
|
+
break;
|
|
660
|
+
provenFrontiers.delete(version);
|
|
661
|
+
provenFrontierEntries -= frontier.entries.length;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
696
664
|
/** Seal the compact full identity frontier beside the current transport. The
|
|
697
665
|
* evidence is disposable, but its content is accepted only when its hash
|
|
698
666
|
* equals the immutable transport header that names it. */
|
|
699
|
-
export function sealCachedRepositoryIdentity(cacheDir, transportVersion, identity, identityHash) {
|
|
700
|
-
|
|
701
|
-
if (planned.identityHash !== identityHash) {
|
|
667
|
+
export async function sealCachedRepositoryIdentity(cacheDir, transportVersion, identity, identityHash) {
|
|
668
|
+
if (identity.identityHash !== identityHash) {
|
|
702
669
|
throw new Error("repository identity evidence does not match its transport");
|
|
703
670
|
}
|
|
704
|
-
|
|
705
|
-
|
|
671
|
+
rememberFrontier(transportVersion, identity);
|
|
672
|
+
await writeCacheHint(cacheDir, repositoryIdentityCacheFile(cacheDir, transportVersion), Buffer.from(JSON.stringify({
|
|
706
673
|
contract: REPOSITORY_IDENTITY_CACHE_CONTRACT,
|
|
707
674
|
transportVersion,
|
|
708
675
|
identityHash,
|
|
709
|
-
identity:
|
|
710
|
-
}));
|
|
676
|
+
identity: identity.entries,
|
|
677
|
+
}), "utf8"));
|
|
711
678
|
}
|
|
712
|
-
function readSealedRepositoryIdentity(cacheDir, transportVersion, identityHash) {
|
|
679
|
+
async function readSealedRepositoryIdentity(cacheDir, transportVersion, identityHash) {
|
|
713
680
|
try {
|
|
714
|
-
const value = JSON.parse(
|
|
681
|
+
const value = JSON.parse(await readFile(repositoryIdentityCacheFile(cacheDir, transportVersion), "utf8"));
|
|
715
682
|
if (value.contract !== REPOSITORY_IDENTITY_CACHE_CONTRACT
|
|
716
683
|
|| value.transportVersion !== transportVersion
|
|
717
684
|
|| value.identityHash !== identityHash
|
|
718
685
|
|| !Array.isArray(value.identity))
|
|
719
686
|
return null;
|
|
720
|
-
const
|
|
721
|
-
return
|
|
687
|
+
const identity = await finishStepsInTurns(repositoryIdentityMapSteps(value.identity));
|
|
688
|
+
return identity.identityHash === identityHash ? identity : null;
|
|
722
689
|
}
|
|
723
690
|
catch {
|
|
724
691
|
return null;
|
|
@@ -728,12 +695,17 @@ function readSealedRepositoryIdentity(cacheDir, transportVersion, identityHash)
|
|
|
728
695
|
* repository transport. Compact sealed evidence survives ancestor-pack
|
|
729
696
|
* pruning; without it, a complete cached chain is replayed once and seals the
|
|
730
697
|
* frontier. No mutable entity row can substitute for either proof. */
|
|
731
|
-
export function readCachedRepositoryIdentity(cacheDir, transportVersion) {
|
|
698
|
+
export async function readCachedRepositoryIdentity(cacheDir, transportVersion) {
|
|
699
|
+
const proven = provenFrontiers.get(transportVersion);
|
|
700
|
+
if (proven)
|
|
701
|
+
return proven;
|
|
732
702
|
try {
|
|
733
|
-
const current = inspectRepositoryTransportFile(join(cacheDir, `${transportVersion}.bin`));
|
|
734
|
-
const sealed = readSealedRepositoryIdentity(cacheDir, transportVersion, current.identityHash);
|
|
735
|
-
if (sealed)
|
|
703
|
+
const current = await inspectRepositoryTransportFile(join(cacheDir, `${transportVersion}.bin`));
|
|
704
|
+
const sealed = await readSealedRepositoryIdentity(cacheDir, transportVersion, current.identityHash);
|
|
705
|
+
if (sealed) {
|
|
706
|
+
rememberFrontier(transportVersion, sealed);
|
|
736
707
|
return sealed;
|
|
708
|
+
}
|
|
737
709
|
const reversed = [];
|
|
738
710
|
const versions = new Set();
|
|
739
711
|
let version = transportVersion;
|
|
@@ -741,10 +713,7 @@ export function readCachedRepositoryIdentity(cacheDir, transportVersion) {
|
|
|
741
713
|
if (versions.has(version))
|
|
742
714
|
return null;
|
|
743
715
|
versions.add(version);
|
|
744
|
-
const
|
|
745
|
-
if (!existsSync(file))
|
|
746
|
-
return null;
|
|
747
|
-
const layout = inspectRepositoryTransportFile(file);
|
|
716
|
+
const layout = await inspectRepositoryTransportFile(join(cacheDir, `${version}.bin`));
|
|
748
717
|
reversed.push({ version, layout });
|
|
749
718
|
version = layout.parentTransportVersion;
|
|
750
719
|
}
|
|
@@ -756,61 +725,42 @@ export function readCachedRepositoryIdentity(cacheDir, transportVersion) {
|
|
|
756
725
|
identity = applyRepositoryIdentity(identity, node.layout, sha256Hex);
|
|
757
726
|
parent = node.version;
|
|
758
727
|
}
|
|
759
|
-
|
|
760
|
-
|
|
728
|
+
// Each applied node proved its declared map, the last one this header's.
|
|
729
|
+
const result = { entries: identity ?? [], identityHash: current.identityHash };
|
|
730
|
+
await sealCachedRepositoryIdentity(cacheDir, transportVersion, result, current.identityHash);
|
|
761
731
|
return result;
|
|
762
732
|
}
|
|
763
733
|
catch {
|
|
764
734
|
return null;
|
|
765
735
|
}
|
|
766
736
|
}
|
|
737
|
+
async function* hashed(source, hash) {
|
|
738
|
+
for await (const chunk of source) {
|
|
739
|
+
hash.update(chunk);
|
|
740
|
+
yield chunk;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
767
743
|
async function importPackFile(repository, file, pack) {
|
|
768
744
|
const hash = createHash("sha256");
|
|
769
|
-
const witness = new Transform({
|
|
770
|
-
transform(chunk, _encoding, callback) {
|
|
771
|
-
hash.update(chunk);
|
|
772
|
-
callback(null, chunk);
|
|
773
|
-
},
|
|
774
|
-
});
|
|
775
|
-
const child = spawn("git", [
|
|
776
|
-
...DURABLE_GIT_ARGS,
|
|
777
|
-
"index-pack", "--stdin", "--fix-thin", "--keep=amalgm repository transport",
|
|
778
|
-
], {
|
|
779
|
-
cwd: repository,
|
|
780
|
-
stdio: ["pipe", "ignore", "pipe"],
|
|
781
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_TEST_FSYNC: "1" },
|
|
782
|
-
});
|
|
783
|
-
const stderr = [];
|
|
784
|
-
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
785
|
-
const completed = new Promise((resolveChild, rejectChild) => {
|
|
786
|
-
child.once("error", rejectChild);
|
|
787
|
-
child.once("close", (code) => {
|
|
788
|
-
if (code === 0)
|
|
789
|
-
resolveChild();
|
|
790
|
-
else
|
|
791
|
-
rejectChild(new Error(`git index-pack failed${stderr.length
|
|
792
|
-
? `: ${Buffer.concat(stderr).toString("utf8").trim()}` : ""}`));
|
|
793
|
-
});
|
|
794
|
-
});
|
|
795
745
|
const source = createReadStream(file, {
|
|
796
746
|
start: pack.range.offset,
|
|
797
747
|
end: pack.range.offset + pack.range.bytes - 1,
|
|
798
748
|
});
|
|
799
|
-
await
|
|
749
|
+
await runDurableGit(repository, ["index-pack", "--stdin", "--fix-thin", "--keep=amalgm repository transport"], hashed(source, hash));
|
|
800
750
|
if (hash.digest("hex") !== pack.sha256)
|
|
801
751
|
throw new Error("repository Git pack checksum mismatch");
|
|
802
752
|
}
|
|
803
|
-
/**
|
|
804
|
-
* closes over its parent's
|
|
805
|
-
* first
|
|
753
|
+
/** A transport's pack delivers every object its state wants, and each pack
|
|
754
|
+
* closes over its parent's wants. Walking the chain from the newest node,
|
|
755
|
+
* the first whose wants the repository already holds ends what must be
|
|
806
756
|
* imported: only the ranges after it are absent. */
|
|
807
|
-
function firstAbsentTransport(repository, transports) {
|
|
757
|
+
async function firstAbsentTransport(repository, transports) {
|
|
808
758
|
for (let index = transports.length - 1; index >= 0; index -= 1) {
|
|
809
|
-
const
|
|
810
|
-
if (
|
|
759
|
+
const wants = stateWants(transports[index].layout);
|
|
760
|
+
if (wants.length === 0)
|
|
811
761
|
return index + 1;
|
|
812
|
-
const report = runGit(repository, ["cat-file", "--batch-check=%(objectname) %(objecttype)"], Buffer.from(`${
|
|
813
|
-
if (!report.split("\n").some((line) => line.endsWith(" missing")))
|
|
762
|
+
const report = await runGit(repository, ["cat-file", "--batch-check=%(objectname) %(objecttype)"], { input: Buffer.from(`${wants.join("\n")}\n`, "utf8"), purpose: "apply" });
|
|
763
|
+
if (!report.toString("utf8").split("\n").some((line) => line.endsWith(" missing")))
|
|
814
764
|
return index + 1;
|
|
815
765
|
}
|
|
816
766
|
return 0;
|
|
@@ -818,72 +768,69 @@ function firstAbsentTransport(repository, transports) {
|
|
|
818
768
|
function readRange(file, range) {
|
|
819
769
|
return range.bytes === 0 ? Buffer.alloc(0) : readExact(file, range.offset, range.bytes);
|
|
820
770
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
worktree: entry.worktree === null ? null : {
|
|
844
|
-
mode: entry.worktree.mode,
|
|
845
|
-
sha256: entry.worktree.sha256,
|
|
846
|
-
content,
|
|
771
|
+
/** A lazy byte view lets the portable validator govern file-backed replay
|
|
772
|
+
* without buffering the Git pack or all Checkpoint contents at once. Packs
|
|
773
|
+
* are hash-verified separately by the streaming importer before state writes. */
|
|
774
|
+
function repositoryTransportFromFile(file, layout) {
|
|
775
|
+
return {
|
|
776
|
+
...layout, pack: null,
|
|
777
|
+
checkpoint: {
|
|
778
|
+
...layout.checkpoint,
|
|
779
|
+
stagedPatch: readRange(file, layout.checkpoint.stagedPatch),
|
|
780
|
+
unstagedPatch: readRange(file, layout.checkpoint.unstagedPatch),
|
|
781
|
+
untracked: layout.checkpoint.untracked.map(entry => ({ ...entry,
|
|
782
|
+
get content() { return entry.content === null ? null : readRange(file, entry.content); },
|
|
783
|
+
})),
|
|
784
|
+
unmerged: layout.checkpoint.unmerged.map(entry => ({ ...entry,
|
|
785
|
+
worktree: entry.worktree === null ? null : { ...entry.worktree,
|
|
786
|
+
get content() { return entry.worktree.content === null ? null : readRange(file, entry.worktree.content); },
|
|
787
|
+
},
|
|
788
|
+
})),
|
|
789
|
+
operation: layout.checkpoint.operation === null ? null : {
|
|
790
|
+
...layout.checkpoint.operation,
|
|
791
|
+
message: readRange(file, layout.checkpoint.operation.message),
|
|
792
|
+
mode: readRange(file, layout.checkpoint.operation.mode),
|
|
847
793
|
},
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
794
|
+
},
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function checkpointFileSource(file, checkpoint) {
|
|
798
|
+
const content = (range) => {
|
|
799
|
+
if (range === null)
|
|
800
|
+
throw new Error("repository transport omitted checkpoint content");
|
|
801
|
+
return readRange(file, range);
|
|
802
|
+
};
|
|
803
|
+
return {
|
|
804
|
+
baseCommit: checkpoint.baseCommit,
|
|
805
|
+
stagedPatch: readRange(file, checkpoint.stagedPatch),
|
|
806
|
+
unstagedPatch: readRange(file, checkpoint.unstagedPatch),
|
|
807
|
+
untracked: checkpoint.untracked.map(entry => ({ ...entry, read: () => content(entry.content) })),
|
|
808
|
+
unmerged: checkpoint.unmerged.map(entry => ({ ...entry,
|
|
809
|
+
worktree: entry.worktree === null ? null : { ...entry.worktree, path: entry.path,
|
|
810
|
+
read: () => content(entry.worktree.content) },
|
|
811
|
+
})),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
async function applyCheckpointFile(repository, file, checkpoint, identity, ground, installMetadata) {
|
|
815
|
+
await installLiteralCheckpoint(repository, checkpointFileSource(file, checkpoint), identity, ground, installMetadata);
|
|
816
|
+
await installMergeOperation(repository, checkpoint.operation === null ? null : {
|
|
817
|
+
kind: checkpoint.operation.kind, heads: checkpoint.operation.heads,
|
|
818
|
+
message: readRange(file, checkpoint.operation.message), mode: readRange(file, checkpoint.operation.mode),
|
|
856
819
|
});
|
|
857
|
-
for (const entry of checkpoint.untracked) {
|
|
858
|
-
if (entry.content === null) {
|
|
859
|
-
throw new Error(`repository transport omitted untracked content for ${entry.path}`);
|
|
860
|
-
}
|
|
861
|
-
const content = readRange(file, entry.content);
|
|
862
|
-
if (sha256Hex(content) !== entry.sha256) {
|
|
863
|
-
throw new Error(`repository untracked content checksum mismatch for ${entry.path}`);
|
|
864
|
-
}
|
|
865
|
-
const absolutePath = join(repository, entry.path);
|
|
866
|
-
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
867
|
-
if (entry.mode === "120000") {
|
|
868
|
-
rmSync(absolutePath, { force: true });
|
|
869
|
-
symlinkSync(content.toString("utf8"), absolutePath);
|
|
870
|
-
}
|
|
871
|
-
else {
|
|
872
|
-
writeFileSync(absolutePath, content);
|
|
873
|
-
chmodSync(absolutePath, entry.mode === "100755" ? 0o755 : 0o644);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
820
|
}
|
|
877
|
-
/**
|
|
878
|
-
|
|
879
|
-
export async function applyRepositoryFiles(repository, chain) {
|
|
821
|
+
/** Shared immutable chain validation for retained contents and installation. */
|
|
822
|
+
async function readRepositoryChain(chain) {
|
|
880
823
|
if (chain.length === 0)
|
|
881
824
|
throw new Error("repository transport chain is empty");
|
|
882
825
|
const transports = [];
|
|
883
826
|
let priorVersion = null;
|
|
884
827
|
let identity = null;
|
|
885
828
|
for (const sealed of chain) {
|
|
886
|
-
|
|
829
|
+
if (await hashContentFile(sealed.file) !== sealed.transportVersion) {
|
|
830
|
+
throw new Error("repository transport failed its immutable hash");
|
|
831
|
+
}
|
|
832
|
+
const layout = await inspectRepositoryTransportFile(sealed.file);
|
|
833
|
+
assertRepositoryTransportTruth(repositoryTransportFromFile(sealed.file, layout), sha256Hex);
|
|
887
834
|
if (layout.parentTransportVersion !== priorVersion) {
|
|
888
835
|
throw new Error("repository transport chain is discontinuous");
|
|
889
836
|
}
|
|
@@ -892,40 +839,76 @@ export async function applyRepositoryFiles(repository, chain) {
|
|
|
892
839
|
priorVersion = sealed.transportVersion;
|
|
893
840
|
}
|
|
894
841
|
const latest = transports.at(-1);
|
|
842
|
+
return { transports, latest, identity: identity, transportVersion: priorVersion };
|
|
843
|
+
}
|
|
844
|
+
/** Recover retained working bytes from their immutable transport chain. This
|
|
845
|
+
* owns private Git objects only; it never checks out files or touches the live
|
|
846
|
+
* repository, its index, refs, or metadata. The caller must close the result. */
|
|
847
|
+
export async function prepareRepositoryFiles(chain) {
|
|
848
|
+
const { transports, latest, identity } = await readRepositoryChain(chain);
|
|
849
|
+
const scratch = await mkdtemp(join(tmpdir(), "amalgm-retained-repository-"));
|
|
850
|
+
let checkpoint;
|
|
851
|
+
try {
|
|
852
|
+
await runGit(scratch, ["init", "--quiet", `--object-format=${latest.layout.card.objectFormat}`, "--template="], { purpose: "apply" });
|
|
853
|
+
for (const transport of transports) {
|
|
854
|
+
if (transport.layout.pack)
|
|
855
|
+
await importPackFile(scratch, transport.file, transport.layout.pack);
|
|
856
|
+
}
|
|
857
|
+
checkpoint = await prepareLiteralCheckpoint(scratch, checkpointFileSource(latest.file, latest.layout.checkpoint), identity);
|
|
858
|
+
return {
|
|
859
|
+
layout: latest.layout, identity, checkpoint,
|
|
860
|
+
close: async () => { await checkpoint.close(); await rm(scratch, { recursive: true, force: true }); },
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
await checkpoint?.close();
|
|
865
|
+
await rm(scratch, { recursive: true, force: true });
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/** Apply sealed repository artifacts by streaming each Git range directly
|
|
870
|
+
* into index-pack. Download already verified each complete transport hash. */
|
|
871
|
+
export async function applyRepositoryFiles(repository, chain, ground) {
|
|
872
|
+
const { transports, latest, identity, transportVersion } = await readRepositoryChain(chain);
|
|
895
873
|
const state = latest.layout;
|
|
896
874
|
mkdirSync(repository, { recursive: true });
|
|
897
875
|
const fresh = !hasGitMarker(repository);
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
for (const transport of transports.slice(
|
|
876
|
+
await initializeMaterializedRepository(repository, state.card.objectFormat);
|
|
877
|
+
// An init template may carry pre-existing objects. Git has not necessarily
|
|
878
|
+
// written them, so their presence cannot become a durability acknowledgement.
|
|
879
|
+
const priorFiles = fresh ? new Set(await gitObjectFiles(repository)) : null;
|
|
880
|
+
const absent = fresh ? 0 : await firstAbsentTransport(repository, transports);
|
|
881
|
+
for (const transport of transports.slice(absent)) {
|
|
904
882
|
if (transport.layout.pack) {
|
|
905
883
|
await importPackFile(repository, transport.file, transport.layout.pack);
|
|
906
884
|
}
|
|
907
885
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
886
|
+
await applyCheckpointFile(repository, latest.file, state.checkpoint, identity, ground, async () => {
|
|
887
|
+
markRepositoryReveal(repository);
|
|
888
|
+
await installShallowBoundary(repository, state.card.shallow ?? []);
|
|
889
|
+
await forceRemotes(repository, state.card.remotes);
|
|
890
|
+
await forceRefs(repository, state);
|
|
891
|
+
await forceStash(repository, state);
|
|
892
|
+
await forceHead(repository, state);
|
|
893
|
+
});
|
|
894
|
+
// A target proof only: kept remainder is accounted by Verify, not here.
|
|
895
|
+
const kept = ground.kind === "working" ? ground.remainder : new Set();
|
|
896
|
+
// The readback closes before its reveal marker retires.
|
|
897
|
+
const reading = await readRepository(repository, nestedRepositoryPaths(identity), "apply", true);
|
|
898
|
+
const proof = withoutUntracked(reading.state, path => kept.has(path));
|
|
914
899
|
if (proof.cardId !== state.cardId || proof.checkpointId !== state.checkpointId) {
|
|
915
900
|
throw new Error("materialized repository does not reproduce its declared state");
|
|
916
901
|
}
|
|
917
|
-
if (JSON.stringify(proof.card.shallow ?? []) !== JSON.stringify(state.card.shallow ?? [])) {
|
|
918
|
-
throw new Error("materialized repository does not reproduce its shallow boundary");
|
|
919
|
-
}
|
|
920
902
|
rmSync(join(gitDirectory(repository), REVEAL_MARKER), { force: true });
|
|
921
903
|
return {
|
|
922
904
|
stateId: state.stateId,
|
|
923
905
|
cardId: state.cardId,
|
|
924
906
|
checkpointId: state.checkpointId,
|
|
925
907
|
card: state.card,
|
|
926
|
-
transportVersion
|
|
927
|
-
identity
|
|
928
|
-
durableFiles: newlyDurableGitObjects(repository, priorFiles),
|
|
908
|
+
transportVersion,
|
|
909
|
+
identity,
|
|
910
|
+
durableFiles: await newlyDurableGitObjects(repository, priorFiles),
|
|
911
|
+
reading,
|
|
929
912
|
};
|
|
930
913
|
}
|
|
931
914
|
//# sourceMappingURL=git-repository-host.js.map
|