@zq-silk/yui 0.15.11 → 0.16.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/ARCHITECTURE.md +11 -6
- package/ARCHITECTURE.zh-CN.md +8 -4
- package/README.md +13 -5
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/artifacts/artifactCommitLock.js +16 -55
- package/dist/artifacts/taskArtifactRepository.js +3 -3
- package/dist/cli/agentConfigurationPicker.js +1 -1
- package/dist/cli/commandCatalog.js +92 -48
- package/dist/cli/completion.js +18 -18
- package/dist/cli/completionWizard.js +1 -1
- package/dist/cli/dynamicCompletion.js +1 -1
- package/dist/cli/interactionPolicy.js +11 -3
- package/dist/cli/invocationAuthority.js +64 -0
- package/dist/cli/managedDiagnostics.js +2 -2
- package/dist/cli/parseRepeatable.js +35 -0
- package/dist/cli/updatePorts.js +26 -5
- package/dist/cli.js +1303 -1235
- package/dist/commands/agentCommands.js +4 -4
- package/dist/commands/capabilityCommands.js +1 -1
- package/dist/commands/configCommands.js +6 -57
- package/dist/commands/configOverview.js +2 -2
- package/dist/commands/durableJobCommands.js +12 -6
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +78 -29
- package/dist/commands/grantCommands.js +0 -3
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/operatorCommands.js +3 -3
- package/dist/commands/projectCommands.js +45 -25
- package/dist/commands/resourcesCommands.js +10 -40
- package/dist/commands/roleConfiguration.js +2 -6
- package/dist/commands/taskActivationCommands.js +4 -8
- package/dist/commands/taskAgentCapabilities.js +9 -0
- package/dist/commands/taskCommandSupport.js +155 -0
- package/dist/commands/taskCommandTypes.js +1 -0
- package/dist/commands/taskCommands.js +355 -732
- package/dist/commands/taskCompletionGate.js +1 -1
- package/dist/commands/taskExecutionCommands.js +1 -1
- package/dist/commands/taskFactCommands.js +325 -0
- package/dist/commands/taskInputCommands.js +12 -42
- package/dist/commands/taskIntegrationCommands.js +20 -58
- package/dist/commands/taskNextActionCommand.js +7 -6
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +12 -3
- package/dist/commands/taskPublicationVerifyCommand.js +24 -40
- package/dist/commands/taskRemoteDeliveryCommand.js +5 -86
- package/dist/commands/taskUpstreamCommands.js +19 -8
- package/dist/commands/workflowCommands.js +6 -1
- package/dist/completion/completionInstaller.js +26 -26
- package/dist/completion/completionState.js +26 -26
- package/dist/completion/fileCompletionManager.js +6 -11
- package/dist/config/configCatalog.js +0 -2
- package/dist/config/timeZone.js +14 -0
- package/dist/config/yuiConfig.js +0 -29
- package/dist/context/runContextPack.js +5 -3
- package/dist/context/taskCatalog.js +185 -0
- package/dist/context/taskContext.js +152 -151
- package/dist/context/wakeRunReferences.js +11 -0
- package/dist/controller/agentCapabilities.js +58 -0
- package/dist/controller/agentRuntimeObserver.js +6 -11
- package/dist/controller/clientRuntime.js +22 -14
- package/dist/controller/controller.js +20 -24
- package/dist/controller/fileSchedulerStoreAdapter.js +191 -225
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/jobClient.js +4 -4
- package/dist/controller/jobControl.js +60 -25
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +85 -43
- package/dist/controller/runtimeEventProcessor.js +0 -5
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/controller/taskAgentError.js +35 -0
- package/dist/coordination/workMailboxQueue.js +2 -24
- package/dist/core/controllerClient.js +2 -2
- package/dist/core/fileLockOwner.js +74 -0
- package/dist/decision/decision.js +17 -0
- package/dist/doctor/doctor.js +18 -9
- package/dist/event/taskEvent.js +12 -0
- package/dist/execution/workItemExecutionProjection.js +2 -2
- package/dist/executor/agentCapabilityConfig.js +43 -0
- package/dist/executor/agentConfigurationCatalog.js +49 -11
- package/dist/executor/agentExecutor.js +5 -8
- package/dist/executor/fileRoleLaunchPlanner.js +11 -53
- package/dist/executor/taskAgentCapabilities.js +64 -0
- package/dist/integration/deliveryObligation.js +1 -69
- package/dist/integration/gitIntegrationService.js +79 -90
- package/dist/integration/integrationAttempt.js +3 -14
- package/dist/integration/integrationSourceApplication.js +4 -12
- package/dist/integration/manifestTags.js +2 -2
- package/dist/job/durableJob.js +4 -25
- package/dist/job/jobAssignmentScope.js +22 -0
- package/dist/job/jobOperation.js +16 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/stepDirectory.js +15 -0
- package/dist/kernel/builtinCapabilities.js +15 -11
- package/dist/kernel/kernelPorts.js +1 -17
- package/dist/lifecycle/exactRunTerminalization.js +5 -10
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/message/inputControlResolution.js +3 -4
- package/dist/message/message.js +17 -20
- package/dist/message/messageContinuation.js +1 -1
- package/dist/milestone/milestone.js +11 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/observability/orchestrationMetrics.js +24 -1
- package/dist/observability/runtimeIdentity.js +1 -17
- package/dist/output/agentConfigurationPresentation.js +2 -0
- package/dist/output/timePresentation.js +1 -14
- package/dist/plugins/pluginService.js +12 -4
- package/dist/release/releaseWorkflowPorts.js +45 -96
- package/dist/release/runtimeRelease.js +9 -15
- package/dist/repository/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +98 -82
- package/dist/repository/taskBaseFreshness.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +82 -144
- package/dist/repository/taskWorkspacePreparer.js +113 -47
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/autoResourceGc.js +4 -77
- package/dist/resources/liveReferences.js +7 -54
- package/dist/resources/resourceDiscovery.js +5 -118
- package/dist/resources/resourceGc.js +233 -181
- package/dist/resources/resourceRegistrar.js +5 -4
- package/dist/resources/resourceRegistry.js +3 -37
- package/dist/resources/resourceRegistryStore.js +13 -36
- package/dist/resources/sqliteResourceRegistry.js +43 -26
- package/dist/review/deltaRecheck.js +1 -1
- package/dist/review/reviewAcceptance.js +1 -1
- package/dist/review/reviewDecision.js +1 -1
- package/dist/review/reviewRound.js +8 -7
- package/dist/review/taskFinalReviewContractResolution.js +1 -1
- package/dist/runtime/acpProtocol.js +4 -51
- package/dist/runtime/acpSession.js +10 -75
- package/dist/runtime/acpSessionConfiguration.js +1 -8
- package/dist/runtime/agentEndpoint.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentFailureContext.js +43 -0
- package/dist/runtime/agentHost.js +42 -25
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/codexInteractiveHost.js +2 -2
- package/dist/runtime/index.js +0 -1
- package/dist/runtime/managedCaller.js +21 -1
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerErrors.js +38 -0
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/runtimeObservation.js +0 -5
- package/dist/runtime/runtimeSessionCandidate.js +2 -3
- package/dist/runtime/sessionOwnerIdentity.js +1 -1
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +10 -43
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +6 -8
- package/dist/scheduler/activeRoleRunDelivery.js +19 -7
- package/dist/scheduler/leaderWakeupProcessor.js +24 -6
- package/dist/scheduler/operatorEvent.js +12 -20
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +5 -8
- package/dist/scheduler/roleRunLiveness.js +4 -4
- package/dist/scheduler/roleRunStall.js +16 -23
- package/dist/scheduler/taskExecutionProjection.js +49 -36
- package/dist/scheduler/taskObservabilityProjection.js +6 -45
- package/dist/scheduler/taskWake.js +5 -10
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/scheduler/wakeupQueue.js +2 -4
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/contextRecords.js +106 -0
- package/dist/storage/currentTaskStore.js +1 -1
- package/dist/storage/homeLayout.js +13 -17
- package/dist/storage/migrations/agentFailureContext.js +22 -0
- package/dist/storage/migrations/currentInputContract.js +86 -0
- package/dist/storage/migrations/currentRuntimeContract.js +228 -0
- package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
- package/dist/storage/migrations/integrationContinuation.js +5 -4
- package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
- package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
- package/dist/storage/migrations/verificationPlanV1.js +162 -0
- package/dist/storage/migrations/verificationPolicy.js +74 -0
- package/dist/storage/migrations/workItemHistory.js +46 -0
- package/dist/storage/persistenceWorker.js +12 -4
- package/dist/storage/recordValidation.js +87 -0
- package/dist/storage/sqliteSchema.js +173 -1
- package/dist/storage/sqliteStore.js +172 -161
- package/dist/storage/storageSchema.js +2 -14
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +16 -7
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +5 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/completionReadiness.js +5 -24
- package/dist/task/draftPlan.js +0 -53
- package/dist/task/nextAction.js +7 -13
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/task/remoteDeliveryService.js +89 -0
- package/dist/task/taskActivation.js +2 -25
- package/dist/task/taskActivationService.js +0 -2
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/task/taskSubmission.js +3 -14
- package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
- package/dist/telemetry/sqliteTelemetryStore.js +49 -49
- package/dist/telemetry/telemetryWiring.js +4 -3
- package/dist/tmux/tmuxManager.js +29 -20
- package/dist/verification/gateArtifact.js +15 -24
- package/dist/verification/gateArtifactStore.js +14 -7
- package/dist/verification/verificationGateService.js +29 -88
- package/dist/verification/verificationPlan.js +27 -72
- package/dist/web/assets/client/app.js +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +6 -5
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +12 -3
- package/dist/web/webSnapshot.js +14 -85
- package/dist/web/webTaskSurface.js +17 -46
- package/dist/workItem/workItem.js +1 -12
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +42 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +33 -3
- package/docs/managed-turn-and-session-runtime.md +121 -1
- package/docs/managed-turn-and-session-runtime.zh-CN.md +96 -2
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/plugin-sdk.md +4 -2
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/provider-runtime.md +3 -1
- package/docs/provider-runtime.zh-CN.md +4 -2
- package/docs/release-workflow.md +190 -7
- package/docs/release-workflow.zh-CN.md +139 -5
- package/docs/roles-and-configuration.md +29 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/sqlite-control-plane-design.md +16 -3
- package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
- package/docs/task-dag-semantics.md +3 -3
- package/docs/task-dag-semantics.zh-CN.md +2 -2
- package/docs/task-delivery.md +223 -17
- package/docs/task-delivery.zh-CN.md +171 -14
- package/docs/task-discovery.md +101 -0
- package/docs/task-discovery.zh-CN.md +85 -0
- package/docs/testing/verification-levels.md +161 -9
- package/docs/testing/verification-levels.zh-CN.md +116 -9
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -0
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-leader/references/integration.md +9 -5
- package/skills/yui-leader/references/planning.md +9 -3
- package/skills/yui-operator/SKILL.md +24 -8
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +71 -0
- package/dist/agent/agentRegistry.js +0 -10
- package/dist/agentRun/runIdentity.js +0 -22
- package/dist/commands/deliveryGuardPreflight.js +0 -30
- package/dist/commands/taskIntegrationQueueCommands.js +0 -226
- package/dist/commands/taskOverviewCommand.js +0 -376
- package/dist/completion/completionWizard.js +0 -125
- package/dist/context/dispatchContext.js +0 -110
- package/dist/context/wakeNotification.js +0 -144
- package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
- package/dist/integration/integrationQueueEntry.js +0 -191
- package/dist/integration/integrationQueueService.js +0 -810
- package/dist/release/fakeReleasePorts.js +0 -55
- package/dist/runtime/sessionOwnerRegistry.js +0 -137
- package/dist/task/deliveryGuard.js +0 -226
- /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
|
@@ -3,7 +3,18 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
3
3
|
import { lstat, mkdir, readFile, realpath, rm } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { cleanupFailure } from "../workspace/cleanupInspection.js";
|
|
7
|
+
import { externalProgramConfigViolations } from "../artifacts/managedGit.js";
|
|
6
8
|
const executeFile = promisify(execFile);
|
|
9
|
+
/** Observed partial effects, not a rollback or permission to replay a refresh. */
|
|
10
|
+
export class GitWorkspaceRefreshError extends Error {
|
|
11
|
+
result;
|
|
12
|
+
constructor(message, result, options) {
|
|
13
|
+
super(message, options);
|
|
14
|
+
this.result = result;
|
|
15
|
+
this.name = "GitWorkspaceRefreshError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
7
18
|
/** A remote baseline merge stopped for an explicit Leader resolution. */
|
|
8
19
|
export class RemoteBaselineConflictError extends Error {
|
|
9
20
|
affectedPaths;
|
|
@@ -86,6 +97,7 @@ export class NodeGitWorkspace {
|
|
|
86
97
|
return git([
|
|
87
98
|
"-C", input.repositoryPath,
|
|
88
99
|
"diff", "--no-color", "--no-ext-diff",
|
|
100
|
+
...(input.exact ? ["--no-textconv", "--binary", "--full-index", "--no-renames"] : []),
|
|
89
101
|
input.fromCommit, input.toCommit
|
|
90
102
|
]);
|
|
91
103
|
}
|
|
@@ -172,47 +184,117 @@ export class NodeGitWorkspace {
|
|
|
172
184
|
const initial = await this.inspect(input.repositoryPath, "HEAD");
|
|
173
185
|
await this.#assertRefreshCheckout(initial.root, configuredStableRef, initial.baseCommit);
|
|
174
186
|
const stableBranch = configuredStableRef === "HEAD"
|
|
175
|
-
? await resolveRemoteHeadBranch(remote)
|
|
187
|
+
? await resolveRemoteHeadBranch(remote, initial.root)
|
|
176
188
|
: configuredStableRef;
|
|
177
189
|
if (stableBranch !== configuredStableRef) {
|
|
178
190
|
await this.#assertRefreshCheckout(initial.root, stableBranch, initial.baseCommit);
|
|
179
191
|
}
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
192
|
+
const mapping = await resolveFetchTracking(initial.root, remote, stableBranch);
|
|
193
|
+
const previous = mapping.status === "managed"
|
|
194
|
+
? await readDirectTrackingRef(initial.root, mapping.ref)
|
|
195
|
+
: null;
|
|
196
|
+
let tracking = mapping.status === "managed"
|
|
197
|
+
? { status: "current", remoteName: mapping.remoteName, ref: mapping.ref,
|
|
198
|
+
fromCommit: previous, toCommit: previous }
|
|
199
|
+
: { status: "unmanaged", reason: mapping.reason };
|
|
200
|
+
const temporaryRef = `refs/yui/project-refresh/${randomBytes(16).toString("hex")}`;
|
|
201
|
+
let fetchedCommit;
|
|
202
|
+
let verifiedCommit;
|
|
203
|
+
let failure;
|
|
204
|
+
let cleanupFailed = false;
|
|
205
|
+
const assertMapping = async () => {
|
|
206
|
+
const current = await resolveFetchTracking(initial.root, remote, stableBranch);
|
|
207
|
+
if (JSON.stringify(current) !== JSON.stringify(mapping)) {
|
|
208
|
+
throw new Error("Project fetch URL or tracking mapping changed during refresh.");
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
// An empty refmap disables opportunistic tracking updates even when the
|
|
213
|
+
// Project URL is a named remote. Ignore configured prune/submodule effects.
|
|
214
|
+
await git([
|
|
215
|
+
"-C", initial.root, "fetch", "--no-tags", "--no-prune", "--no-prune-tags",
|
|
216
|
+
"--no-recurse-submodules", "--no-auto-maintenance", "--no-write-fetch-head", "--refmap=",
|
|
217
|
+
remote, `refs/heads/${stableBranch}:${temporaryRef}`
|
|
218
|
+
]);
|
|
219
|
+
fetchedCommit = await resolveFetchedCommit(initial.root, temporaryRef);
|
|
220
|
+
const advertisedCommit = await resolveRemoteBranchCommit(remote, stableBranch, initial.root);
|
|
221
|
+
if (fetchedCommit !== advertisedCommit) {
|
|
222
|
+
throw new Error(`Project remote stable branch changed while it was fetched: ${stableBranch}.`);
|
|
223
|
+
}
|
|
224
|
+
verifiedCommit = fetchedCommit;
|
|
225
|
+
await assertMapping();
|
|
226
|
+
await this.#assertRefreshCheckout(initial.root, stableBranch, initial.baseCommit);
|
|
227
|
+
if (fetchedCommit !== initial.baseCommit) {
|
|
228
|
+
if (!await this.isAncestor(initial.root, initial.baseCommit, fetchedCommit)) {
|
|
229
|
+
throw new Error(`Project checkout cannot be fast-forwarded from ${initial.baseCommit} to ${fetchedCommit}.`);
|
|
230
|
+
}
|
|
231
|
+
await git(["-C", initial.root, "merge", "--ff-only", "--no-edit", fetchedCommit]);
|
|
232
|
+
}
|
|
233
|
+
await this.#assertRefreshCheckout(initial.root, stableBranch, fetchedCommit);
|
|
234
|
+
await assertMapping();
|
|
235
|
+
if (mapping.status === "managed") {
|
|
236
|
+
// Do not follow a symbolic tracking ref into somebody else's branch.
|
|
237
|
+
await readDirectTrackingRef(initial.root, mapping.ref);
|
|
238
|
+
const old = previous ?? "0".repeat(fetchedCommit.length);
|
|
239
|
+
// Verify the stable branch and CAS the one configured destination in
|
|
240
|
+
// the same Git transaction. HEAD/index/worktree still use normal ff.
|
|
241
|
+
await git(["-C", initial.root, "update-ref", "--stdin"], { input: [
|
|
242
|
+
"start", "option no-deref",
|
|
243
|
+
`verify refs/heads/${stableBranch} ${fetchedCommit}`,
|
|
244
|
+
"option no-deref",
|
|
245
|
+
`update ${mapping.ref} ${fetchedCommit} ${old}`,
|
|
246
|
+
"prepare", "commit", ""
|
|
247
|
+
].join("\n") });
|
|
248
|
+
tracking = {
|
|
249
|
+
status: previous === fetchedCommit ? "current" : "updated",
|
|
250
|
+
remoteName: mapping.remoteName, ref: mapping.ref,
|
|
251
|
+
fromCommit: previous, toCommit: fetchedCommit
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
await this.#assertRefreshCheckout(initial.root, stableBranch, fetchedCommit);
|
|
255
|
+
await assertMapping();
|
|
256
|
+
if (mapping.status === "managed"
|
|
257
|
+
&& await readDirectTrackingRef(initial.root, mapping.ref) !== fetchedCommit) {
|
|
258
|
+
throw new Error(`Project tracking ref changed after refresh: ${mapping.ref}.`);
|
|
259
|
+
}
|
|
200
260
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
261
|
+
catch (error) {
|
|
262
|
+
failure = error;
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
if (fetchedCommit !== undefined) {
|
|
266
|
+
cleanupFailed = !await gitSucceeds([
|
|
267
|
+
"-C", initial.root, "update-ref", "--no-deref", "-d", temporaryRef, fetchedCommit
|
|
268
|
+
]);
|
|
269
|
+
if (cleanupFailed && failure === undefined) {
|
|
270
|
+
failure = new Error(`Project refresh temporary ref cleanup failed: ${temporaryRef}.`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (failure !== undefined) {
|
|
275
|
+
const head = await this.inspect(initial.root, "HEAD").then((value) => value.baseCommit, () => null);
|
|
276
|
+
const detail = failure instanceof Error ? failure.message : String(failure);
|
|
277
|
+
if (mapping.status === "managed") {
|
|
278
|
+
tracking = {
|
|
279
|
+
status: "failed", remoteName: mapping.remoteName, ref: mapping.ref,
|
|
280
|
+
fromCommit: previous,
|
|
281
|
+
toCommit: await readDirectTrackingRef(initial.root, mapping.ref).catch(() => null),
|
|
282
|
+
reason: detail
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
throw new GitWorkspaceRefreshError(`Project refresh failed: ${detail} HEAD ${initial.baseCommit} -> ${head ?? "unknown"}; ` +
|
|
286
|
+
(tracking.status === "unmanaged" ? `tracking unmanaged: ${tracking.reason}` :
|
|
287
|
+
`tracking ${tracking.ref}: ${tracking.fromCommit ?? "absent"} -> ${tracking.toCommit ?? "absent or unreadable"}`) +
|
|
288
|
+
(cleanupFailed ? `; temporary ref retained: ${temporaryRef}` : ""), { fromCommit: initial.baseCommit, toCommit: head,
|
|
289
|
+
changed: head === null ? null : head !== initial.baseCommit,
|
|
290
|
+
...(verifiedCommit === undefined ? {} : { verifiedCommit }), tracking,
|
|
291
|
+
...(cleanupFailed || fetchedCommit === undefined ? { temporaryRef } : {}) }, { cause: failure });
|
|
206
292
|
}
|
|
207
|
-
await git([
|
|
208
|
-
"-C", initial.root,
|
|
209
|
-
"merge", "--ff-only", "--no-edit", fetchedCommit
|
|
210
|
-
]);
|
|
211
|
-
await this.#assertRefreshCheckout(initial.root, stableBranch, fetchedCommit);
|
|
212
293
|
return {
|
|
213
294
|
fromCommit: initial.baseCommit,
|
|
214
|
-
toCommit:
|
|
215
|
-
changed:
|
|
295
|
+
toCommit: verifiedCommit,
|
|
296
|
+
changed: initial.baseCommit !== verifiedCommit,
|
|
297
|
+
tracking
|
|
216
298
|
};
|
|
217
299
|
}
|
|
218
300
|
/**
|
|
@@ -513,47 +595,17 @@ export class NodeGitWorkspace {
|
|
|
513
595
|
}
|
|
514
596
|
async inspectRemoteTracking(input) {
|
|
515
597
|
const root = (await this.inspect(input.repositoryPath)).root;
|
|
516
|
-
const wantedUrl = requireText(input.remoteUrl, "Project remote URL");
|
|
517
|
-
let output;
|
|
518
|
-
try {
|
|
519
|
-
output = await git([
|
|
520
|
-
"-C", root,
|
|
521
|
-
"config", "--get-regexp", "--null",
|
|
522
|
-
"^remote\\..*\\.url$"
|
|
523
|
-
]);
|
|
524
|
-
}
|
|
525
|
-
catch {
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
const records = output.split("\0").filter((record) => record.length > 0);
|
|
529
|
-
const matches = [];
|
|
530
|
-
for (const record of records) {
|
|
531
|
-
const separator = record.indexOf("\n");
|
|
532
|
-
if (separator < 0)
|
|
533
|
-
continue;
|
|
534
|
-
const key = record.slice(0, separator).trim();
|
|
535
|
-
const url = record.slice(separator + 1).trim();
|
|
536
|
-
const prefix = "remote.";
|
|
537
|
-
const suffix = ".url";
|
|
538
|
-
if (!key.startsWith(prefix) || !key.endsWith(suffix) || url !== wantedUrl)
|
|
539
|
-
continue;
|
|
540
|
-
matches.push({ remoteName: key.slice(prefix.length, -suffix.length), url });
|
|
541
|
-
}
|
|
542
|
-
if (matches.length !== 1)
|
|
543
|
-
return null;
|
|
544
|
-
const remoteName = requireText(matches[0].remoteName, "Git remote name");
|
|
545
598
|
const branch = await safeFetchBranch(input.branch);
|
|
546
|
-
const
|
|
547
|
-
if (
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (!await this.refExists(root, ref))
|
|
599
|
+
const mapping = await resolveFetchTracking(root, safeRemote(input.remoteUrl), branch);
|
|
600
|
+
if (mapping.status !== "managed")
|
|
601
|
+
return null;
|
|
602
|
+
if (!await this.refExists(root, mapping.ref))
|
|
551
603
|
return null;
|
|
552
604
|
return {
|
|
553
|
-
remoteName,
|
|
554
|
-
remoteUrl:
|
|
555
|
-
ref,
|
|
556
|
-
commit: (await this.inspect(root, ref)).baseCommit
|
|
605
|
+
remoteName: mapping.remoteName,
|
|
606
|
+
remoteUrl: mapping.remoteUrl,
|
|
607
|
+
ref: mapping.ref,
|
|
608
|
+
commit: (await this.inspect(root, mapping.ref)).baseCommit
|
|
557
609
|
};
|
|
558
610
|
}
|
|
559
611
|
async mergeBase(input) {
|
|
@@ -571,9 +623,13 @@ export class NodeGitWorkspace {
|
|
|
571
623
|
}
|
|
572
624
|
async isClean(repositoryPath) {
|
|
573
625
|
const root = (await this.inspect(repositoryPath)).root;
|
|
574
|
-
const status = await git([
|
|
575
|
-
"
|
|
576
|
-
|
|
626
|
+
const status = await git(["--no-optional-locks", "-C", root,
|
|
627
|
+
"status", "--porcelain=v1", "--untracked-files=all"]);
|
|
628
|
+
return status.length === 0;
|
|
629
|
+
}
|
|
630
|
+
async inspectClean(repositoryPath) {
|
|
631
|
+
const root = (await this.inspect(repositoryPath)).root;
|
|
632
|
+
const status = await readWorktreeStatus(root);
|
|
577
633
|
return status.length === 0;
|
|
578
634
|
}
|
|
579
635
|
async mergeWorktree(input) {
|
|
@@ -660,6 +716,11 @@ export class NodeGitWorkspace {
|
|
|
660
716
|
.filter((line) => line.length > 0);
|
|
661
717
|
}
|
|
662
718
|
async #assertRefreshCheckout(repositoryPath, stableRef, expectedCommit) {
|
|
719
|
+
// Status can take time. Re-read branch and HEAD after it so a checkout
|
|
720
|
+
// switched while status was running is not subsequently fast-forwarded.
|
|
721
|
+
if (!await this.isClean(repositoryPath)) {
|
|
722
|
+
throw new Error("Project checkout must be clean before it can be refreshed.");
|
|
723
|
+
}
|
|
663
724
|
if (stableRef !== "HEAD") {
|
|
664
725
|
const currentBranch = await this.headRef(repositoryPath);
|
|
665
726
|
if (currentBranch !== stableRef) {
|
|
@@ -671,9 +732,6 @@ export class NodeGitWorkspace {
|
|
|
671
732
|
if (head.baseCommit !== expectedCommit) {
|
|
672
733
|
throw new Error("Project checkout changed while it was being refreshed.");
|
|
673
734
|
}
|
|
674
|
-
if (!await this.isClean(head.root)) {
|
|
675
|
-
throw new Error("Project checkout must be clean before it can be refreshed.");
|
|
676
|
-
}
|
|
677
735
|
}
|
|
678
736
|
async ensureWorktree(input) {
|
|
679
737
|
return this.#ensureManagedWorktree({
|
|
@@ -786,34 +844,40 @@ export class NodeGitWorkspace {
|
|
|
786
844
|
return "removed";
|
|
787
845
|
}
|
|
788
846
|
async removeTaskClone(input) {
|
|
847
|
+
const state = await this.inspectTaskClone(input);
|
|
848
|
+
if (state !== "clean")
|
|
849
|
+
return state;
|
|
850
|
+
await rm(managedPath(resolve(input.container), input.directory), { recursive: true });
|
|
851
|
+
return "removed";
|
|
852
|
+
}
|
|
853
|
+
async inspectTaskClone(input) {
|
|
789
854
|
const identity = worktreeIdentity(input.taskSegment, "main");
|
|
790
855
|
const expected = managedPath(resolve(input.container), input.directory);
|
|
791
856
|
if (resolve(input.path) !== expected || input.branch !== identity.branch) {
|
|
792
|
-
|
|
857
|
+
cleanupFailure("workspace-identity-mismatch", "Task clone does not match its recorded managed identity.", { branch: identity.branch }, { branch: input.branch, pathMatches: resolve(input.path) === expected });
|
|
793
858
|
}
|
|
794
859
|
const kind = await pathKind(expected);
|
|
795
860
|
if (kind === undefined)
|
|
796
861
|
return "missing";
|
|
797
862
|
if (kind === "symlink")
|
|
798
|
-
|
|
863
|
+
cleanupFailure("workspace-identity-mismatch", "Task clone must not be a symbolic link.", "real directory", "symbolic link");
|
|
799
864
|
await canonicalContainer(input.container, false);
|
|
800
865
|
if (await realpath(expected) !== expected || await pathKind(join(expected, ".git")) !== "directory") {
|
|
801
|
-
|
|
866
|
+
cleanupFailure("workspace-identity-mismatch", "Task clone is not a standalone owned Git directory.", "standalone owned clone", "different Git layout or symbolic ancestor");
|
|
802
867
|
}
|
|
803
868
|
const repository = await this.inspect(expected);
|
|
804
869
|
if (repository.root !== expected || repository.gitDirectory !== join(expected, ".git")) {
|
|
805
|
-
|
|
870
|
+
cleanupFailure("workspace-identity-mismatch", "Task clone Git ownership cannot be established.", "exact root and Git directory", "different Git ownership");
|
|
806
871
|
}
|
|
807
872
|
await assertExpectedBranch(expected, input.branch);
|
|
808
|
-
if (!await this.
|
|
873
|
+
if (!await this.inspectClean(expected))
|
|
809
874
|
return "dirty";
|
|
810
875
|
const worktrees = (await git(["-C", expected, "worktree", "list", "--porcelain", "-z"]))
|
|
811
876
|
.split("\0").filter(line => line.startsWith("worktree "));
|
|
812
877
|
if (worktrees.length !== 1 || worktrees[0] !== `worktree ${expected}`) {
|
|
813
|
-
|
|
878
|
+
cleanupFailure("git-worktree-registrations", "Task clone still owns Git worktree registrations; retained.", 1, worktrees.length);
|
|
814
879
|
}
|
|
815
|
-
|
|
816
|
-
return "removed";
|
|
880
|
+
return "clean";
|
|
817
881
|
}
|
|
818
882
|
async inspectRecordedWorktree(input) {
|
|
819
883
|
const inspected = await inspectExactRecordedWorktree(input);
|
|
@@ -862,14 +926,19 @@ export class NodeGitWorkspace {
|
|
|
862
926
|
const container = resolve(input.container);
|
|
863
927
|
const path = managedPath(container, input.directory);
|
|
864
928
|
const kind = await pathKind(path);
|
|
865
|
-
|
|
929
|
+
const branch = input.expectedBranch ?? worktreeIdentity(input.taskSegment, input.roleName).branch;
|
|
930
|
+
if (kind === undefined) {
|
|
931
|
+
const project = await this.inspect(input.repositoryPath);
|
|
932
|
+
await inspectMissingWorktreeRegistration(project.root, path, branch);
|
|
866
933
|
return "missing";
|
|
934
|
+
}
|
|
867
935
|
if (kind === "symlink")
|
|
868
|
-
|
|
936
|
+
cleanupFailure("workspace-identity-mismatch", "Managed worktree path must not be a symbolic link.", "real directory", "symbolic link");
|
|
869
937
|
const canonicalContainerPath = await canonicalContainer(container, false);
|
|
870
938
|
const project = await this.inspect(input.repositoryPath);
|
|
871
939
|
await assertOwnedWorktree(project, canonicalContainerPath, path);
|
|
872
|
-
|
|
940
|
+
await assertExpectedBranch(path, branch);
|
|
941
|
+
const porcelain = await readWorktreeStatus(path);
|
|
873
942
|
return porcelain.length > 0 ? "dirty" : "clean";
|
|
874
943
|
}
|
|
875
944
|
async removeIntegrationWorktree(input) {
|
|
@@ -901,9 +970,7 @@ export class NodeGitWorkspace {
|
|
|
901
970
|
await git(["-C", repository.root, "worktree", "remove", "--force", "--", path]);
|
|
902
971
|
}
|
|
903
972
|
else {
|
|
904
|
-
const porcelain = await
|
|
905
|
-
"-C", path, "status", "--porcelain=v1", "--untracked-files=all"
|
|
906
|
-
]);
|
|
973
|
+
const porcelain = await readWorktreeStatus(path);
|
|
907
974
|
if (porcelain.length > 0)
|
|
908
975
|
return "dirty";
|
|
909
976
|
if (input.expectedBaseCommit !== undefined
|
|
@@ -930,16 +997,22 @@ export class NodeGitWorkspace {
|
|
|
930
997
|
* workspace's registration as a side effect of cleaning this exact owner.
|
|
931
998
|
*/
|
|
932
999
|
async function removeMissingWorktreeRegistration(repositoryRoot, path, branch) {
|
|
1000
|
+
if (!await inspectMissingWorktreeRegistration(repositoryRoot, path, branch))
|
|
1001
|
+
return;
|
|
1002
|
+
if (await pathKind(path) !== undefined)
|
|
1003
|
+
throw new Error(`Worktree reappeared before metadata cleanup: ${path}.`);
|
|
1004
|
+
await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
|
|
1005
|
+
}
|
|
1006
|
+
async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch) {
|
|
933
1007
|
const records = (await git(["-C", repositoryRoot, "worktree", "list", "--porcelain", "-z"])).split("\0\0");
|
|
934
1008
|
const fields = records.map(record => record.split("\0")).find(record => record.includes(`worktree ${path}`));
|
|
935
1009
|
if (fields === undefined)
|
|
936
|
-
return;
|
|
1010
|
+
return false;
|
|
937
1011
|
if (!fields.includes(`branch refs/heads/${branch}`) || fields.some(field => field.startsWith("locked"))) {
|
|
938
|
-
|
|
1012
|
+
cleanupFailure("git-registration-mismatch", "Missing worktree has uncertain or locked Git ownership; metadata retained.", { branch, locked: false }, { branch: fields.find(field => field.startsWith("branch "))?.slice(7) ?? null,
|
|
1013
|
+
locked: fields.some(field => field.startsWith("locked")) });
|
|
939
1014
|
}
|
|
940
|
-
|
|
941
|
-
throw new Error(`Worktree reappeared before metadata cleanup: ${path}.`);
|
|
942
|
-
await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
|
|
1015
|
+
return true;
|
|
943
1016
|
}
|
|
944
1017
|
async function deleteBranchIfPresent(repositoryRoot, branch) {
|
|
945
1018
|
const branchRef = `refs/heads/${branch}`;
|
|
@@ -999,7 +1072,7 @@ async function assertExpectedBranch(path, expected, allowRebase = false) {
|
|
|
999
1072
|
}
|
|
1000
1073
|
const branch = await gitLine(["-C", path, "symbolic-ref", "--short", "HEAD"]);
|
|
1001
1074
|
if (branch !== expected) {
|
|
1002
|
-
|
|
1075
|
+
cleanupFailure("workspace-identity-mismatch", "Managed worktree is on an unexpected branch.", expected, branch);
|
|
1003
1076
|
}
|
|
1004
1077
|
}
|
|
1005
1078
|
async function assertOwnedWorktree(project, container, path) {
|
|
@@ -1012,7 +1085,7 @@ async function assertOwnedWorktree(project, container, path) {
|
|
|
1012
1085
|
throw new Error("Managed worktree root does not match its deterministic path.");
|
|
1013
1086
|
const common = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"]), "Managed Git common directory");
|
|
1014
1087
|
if (common !== project.gitDirectory) {
|
|
1015
|
-
|
|
1088
|
+
cleanupFailure("workspace-identity-mismatch", "Managed worktree belongs to another project.", "Task main Git common directory", "different Git common directory");
|
|
1016
1089
|
}
|
|
1017
1090
|
}
|
|
1018
1091
|
async function inspectExactRecordedWorktree(input) {
|
|
@@ -1147,13 +1220,72 @@ async function pathKind(path) {
|
|
|
1147
1220
|
throw error;
|
|
1148
1221
|
}
|
|
1149
1222
|
}
|
|
1150
|
-
|
|
1223
|
+
/** Status can execute clean/process filters to compare equal-size changed
|
|
1224
|
+
* files. Preserve native normalization: report unknown instead of disabling
|
|
1225
|
+
* a filter and misclassifying its output as clean/dirty. Include scopes so a
|
|
1226
|
+
* native global/worktree include cannot hide the same execution requirement.
|
|
1227
|
+
*/
|
|
1228
|
+
async function readWorktreeStatus(path) {
|
|
1229
|
+
await assertStatusProgramsAbsent(path);
|
|
1230
|
+
return readStatusGit(path, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
1231
|
+
}
|
|
1232
|
+
async function readStatusGit(path, args) {
|
|
1233
|
+
return git(["--no-optional-locks", "-c", "core.fsmonitor=false",
|
|
1234
|
+
"-c", "protocol.allow=never", "-c", "protocol.file.allow=never",
|
|
1235
|
+
"-C", path, ...args], { environment: { GIT_NO_LAZY_FETCH: "1" } });
|
|
1236
|
+
}
|
|
1237
|
+
async function assertStatusProgramsAbsent(path) {
|
|
1238
|
+
const configuration = await readStatusGit(path, ["config", "--list", "--null", "--includes"]);
|
|
1239
|
+
const filters = externalProgramConfigViolations(configuration)
|
|
1240
|
+
.filter(key => key.startsWith("filter.") && (key.endsWith(".clean") || key.endsWith(".process")));
|
|
1241
|
+
const files = (await readStatusGit(path, ["ls-files", "--stage", "-z"])).split("\0");
|
|
1242
|
+
if (filters.length > 0) {
|
|
1243
|
+
// Merely having native LFS configured globally is not a violation. Only
|
|
1244
|
+
// filters selected by tracked regular files' effective attributes can run
|
|
1245
|
+
// during status. check-attr reads those selections without executing them.
|
|
1246
|
+
const configured = new Set(filters.map(key => key.slice(7, key.lastIndexOf("."))));
|
|
1247
|
+
const tracked = [...new Set(files.filter(line => line.startsWith("100"))
|
|
1248
|
+
.map(line => line.slice(line.indexOf("\t") + 1)))];
|
|
1249
|
+
for (let offset = 0; offset < tracked.length; offset += 128) {
|
|
1250
|
+
const attributes = (await readStatusGit(path, ["check-attr", "-z", "filter", "--",
|
|
1251
|
+
...tracked.slice(offset, offset + 128)])).split("\0");
|
|
1252
|
+
for (let index = 2; index < attributes.length; index += 3) {
|
|
1253
|
+
if (!configured.has(attributes[index].toLowerCase()))
|
|
1254
|
+
continue;
|
|
1255
|
+
cleanupFailure("git-status-requires-filter", "Git status may execute a selected filter program; read-only inspection will not run it.", "status without external filter execution", { trackedPath: attributes[index - 2], filter: attributes[index] }, "unknown");
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
// Status also examines initialized submodules. Inspect their configuration
|
|
1260
|
+
// before asking Git to recurse; uninitialized gitlinks execute nothing.
|
|
1261
|
+
for (const record of files.filter(line => line.startsWith("160000 "))) {
|
|
1262
|
+
const child = join(path, record.slice(record.indexOf("\t") + 1));
|
|
1263
|
+
assertContained(path, child);
|
|
1264
|
+
const gitEntry = await lstat(join(child, ".git")).catch(error => {
|
|
1265
|
+
if (isErrno(error, "ENOENT"))
|
|
1266
|
+
return undefined;
|
|
1267
|
+
throw error;
|
|
1268
|
+
});
|
|
1269
|
+
if (gitEntry === undefined)
|
|
1270
|
+
continue;
|
|
1271
|
+
if (await realpath(child) !== child
|
|
1272
|
+
|| (await readStatusGit(child, ["rev-parse", "--show-toplevel"])).trimEnd() !== child) {
|
|
1273
|
+
cleanupFailure("workspace-identity-mismatch", "Submodule identity cannot be safely inspected.", "owned submodule directory", "different or symbolic root");
|
|
1274
|
+
}
|
|
1275
|
+
await assertStatusProgramsAbsent(child);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
async function git(args, options = {}) {
|
|
1151
1279
|
try {
|
|
1152
|
-
const
|
|
1280
|
+
const execution = executeFile("git", [...args], {
|
|
1153
1281
|
encoding: "utf8",
|
|
1154
1282
|
maxBuffer: 1024 * 1024,
|
|
1155
|
-
timeout: 30_000
|
|
1283
|
+
timeout: 30_000,
|
|
1284
|
+
...(options.environment === undefined ? {} : { env: { ...process.env, ...options.environment } })
|
|
1156
1285
|
});
|
|
1286
|
+
if (options.input !== undefined)
|
|
1287
|
+
execution.child.stdin.end(options.input);
|
|
1288
|
+
const result = await execution;
|
|
1157
1289
|
return result.stdout;
|
|
1158
1290
|
}
|
|
1159
1291
|
catch (error) {
|
|
@@ -1181,6 +1313,134 @@ async function gitLine(args) {
|
|
|
1181
1313
|
}
|
|
1182
1314
|
return lines[0];
|
|
1183
1315
|
}
|
|
1316
|
+
/** Match one exact or one-star Git refspec, in either direction. */
|
|
1317
|
+
function refspecCapture(pattern, ref) {
|
|
1318
|
+
const star = pattern.indexOf("*");
|
|
1319
|
+
if (star < 0)
|
|
1320
|
+
return pattern === ref ? "" : null;
|
|
1321
|
+
const prefix = pattern.slice(0, star);
|
|
1322
|
+
const suffix = pattern.slice(star + 1);
|
|
1323
|
+
return ref.startsWith(prefix) && ref.endsWith(suffix)
|
|
1324
|
+
&& ref.length >= prefix.length + suffix.length
|
|
1325
|
+
? ref.slice(prefix.length, ref.length - suffix.length) : null;
|
|
1326
|
+
}
|
|
1327
|
+
async function parseFetchSpec(value) {
|
|
1328
|
+
const negative = value.startsWith("^");
|
|
1329
|
+
const text = value.replace(/^[+^]/u, "");
|
|
1330
|
+
const parts = text.split(":");
|
|
1331
|
+
const source = parts[0];
|
|
1332
|
+
const destination = parts[1] || undefined;
|
|
1333
|
+
if (parts.length > 2 || (negative && parts.length !== 1)
|
|
1334
|
+
|| !source.startsWith("refs/")
|
|
1335
|
+
|| !await gitSucceeds(["check-ref-format", "--refspec-pattern", source])
|
|
1336
|
+
|| (destination !== undefined && (!destination.startsWith("refs/")
|
|
1337
|
+
|| !await gitSucceeds(["check-ref-format", "--refspec-pattern", destination])
|
|
1338
|
+
|| source.includes("*") !== destination.includes("*"))))
|
|
1339
|
+
return null;
|
|
1340
|
+
return { source, ...(destination === undefined ? {} : { destination }), negative };
|
|
1341
|
+
}
|
|
1342
|
+
async function gitConfigValues(root, key) {
|
|
1343
|
+
try {
|
|
1344
|
+
return (await git(["-C", root, "config", "--null", "--get-all", key]))
|
|
1345
|
+
.split("\0").filter((value) => value.length > 0);
|
|
1346
|
+
}
|
|
1347
|
+
catch (error) {
|
|
1348
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
1349
|
+
if (typeof cause === "object" && cause !== null && "code" in cause && cause.code === 1) {
|
|
1350
|
+
return [];
|
|
1351
|
+
}
|
|
1352
|
+
throw error;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Prove a unique configured fetch destination, never infer it from a remote
|
|
1357
|
+
* name. Git resolves insteadOf aliases; push URLs are deliberately irrelevant.
|
|
1358
|
+
* Unsafe/ambiguous configurations remain usable for HEAD-only refresh.
|
|
1359
|
+
*/
|
|
1360
|
+
async function resolveFetchTracking(root, remote, branch) {
|
|
1361
|
+
const wantedUrl = await gitLine(["-C", root, "ls-remote", "--get-url", "--", remote]);
|
|
1362
|
+
const names = (await git(["-C", root, "remote"])).trim().split("\n").filter(Boolean).sort();
|
|
1363
|
+
const remotes = await Promise.all(names.map(async (name) => ({
|
|
1364
|
+
name,
|
|
1365
|
+
urls: (await git(["-C", root, "remote", "get-url", "--all", "--", name])).trimEnd().split("\n"),
|
|
1366
|
+
specs: await gitConfigValues(root, `remote.${name}.fetch`)
|
|
1367
|
+
})));
|
|
1368
|
+
const evidence = JSON.stringify({ wantedUrl, remotes });
|
|
1369
|
+
const unmanaged = (reason) => ({ status: "unmanaged", reason, evidence });
|
|
1370
|
+
const matching = remotes.filter(({ urls }) => urls[0] === wantedUrl);
|
|
1371
|
+
if (matching.length !== 1 || matching[0].urls.length !== 1) {
|
|
1372
|
+
return unmanaged("Project fetch URL does not identify exactly one single-URL Git remote.");
|
|
1373
|
+
}
|
|
1374
|
+
const selected = matching[0];
|
|
1375
|
+
const candidates = await Promise.all(remotes.map(async (entry) => ({
|
|
1376
|
+
...entry, parsed: await Promise.all(entry.specs.map(parseFetchSpec))
|
|
1377
|
+
})));
|
|
1378
|
+
if (candidates.some(({ parsed: specs }) => specs.some((spec) => spec === null))) {
|
|
1379
|
+
return unmanaged("A configured fetch refspec is outside the supported full-ref exact/one-star mapping.");
|
|
1380
|
+
}
|
|
1381
|
+
const parsed = candidates.map((entry) => ({
|
|
1382
|
+
...entry, parsed: entry.parsed.filter((spec) => spec !== null)
|
|
1383
|
+
}));
|
|
1384
|
+
const source = `refs/heads/${branch}`;
|
|
1385
|
+
const excluded = (specs, ref) => specs.some((spec) => spec.negative && refspecCapture(spec.source, ref) !== null);
|
|
1386
|
+
const selectedSpecs = parsed.find(({ name }) => name === selected.name).parsed;
|
|
1387
|
+
if (excluded(selectedSpecs, source))
|
|
1388
|
+
return unmanaged(`Fetch refspec excludes ${source}.`);
|
|
1389
|
+
const destinations = new Set();
|
|
1390
|
+
for (const spec of selectedSpecs) {
|
|
1391
|
+
if (spec.negative || spec.destination === undefined)
|
|
1392
|
+
continue;
|
|
1393
|
+
const capture = refspecCapture(spec.source, source);
|
|
1394
|
+
if (capture !== null)
|
|
1395
|
+
destinations.add(spec.destination.replace("*", capture));
|
|
1396
|
+
}
|
|
1397
|
+
if (destinations.size !== 1) {
|
|
1398
|
+
return unmanaged(`Fetch refspec does not map ${source} to exactly one destination.`);
|
|
1399
|
+
}
|
|
1400
|
+
const ref = [...destinations][0];
|
|
1401
|
+
if (!ref.startsWith("refs/remotes/") || !await gitSucceeds(["check-ref-format", ref])) {
|
|
1402
|
+
return unmanaged(`Fetch destination is not a supported remote-tracking ref: ${ref}.`);
|
|
1403
|
+
}
|
|
1404
|
+
for (const entry of parsed) {
|
|
1405
|
+
for (const spec of entry.parsed) {
|
|
1406
|
+
if (spec.negative || spec.destination === undefined)
|
|
1407
|
+
continue;
|
|
1408
|
+
const capture = refspecCapture(spec.destination, ref);
|
|
1409
|
+
if (capture === null)
|
|
1410
|
+
continue;
|
|
1411
|
+
const otherSource = spec.source.replace("*", capture);
|
|
1412
|
+
if (!excluded(entry.parsed, otherSource)
|
|
1413
|
+
&& (entry.name !== selected.name || otherSource !== source)) {
|
|
1414
|
+
return unmanaged(`Another fetch source also manages ${ref}.`);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
try {
|
|
1419
|
+
await readDirectTrackingRef(root, ref);
|
|
1420
|
+
}
|
|
1421
|
+
catch (error) {
|
|
1422
|
+
return unmanaged(error instanceof Error ? error.message : String(error));
|
|
1423
|
+
}
|
|
1424
|
+
return { status: "managed", remoteName: selected.name, remoteUrl: wantedUrl, ref, evidence };
|
|
1425
|
+
}
|
|
1426
|
+
async function readDirectTrackingRef(root, ref) {
|
|
1427
|
+
const output = await git([
|
|
1428
|
+
"-C", root, "for-each-ref", "--format=%(refname)%09%(objectname)%09%(symref)", "--", ref
|
|
1429
|
+
]);
|
|
1430
|
+
const record = output.split("\n").find((line) => line.split("\t")[0] === ref);
|
|
1431
|
+
if (record === undefined) {
|
|
1432
|
+
// for-each-ref omits dangling symbolic refs. Never treat those as absent.
|
|
1433
|
+
if (await gitSucceeds(["-C", root, "symbolic-ref", "--quiet", ref])) {
|
|
1434
|
+
throw new Error(`Tracking destination is symbolic: ${ref}.`);
|
|
1435
|
+
}
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
const [, commit, symbolic] = record.split("\t");
|
|
1439
|
+
if (symbolic !== "" || commit === undefined || !isCommit(commit)) {
|
|
1440
|
+
throw new Error(`Tracking destination is not a direct object ref: ${ref}.`);
|
|
1441
|
+
}
|
|
1442
|
+
return commit.toLowerCase();
|
|
1443
|
+
}
|
|
1184
1444
|
function safeIdentity(value, label) {
|
|
1185
1445
|
const identity = requireText(value, label);
|
|
1186
1446
|
if ([".", "..", "__proto__", "prototype", "constructor"].includes(identity)
|
|
@@ -1208,10 +1468,13 @@ async function safeFetchBranch(value) {
|
|
|
1208
1468
|
}
|
|
1209
1469
|
return branch;
|
|
1210
1470
|
}
|
|
1211
|
-
async function resolveRemoteHeadBranch(remote) {
|
|
1471
|
+
async function resolveRemoteHeadBranch(remote, repositoryPath) {
|
|
1212
1472
|
let output;
|
|
1213
1473
|
try {
|
|
1214
|
-
output = await git([
|
|
1474
|
+
output = await git([
|
|
1475
|
+
...(repositoryPath === undefined ? [] : ["-C", repositoryPath]),
|
|
1476
|
+
"ls-remote", "--symref", remote, "HEAD"
|
|
1477
|
+
]);
|
|
1215
1478
|
}
|
|
1216
1479
|
catch (error) {
|
|
1217
1480
|
const detail = error instanceof Error ? error.message : "Git command failed.";
|
|
@@ -1234,11 +1497,14 @@ async function resolveRemoteHeadBranch(remote) {
|
|
|
1234
1497
|
throw invalidRemoteHead();
|
|
1235
1498
|
}
|
|
1236
1499
|
}
|
|
1237
|
-
async function resolveRemoteBranchCommit(remote, branch) {
|
|
1500
|
+
async function resolveRemoteBranchCommit(remote, branch, repositoryPath) {
|
|
1238
1501
|
const target = `refs/heads/${branch}`;
|
|
1239
1502
|
let output;
|
|
1240
1503
|
try {
|
|
1241
|
-
output = await git([
|
|
1504
|
+
output = await git([
|
|
1505
|
+
...(repositoryPath === undefined ? [] : ["-C", repositoryPath]),
|
|
1506
|
+
"ls-remote", "--refs", remote, target
|
|
1507
|
+
]);
|
|
1242
1508
|
}
|
|
1243
1509
|
catch (error) {
|
|
1244
1510
|
const detail = error instanceof Error ? error.message : "Git command failed.";
|