@astrosheep/keiyaku 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -8
- package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +2 -2
- package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +4 -4
- package/build/integrations/marketplace/plugins/keiyaku/opencode.js +2 -2
- package/build/integrations/marketplace/plugins/keiyaku/package.json +2 -2
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +13 -5
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +91 -32
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +134 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/agents/openai.yaml +4 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +41 -6
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +262 -46
- package/build/src/akuma/abort.d.ts +2 -0
- package/build/src/akuma/abort.js +50 -0
- package/build/src/akuma/akuma.d.ts +50 -33
- package/build/src/akuma/akuma.js +175 -127
- package/build/src/akuma/archetype.d.ts +14 -5
- package/build/src/akuma/archetype.js +85 -39
- package/build/src/akuma/body.d.ts +3 -5
- package/build/src/akuma/body.js +490 -183
- package/build/src/akuma/coordinate.d.ts +9 -0
- package/build/src/akuma/coordinate.js +22 -0
- package/build/src/akuma/heart/facts.d.ts +88 -45
- package/build/src/akuma/heart/facts.js +19 -8
- package/build/src/akuma/heart/index.d.ts +64 -55
- package/build/src/akuma/heart/index.js +172 -252
- package/build/src/akuma/heart/rows.d.ts +38 -83
- package/build/src/akuma/heart/rows.js +117 -155
- package/build/src/akuma/heart/schema.d.ts +1 -1
- package/build/src/akuma/heart/schema.js +61 -22
- package/build/src/akuma/heart/soul.d.ts +8 -0
- package/build/src/akuma/heart/soul.js +136 -0
- package/build/src/akuma/heart/storage.d.ts +47 -0
- package/build/src/akuma/heart/storage.js +224 -0
- package/build/src/akuma/heart/tells.d.ts +12 -0
- package/build/src/akuma/heart/tells.js +79 -0
- package/build/src/akuma/heart/timeline.d.ts +15 -0
- package/build/src/akuma/heart/timeline.js +86 -0
- package/build/src/akuma/identity.d.ts +2 -2
- package/build/src/akuma/identity.js +9 -9
- package/build/src/akuma/index.d.ts +10 -3
- package/build/src/akuma/index.js +7 -1
- package/build/src/akuma/projection.d.ts +176 -0
- package/build/src/akuma/projection.js +256 -0
- package/build/src/akuma/provider-recipe.d.ts +24 -0
- package/build/src/akuma/provider-recipe.js +100 -0
- package/build/src/akuma/provider.d.ts +67 -13
- package/build/src/akuma/provider.js +172 -105
- package/build/src/akuma/providers/acp/core.d.ts +19 -0
- package/build/src/akuma/providers/acp/core.js +159 -0
- package/build/src/akuma/providers/acp/events.d.ts +20 -0
- package/build/src/akuma/providers/acp/events.js +73 -0
- package/build/src/akuma/providers/acp/index.d.ts +12 -0
- package/build/src/akuma/providers/acp/index.js +94 -0
- package/build/src/akuma/providers/{claude.d.ts → claude/events.d.ts} +9 -41
- package/build/src/akuma/providers/claude/events.js +238 -0
- package/build/src/akuma/providers/claude/index.d.ts +47 -0
- package/build/src/akuma/providers/claude/index.js +279 -0
- package/build/src/akuma/providers/claude/input.d.ts +13 -0
- package/build/src/akuma/providers/claude/input.js +109 -0
- package/build/src/akuma/providers/codex-app-server/events.d.ts +1 -1
- package/build/src/akuma/providers/codex-app-server/events.js +15 -12
- package/build/src/akuma/providers/codex-app-server/index.d.ts +15 -13
- package/build/src/akuma/providers/codex-app-server/index.js +77 -33
- package/build/src/akuma/providers/grok-build/index.d.ts +4 -0
- package/build/src/akuma/providers/grok-build/index.js +70 -0
- package/build/src/akuma/providers/index.d.ts +5 -3
- package/build/src/akuma/providers/index.js +35 -56
- package/build/src/akuma/providers/opencode-sdk/events.d.ts +54 -0
- package/build/src/akuma/providers/opencode-sdk/events.js +235 -0
- package/build/src/akuma/providers/opencode-sdk/index.d.ts +33 -0
- package/build/src/akuma/providers/opencode-sdk/index.js +308 -0
- package/build/src/akuma/providers/opencode-sdk/session.d.ts +26 -0
- package/build/src/akuma/providers/opencode-sdk/session.js +63 -0
- package/build/src/akuma/providers/pi/events.d.ts +33 -0
- package/build/src/akuma/providers/pi/events.js +166 -0
- package/build/src/akuma/providers/pi/index.d.ts +11 -0
- package/build/src/akuma/providers/pi/index.js +191 -0
- package/build/src/akuma/publication.d.ts +3 -3
- package/build/src/akuma/publication.js +28 -40
- package/build/src/akuma/requests.d.ts +9 -6
- package/build/src/akuma/requests.js +95 -65
- package/build/src/alias/index.d.ts +4 -3
- package/build/src/alias/index.js +12 -13
- package/build/src/body/decode.js +7 -1
- package/build/src/body/region.d.ts +2 -0
- package/build/src/body/region.js +77 -7
- package/build/src/body/render.js +2 -1
- package/build/src/body/shape.js +7 -1
- package/build/src/body/verification.js +19 -4
- package/build/src/cli/accepted.d.ts +18 -12
- package/build/src/cli/accepted.js +73 -34
- package/build/src/cli/actor.js +3 -3
- package/build/src/cli/commands/akuma-invoke.d.ts +44 -19
- package/build/src/cli/commands/akuma-invoke.js +101 -52
- package/build/src/cli/commands/akuma.d.ts +27 -9
- package/build/src/cli/commands/akuma.js +151 -80
- package/build/src/cli/commands/contract.d.ts +155 -0
- package/build/src/cli/commands/contract.js +20 -0
- package/build/src/cli/commands/install.js +2 -0
- package/build/src/cli/commands/task-invoke.d.ts +19 -4
- package/build/src/cli/commands/task-invoke.js +65 -17
- package/build/src/cli/commands/task-query.d.ts +5 -0
- package/build/src/cli/commands/task-query.js +247 -0
- package/build/src/cli/commands/task.d.ts +3 -1
- package/build/src/cli/commands/task.js +52 -14
- package/build/src/cli/coordinates.d.ts +18 -0
- package/build/src/cli/coordinates.js +120 -0
- package/build/src/cli/draft.d.ts +8 -0
- package/build/src/cli/draft.js +96 -0
- package/build/src/cli/invoke.d.ts +9 -3
- package/build/src/cli/invoke.js +287 -108
- package/build/src/cli/main.js +54 -7
- package/build/src/cli/parse.d.ts +21 -117
- package/build/src/cli/parse.js +79 -84
- package/build/src/cli/render/akuma-tool-command.d.ts +6 -0
- package/build/src/cli/render/akuma-tool-command.js +144 -0
- package/build/src/cli/render/akuma-tool.d.ts +4 -2
- package/build/src/cli/render/akuma-tool.js +35 -5
- package/build/src/cli/render/akuma.d.ts +3 -1
- package/build/src/cli/render/akuma.js +247 -212
- package/build/src/cli/render/audit.d.ts +3 -0
- package/build/src/cli/render/audit.js +104 -0
- package/build/src/cli/render/catalog.d.ts +2 -0
- package/build/src/cli/render/catalog.js +16 -0
- package/build/src/cli/render/contract.d.ts +3 -3
- package/build/src/cli/render/contract.js +200 -54
- package/build/src/cli/render/kanshi.js +272 -129
- package/build/src/cli/render/receipt.d.ts +19 -0
- package/build/src/cli/render/receipt.js +106 -0
- package/build/src/cli/render/refusal.d.ts +16 -2
- package/build/src/cli/render/refusal.js +88 -3
- package/build/src/cli/render/region.d.ts +2 -0
- package/build/src/cli/render/region.js +17 -0
- package/build/src/cli/render/task.d.ts +2 -1
- package/build/src/cli/render/task.js +223 -60
- package/build/src/cli/render/terminal.d.ts +20 -0
- package/build/src/cli/render/terminal.js +121 -0
- package/build/src/cli/render/text.js +11 -3
- package/build/src/cli/result.d.ts +126 -19
- package/build/src/cli/usage.d.ts +1 -0
- package/build/src/cli/usage.js +3 -0
- package/build/src/contract-worktree.d.ts +44 -0
- package/build/src/contract-worktree.js +251 -0
- package/build/src/coordination/durable-file.d.ts +5 -1
- package/build/src/coordination/durable-file.js +57 -10
- package/build/src/coordination/sqlite-transaction-lock.d.ts +4 -0
- package/build/src/coordination/sqlite-transaction-lock.js +22 -5
- package/build/src/core/facts/codec.js +23 -4
- package/build/src/core/facts/eligibility.d.ts +1 -10
- package/build/src/core/facts/eligibility.js +0 -49
- package/build/src/core/facts/fold.js +1 -6
- package/build/src/core/facts/gate.d.ts +1 -0
- package/build/src/core/facts/gate.js +1 -0
- package/build/src/core/facts/observation.d.ts +3 -0
- package/build/src/core/facts/observation.js +12 -0
- package/build/src/core/facts/types.d.ts +10 -3
- package/build/src/core/subject.js +2 -2
- package/build/src/core/verbs/amend.d.ts +1 -1
- package/build/src/core/verbs/amend.js +3 -18
- package/build/src/core/verbs/bind.js +2 -16
- package/build/src/core/verbs/deliver.d.ts +1 -1
- package/build/src/core/verbs/deliver.js +14 -4
- package/build/src/core/verbs/placement.d.ts +1 -1
- package/build/src/core/verbs/placement.js +7 -9
- package/build/src/dispatch/index.d.ts +5 -3
- package/build/src/dispatch/index.js +30 -21
- package/build/src/duration.d.ts +10 -0
- package/build/src/duration.js +22 -0
- package/build/src/git/admission.d.ts +2 -2
- package/build/src/git/admission.js +33 -23
- package/build/src/git/hooks.d.ts +15 -0
- package/build/src/git/hooks.js +70 -18
- package/build/src/git/identity.d.ts +5 -1
- package/build/src/git/identity.js +23 -4
- package/build/src/git/integration.d.ts +48 -0
- package/build/src/git/integration.js +250 -0
- package/build/src/git/observe.d.ts +29 -15
- package/build/src/git/observe.js +185 -76
- package/build/src/git/read-observation.d.ts +41 -0
- package/build/src/git/read-observation.js +361 -0
- package/build/src/git/reconcile.d.ts +17 -5
- package/build/src/git/reconcile.js +305 -109
- package/build/src/git/repository.d.ts +41 -18
- package/build/src/git/repository.js +220 -114
- package/build/src/git/scratch.d.ts +17 -0
- package/build/src/git/scratch.js +73 -0
- package/build/src/git/target-placement.d.ts +92 -0
- package/build/src/git/target-placement.js +375 -0
- package/build/src/git/tender.d.ts +49 -0
- package/build/src/git/tender.js +63 -0
- package/build/src/git/terminal-seal.d.ts +24 -0
- package/build/src/git/terminal-seal.js +89 -0
- package/build/src/git/tree.d.ts +2 -2
- package/build/src/git/tree.js +4 -0
- package/build/src/git/workspace.d.ts +53 -0
- package/build/src/git/workspace.js +139 -0
- package/build/src/identity/selector.d.ts +6 -0
- package/build/src/identity/selector.js +19 -0
- package/build/src/index.d.ts +4 -2
- package/build/src/index.js +2 -1
- package/build/src/kanshi/index.d.ts +2 -2
- package/build/src/kanshi/index.js +1 -1
- package/build/src/kanshi/read.d.ts +7 -3
- package/build/src/kanshi/read.js +245 -45
- package/build/src/kanshi/report.d.ts +81 -6
- package/build/src/kanshi/select.d.ts +5 -0
- package/build/src/kanshi/select.js +33 -1
- package/build/src/library/address.d.ts +57 -0
- package/build/src/library/address.js +165 -0
- package/build/src/library/akuma-creation.d.ts +16 -7
- package/build/src/library/akuma-creation.js +96 -22
- package/build/src/library/audit.d.ts +18 -0
- package/build/src/library/audit.js +44 -0
- package/build/src/library/bind.d.ts +21 -0
- package/build/src/library/bind.js +56 -0
- package/build/src/library/catalog.d.ts +58 -0
- package/build/src/library/catalog.js +69 -0
- package/build/src/library/configuration.d.ts +6 -15
- package/build/src/library/configuration.js +14 -17
- package/build/src/library/contract.d.ts +23 -49
- package/build/src/library/contract.js +171 -185
- package/build/src/library/delivery.d.ts +24 -0
- package/build/src/library/delivery.js +21 -0
- package/build/src/library/fleet.d.ts +65 -0
- package/build/src/library/fleet.js +153 -0
- package/build/src/library/input.d.ts +3 -0
- package/build/src/library/input.js +19 -0
- package/build/src/library/keiyaku.d.ts +18 -3
- package/build/src/library/keiyaku.js +12 -1
- package/build/src/library/mutation.d.ts +39 -0
- package/build/src/library/mutation.js +37 -0
- package/build/src/library/reconcile.d.ts +29 -0
- package/build/src/library/reconcile.js +179 -0
- package/build/src/library/refusal.d.ts +21 -0
- package/build/src/library/refusal.js +29 -0
- package/build/src/library/region.d.ts +14 -1
- package/build/src/library/region.js +18 -4
- package/build/src/library/repo.d.ts +4 -13
- package/build/src/library/repo.js +14 -24
- package/build/src/protocol/attempt.d.ts +16 -3
- package/build/src/protocol/attempt.js +41 -10
- package/build/src/protocol/bind.d.ts +9 -2
- package/build/src/protocol/bind.js +48 -40
- package/build/src/protocol/intent.d.ts +57 -20
- package/build/src/protocol/intent.js +55 -79
- package/build/src/protocol/operations.d.ts +152 -43
- package/build/src/protocol/operations.js +482 -166
- package/build/src/protocol/outcome.d.ts +14 -3
- package/build/src/protocol/outcome.js +11 -3
- package/build/src/protocol/placement.d.ts +41 -0
- package/build/src/protocol/placement.js +147 -0
- package/build/src/protocol/read/documents.d.ts +2 -2
- package/build/src/protocol/read/documents.js +4 -4
- package/build/src/protocol/read/status.d.ts +18 -5
- package/build/src/protocol/read/status.js +73 -13
- package/build/src/protocol/run.d.ts +49 -7
- package/build/src/protocol/run.js +53 -25
- package/build/src/runtime/proc/line-rpc.d.ts +4 -8
- package/build/src/runtime/proc/line-rpc.js +22 -28
- package/build/src/runtime/proc/run.d.ts +23 -21
- package/build/src/runtime/proc/run.js +123 -89
- package/build/src/runtime/proc/stdio.d.ts +18 -0
- package/build/src/runtime/proc/stdio.js +58 -0
- package/build/src/settings.d.ts +7 -1
- package/build/src/settings.js +32 -17
- package/build/src/settlement/fence.d.ts +5 -0
- package/build/src/settlement/fence.js +14 -0
- package/build/src/settlement/holder.d.ts +35 -4
- package/build/src/settlement/holder.js +127 -28
- package/build/src/settlement/settle.d.ts +9 -1
- package/build/src/settlement/settle.js +74 -37
- package/build/src/task/board.d.ts +23 -5
- package/build/src/task/board.js +17 -10
- package/build/src/task/compose.d.ts +2 -2
- package/build/src/task/compose.js +18 -10
- package/build/src/task/context.d.ts +3 -3
- package/build/src/task/context.js +15 -20
- package/build/src/task/document.d.ts +2 -1
- package/build/src/task/document.js +15 -5
- package/build/src/task/index.d.ts +27 -15
- package/build/src/task/index.js +57 -35
- package/build/src/task/operations.d.ts +38 -13
- package/build/src/task/operations.js +80 -46
- package/build/src/task/query.d.ts +67 -0
- package/build/src/task/query.js +279 -0
- package/build/src/task/store.d.ts +5 -7
- package/build/src/task/store.js +43 -28
- package/build/src/verification/declaration.d.ts +1 -0
- package/build/src/verification/execution.d.ts +51 -0
- package/build/src/verification/execution.js +108 -0
- package/build/src/workspace-place.d.ts +41 -0
- package/build/src/workspace-place.js +386 -0
- package/build/src/world.d.ts +24 -0
- package/build/src/world.js +136 -0
- package/package.json +8 -4
- package/build/src/akuma/activity.d.ts +0 -66
- package/build/src/akuma/activity.js +0 -139
- package/build/src/akuma/providers/claude.js +0 -305
- package/build/src/context-root.d.ts +0 -4
- package/build/src/context-root.js +0 -15
- package/build/src/git/delivery.d.ts +0 -20
- package/build/src/git/delivery.js +0 -109
- package/build/src/git/verification.d.ts +0 -14
- package/build/src/git/verification.js +0 -56
- package/build/src/protocol/read/audit.d.ts +0 -25
- package/build/src/protocol/read/audit.js +0 -39
- package/build/src/verification/producer.d.ts +0 -23
- package/build/src/verification/producer.js +0 -54
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { encodeEntry, } from "../core/facts/codec.js";
|
|
2
|
-
import { GIT_FORMAT_BYTES, GIT_FORMAT_PATH, GIT_REF,
|
|
3
|
-
import { contractJournalPath, gitObjectIdForSnapshot, mintContractHead, mintSnapshotId, } from "./identity.js";
|
|
1
|
+
import { decodeJournal, encodeEntry, } from "../core/facts/codec.js";
|
|
2
|
+
import { GIT_FORMAT_BYTES, GIT_FORMAT_PATH, GIT_REF, updateGitTreeFromFrozenDirectories, updateRefsAtomically, writeBlob, writeCommit, } from "./repository.js";
|
|
3
|
+
import { contractJournalPath, contractJournalPaths, gitObjectIdForSnapshot, mintContractHead, mintSnapshotId, } from "./identity.js";
|
|
4
4
|
import { validPath } from "./tree.js";
|
|
5
|
-
const CONTRACT_JOURNAL_PATH = /^contracts\/[0-9a-f]{2}\/[0-9a-f]{2}\/[0-9a-f]{60}\.jsonl$/u;
|
|
5
|
+
const CONTRACT_JOURNAL_PATH = /^contracts\/(?:active|terminal)\/[0-9a-f]{2}\/[0-9a-f]{2}\/[0-9a-f]{60}\.jsonl$/u;
|
|
6
6
|
function assertAppendStructure(appends) {
|
|
7
7
|
const seen = new Set();
|
|
8
8
|
for (const append of appends) {
|
|
@@ -20,7 +20,7 @@ function assertAppendStructure(appends) {
|
|
|
20
20
|
return appends;
|
|
21
21
|
}
|
|
22
22
|
function assertCompanionStructure(companions, appends) {
|
|
23
|
-
const reserved = new Set([GIT_FORMAT_PATH, ...appends.
|
|
23
|
+
const reserved = new Set([GIT_FORMAT_PATH, ...appends.flatMap((append) => contractJournalPaths(append.contractId))]);
|
|
24
24
|
const seen = new Set();
|
|
25
25
|
for (const companion of companions) {
|
|
26
26
|
if (!companion || typeof companion !== "object")
|
|
@@ -39,49 +39,59 @@ function assertCompanionStructure(companions, appends) {
|
|
|
39
39
|
}
|
|
40
40
|
function readCanonicalJournal(admission, id) {
|
|
41
41
|
const snapshot = admission.snapshot;
|
|
42
|
-
const
|
|
42
|
+
const matches = contractJournalPaths(id).filter((path) => snapshot.paths.has(path));
|
|
43
|
+
if (matches.length > 1)
|
|
44
|
+
throw new Error(`duplicate contract journal identity: ${id}`);
|
|
45
|
+
const path = matches[0];
|
|
46
|
+
if (path === undefined)
|
|
47
|
+
return { bytes: Buffer.alloc(0), path: null };
|
|
43
48
|
const entry = snapshot.paths.get(path);
|
|
44
|
-
if (entry === undefined)
|
|
45
|
-
return Buffer.alloc(0);
|
|
46
49
|
if (entry.type !== "blob")
|
|
47
50
|
throw new Error(`journal path is not a blob: ${path}`);
|
|
48
51
|
const journal = admission.frozenJournalBytes.get(entry.oid);
|
|
49
52
|
if (journal === undefined)
|
|
50
53
|
throw new Error(`missing frozen journal bytes: ${path}`);
|
|
51
|
-
return journal;
|
|
54
|
+
return { bytes: journal, path };
|
|
52
55
|
}
|
|
53
|
-
function buildOffer(repository, admission, appends, companions) {
|
|
56
|
+
async function buildOffer(repository, admission, appends, companions) {
|
|
54
57
|
const snapshot = admission.snapshot;
|
|
55
58
|
const changes = new Map();
|
|
56
59
|
const heads = {};
|
|
57
60
|
if (snapshot.commit === null) {
|
|
58
|
-
changes.set(GIT_FORMAT_PATH, { oid: writeBlob(repository, GIT_FORMAT_BYTES), mode: "100644", type: "blob" });
|
|
61
|
+
changes.set(GIT_FORMAT_PATH, { oid: await writeBlob(repository, GIT_FORMAT_BYTES), mode: "100644", type: "blob" });
|
|
59
62
|
}
|
|
60
63
|
for (const companion of companions) {
|
|
61
64
|
changes.set(companion.path, {
|
|
62
|
-
oid: writeBlob(repository, companion.bytes),
|
|
65
|
+
oid: await writeBlob(repository, companion.bytes),
|
|
63
66
|
mode: "100644",
|
|
64
67
|
type: "blob",
|
|
65
68
|
});
|
|
66
69
|
}
|
|
67
70
|
for (const append of appends) {
|
|
68
|
-
|
|
71
|
+
const current = readCanonicalJournal(admission, append.contractId);
|
|
72
|
+
let journal = current.bytes;
|
|
69
73
|
for (const entry of append.entries)
|
|
70
74
|
journal = Buffer.concat([journal, Buffer.from(encodeEntry(entry))]);
|
|
71
|
-
const blob = writeBlob(repository, journal);
|
|
72
|
-
|
|
75
|
+
const blob = await writeBlob(repository, journal);
|
|
76
|
+
const entries = decodeJournal(journal.toString("utf8"));
|
|
77
|
+
const terminal = entries.some((entry) => entry.kind === "claimed" || entry.kind === "abandoned");
|
|
78
|
+
const destination = contractJournalPath(append.contractId, terminal ? "terminal" : "active");
|
|
79
|
+
if (current.path !== null && current.path !== destination)
|
|
80
|
+
changes.set(current.path, null);
|
|
81
|
+
changes.set(destination, { oid: blob, mode: "100644", type: "blob" });
|
|
73
82
|
heads[append.contractId] = mintContractHead(blob);
|
|
74
83
|
}
|
|
75
84
|
return { changes, heads };
|
|
76
85
|
}
|
|
77
|
-
function publishOffer(repository,
|
|
78
|
-
const
|
|
79
|
-
const
|
|
86
|
+
async function publishOffer(repository, admission, target, changes, assertions) {
|
|
87
|
+
const { snapshot } = admission;
|
|
88
|
+
const gitTree = await updateGitTreeFromFrozenDirectories(repository, snapshot.tree, admission.treeDirectories, changes);
|
|
89
|
+
const gitCommit = mintSnapshotId(await writeCommit({
|
|
80
90
|
repository,
|
|
81
91
|
tree: gitTree,
|
|
82
92
|
parent: snapshot.commit,
|
|
83
93
|
}));
|
|
84
|
-
return updateRefsAtomically(repository, [
|
|
94
|
+
return await updateRefsAtomically(repository, [
|
|
85
95
|
{ ref: GIT_REF, newOid: gitObjectIdForSnapshot(gitCommit), expectedOid: snapshot.commit },
|
|
86
96
|
...(target === null
|
|
87
97
|
? []
|
|
@@ -90,17 +100,17 @@ function publishOffer(repository, snapshot, target, changes) {
|
|
|
90
100
|
newOid: gitObjectIdForSnapshot(target.newOid),
|
|
91
101
|
expectedOid: gitObjectIdForSnapshot(target.expectedOid),
|
|
92
102
|
}]),
|
|
93
|
-
]);
|
|
103
|
+
], assertions);
|
|
94
104
|
}
|
|
95
|
-
export function admit(repository, offer, admission) {
|
|
105
|
+
export async function admit(repository, offer, admission, assertions = []) {
|
|
96
106
|
if (!Array.isArray(offer.facts) || offer.facts.length === 0) {
|
|
97
107
|
throw new Error("facts must be a nonempty array");
|
|
98
108
|
}
|
|
99
109
|
const appends = assertAppendStructure(offer.facts);
|
|
100
110
|
const companions = assertCompanionStructure(offer.companions ?? [], appends);
|
|
101
111
|
const target = offer.target ?? null;
|
|
102
|
-
const attempt = buildOffer(repository, admission, appends, companions);
|
|
103
|
-
const publication = publishOffer(repository, admission
|
|
112
|
+
const attempt = await buildOffer(repository, admission, appends, companions);
|
|
113
|
+
const publication = await publishOffer(repository, admission, target, attempt.changes, assertions);
|
|
104
114
|
if (publication.kind === "published") {
|
|
105
115
|
return {
|
|
106
116
|
kind: "accepted",
|
package/build/src/git/hooks.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Settings } from "../settings.js";
|
|
1
2
|
export type HookCommand = Readonly<{
|
|
2
3
|
argv: readonly string[];
|
|
3
4
|
timeoutMs: number;
|
|
@@ -28,7 +29,21 @@ export type WorktreeHookLag = Readonly<{
|
|
|
28
29
|
failure: HookFailure;
|
|
29
30
|
}>;
|
|
30
31
|
type HookPhase = "create" | "destroy";
|
|
32
|
+
export declare function worktreeHooksFrom(input: Readonly<{
|
|
33
|
+
settings: Settings;
|
|
34
|
+
}>): WorktreeHooks;
|
|
31
35
|
export declare function hookMarkerPath(administrationDirectory: string): string;
|
|
36
|
+
export type HookCommandRun = Readonly<{
|
|
37
|
+
kind: "ok";
|
|
38
|
+
}> | Readonly<{
|
|
39
|
+
kind: "cancelled";
|
|
40
|
+
}> | Readonly<{
|
|
41
|
+
kind: "failed";
|
|
42
|
+
command: number;
|
|
43
|
+
failure: HookFailure;
|
|
44
|
+
}>;
|
|
45
|
+
/** Execute an ordered command list without lifecycle state. Scratch owns this policy. */
|
|
46
|
+
export declare function runHookCommands(worktree: string, commands: readonly HookCommand[], signal?: AbortSignal): Promise<HookCommandRun>;
|
|
32
47
|
export declare function runCreateHooks(worktree: string, administrationDirectory: string, hooks: WorktreeHooks, retryHooks: boolean): Promise<WorktreeHookLag | null>;
|
|
33
48
|
export declare function runDestroyHooks(worktree: string, administrationDirectory: string, hooks: WorktreeHooks, retryHooks: boolean): Promise<WorktreeHookLag | null>;
|
|
34
49
|
export {};
|
package/build/src/git/hooks.js
CHANGED
|
@@ -1,13 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { replaceFileDurably } from "../coordination/durable-file.js";
|
|
5
5
|
import { acquireSqliteTransactionLock } from "../coordination/sqlite-transaction-lock.js";
|
|
6
6
|
import { runProcess, runProcessToExit } from "../runtime/proc/run.js";
|
|
7
|
+
import { SettingsError } from "../settings.js";
|
|
7
8
|
const MARKER_VERSION = 1;
|
|
8
9
|
function object(value) {
|
|
9
10
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
11
|
}
|
|
12
|
+
export function worktreeHooksFrom(input) {
|
|
13
|
+
if (!object(input))
|
|
14
|
+
throw new TypeError("worktreeHooksFrom input must be an object");
|
|
15
|
+
const view = input.settings.namespace("worktree");
|
|
16
|
+
if (view.kind === "failed") {
|
|
17
|
+
throw new SettingsError(view.failures.map((failure) => `${failure.scope}: ${failure.diagnostic}`).join("; "));
|
|
18
|
+
}
|
|
19
|
+
for (const entry of view.entries) {
|
|
20
|
+
if (entry.name !== "create" && entry.name !== "destroy")
|
|
21
|
+
throw new SettingsError(`worktree has unknown entry: ${entry.name}`);
|
|
22
|
+
}
|
|
23
|
+
const commands = (phase) => {
|
|
24
|
+
const selected = view.entries.find((entry) => entry.name === phase);
|
|
25
|
+
if (selected === undefined)
|
|
26
|
+
return Object.freeze([]);
|
|
27
|
+
if (!Array.isArray(selected.value))
|
|
28
|
+
throw new SettingsError(`worktree.${phase} must be an array`);
|
|
29
|
+
return Object.freeze(selected.value.map((item, index) => {
|
|
30
|
+
if (!object(item))
|
|
31
|
+
throw new SettingsError(`worktree.${phase}[${index}] must be an object`);
|
|
32
|
+
const coordinate = `worktree.${phase}[${index}]`;
|
|
33
|
+
exactKeys(item, ["argv", "timeoutMs"], coordinate);
|
|
34
|
+
if (!Array.isArray(item.argv) || item.argv.length === 0 || !item.argv.every((value) => typeof value === "string")) {
|
|
35
|
+
throw new SettingsError(`${coordinate}.argv must be a nonempty string array`);
|
|
36
|
+
}
|
|
37
|
+
if (item.argv[0].trim().length === 0)
|
|
38
|
+
throw new SettingsError(`${coordinate}.argv[0] must be nonblank`);
|
|
39
|
+
if (!Number.isSafeInteger(item.timeoutMs) || item.timeoutMs < 1 || item.timeoutMs > 2_147_483_647) {
|
|
40
|
+
throw new SettingsError(`${coordinate}.timeoutMs must be an integer from 1 through 2147483647`);
|
|
41
|
+
}
|
|
42
|
+
return Object.freeze({ argv: Object.freeze([...item.argv]), timeoutMs: item.timeoutMs });
|
|
43
|
+
}));
|
|
44
|
+
};
|
|
45
|
+
return Object.freeze({ create: commands("create"), destroy: commands("destroy") });
|
|
46
|
+
}
|
|
11
47
|
function exactKeys(value, keys, coordinate) {
|
|
12
48
|
const actual = Object.keys(value).sort();
|
|
13
49
|
const expected = [...keys].sort();
|
|
@@ -104,9 +140,9 @@ function decodeMarker(bytes) {
|
|
|
104
140
|
export function hookMarkerPath(administrationDirectory) {
|
|
105
141
|
return join(administrationDirectory, "keiyaku", "hooks.json");
|
|
106
142
|
}
|
|
107
|
-
function readMarker(administrationDirectory) {
|
|
143
|
+
async function readMarker(administrationDirectory) {
|
|
108
144
|
try {
|
|
109
|
-
return decodeMarker(
|
|
145
|
+
return decodeMarker(await readFile(hookMarkerPath(administrationDirectory), "utf8"));
|
|
110
146
|
}
|
|
111
147
|
catch (error) {
|
|
112
148
|
if (error.code === "ENOENT")
|
|
@@ -114,10 +150,10 @@ function readMarker(administrationDirectory) {
|
|
|
114
150
|
throw error;
|
|
115
151
|
}
|
|
116
152
|
}
|
|
117
|
-
function writeMarker(administrationDirectory, marker) {
|
|
153
|
+
async function writeMarker(administrationDirectory, marker) {
|
|
118
154
|
const path = hookMarkerPath(administrationDirectory);
|
|
119
|
-
|
|
120
|
-
replaceFileDurably(path, `${JSON.stringify(marker)}\n`);
|
|
155
|
+
await mkdir(dirname(path), { recursive: true });
|
|
156
|
+
await replaceFileDurably(path, `${JSON.stringify(marker)}\n`);
|
|
121
157
|
}
|
|
122
158
|
function freshMarker(commands, createPending) {
|
|
123
159
|
return {
|
|
@@ -139,6 +175,23 @@ function failedOutcome(outcome) {
|
|
|
139
175
|
}
|
|
140
176
|
return outcome;
|
|
141
177
|
}
|
|
178
|
+
/** Execute an ordered command list without lifecycle state. Scratch owns this policy. */
|
|
179
|
+
export async function runHookCommands(worktree, commands, signal) {
|
|
180
|
+
for (const [command, value] of commands.entries()) {
|
|
181
|
+
const outcome = await runProcess({
|
|
182
|
+
argv: value.argv,
|
|
183
|
+
timeoutMs: value.timeoutMs,
|
|
184
|
+
cwd: worktree,
|
|
185
|
+
...(signal === undefined ? {} : { signal }),
|
|
186
|
+
});
|
|
187
|
+
if (outcome.kind === "cancelled")
|
|
188
|
+
return outcome;
|
|
189
|
+
const failure = failedOutcome(outcome);
|
|
190
|
+
if (failure !== null)
|
|
191
|
+
return { kind: "failed", command, failure };
|
|
192
|
+
}
|
|
193
|
+
return { kind: "ok" };
|
|
194
|
+
}
|
|
142
195
|
function withProgress(marker, phase, progress) {
|
|
143
196
|
return { ...marker, [phase]: progress };
|
|
144
197
|
}
|
|
@@ -174,7 +227,7 @@ async function executePendingCommand(input) {
|
|
|
174
227
|
mode: "immediate",
|
|
175
228
|
});
|
|
176
229
|
try {
|
|
177
|
-
const marker = readMarker(input.administrationDirectory);
|
|
230
|
+
const marker = await readMarker(input.administrationDirectory);
|
|
178
231
|
if (marker === null)
|
|
179
232
|
throw new Error("worktree hook marker disappeared before execution");
|
|
180
233
|
const progress = marker[input.phase];
|
|
@@ -183,13 +236,12 @@ async function executePendingCommand(input) {
|
|
|
183
236
|
const command = marker.commands[input.phase][input.command];
|
|
184
237
|
if (command === undefined)
|
|
185
238
|
throw new Error("worktree hook marker pending index is out of range");
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}));
|
|
239
|
+
const result = await runHookCommands(input.worktree, [command]);
|
|
240
|
+
if (result.kind === "cancelled")
|
|
241
|
+
throw new Error("managed hook runner cancelled without a signal");
|
|
242
|
+
const failure = result.kind === "failed" ? result.failure : null;
|
|
191
243
|
const next = input.command + 1;
|
|
192
|
-
writeMarker(input.administrationDirectory, withProgress(marker, input.phase, failure === null
|
|
244
|
+
await writeMarker(input.administrationDirectory, withProgress(marker, input.phase, failure === null
|
|
193
245
|
? next === marker.commands[input.phase].length ? { status: "ok" } : { status: "pending", next }
|
|
194
246
|
: { status: "failed", command: input.command, failure }));
|
|
195
247
|
}
|
|
@@ -216,14 +268,14 @@ async function runPendingCommand(input) {
|
|
|
216
268
|
throw runnerFailure(outcome);
|
|
217
269
|
}
|
|
218
270
|
async function runPhase(worktree, administrationDirectory, hooks, phase, retryHooks) {
|
|
219
|
-
let marker = readMarker(administrationDirectory);
|
|
271
|
+
let marker = await readMarker(administrationDirectory);
|
|
220
272
|
if (marker === null) {
|
|
221
273
|
marker = freshMarker(hooks, phase === "create");
|
|
222
|
-
writeMarker(administrationDirectory, marker);
|
|
274
|
+
await writeMarker(administrationDirectory, marker);
|
|
223
275
|
}
|
|
224
276
|
let retryFailure = retryHooks && marker[phase].status === "failed";
|
|
225
277
|
for (;;) {
|
|
226
|
-
marker = readMarker(administrationDirectory);
|
|
278
|
+
marker = await readMarker(administrationDirectory);
|
|
227
279
|
if (marker === null)
|
|
228
280
|
throw new Error("worktree hook marker disappeared during execution");
|
|
229
281
|
const progress = marker[phase];
|
|
@@ -234,11 +286,11 @@ async function runPhase(worktree, administrationDirectory, hooks, phase, retryHo
|
|
|
234
286
|
return { kind: "worktree-hook-failed", phase, path: worktree, command: progress.command, failure: progress.failure };
|
|
235
287
|
}
|
|
236
288
|
retryFailure = false;
|
|
237
|
-
writeMarker(administrationDirectory, withProgress(marker, phase, { status: "pending", next: progress.command }));
|
|
289
|
+
await writeMarker(administrationDirectory, withProgress(marker, phase, { status: "pending", next: progress.command }));
|
|
238
290
|
continue;
|
|
239
291
|
}
|
|
240
292
|
if (progress.next === marker.commands[phase].length) {
|
|
241
|
-
writeMarker(administrationDirectory, withProgress(marker, phase, { status: "ok" }));
|
|
293
|
+
await writeMarker(administrationDirectory, withProgress(marker, phase, { status: "ok" }));
|
|
242
294
|
continue;
|
|
243
295
|
}
|
|
244
296
|
await runPendingCommand({ administrationDirectory, worktree, phase, command: progress.next });
|
|
@@ -4,7 +4,11 @@ export type GitObjectId = string & {
|
|
|
4
4
|
readonly [gitObjectIdBrand]: "GitObjectId";
|
|
5
5
|
};
|
|
6
6
|
export declare function contractLocator(contract: ContractId): string;
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function gitRefLocator(ref: string): string;
|
|
8
|
+
export declare function contractPhysicalName(contract: ContractId): string;
|
|
9
|
+
export type ContractJournalClass = "active" | "terminal";
|
|
10
|
+
export declare function contractJournalPath(contract: ContractId, journalClass?: ContractJournalClass): string;
|
|
11
|
+
export declare function contractJournalPaths(contract: ContractId): readonly string[];
|
|
8
12
|
export declare function gitObjectId(value: string, label?: string): GitObjectId;
|
|
9
13
|
export declare function mintSnapshotId(value: string): SnapshotId;
|
|
10
14
|
export declare function mintChangeId(value: string): ChangeId;
|
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { changeId, contractHead, contractId, snapshotId, } from "../core/facts/types.js";
|
|
3
3
|
const GIT_OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
4
|
+
function locator(value) {
|
|
5
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
6
|
+
}
|
|
4
7
|
export function contractLocator(contract) {
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
return locator(contractId(contract));
|
|
9
|
+
}
|
|
10
|
+
export function gitRefLocator(ref) {
|
|
11
|
+
if (ref.length === 0)
|
|
12
|
+
throw new Error("Git ref locator input must be nonempty");
|
|
13
|
+
return locator(ref);
|
|
14
|
+
}
|
|
15
|
+
export function contractPhysicalName(contract) {
|
|
16
|
+
return contractId(contract).replace("/", "-");
|
|
7
17
|
}
|
|
8
|
-
|
|
18
|
+
function contractJournalSuffix(contract) {
|
|
9
19
|
const digest = contractLocator(contract);
|
|
10
|
-
return
|
|
20
|
+
return `${digest.slice(0, 2)}/${digest.slice(2, 4)}/${digest.slice(4)}.jsonl`;
|
|
21
|
+
}
|
|
22
|
+
export function contractJournalPath(contract, journalClass = "active") {
|
|
23
|
+
return `contracts/${journalClass}/${contractJournalSuffix(contract)}`;
|
|
24
|
+
}
|
|
25
|
+
export function contractJournalPaths(contract) {
|
|
26
|
+
return [
|
|
27
|
+
contractJournalPath(contract, "active"),
|
|
28
|
+
contractJournalPath(contract, "terminal"),
|
|
29
|
+
];
|
|
11
30
|
}
|
|
12
31
|
export function gitObjectId(value, label = "Git object ID") {
|
|
13
32
|
if (!GIT_OBJECT_ID.test(value))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Preparation } from "../core/decide.js";
|
|
2
|
+
import type { ChangeId, ContractCoordinates, ContractId, SnapshotId } from "../core/facts/types.js";
|
|
3
|
+
import { type GitObjectId } from "./identity.js";
|
|
4
|
+
import { type GitRepository } from "./repository.js";
|
|
5
|
+
import type { TenderCapture } from "./tender.js";
|
|
6
|
+
declare const REQUIRED_GIT: "2.38";
|
|
7
|
+
export type IntegrationPreparationRefusal = Readonly<{
|
|
8
|
+
kind: "integration-failed";
|
|
9
|
+
contractId: ContractId;
|
|
10
|
+
reason: "not-based-on-target" | "unrelated-histories" | "conflict";
|
|
11
|
+
targetHead: SnapshotId;
|
|
12
|
+
conflictPaths?: readonly string[];
|
|
13
|
+
}> | Readonly<{
|
|
14
|
+
kind: "integration-unsupported";
|
|
15
|
+
contractId: ContractId;
|
|
16
|
+
requiredGit: typeof REQUIRED_GIT;
|
|
17
|
+
}>;
|
|
18
|
+
export type IntegrationCoordinates = Readonly<{
|
|
19
|
+
contractId: ContractId;
|
|
20
|
+
coordinates: ContractCoordinates;
|
|
21
|
+
}>;
|
|
22
|
+
export type IntegrationPlan = Readonly<{
|
|
23
|
+
predecessor: SnapshotId;
|
|
24
|
+
tree: GitObjectId;
|
|
25
|
+
}>;
|
|
26
|
+
/** Mint the one worktree-content identity shared by review and delivery. */
|
|
27
|
+
export declare function worktreeChangeId(repository: GitRepository, input: IntegrationCoordinates, tender: TenderCapture): Promise<ChangeId>;
|
|
28
|
+
/** Materialize the exact integration tree against its observed predecessor. */
|
|
29
|
+
export declare function materializeIntegrationSnapshot(repository: GitRepository, tree: GitObjectId, parent: SnapshotId, input: Readonly<{
|
|
30
|
+
contractId: ContractId;
|
|
31
|
+
title: string;
|
|
32
|
+
message?: string;
|
|
33
|
+
}>): Promise<SnapshotId>;
|
|
34
|
+
export declare function planIntegration(repository: GitRepository, input: IntegrationCoordinates, tender: TenderCapture, requireBranchesToBeUpToDate: boolean): Promise<Preparation<IntegrationPlan, Readonly<{
|
|
35
|
+
kind: "target-missing";
|
|
36
|
+
contractId: ContractId;
|
|
37
|
+
}> | IntegrationPreparationRefusal>>;
|
|
38
|
+
export type DeliveryDiffScope = Readonly<{
|
|
39
|
+
filesChanged: number;
|
|
40
|
+
insertions: number;
|
|
41
|
+
deletions: number;
|
|
42
|
+
paths?: readonly string[];
|
|
43
|
+
}>;
|
|
44
|
+
/** Compute predecessor-to-candidate scope from the exact integration trees. */
|
|
45
|
+
export declare function readDeliveryScope(repository: GitRepository, predecessor: SnapshotId, candidate: SnapshotId, includePaths: boolean): Promise<DeliveryDiffScope>;
|
|
46
|
+
/** Read a recorded integration pair's patch, or null when Git no longer has it. */
|
|
47
|
+
export declare function readDeliveryDiff(repository: GitRepository, predecessor: SnapshotId, candidate: SnapshotId): Promise<string | null>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { AuthorityCorruptionError } from "../core/facts/errors.js";
|
|
2
|
+
import { gitObjectId, gitObjectIdForSnapshot, mintChangeId, mintSnapshotId } from "./identity.js";
|
|
3
|
+
import { decodeGitNameOnly, decodeGitNumstat, GitPlumbingError, readRef, runGit, runGitWithEnvironment, } from "./repository.js";
|
|
4
|
+
const REQUIRED_GIT = "2.38";
|
|
5
|
+
async function runAllowingNonzero(repository, args, input) {
|
|
6
|
+
try {
|
|
7
|
+
return { stdout: await runGit(repository, args, input), stderr: Buffer.alloc(0), status: 0 };
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (!(error instanceof GitPlumbingError))
|
|
11
|
+
throw error;
|
|
12
|
+
return { stdout: error.stdout, stderr: error.stderr, status: error.status };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Mint the one worktree-content identity shared by review and delivery. */
|
|
16
|
+
export async function worktreeChangeId(repository, input, tender) {
|
|
17
|
+
const patch = await runGit(repository, [
|
|
18
|
+
"-c", "core.quotePath=false",
|
|
19
|
+
"-c", "core.abbrev=40",
|
|
20
|
+
"-c", "diff.algorithm=myers",
|
|
21
|
+
"-c", "diff.renames=false",
|
|
22
|
+
"-c", "diff.indentHeuristic=false",
|
|
23
|
+
"-c", "diff.suppressBlankEmpty=false",
|
|
24
|
+
"diff",
|
|
25
|
+
"--no-ext-diff",
|
|
26
|
+
"--no-textconv",
|
|
27
|
+
"--no-indent-heuristic",
|
|
28
|
+
"--no-renames",
|
|
29
|
+
"--full-index",
|
|
30
|
+
"--binary",
|
|
31
|
+
"--no-color",
|
|
32
|
+
"--diff-algorithm=myers",
|
|
33
|
+
"--unified=3",
|
|
34
|
+
"--src-prefix=a/",
|
|
35
|
+
"--dst-prefix=b/",
|
|
36
|
+
"--inter-hunk-context=0",
|
|
37
|
+
"--no-relative",
|
|
38
|
+
"--ignore-submodules=none",
|
|
39
|
+
"--submodule=short",
|
|
40
|
+
gitObjectIdForSnapshot(input.coordinates.start),
|
|
41
|
+
tender.tree,
|
|
42
|
+
]);
|
|
43
|
+
const id = (await runGit(repository, ["patch-id", "--verbatim"], patch)).toString("utf8").trim().split(/\s/, 1)[0] ?? "";
|
|
44
|
+
return mintChangeId(id === "" ? "0000000000000000000000000000000000000000" : id);
|
|
45
|
+
}
|
|
46
|
+
/** Materialize the exact integration tree against its observed predecessor. */
|
|
47
|
+
export async function materializeIntegrationSnapshot(repository, tree, parent, input) {
|
|
48
|
+
const commit = (await runGitWithEnvironment(repository, ["commit-tree", tree, "-p", gitObjectIdForSnapshot(parent)], `${input.message ?? `${input.contractId}: ${input.title}`}\n\nKeiyaku-Contract: ${input.contractId}\n`, {
|
|
49
|
+
GIT_AUTHOR_NAME: "Keiyaku",
|
|
50
|
+
GIT_AUTHOR_EMAIL: "keiyaku@localhost",
|
|
51
|
+
GIT_COMMITTER_NAME: "Keiyaku",
|
|
52
|
+
GIT_COMMITTER_EMAIL: "keiyaku@localhost",
|
|
53
|
+
GIT_AUTHOR_DATE: "Thu, 01 Jan 1970 00:00:00 +0000",
|
|
54
|
+
GIT_COMMITTER_DATE: "Thu, 01 Jan 1970 00:00:00 +0000",
|
|
55
|
+
})).toString("utf8").trim();
|
|
56
|
+
return mintSnapshotId(commit);
|
|
57
|
+
}
|
|
58
|
+
async function isAncestor(repository, ancestor, descendant) {
|
|
59
|
+
const result = await runAllowingNonzero(repository, [
|
|
60
|
+
"merge-base",
|
|
61
|
+
"--is-ancestor",
|
|
62
|
+
gitObjectIdForSnapshot(ancestor),
|
|
63
|
+
gitObjectIdForSnapshot(descendant),
|
|
64
|
+
]);
|
|
65
|
+
if (result.status === 0)
|
|
66
|
+
return true;
|
|
67
|
+
if (result.status === 1)
|
|
68
|
+
return false;
|
|
69
|
+
throw new GitPlumbingError({
|
|
70
|
+
stderr: result.stderr,
|
|
71
|
+
status: result.status,
|
|
72
|
+
message: "git merge-base --is-ancestor failed",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function commonAncestor(repository, left, right) {
|
|
76
|
+
const result = await runAllowingNonzero(repository, [
|
|
77
|
+
"merge-base",
|
|
78
|
+
gitObjectIdForSnapshot(left),
|
|
79
|
+
gitObjectIdForSnapshot(right),
|
|
80
|
+
]);
|
|
81
|
+
if (result.status === 1)
|
|
82
|
+
return null;
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
throw new GitPlumbingError({
|
|
85
|
+
stderr: result.stderr,
|
|
86
|
+
status: result.status,
|
|
87
|
+
message: "git merge-base failed",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return mintSnapshotId(result.stdout.toString("utf8").trim());
|
|
91
|
+
}
|
|
92
|
+
async function supportsMergeTree(repository) {
|
|
93
|
+
return (await runAllowingNonzero(repository, ["merge-tree", "--write-tree", "--stdin"], "")).status === 0;
|
|
94
|
+
}
|
|
95
|
+
function nulFields(output) {
|
|
96
|
+
const fields = output.toString("utf8").split("\0");
|
|
97
|
+
if (fields.at(-1) !== "")
|
|
98
|
+
throw new Error("Git merge-tree output is not NUL terminated");
|
|
99
|
+
return fields.slice(0, -1);
|
|
100
|
+
}
|
|
101
|
+
async function mergedTree(repository, contractId, base, targetHead, tenderTree) {
|
|
102
|
+
if (!(await supportsMergeTree(repository))) {
|
|
103
|
+
return { kind: "refused", refusal: { kind: "integration-unsupported", contractId, requiredGit: REQUIRED_GIT } };
|
|
104
|
+
}
|
|
105
|
+
const result = await runAllowingNonzero(repository, [
|
|
106
|
+
"merge-tree",
|
|
107
|
+
"--write-tree",
|
|
108
|
+
"--no-messages",
|
|
109
|
+
"-z",
|
|
110
|
+
"--name-only",
|
|
111
|
+
"--merge-base",
|
|
112
|
+
gitObjectIdForSnapshot(base),
|
|
113
|
+
gitObjectIdForSnapshot(targetHead),
|
|
114
|
+
tenderTree,
|
|
115
|
+
]);
|
|
116
|
+
const fields = nulFields(result.stdout);
|
|
117
|
+
if (result.status === 0) {
|
|
118
|
+
if (fields.length !== 1)
|
|
119
|
+
throw new Error("successful Git merge-tree output has unexpected fields");
|
|
120
|
+
return { kind: "prepared", data: gitObjectId(fields[0], "integration tree") };
|
|
121
|
+
}
|
|
122
|
+
if (result.status === 1) {
|
|
123
|
+
if (fields.length < 2)
|
|
124
|
+
throw new Error("conflicted Git merge-tree output is missing paths");
|
|
125
|
+
return {
|
|
126
|
+
kind: "refused",
|
|
127
|
+
refusal: {
|
|
128
|
+
kind: "integration-failed",
|
|
129
|
+
contractId,
|
|
130
|
+
reason: "conflict",
|
|
131
|
+
targetHead,
|
|
132
|
+
conflictPaths: Object.freeze([...new Set(fields.slice(1))].sort()),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
throw new GitPlumbingError({
|
|
137
|
+
stderr: result.stderr,
|
|
138
|
+
status: result.status,
|
|
139
|
+
message: "git merge-tree --write-tree failed",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export async function planIntegration(repository, input, tender, requireBranchesToBeUpToDate) {
|
|
143
|
+
const target = input.coordinates.target;
|
|
144
|
+
if (target === undefined) {
|
|
145
|
+
return {
|
|
146
|
+
kind: "prepared",
|
|
147
|
+
data: {
|
|
148
|
+
predecessor: input.coordinates.start,
|
|
149
|
+
tree: tender.tree,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const observed = await readRef(repository, target);
|
|
154
|
+
if (observed === null) {
|
|
155
|
+
return { kind: "refused", refusal: { kind: "target-missing", contractId: input.contractId } };
|
|
156
|
+
}
|
|
157
|
+
const targetHead = mintSnapshotId(observed);
|
|
158
|
+
let tree;
|
|
159
|
+
if (requireBranchesToBeUpToDate) {
|
|
160
|
+
if (!(await isAncestor(repository, targetHead, tender.head))) {
|
|
161
|
+
return {
|
|
162
|
+
kind: "refused",
|
|
163
|
+
refusal: {
|
|
164
|
+
kind: "integration-failed",
|
|
165
|
+
contractId: input.contractId,
|
|
166
|
+
reason: "not-based-on-target",
|
|
167
|
+
targetHead,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
tree = tender.tree;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const base = await commonAncestor(repository, tender.head, targetHead);
|
|
175
|
+
if (base === null) {
|
|
176
|
+
return {
|
|
177
|
+
kind: "refused",
|
|
178
|
+
refusal: {
|
|
179
|
+
kind: "integration-failed",
|
|
180
|
+
contractId: input.contractId,
|
|
181
|
+
reason: "unrelated-histories",
|
|
182
|
+
targetHead,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const merged = await mergedTree(repository, input.contractId, base, targetHead, tender.tree);
|
|
187
|
+
if (merged.kind === "refused")
|
|
188
|
+
return merged;
|
|
189
|
+
tree = merged.data;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
kind: "prepared",
|
|
193
|
+
data: {
|
|
194
|
+
predecessor: targetHead,
|
|
195
|
+
tree,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function deliverySnapshotAvailability(repository, predecessor, candidate) {
|
|
200
|
+
const objects = [gitObjectIdForSnapshot(predecessor), gitObjectIdForSnapshot(candidate)];
|
|
201
|
+
const types = (await runGit(repository, ["cat-file", "--batch-check=%(objectname) %(objecttype)"], `${objects.join("\n")}\n`)).toString("ascii").trimEnd().split("\n").map((record) => record.split(" ")[1]);
|
|
202
|
+
if (types.includes("missing"))
|
|
203
|
+
return "unavailable";
|
|
204
|
+
if (types.some((type) => type !== "commit")) {
|
|
205
|
+
throw new AuthorityCorruptionError("recorded delivery snapshot is not a Git commit");
|
|
206
|
+
}
|
|
207
|
+
return "available";
|
|
208
|
+
}
|
|
209
|
+
/** Compute predecessor-to-candidate scope from the exact integration trees. */
|
|
210
|
+
export async function readDeliveryScope(repository, predecessor, candidate, includePaths) {
|
|
211
|
+
const scope = decodeGitNumstat(await runGit(repository, [
|
|
212
|
+
"diff",
|
|
213
|
+
"--numstat",
|
|
214
|
+
"-z",
|
|
215
|
+
gitObjectIdForSnapshot(predecessor),
|
|
216
|
+
gitObjectIdForSnapshot(candidate),
|
|
217
|
+
]));
|
|
218
|
+
if (!includePaths)
|
|
219
|
+
return scope;
|
|
220
|
+
return {
|
|
221
|
+
...scope,
|
|
222
|
+
paths: decodeGitNameOnly(await runGit(repository, [
|
|
223
|
+
"diff",
|
|
224
|
+
"--name-only",
|
|
225
|
+
"--no-renames",
|
|
226
|
+
"-z",
|
|
227
|
+
gitObjectIdForSnapshot(predecessor),
|
|
228
|
+
gitObjectIdForSnapshot(candidate),
|
|
229
|
+
])),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** Read a recorded integration pair's patch, or null when Git no longer has it. */
|
|
233
|
+
export async function readDeliveryDiff(repository, predecessor, candidate) {
|
|
234
|
+
if (await deliverySnapshotAvailability(repository, predecessor, candidate) === "unavailable")
|
|
235
|
+
return null;
|
|
236
|
+
try {
|
|
237
|
+
return (await runGit(repository, [
|
|
238
|
+
"diff",
|
|
239
|
+
"--no-ext-diff",
|
|
240
|
+
"--no-color",
|
|
241
|
+
gitObjectIdForSnapshot(predecessor),
|
|
242
|
+
gitObjectIdForSnapshot(candidate),
|
|
243
|
+
])).toString("utf8");
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (await deliverySnapshotAvailability(repository, predecessor, candidate) === "unavailable")
|
|
247
|
+
return null;
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|