@markjaquith/agency 2.58.1 → 2.58.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1882,6 +1882,111 @@ pr: null
|
|
|
1882
1882
|
).toBe(true)
|
|
1883
1883
|
})
|
|
1884
1884
|
|
|
1885
|
+
test("rebuilds clean drifted jj references without discarding changes", async () => {
|
|
1886
|
+
if (!Bun.which("jj")) return
|
|
1887
|
+
const first = new TextDecoder()
|
|
1888
|
+
.decode(
|
|
1889
|
+
Bun.spawnSync(["git", "-C", source, "rev-parse", "HEAD"], {
|
|
1890
|
+
stdout: "pipe",
|
|
1891
|
+
}).stdout,
|
|
1892
|
+
)
|
|
1893
|
+
.trim()
|
|
1894
|
+
await Bun.write(join(source, "README.md"), "updated\n")
|
|
1895
|
+
await git(["add", "README.md"], source)
|
|
1896
|
+
await git(["-c", "commit.gpgsign=false", "commit", "-m", "update"], source)
|
|
1897
|
+
const second = new TextDecoder()
|
|
1898
|
+
.decode(
|
|
1899
|
+
Bun.spawnSync(["git", "-C", source, "rev-parse", "HEAD"], {
|
|
1900
|
+
stdout: "pipe",
|
|
1901
|
+
}).stdout,
|
|
1902
|
+
)
|
|
1903
|
+
.trim()
|
|
1904
|
+
const repository = join(root, "repos/agency")
|
|
1905
|
+
await rm(repository, { recursive: true, force: true })
|
|
1906
|
+
await jj(["git", "clone", "--no-colocate", source, repository])
|
|
1907
|
+
await Bun.write(
|
|
1908
|
+
join(root, "agency.json"),
|
|
1909
|
+
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
1910
|
+
)
|
|
1911
|
+
const createDriftedReview = async (id: string) => {
|
|
1912
|
+
const task = await runTestEffect(
|
|
1913
|
+
TaskService.pipe(
|
|
1914
|
+
Effect.flatMap((service) =>
|
|
1915
|
+
service.create(
|
|
1916
|
+
{
|
|
1917
|
+
id,
|
|
1918
|
+
ticketUrl: null,
|
|
1919
|
+
review: {
|
|
1920
|
+
repo: "agency",
|
|
1921
|
+
source: {
|
|
1922
|
+
kind: "branch",
|
|
1923
|
+
ref: "refs/heads/main",
|
|
1924
|
+
},
|
|
1925
|
+
commit: first,
|
|
1926
|
+
refreshedAt: "2026-08-10T00:00:00.000Z",
|
|
1927
|
+
},
|
|
1928
|
+
},
|
|
1929
|
+
root,
|
|
1930
|
+
),
|
|
1931
|
+
),
|
|
1932
|
+
),
|
|
1933
|
+
)
|
|
1934
|
+
const workspace = await runTestEffect(
|
|
1935
|
+
WorktreeService.pipe(
|
|
1936
|
+
Effect.flatMap((service) => service.materialize(id, undefined, root)),
|
|
1937
|
+
),
|
|
1938
|
+
)
|
|
1939
|
+
await Bun.write(task.path, task.content.replace(first, second))
|
|
1940
|
+
return workspace
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
const workspace = await createDriftedReview("drifted-review")
|
|
1944
|
+
|
|
1945
|
+
await expect(
|
|
1946
|
+
runTestEffect(
|
|
1947
|
+
WorktreeService.pipe(
|
|
1948
|
+
Effect.flatMap((service) =>
|
|
1949
|
+
service.materialize("drifted-review", undefined, root),
|
|
1950
|
+
),
|
|
1951
|
+
),
|
|
1952
|
+
),
|
|
1953
|
+
).rejects.toThrow("does not match reference")
|
|
1954
|
+
const inspection = await runTestEffect(
|
|
1955
|
+
WorktreeService.pipe(
|
|
1956
|
+
Effect.flatMap((service) =>
|
|
1957
|
+
service.inspect("drifted-review", undefined, root),
|
|
1958
|
+
),
|
|
1959
|
+
),
|
|
1960
|
+
)
|
|
1961
|
+
expect(inspection.conflicts).toEqual([
|
|
1962
|
+
expect.objectContaining({ kind: "reference-drift" }),
|
|
1963
|
+
])
|
|
1964
|
+
await runTestEffect(
|
|
1965
|
+
WorktreeService.pipe(
|
|
1966
|
+
Effect.flatMap((service) =>
|
|
1967
|
+
service.rebuild("drifted-review", undefined, root),
|
|
1968
|
+
),
|
|
1969
|
+
),
|
|
1970
|
+
)
|
|
1971
|
+
expect(
|
|
1972
|
+
await Bun.file(join(workspace.reviewPath!, "README.md")).text(),
|
|
1973
|
+
).toBe("updated\n")
|
|
1974
|
+
|
|
1975
|
+
const dirtyWorkspace = await createDriftedReview("dirty-drifted-review")
|
|
1976
|
+
const dirtyPath = join(dirtyWorkspace.reviewPath!, "dirty.txt")
|
|
1977
|
+
await Bun.write(dirtyPath, "preserve me\n")
|
|
1978
|
+
await expect(
|
|
1979
|
+
runTestEffect(
|
|
1980
|
+
WorktreeService.pipe(
|
|
1981
|
+
Effect.flatMap((service) =>
|
|
1982
|
+
service.rebuild("dirty-drifted-review", undefined, root),
|
|
1983
|
+
),
|
|
1984
|
+
),
|
|
1985
|
+
),
|
|
1986
|
+
).rejects.toThrow("checkout has uncommitted changes")
|
|
1987
|
+
expect(await Bun.file(dirtyPath).text()).toBe("preserve me\n")
|
|
1988
|
+
})
|
|
1989
|
+
|
|
1885
1990
|
test("restores removed worktrees when rebuild creation fails", async () => {
|
|
1886
1991
|
await runTestEffect(
|
|
1887
1992
|
TaskService.pipe(
|
|
@@ -254,11 +254,13 @@ const originRef = (ref: string) =>
|
|
|
254
254
|
interface MaterializeOptions extends BaseCommandOptions {
|
|
255
255
|
readonly force?: boolean
|
|
256
256
|
readonly lockHeld?: boolean
|
|
257
|
+
readonly allowReferenceDrift?: boolean
|
|
257
258
|
}
|
|
258
259
|
|
|
259
260
|
interface RemoveOptions extends BaseCommandOptions {
|
|
260
261
|
readonly snapshots?: WorktreeRemovalSnapshot[]
|
|
261
262
|
readonly lockHeld?: boolean
|
|
263
|
+
readonly allowReferenceDrift?: boolean
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
interface LifecycleOptions extends BaseCommandOptions {
|
|
@@ -1010,12 +1012,32 @@ const materializeJj = (options: {
|
|
|
1010
1012
|
})
|
|
1011
1013
|
}
|
|
1012
1014
|
if (exists && registered) {
|
|
1015
|
+
const actualCommit = yield* backend.workspaceHead(workspacePath)
|
|
1016
|
+
if ("ref" in checkout) {
|
|
1017
|
+
const expectedCommit = yield* backend.resolveRevision(
|
|
1018
|
+
repositoryPath,
|
|
1019
|
+
checkout.ref,
|
|
1020
|
+
)
|
|
1021
|
+
if (!expectedCommit) {
|
|
1022
|
+
return yield* new WorktreeError({
|
|
1023
|
+
message: `Reference '${checkout.ref}' for repository '${checkout.repo}' does not resolve to a commit; run 'agency repo fetch ${checkout.repo}' and retry`,
|
|
1024
|
+
})
|
|
1025
|
+
}
|
|
1026
|
+
if (
|
|
1027
|
+
actualCommit !== expectedCommit &&
|
|
1028
|
+
!options.commandOptions.allowReferenceDrift
|
|
1029
|
+
) {
|
|
1030
|
+
return yield* new WorktreeError({
|
|
1031
|
+
message: `Existing checkout ${workspacePath} does not match reference '${checkout.ref}' (${expectedCommit})`,
|
|
1032
|
+
})
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1013
1035
|
reports.push({
|
|
1014
1036
|
repo: checkout.repo,
|
|
1015
1037
|
kind: "branch" in checkout ? "writable" : "reference",
|
|
1016
1038
|
path: workspacePath,
|
|
1017
1039
|
requestedRef: "branch" in checkout ? checkout.branch : checkout.ref,
|
|
1018
|
-
resolvedCommit:
|
|
1040
|
+
resolvedCommit: actualCommit,
|
|
1019
1041
|
action: "reused",
|
|
1020
1042
|
})
|
|
1021
1043
|
continue
|
|
@@ -1221,6 +1243,9 @@ const removeJj = (
|
|
|
1221
1243
|
.filter(
|
|
1222
1244
|
(conflict) =>
|
|
1223
1245
|
conflict.kind !== "stale-registration" &&
|
|
1246
|
+
!(
|
|
1247
|
+
options.allowReferenceDrift && conflict.kind === "reference-drift"
|
|
1248
|
+
) &&
|
|
1224
1249
|
!(
|
|
1225
1250
|
conflict.kind === "unregistered-checkout" &&
|
|
1226
1251
|
recoverable.has(checkout.path)
|
|
@@ -2392,7 +2417,12 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2392
2417
|
root,
|
|
2393
2418
|
)
|
|
2394
2419
|
const blockingConflicts = inspection.conflicts.filter(
|
|
2395
|
-
(conflict) =>
|
|
2420
|
+
(conflict) =>
|
|
2421
|
+
conflict.kind !== "stale-registration" &&
|
|
2422
|
+
!(
|
|
2423
|
+
options.allowReferenceDrift &&
|
|
2424
|
+
conflict.kind === "reference-drift"
|
|
2425
|
+
),
|
|
2396
2426
|
)
|
|
2397
2427
|
if (blockingConflicts.length > 0) {
|
|
2398
2428
|
return yield* new WorktreeError({
|
|
@@ -2723,21 +2753,29 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2723
2753
|
)
|
|
2724
2754
|
}
|
|
2725
2755
|
const inspection = yield* inspectExecution(taskId, phaseId, root)
|
|
2726
|
-
const
|
|
2727
|
-
(checkout) => checkout.dirty
|
|
2756
|
+
const notProvablyClean = inspection.checkouts.filter(
|
|
2757
|
+
(checkout) => checkout.exists && checkout.dirty !== false,
|
|
2728
2758
|
)
|
|
2729
|
-
if (
|
|
2759
|
+
if (notProvablyClean.length > 0) {
|
|
2760
|
+
const unknown = notProvablyClean.some(
|
|
2761
|
+
(checkout) => checkout.dirty === null,
|
|
2762
|
+
)
|
|
2730
2763
|
return yield* new WorktreeError({
|
|
2731
|
-
message: `Cannot rebuild ${
|
|
2732
|
-
conflicts:
|
|
2764
|
+
message: `Cannot rebuild ${notProvablyClean.map((checkout) => checkout.path).join(", ")}; ${unknown ? "checkout cleanliness could not be verified" : "checkout has uncommitted changes"}`,
|
|
2765
|
+
conflicts: notProvablyClean.flatMap(
|
|
2766
|
+
(checkout) => checkout.conflicts,
|
|
2767
|
+
),
|
|
2733
2768
|
})
|
|
2734
2769
|
}
|
|
2735
|
-
|
|
2770
|
+
const blockingConflicts = inspection.conflicts.filter(
|
|
2771
|
+
(conflict) => conflict.kind !== "reference-drift",
|
|
2772
|
+
)
|
|
2773
|
+
if (blockingConflicts.length > 0) {
|
|
2736
2774
|
return yield* new WorktreeError({
|
|
2737
|
-
message:
|
|
2775
|
+
message: blockingConflicts
|
|
2738
2776
|
.map(({ message }) => message)
|
|
2739
2777
|
.join("\n"),
|
|
2740
|
-
conflicts:
|
|
2778
|
+
conflicts: blockingConflicts,
|
|
2741
2779
|
})
|
|
2742
2780
|
}
|
|
2743
2781
|
yield* service.materialize(taskId, phaseId, inspection.root, {
|
|
@@ -2745,6 +2783,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2745
2783
|
dryRun: true,
|
|
2746
2784
|
silent: true,
|
|
2747
2785
|
lockHeld: true,
|
|
2786
|
+
allowReferenceDrift: true,
|
|
2748
2787
|
})
|
|
2749
2788
|
if (options.dryRun) {
|
|
2750
2789
|
return {
|
|
@@ -2768,6 +2807,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2768
2807
|
...options,
|
|
2769
2808
|
snapshots,
|
|
2770
2809
|
lockHeld: true,
|
|
2810
|
+
allowReferenceDrift: true,
|
|
2771
2811
|
},
|
|
2772
2812
|
)
|
|
2773
2813
|
const workspace = yield* service
|