@markjaquith/agency 2.57.0 → 2.58.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -6
- package/package.json +1 -1
- package/src/cli-parser.test.ts +8 -0
- package/src/cli-parser.ts +7 -1
- package/src/commands/archive.test.ts +39 -1
- package/src/commands/archive.ts +25 -2
- package/src/commands/restore.test.ts +8 -0
- package/src/services/ArchiveBulkService.test.ts +485 -0
- package/src/services/ArchiveService.test.ts +336 -2
- package/src/services/ArchiveService.ts +447 -29
- package/src/services/WorktreeService.test.ts +4 -0
- package/src/services/WorktreeService.ts +155 -13
|
@@ -449,11 +449,7 @@ const inspectExecution = (
|
|
|
449
449
|
: yield* backend.workspaceHead(checkoutPath)
|
|
450
450
|
: null
|
|
451
451
|
const dirty =
|
|
452
|
-
exists && atPath
|
|
453
|
-
? atPath.dirty !== undefined
|
|
454
|
-
? atPath.dirty
|
|
455
|
-
: yield* backend.workspaceDirty(checkoutPath)
|
|
456
|
-
: null
|
|
452
|
+
exists && atPath ? yield* backend.workspaceDirty(checkoutPath) : null
|
|
457
453
|
const expectedCommit =
|
|
458
454
|
"branch" in checkout && context?.skipWritableRevisionResolution
|
|
459
455
|
? actualCommit
|
|
@@ -774,6 +770,81 @@ const jjWorkspaceName = (
|
|
|
774
770
|
repo: string,
|
|
775
771
|
) => `agency-${taskId}-${phaseId ?? "task"}-${repo}`
|
|
776
772
|
|
|
773
|
+
// A process can stop after `jj workspace forget` but before checkout deletion.
|
|
774
|
+
// Only recover that residual when its repository and complete tree still match.
|
|
775
|
+
const inspectJjResidual = (
|
|
776
|
+
root: string,
|
|
777
|
+
repositoryPath: string,
|
|
778
|
+
workspacePath: string,
|
|
779
|
+
revision: string,
|
|
780
|
+
) =>
|
|
781
|
+
Effect.gen(function* () {
|
|
782
|
+
const fs = yield* FileSystemService
|
|
783
|
+
const repoPointer = join(workspacePath, ".jj", "repo")
|
|
784
|
+
if (!(yield* fs.exists(repoPointer))) return "unowned" as const
|
|
785
|
+
|
|
786
|
+
const pointer = (yield* fs.readFile(repoPointer)).trim()
|
|
787
|
+
if (!pointer) return "unowned" as const
|
|
788
|
+
const linkedRepository = resolve(dirname(repoPointer), pointer)
|
|
789
|
+
const expectedRepository = join(repositoryPath, ".jj", "repo")
|
|
790
|
+
if (
|
|
791
|
+
!(yield* fs.exists(linkedRepository)) ||
|
|
792
|
+
(yield* fs.realPath(linkedRepository)) !==
|
|
793
|
+
(yield* fs.realPath(expectedRepository))
|
|
794
|
+
)
|
|
795
|
+
return "unowned" as const
|
|
796
|
+
|
|
797
|
+
const comparisonPath = join(
|
|
798
|
+
root,
|
|
799
|
+
`.agency-jj-residual-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
800
|
+
)
|
|
801
|
+
const comparisonName = basename(comparisonPath)
|
|
802
|
+
return yield* Effect.gen(function* () {
|
|
803
|
+
const created = yield* fs.runCommand(
|
|
804
|
+
[
|
|
805
|
+
"jj",
|
|
806
|
+
"-R",
|
|
807
|
+
repositoryPath,
|
|
808
|
+
"workspace",
|
|
809
|
+
"add",
|
|
810
|
+
"--name",
|
|
811
|
+
comparisonName,
|
|
812
|
+
"-r",
|
|
813
|
+
revision,
|
|
814
|
+
comparisonPath,
|
|
815
|
+
],
|
|
816
|
+
{ captureOutput: true },
|
|
817
|
+
)
|
|
818
|
+
if (created.exitCode !== 0) {
|
|
819
|
+
return yield* new WorktreeError({
|
|
820
|
+
message: `Cannot verify unregistered jj checkout ${workspacePath}: ${created.stderr}`,
|
|
821
|
+
})
|
|
822
|
+
}
|
|
823
|
+
const comparison = yield* fs.runCommand(
|
|
824
|
+
["diff", "-qr", "-x", ".jj", workspacePath, comparisonPath],
|
|
825
|
+
{ captureOutput: true },
|
|
826
|
+
)
|
|
827
|
+
if (comparison.exitCode > 1) {
|
|
828
|
+
return yield* new WorktreeError({
|
|
829
|
+
message: `Cannot verify unregistered jj checkout ${workspacePath}: ${comparison.stderr}`,
|
|
830
|
+
})
|
|
831
|
+
}
|
|
832
|
+
return comparison.exitCode === 0
|
|
833
|
+
? ("clean" as const)
|
|
834
|
+
: ("modified" as const)
|
|
835
|
+
}).pipe(
|
|
836
|
+
Effect.ensuring(
|
|
837
|
+
Effect.gen(function* () {
|
|
838
|
+
yield* fs.runCommand(
|
|
839
|
+
["jj", "-R", repositoryPath, "workspace", "forget", comparisonName],
|
|
840
|
+
{ captureOutput: true },
|
|
841
|
+
)
|
|
842
|
+
yield* fs.deleteDirectory(comparisonPath)
|
|
843
|
+
}).pipe(Effect.ignore),
|
|
844
|
+
),
|
|
845
|
+
)
|
|
846
|
+
})
|
|
847
|
+
|
|
777
848
|
const buildInspectionContext = (
|
|
778
849
|
startPath: string,
|
|
779
850
|
skipWritableRevisionResolution: boolean,
|
|
@@ -1107,10 +1178,63 @@ const removeJj = (
|
|
|
1107
1178
|
Effect.gen(function* () {
|
|
1108
1179
|
const fs = yield* FileSystemService
|
|
1109
1180
|
const versionControl = yield* VersionControlService
|
|
1181
|
+
const tasks = yield* TaskService
|
|
1182
|
+
const phases = yield* PhaseService
|
|
1110
1183
|
const backend = yield* versionControl.forWorkbase(root)
|
|
1111
1184
|
const inspection = yield* inspectExecution(taskId, phaseId, root)
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1185
|
+
const task = yield* tasks.show(taskId, root)
|
|
1186
|
+
const execution =
|
|
1187
|
+
"phases" in task.data && phaseId
|
|
1188
|
+
? (yield* phases.show(taskId, phaseId, root)).data
|
|
1189
|
+
: task.data
|
|
1190
|
+
const recoverable = new Map<string, string>()
|
|
1191
|
+
const modifiedResiduals = new Map<string, string>()
|
|
1192
|
+
for (const checkout of inspection.checkouts) {
|
|
1193
|
+
const recoveryRevision =
|
|
1194
|
+
checkout.expectedCommit ??
|
|
1195
|
+
(checkout.kind === "writable" && "base" in execution
|
|
1196
|
+
? yield* backend.resolveRevision(
|
|
1197
|
+
join(root, "repos", checkout.repo),
|
|
1198
|
+
execution.base,
|
|
1199
|
+
)
|
|
1200
|
+
: null)
|
|
1201
|
+
if (
|
|
1202
|
+
!checkout.conflicts.some(
|
|
1203
|
+
(conflict) => conflict.kind === "unregistered-checkout",
|
|
1204
|
+
) ||
|
|
1205
|
+
!recoveryRevision
|
|
1206
|
+
)
|
|
1207
|
+
continue
|
|
1208
|
+
const repositoryPath = join(root, "repos", checkout.repo)
|
|
1209
|
+
const residual = yield* inspectJjResidual(
|
|
1210
|
+
root,
|
|
1211
|
+
repositoryPath,
|
|
1212
|
+
checkout.path,
|
|
1213
|
+
recoveryRevision,
|
|
1214
|
+
)
|
|
1215
|
+
if (residual === "clean") recoverable.set(checkout.path, recoveryRevision)
|
|
1216
|
+
else if (residual === "modified")
|
|
1217
|
+
modifiedResiduals.set(checkout.path, recoveryRevision)
|
|
1218
|
+
}
|
|
1219
|
+
const blocking = inspection.checkouts.flatMap((checkout) =>
|
|
1220
|
+
checkout.conflicts
|
|
1221
|
+
.filter(
|
|
1222
|
+
(conflict) =>
|
|
1223
|
+
conflict.kind !== "stale-registration" &&
|
|
1224
|
+
!(
|
|
1225
|
+
conflict.kind === "unregistered-checkout" &&
|
|
1226
|
+
recoverable.has(checkout.path)
|
|
1227
|
+
),
|
|
1228
|
+
)
|
|
1229
|
+
.map((conflict) =>
|
|
1230
|
+
conflict.kind === "unregistered-checkout" &&
|
|
1231
|
+
modifiedResiduals.has(checkout.path)
|
|
1232
|
+
? {
|
|
1233
|
+
...conflict,
|
|
1234
|
+
message: `Cannot remove unregistered jj checkout ${checkout.path}; its contents do not match expected revision '${modifiedResiduals.get(checkout.path)}'`,
|
|
1235
|
+
}
|
|
1236
|
+
: conflict,
|
|
1237
|
+
),
|
|
1114
1238
|
)
|
|
1115
1239
|
if (blocking.length > 0) {
|
|
1116
1240
|
return yield* new WorktreeError({
|
|
@@ -1125,6 +1249,7 @@ const removeJj = (
|
|
|
1125
1249
|
workspaceName: string
|
|
1126
1250
|
head: string
|
|
1127
1251
|
branch: string | null
|
|
1252
|
+
registered: boolean
|
|
1128
1253
|
}[] = []
|
|
1129
1254
|
for (const checkout of inspection.checkouts) {
|
|
1130
1255
|
if (checkout.dirty) {
|
|
@@ -1132,8 +1257,20 @@ const removeJj = (
|
|
|
1132
1257
|
message: `Failed to remove workspace for '${checkout.repo}': checkout has uncommitted changes`,
|
|
1133
1258
|
})
|
|
1134
1259
|
}
|
|
1135
|
-
if (!checkout.registeredPath) continue
|
|
1136
1260
|
const repositoryPath = join(root, "repos", checkout.repo)
|
|
1261
|
+
if (!checkout.registeredPath) {
|
|
1262
|
+
const recoveryRevision = recoverable.get(checkout.path)
|
|
1263
|
+
if (!recoveryRevision) continue
|
|
1264
|
+
plans.push({
|
|
1265
|
+
repositoryPath,
|
|
1266
|
+
workspacePath: checkout.path,
|
|
1267
|
+
workspaceName: jjWorkspaceName(taskId, phaseId, checkout.repo),
|
|
1268
|
+
head: recoveryRevision,
|
|
1269
|
+
branch: checkout.actualBranch,
|
|
1270
|
+
registered: false,
|
|
1271
|
+
})
|
|
1272
|
+
continue
|
|
1273
|
+
}
|
|
1137
1274
|
const registered = (yield* backend.listWorkspaces(repositoryPath)).find(
|
|
1138
1275
|
(workspace) => workspace.path === checkout.registeredPath,
|
|
1139
1276
|
)
|
|
@@ -1148,6 +1285,7 @@ const removeJj = (
|
|
|
1148
1285
|
workspaceName: registered.name,
|
|
1149
1286
|
head: checkout.actualCommit,
|
|
1150
1287
|
branch: checkout.actualBranch,
|
|
1288
|
+
registered: true,
|
|
1151
1289
|
})
|
|
1152
1290
|
}
|
|
1153
1291
|
|
|
@@ -1166,11 +1304,15 @@ const removeJj = (
|
|
|
1166
1304
|
const completed: typeof plans = []
|
|
1167
1305
|
return yield* Effect.gen(function* () {
|
|
1168
1306
|
for (const plan of plans) {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1307
|
+
if (plan.registered) {
|
|
1308
|
+
yield* backend.removeWorkspace({
|
|
1309
|
+
repositoryPath: plan.repositoryPath,
|
|
1310
|
+
workspacePath: plan.workspacePath,
|
|
1311
|
+
workspaceName: plan.workspaceName,
|
|
1312
|
+
})
|
|
1313
|
+
} else {
|
|
1314
|
+
yield* fs.deleteDirectory(plan.workspacePath)
|
|
1315
|
+
}
|
|
1174
1316
|
completed.push(plan)
|
|
1175
1317
|
}
|
|
1176
1318
|
yield* fs.deleteDirectoryIfEmpty(inspection.codePath)
|