@zq-silk/yui 2.3.0 → 2.4.0
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 +15 -1
- package/dist/artifacts/artifactCapability.js +39 -10
- package/dist/artifacts/imageMetadata.js +120 -0
- package/dist/artifacts/managedGit.js +7 -4
- package/dist/artifacts/taskArtifactRepository.js +6 -0
- package/dist/cli/commandCatalog.js +17 -8
- package/dist/cli/invocationAuthority.js +1 -1
- package/dist/commands/executionAuditCommands.js +5 -0
- package/dist/commands/globalRoleCommands.js +41 -3
- package/dist/commands/projectCommands.js +89 -94
- package/dist/commands/roleSkillValidation.js +4 -8
- package/dist/commands/taskCommands.js +25 -2
- package/dist/commands/taskUsageCommand.js +38 -0
- package/dist/context/roleSessionContext.js +18 -2
- package/dist/context/runContextPack.js +5 -1
- package/dist/context/sessionBootstrapManifest.js +10 -1
- package/dist/context/skillCatalog.js +108 -0
- package/dist/context/skillPackage.js +161 -0
- package/dist/context/taskContext.js +3 -2
- package/dist/context/taskSearch.js +127 -0
- package/dist/controlPlaneCli.js +25 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
- package/dist/controller/jobControl.js +2 -0
- package/dist/controller/nativeHostInput.js +37 -0
- package/dist/controller/runtime.js +93 -2
- package/dist/controller/runtimeEventInbox.js +2 -2
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/doctor/doctor.js +10 -0
- package/dist/execution/reviewMainRun.js +1 -0
- package/dist/execution/workItemMainRun.js +1 -0
- package/dist/executor/codexConfigConflict.js +6 -4
- package/dist/executor/effectiveLaunch.js +24 -1
- package/dist/executor/fileRoleLaunchPlanner.js +13 -5
- package/dist/integration/gitIntegrationService.js +7 -12
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/job/httpPreview.js +61 -0
- package/dist/job/jobRunner.js +12 -2
- package/dist/kernel/builtinCapabilities.js +3 -1
- package/dist/message/message.js +15 -1
- package/dist/output/rolePresentation.js +4 -1
- package/dist/repository/executionLaneGitSnapshot.js +2 -1
- package/dist/repository/gitCompatibility.js +62 -0
- package/dist/repository/gitWorkspace.js +48 -71
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +2 -1
- package/dist/runtime/agentEndpoint.js +8 -0
- package/dist/runtime/agentHost.js +170 -3
- package/dist/runtime/codexAppServerRuntime.js +1 -1
- package/dist/runtime/codexNativeAccess.js +175 -0
- package/dist/runtime/controlledCodexTui.js +116 -0
- package/dist/runtime/publicReply.js +23 -0
- package/dist/runtime/runtimeObservation.js +48 -0
- package/dist/runtime/structuredProviderHost.js +16 -7
- package/dist/runtime/taskCostMetrics.js +133 -0
- package/dist/runtime/taskUsageMetrics.js +11 -2
- package/dist/runtime/taskUsageQuery.js +38 -0
- package/dist/scheduler/taskExecutionProjection.js +2 -0
- package/dist/scheduler/taskObservabilityProjection.js +9 -1
- package/dist/storage/homeLayout.js +13 -0
- package/dist/storage/sqliteSchema.js +44 -0
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskSearch.js +35 -0
- package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
- package/dist/task/ordinaryArchive.js +42 -0
- package/dist/tmux/tmuxManager.js +8 -8
- package/dist/web/assets/assetManifest.js +11 -51
- package/dist/web/assets/client/app/catalog.js +215 -0
- package/dist/web/assets/client/app/main.js +148 -0
- package/dist/web/assets/client/app/selection.js +139 -0
- package/dist/web/assets/client/app/settings.js +326 -0
- package/dist/web/assets/client/app/taskView.js +280 -0
- package/dist/web/assets/client/domain/context.js +96 -0
- package/dist/web/assets/client/domain/records.js +126 -0
- package/dist/web/assets/client/domain/runs.js +119 -0
- package/dist/web/assets/client/domain/taskForms.js +197 -0
- package/dist/web/assets/client/domain/vocab.js +69 -0
- package/dist/web/assets/client/domain/work.js +116 -0
- package/dist/web/assets/client/i18n/en.js +900 -0
- package/dist/web/assets/client/i18n/zh-CN.js +898 -0
- package/dist/web/assets/client/index.js +114 -0
- package/dist/web/assets/client/layout/resizer.js +37 -0
- package/dist/web/assets/client/layout/router.js +35 -0
- package/dist/web/assets/client/layout/shortcuts.js +45 -0
- package/dist/web/assets/client/layout/sizing.js +95 -0
- package/dist/web/assets/client/layout/workspace.js +247 -0
- package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
- package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
- package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
- package/dist/web/assets/client/lib/i18n.js +54 -0
- package/dist/web/assets/client/lib/markdown.js +167 -0
- package/dist/web/assets/client/lib/prefs.js +28 -0
- package/dist/web/assets/client/lib/theme.js +68 -0
- package/dist/web/assets/client/ui/containers.js +101 -0
- package/dist/web/assets/client/ui/controls.js +51 -0
- package/dist/web/assets/client/ui/forms.js +48 -0
- package/dist/web/assets/client/ui/metrics.js +137 -0
- package/dist/web/assets/client/ui/primitives.js +92 -0
- package/dist/web/assets/client/ui/text.js +33 -0
- package/dist/web/assets/client/ui/toast.js +12 -0
- package/dist/web/assets/client/views/bodySearch.js +172 -0
- package/dist/web/assets/client/views/createTask.js +85 -0
- package/dist/web/assets/client/views/dock/conversation.js +612 -0
- package/dist/web/assets/client/views/dock/discussion.js +173 -0
- package/dist/web/assets/client/views/dock/preview.js +129 -0
- package/dist/web/assets/client/views/dock/terminal.js +199 -0
- package/dist/web/assets/client/views/globalInput.js +67 -0
- package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
- package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
- package/dist/web/assets/client/views/skills.js +152 -0
- package/dist/web/assets/client/views/task/actions.js +36 -0
- package/dist/web/assets/client/views/task/delivery.js +185 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
- package/dist/web/assets/client/views/task/files.js +272 -0
- package/dist/web/assets/client/views/task/observation.js +153 -0
- package/dist/web/assets/client/views/task/overview.js +161 -0
- package/dist/web/assets/client/views/task/page.js +131 -0
- package/dist/web/assets/client/views/task/records.js +163 -0
- package/dist/web/assets/client/views/task/runtime.js +130 -0
- package/dist/web/assets/client/views/task/timeline.js +190 -0
- package/dist/web/assets/shared/geometry.js +19 -0
- package/dist/web/assets/shared/taskTabs.js +8 -0
- package/dist/web/assets/shared/themes.js +93 -0
- package/dist/web/assets/shell/dialogs.js +63 -0
- package/dist/web/assets/shell/dock.js +40 -0
- package/dist/web/assets/shell/index.js +40 -0
- package/dist/web/assets/shell/settings.js +34 -0
- package/dist/web/assets/shell/sidebar.js +45 -0
- package/dist/web/assets/styles/index.js +41 -0
- package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
- package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
- package/dist/web/assets/styles/markdown.js +22 -3
- package/dist/web/assets/styles/tokens.js +10 -63
- package/dist/web/assets/styles/ui/controls.js +76 -0
- package/dist/web/assets/styles/ui/forms.js +46 -0
- package/dist/web/assets/styles/ui/surfaces.js +122 -0
- package/dist/web/assets/styles/views/delivery.js +29 -0
- package/dist/web/assets/styles/views/dock.js +71 -0
- package/dist/web/assets/styles/views/page.js +32 -0
- package/dist/web/assets/styles/views/records.js +70 -0
- package/dist/web/assets/styles/views/settings.js +26 -0
- package/dist/web/assets/styles/views/sidebar.js +74 -0
- package/dist/web/assets/styles/views/task.js +80 -0
- package/dist/web/assets/styles/views/terminal.js +38 -0
- package/dist/web/taskPreviews.js +205 -0
- package/dist/web/tmuxWebTerminal.js +16 -9
- package/dist/web/webConversation.js +373 -0
- package/dist/web/webMaterials.js +155 -0
- package/dist/web/webServer.js +295 -18
- package/dist/web/webSettings.js +331 -0
- package/dist/web/webSkills.js +86 -0
- package/dist/web/webSnapshot.js +40 -8
- package/dist/web/webTaskSurface.js +106 -8
- package/dist/workspace/gitChangeSetCapture.js +3 -1
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
- package/docs/release-workflow.md +28 -0
- package/docs/release-workflow.zh-CN.md +21 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/dist/web/assets/client/app.js +0 -808
- package/dist/web/assets/client/components.js +0 -477
- package/dist/web/assets/client/detail.js +0 -379
- package/dist/web/assets/client/dock.js +0 -309
- package/dist/web/assets/client/evidence.js +0 -364
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1277
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -198
- package/dist/web/assets/client/sections.js +0 -349
- package/dist/web/assets/client/theme.js +0 -71
- package/dist/web/assets/shell.js +0 -161
- package/dist/web/assets/styles/components.js +0 -216
- package/dist/web/assets/styles/views.js +0 -309
- /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
|
@@ -59,7 +59,8 @@ const definitions = [
|
|
|
59
59
|
{
|
|
60
60
|
name: "artifact.save", summary: "Save a file artifact by relativePath into the Task's local Git repository; returns a commit-pinned reference.",
|
|
61
61
|
effect: "local-mutation", requiredPermissions: ["task:read"], source: "artifacts.saveArtifactCapability",
|
|
62
|
-
inputSchema: object({ taskId: text, relativePath: text, content: { type: "string" },
|
|
62
|
+
inputSchema: object({ taskId: text, relativePath: text, content: { type: "string" },
|
|
63
|
+
encoding: { enum: ["utf8", "base64"] }, message: text, expectedHead: text }, ["taskId", "relativePath", "content"]),
|
|
63
64
|
outputSchema: { type: "object", required: ["taskId", "commit", "relativePath"] }
|
|
64
65
|
},
|
|
65
66
|
{
|
|
@@ -319,6 +320,7 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
319
320
|
if (name === "artifact.save")
|
|
320
321
|
return saveArtifactCapability(store.rootDirectory(), taskId, {
|
|
321
322
|
relativePath: params.relativePath, content: params.content,
|
|
323
|
+
...(params.encoding === undefined ? {} : { encoding: params.encoding }),
|
|
322
324
|
...(params.message === undefined ? {} : { message: params.message }),
|
|
323
325
|
...(params.expectedHead === undefined ? {} : { expectedHead: params.expectedHead })
|
|
324
326
|
});
|
package/dist/message/message.js
CHANGED
|
@@ -42,6 +42,9 @@ export function createTaskMessage(id, taskId, body, kind, author, now, context =
|
|
|
42
42
|
: { inputControl: {
|
|
43
43
|
action: context.inputControl.action,
|
|
44
44
|
requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
|
|
45
|
+
...(context.inputControl.expectedSessionId === undefined ? {} : {
|
|
46
|
+
expectedSessionId: requireText(context.inputControl.expectedSessionId, "Message Session")
|
|
47
|
+
}),
|
|
45
48
|
...(context.inputControl.expectedTarget === undefined
|
|
46
49
|
? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
|
|
47
50
|
} }),
|
|
@@ -236,6 +239,8 @@ export function validateTaskMessage(message) {
|
|
|
236
239
|
throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
|
|
237
240
|
}
|
|
238
241
|
requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
|
|
242
|
+
if (message.inputControl.expectedSessionId !== undefined)
|
|
243
|
+
requireText(message.inputControl.expectedSessionId, "Message Session");
|
|
239
244
|
if (message.inputControl.expectedTarget !== undefined) {
|
|
240
245
|
requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
|
|
241
246
|
if (message.inputControl.action !== "steer") {
|
|
@@ -317,7 +322,11 @@ export function validateTaskMessage(message) {
|
|
|
317
322
|
throw new Error("A recipient requires an exact WorkItem or ReviewRound.");
|
|
318
323
|
}
|
|
319
324
|
}
|
|
320
|
-
|
|
325
|
+
const selectedSessionNondelivery = message.inputControl?.action === "queue"
|
|
326
|
+
&& message.inputControl.expectedSessionId !== undefined
|
|
327
|
+
&& message.continuation?.runId === undefined
|
|
328
|
+
&& message.continuation?.notDeliveredReason !== undefined;
|
|
329
|
+
if (message.continuation !== undefined && message.recipient?.ownerRunId === undefined && !selectedSessionNondelivery) {
|
|
321
330
|
throw new Error("Message continuation requires an owner Assignment.");
|
|
322
331
|
}
|
|
323
332
|
if (message.continuation?.runId !== undefined) {
|
|
@@ -373,6 +382,9 @@ export function createGlobalRoleMessage(id, roleName, body, kind, author, now, c
|
|
|
373
382
|
: { inputControl: {
|
|
374
383
|
action: context.inputControl.action,
|
|
375
384
|
requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
|
|
385
|
+
...(context.inputControl.expectedSessionId === undefined ? {} : {
|
|
386
|
+
expectedSessionId: requireText(context.inputControl.expectedSessionId, "Message Session")
|
|
387
|
+
}),
|
|
376
388
|
...(context.inputControl.expectedTarget === undefined
|
|
377
389
|
? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
|
|
378
390
|
} }),
|
|
@@ -404,6 +416,8 @@ export function validateGlobalRoleMessage(message) {
|
|
|
404
416
|
throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
|
|
405
417
|
}
|
|
406
418
|
requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
|
|
419
|
+
if (message.inputControl.expectedSessionId !== undefined)
|
|
420
|
+
requireText(message.inputControl.expectedSessionId, "Message Session");
|
|
407
421
|
if (message.inputControl.expectedTarget !== undefined) {
|
|
408
422
|
requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
|
|
409
423
|
if (message.inputControl.action !== "steer") {
|
|
@@ -22,7 +22,10 @@ export function renderRoleLaunchComparison(role, effective) {
|
|
|
22
22
|
permission(binding),
|
|
23
23
|
current.defaultAccess
|
|
24
24
|
];
|
|
25
|
-
}), defaultTableWidth())
|
|
25
|
+
}), defaultTableWidth()) + "\n\nSkill package evidence:\n" + (effective.skillPackages === undefined
|
|
26
|
+
? " Legacy / externally recorded: complete package versions were not recorded."
|
|
27
|
+
: effective.skillPackages.map(skill => ` ${skill.id} @ ${skill.digest}\n ${skill.source.kind}: ${skill.source.path}\n`
|
|
28
|
+
+ ` ${skill.fileCount} files; ${skill.byteSize} bytes; inventory: ${skill.manifestPath}`).join("\n"));
|
|
26
29
|
}
|
|
27
30
|
export function activeRoleSummary(role) {
|
|
28
31
|
const binding = role.agentBindings[role.activeAgentId];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { supportedGit } from "./gitCompatibility.js";
|
|
3
4
|
/**
|
|
4
5
|
* Freeze the exact committed heads of a durable managed Lane workspace at the
|
|
5
6
|
* synchronous runtime-terminalization boundary, before opening the result
|
|
@@ -67,7 +68,7 @@ export function snapshotExecutionLaneWorkspaceSync(store, workspace) {
|
|
|
67
68
|
}
|
|
68
69
|
function git(cwd, args) {
|
|
69
70
|
try {
|
|
70
|
-
return execFileSync(
|
|
71
|
+
return execFileSync(supportedGit().path, args, {
|
|
71
72
|
cwd,
|
|
72
73
|
encoding: "utf8",
|
|
73
74
|
timeout: 5_000,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { resolveExecutable } from "../external/pinnedCommandRunner.js";
|
|
4
|
+
// The fetch isolation boundary needs --no-write-fetch-head. Older Git cannot
|
|
5
|
+
// implement it by saving/restoring FETCH_HEAD without racing another writer.
|
|
6
|
+
export const MINIMUM_GIT_VERSION = "2.29.0";
|
|
7
|
+
export function gitVersionSupport(output) {
|
|
8
|
+
const match = /^git version (\d+)\.(\d+)(?:\.(\d+))?(?:[.\s-].*)?$/u.exec(output.trim());
|
|
9
|
+
const version = match === null ? "unknown" : `${match[1]}.${match[2]}.${match[3] ?? "0"}`;
|
|
10
|
+
const supported = match !== null && (Number(match[1]) > 2
|
|
11
|
+
|| (Number(match[1]) === 2 && Number(match[2]) >= 29));
|
|
12
|
+
return {
|
|
13
|
+
version, supported,
|
|
14
|
+
detail: `Git ${version}; requires >= ${MINIMUM_GIT_VERSION} (isolated fetch without writing FETCH_HEAD). `
|
|
15
|
+
+ "Read-only version check; repository permissions, transport and server capabilities are not tested."
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
let cached;
|
|
19
|
+
/** Pin the executable checked, reusing only an unchanged on-disk identity. */
|
|
20
|
+
export function supportedGit() {
|
|
21
|
+
const selected = resolveExecutable("git", process.env.PATH);
|
|
22
|
+
if (selected === undefined)
|
|
23
|
+
throw new Error("Git executable not found in PATH; install a supported Git before retrying. No Git operation was started.");
|
|
24
|
+
const path = realpathSync(selected);
|
|
25
|
+
const stat = statSync(path);
|
|
26
|
+
const identity = `${path}:${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
|
27
|
+
if (cached?.identity === identity)
|
|
28
|
+
return cached;
|
|
29
|
+
const output = execFileSync(path, ["--version"], {
|
|
30
|
+
encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"]
|
|
31
|
+
});
|
|
32
|
+
const support = gitVersionSupport(output);
|
|
33
|
+
if (!support.supported) {
|
|
34
|
+
throw new Error(`${path}: ${support.detail} No Git operation was started by this call. `
|
|
35
|
+
+ "Select a supported Git in PATH, run doctor, then retry; existing resources are retained.");
|
|
36
|
+
}
|
|
37
|
+
cached = { identity, path, version: support.version };
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
40
|
+
/** Git paths are relative to the actual -C directory, not the Node process. */
|
|
41
|
+
export function gitOutputLine(output) {
|
|
42
|
+
if (!output.endsWith("\n") || output.length === 1 || output.includes("\0")) {
|
|
43
|
+
throw new Error("Git returned an invalid path or line.");
|
|
44
|
+
}
|
|
45
|
+
// Remove exactly Git's record terminator, never spaces/tabs in a path.
|
|
46
|
+
return output.slice(0, -1);
|
|
47
|
+
}
|
|
48
|
+
/** rev-parse --verify accepts exactly one validated operand on old Git.
|
|
49
|
+
* Keep option-looking data out before removing the newer option terminator.
|
|
50
|
+
*/
|
|
51
|
+
export function compatibleGitArguments(args) {
|
|
52
|
+
const marker = args.indexOf("--end-of-options");
|
|
53
|
+
if (marker < 0)
|
|
54
|
+
return [...args];
|
|
55
|
+
const operand = args[marker + 1];
|
|
56
|
+
if (!args.includes("rev-parse") || !args.includes("--verify")
|
|
57
|
+
|| marker !== args.length - 2 || operand === undefined
|
|
58
|
+
|| operand.length === 0 || operand.startsWith("-") || /[\0\r\n]/u.test(operand)) {
|
|
59
|
+
throw new Error("Unsafe Git revision operand; no Git operation was started.");
|
|
60
|
+
}
|
|
61
|
+
return [...args.slice(0, marker), operand];
|
|
62
|
+
}
|
|
@@ -5,7 +5,13 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { cleanupFailure } from "../workspace/cleanupInspection.js";
|
|
7
7
|
import { externalProgramConfigViolations } from "../artifacts/managedGit.js";
|
|
8
|
+
import { compatibleGitArguments, gitOutputLine, supportedGit } from "./gitCompatibility.js";
|
|
9
|
+
import { gitPath, readGitWorktrees } from "./gitWorktreeInventory.js";
|
|
8
10
|
const executeFile = promisify(execFile);
|
|
11
|
+
const isolatedFetchOptions = [
|
|
12
|
+
"--no-tags", "--no-prune", "--no-prune-tags", "--no-recurse-submodules",
|
|
13
|
+
"--no-auto-gc", "--no-write-commit-graph", "--no-write-fetch-head", "--refmap="
|
|
14
|
+
];
|
|
9
15
|
/** Observed partial effects, not a rollback or permission to replay a refresh. */
|
|
10
16
|
export class GitWorkspaceRefreshError extends Error {
|
|
11
17
|
result;
|
|
@@ -160,13 +166,13 @@ export class NodeGitWorkspace {
|
|
|
160
166
|
const remote = safeRemote(input.remoteUrl);
|
|
161
167
|
const configuredBranch = await safeFetchBranch(input.branch);
|
|
162
168
|
const branch = configuredBranch === "HEAD"
|
|
163
|
-
? await resolveRemoteHeadBranch(remote)
|
|
169
|
+
? await resolveRemoteHeadBranch(remote, repositoryPath)
|
|
164
170
|
: configuredBranch;
|
|
165
171
|
const fetchedRef = remoteBaselineRef(remote, branch);
|
|
166
172
|
try {
|
|
167
173
|
await git([
|
|
168
174
|
"-C", repositoryPath,
|
|
169
|
-
"fetch",
|
|
175
|
+
"fetch", ...isolatedFetchOptions,
|
|
170
176
|
remote,
|
|
171
177
|
`refs/heads/${branch}:${fetchedRef}`
|
|
172
178
|
]);
|
|
@@ -212,8 +218,7 @@ export class NodeGitWorkspace {
|
|
|
212
218
|
// An empty refmap disables opportunistic tracking updates even when the
|
|
213
219
|
// Project URL is a named remote. Ignore configured prune/submodule effects.
|
|
214
220
|
await git([
|
|
215
|
-
"-C", initial.root, "fetch",
|
|
216
|
-
"--no-recurse-submodules", "--no-auto-maintenance", "--no-write-fetch-head", "--refmap=",
|
|
221
|
+
"-C", initial.root, "fetch", ...isolatedFetchOptions,
|
|
217
222
|
remote, `refs/heads/${stableBranch}:${temporaryRef}`
|
|
218
223
|
]);
|
|
219
224
|
fetchedCommit = await resolveFetchedCommit(initial.root, temporaryRef);
|
|
@@ -239,11 +244,11 @@ export class NodeGitWorkspace {
|
|
|
239
244
|
// Verify the stable branch and CAS the one configured destination in
|
|
240
245
|
// the same Git transaction. HEAD/index/worktree still use normal ff.
|
|
241
246
|
await git(["-C", initial.root, "update-ref", "--stdin"], { input: [
|
|
242
|
-
"
|
|
247
|
+
"option no-deref",
|
|
243
248
|
`verify refs/heads/${stableBranch} ${fetchedCommit}`,
|
|
244
249
|
"option no-deref",
|
|
245
250
|
`update ${mapping.ref} ${fetchedCommit} ${old}`,
|
|
246
|
-
"
|
|
251
|
+
""
|
|
247
252
|
].join("\n") });
|
|
248
253
|
tracking = {
|
|
249
254
|
status: previous === fetchedCommit ? "current" : "updated",
|
|
@@ -305,15 +310,15 @@ export class NodeGitWorkspace {
|
|
|
305
310
|
async resolveRemoteBaseline(input) {
|
|
306
311
|
const remote = safeRemote(input.remoteUrl);
|
|
307
312
|
const configuredRef = await safeFetchBranch(input.developmentRef);
|
|
313
|
+
const repository = await this.inspect(input.repositoryPath, "HEAD");
|
|
308
314
|
const branch = configuredRef === "HEAD"
|
|
309
|
-
? await resolveRemoteHeadBranch(remote)
|
|
315
|
+
? await resolveRemoteHeadBranch(remote, repository.root)
|
|
310
316
|
: configuredRef;
|
|
311
|
-
const repository = await this.inspect(input.repositoryPath, "HEAD");
|
|
312
317
|
const temporaryRef = remoteBaselineRef(remote, branch);
|
|
313
318
|
try {
|
|
314
319
|
await git([
|
|
315
320
|
"-C", repository.root,
|
|
316
|
-
"fetch",
|
|
321
|
+
"fetch", ...isolatedFetchOptions,
|
|
317
322
|
remote,
|
|
318
323
|
`refs/heads/${branch}:${temporaryRef}`
|
|
319
324
|
]);
|
|
@@ -321,7 +326,7 @@ export class NodeGitWorkspace {
|
|
|
321
326
|
"-C", repository.root,
|
|
322
327
|
"rev-parse", "--verify", "--end-of-options", `${temporaryRef}^{commit}`
|
|
323
328
|
]);
|
|
324
|
-
const advertisedCommit = await resolveRemoteBranchCommit(remote, branch);
|
|
329
|
+
const advertisedCommit = await resolveRemoteBranchCommit(remote, branch, repository.root);
|
|
325
330
|
if (fetchedCommit.toLowerCase() !== advertisedCommit) {
|
|
326
331
|
throw new Error(`Project remote development branch changed while it was fetched: ${branch}.`);
|
|
327
332
|
}
|
|
@@ -341,8 +346,10 @@ export class NodeGitWorkspace {
|
|
|
341
346
|
}
|
|
342
347
|
}
|
|
343
348
|
async clone(input) {
|
|
349
|
+
supportedGit();
|
|
344
350
|
const remote = safeRemote(input.remoteUrl);
|
|
345
|
-
|
|
351
|
+
requireText(input.destination, "Project destination");
|
|
352
|
+
const destination = resolve(input.destination);
|
|
346
353
|
if (await pathKind(destination) !== undefined) {
|
|
347
354
|
throw new Error(`Project destination already exists: ${destination}.`);
|
|
348
355
|
}
|
|
@@ -415,10 +422,8 @@ export class NodeGitWorkspace {
|
|
|
415
422
|
async inspect(repositoryPath, baseRef = "HEAD") {
|
|
416
423
|
const requested = await canonicalDirectory(repositoryPath, "Project");
|
|
417
424
|
const ref = safeRef(baseRef);
|
|
418
|
-
const root = await canonicalDirectory(await
|
|
419
|
-
const gitDirectory = await canonicalDirectory(await
|
|
420
|
-
"-C", root, "rev-parse", "--path-format=absolute", "--git-common-dir"
|
|
421
|
-
]), "Git common directory");
|
|
425
|
+
const root = await canonicalDirectory(await gitPath(requested, ["--show-toplevel"]), "Project root");
|
|
426
|
+
const gitDirectory = await canonicalDirectory(await gitPath(root, ["--git-common-dir"]), "Git common directory");
|
|
422
427
|
const baseCommit = await gitLine([
|
|
423
428
|
"-C", root, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`
|
|
424
429
|
]);
|
|
@@ -485,7 +490,7 @@ export class NodeGitWorkspace {
|
|
|
485
490
|
const importedRef = `${importRoot}/${index}`;
|
|
486
491
|
await git([
|
|
487
492
|
"-C", destination.root,
|
|
488
|
-
"fetch",
|
|
493
|
+
"fetch", ...isolatedFetchOptions, "--",
|
|
489
494
|
source.root, `${snapshot.ref}:${importedRef}`
|
|
490
495
|
]);
|
|
491
496
|
const imported = await resolveRefCommit(destination.root, importedRef);
|
|
@@ -567,36 +572,11 @@ export class NodeGitWorkspace {
|
|
|
567
572
|
const excluded = input.excludeWorktreePath === undefined
|
|
568
573
|
? undefined
|
|
569
574
|
: await realpath(resolve(input.excludeWorktreePath)).catch(() => undefined);
|
|
570
|
-
const
|
|
571
|
-
const records = [];
|
|
572
|
-
let current = { prunable: false };
|
|
573
|
-
for (const line of porcelain.split("\n")) {
|
|
574
|
-
if (line.length === 0) {
|
|
575
|
-
const path = current.path;
|
|
576
|
-
if (path !== undefined) {
|
|
577
|
-
records.push({ path, branch: current.branch, prunable: current.prunable });
|
|
578
|
-
}
|
|
579
|
-
current = { prunable: false };
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
if (line.startsWith("worktree ")) {
|
|
583
|
-
current.path = line.slice("worktree ".length);
|
|
584
|
-
}
|
|
585
|
-
else if (line.startsWith("branch ")) {
|
|
586
|
-
current.branch = line.slice("branch ".length);
|
|
587
|
-
}
|
|
588
|
-
else if (line.startsWith("prunable")) {
|
|
589
|
-
current.prunable = true;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
const lastPath = current.path;
|
|
593
|
-
if (lastPath !== undefined) {
|
|
594
|
-
records.push({ path: lastPath, branch: current.branch, prunable: current.prunable });
|
|
595
|
-
}
|
|
575
|
+
const records = await readGitWorktrees(root);
|
|
596
576
|
for (const record of records) {
|
|
597
577
|
// A dead registration (its directory is gone) occupies nothing; a
|
|
598
578
|
// worktree on another ref or a detached HEAD does not occupy this one.
|
|
599
|
-
if (record.prunable || record.branch !== wanted)
|
|
579
|
+
if ((record.prunable && !record.locked) || record.branch !== wanted)
|
|
600
580
|
continue;
|
|
601
581
|
// git reports canonical absolute paths; compare canonicals so a
|
|
602
582
|
// symlinked workspace root cannot make the recorded worktree look
|
|
@@ -724,11 +704,7 @@ export class NodeGitWorkspace {
|
|
|
724
704
|
}
|
|
725
705
|
async listWorktrees(repositoryPath) {
|
|
726
706
|
const root = await canonicalDirectory(repositoryPath, "Project");
|
|
727
|
-
|
|
728
|
-
return output
|
|
729
|
-
.split(/\r?\n/u)
|
|
730
|
-
.filter((line) => line.startsWith("worktree "))
|
|
731
|
-
.map((line) => line.slice("worktree ".length).trim());
|
|
707
|
+
return (await readGitWorktrees(root)).map(entry => entry.path);
|
|
732
708
|
}
|
|
733
709
|
async listCommitsBetween(input) {
|
|
734
710
|
const root = await canonicalDirectory(input.repositoryPath, "Project");
|
|
@@ -919,9 +895,8 @@ export class NodeGitWorkspace {
|
|
|
919
895
|
await assertExpectedBranch(expected, input.branch);
|
|
920
896
|
if (!await this.inspectClean(expected))
|
|
921
897
|
return "dirty";
|
|
922
|
-
const worktrees =
|
|
923
|
-
|
|
924
|
-
if (worktrees.length !== 1 || worktrees[0] !== `worktree ${expected}`) {
|
|
898
|
+
const worktrees = await readGitWorktrees(expected);
|
|
899
|
+
if (worktrees.length !== 1 || worktrees[0].path !== expected) {
|
|
925
900
|
cleanupFailure("git-worktree-registrations", "Task clone still owns Git worktree registrations; retained.", 1, worktrees.length);
|
|
926
901
|
}
|
|
927
902
|
return "clean";
|
|
@@ -1051,16 +1026,14 @@ async function removeMissingWorktreeRegistration(repositoryRoot, path, branch, a
|
|
|
1051
1026
|
await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
|
|
1052
1027
|
}
|
|
1053
1028
|
async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
if (fields === undefined)
|
|
1029
|
+
const entry = (await readGitWorktrees(repositoryRoot)).find(record => record.path === path);
|
|
1030
|
+
if (entry === undefined)
|
|
1057
1031
|
return false;
|
|
1058
|
-
const pinnedDetached = allowDetached &&
|
|
1032
|
+
const pinnedDetached = allowDetached && entry.detached
|
|
1059
1033
|
&& await gitSucceeds(["-C", repositoryRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`])
|
|
1060
|
-
&&
|
|
1061
|
-
if ((
|
|
1062
|
-
cleanupFailure("git-registration-mismatch", "Missing worktree has uncertain or locked Git ownership; metadata retained.", { branch, locked: false }, { branch:
|
|
1063
|
-
locked: fields.some(field => field.startsWith("locked")) });
|
|
1034
|
+
&& entry.head === await resolveRefCommit(repositoryRoot, `refs/heads/${branch}`);
|
|
1035
|
+
if ((entry.branch !== `refs/heads/${branch}` && !pinnedDetached) || entry.locked) {
|
|
1036
|
+
cleanupFailure("git-registration-mismatch", "Missing worktree has uncertain or locked Git ownership; metadata retained.", { branch, locked: false }, { branch: entry.branch ?? null, locked: entry.locked });
|
|
1064
1037
|
}
|
|
1065
1038
|
return true;
|
|
1066
1039
|
}
|
|
@@ -1103,8 +1076,7 @@ async function assertExpectedBranch(path, expected, allowRebase = false) {
|
|
|
1103
1076
|
// Rebase temporarily detaches HEAD. Git's per-worktree metadata must still
|
|
1104
1077
|
// identify this exact Integration branch; unrelated detachment is invalid.
|
|
1105
1078
|
for (const backend of ["rebase-merge", "rebase-apply"]) {
|
|
1106
|
-
const headNamePath = await
|
|
1107
|
-
"--git-path", `${backend}/head-name`]);
|
|
1079
|
+
const headNamePath = await gitPath(path, ["--git-path", `${backend}/head-name`]);
|
|
1108
1080
|
let headName;
|
|
1109
1081
|
try {
|
|
1110
1082
|
headName = (await readFile(headNamePath, "utf8")).trim();
|
|
@@ -1135,10 +1107,10 @@ async function assertOwnedWorktree(project, container, path) {
|
|
|
1135
1107
|
assertContained(container, canonicalPath);
|
|
1136
1108
|
if (canonicalPath !== path)
|
|
1137
1109
|
throw new Error("Managed worktree resolves through a symbolic link.");
|
|
1138
|
-
const root = await canonicalDirectory(await
|
|
1110
|
+
const root = await canonicalDirectory(await gitPath(path, ["--show-toplevel"]), "Managed worktree root");
|
|
1139
1111
|
if (root !== path)
|
|
1140
1112
|
throw new Error("Managed worktree root does not match its deterministic path.");
|
|
1141
|
-
const common = await canonicalDirectory(await
|
|
1113
|
+
const common = await canonicalDirectory(await gitPath(path, ["--git-common-dir"]), "Managed Git common directory");
|
|
1142
1114
|
if (common !== project.gitDirectory) {
|
|
1143
1115
|
cleanupFailure("workspace-identity-mismatch", "Managed worktree belongs to another project.", "Task main Git common directory", "different Git common directory");
|
|
1144
1116
|
}
|
|
@@ -1162,7 +1134,7 @@ async function inspectExactRecordedWorktree(input) {
|
|
|
1162
1134
|
if (path !== expectedPath) {
|
|
1163
1135
|
throw new Error("Recorded managed worktree resolves through a symbolic link.");
|
|
1164
1136
|
}
|
|
1165
|
-
const root = await canonicalDirectory(await
|
|
1137
|
+
const root = await canonicalDirectory(await gitPath(path, ["--show-toplevel"]), "Recorded managed worktree root");
|
|
1166
1138
|
if (root !== path) {
|
|
1167
1139
|
throw new Error("Recorded managed worktree root does not match its deterministic path.");
|
|
1168
1140
|
}
|
|
@@ -1170,7 +1142,7 @@ async function inspectExactRecordedWorktree(input) {
|
|
|
1170
1142
|
if (branch !== input.branch) {
|
|
1171
1143
|
throw new Error(`Recorded managed worktree is on an unexpected branch: ${branch}.`);
|
|
1172
1144
|
}
|
|
1173
|
-
const gitDirectory = await canonicalDirectory(await
|
|
1145
|
+
const gitDirectory = await canonicalDirectory(await gitPath(path, ["--git-common-dir"]), "Recorded managed Git common directory");
|
|
1174
1146
|
const destination = await new NodeGitWorkspace().inspect(input.repositoryPath);
|
|
1175
1147
|
const head = await resolveRefCommit(path, "HEAD");
|
|
1176
1148
|
const retained = await resolveRefCommit(destination.root, `refs/heads/${input.branch}`);
|
|
@@ -1267,8 +1239,8 @@ async function cleanupContainer(path) {
|
|
|
1267
1239
|
}
|
|
1268
1240
|
}
|
|
1269
1241
|
async function canonicalDirectory(path, label) {
|
|
1270
|
-
|
|
1271
|
-
const canonical = await realpath(isAbsolute(
|
|
1242
|
+
requireText(path, label);
|
|
1243
|
+
const canonical = await realpath(isAbsolute(path) ? path : resolve(path));
|
|
1272
1244
|
return canonical;
|
|
1273
1245
|
}
|
|
1274
1246
|
function managedPath(container, directory) {
|
|
@@ -1307,7 +1279,7 @@ async function readWorktreeStatus(path) {
|
|
|
1307
1279
|
return readStatusGit(path, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
1308
1280
|
}
|
|
1309
1281
|
async function readStatusGit(path, args) {
|
|
1310
|
-
return git(["--no-optional-locks", "-c", "core.fsmonitor=
|
|
1282
|
+
return git(["--no-optional-locks", "-c", "core.fsmonitor=",
|
|
1311
1283
|
"-c", "protocol.allow=never", "-c", "protocol.file.allow=never",
|
|
1312
1284
|
"-C", path, ...args], { environment: { GIT_NO_LAZY_FETCH: "1" } });
|
|
1313
1285
|
}
|
|
@@ -1353,8 +1325,10 @@ async function assertStatusProgramsAbsent(path) {
|
|
|
1353
1325
|
}
|
|
1354
1326
|
}
|
|
1355
1327
|
async function git(args, options = {}) {
|
|
1328
|
+
const program = supportedGit();
|
|
1329
|
+
const invocation = compatibleGitArguments(args);
|
|
1356
1330
|
try {
|
|
1357
|
-
const execution = executeFile(
|
|
1331
|
+
const execution = executeFile(program.path, invocation, {
|
|
1358
1332
|
encoding: "utf8",
|
|
1359
1333
|
maxBuffer: 1024 * 1024,
|
|
1360
1334
|
timeout: 30_000,
|
|
@@ -1375,6 +1349,7 @@ async function git(args, options = {}) {
|
|
|
1375
1349
|
}
|
|
1376
1350
|
}
|
|
1377
1351
|
async function gitSucceeds(args) {
|
|
1352
|
+
supportedGit();
|
|
1378
1353
|
try {
|
|
1379
1354
|
await git(args);
|
|
1380
1355
|
return true;
|
|
@@ -1384,7 +1359,7 @@ async function gitSucceeds(args) {
|
|
|
1384
1359
|
}
|
|
1385
1360
|
}
|
|
1386
1361
|
async function gitLine(args) {
|
|
1387
|
-
const lines = (await git(args)).
|
|
1362
|
+
const lines = gitOutputLine(await git(args)).split("\n");
|
|
1388
1363
|
if (lines.length !== 1 || lines[0]?.length === 0 || lines[0]?.includes("\0")) {
|
|
1389
1364
|
throw new Error("Git returned invalid output.");
|
|
1390
1365
|
}
|
|
@@ -1533,6 +1508,7 @@ function safeRef(value) {
|
|
|
1533
1508
|
return ref;
|
|
1534
1509
|
}
|
|
1535
1510
|
async function safeFetchBranch(value) {
|
|
1511
|
+
supportedGit();
|
|
1536
1512
|
const configuredRef = safeRef(value);
|
|
1537
1513
|
if (configuredRef === "HEAD")
|
|
1538
1514
|
return configuredRef;
|
|
@@ -1624,7 +1600,8 @@ function isCommit(value) {
|
|
|
1624
1600
|
return /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(value);
|
|
1625
1601
|
}
|
|
1626
1602
|
function safeRemote(value) {
|
|
1627
|
-
|
|
1603
|
+
requireText(value, "Git remote URL");
|
|
1604
|
+
const remote = value;
|
|
1628
1605
|
if (remote.startsWith("-") || /[\r\n]/.test(remote)) {
|
|
1629
1606
|
throw new Error("Git remote URL is invalid.");
|
|
1630
1607
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { gitOutputLine, supportedGit } from "./gitCompatibility.js";
|
|
6
|
+
const execute = promisify(execFile);
|
|
7
|
+
async function run(args) {
|
|
8
|
+
return (await execute(supportedGit().path, args, {
|
|
9
|
+
encoding: "utf8", timeout: 10_000, maxBuffer: 1024 * 1024
|
|
10
|
+
})).stdout;
|
|
11
|
+
}
|
|
12
|
+
export async function gitPath(cwd, args) {
|
|
13
|
+
return resolve(cwd, gitOutputLine(await run(["-C", cwd, "rev-parse", ...args])));
|
|
14
|
+
}
|
|
15
|
+
async function exists(path) {
|
|
16
|
+
try {
|
|
17
|
+
await lstat(path);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code === "ENOENT")
|
|
22
|
+
return false;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Old porcelain omits locks and does not delimit/quote arbitrary paths. Read
|
|
28
|
+
* Git's worktree administrative records instead. These are observations only:
|
|
29
|
+
* never write/prune them, and never infer ownership merely from a directory.
|
|
30
|
+
* Mutating consumers still check the recorded owner and ask Git to remove it.
|
|
31
|
+
*/
|
|
32
|
+
export async function readGitWorktrees(cwd) {
|
|
33
|
+
const common = await realpath(await gitPath(cwd, ["--git-common-dir"]));
|
|
34
|
+
const result = [];
|
|
35
|
+
// This is also Git's own main-worktree listing convention (including bare
|
|
36
|
+
// and separate-git-dir repositories), not a claim of managed ownership.
|
|
37
|
+
const main = basename(common) === ".git" ? dirname(common) : common;
|
|
38
|
+
result.push(await inspect(common, main, false));
|
|
39
|
+
const directory = join(common, "worktrees");
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code === "ENOENT")
|
|
46
|
+
return result;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry.isDirectory())
|
|
51
|
+
throw new Error(`Uncertain Git worktree administration: ${join(directory, entry.name)}; retained.`);
|
|
52
|
+
const admin = join(directory, entry.name);
|
|
53
|
+
const pointer = gitOutputLine(await readFile(join(admin, "gitdir"), "utf8"));
|
|
54
|
+
if (basename(pointer) !== ".git") {
|
|
55
|
+
throw new Error(`Invalid Git worktree back-pointer: ${admin}; retained.`);
|
|
56
|
+
}
|
|
57
|
+
// Relative back-pointers are based at the administrative directory,
|
|
58
|
+
// not the caller's cwd (Git's worktree.useRelativePaths setting).
|
|
59
|
+
const path = dirname(resolve(admin, pointer));
|
|
60
|
+
const adminCommon = await realpath(resolve(cwd, gitOutputLine(await run([
|
|
61
|
+
"-C", cwd, `--git-dir=${admin}`, "rev-parse", "--git-common-dir"
|
|
62
|
+
]))));
|
|
63
|
+
if (adminCommon !== common)
|
|
64
|
+
throw new Error(`Git worktree common directory mismatch: ${admin}; retained.`);
|
|
65
|
+
if (await exists(path)) {
|
|
66
|
+
const actual = await realpath(await gitPath(path, ["--git-dir"]));
|
|
67
|
+
if (actual !== admin)
|
|
68
|
+
throw new Error(`Git worktree back-pointer mismatch: ${path}; retained.`);
|
|
69
|
+
}
|
|
70
|
+
result.push(await inspect(admin, path, true));
|
|
71
|
+
}
|
|
72
|
+
if (new Set(result.map(entry => entry.path)).size !== result.length) {
|
|
73
|
+
throw new Error("Duplicate Git worktree registrations; retained.");
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
async function inspect(admin, path, linked) {
|
|
78
|
+
// Ask Git rather than parsing HEAD: Git owns ref storage (including newer
|
|
79
|
+
// backends), symbolic resolution and per-worktree pseudorefs.
|
|
80
|
+
let branch;
|
|
81
|
+
try {
|
|
82
|
+
branch = gitOutputLine(await run([`--git-dir=${admin}`, "symbolic-ref", "--quiet", "HEAD"]));
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.code !== 1)
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
if (branch !== undefined && !branch.startsWith("refs/")) {
|
|
89
|
+
throw new Error(`Invalid Git worktree HEAD: ${admin}; retained.`);
|
|
90
|
+
}
|
|
91
|
+
let head;
|
|
92
|
+
try {
|
|
93
|
+
head = gitOutputLine(await run([`--git-dir=${admin}`, "rev-parse", "--verify", "HEAD"]));
|
|
94
|
+
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(head))
|
|
95
|
+
throw new Error("Invalid Git HEAD");
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// A genuinely unborn branch (main or linked) is valid discovery, not cleanup proof.
|
|
99
|
+
if (branch === undefined)
|
|
100
|
+
throw error;
|
|
101
|
+
try {
|
|
102
|
+
await run([`--git-dir=${admin}`, "show-ref", "--verify", "--quiet", branch]);
|
|
103
|
+
}
|
|
104
|
+
catch (missing) {
|
|
105
|
+
if (missing.code === 1) {
|
|
106
|
+
return {
|
|
107
|
+
path, branch, detached: false, prunable: !await exists(path),
|
|
108
|
+
locked: linked && await exists(join(admin, "locked"))
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
throw missing;
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
path, ...(head === undefined ? {} : { head }),
|
|
117
|
+
...(branch === undefined ? {} : { branch }),
|
|
118
|
+
detached: branch === undefined, prunable: !await exists(path),
|
|
119
|
+
locked: linked && await exists(join(admin, "locked"))
|
|
120
|
+
};
|
|
121
|
+
}
|