@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,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import { isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { AuthorityCorruptionError } from "../core/facts/errors.js";
|
|
4
|
+
import { consumeProcessStdout } from "../runtime/proc/run.js";
|
|
4
5
|
import { gitObjectId } from "./identity.js";
|
|
5
6
|
import { parseTreeObject, prepareTreeUpdate, treeUpdate, validPath, } from "./tree.js";
|
|
6
7
|
export const GIT_REF = "refs/heads/keiyaku-state";
|
|
7
8
|
export const DELIVERY_REF_NAMESPACE = "refs/heads/keiyaku-delivery";
|
|
8
9
|
export const CANDIDATE_PIN_REF_NAMESPACE = "refs/heads/keiyaku-candidate";
|
|
9
10
|
export const GIT_FORMAT_PATH = "meta/format.json";
|
|
10
|
-
const CURRENT_FORMAT_VERSION =
|
|
11
|
+
const CURRENT_FORMAT_VERSION = 4;
|
|
11
12
|
export const GIT_FORMAT_BYTES = `{"version":${CURRENT_FORMAT_VERSION}}\n`;
|
|
12
13
|
export function isKeiyakuOwnedRef(ref) {
|
|
13
14
|
return ref === GIT_REF
|
|
@@ -25,20 +26,23 @@ export class NoGitWorldError extends Error {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
export class GitPlumbingError extends Error {
|
|
29
|
+
stdout;
|
|
28
30
|
stderr;
|
|
29
31
|
status;
|
|
30
32
|
pid;
|
|
31
33
|
constructor(input) {
|
|
32
34
|
super(input.message);
|
|
33
35
|
this.name = "GitPlumbingError";
|
|
36
|
+
this.stdout = Buffer.from(input.stdout ?? "");
|
|
34
37
|
this.stderr = Buffer.from(input.stderr);
|
|
35
38
|
this.status = input.status;
|
|
36
39
|
this.pid = input.pid ?? null;
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
|
-
|
|
42
|
+
const GIT_STDERR_BYTES = 16 * 1024;
|
|
43
|
+
function worktreesFromPorcelain(output) {
|
|
40
44
|
const fields = output.toString("utf8").split("\0");
|
|
41
|
-
const
|
|
45
|
+
const worktrees = [];
|
|
42
46
|
let record = [];
|
|
43
47
|
const finishRecord = () => {
|
|
44
48
|
if (record.length === 0)
|
|
@@ -50,7 +54,11 @@ function worktreePathsFromPorcelain(output) {
|
|
|
50
54
|
const path = worktree.slice("worktree ".length);
|
|
51
55
|
if (path.length === 0)
|
|
52
56
|
throw new Error("Git worktree porcelain output has an empty worktree path");
|
|
53
|
-
|
|
57
|
+
const branchField = record.find((field) => field.startsWith("branch "));
|
|
58
|
+
const branch = branchField === undefined ? null : branchField.slice("branch ".length);
|
|
59
|
+
if (branch !== null && !branch.startsWith("refs/"))
|
|
60
|
+
throw new Error("Git worktree porcelain output has an invalid branch");
|
|
61
|
+
worktrees.push({ path: resolve(path), branch });
|
|
54
62
|
record = [];
|
|
55
63
|
};
|
|
56
64
|
for (const field of fields) {
|
|
@@ -61,22 +69,31 @@ function worktreePathsFromPorcelain(output) {
|
|
|
61
69
|
record.push(field);
|
|
62
70
|
}
|
|
63
71
|
finishRecord();
|
|
64
|
-
if (
|
|
72
|
+
if (worktrees.length === 0)
|
|
65
73
|
throw new Error("Git worktree porcelain output has no worktrees");
|
|
66
|
-
return
|
|
74
|
+
return worktrees;
|
|
67
75
|
}
|
|
68
|
-
export function
|
|
69
|
-
return
|
|
76
|
+
export async function registeredWorktrees(repository) {
|
|
77
|
+
return worktreesFromPorcelain(await runGit(repository, ["worktree", "list", "--porcelain", "-z"]));
|
|
70
78
|
}
|
|
71
|
-
export function
|
|
79
|
+
export async function registeredWorktreePaths(repository) {
|
|
80
|
+
return (await registeredWorktrees(repository)).map((worktree) => worktree.path);
|
|
81
|
+
}
|
|
82
|
+
export async function repositoryAt(cwd) {
|
|
72
83
|
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
73
84
|
throw new Error("repository path must be a nonempty string");
|
|
74
85
|
}
|
|
75
86
|
const effectiveCwd = resolve(cwd);
|
|
76
|
-
const provisional = {
|
|
87
|
+
const provisional = {
|
|
88
|
+
effectiveCwd,
|
|
89
|
+
primaryWorktree: effectiveCwd,
|
|
90
|
+
commonDirectory: effectiveCwd,
|
|
91
|
+
};
|
|
77
92
|
let primaryWorktree;
|
|
93
|
+
let commonDirectory;
|
|
78
94
|
try {
|
|
79
|
-
primaryWorktree = registeredWorktreePaths(provisional)[0];
|
|
95
|
+
primaryWorktree = (await registeredWorktreePaths(provisional))[0];
|
|
96
|
+
commonDirectory = await absoluteGitPath(provisional, ["--git-common-dir"], "common Git directory");
|
|
80
97
|
}
|
|
81
98
|
catch (error) {
|
|
82
99
|
if (error instanceof GitPlumbingError && error.status === 128) {
|
|
@@ -84,58 +101,100 @@ export function repositoryAt(cwd) {
|
|
|
84
101
|
}
|
|
85
102
|
throw error;
|
|
86
103
|
}
|
|
87
|
-
return { effectiveCwd, primaryWorktree };
|
|
104
|
+
return { effectiveCwd, primaryWorktree, commonDirectory };
|
|
88
105
|
}
|
|
89
|
-
function absoluteGitPath(repository, args, label) {
|
|
90
|
-
const value = runGit(repository, ["rev-parse", "--path-format=absolute", ...args]).toString("utf8").trim();
|
|
106
|
+
async function absoluteGitPath(repository, args, label) {
|
|
107
|
+
const value = (await runGit(repository, ["rev-parse", "--path-format=absolute", ...args])).toString("utf8").trim();
|
|
91
108
|
if (value.length === 0)
|
|
92
109
|
throw new Error(`${label} is empty`);
|
|
93
110
|
return resolve(isAbsolute(value) ? value : resolve(repository.effectiveCwd, value));
|
|
94
111
|
}
|
|
95
112
|
export function commonGitDirectory(repository) {
|
|
96
|
-
return
|
|
113
|
+
return repository.commonDirectory;
|
|
97
114
|
}
|
|
98
|
-
export function worktreeGitDirectory(repository, worktree) {
|
|
115
|
+
export async function worktreeGitDirectory(repository, worktree) {
|
|
99
116
|
const scoped = { ...repository, effectiveCwd: worktree };
|
|
100
|
-
return absoluteGitPath(scoped, ["--git-dir"], "worktree Git directory");
|
|
117
|
+
return await absoluteGitPath(scoped, ["--git-dir"], "worktree Git directory");
|
|
118
|
+
}
|
|
119
|
+
export async function worktreeRoot(repository) {
|
|
120
|
+
return await absoluteGitPath(repository, ["--show-toplevel"], "worktree root");
|
|
101
121
|
}
|
|
102
122
|
function commandError(command, error) {
|
|
103
123
|
const candidate = error;
|
|
104
124
|
const stderr = candidate.stderr === undefined ? Buffer.alloc(0) : Buffer.from(candidate.stderr);
|
|
125
|
+
const stdout = candidate.stdout === undefined ? Buffer.alloc(0) : Buffer.from(candidate.stdout);
|
|
105
126
|
const detail = stderr.length === 0 ? candidate.message ?? "git command failed" : stderr.toString("utf8");
|
|
106
127
|
return new GitPlumbingError({
|
|
128
|
+
stdout,
|
|
107
129
|
stderr,
|
|
108
130
|
status: candidate.status ?? null,
|
|
109
131
|
message: `${command.join(" ")}: ${detail}`,
|
|
110
132
|
pid: candidate.pid ?? null,
|
|
111
133
|
});
|
|
112
134
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
async function executeGit(repository, args, input, environment) {
|
|
136
|
+
const stdout = [];
|
|
137
|
+
const stderr = [];
|
|
138
|
+
let stderrBytes = 0;
|
|
139
|
+
let inputError;
|
|
140
|
+
const child = spawn("git", [...args], {
|
|
141
|
+
cwd: repository.effectiveCwd,
|
|
142
|
+
...(environment === undefined ? {} : { env: { ...process.env, ...environment } }),
|
|
143
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
});
|
|
146
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
147
|
+
child.stderr.on("data", (chunk) => {
|
|
148
|
+
if (stderrBytes >= GIT_STDERR_BYTES)
|
|
149
|
+
return;
|
|
150
|
+
const retained = chunk.subarray(0, GIT_STDERR_BYTES - stderrBytes);
|
|
151
|
+
stderr.push(retained);
|
|
152
|
+
stderrBytes += retained.length;
|
|
153
|
+
});
|
|
154
|
+
child.stdin.on("error", (error) => { inputError = error; });
|
|
155
|
+
child.stdin.end(input);
|
|
156
|
+
const terminal = await new Promise((resolveTerminal) => {
|
|
157
|
+
child.once("error", (error) => resolveTerminal({ code: null, error }));
|
|
158
|
+
child.once("close", (code) => resolveTerminal({ code }));
|
|
159
|
+
});
|
|
160
|
+
const output = Buffer.concat(stdout);
|
|
161
|
+
if (terminal.code === 0 && inputError === undefined)
|
|
162
|
+
return output;
|
|
163
|
+
throw commandError(args, {
|
|
164
|
+
message: terminal.error?.message ?? inputError?.message ?? `git exited with status ${terminal.code ?? "unknown"}`,
|
|
165
|
+
stdout: output,
|
|
166
|
+
stderr: Buffer.concat(stderr),
|
|
167
|
+
status: terminal.code,
|
|
168
|
+
pid: child.pid,
|
|
169
|
+
});
|
|
125
170
|
}
|
|
126
|
-
export function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
171
|
+
export async function runGit(repository, args, input) {
|
|
172
|
+
return await executeGit(repository, args, input);
|
|
173
|
+
}
|
|
174
|
+
export async function consumeGitStdout(repository, args, consume) {
|
|
175
|
+
const result = await consumeProcessStdout({
|
|
176
|
+
argv: ["git", ...args],
|
|
177
|
+
cwd: repository.effectiveCwd,
|
|
178
|
+
}, consume);
|
|
179
|
+
const { outcome } = result;
|
|
180
|
+
if (outcome.kind === "terminal" && outcome.code === 0)
|
|
181
|
+
return;
|
|
182
|
+
const status = outcome.kind === "terminal" ? outcome.code : null;
|
|
183
|
+
const stderr = outcome.kind === "terminal" ? outcome.stderr : "";
|
|
184
|
+
const message = outcome.kind === "terminal"
|
|
185
|
+
? `git exited with status ${outcome.code}`
|
|
186
|
+
: outcome.kind === "spawn-error" || outcome.kind === "stream-error"
|
|
187
|
+
? outcome.diagnostic
|
|
188
|
+
: `git process ended with ${outcome.kind}`;
|
|
189
|
+
throw commandError(args, {
|
|
190
|
+
message,
|
|
191
|
+
stderr,
|
|
192
|
+
status,
|
|
193
|
+
pid: result.pid ?? undefined,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
export async function runGitWithEnvironment(repository, args, input, environment) {
|
|
197
|
+
return await executeGit(repository, args, input, environment);
|
|
139
198
|
}
|
|
140
199
|
function assertOid(oid, label) {
|
|
141
200
|
gitObjectId(oid, label);
|
|
@@ -145,10 +204,14 @@ function assertRef(ref) {
|
|
|
145
204
|
throw new Error(`invalid Git ref: ${ref}`);
|
|
146
205
|
}
|
|
147
206
|
}
|
|
148
|
-
|
|
149
|
-
|
|
207
|
+
function assertAssertionRef(ref) {
|
|
208
|
+
if (ref !== "HEAD")
|
|
209
|
+
assertRef(ref);
|
|
210
|
+
}
|
|
211
|
+
export async function readRef(repository, ref) {
|
|
212
|
+
assertAssertionRef(ref);
|
|
150
213
|
try {
|
|
151
|
-
const oid = runGit(repository, ["rev-parse", "--verify", "--quiet", ref]).toString("utf8").trim();
|
|
214
|
+
const oid = (await runGit(repository, ["rev-parse", "--verify", "--quiet", ref])).toString("utf8").trim();
|
|
152
215
|
if (oid.length === 0)
|
|
153
216
|
return null;
|
|
154
217
|
assertOid(oid, `ref ${ref}`);
|
|
@@ -160,9 +223,9 @@ export function readRef(repository, ref) {
|
|
|
160
223
|
throw error;
|
|
161
224
|
}
|
|
162
225
|
}
|
|
163
|
-
function readTreeForCommit(repository, commit) {
|
|
226
|
+
async function readTreeForCommit(repository, commit) {
|
|
164
227
|
assertOid(commit, "Git commit");
|
|
165
|
-
const tree = runGit(repository, ["
|
|
228
|
+
const tree = (await runGit(repository, ["show", "-s", "--format=%T", commit])).toString("utf8").trim();
|
|
166
229
|
assertOid(tree, "Git tree");
|
|
167
230
|
return tree;
|
|
168
231
|
}
|
|
@@ -184,30 +247,30 @@ function parseTreeEntries(output) {
|
|
|
184
247
|
}
|
|
185
248
|
return entries;
|
|
186
249
|
}
|
|
187
|
-
function readTreeEntries(repository, tree) {
|
|
250
|
+
export async function readTreeEntries(repository, tree) {
|
|
188
251
|
assertOid(tree, "tree");
|
|
189
|
-
const output = runGit(repository, ["ls-tree", "-r", "-z", "--full-tree", tree]);
|
|
252
|
+
const output = await runGit(repository, ["ls-tree", "-r", "-z", "--full-tree", tree]);
|
|
190
253
|
return parseTreeEntries(output);
|
|
191
254
|
}
|
|
192
|
-
export function readGit(repository) {
|
|
193
|
-
const commit = readRef(repository, GIT_REF);
|
|
255
|
+
export async function readGit(repository) {
|
|
256
|
+
const commit = await readRef(repository, GIT_REF);
|
|
194
257
|
if (commit === null) {
|
|
195
258
|
return { commit: null, tree: null, paths: new Map() };
|
|
196
259
|
}
|
|
197
|
-
const tree = readTreeForCommit(repository, commit);
|
|
198
|
-
const paths = readTreeEntries(repository, tree);
|
|
199
|
-
validateGitFormat(repository, paths);
|
|
260
|
+
const tree = await readTreeForCommit(repository, commit);
|
|
261
|
+
const paths = await readTreeEntries(repository, tree);
|
|
262
|
+
await validateGitFormat(repository, paths);
|
|
200
263
|
return { commit, tree, paths };
|
|
201
264
|
}
|
|
202
265
|
/** Read only the requested private Git paths from one immutable Git tree. */
|
|
203
|
-
export function readGitPaths(repository, requestedPaths) {
|
|
266
|
+
export async function readGitPaths(repository, requestedPaths) {
|
|
204
267
|
for (const path of requestedPaths)
|
|
205
268
|
validPath(path);
|
|
206
|
-
const commit = readRef(repository, GIT_REF);
|
|
269
|
+
const commit = await readRef(repository, GIT_REF);
|
|
207
270
|
if (commit === null)
|
|
208
271
|
return { commit: null, tree: null, paths: new Map() };
|
|
209
|
-
const tree = readTreeForCommit(repository, commit);
|
|
210
|
-
const paths = parseTreeEntries(runGit(repository, [
|
|
272
|
+
const tree = await readTreeForCommit(repository, commit);
|
|
273
|
+
const paths = parseTreeEntries(await runGit(repository, [
|
|
211
274
|
"ls-tree",
|
|
212
275
|
"-z",
|
|
213
276
|
"--full-tree",
|
|
@@ -216,16 +279,16 @@ export function readGitPaths(repository, requestedPaths) {
|
|
|
216
279
|
GIT_FORMAT_PATH,
|
|
217
280
|
...requestedPaths,
|
|
218
281
|
]));
|
|
219
|
-
validateGitFormat(repository, paths);
|
|
282
|
+
await validateGitFormat(repository, paths);
|
|
220
283
|
return { commit, tree, paths };
|
|
221
284
|
}
|
|
222
285
|
/** Add paths from the same immutable Git tree without rereading its ref. */
|
|
223
|
-
export function extendGitPaths(repository, snapshot, requestedPaths) {
|
|
286
|
+
export async function extendGitPaths(repository, snapshot, requestedPaths) {
|
|
224
287
|
for (const path of requestedPaths)
|
|
225
288
|
validPath(path);
|
|
226
289
|
if (snapshot.tree === null || requestedPaths.length === 0)
|
|
227
290
|
return snapshot;
|
|
228
|
-
const additions = parseTreeEntries(runGit(repository, [
|
|
291
|
+
const additions = parseTreeEntries(await runGit(repository, [
|
|
229
292
|
"ls-tree",
|
|
230
293
|
"-z",
|
|
231
294
|
"--full-tree",
|
|
@@ -236,19 +299,19 @@ export function extendGitPaths(repository, snapshot, requestedPaths) {
|
|
|
236
299
|
return { ...snapshot, paths: new Map([...snapshot.paths, ...additions]) };
|
|
237
300
|
}
|
|
238
301
|
/** Expand a targeted immutable snapshot to its complete already-pinned tree. */
|
|
239
|
-
export function expandGitSnapshot(repository, snapshot) {
|
|
302
|
+
export async function expandGitSnapshot(repository, snapshot) {
|
|
240
303
|
if (snapshot.tree === null)
|
|
241
304
|
return snapshot;
|
|
242
|
-
return { ...snapshot, paths: readTreeEntries(repository, snapshot.tree) };
|
|
305
|
+
return { ...snapshot, paths: await readTreeEntries(repository, snapshot.tree) };
|
|
243
306
|
}
|
|
244
|
-
export function writeBlob(repository, bytes) {
|
|
245
|
-
const oid = runGit(repository, ["hash-object", "-w", "--stdin"], bytes).toString("utf8").trim();
|
|
307
|
+
export async function writeBlob(repository, bytes) {
|
|
308
|
+
const oid = (await runGit(repository, ["hash-object", "-w", "--stdin"], bytes)).toString("utf8").trim();
|
|
246
309
|
assertOid(oid, "written blob");
|
|
247
310
|
return oid;
|
|
248
311
|
}
|
|
249
|
-
export function readBlob(repository, oid) {
|
|
312
|
+
export async function readBlob(repository, oid) {
|
|
250
313
|
assertOid(oid, "blob");
|
|
251
|
-
return runGit(repository, ["cat-file", "blob", oid]);
|
|
314
|
+
return await runGit(repository, ["cat-file", "blob", oid]);
|
|
252
315
|
}
|
|
253
316
|
function malformedBatchOutput(detail) {
|
|
254
317
|
throw new GitPlumbingError({
|
|
@@ -257,13 +320,13 @@ function malformedBatchOutput(detail) {
|
|
|
257
320
|
message: `malformed cat-file --batch output: ${detail}`,
|
|
258
321
|
});
|
|
259
322
|
}
|
|
260
|
-
function readObjects(repository, oids) {
|
|
323
|
+
async function readObjects(repository, oids) {
|
|
261
324
|
const unique = [...new Set(oids)];
|
|
262
325
|
for (const oid of unique)
|
|
263
326
|
assertOid(oid, "Git object");
|
|
264
327
|
if (unique.length === 0)
|
|
265
328
|
return new Map();
|
|
266
|
-
const output = runGit(repository, ["cat-file", "--batch"], `${unique.join("\n")}\n`);
|
|
329
|
+
const output = await runGit(repository, ["cat-file", "--batch"], `${unique.join("\n")}\n`);
|
|
267
330
|
const objects = new Map();
|
|
268
331
|
let offset = 0;
|
|
269
332
|
for (const oid of unique) {
|
|
@@ -299,32 +362,22 @@ function readObjects(repository, oids) {
|
|
|
299
362
|
malformedBatchOutput("trailing bytes");
|
|
300
363
|
return objects;
|
|
301
364
|
}
|
|
302
|
-
|
|
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
|
-
function validateGitFormat(repository, paths) {
|
|
365
|
+
async function validateGitFormat(repository, paths) {
|
|
313
366
|
const format = paths.get(GIT_FORMAT_PATH);
|
|
314
367
|
if (format === undefined)
|
|
315
368
|
throw new AuthorityCorruptionError(`Git is missing ${GIT_FORMAT_PATH}`);
|
|
316
369
|
if (format.type !== "blob")
|
|
317
370
|
throw new AuthorityCorruptionError(`Git format is not a blob: ${GIT_FORMAT_PATH}`);
|
|
318
|
-
const bytes = readBlob(repository, format.oid);
|
|
371
|
+
const bytes = await readBlob(repository, format.oid);
|
|
319
372
|
if (bytes.toString("utf8") !== GIT_FORMAT_BYTES) {
|
|
320
373
|
throw new AuthorityCorruptionError(`Git format is not current: ${GIT_FORMAT_PATH}`);
|
|
321
374
|
}
|
|
322
375
|
}
|
|
323
|
-
function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
|
|
376
|
+
async function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
|
|
324
377
|
const byPath = new Map();
|
|
325
378
|
if (baseTree === null)
|
|
326
379
|
return byPath;
|
|
327
|
-
const ancestry = parseTreeEntries(runGit(repository, [
|
|
380
|
+
const ancestry = parseTreeEntries(await runGit(repository, [
|
|
328
381
|
"ls-tree",
|
|
329
382
|
"-z",
|
|
330
383
|
"-t",
|
|
@@ -345,7 +398,7 @@ function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
|
|
|
345
398
|
throw new Error(`Git path is both file and directory: ${path}`);
|
|
346
399
|
treeByPath.set(path, entry.oid);
|
|
347
400
|
}
|
|
348
|
-
const objects = readObjects(repository, [...treeByPath.values()]);
|
|
401
|
+
const objects = await readObjects(repository, [...treeByPath.values()]);
|
|
349
402
|
const oidBytes = baseTree.length / 2;
|
|
350
403
|
for (const [path, oid] of treeByPath) {
|
|
351
404
|
const object = objects.get(oid);
|
|
@@ -356,8 +409,8 @@ function baseTreeEntries(repository, baseTree, changedPaths, nodePaths) {
|
|
|
356
409
|
}
|
|
357
410
|
return byPath;
|
|
358
411
|
}
|
|
359
|
-
function writePreparedTrees(repository, prepared) {
|
|
360
|
-
const output = runGit(repository, ["mktree", "--batch"], prepared.map(({ records }) => `${records}\n`).join(""));
|
|
412
|
+
async function writePreparedTrees(repository, prepared) {
|
|
413
|
+
const output = await runGit(repository, ["mktree", "--batch"], prepared.map(({ records }) => `${records}\n`).join(""));
|
|
361
414
|
const written = output.toString("ascii").trimEnd().split("\n");
|
|
362
415
|
if (written.length !== prepared.length) {
|
|
363
416
|
throw new GitPlumbingError({ stderr: output, status: null, message: "malformed mktree --batch output" });
|
|
@@ -372,17 +425,31 @@ function writePreparedTrees(repository, prepared) {
|
|
|
372
425
|
}
|
|
373
426
|
}
|
|
374
427
|
/** Copy only the changed paths' tree ancestors, preserving untouched subtrees by object ID. */
|
|
375
|
-
export function updateGitTree(repository, baseTree, changes) {
|
|
428
|
+
export async function updateGitTree(repository, baseTree, changes) {
|
|
376
429
|
const update = treeUpdate(changes);
|
|
377
|
-
const bases = baseTreeEntries(repository, baseTree, [...changes.keys()], update.nodePaths);
|
|
378
|
-
const representativeOid = baseTree ?? changes.values().
|
|
430
|
+
const bases = await baseTreeEntries(repository, baseTree, [...changes.keys()], update.nodePaths);
|
|
431
|
+
const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
|
|
379
432
|
if (representativeOid === undefined)
|
|
380
433
|
throw new Error("cannot write an empty Git tree without an object format");
|
|
381
434
|
const prepared = prepareTreeUpdate({ update, bases, oidBytes: representativeOid.length / 2 });
|
|
382
|
-
writePreparedTrees(repository, prepared.trees);
|
|
435
|
+
await writePreparedTrees(repository, prepared.trees);
|
|
436
|
+
return prepared.root;
|
|
437
|
+
}
|
|
438
|
+
/** Write a tree update from directory entries frozen with the admitted base tree. */
|
|
439
|
+
export async function updateGitTreeFromFrozenDirectories(repository, baseTree, directories, changes) {
|
|
440
|
+
const update = treeUpdate(changes);
|
|
441
|
+
const representativeOid = baseTree ?? [...changes.values()].find((change) => change !== null)?.oid;
|
|
442
|
+
if (representativeOid === undefined)
|
|
443
|
+
throw new Error("cannot write an empty Git tree without an object format");
|
|
444
|
+
const prepared = prepareTreeUpdate({
|
|
445
|
+
update,
|
|
446
|
+
bases: directories,
|
|
447
|
+
oidBytes: representativeOid.length / 2,
|
|
448
|
+
});
|
|
449
|
+
await writePreparedTrees(repository, prepared.trees);
|
|
383
450
|
return prepared.root;
|
|
384
451
|
}
|
|
385
|
-
function writeCommitObject(input) {
|
|
452
|
+
async function writeCommitObject(input) {
|
|
386
453
|
const { repository, tree, parents, message, actor, email, at } = input;
|
|
387
454
|
assertOid(tree, "commit tree");
|
|
388
455
|
for (const parent of parents)
|
|
@@ -390,20 +457,20 @@ function writeCommitObject(input) {
|
|
|
390
457
|
const args = ["commit-tree", tree];
|
|
391
458
|
for (const parent of parents)
|
|
392
459
|
args.push("-p", parent);
|
|
393
|
-
const commit = runGitWithEnvironment(repository, args, `${message}\n`, {
|
|
460
|
+
const commit = (await runGitWithEnvironment(repository, args, `${message}\n`, {
|
|
394
461
|
GIT_AUTHOR_NAME: actor,
|
|
395
462
|
GIT_AUTHOR_EMAIL: email,
|
|
396
463
|
GIT_COMMITTER_NAME: actor,
|
|
397
464
|
GIT_COMMITTER_EMAIL: email,
|
|
398
465
|
...(at === undefined ? {} : { GIT_AUTHOR_DATE: at, GIT_COMMITTER_DATE: at }),
|
|
399
|
-
})
|
|
466
|
+
}))
|
|
400
467
|
.toString("utf8")
|
|
401
468
|
.trim();
|
|
402
469
|
assertOid(commit, "written Git commit");
|
|
403
470
|
return commit;
|
|
404
471
|
}
|
|
405
|
-
export function writeCommit(input) {
|
|
406
|
-
return writeCommitObject({
|
|
472
|
+
export async function writeCommit(input) {
|
|
473
|
+
return await writeCommitObject({
|
|
407
474
|
repository: input.repository,
|
|
408
475
|
tree: input.tree,
|
|
409
476
|
parents: input.parent === null ? [] : [input.parent],
|
|
@@ -413,7 +480,23 @@ export function writeCommit(input) {
|
|
|
413
480
|
...(input.at === undefined ? {} : { at: input.at }),
|
|
414
481
|
});
|
|
415
482
|
}
|
|
416
|
-
|
|
483
|
+
function refAssertionLine(assertion) {
|
|
484
|
+
assertAssertionRef(assertion.ref);
|
|
485
|
+
assertOid(assertion.oid, `expected oid for ${assertion.ref}`);
|
|
486
|
+
return `${assertion.ref === "HEAD" ? "" : "option no-deref\n"}verify ${assertion.ref} ${assertion.oid}`;
|
|
487
|
+
}
|
|
488
|
+
function refUpdateLine(update, index) {
|
|
489
|
+
assertRef(update.ref);
|
|
490
|
+
if (index > 0 && update.ref === GIT_REF)
|
|
491
|
+
throw new Error(`duplicate ref update: ${update.ref}`);
|
|
492
|
+
if (index > 0 && update.expectedOid === null)
|
|
493
|
+
throw new Error("target ref updates require non-null OIDs");
|
|
494
|
+
assertOid(update.newOid, `new oid for ${update.ref}`);
|
|
495
|
+
if (update.expectedOid !== null)
|
|
496
|
+
assertOid(update.expectedOid, `expected oid for ${update.ref}`);
|
|
497
|
+
return `option no-deref\nupdate ${update.ref} ${update.newOid} ${update.expectedOid ?? "0".repeat(update.newOid.length)}`;
|
|
498
|
+
}
|
|
499
|
+
export async function updateRefsAtomically(repository, updates, assertions = []) {
|
|
417
500
|
if (updates.length === 0)
|
|
418
501
|
throw new Error("an atomic ref transaction needs a Git update");
|
|
419
502
|
if (updates.length > 2)
|
|
@@ -422,24 +505,16 @@ export function updateRefsAtomically(repository, updates) {
|
|
|
422
505
|
if (git === undefined || git.ref !== GIT_REF) {
|
|
423
506
|
throw new Error(`the first atomic update must be the Git ref: ${GIT_REF}`);
|
|
424
507
|
}
|
|
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", "");
|
|
508
|
+
const lines = [
|
|
509
|
+
"start",
|
|
510
|
+
...assertions.map(refAssertionLine),
|
|
511
|
+
...updates.map(refUpdateLine),
|
|
512
|
+
"prepare",
|
|
513
|
+
"commit",
|
|
514
|
+
"",
|
|
515
|
+
];
|
|
441
516
|
try {
|
|
442
|
-
runGit(repository, ["update-ref", "--stdin"
|
|
517
|
+
await runGit(repository, ["update-ref", "--stdin"], lines.join("\n"));
|
|
443
518
|
return { kind: "published" };
|
|
444
519
|
}
|
|
445
520
|
catch (error) {
|
|
@@ -454,3 +529,34 @@ export function updateRefsAtomically(repository, updates) {
|
|
|
454
529
|
throw error;
|
|
455
530
|
}
|
|
456
531
|
}
|
|
532
|
+
export function decodeGitNumstat(output) {
|
|
533
|
+
const fields = output.toString("utf8").split("\0");
|
|
534
|
+
if (fields.at(-1) !== "")
|
|
535
|
+
throw new Error("Git numstat output is not NUL terminated");
|
|
536
|
+
let filesChanged = 0;
|
|
537
|
+
let insertions = 0;
|
|
538
|
+
let deletions = 0;
|
|
539
|
+
for (let index = 0; index < fields.length - 1; index += 1) {
|
|
540
|
+
const field = fields[index];
|
|
541
|
+
const [added, deleted, path, extra] = field.split("\t");
|
|
542
|
+
if (added === undefined || deleted === undefined || path === undefined || extra !== undefined) {
|
|
543
|
+
throw new Error("Git numstat output is malformed");
|
|
544
|
+
}
|
|
545
|
+
if (path.length === 0) {
|
|
546
|
+
if (fields[index + 1] === undefined || fields[index + 2] === undefined) {
|
|
547
|
+
throw new Error("Git rename numstat output is missing paths");
|
|
548
|
+
}
|
|
549
|
+
index += 2;
|
|
550
|
+
}
|
|
551
|
+
filesChanged += 1;
|
|
552
|
+
insertions += added === "-" ? 0 : Number.parseInt(added, 10);
|
|
553
|
+
deletions += deleted === "-" ? 0 : Number.parseInt(deleted, 10);
|
|
554
|
+
}
|
|
555
|
+
return { filesChanged, insertions, deletions };
|
|
556
|
+
}
|
|
557
|
+
export function decodeGitNameOnly(output) {
|
|
558
|
+
const names = output.toString("utf8").split("\0");
|
|
559
|
+
if (names.at(-1) !== "")
|
|
560
|
+
throw new Error("Git name-only output is not NUL terminated");
|
|
561
|
+
return names.slice(0, -1);
|
|
562
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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: () => Promise<WorktreeLeak | null>;
|
|
6
|
+
}>;
|
|
7
|
+
export type CollectableScratchWorktree = Readonly<{
|
|
8
|
+
path: string;
|
|
9
|
+
release(): void;
|
|
10
|
+
}>;
|
|
11
|
+
export type WorktreeLeak = Readonly<{
|
|
12
|
+
path: string;
|
|
13
|
+
diagnostic: string;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function collectableScratchWorktrees(paths: Iterable<string>): Promise<readonly CollectableScratchWorktree[]>;
|
|
16
|
+
/** Materialize a disposable candidate worktree and own only its removal. */
|
|
17
|
+
export declare function materializeScratchCandidate(repository: GitRepository, candidate: SnapshotId): Promise<MaterializedScratchCandidate>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { tryAcquireSqliteTransactionLock, } from "../coordination/sqlite-transaction-lock.js";
|
|
6
|
+
import { gitObjectIdForSnapshot } from "./identity.js";
|
|
7
|
+
import { runGit } from "./repository.js";
|
|
8
|
+
const SCRATCH_PREFIX = "keiyaku-v4-verify-";
|
|
9
|
+
const SCRATCH_ROOT = await realpath(tmpdir());
|
|
10
|
+
const SCRATCH_PATTERN = /^keiyaku-v4-verify-([0-9a-f]{24})$/;
|
|
11
|
+
function scratchPath() {
|
|
12
|
+
return join(SCRATCH_ROOT, `${SCRATCH_PREFIX}${randomBytes(12).toString("hex")}`);
|
|
13
|
+
}
|
|
14
|
+
function ownershipLockPath(path) {
|
|
15
|
+
if (dirname(path) !== SCRATCH_ROOT)
|
|
16
|
+
return null;
|
|
17
|
+
const match = SCRATCH_PATTERN.exec(basename(path));
|
|
18
|
+
return match === null ? null : join(SCRATCH_ROOT, `.${SCRATCH_PREFIX}${match[1]}.owner.sqlite`);
|
|
19
|
+
}
|
|
20
|
+
export async function collectableScratchWorktrees(paths) {
|
|
21
|
+
const collectable = [];
|
|
22
|
+
for (const path of paths) {
|
|
23
|
+
const lockPath = ownershipLockPath(path);
|
|
24
|
+
if (lockPath === null)
|
|
25
|
+
continue;
|
|
26
|
+
const lock = await tryAcquireSqliteTransactionLock({ path: lockPath, mode: "exclusive" });
|
|
27
|
+
if (lock !== null)
|
|
28
|
+
collectable.push({ path, release: () => lock.close() });
|
|
29
|
+
}
|
|
30
|
+
return collectable;
|
|
31
|
+
}
|
|
32
|
+
/** Materialize a disposable candidate worktree and own only its removal. */
|
|
33
|
+
export async function materializeScratchCandidate(repository, candidate) {
|
|
34
|
+
const cwd = scratchPath();
|
|
35
|
+
const lockPath = ownershipLockPath(cwd);
|
|
36
|
+
if (lockPath === null)
|
|
37
|
+
throw new Error("scratch ownership lock path is invalid");
|
|
38
|
+
const ownership = await tryAcquireSqliteTransactionLock({
|
|
39
|
+
path: lockPath,
|
|
40
|
+
mode: "exclusive",
|
|
41
|
+
});
|
|
42
|
+
if (ownership === null)
|
|
43
|
+
throw new Error("new scratch ownership lock is unexpectedly held");
|
|
44
|
+
try {
|
|
45
|
+
await runGit(repository, ["worktree", "add", "--detach", cwd, gitObjectIdForSnapshot(candidate)]);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
ownership.close();
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
let disposed = false;
|
|
52
|
+
return {
|
|
53
|
+
cwd,
|
|
54
|
+
dispose: async () => {
|
|
55
|
+
if (disposed)
|
|
56
|
+
return null;
|
|
57
|
+
disposed = true;
|
|
58
|
+
try {
|
|
59
|
+
await runGit(repository, ["worktree", "remove", "--force", cwd]);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
path: cwd,
|
|
65
|
+
diagnostic: error instanceof Error ? error.message : String(error),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
ownership.close();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|