@astrosheep/keiyaku 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -9
- 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 +11 -3
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +9 -6
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +94 -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 +162 -45
- package/build/src/akuma/akuma.d.ts +33 -25
- package/build/src/akuma/akuma.js +93 -61
- package/build/src/akuma/archetype.d.ts +8 -0
- package/build/src/akuma/archetype.js +28 -9
- package/build/src/akuma/body.js +196 -84
- 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 +67 -15
- package/build/src/akuma/heart/facts.js +3 -3
- package/build/src/akuma/heart/index.d.ts +41 -29
- package/build/src/akuma/heart/index.js +72 -193
- package/build/src/akuma/heart/rows.d.ts +30 -53
- package/build/src/akuma/heart/rows.js +91 -111
- package/build/src/akuma/heart/schema.d.ts +1 -1
- package/build/src/akuma/heart/schema.js +55 -8
- package/build/src/akuma/heart/storage.d.ts +33 -0
- package/build/src/akuma/heart/storage.js +164 -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/index.d.ts +4 -2
- package/build/src/akuma/index.js +1 -1
- package/build/src/akuma/projection.d.ts +97 -0
- package/build/src/akuma/projection.js +145 -0
- package/build/src/akuma/provider.d.ts +57 -11
- package/build/src/akuma/provider.js +57 -37
- package/build/src/akuma/providers/{claude.d.ts → claude/events.d.ts} +9 -41
- package/build/src/akuma/providers/claude/events.js +185 -0
- package/build/src/akuma/providers/claude/index.d.ts +46 -0
- package/build/src/akuma/providers/claude/index.js +265 -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 +12 -12
- package/build/src/akuma/providers/codex-app-server/index.d.ts +13 -11
- package/build/src/akuma/providers/codex-app-server/index.js +73 -22
- package/build/src/akuma/providers/index.js +22 -8
- package/build/src/akuma/providers/opencode-sdk/events.d.ts +48 -0
- package/build/src/akuma/providers/opencode-sdk/events.js +116 -0
- package/build/src/akuma/providers/opencode-sdk/index.d.ts +32 -0
- package/build/src/akuma/providers/opencode-sdk/index.js +203 -0
- package/build/src/akuma/providers/opencode-sdk/session.d.ts +41 -0
- package/build/src/akuma/providers/opencode-sdk/session.js +62 -0
- package/build/src/akuma/providers/pi/events.d.ts +33 -0
- package/build/src/akuma/providers/pi/events.js +128 -0
- package/build/src/akuma/providers/pi/index.d.ts +11 -0
- package/build/src/akuma/providers/pi/index.js +162 -0
- package/build/src/akuma/requests.d.ts +1 -0
- package/build/src/akuma/requests.js +5 -2
- package/build/src/alias/index.d.ts +4 -3
- package/build/src/alias/index.js +2 -3
- 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 +2 -0
- package/build/src/cli/accepted.js +2 -1
- package/build/src/cli/commands/akuma-invoke.d.ts +42 -18
- package/build/src/cli/commands/akuma-invoke.js +94 -50
- package/build/src/cli/commands/akuma.d.ts +15 -7
- package/build/src/cli/commands/akuma.js +94 -56
- package/build/src/cli/commands/contract.d.ts +142 -0
- package/build/src/cli/commands/contract.js +14 -0
- package/build/src/cli/commands/install.js +2 -0
- package/build/src/cli/commands/task-invoke.d.ts +3 -1
- package/build/src/cli/commands/task-invoke.js +27 -2
- package/build/src/cli/commands/task.js +2 -2
- package/build/src/cli/invoke.d.ts +6 -1
- package/build/src/cli/invoke.js +241 -43
- package/build/src/cli/main.js +20 -1
- package/build/src/cli/parse.d.ts +13 -116
- package/build/src/cli/parse.js +51 -78
- package/build/src/cli/render/akuma-tool.d.ts +2 -0
- package/build/src/cli/render/akuma-tool.js +6 -2
- package/build/src/cli/render/akuma.d.ts +2 -1
- package/build/src/cli/render/akuma.js +157 -217
- 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 +1 -1
- package/build/src/cli/render/contract.js +38 -5
- package/build/src/cli/render/kanshi.js +53 -5
- package/build/src/cli/render/refusal.js +17 -0
- package/build/src/cli/render/terminal.d.ts +13 -0
- package/build/src/cli/render/terminal.js +75 -0
- package/build/src/cli/render/text.js +5 -0
- package/build/src/cli/result.d.ts +15 -3
- package/build/src/contract-worktree.d.ts +43 -0
- package/build/src/contract-worktree.js +213 -0
- package/build/src/coordination/durable-file.d.ts +3 -0
- package/build/src/coordination/durable-file.js +44 -1
- 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/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.js +12 -16
- package/build/src/core/verbs/bind.js +2 -16
- package/build/src/core/verbs/deliver.js +20 -4
- package/build/src/core/verbs/placement.js +3 -8
- package/build/src/dispatch/index.d.ts +3 -1
- package/build/src/dispatch/index.js +14 -5
- 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 +26 -16
- package/build/src/git/hooks.d.ts +15 -0
- package/build/src/git/hooks.js +57 -5
- package/build/src/git/identity.d.ts +5 -1
- package/build/src/git/identity.js +23 -4
- package/build/src/git/integration.d.ts +49 -0
- package/build/src/git/integration.js +214 -0
- package/build/src/git/observe.d.ts +25 -13
- package/build/src/git/observe.js +175 -72
- 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 +15 -5
- package/build/src/git/reconcile.js +238 -85
- package/build/src/git/repository.d.ts +19 -4
- package/build/src/git/repository.js +77 -43
- package/build/src/git/scratch.d.ts +13 -0
- package/build/src/git/scratch.js +66 -0
- package/build/src/git/target-placement.d.ts +56 -0
- package/build/src/git/target-placement.js +255 -0
- package/build/src/git/tender.d.ts +48 -0
- package/build/src/git/tender.js +82 -0
- package/build/src/git/terminal-seal.d.ts +24 -0
- package/build/src/git/terminal-seal.js +87 -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 +21 -0
- package/build/src/git/workspace.js +104 -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 +1 -1
- package/build/src/kanshi/read.d.ts +5 -2
- package/build/src/kanshi/read.js +154 -45
- package/build/src/kanshi/report.d.ts +31 -6
- package/build/src/kanshi/select.js +1 -1
- package/build/src/library/address.d.ts +48 -0
- package/build/src/library/address.js +134 -0
- package/build/src/library/akuma-creation.d.ts +10 -6
- package/build/src/library/akuma-creation.js +12 -11
- 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 +72 -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 +21 -46
- package/build/src/library/contract.js +193 -183
- package/build/src/library/delivery.d.ts +23 -0
- package/build/src/library/delivery.js +21 -0
- package/build/src/library/fleet.d.ts +65 -0
- package/build/src/library/fleet.js +189 -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 +16 -2
- package/build/src/library/keiyaku.js +11 -1
- package/build/src/library/mutation.d.ts +38 -0
- package/build/src/library/mutation.js +39 -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 +2 -1
- package/build/src/library/region.js +3 -3
- package/build/src/library/repo.d.ts +4 -1
- package/build/src/library/repo.js +31 -16
- package/build/src/protocol/attempt.d.ts +16 -3
- package/build/src/protocol/attempt.js +39 -8
- package/build/src/protocol/bind.d.ts +9 -2
- package/build/src/protocol/bind.js +47 -39
- package/build/src/protocol/intent.d.ts +42 -18
- package/build/src/protocol/intent.js +36 -75
- package/build/src/protocol/operations.d.ts +93 -35
- package/build/src/protocol/operations.js +235 -123
- package/build/src/protocol/outcome.d.ts +7 -2
- package/build/src/protocol/outcome.js +9 -3
- package/build/src/protocol/placement.d.ts +30 -0
- package/build/src/protocol/placement.js +114 -0
- package/build/src/protocol/read/audit.d.ts +9 -3
- package/build/src/protocol/read/audit.js +25 -9
- 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 +11 -5
- package/build/src/protocol/read/status.js +28 -15
- 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 +1 -0
- package/build/src/runtime/proc/line-rpc.js +21 -0
- package/build/src/runtime/proc/run.d.ts +18 -2
- package/build/src/runtime/proc/run.js +34 -8
- package/build/src/settings.d.ts +6 -0
- package/build/src/settings.js +29 -14
- package/build/src/settlement/fence.d.ts +9 -0
- package/build/src/settlement/fence.js +23 -0
- package/build/src/settlement/holder.d.ts +25 -4
- package/build/src/settlement/holder.js +102 -28
- package/build/src/settlement/settle.d.ts +8 -0
- package/build/src/settlement/settle.js +102 -28
- package/build/src/task/board.d.ts +20 -2
- package/build/src/task/board.js +11 -1
- package/build/src/task/compose.d.ts +2 -2
- package/build/src/task/compose.js +2 -2
- package/build/src/task/context.js +2 -7
- package/build/src/task/index.d.ts +9 -9
- package/build/src/task/index.js +15 -12
- package/build/src/task/operations.d.ts +31 -12
- package/build/src/task/operations.js +8 -4
- package/build/src/task/store.d.ts +4 -6
- package/build/src/task/store.js +3 -3
- package/build/src/verification/declaration.d.ts +1 -0
- package/build/src/verification/execution.d.ts +49 -0
- package/build/src/verification/execution.js +104 -0
- package/build/src/world.d.ts +13 -0
- package/build/src/world.js +103 -0
- package/package.json +7 -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/verification/producer.d.ts +0 -23
- package/build/src/verification/producer.js +0 -54
|
@@ -7,7 +7,7 @@ export const GIT_REF = "refs/heads/keiyaku-state";
|
|
|
7
7
|
export const DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
|
|
8
8
|
export const CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
|
|
9
9
|
export const GIT_FORMAT_PATH = "meta/format.json";
|
|
10
|
-
const CURRENT_FORMAT_VERSION =
|
|
10
|
+
const CURRENT_FORMAT_VERSION = 4;
|
|
11
11
|
export const GIT_FORMAT_BYTES = `{"version":${CURRENT_FORMAT_VERSION}}\n`;
|
|
12
12
|
export function isKeiyakuOwnedRef(ref) {
|
|
13
13
|
return ref === GIT_REF
|
|
@@ -25,20 +25,22 @@ export class NoGitWorldError extends Error {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
export class GitPlumbingError extends Error {
|
|
28
|
+
stdout;
|
|
28
29
|
stderr;
|
|
29
30
|
status;
|
|
30
31
|
pid;
|
|
31
32
|
constructor(input) {
|
|
32
33
|
super(input.message);
|
|
33
34
|
this.name = "GitPlumbingError";
|
|
35
|
+
this.stdout = Buffer.from(input.stdout ?? "");
|
|
34
36
|
this.stderr = Buffer.from(input.stderr);
|
|
35
37
|
this.status = input.status;
|
|
36
38
|
this.pid = input.pid ?? null;
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
|
-
function
|
|
41
|
+
function worktreesFromPorcelain(output) {
|
|
40
42
|
const fields = output.toString("utf8").split("\0");
|
|
41
|
-
const
|
|
43
|
+
const worktrees = [];
|
|
42
44
|
let record = [];
|
|
43
45
|
const finishRecord = () => {
|
|
44
46
|
if (record.length === 0)
|
|
@@ -50,7 +52,11 @@ function worktreePathsFromPorcelain(output) {
|
|
|
50
52
|
const path = worktree.slice("worktree ".length);
|
|
51
53
|
if (path.length === 0)
|
|
52
54
|
throw new Error("Git worktree porcelain output has an empty worktree path");
|
|
53
|
-
|
|
55
|
+
const branchField = record.find((field) => field.startsWith("branch "));
|
|
56
|
+
const branch = branchField === undefined ? null : branchField.slice("branch ".length);
|
|
57
|
+
if (branch !== null && !branch.startsWith("refs/"))
|
|
58
|
+
throw new Error("Git worktree porcelain output has an invalid branch");
|
|
59
|
+
worktrees.push({ path: resolve(path), branch });
|
|
54
60
|
record = [];
|
|
55
61
|
};
|
|
56
62
|
for (const field of fields) {
|
|
@@ -61,22 +67,29 @@ function worktreePathsFromPorcelain(output) {
|
|
|
61
67
|
record.push(field);
|
|
62
68
|
}
|
|
63
69
|
finishRecord();
|
|
64
|
-
if (
|
|
70
|
+
if (worktrees.length === 0)
|
|
65
71
|
throw new Error("Git worktree porcelain output has no worktrees");
|
|
66
|
-
return
|
|
72
|
+
return worktrees;
|
|
67
73
|
}
|
|
68
|
-
export function
|
|
69
|
-
return
|
|
74
|
+
export function registeredWorktrees(repository) {
|
|
75
|
+
return worktreesFromPorcelain(runGit(repository, ["worktree", "list", "--porcelain", "-z"]));
|
|
70
76
|
}
|
|
77
|
+
export function registeredWorktreePaths(repository) { return registeredWorktrees(repository).map((worktree) => worktree.path); }
|
|
71
78
|
export function repositoryAt(cwd) {
|
|
72
79
|
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
73
80
|
throw new Error("repository path must be a nonempty string");
|
|
74
81
|
}
|
|
75
82
|
const effectiveCwd = resolve(cwd);
|
|
76
|
-
const provisional = {
|
|
83
|
+
const provisional = {
|
|
84
|
+
effectiveCwd,
|
|
85
|
+
primaryWorktree: effectiveCwd,
|
|
86
|
+
commonDirectory: effectiveCwd,
|
|
87
|
+
};
|
|
77
88
|
let primaryWorktree;
|
|
89
|
+
let commonDirectory;
|
|
78
90
|
try {
|
|
79
91
|
primaryWorktree = registeredWorktreePaths(provisional)[0];
|
|
92
|
+
commonDirectory = absoluteGitPath(provisional, ["--git-common-dir"], "common Git directory");
|
|
80
93
|
}
|
|
81
94
|
catch (error) {
|
|
82
95
|
if (error instanceof GitPlumbingError && error.status === 128) {
|
|
@@ -84,7 +97,7 @@ export function repositoryAt(cwd) {
|
|
|
84
97
|
}
|
|
85
98
|
throw error;
|
|
86
99
|
}
|
|
87
|
-
return { effectiveCwd, primaryWorktree };
|
|
100
|
+
return { effectiveCwd, primaryWorktree, commonDirectory };
|
|
88
101
|
}
|
|
89
102
|
function absoluteGitPath(repository, args, label) {
|
|
90
103
|
const value = runGit(repository, ["rev-parse", "--path-format=absolute", ...args]).toString("utf8").trim();
|
|
@@ -93,17 +106,22 @@ function absoluteGitPath(repository, args, label) {
|
|
|
93
106
|
return resolve(isAbsolute(value) ? value : resolve(repository.effectiveCwd, value));
|
|
94
107
|
}
|
|
95
108
|
export function commonGitDirectory(repository) {
|
|
96
|
-
return
|
|
109
|
+
return repository.commonDirectory;
|
|
97
110
|
}
|
|
98
111
|
export function worktreeGitDirectory(repository, worktree) {
|
|
99
112
|
const scoped = { ...repository, effectiveCwd: worktree };
|
|
100
113
|
return absoluteGitPath(scoped, ["--git-dir"], "worktree Git directory");
|
|
101
114
|
}
|
|
115
|
+
export function worktreeRoot(repository) {
|
|
116
|
+
return absoluteGitPath(repository, ["--show-toplevel"], "worktree root");
|
|
117
|
+
}
|
|
102
118
|
function commandError(command, error) {
|
|
103
119
|
const candidate = error;
|
|
104
120
|
const stderr = candidate.stderr === undefined ? Buffer.alloc(0) : Buffer.from(candidate.stderr);
|
|
121
|
+
const stdout = candidate.stdout === undefined ? Buffer.alloc(0) : Buffer.from(candidate.stdout);
|
|
105
122
|
const detail = stderr.length === 0 ? candidate.message ?? "git command failed" : stderr.toString("utf8");
|
|
106
123
|
return new GitPlumbingError({
|
|
124
|
+
stdout,
|
|
107
125
|
stderr,
|
|
108
126
|
status: candidate.status ?? null,
|
|
109
127
|
message: `${command.join(" ")}: ${detail}`,
|
|
@@ -145,8 +163,12 @@ function assertRef(ref) {
|
|
|
145
163
|
throw new Error(`invalid Git ref: ${ref}`);
|
|
146
164
|
}
|
|
147
165
|
}
|
|
166
|
+
function assertAssertionRef(ref) {
|
|
167
|
+
if (ref !== "HEAD")
|
|
168
|
+
assertRef(ref);
|
|
169
|
+
}
|
|
148
170
|
export function readRef(repository, ref) {
|
|
149
|
-
|
|
171
|
+
assertAssertionRef(ref);
|
|
150
172
|
try {
|
|
151
173
|
const oid = runGit(repository, ["rev-parse", "--verify", "--quiet", ref]).toString("utf8").trim();
|
|
152
174
|
if (oid.length === 0)
|
|
@@ -162,7 +184,7 @@ export function readRef(repository, ref) {
|
|
|
162
184
|
}
|
|
163
185
|
function readTreeForCommit(repository, commit) {
|
|
164
186
|
assertOid(commit, "Git commit");
|
|
165
|
-
const tree = runGit(repository, ["
|
|
187
|
+
const tree = runGit(repository, ["show", "-s", "--format=%T", commit]).toString("utf8").trim();
|
|
166
188
|
assertOid(tree, "Git tree");
|
|
167
189
|
return tree;
|
|
168
190
|
}
|
|
@@ -184,7 +206,7 @@ function parseTreeEntries(output) {
|
|
|
184
206
|
}
|
|
185
207
|
return entries;
|
|
186
208
|
}
|
|
187
|
-
function readTreeEntries(repository, tree) {
|
|
209
|
+
export function readTreeEntries(repository, tree) {
|
|
188
210
|
assertOid(tree, "tree");
|
|
189
211
|
const output = runGit(repository, ["ls-tree", "-r", "-z", "--full-tree", tree]);
|
|
190
212
|
return parseTreeEntries(output);
|
|
@@ -299,16 +321,6 @@ function readObjects(repository, oids) {
|
|
|
299
321
|
malformedBatchOutput("trailing bytes");
|
|
300
322
|
return objects;
|
|
301
323
|
}
|
|
302
|
-
/** Read immutable Git objects through one structured, length-delimited batch. */
|
|
303
|
-
export function readBlobs(repository, oids) {
|
|
304
|
-
const blobs = new Map();
|
|
305
|
-
for (const [oid, object] of readObjects(repository, oids)) {
|
|
306
|
-
if (object.type !== "blob")
|
|
307
|
-
malformedBatchOutput(`expected blob for ${oid}, found ${object.type}`);
|
|
308
|
-
blobs.set(oid, object.bytes);
|
|
309
|
-
}
|
|
310
|
-
return blobs;
|
|
311
|
-
}
|
|
312
324
|
function validateGitFormat(repository, paths) {
|
|
313
325
|
const format = paths.get(GIT_FORMAT_PATH);
|
|
314
326
|
if (format === undefined)
|
|
@@ -375,13 +387,27 @@ function writePreparedTrees(repository, prepared) {
|
|
|
375
387
|
export function updateGitTree(repository, baseTree, changes) {
|
|
376
388
|
const update = treeUpdate(changes);
|
|
377
389
|
const bases = baseTreeEntries(repository, baseTree, [...changes.keys()], update.nodePaths);
|
|
378
|
-
const representativeOid = baseTree ?? changes.values().
|
|
390
|
+
const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
|
|
379
391
|
if (representativeOid === undefined)
|
|
380
392
|
throw new Error("cannot write an empty Git tree without an object format");
|
|
381
393
|
const prepared = prepareTreeUpdate({ update, bases, oidBytes: representativeOid.length / 2 });
|
|
382
394
|
writePreparedTrees(repository, prepared.trees);
|
|
383
395
|
return prepared.root;
|
|
384
396
|
}
|
|
397
|
+
/** Write a tree update from directory entries frozen with the admitted base tree. */
|
|
398
|
+
export function updateGitTreeFromFrozenDirectories(repository, baseTree, directories, changes) {
|
|
399
|
+
const update = treeUpdate(changes);
|
|
400
|
+
const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
|
|
401
|
+
if (representativeOid === undefined)
|
|
402
|
+
throw new Error("cannot write an empty Git tree without an object format");
|
|
403
|
+
const prepared = prepareTreeUpdate({
|
|
404
|
+
update,
|
|
405
|
+
bases: directories,
|
|
406
|
+
oidBytes: representativeOid.length / 2,
|
|
407
|
+
});
|
|
408
|
+
writePreparedTrees(repository, prepared.trees);
|
|
409
|
+
return prepared.root;
|
|
410
|
+
}
|
|
385
411
|
function writeCommitObject(input) {
|
|
386
412
|
const { repository, tree, parents, message, actor, email, at } = input;
|
|
387
413
|
assertOid(tree, "commit tree");
|
|
@@ -413,7 +439,23 @@ export function writeCommit(input) {
|
|
|
413
439
|
...(input.at === undefined ? {} : { at: input.at }),
|
|
414
440
|
});
|
|
415
441
|
}
|
|
416
|
-
|
|
442
|
+
function refAssertionLine(assertion) {
|
|
443
|
+
assertAssertionRef(assertion.ref);
|
|
444
|
+
assertOid(assertion.oid, `expected oid for ${assertion.ref}`);
|
|
445
|
+
return `${assertion.ref === "HEAD" ? "" : "option no-deref\n"}verify ${assertion.ref} ${assertion.oid}`;
|
|
446
|
+
}
|
|
447
|
+
function refUpdateLine(update, index) {
|
|
448
|
+
assertRef(update.ref);
|
|
449
|
+
if (index > 0 && update.ref === GIT_REF)
|
|
450
|
+
throw new Error(`duplicate ref update: ${update.ref}`);
|
|
451
|
+
if (index > 0 && update.expectedOid === null)
|
|
452
|
+
throw new Error("target ref updates require non-null OIDs");
|
|
453
|
+
assertOid(update.newOid, `new oid for ${update.ref}`);
|
|
454
|
+
if (update.expectedOid !== null)
|
|
455
|
+
assertOid(update.expectedOid, `expected oid for ${update.ref}`);
|
|
456
|
+
return `option no-deref\nupdate ${update.ref} ${update.newOid} ${update.expectedOid ?? "0".repeat(update.newOid.length)}`;
|
|
457
|
+
}
|
|
458
|
+
export function updateRefsAtomically(repository, updates, assertions = []) {
|
|
417
459
|
if (updates.length === 0)
|
|
418
460
|
throw new Error("an atomic ref transaction needs a Git update");
|
|
419
461
|
if (updates.length > 2)
|
|
@@ -422,24 +464,16 @@ export function updateRefsAtomically(repository, updates) {
|
|
|
422
464
|
if (git === undefined || git.ref !== GIT_REF) {
|
|
423
465
|
throw new Error(`the first atomic update must be the Git ref: ${GIT_REF}`);
|
|
424
466
|
}
|
|
425
|
-
const lines = [
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
assertOid(update.newOid, `new oid for ${update.ref}`);
|
|
435
|
-
if (update.expectedOid !== null)
|
|
436
|
-
assertOid(update.expectedOid, `expected oid for ${update.ref}`);
|
|
437
|
-
const expectedOid = update.expectedOid ?? "0".repeat(update.newOid.length);
|
|
438
|
-
lines.push(`update ${update.ref} ${update.newOid} ${expectedOid}`);
|
|
439
|
-
}
|
|
440
|
-
lines.push("prepare", "commit", "");
|
|
467
|
+
const lines = [
|
|
468
|
+
"start",
|
|
469
|
+
...assertions.map(refAssertionLine),
|
|
470
|
+
...updates.map(refUpdateLine),
|
|
471
|
+
"prepare",
|
|
472
|
+
"commit",
|
|
473
|
+
"",
|
|
474
|
+
];
|
|
441
475
|
try {
|
|
442
|
-
runGit(repository, ["update-ref", "--stdin"
|
|
476
|
+
runGit(repository, ["update-ref", "--stdin"], lines.join("\n"));
|
|
443
477
|
return { kind: "published" };
|
|
444
478
|
}
|
|
445
479
|
catch (error) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SnapshotId } from "../core/facts/types.js";
|
|
2
|
+
import { type GitRepository } from "./repository.js";
|
|
3
|
+
export type MaterializedScratchCandidate = Readonly<{
|
|
4
|
+
cwd: string;
|
|
5
|
+
dispose: () => WorktreeLeak | null;
|
|
6
|
+
}>;
|
|
7
|
+
export type WorktreeLeak = Readonly<{
|
|
8
|
+
path: string;
|
|
9
|
+
diagnostic: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function orphanedScratchWorktrees(paths: Iterable<string>): readonly string[];
|
|
12
|
+
/** Materialize a disposable candidate worktree and own only its removal. */
|
|
13
|
+
export declare function materializeScratchCandidate(repository: GitRepository, candidate: SnapshotId): MaterializedScratchCandidate;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { gitObjectIdForSnapshot } from "./identity.js";
|
|
6
|
+
import { runGit } from "./repository.js";
|
|
7
|
+
import { currentProcessIdentity, probeProcessIdentity, } from "../runtime/proc/run.js";
|
|
8
|
+
const SCRATCH_PREFIX = "keiyaku-v4-verify-";
|
|
9
|
+
const SCRATCH_ROOT = realpathSync(tmpdir());
|
|
10
|
+
function scratchPath() {
|
|
11
|
+
const owner = currentProcessIdentity();
|
|
12
|
+
const identity = Buffer.from(owner.spawnedAt, "utf8").toString("base64url");
|
|
13
|
+
return join(SCRATCH_ROOT, `${SCRATCH_PREFIX}${owner.pid}-${identity}-${randomBytes(12).toString("hex")}`);
|
|
14
|
+
}
|
|
15
|
+
function scratchOwner(path) {
|
|
16
|
+
const prefix = join(SCRATCH_ROOT, SCRATCH_PREFIX);
|
|
17
|
+
if (!path.startsWith(prefix))
|
|
18
|
+
return null;
|
|
19
|
+
const suffix = path.slice(prefix.length);
|
|
20
|
+
const match = /^([1-9][0-9]*)-([A-Za-z0-9_-]+)-([0-9a-f]{24})$/.exec(suffix);
|
|
21
|
+
if (match === null)
|
|
22
|
+
return null;
|
|
23
|
+
const pid = Number(match[1]);
|
|
24
|
+
if (!Number.isSafeInteger(pid))
|
|
25
|
+
return null;
|
|
26
|
+
let spawnedAt;
|
|
27
|
+
try {
|
|
28
|
+
spawnedAt = Buffer.from(match[2], "base64url").toString("utf8");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return spawnedAt.length === 0 ? null : { pid, spawnedAt };
|
|
34
|
+
}
|
|
35
|
+
export function orphanedScratchWorktrees(paths) {
|
|
36
|
+
const orphaned = [];
|
|
37
|
+
for (const path of paths) {
|
|
38
|
+
const owner = scratchOwner(path);
|
|
39
|
+
if (owner === null)
|
|
40
|
+
continue;
|
|
41
|
+
const state = probeProcessIdentity(owner);
|
|
42
|
+
if (state.kind === "gone" || state.kind === "replaced")
|
|
43
|
+
orphaned.push(path);
|
|
44
|
+
}
|
|
45
|
+
return orphaned;
|
|
46
|
+
}
|
|
47
|
+
/** Materialize a disposable candidate worktree and own only its removal. */
|
|
48
|
+
export function materializeScratchCandidate(repository, candidate) {
|
|
49
|
+
const cwd = scratchPath();
|
|
50
|
+
runGit(repository, ["worktree", "add", "--detach", cwd, gitObjectIdForSnapshot(candidate)]);
|
|
51
|
+
return {
|
|
52
|
+
cwd,
|
|
53
|
+
dispose: () => {
|
|
54
|
+
try {
|
|
55
|
+
runGit(repository, ["worktree", "remove", "--force", cwd]);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
path: cwd,
|
|
61
|
+
diagnostic: error instanceof Error ? error.message : String(error),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type HeldSqliteTransactionLock } from "../coordination/sqlite-transaction-lock.js";
|
|
2
|
+
import type { ContractId, ContractState, SnapshotId } from "../core/facts/types.js";
|
|
3
|
+
import type { RefOperation } from "../core/facts/offer.js";
|
|
4
|
+
import { type GitRepository } from "./repository.js";
|
|
5
|
+
export type WorkspaceNotOnTargetRefusal = Readonly<{
|
|
6
|
+
kind: "workspace-not-on-target";
|
|
7
|
+
contractId: ContractId;
|
|
8
|
+
target: string;
|
|
9
|
+
branch: string | null;
|
|
10
|
+
}>;
|
|
11
|
+
export type CheckoutNotFollowableRefusal = Readonly<{
|
|
12
|
+
kind: "checkout-not-followable";
|
|
13
|
+
contractId: ContractId;
|
|
14
|
+
target: string;
|
|
15
|
+
path: string;
|
|
16
|
+
reason: "staged" | "conflict" | "untracked";
|
|
17
|
+
paths: readonly string[];
|
|
18
|
+
}>;
|
|
19
|
+
export type TargetPlacementRefusal = CheckoutNotFollowableRefusal | WorkspaceNotOnTargetRefusal;
|
|
20
|
+
export type TargetCheckoutEffect = Readonly<{
|
|
21
|
+
kind: "target-checkout";
|
|
22
|
+
path: string;
|
|
23
|
+
target: string;
|
|
24
|
+
action: "followed" | "recovered";
|
|
25
|
+
}>;
|
|
26
|
+
export type TargetCheckoutLag = Readonly<{
|
|
27
|
+
kind: "target-checkout-retained";
|
|
28
|
+
path: string;
|
|
29
|
+
target: string;
|
|
30
|
+
diagnostic: string;
|
|
31
|
+
}>;
|
|
32
|
+
export type TargetPlacementPhysicalResult = Readonly<{
|
|
33
|
+
effects: readonly TargetCheckoutEffect[];
|
|
34
|
+
lag: readonly TargetCheckoutLag[];
|
|
35
|
+
}>;
|
|
36
|
+
type FollowArm = Readonly<{
|
|
37
|
+
kind: "ordinary" | "here";
|
|
38
|
+
path: string;
|
|
39
|
+
}>;
|
|
40
|
+
export type PreparedTargetPlacement = Readonly<{
|
|
41
|
+
target: RefOperation;
|
|
42
|
+
arms: readonly FollowArm[];
|
|
43
|
+
}>;
|
|
44
|
+
export type TargetPlacementPreparation = Readonly<{
|
|
45
|
+
kind: "prepared";
|
|
46
|
+
placement: PreparedTargetPlacement;
|
|
47
|
+
}> | Readonly<{
|
|
48
|
+
kind: "refused";
|
|
49
|
+
refusal: TargetPlacementRefusal;
|
|
50
|
+
}>;
|
|
51
|
+
export declare function observeTargetHead(repository: GitRepository, target: string): SnapshotId | null;
|
|
52
|
+
export declare function acquireTargetPlacementFence(repository: GitRepository, target: string): Promise<HeldSqliteTransactionLock>;
|
|
53
|
+
export declare function prepareTargetPlacement(repository: GitRepository, state: ContractState, target: RefOperation): TargetPlacementPreparation;
|
|
54
|
+
export declare function followTargetPlacement(repository: GitRepository, prepared: PreparedTargetPlacement): TargetPlacementPhysicalResult;
|
|
55
|
+
export declare function recoverTargetPlacement(repository: GitRepository, state: ContractState): TargetPlacementPhysicalResult;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { acquireSqliteTransactionLock } from "../coordination/sqlite-transaction-lock.js";
|
|
3
|
+
import { gitObjectId, gitObjectIdForSnapshot, gitRefLocator, mintSnapshotId } from "./identity.js";
|
|
4
|
+
import { currentBranch } from "./observe.js";
|
|
5
|
+
import { commonGitDirectory, GitPlumbingError, readRef, registeredWorktrees, runGit, } from "./repository.js";
|
|
6
|
+
import { captureWorkspaceTree, withPrivateGitIndex } from "./workspace.js";
|
|
7
|
+
export function observeTargetHead(repository, target) {
|
|
8
|
+
const value = readRef(repository, target);
|
|
9
|
+
return value === null ? null : mintSnapshotId(value);
|
|
10
|
+
}
|
|
11
|
+
function diagnostic(error) {
|
|
12
|
+
return error instanceof Error ? error.message : String(error);
|
|
13
|
+
}
|
|
14
|
+
function nulPaths(bytes) {
|
|
15
|
+
const fields = bytes.toString("utf8").split("\0");
|
|
16
|
+
if (fields.at(-1) !== "")
|
|
17
|
+
throw new Error("Git path output is not NUL terminated");
|
|
18
|
+
return [...new Set(fields.slice(0, -1))].sort();
|
|
19
|
+
}
|
|
20
|
+
function gitPaths(repository, path, args) {
|
|
21
|
+
return nulPaths(runGit(repository, ["-C", path, ...args]));
|
|
22
|
+
}
|
|
23
|
+
function commitTree(repository, snapshot) {
|
|
24
|
+
return gitObjectId(runGit(repository, ["show", "-s", "--format=%T", gitObjectIdForSnapshot(snapshot)])
|
|
25
|
+
.toString("utf8")
|
|
26
|
+
.trim(), "commit tree");
|
|
27
|
+
}
|
|
28
|
+
function pathsOverlap(left, right) {
|
|
29
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
30
|
+
}
|
|
31
|
+
function intersection(left, right) {
|
|
32
|
+
return left.filter((candidate) => right.some((path) => pathsOverlap(candidate, path))).sort();
|
|
33
|
+
}
|
|
34
|
+
function failurePaths(error) {
|
|
35
|
+
if (!(error instanceof GitPlumbingError))
|
|
36
|
+
return [];
|
|
37
|
+
const paths = new Set();
|
|
38
|
+
for (const match of error.stderr.toString("utf8").matchAll(/(?:Entry|Untracked working tree file) '([^']+)'/gu)) {
|
|
39
|
+
paths.add(match[1]);
|
|
40
|
+
}
|
|
41
|
+
return [...paths].sort();
|
|
42
|
+
}
|
|
43
|
+
function checkoutRefusal(contractId, target, path, reason, error) {
|
|
44
|
+
const paths = failurePaths(error);
|
|
45
|
+
if (paths.length === 0)
|
|
46
|
+
throw error;
|
|
47
|
+
return { kind: "checkout-not-followable", contractId, target: target.target, path, reason, paths };
|
|
48
|
+
}
|
|
49
|
+
function indexMatchesTreeOnPaths(repository, path, tree, paths) {
|
|
50
|
+
return gitPaths(repository, path, ["diff-index", "--cached", "--name-only", "-z", tree, "--", ...paths]).length === 0;
|
|
51
|
+
}
|
|
52
|
+
function workspaceMatchesTreeOnPaths(repository, path, tree, workspaceTree, paths) {
|
|
53
|
+
return gitPaths(repository, path, ["diff", "--name-only", "-z", tree, workspaceTree, "--", ...paths]).length === 0;
|
|
54
|
+
}
|
|
55
|
+
function sourceWorktree(repository) {
|
|
56
|
+
return resolve(runGit(repository, ["rev-parse", "--show-toplevel"]).toString("utf8").trim());
|
|
57
|
+
}
|
|
58
|
+
function ordinaryPrecheck(repository, contractId, target, path) {
|
|
59
|
+
const predecessor = gitObjectIdForSnapshot(target.expectedOid);
|
|
60
|
+
const candidate = gitObjectIdForSnapshot(target.newOid);
|
|
61
|
+
try {
|
|
62
|
+
withPrivateGitIndex((environment) => {
|
|
63
|
+
runGit(repository, ["-C", path, "read-tree", "-i", `--index-output=${environment.GIT_INDEX_FILE}`, "-m", predecessor, candidate]);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return checkoutRefusal(contractId, target, path, "staged", error);
|
|
68
|
+
}
|
|
69
|
+
const changed = gitPaths(repository, path, ["diff", "--name-only", "-z", predecessor, candidate]);
|
|
70
|
+
const dirty = gitPaths(repository, path, ["diff-files", "--name-only", "-z"]);
|
|
71
|
+
const conflicts = intersection(changed, dirty);
|
|
72
|
+
if (conflicts.length > 0) {
|
|
73
|
+
return {
|
|
74
|
+
kind: "checkout-not-followable",
|
|
75
|
+
contractId,
|
|
76
|
+
target: target.target,
|
|
77
|
+
path,
|
|
78
|
+
reason: "conflict",
|
|
79
|
+
paths: conflicts,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const additions = gitPaths(repository, path, ["diff", "--name-only", "--diff-filter=A", "-z", predecessor, candidate]);
|
|
83
|
+
const untracked = gitPaths(repository, path, ["ls-files", "--others", "-z"]);
|
|
84
|
+
const collisions = intersection(additions, untracked);
|
|
85
|
+
if (collisions.length > 0) {
|
|
86
|
+
return {
|
|
87
|
+
kind: "checkout-not-followable",
|
|
88
|
+
contractId,
|
|
89
|
+
target: target.target,
|
|
90
|
+
path,
|
|
91
|
+
reason: "untracked",
|
|
92
|
+
paths: collisions,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
runGit(repository, ["-C", path, "read-tree", "--dry-run", "-m", "-u", predecessor, candidate]);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const reason = error instanceof GitPlumbingError && /untracked working tree file/iu.test(error.stderr.toString("utf8"))
|
|
100
|
+
? "untracked"
|
|
101
|
+
: "conflict";
|
|
102
|
+
return checkoutRefusal(contractId, target, path, reason, error);
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
export async function acquireTargetPlacementFence(repository, target) {
|
|
107
|
+
const locator = gitRefLocator(target);
|
|
108
|
+
return acquireSqliteTransactionLock({
|
|
109
|
+
path: resolve(commonGitDirectory(repository), "keiyaku", "locks", "target-placement", `${locator}.sqlite`),
|
|
110
|
+
mode: "immediate",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
export function prepareTargetPlacement(repository, state, target) {
|
|
114
|
+
if (state.coordinates.target !== target.target || state.delivery?.data.integration.snapshot !== target.newOid) {
|
|
115
|
+
throw new Error("placement state does not match its offered target movement");
|
|
116
|
+
}
|
|
117
|
+
const worktrees = registeredWorktrees(repository)
|
|
118
|
+
.filter((worktree) => worktree.branch === target.target)
|
|
119
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
120
|
+
const hereSource = state.coordinates.workspace === "here" ? sourceWorktree(repository) : null;
|
|
121
|
+
if (hereSource !== null) {
|
|
122
|
+
const branch = currentBranch(repository, hereSource);
|
|
123
|
+
if (branch !== target.target) {
|
|
124
|
+
return {
|
|
125
|
+
kind: "refused",
|
|
126
|
+
refusal: {
|
|
127
|
+
kind: "workspace-not-on-target",
|
|
128
|
+
contractId: state.id,
|
|
129
|
+
target: target.target,
|
|
130
|
+
branch,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const arms = [];
|
|
136
|
+
for (const worktree of worktrees) {
|
|
137
|
+
const kind = hereSource !== null && resolve(worktree.path) === hereSource ? "here" : "ordinary";
|
|
138
|
+
if (kind === "ordinary") {
|
|
139
|
+
const refusal = ordinaryPrecheck(repository, state.id, target, worktree.path);
|
|
140
|
+
if (refusal !== null)
|
|
141
|
+
return { kind: "refused", refusal };
|
|
142
|
+
}
|
|
143
|
+
arms.push({ kind, path: worktree.path });
|
|
144
|
+
}
|
|
145
|
+
if (hereSource !== null && !arms.some((arm) => arm.kind === "here")) {
|
|
146
|
+
throw new Error("targeted here workspace is not a registered checkout of its target");
|
|
147
|
+
}
|
|
148
|
+
return { kind: "prepared", placement: { target, arms } };
|
|
149
|
+
}
|
|
150
|
+
export function followTargetPlacement(repository, prepared) {
|
|
151
|
+
const effects = [];
|
|
152
|
+
const lag = [];
|
|
153
|
+
const predecessor = gitObjectIdForSnapshot(prepared.target.expectedOid);
|
|
154
|
+
const candidate = gitObjectIdForSnapshot(prepared.target.newOid);
|
|
155
|
+
for (const arm of prepared.arms) {
|
|
156
|
+
try {
|
|
157
|
+
if (arm.kind === "here")
|
|
158
|
+
runGit(repository, ["-C", arm.path, "read-tree", candidate]);
|
|
159
|
+
else
|
|
160
|
+
runGit(repository, ["-C", arm.path, "read-tree", "-m", "-u", predecessor, candidate]);
|
|
161
|
+
effects.push({ kind: "target-checkout", path: arm.path, target: prepared.target.target, action: "followed" });
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
lag.push({
|
|
165
|
+
kind: "target-checkout-retained",
|
|
166
|
+
path: arm.path,
|
|
167
|
+
target: prepared.target.target,
|
|
168
|
+
diagnostic: diagnostic(error),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { effects, lag };
|
|
173
|
+
}
|
|
174
|
+
function recoveryLag(path, target, detail) {
|
|
175
|
+
return { kind: "target-checkout-retained", path, target, diagnostic: detail };
|
|
176
|
+
}
|
|
177
|
+
function recoverCheckout(input) {
|
|
178
|
+
const { repository, path, predecessor, candidate, predecessorTree, candidateTree } = input;
|
|
179
|
+
const changedPaths = gitPaths(repository, path, ["diff", "--name-only", "-z", predecessorTree, candidateTree]);
|
|
180
|
+
if (changedPaths.length === 0)
|
|
181
|
+
return "complete";
|
|
182
|
+
const workspaceTree = captureWorkspaceTree(repository, path).tree;
|
|
183
|
+
const candidateIndex = indexMatchesTreeOnPaths(repository, path, candidateTree, changedPaths);
|
|
184
|
+
const candidateWorkspace = workspaceMatchesTreeOnPaths(repository, path, candidateTree, workspaceTree, changedPaths);
|
|
185
|
+
if (candidateIndex && candidateWorkspace)
|
|
186
|
+
return "complete";
|
|
187
|
+
if (workspaceTree === candidateTree) {
|
|
188
|
+
runGit(repository, ["-C", path, "read-tree", gitObjectIdForSnapshot(candidate)]);
|
|
189
|
+
return "recovered";
|
|
190
|
+
}
|
|
191
|
+
const predecessorIndex = indexMatchesTreeOnPaths(repository, path, predecessorTree, changedPaths);
|
|
192
|
+
if (predecessorIndex && candidateWorkspace) {
|
|
193
|
+
runGit(repository, [
|
|
194
|
+
"-C",
|
|
195
|
+
path,
|
|
196
|
+
"read-tree",
|
|
197
|
+
"-i",
|
|
198
|
+
"-m",
|
|
199
|
+
gitObjectIdForSnapshot(predecessor),
|
|
200
|
+
gitObjectIdForSnapshot(candidate),
|
|
201
|
+
]);
|
|
202
|
+
return "recovered";
|
|
203
|
+
}
|
|
204
|
+
const predecessorWorkspace = workspaceMatchesTreeOnPaths(repository, path, predecessorTree, workspaceTree, changedPaths);
|
|
205
|
+
if (!predecessorIndex || !predecessorWorkspace)
|
|
206
|
+
return "retained";
|
|
207
|
+
runGit(repository, [
|
|
208
|
+
"-C",
|
|
209
|
+
path,
|
|
210
|
+
"read-tree",
|
|
211
|
+
"-m",
|
|
212
|
+
"-u",
|
|
213
|
+
gitObjectIdForSnapshot(predecessor),
|
|
214
|
+
gitObjectIdForSnapshot(candidate),
|
|
215
|
+
]);
|
|
216
|
+
return "recovered";
|
|
217
|
+
}
|
|
218
|
+
export function recoverTargetPlacement(repository, state) {
|
|
219
|
+
const target = state.coordinates.target;
|
|
220
|
+
const delivery = state.delivery;
|
|
221
|
+
if (state.terminal?.kind !== "claimed" || target === undefined || delivery === null)
|
|
222
|
+
return { effects: [], lag: [] };
|
|
223
|
+
if (readRef(repository, target) !== delivery.data.integration.snapshot)
|
|
224
|
+
return { effects: [], lag: [] };
|
|
225
|
+
const candidateTree = commitTree(repository, delivery.data.integration.snapshot);
|
|
226
|
+
const predecessorTree = commitTree(repository, delivery.data.integration.predecessor);
|
|
227
|
+
const worktrees = registeredWorktrees(repository)
|
|
228
|
+
.filter((worktree) => worktree.branch === target)
|
|
229
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
230
|
+
const effects = [];
|
|
231
|
+
const lag = [];
|
|
232
|
+
for (const worktree of worktrees) {
|
|
233
|
+
try {
|
|
234
|
+
const recovery = recoverCheckout({
|
|
235
|
+
repository,
|
|
236
|
+
path: worktree.path,
|
|
237
|
+
predecessor: delivery.data.integration.predecessor,
|
|
238
|
+
candidate: delivery.data.integration.snapshot,
|
|
239
|
+
predecessorTree,
|
|
240
|
+
candidateTree,
|
|
241
|
+
});
|
|
242
|
+
if (recovery === "retained") {
|
|
243
|
+
lag.push(recoveryLag(worktree.path, target, "target checkout entries are neither predecessor nor candidate"));
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (recovery === "recovered") {
|
|
247
|
+
effects.push({ kind: "target-checkout", path: worktree.path, target, action: "recovered" });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
lag.push(recoveryLag(worktree.path, target, diagnostic(error)));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { effects, lag };
|
|
255
|
+
}
|