@markjaquith/agency 2.56.1 → 2.58.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 -9
- 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/pr.test.ts +40 -1
- package/src/commands/pr.ts +6 -0
- package/src/commands/restore.test.ts +8 -0
- package/src/services/ArchiveBulkService.test.ts +485 -0
- package/src/services/ArchiveService.test.ts +164 -1
- package/src/services/ArchiveService.ts +447 -29
- package/src/services/ContextService.ts +27 -10
- package/src/services/DoctorService.ts +6 -11
- package/src/services/GraphService.ts +60 -102
- package/src/services/PullRequestService.test.ts +45 -1
- package/src/services/PullRequestService.ts +18 -1
- package/src/services/PushService.test.ts +10 -13
- package/src/services/RepositoryService.test.ts +6 -1
- package/src/services/RepositoryService.ts +57 -114
- package/src/services/ReviewService.test.ts +2 -2
- package/src/services/ReviewService.ts +47 -28
- package/src/services/SyncService.test.ts +17 -2
- package/src/services/SyncService.ts +41 -18
- package/src/services/TaskService.ts +18 -2
- package/src/services/VcsMigrationService.test.ts +58 -1
- package/src/services/VcsMigrationService.ts +9 -0
- package/src/services/VersionControlService.test.ts +38 -0
- package/src/services/VersionControlService.ts +211 -2
- package/src/services/WorktreeService.test.ts +4 -0
- package/src/services/WorktreeService.ts +1 -5
- package/src/vcs-status-fast.ts +3 -8
- package/src/workbase/delivery-command.test.ts +11 -2
- package/src/workbase/delivery-command.ts +5 -1
|
@@ -166,11 +166,11 @@ describe("ReviewService", () => {
|
|
|
166
166
|
if (!Bun.which("jj")) return
|
|
167
167
|
const repository = join(root, "repos/agency")
|
|
168
168
|
await rm(repository, { recursive: true, force: true })
|
|
169
|
-
await git(["clone", source, repository])
|
|
170
169
|
await git(["switch", "-c", "jj-review"], source)
|
|
171
170
|
await Bun.write(join(source, "README.md"), "jj review one\n")
|
|
172
171
|
await git(["commit", "-am", "jj review one"], source)
|
|
173
|
-
await jj(["git", "
|
|
172
|
+
await jj(["git", "clone", "--no-colocate", source, repository])
|
|
173
|
+
expect(await Bun.file(join(repository, ".git")).exists()).toBe(false)
|
|
174
174
|
await Bun.write(
|
|
175
175
|
join(root, "agency.json"),
|
|
176
176
|
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
@@ -45,8 +45,15 @@ const githubRepository = (remote: string) => {
|
|
|
45
45
|
const pinRef = (taskId: string) =>
|
|
46
46
|
`refs/agency/reviews/${Buffer.from(taskId).toString("hex")}`
|
|
47
47
|
|
|
48
|
-
const runGit = async (
|
|
49
|
-
|
|
48
|
+
const runGit = async (
|
|
49
|
+
args: readonly string[],
|
|
50
|
+
environment: Record<string, string> = {},
|
|
51
|
+
) => {
|
|
52
|
+
const child = Bun.spawn([...args], {
|
|
53
|
+
stdout: "pipe",
|
|
54
|
+
stderr: "pipe",
|
|
55
|
+
env: { ...process.env, ...environment },
|
|
56
|
+
})
|
|
50
57
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
51
58
|
child.exited,
|
|
52
59
|
new Response(child.stdout).text(),
|
|
@@ -121,7 +128,7 @@ const restoreSnapshots = async (
|
|
|
121
128
|
}
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
const normalizeBranch = (input: string
|
|
131
|
+
const normalizeBranch = (input: string) =>
|
|
125
132
|
Effect.gen(function* () {
|
|
126
133
|
const fs = yield* FileSystemService
|
|
127
134
|
if (
|
|
@@ -150,7 +157,7 @@ const normalizeBranch = (input: string, repositoryPath: string) =>
|
|
|
150
157
|
})
|
|
151
158
|
}
|
|
152
159
|
const checked = yield* fs.runCommand(
|
|
153
|
-
["git", "
|
|
160
|
+
["git", "check-ref-format", "--branch", name],
|
|
154
161
|
{ captureOutput: true },
|
|
155
162
|
)
|
|
156
163
|
if (checked.exitCode !== 0) {
|
|
@@ -168,6 +175,7 @@ const fetchCommit = (
|
|
|
168
175
|
) =>
|
|
169
176
|
Effect.gen(function* () {
|
|
170
177
|
const fs = yield* FileSystemService
|
|
178
|
+
const environment = yield* backend.gitEnvironment(repoPath)
|
|
171
179
|
const temporaryRef = `refs/agency/review-fetch/${process.pid}-${randomUUID()}`
|
|
172
180
|
const fetched = yield* fs.runCommand(
|
|
173
181
|
[
|
|
@@ -179,12 +187,12 @@ const fetchCommit = (
|
|
|
179
187
|
"origin",
|
|
180
188
|
`+${sourceRef}:${temporaryRef}`,
|
|
181
189
|
],
|
|
182
|
-
{ captureOutput: true },
|
|
190
|
+
{ captureOutput: true, env: environment },
|
|
183
191
|
)
|
|
184
192
|
if (fetched.exitCode !== 0) {
|
|
185
193
|
const cleanup = yield* fs.runCommand(
|
|
186
194
|
["git", "-C", repoPath, "update-ref", "-d", temporaryRef],
|
|
187
|
-
{ captureOutput: true },
|
|
195
|
+
{ captureOutput: true, env: environment },
|
|
188
196
|
)
|
|
189
197
|
return yield* new ReviewError({
|
|
190
198
|
message: `Review source '${sourceRef}' could not be fetched: ${fetched.stderr.trim()}${cleanup.exitCode === 0 ? "" : `; temporary ref cleanup failed: ${cleanup.stderr.trim()}`}`,
|
|
@@ -199,13 +207,13 @@ const fetchCommit = (
|
|
|
199
207
|
"--verify",
|
|
200
208
|
`${temporaryRef}^{commit}`,
|
|
201
209
|
],
|
|
202
|
-
{ captureOutput: true },
|
|
210
|
+
{ captureOutput: true, env: environment },
|
|
203
211
|
)
|
|
204
212
|
const commit = resolved.stdout.trim()
|
|
205
213
|
if (resolved.exitCode !== 0 || !/^[a-f0-9]{40}$/.test(commit)) {
|
|
206
214
|
yield* fs.runCommand(
|
|
207
215
|
["git", "-C", repoPath, "update-ref", "-d", temporaryRef],
|
|
208
|
-
{ captureOutput: true },
|
|
216
|
+
{ captureOutput: true, env: environment },
|
|
209
217
|
)
|
|
210
218
|
return yield* new ReviewError({
|
|
211
219
|
message: `Review source '${sourceRef}' did not resolve to a commit`,
|
|
@@ -214,7 +222,7 @@ const fetchCommit = (
|
|
|
214
222
|
const imported = yield* Effect.either(backend.importGitRefs(repoPath))
|
|
215
223
|
const cleanup = yield* fs.runCommand(
|
|
216
224
|
["git", "-C", repoPath, "update-ref", "-d", temporaryRef],
|
|
217
|
-
{ captureOutput: true },
|
|
225
|
+
{ captureOutput: true, env: environment },
|
|
218
226
|
)
|
|
219
227
|
if (cleanup.exitCode !== 0) {
|
|
220
228
|
return yield* new ReviewError({
|
|
@@ -284,7 +292,7 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
284
292
|
fetchRef: sourceRef,
|
|
285
293
|
}
|
|
286
294
|
} else if (input.ref) {
|
|
287
|
-
sourceRef = yield* normalizeBranch(input.ref
|
|
295
|
+
sourceRef = yield* normalizeBranch(input.ref)
|
|
288
296
|
source = { kind: "branch", ref: sourceRef }
|
|
289
297
|
} else {
|
|
290
298
|
return yield* new ReviewError({
|
|
@@ -366,6 +374,11 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
366
374
|
task.data.review.repo,
|
|
367
375
|
root,
|
|
368
376
|
)
|
|
377
|
+
const versionControl = yield* VersionControlService
|
|
378
|
+
const backend = yield* versionControl.forWorkbase(root)
|
|
379
|
+
const gitEnvironment = yield* backend.gitEnvironment(
|
|
380
|
+
repository.path,
|
|
381
|
+
)
|
|
369
382
|
const hadCheckout = inspection.checkouts.some(
|
|
370
383
|
(checkout) => checkout.exists || checkout.registered,
|
|
371
384
|
)
|
|
@@ -391,25 +404,31 @@ export class ReviewService extends Effect.Service<ReviewService>()(
|
|
|
391
404
|
steps.push({
|
|
392
405
|
label: `advance review pin for ${taskId}`,
|
|
393
406
|
apply: () =>
|
|
394
|
-
runGit(
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
407
|
+
runGit(
|
|
408
|
+
[
|
|
409
|
+
"git",
|
|
410
|
+
"-C",
|
|
411
|
+
repository.path,
|
|
412
|
+
"update-ref",
|
|
413
|
+
pinRef(taskId),
|
|
414
|
+
latest.commit,
|
|
415
|
+
previousReview.commit,
|
|
416
|
+
],
|
|
417
|
+
gitEnvironment,
|
|
418
|
+
).then(() => undefined),
|
|
403
419
|
rollback: () =>
|
|
404
|
-
runGit(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
420
|
+
runGit(
|
|
421
|
+
[
|
|
422
|
+
"git",
|
|
423
|
+
"-C",
|
|
424
|
+
repository.path,
|
|
425
|
+
"update-ref",
|
|
426
|
+
pinRef(taskId),
|
|
427
|
+
previousReview.commit,
|
|
428
|
+
latest.commit,
|
|
429
|
+
],
|
|
430
|
+
gitEnvironment,
|
|
431
|
+
).then(() => undefined),
|
|
413
432
|
manualRecovery: `Reset ${pinRef(taskId)} to ${previousReview.commit}`,
|
|
414
433
|
})
|
|
415
434
|
if (hadCheckout) {
|
|
@@ -59,6 +59,7 @@ describe("SyncService", () => {
|
|
|
59
59
|
await Bun.write(
|
|
60
60
|
gh,
|
|
61
61
|
`#!/bin/sh
|
|
62
|
+
if [ -n "$GH_CAPTURE" ]; then printf '%s\n' "$@" "GIT_DIR=$GIT_DIR" > "$GH_CAPTURE"; fi
|
|
62
63
|
case "$*" in
|
|
63
64
|
*mergeable*) ;;
|
|
64
65
|
*) echo "mergeable field was not requested" >&2; exit 2 ;;
|
|
@@ -82,6 +83,7 @@ JSON
|
|
|
82
83
|
afterEach(async () => {
|
|
83
84
|
if (originalPath === undefined) delete process.env.PATH
|
|
84
85
|
else process.env.PATH = originalPath
|
|
86
|
+
delete process.env.GH_CAPTURE
|
|
85
87
|
await cleanupTempDir(root)
|
|
86
88
|
})
|
|
87
89
|
|
|
@@ -125,8 +127,14 @@ pr: null
|
|
|
125
127
|
if (!Bun.which("jj")) return
|
|
126
128
|
const repository = join(root, "repos/agency")
|
|
127
129
|
await rm(repository, { recursive: true, force: true })
|
|
128
|
-
await
|
|
129
|
-
|
|
130
|
+
await jj([
|
|
131
|
+
"git",
|
|
132
|
+
"clone",
|
|
133
|
+
"--no-colocate",
|
|
134
|
+
join(root, "source"),
|
|
135
|
+
repository,
|
|
136
|
+
])
|
|
137
|
+
expect(await Bun.file(join(repository, ".git")).exists()).toBe(false)
|
|
130
138
|
await Bun.write(
|
|
131
139
|
join(root, "agency.json"),
|
|
132
140
|
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
@@ -147,6 +155,8 @@ pr: null
|
|
|
147
155
|
),
|
|
148
156
|
),
|
|
149
157
|
)
|
|
158
|
+
const capture = join(root, "gh-capture")
|
|
159
|
+
process.env.GH_CAPTURE = capture
|
|
150
160
|
|
|
151
161
|
const planned = await runTestEffect(
|
|
152
162
|
SyncService.pipe(
|
|
@@ -160,6 +170,10 @@ pr: null
|
|
|
160
170
|
status: "planned",
|
|
161
171
|
}),
|
|
162
172
|
)
|
|
173
|
+
const invocation = await Bun.file(capture).text()
|
|
174
|
+
expect(invocation).toContain("--repo\n")
|
|
175
|
+
expect(invocation).toContain("GIT_DIR=")
|
|
176
|
+
expect(invocation).toContain(".jj")
|
|
163
177
|
|
|
164
178
|
const applied = await runTestEffect(
|
|
165
179
|
SyncService.pipe(
|
|
@@ -174,6 +188,7 @@ pr: null
|
|
|
174
188
|
branch: "task/jj-sync",
|
|
175
189
|
dirty: false,
|
|
176
190
|
})
|
|
191
|
+
delete process.env.GH_CAPTURE
|
|
177
192
|
})
|
|
178
193
|
|
|
179
194
|
test("observes drift without mutation and applies only safe transitions", async () => {
|
|
@@ -339,20 +339,34 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
339
339
|
env: resolved.environment,
|
|
340
340
|
})
|
|
341
341
|
} else if (!config.delivery && existing) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
342
|
+
const environment = yield* Effect.either(
|
|
343
|
+
backend.gitEnvironment(repositoryPath),
|
|
344
|
+
)
|
|
345
|
+
result = yield* runExternal(
|
|
346
|
+
[
|
|
347
|
+
"gh",
|
|
348
|
+
"pr",
|
|
349
|
+
"view",
|
|
350
|
+
existing.url,
|
|
351
|
+
"--json",
|
|
352
|
+
"number,state,title,isDraft,headRefName,baseRefName,url,mergedAt,mergeCommit,mergeable",
|
|
353
|
+
],
|
|
354
|
+
{
|
|
355
|
+
cwd: repositoryPath,
|
|
356
|
+
env: Either.isRight(environment) ? environment.right : {},
|
|
357
|
+
},
|
|
358
|
+
)
|
|
350
359
|
} else if (!config.delivery) {
|
|
360
|
+
const environment = yield* Effect.either(
|
|
361
|
+
backend.gitEnvironment(repositoryPath),
|
|
362
|
+
)
|
|
351
363
|
result = yield* runExternal(
|
|
352
364
|
[
|
|
353
365
|
"gh",
|
|
354
366
|
"pr",
|
|
355
367
|
"list",
|
|
368
|
+
"--repo",
|
|
369
|
+
remoteRepository,
|
|
356
370
|
"--head",
|
|
357
371
|
data.branch,
|
|
358
372
|
"--state",
|
|
@@ -360,7 +374,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
360
374
|
"--json",
|
|
361
375
|
"number,state,title,isDraft,headRefName,baseRefName,url,mergedAt,mergeCommit,mergeable",
|
|
362
376
|
],
|
|
363
|
-
{
|
|
377
|
+
{
|
|
378
|
+
cwd: repositoryPath,
|
|
379
|
+
env: Either.isRight(environment) ? environment.right : {},
|
|
380
|
+
},
|
|
364
381
|
)
|
|
365
382
|
}
|
|
366
383
|
return [
|
|
@@ -531,14 +548,18 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
531
548
|
let resolvedCommit: string | null = null
|
|
532
549
|
if ("ref" in checkout) {
|
|
533
550
|
if (!isCommitId(checkout.ref)) {
|
|
534
|
-
const
|
|
535
|
-
"git",
|
|
536
|
-
"-C",
|
|
551
|
+
const remote = yield* backend.remoteUrl(
|
|
537
552
|
repositoryPath,
|
|
538
|
-
"ls-remote",
|
|
539
553
|
"origin",
|
|
540
|
-
|
|
541
|
-
|
|
554
|
+
)
|
|
555
|
+
const remoteRef = remote
|
|
556
|
+
? yield* runExternal([
|
|
557
|
+
"git",
|
|
558
|
+
"ls-remote",
|
|
559
|
+
remote,
|
|
560
|
+
originRef(checkout.ref),
|
|
561
|
+
])
|
|
562
|
+
: { exitCode: -1, stdout: "", stderr: "origin unavailable" }
|
|
542
563
|
resolvedCommit =
|
|
543
564
|
remoteRef.stdout.match(/^([0-9a-f]{40,64})\s/m)?.[1] ?? null
|
|
544
565
|
if (!resolvedCommit) {
|
|
@@ -998,12 +1019,14 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
998
1019
|
})
|
|
999
1020
|
}
|
|
1000
1021
|
const repositoryPath = join(root, "repos", data.review.repo)
|
|
1022
|
+
const reviewRemote = yield* backend.remoteUrl(
|
|
1023
|
+
repositoryPath,
|
|
1024
|
+
"origin",
|
|
1025
|
+
)
|
|
1001
1026
|
const source = yield* runExternal([
|
|
1002
1027
|
"git",
|
|
1003
|
-
"-C",
|
|
1004
|
-
repositoryPath,
|
|
1005
1028
|
"ls-remote",
|
|
1006
|
-
"origin",
|
|
1029
|
+
reviewRemote ?? "origin",
|
|
1007
1030
|
data.review.source.kind === "pull-request"
|
|
1008
1031
|
? data.review.source.fetchRef
|
|
1009
1032
|
: originRef(data.review.source.ref),
|
|
@@ -3,6 +3,7 @@ import { Data, Effect, Either } from "effect"
|
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { FileSystemService } from "./FileSystemService"
|
|
5
5
|
import { WorkbaseService } from "./WorkbaseService"
|
|
6
|
+
import { VersionControlService } from "./VersionControlService"
|
|
6
7
|
import { EpicService, type EpicRecord } from "./EpicService"
|
|
7
8
|
import {
|
|
8
9
|
EntityId,
|
|
@@ -88,11 +89,16 @@ const reviewPinStep = (
|
|
|
88
89
|
root: string,
|
|
89
90
|
taskId: string,
|
|
90
91
|
review: ReviewRecord,
|
|
92
|
+
environment: Record<string, string>,
|
|
91
93
|
): TransactionStep => {
|
|
92
94
|
const repositoryPath = join(root, "repos", review.repo)
|
|
93
95
|
const ref = reviewPinRef(taskId)
|
|
94
96
|
const run = async (args: readonly string[]) => {
|
|
95
|
-
const child = Bun.spawn([...args], {
|
|
97
|
+
const child = Bun.spawn([...args], {
|
|
98
|
+
stdout: "pipe",
|
|
99
|
+
stderr: "pipe",
|
|
100
|
+
env: { ...process.env, ...environment },
|
|
101
|
+
})
|
|
96
102
|
const [exitCode, stderr] = await Promise.all([
|
|
97
103
|
child.exited,
|
|
98
104
|
new Response(child.stderr).text(),
|
|
@@ -132,6 +138,7 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
132
138
|
const fs = yield* FileSystemService
|
|
133
139
|
const workbase = yield* WorkbaseService
|
|
134
140
|
const epics = yield* EpicService
|
|
141
|
+
const versionControl = yield* VersionControlService
|
|
135
142
|
const root = yield* workbase.discover(startPath)
|
|
136
143
|
const id = yield* decodeId(input.id)
|
|
137
144
|
const directory = join(root, "tasks", id)
|
|
@@ -257,13 +264,22 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
|
|
|
257
264
|
const updated = formatMarkdownDocument(epicData, parsed.body)
|
|
258
265
|
writes.push({ path: parentEpic.path, content: updated })
|
|
259
266
|
}
|
|
267
|
+
let reviewEnvironment: Record<string, string> = {}
|
|
268
|
+
if (input.review) {
|
|
269
|
+
const backend = yield* versionControl.forWorkbase(root)
|
|
270
|
+
reviewEnvironment = yield* backend.gitEnvironment(
|
|
271
|
+
join(root, "repos", input.review.repo),
|
|
272
|
+
)
|
|
273
|
+
}
|
|
260
274
|
yield* runLifecycleTransaction({
|
|
261
275
|
root,
|
|
262
276
|
preconditions: parentEpic
|
|
263
277
|
? [{ path: parentEpic.path, revision: parentEpic.revision }]
|
|
264
278
|
: [],
|
|
265
279
|
steps: [
|
|
266
|
-
...(input.review
|
|
280
|
+
...(input.review
|
|
281
|
+
? [reviewPinStep(root, id, input.review, reviewEnvironment)]
|
|
282
|
+
: []),
|
|
267
283
|
documentWriteStep(root, writes),
|
|
268
284
|
],
|
|
269
285
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { lstat, mkdir } from "node:fs/promises"
|
|
3
|
+
import { lstat, mkdir, rm } from "node:fs/promises"
|
|
4
4
|
import { join } from "node:path"
|
|
5
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
6
|
import { ClaimService } from "./ClaimService"
|
|
@@ -196,6 +196,63 @@ describe("VcsMigrationService", () => {
|
|
|
196
196
|
).toEqual([])
|
|
197
197
|
})
|
|
198
198
|
|
|
199
|
+
test("blocks non-colocated jj to Git migration before mutation", async () => {
|
|
200
|
+
if (!Bun.which("jj")) return
|
|
201
|
+
await runTestEffect(
|
|
202
|
+
WorktreeService.pipe(
|
|
203
|
+
Effect.flatMap((service) => service.remove("example", undefined, root)),
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
for (const alias of ["agency", "effect"]) {
|
|
207
|
+
const repository = join(root, "repos", alias)
|
|
208
|
+
await rm(repository, { recursive: true, force: true })
|
|
209
|
+
await run([
|
|
210
|
+
"jj",
|
|
211
|
+
"git",
|
|
212
|
+
"clone",
|
|
213
|
+
"--no-colocate",
|
|
214
|
+
join(root, "source"),
|
|
215
|
+
repository,
|
|
216
|
+
])
|
|
217
|
+
expect(await exists(join(repository, ".git"))).toBe(false)
|
|
218
|
+
}
|
|
219
|
+
await Bun.write(
|
|
220
|
+
join(root, "agency.json"),
|
|
221
|
+
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const planned = await runTestEffect(
|
|
225
|
+
VcsMigrationService.pipe(
|
|
226
|
+
Effect.flatMap((service) => service.migrate("git", root)),
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
expect(planned.blockers).toEqual(
|
|
230
|
+
expect.arrayContaining([
|
|
231
|
+
expect.objectContaining({
|
|
232
|
+
target: "repository:agency",
|
|
233
|
+
message: expect.stringContaining("non-colocated"),
|
|
234
|
+
}),
|
|
235
|
+
expect.objectContaining({
|
|
236
|
+
target: "repository:effect",
|
|
237
|
+
message: expect.stringContaining("non-colocated"),
|
|
238
|
+
}),
|
|
239
|
+
]),
|
|
240
|
+
)
|
|
241
|
+
expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("jj")
|
|
242
|
+
expect(await exists(join(root, "repos/agency/.jj"))).toBe(true)
|
|
243
|
+
await expect(
|
|
244
|
+
runTestEffect(
|
|
245
|
+
VcsMigrationService.pipe(
|
|
246
|
+
Effect.flatMap((service) =>
|
|
247
|
+
service.migrate("git", root, { apply: true }),
|
|
248
|
+
),
|
|
249
|
+
),
|
|
250
|
+
),
|
|
251
|
+
).rejects.toThrow("non-colocated")
|
|
252
|
+
expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("jj")
|
|
253
|
+
expect(await exists(join(root, "repos/agency/.jj"))).toBe(true)
|
|
254
|
+
})
|
|
255
|
+
|
|
199
256
|
test("allows clean unclaimed working workspaces", async () => {
|
|
200
257
|
if (!Bun.which("jj")) return
|
|
201
258
|
await runTestEffect(
|
|
@@ -309,6 +309,15 @@ const inspectMigration = (startPath: string, requestedTarget?: VcsKind) =>
|
|
|
309
309
|
remote: repository.declaredRemote ?? repository.remote,
|
|
310
310
|
})
|
|
311
311
|
if (source === "jj" && target === "git" && initialized) {
|
|
312
|
+
const gitEnvironment: Record<string, string> =
|
|
313
|
+
yield* sourceBackend.gitEnvironment(targetPath)
|
|
314
|
+
if (gitEnvironment.GIT_DIR?.includes(`${join(targetPath, ".jj")}/`)) {
|
|
315
|
+
blockers.push({
|
|
316
|
+
kind: "repository",
|
|
317
|
+
target: `repository:${repository.alias}`,
|
|
318
|
+
message: `Repository '${repository.alias}' is non-colocated; migrate it to colocated jj before converting the workbase to Git`,
|
|
319
|
+
})
|
|
320
|
+
}
|
|
312
321
|
const dirty = yield* sourceBackend.workspaceDirty(targetPath)
|
|
313
322
|
if (dirty !== false) {
|
|
314
323
|
blockers.push({
|
|
@@ -97,4 +97,42 @@ describe("VersionControlService", () => {
|
|
|
97
97
|
)
|
|
98
98
|
expect(await Bun.file(workspace).exists()).toBe(false)
|
|
99
99
|
})
|
|
100
|
+
|
|
101
|
+
test("clones non-colocated jj repositories and exposes their backing Git directory", async () => {
|
|
102
|
+
if (!Bun.which("jj")) return
|
|
103
|
+
const root = await createTempDir()
|
|
104
|
+
roots.push(root)
|
|
105
|
+
await Bun.write(
|
|
106
|
+
join(root, "agency.json"),
|
|
107
|
+
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
108
|
+
)
|
|
109
|
+
const source = join(root, "source")
|
|
110
|
+
const repository = join(root, "repository")
|
|
111
|
+
await mkdir(source)
|
|
112
|
+
await run(["git", "init", "--initial-branch=main"], source)
|
|
113
|
+
await run(["git", "config", "user.email", "test@example.com"], source)
|
|
114
|
+
await run(["git", "config", "user.name", "Test"], source)
|
|
115
|
+
await Bun.write(join(source, "README.md"), "example\n")
|
|
116
|
+
await run(["git", "add", "README.md"], source)
|
|
117
|
+
await run(["git", "commit", "-m", "initial"], source)
|
|
118
|
+
|
|
119
|
+
const backend = await runTestEffect(
|
|
120
|
+
VersionControlService.pipe(
|
|
121
|
+
Effect.flatMap((service) => service.forWorkbase(root)),
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
await runTestEffect(backend.cloneRepository(source, repository))
|
|
125
|
+
|
|
126
|
+
expect((await stat(join(repository, ".jj"))).isDirectory()).toBe(true)
|
|
127
|
+
expect(await Bun.file(join(repository, ".git")).exists()).toBe(false)
|
|
128
|
+
const environment: Record<string, string> = await runTestEffect(
|
|
129
|
+
backend.gitEnvironment(repository),
|
|
130
|
+
)
|
|
131
|
+
expect(environment.GIT_DIR).toContain(join(repository, ".jj"))
|
|
132
|
+
expect((await stat(environment.GIT_DIR!)).isDirectory()).toBe(true)
|
|
133
|
+
expect(await runTestEffect(backend.inspectRepository(repository))).toEqual({
|
|
134
|
+
kind: "repository",
|
|
135
|
+
remote: await realpath(source),
|
|
136
|
+
})
|
|
137
|
+
})
|
|
100
138
|
})
|