@markjaquith/agency 2.73.2 → 3.0.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 +35 -134
- package/cli-main.ts +0 -26
- package/cli.ts +1 -17
- package/package.json +1 -2
- package/schemas/agency-graph-v1.schema.json +1 -1
- package/src/cli-parser.test.ts +0 -19
- package/src/cli-parser.ts +0 -23
- package/src/cli.test.ts +3 -3
- package/src/commands/context.test.ts +11 -71
- package/src/commands/context.ts +7 -7
- package/src/commands/init.test.ts +1 -2
- package/src/commands/pr.test.ts +2 -87
- package/src/commands/pr.ts +2 -79
- package/src/commands/push.test.ts +1 -2
- package/src/graph-schema.ts +1 -1
- package/src/services/ArchiveBulkService.test.ts +1 -142
- package/src/services/ArchiveService.test.ts +1 -172
- package/src/services/ArchiveService.ts +0 -26
- package/src/services/ContextService.ts +7 -50
- package/src/services/DoctorService.ts +8 -24
- package/src/services/GraphService.test.ts +1 -0
- package/src/services/GraphService.ts +2 -5
- package/src/services/IntegrationService.test.ts +4 -0
- package/src/services/PhaseService.ts +70 -191
- package/src/services/PullRequestService.test.ts +1 -45
- package/src/services/PullRequestService.ts +1 -22
- package/src/services/PushService.test.ts +29 -295
- package/src/services/PushService.ts +10 -239
- package/src/services/RepositoryService.test.ts +1 -61
- package/src/services/RepositoryService.ts +2 -13
- package/src/services/ReviewService.test.ts +1 -84
- package/src/services/ReviewService.ts +9 -48
- package/src/services/SyncService.test.ts +0 -81
- package/src/services/SyncService.ts +12 -39
- package/src/services/TaskPhaseService.test.ts +0 -80
- package/src/services/TaskService.ts +1 -9
- package/src/services/VersionControlService.test.ts +4 -117
- package/src/services/VersionControlService.ts +3 -426
- package/src/services/WorkbaseService.test.ts +0 -20
- package/src/services/WorkbaseService.ts +1 -18
- package/src/services/WorktreeService.test.ts +1 -705
- package/src/services/WorktreeService.ts +395 -1604
- package/src/services/push-validation.ts +0 -7
- package/src/test-utils.ts +0 -4
- package/src/workbase/AGENTS.md +7 -7
- package/src/workbase/checkout-command.test.ts +1 -1
- package/src/workbase/checkout-command.ts +1 -1
- package/src/workbase/delivery-command.test.ts +4 -35
- package/src/workbase/delivery-command.ts +2 -15
- package/src/workbase/schemas.test.ts +0 -19
- package/src/workbase/schemas.ts +1 -2
- package/src/commands/vcs.test.ts +0 -75
- package/src/commands/vcs.ts +0 -72
- package/src/services/VcsMigrationService.test.ts +0 -348
- package/src/services/VcsMigrationService.ts +0 -857
- package/src/vcs-status-fast.ts +0 -310
- package/src/workbase/version-control.ts +0 -5
- package/src/workbase/workspace-command.test.ts +0 -70
- package/src/workbase/workspace-command.ts +0 -63
|
@@ -19,14 +19,13 @@ interface CommandResult {
|
|
|
19
19
|
type CommitMetadata = PushCommitMetadata
|
|
20
20
|
|
|
21
21
|
interface PushResult {
|
|
22
|
-
readonly vcs: "git"
|
|
22
|
+
readonly vcs: "git"
|
|
23
23
|
readonly taskId: string
|
|
24
24
|
readonly phaseId?: string
|
|
25
25
|
readonly branch: string
|
|
26
26
|
readonly base: string
|
|
27
27
|
readonly remote: string
|
|
28
28
|
readonly tip: string
|
|
29
|
-
readonly changeId?: string
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
type PushStage = "context" | "fetch" | "inspect" | "validate" | "publish"
|
|
@@ -234,224 +233,6 @@ const publishGit = (
|
|
|
234
233
|
return { tip }
|
|
235
234
|
})
|
|
236
235
|
|
|
237
|
-
const jjTemplate =
|
|
238
|
-
'"{\\"commitId\\":" ++ json(commit_id) ++ ",\\"changeId\\":" ++ json(change_id) ++ ",\\"description\\":" ++ json(description) ++ ",\\"empty\\":" ++ json(empty) ++ ",\\"authorName\\":" ++ json(author.name()) ++ ",\\"authorEmail\\":" ++ json(author.email()) ++ ",\\"conflict\\":" ++ json(conflict) ++ ",\\"parents\\":" ++ json(parents.map(|parent| parent.commit_id())) ++ "}\\n"'
|
|
239
|
-
|
|
240
|
-
const jjExact = (value: string) =>
|
|
241
|
-
value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')
|
|
242
|
-
|
|
243
|
-
const jj = (
|
|
244
|
-
fs: FileSystemService,
|
|
245
|
-
cwd: string,
|
|
246
|
-
args: readonly string[],
|
|
247
|
-
label: string,
|
|
248
|
-
) => requireCommand(fs, ["jj", "--no-pager", ...args], cwd, label)
|
|
249
|
-
|
|
250
|
-
const jjCommits = (fs: FileSystemService, cwd: string, revision: string) =>
|
|
251
|
-
jj(
|
|
252
|
-
fs,
|
|
253
|
-
cwd,
|
|
254
|
-
["log", "--no-graph", "-r", revision, "-T", jjTemplate],
|
|
255
|
-
"Failed to inspect jj changes",
|
|
256
|
-
).pipe(
|
|
257
|
-
Effect.map((result) =>
|
|
258
|
-
result.stdout
|
|
259
|
-
.split("\n")
|
|
260
|
-
.filter(Boolean)
|
|
261
|
-
.map((line) => JSON.parse(line) as CommitMetadata),
|
|
262
|
-
),
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
const jjRevision = (fs: FileSystemService, cwd: string, revision: string) =>
|
|
266
|
-
jjCommits(fs, cwd, revision).pipe(
|
|
267
|
-
Effect.map((commits) => {
|
|
268
|
-
if (commits.length === 0) return null
|
|
269
|
-
if (commits.length === 1) return commits[0]!
|
|
270
|
-
throw new PushError({
|
|
271
|
-
message: `Revision '${revision}' resolves to multiple jj commits`,
|
|
272
|
-
})
|
|
273
|
-
}),
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
const optionalJjRevision = (
|
|
277
|
-
fs: FileSystemService,
|
|
278
|
-
cwd: string,
|
|
279
|
-
revision: string,
|
|
280
|
-
) =>
|
|
281
|
-
jjCommits(fs, cwd, revision).pipe(
|
|
282
|
-
Effect.catchTag("PushError", () => Effect.succeed([])),
|
|
283
|
-
Effect.map((commits) => {
|
|
284
|
-
if (commits.length === 0) return null
|
|
285
|
-
if (commits.length === 1) return commits[0]!
|
|
286
|
-
throw new PushError({
|
|
287
|
-
message: `Bookmark '${revision}' is conflicted or resolves to multiple commits`,
|
|
288
|
-
})
|
|
289
|
-
}),
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
const jjAncestor = (
|
|
293
|
-
fs: FileSystemService,
|
|
294
|
-
cwd: string,
|
|
295
|
-
ancestor: string,
|
|
296
|
-
descendant: string,
|
|
297
|
-
) =>
|
|
298
|
-
jjCommits(fs, cwd, `${ancestor} & ::${descendant}`).pipe(
|
|
299
|
-
Effect.map((commits) => commits.length === 1),
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
const validateJjCommits = (
|
|
303
|
-
commits: readonly CommitMetadata[],
|
|
304
|
-
base: string,
|
|
305
|
-
) => {
|
|
306
|
-
if (commits.length === 0) {
|
|
307
|
-
throw new PushError({
|
|
308
|
-
message: `No changes to publish after base '${base}'`,
|
|
309
|
-
})
|
|
310
|
-
}
|
|
311
|
-
const issues: string[] = []
|
|
312
|
-
for (const commit of commits) {
|
|
313
|
-
const id = commit.changeId!
|
|
314
|
-
if (commit.conflict) {
|
|
315
|
-
issues.push(`Change ${id} contains conflicts. Run: jj resolve -r ${id}`)
|
|
316
|
-
}
|
|
317
|
-
if (!commit.description.trim()) {
|
|
318
|
-
issues.push(`Change ${id} has no description. Run: jj describe -r ${id}`)
|
|
319
|
-
}
|
|
320
|
-
if (!commit.authorName.trim() || !validEmail(commit.authorEmail.trim())) {
|
|
321
|
-
issues.push(
|
|
322
|
-
`Change ${id} has an invalid author. Run: jj metaedit -r ${id} --author 'Name <email>'`,
|
|
323
|
-
)
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
if (issues.length > 0) throw new PushError({ message: issues.join("\n") })
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const publishJj = (
|
|
330
|
-
fs: FileSystemService,
|
|
331
|
-
checkout: string,
|
|
332
|
-
remote: string,
|
|
333
|
-
branch: string,
|
|
334
|
-
base: string,
|
|
335
|
-
onProgress?: (stage: PushStage) => void,
|
|
336
|
-
) =>
|
|
337
|
-
Effect.gen(function* () {
|
|
338
|
-
onProgress?.("fetch")
|
|
339
|
-
yield* jj(
|
|
340
|
-
fs,
|
|
341
|
-
checkout,
|
|
342
|
-
["git", "fetch", "--remote", remote],
|
|
343
|
-
`Failed to fetch remote '${remote}'`,
|
|
344
|
-
)
|
|
345
|
-
onProgress?.("inspect")
|
|
346
|
-
const workingCopy = yield* jjRevision(fs, checkout, "@")
|
|
347
|
-
if (!workingCopy) {
|
|
348
|
-
return yield* new PushError({
|
|
349
|
-
message: "jj working copy commit was not found",
|
|
350
|
-
})
|
|
351
|
-
}
|
|
352
|
-
const canonicalPostCommit =
|
|
353
|
-
workingCopy.empty &&
|
|
354
|
-
!workingCopy.description.trim() &&
|
|
355
|
-
workingCopy.parents.length === 1
|
|
356
|
-
const tip = canonicalPostCommit
|
|
357
|
-
? yield* jjRevision(fs, checkout, "@-")
|
|
358
|
-
: workingCopy
|
|
359
|
-
if (!tip) {
|
|
360
|
-
return yield* new PushError({
|
|
361
|
-
message: "jj publication tip was not found",
|
|
362
|
-
})
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const baseBookmark = `${base}@${remote}`
|
|
366
|
-
const [baseRevision, localBookmark, remoteBookmark] = yield* Effect.all(
|
|
367
|
-
[
|
|
368
|
-
optionalJjRevision(
|
|
369
|
-
fs,
|
|
370
|
-
checkout,
|
|
371
|
-
`remote_bookmarks(exact:"${jjExact(base)}", exact:"${jjExact(remote)}")`,
|
|
372
|
-
),
|
|
373
|
-
optionalJjRevision(
|
|
374
|
-
fs,
|
|
375
|
-
checkout,
|
|
376
|
-
`bookmarks(exact:"${jjExact(branch)}")`,
|
|
377
|
-
),
|
|
378
|
-
optionalJjRevision(
|
|
379
|
-
fs,
|
|
380
|
-
checkout,
|
|
381
|
-
`remote_bookmarks(exact:"${jjExact(branch)}", exact:"${jjExact(remote)}")`,
|
|
382
|
-
),
|
|
383
|
-
],
|
|
384
|
-
{ concurrency: "unbounded" },
|
|
385
|
-
)
|
|
386
|
-
if (!baseRevision) {
|
|
387
|
-
return yield* new PushError({
|
|
388
|
-
message: `Declared base '${base}' was not found on remote '${remote}'`,
|
|
389
|
-
})
|
|
390
|
-
}
|
|
391
|
-
if (
|
|
392
|
-
!(yield* jjAncestor(fs, checkout, baseRevision.commitId, tip.commitId))
|
|
393
|
-
) {
|
|
394
|
-
return yield* new PushError({
|
|
395
|
-
message: `Declared base '${base}' (${baseRevision.commitId}) is not an ancestor of jj tip ${tip.changeId} (${tip.commitId}). Rebase the stack with: jj rebase -s 'roots(${baseBookmark}..@)' -d ${baseBookmark}`,
|
|
396
|
-
})
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
onProgress?.("validate")
|
|
400
|
-
const outgoing = yield* jjCommits(
|
|
401
|
-
fs,
|
|
402
|
-
checkout,
|
|
403
|
-
`${baseRevision.commitId}..${tip.commitId}`,
|
|
404
|
-
)
|
|
405
|
-
yield* Effect.try({
|
|
406
|
-
try: () => validateJjCommits(outgoing, base),
|
|
407
|
-
catch: (cause) => cause as PushError,
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
const [localBookmarkIsAncestor, remoteBookmarkIsAncestor] =
|
|
411
|
-
yield* Effect.all(
|
|
412
|
-
[
|
|
413
|
-
localBookmark
|
|
414
|
-
? jjAncestor(fs, checkout, localBookmark.commitId, tip.commitId)
|
|
415
|
-
: Effect.succeed(true),
|
|
416
|
-
remoteBookmark
|
|
417
|
-
? jjAncestor(fs, checkout, remoteBookmark.commitId, tip.commitId)
|
|
418
|
-
: Effect.succeed(true),
|
|
419
|
-
],
|
|
420
|
-
{ concurrency: "unbounded" },
|
|
421
|
-
)
|
|
422
|
-
if (!localBookmarkIsAncestor) {
|
|
423
|
-
return yield* new PushError({
|
|
424
|
-
message: `Local bookmark '${branch}' is not an ancestor of jj tip ${tip.changeId}; refusing to move it`,
|
|
425
|
-
})
|
|
426
|
-
}
|
|
427
|
-
if (!remoteBookmarkIsAncestor) {
|
|
428
|
-
return yield* new PushError({
|
|
429
|
-
message: `Remote bookmark '${branch}@${remote}' is not an ancestor of jj tip ${tip.changeId}; refusing a non-fast-forward update`,
|
|
430
|
-
})
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
onProgress?.("publish")
|
|
434
|
-
yield* jj(
|
|
435
|
-
fs,
|
|
436
|
-
checkout,
|
|
437
|
-
["bookmark", "set", branch, "-r", tip.commitId],
|
|
438
|
-
`Failed to set declared bookmark '${branch}'`,
|
|
439
|
-
)
|
|
440
|
-
yield* jj(
|
|
441
|
-
fs,
|
|
442
|
-
checkout,
|
|
443
|
-
["git", "push", "--remote", remote, "--bookmark", branch],
|
|
444
|
-
`Failed to push declared bookmark '${branch}'`,
|
|
445
|
-
)
|
|
446
|
-
yield* jj(
|
|
447
|
-
fs,
|
|
448
|
-
checkout,
|
|
449
|
-
["bookmark", "track", `${branch}@${remote}`],
|
|
450
|
-
`Failed to track remote bookmark '${branch}@${remote}'`,
|
|
451
|
-
)
|
|
452
|
-
return { tip: tip.commitId, changeId: tip.changeId }
|
|
453
|
-
})
|
|
454
|
-
|
|
455
236
|
export class PushService extends Effect.Service<PushService>()("PushService", {
|
|
456
237
|
sync: () => ({
|
|
457
238
|
publish: (startPath: string = process.cwd(), options: PushOptions = {}) =>
|
|
@@ -534,26 +315,16 @@ export class PushService extends Effect.Service<PushService>()("PushService", {
|
|
|
534
315
|
const checkout = context.authority.writable.checkoutPath
|
|
535
316
|
const { config } = yield* workbase.loadConfig(context.workbase.root)
|
|
536
317
|
const remote = config.delivery?.remote ?? "origin"
|
|
537
|
-
const published =
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
options.onProgress,
|
|
546
|
-
)
|
|
547
|
-
: yield* publishGit(
|
|
548
|
-
fs,
|
|
549
|
-
checkout,
|
|
550
|
-
remote,
|
|
551
|
-
execution.branch,
|
|
552
|
-
execution.base,
|
|
553
|
-
options.onProgress,
|
|
554
|
-
)
|
|
318
|
+
const published = yield* publishGit(
|
|
319
|
+
fs,
|
|
320
|
+
checkout,
|
|
321
|
+
remote,
|
|
322
|
+
execution.branch,
|
|
323
|
+
execution.base,
|
|
324
|
+
options.onProgress,
|
|
325
|
+
)
|
|
555
326
|
return {
|
|
556
|
-
vcs:
|
|
327
|
+
vcs: "git",
|
|
557
328
|
taskId,
|
|
558
329
|
...(phaseId ? { phaseId } : {}),
|
|
559
330
|
branch: execution.branch,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { mkdir, realpath
|
|
3
|
+
import { mkdir, realpath } from "node:fs/promises"
|
|
4
4
|
import { dirname, join } from "node:path"
|
|
5
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
6
|
import { RepositoryService } from "./RepositoryService"
|
|
@@ -110,41 +110,6 @@ describe("RepositoryService", () => {
|
|
|
110
110
|
])
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
test("initializes a managed clone with jj for a jj workbase", async () => {
|
|
114
|
-
if (!Bun.which("jj")) return
|
|
115
|
-
await Bun.write(
|
|
116
|
-
join(root, "agency.json"),
|
|
117
|
-
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
118
|
-
)
|
|
119
|
-
const source = join(root, "source")
|
|
120
|
-
await runGit(["init", "--initial-branch=main", source])
|
|
121
|
-
await runGit(["-C", source, "config", "user.email", "test@example.com"])
|
|
122
|
-
await runGit(["-C", source, "config", "user.name", "Test"])
|
|
123
|
-
await Bun.write(join(source, "README.md"), "example\n")
|
|
124
|
-
await runGit(["-C", source, "add", "README.md"])
|
|
125
|
-
await runGit(["-C", source, "commit", "-m", "initial"])
|
|
126
|
-
await setPortableOrigin(source, "jj-agency")
|
|
127
|
-
|
|
128
|
-
const destination = await runTestEffect(
|
|
129
|
-
RepositoryService.pipe(
|
|
130
|
-
Effect.flatMap((service) => service.add("agency", source, root)),
|
|
131
|
-
),
|
|
132
|
-
)
|
|
133
|
-
expect((await stat(join(destination, ".jj"))).isDirectory()).toBe(true)
|
|
134
|
-
expect(await Bun.file(join(destination, ".git")).exists()).toBe(false)
|
|
135
|
-
expect(
|
|
136
|
-
await runTestEffect(
|
|
137
|
-
RepositoryService.pipe(
|
|
138
|
-
Effect.flatMap((service) => service.show("agency", root)),
|
|
139
|
-
),
|
|
140
|
-
),
|
|
141
|
-
).toMatchObject({
|
|
142
|
-
kind: "repository",
|
|
143
|
-
remote: portableRemote("jj-agency"),
|
|
144
|
-
states: ["declared", "materialized"],
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
113
|
test("links an existing repository", async () => {
|
|
149
114
|
const target = join(root, "linked-repository")
|
|
150
115
|
await mkdir(target, { recursive: true })
|
|
@@ -380,31 +345,6 @@ status: working
|
|
|
380
345
|
).not.toContain(linkedWorktree)
|
|
381
346
|
})
|
|
382
347
|
|
|
383
|
-
test("refuses to materialize linked aliases in jj workbases", async () => {
|
|
384
|
-
if (!Bun.which("jj")) return
|
|
385
|
-
await Bun.write(
|
|
386
|
-
join(root, "agency.json"),
|
|
387
|
-
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
388
|
-
)
|
|
389
|
-
const target = join(root, "linked-jj-repository")
|
|
390
|
-
await mkdir(target, { recursive: true })
|
|
391
|
-
await runGit(["init", "--initial-branch=main", target])
|
|
392
|
-
await setPortableOrigin(target, "linked-jj")
|
|
393
|
-
await runTestEffect(
|
|
394
|
-
RepositoryService.pipe(
|
|
395
|
-
Effect.flatMap((service) => service.link("linked", target, root)),
|
|
396
|
-
),
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
await expect(
|
|
400
|
-
runTestEffect(
|
|
401
|
-
RepositoryService.pipe(
|
|
402
|
-
Effect.flatMap((service) => service.materialize("linked", root)),
|
|
403
|
-
),
|
|
404
|
-
),
|
|
405
|
-
).rejects.toThrow("only supported for Git workbases")
|
|
406
|
-
})
|
|
407
|
-
|
|
408
348
|
test("rejects invalid and duplicate aliases", async () => {
|
|
409
349
|
const source = join(root, "source.git")
|
|
410
350
|
await runGit(["init", "--bare", "--initial-branch=main", source])
|
|
@@ -471,15 +471,9 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
471
471
|
})
|
|
472
472
|
}
|
|
473
473
|
const inspection = yield* backend.inspectRepository(resolvedTarget)
|
|
474
|
-
if (!inspection
|
|
474
|
+
if (!inspection) {
|
|
475
475
|
return yield* new RepositoryError({
|
|
476
|
-
message: `Path is not a
|
|
477
|
-
})
|
|
478
|
-
}
|
|
479
|
-
yield* backend.initializeRepository(resolvedTarget)
|
|
480
|
-
if (!(yield* backend.inspectRepository(resolvedTarget))) {
|
|
481
|
-
return yield* new RepositoryError({
|
|
482
|
-
message: `Path is not a ${backend.kind} repository: ${resolvedTarget}`,
|
|
476
|
+
message: `Path is not a Git repository: ${resolvedTarget}`,
|
|
483
477
|
})
|
|
484
478
|
}
|
|
485
479
|
const declaredRemote =
|
|
@@ -551,11 +545,6 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
551
545
|
|
|
552
546
|
const state = yield* configState(startPath)
|
|
553
547
|
const backend = yield* versionControl.forWorkbase(state.root)
|
|
554
|
-
if (backend.kind !== "git") {
|
|
555
|
-
return yield* new RepositoryError({
|
|
556
|
-
message: `Repository alias materialization is only supported for Git workbases`,
|
|
557
|
-
})
|
|
558
|
-
}
|
|
559
548
|
|
|
560
549
|
const source = yield* fs.realPath(repository.path)
|
|
561
550
|
const registered = yield* backend.listWorkspaces(repository.path)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { mkdir
|
|
3
|
+
import { mkdir } from "node:fs/promises"
|
|
4
4
|
import { join } from "node:path"
|
|
5
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
6
|
import { TaskService } from "./TaskService"
|
|
@@ -13,7 +13,6 @@ import { PhaseService } from "./PhaseService"
|
|
|
13
13
|
import { ArchiveService } from "./ArchiveService"
|
|
14
14
|
import { ClaimService } from "./ClaimService"
|
|
15
15
|
import { SyncService } from "./SyncService"
|
|
16
|
-
import { DoctorService } from "./DoctorService"
|
|
17
16
|
import { task as taskCommand } from "../commands/task"
|
|
18
17
|
|
|
19
18
|
const git = async (args: string[], cwd?: string) => {
|
|
@@ -27,16 +26,6 @@ const git = async (args: string[], cwd?: string) => {
|
|
|
27
26
|
throw new Error(await new Response(child.stderr).text())
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
const jj = async (args: string[]) => {
|
|
31
|
-
const child = Bun.spawn(["jj", ...args], {
|
|
32
|
-
stdout: "pipe",
|
|
33
|
-
stderr: "pipe",
|
|
34
|
-
})
|
|
35
|
-
await child.exited
|
|
36
|
-
if (child.exitCode !== 0)
|
|
37
|
-
throw new Error(await new Response(child.stderr).text())
|
|
38
|
-
}
|
|
39
|
-
|
|
40
29
|
describe("ReviewService", () => {
|
|
41
30
|
let root: string
|
|
42
31
|
let source: string
|
|
@@ -162,78 +151,6 @@ describe("ReviewService", () => {
|
|
|
162
151
|
expect(new TextDecoder().decode(oldPins.stdout).trim()).toBe("")
|
|
163
152
|
})
|
|
164
153
|
|
|
165
|
-
test("imports jj reviews for creation, diagnostics, materialization, and refresh", async () => {
|
|
166
|
-
if (!Bun.which("jj")) return
|
|
167
|
-
const repository = join(root, "repos/agency")
|
|
168
|
-
await rm(repository, { recursive: true, force: true })
|
|
169
|
-
await git(["switch", "-c", "jj-review"], source)
|
|
170
|
-
await Bun.write(join(source, "README.md"), "jj review one\n")
|
|
171
|
-
await git(["commit", "-am", "jj review one"], source)
|
|
172
|
-
await jj(["git", "clone", "--no-colocate", source, repository])
|
|
173
|
-
expect(await Bun.file(join(repository, ".git")).exists()).toBe(false)
|
|
174
|
-
await Bun.write(
|
|
175
|
-
join(root, "agency.json"),
|
|
176
|
-
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
await runTestEffect(
|
|
180
|
-
taskCommand({
|
|
181
|
-
subcommand: "create",
|
|
182
|
-
args: ["jj-review"],
|
|
183
|
-
review: "agency",
|
|
184
|
-
ref: "jj-review",
|
|
185
|
-
cwd: root,
|
|
186
|
-
silent: true,
|
|
187
|
-
}),
|
|
188
|
-
)
|
|
189
|
-
const created = await runTestEffect(
|
|
190
|
-
TaskService.pipe(
|
|
191
|
-
Effect.flatMap((service) => service.show("jj-review", root)),
|
|
192
|
-
),
|
|
193
|
-
)
|
|
194
|
-
const original = "review" in created.data ? created.data.review.commit : ""
|
|
195
|
-
|
|
196
|
-
const context = await runTestEffect(
|
|
197
|
-
ContextService.pipe(
|
|
198
|
-
Effect.flatMap((service) =>
|
|
199
|
-
service.get({ target: "jj-review", cwd: root }),
|
|
200
|
-
),
|
|
201
|
-
),
|
|
202
|
-
)
|
|
203
|
-
expect(context.workspace!.warnings).toEqual([])
|
|
204
|
-
expect(context.review?.checkout?.resolvedCommit).toBe(original)
|
|
205
|
-
|
|
206
|
-
const doctor = await runTestEffect(
|
|
207
|
-
DoctorService.pipe(Effect.flatMap((service) => service.inspect(root))),
|
|
208
|
-
)
|
|
209
|
-
expect(
|
|
210
|
-
doctor.checks.find((check) => check.id === `ref.agency.${original}`),
|
|
211
|
-
).toMatchObject({ status: "pass" })
|
|
212
|
-
|
|
213
|
-
const workspace = await runTestEffect(
|
|
214
|
-
WorktreeService.pipe(
|
|
215
|
-
Effect.flatMap((service) =>
|
|
216
|
-
service.materialize("jj-review", undefined, root),
|
|
217
|
-
),
|
|
218
|
-
),
|
|
219
|
-
)
|
|
220
|
-
expect(
|
|
221
|
-
await Bun.file(join(workspace.reviewPath!, "README.md")).text(),
|
|
222
|
-
).toBe("jj review one\n")
|
|
223
|
-
|
|
224
|
-
await Bun.write(join(source, "README.md"), "jj review two\n")
|
|
225
|
-
await git(["commit", "-am", "jj review two"], source)
|
|
226
|
-
const refreshed = await runTestEffect(
|
|
227
|
-
ReviewService.pipe(
|
|
228
|
-
Effect.flatMap((service) => service.refresh("jj-review", root)),
|
|
229
|
-
),
|
|
230
|
-
)
|
|
231
|
-
expect(refreshed.changed).toBe(true)
|
|
232
|
-
expect(
|
|
233
|
-
await Bun.file(join(workspace.reviewPath!, "README.md")).text(),
|
|
234
|
-
).toBe("jj review two\n")
|
|
235
|
-
})
|
|
236
|
-
|
|
237
154
|
test("rejects delivery and phase operations even when forced", async () => {
|
|
238
155
|
await createReview()
|
|
239
156
|
await expect(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Data, Effect,
|
|
1
|
+
import { Data, Effect, Layer } from "effect"
|
|
2
2
|
import { randomUUID } from "node:crypto"
|
|
3
3
|
import { lstat, mkdir } from "node:fs/promises"
|
|
4
4
|
import { dirname } from "node:path"
|
|
@@ -13,9 +13,7 @@ import {
|
|
|
13
13
|
} from "./WorktreeService"
|
|
14
14
|
import {
|
|
15
15
|
GitVersionControlService,
|
|
16
|
-
JjVersionControlService,
|
|
17
16
|
VersionControlService,
|
|
18
|
-
type VersionControlBackend,
|
|
19
17
|
} from "./VersionControlService"
|
|
20
18
|
import { withWorktreeLocks } from "./WorktreeLock"
|
|
21
19
|
import {
|
|
@@ -70,7 +68,6 @@ const WorktreeLayer = Layer.mergeAll(
|
|
|
70
68
|
FileSystemService.Default,
|
|
71
69
|
WorkbaseService.Default,
|
|
72
70
|
GitVersionControlService.Default,
|
|
73
|
-
JjVersionControlService.Default,
|
|
74
71
|
VersionControlService.Default,
|
|
75
72
|
TaskService.Default,
|
|
76
73
|
PhaseService.Default,
|
|
@@ -91,22 +88,6 @@ const restoreSnapshots = async (
|
|
|
91
88
|
continue
|
|
92
89
|
} catch {}
|
|
93
90
|
await mkdir(dirname(snapshot.path), { recursive: true })
|
|
94
|
-
if (snapshot.vcs === "jj") {
|
|
95
|
-
await runGit([
|
|
96
|
-
"jj",
|
|
97
|
-
"-R",
|
|
98
|
-
snapshot.repositoryPath,
|
|
99
|
-
"workspace",
|
|
100
|
-
"add",
|
|
101
|
-
"--name",
|
|
102
|
-
snapshot.workspaceName!,
|
|
103
|
-
"-r",
|
|
104
|
-
snapshot.head,
|
|
105
|
-
snapshot.path,
|
|
106
|
-
])
|
|
107
|
-
await runGit(["jj", "-R", snapshot.path, "edit", snapshot.head])
|
|
108
|
-
continue
|
|
109
|
-
}
|
|
110
91
|
await runGit(
|
|
111
92
|
snapshot.branch
|
|
112
93
|
? [
|
|
@@ -172,14 +153,9 @@ const normalizeBranch = (input: string) =>
|
|
|
172
153
|
return `refs/heads/${name}`
|
|
173
154
|
})
|
|
174
155
|
|
|
175
|
-
const fetchCommit = (
|
|
176
|
-
repoPath: string,
|
|
177
|
-
sourceRef: string,
|
|
178
|
-
backend: VersionControlBackend,
|
|
179
|
-
) =>
|
|
156
|
+
const fetchCommit = (repoPath: string, sourceRef: string) =>
|
|
180
157
|
Effect.gen(function* () {
|
|
181
158
|
const fs = yield* FileSystemService
|
|
182
|
-
const environment = yield* backend.gitEnvironment(repoPath)
|
|
183
159
|
const temporaryRef = `refs/agency/review-fetch/${process.pid}-${randomUUID()}`
|
|
184
160
|
const fetched = yield* fs.runCommand(
|
|
185
161
|
[
|
|
@@ -191,12 +167,12 @@ const fetchCommit = (
|
|
|
191
167
|
"origin",
|
|
192
168
|
`+${sourceRef}:${temporaryRef}`,
|
|
193
169
|
],
|
|
194
|
-
{ captureOutput: true
|
|
170
|
+
{ captureOutput: true },
|
|
195
171
|
)
|
|
196
172
|
if (fetched.exitCode !== 0) {
|
|
197
173
|
const cleanup = yield* fs.runCommand(
|
|
198
174
|
["git", "-C", repoPath, "update-ref", "-d", temporaryRef],
|
|
199
|
-
{ captureOutput: true
|
|
175
|
+
{ captureOutput: true },
|
|
200
176
|
)
|
|
201
177
|
return yield* new ReviewError({
|
|
202
178
|
message: `Review source '${sourceRef}' could not be fetched: ${fetched.stderr.trim()}${cleanup.exitCode === 0 ? "" : `; temporary ref cleanup failed: ${cleanup.stderr.trim()}`}`,
|
|
@@ -211,34 +187,27 @@ const fetchCommit = (
|
|
|
211
187
|
"--verify",
|
|
212
188
|
`${temporaryRef}^{commit}`,
|
|
213
189
|
],
|
|
214
|
-
{ captureOutput: true
|
|
190
|
+
{ captureOutput: true },
|
|
215
191
|
)
|
|
216
192
|
const commit = resolved.stdout.trim()
|
|
217
193
|
if (resolved.exitCode !== 0 || !/^[a-f0-9]{40}$/.test(commit)) {
|
|
218
194
|
yield* fs.runCommand(
|
|
219
195
|
["git", "-C", repoPath, "update-ref", "-d", temporaryRef],
|
|
220
|
-
{ captureOutput: true
|
|
196
|
+
{ captureOutput: true },
|
|
221
197
|
)
|
|
222
198
|
return yield* new ReviewError({
|
|
223
199
|
message: `Review source '${sourceRef}' did not resolve to a commit`,
|
|
224
200
|
})
|
|
225
201
|
}
|
|
226
|
-
const imported = yield* Effect.either(backend.importGitRefs(repoPath))
|
|
227
202
|
const cleanup = yield* fs.runCommand(
|
|
228
203
|
["git", "-C", repoPath, "update-ref", "-d", temporaryRef],
|
|
229
|
-
{ captureOutput: true
|
|
204
|
+
{ captureOutput: true },
|
|
230
205
|
)
|
|
231
206
|
if (cleanup.exitCode !== 0) {
|
|
232
207
|
return yield* new ReviewError({
|
|
233
208
|
message: `Failed to remove temporary review fetch ref: ${cleanup.stderr.trim()}`,
|
|
234
209
|
})
|
|
235
210
|
}
|
|
236
|
-
if (Either.isLeft(imported)) {
|
|
237
|
-
return yield* new ReviewError({
|
|
238
|
-
message: `Review source '${sourceRef}' was fetched but could not be imported into ${backend.kind}`,
|
|
239
|
-
cause: imported.left,
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
211
|
return commit
|
|
243
212
|
})
|
|
244
213
|
|
|
@@ -253,8 +222,6 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
253
222
|
) =>
|
|
254
223
|
Effect.gen(function* () {
|
|
255
224
|
const repositories = yield* RepositoryService
|
|
256
|
-
const versionControl = yield* VersionControlService
|
|
257
|
-
const backend = yield* versionControl.forWorkbase(startPath)
|
|
258
225
|
const repository = yield* repositories.show(repo, startPath)
|
|
259
226
|
if (!repository.remote || repository.states.includes("missing")) {
|
|
260
227
|
return yield* new ReviewError({
|
|
@@ -303,7 +270,7 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
303
270
|
message: "Exactly one review source is required",
|
|
304
271
|
})
|
|
305
272
|
}
|
|
306
|
-
const commit = yield* fetchCommit(repository.path, sourceRef
|
|
273
|
+
const commit = yield* fetchCommit(repository.path, sourceRef)
|
|
307
274
|
return {
|
|
308
275
|
repo,
|
|
309
276
|
source,
|
|
@@ -371,8 +338,6 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
371
338
|
message: `Repository alias '${task.data.review.repo}' must be materialized with an origin remote`,
|
|
372
339
|
})
|
|
373
340
|
}
|
|
374
|
-
const versionControl = yield* VersionControlService
|
|
375
|
-
const backend = yield* versionControl.forWorkbase(root)
|
|
376
341
|
const latest = {
|
|
377
342
|
...task.data.review,
|
|
378
343
|
commit: yield* fetchCommit(
|
|
@@ -380,7 +345,6 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
380
345
|
task.data.review.source.kind === "pull-request"
|
|
381
346
|
? task.data.review.source.fetchRef
|
|
382
347
|
: task.data.review.source.ref,
|
|
383
|
-
backend,
|
|
384
348
|
),
|
|
385
349
|
refreshedAt: new Date().toISOString(),
|
|
386
350
|
} satisfies ReviewRecord
|
|
@@ -389,9 +353,7 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
389
353
|
{ ...task.data, review: latest },
|
|
390
354
|
parsed.body,
|
|
391
355
|
)
|
|
392
|
-
const gitEnvironment =
|
|
393
|
-
repository.path,
|
|
394
|
-
)
|
|
356
|
+
const gitEnvironment: Record<string, string> = {}
|
|
395
357
|
const hadCheckout = inspection.checkouts.some(
|
|
396
358
|
(checkout) => checkout.exists || checkout.registered,
|
|
397
359
|
)
|
|
@@ -405,7 +367,6 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
405
367
|
worktrees.remove(taskId, undefined, root, {
|
|
406
368
|
snapshots,
|
|
407
369
|
lockHeld: true,
|
|
408
|
-
persistResume: false,
|
|
409
370
|
}),
|
|
410
371
|
).then(() => undefined),
|
|
411
372
|
rollback: () => restoreSnapshots(snapshots),
|