@markjaquith/agency 2.56.0 → 2.57.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 +14 -3
- package/package.json +2 -1
- package/src/commands/pr.test.ts +40 -1
- package/src/commands/pr.ts +6 -0
- package/src/commands/sync.test.ts +27 -0
- package/src/commands/sync.ts +40 -5
- 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 +132 -2
- package/src/services/SyncService.ts +230 -75
- 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/vcs-status-fast.ts +3 -8
- package/src/workbase/delivery-command.test.ts +11 -2
- package/src/workbase/delivery-command.ts +5 -1
|
@@ -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 () => {
|
|
@@ -796,4 +811,119 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
796
811
|
),
|
|
797
812
|
).toMatchObject({ valid: true, issues: [] })
|
|
798
813
|
})
|
|
814
|
+
|
|
815
|
+
test("reads each repository workspace inventory once", async () => {
|
|
816
|
+
for (const id of ["first", "second"]) {
|
|
817
|
+
await runTestEffect(
|
|
818
|
+
TaskService.pipe(
|
|
819
|
+
Effect.flatMap((service) =>
|
|
820
|
+
service.create(
|
|
821
|
+
{
|
|
822
|
+
id,
|
|
823
|
+
ticketUrl: null,
|
|
824
|
+
repo: "agency",
|
|
825
|
+
branch: `feat/${id}`,
|
|
826
|
+
base: "main",
|
|
827
|
+
},
|
|
828
|
+
root,
|
|
829
|
+
),
|
|
830
|
+
),
|
|
831
|
+
),
|
|
832
|
+
)
|
|
833
|
+
await runTestEffect(
|
|
834
|
+
WorktreeService.pipe(
|
|
835
|
+
Effect.flatMap((service) => service.materialize(id, undefined, root)),
|
|
836
|
+
),
|
|
837
|
+
)
|
|
838
|
+
await runTestEffect(
|
|
839
|
+
TaskService.pipe(
|
|
840
|
+
Effect.flatMap((service) =>
|
|
841
|
+
service.setStatus(id, "done", root, {
|
|
842
|
+
summary: `Completed ${id}`,
|
|
843
|
+
}),
|
|
844
|
+
),
|
|
845
|
+
),
|
|
846
|
+
)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const callsPath = join(root, "workspace-list-calls")
|
|
850
|
+
const realGit = Bun.which("git")!
|
|
851
|
+
const gitWrapper = join(root, "bin", "git")
|
|
852
|
+
await Bun.write(
|
|
853
|
+
gitWrapper,
|
|
854
|
+
`#!/bin/sh
|
|
855
|
+
case "$*" in
|
|
856
|
+
*"worktree list --porcelain -z"*) printf 'call\\n' >> ${JSON.stringify(callsPath)} ;;
|
|
857
|
+
esac
|
|
858
|
+
exec ${JSON.stringify(realGit)} "$@"
|
|
859
|
+
`,
|
|
860
|
+
)
|
|
861
|
+
await chmod(gitWrapper, 0o755)
|
|
862
|
+
|
|
863
|
+
await runTestEffect(
|
|
864
|
+
SyncService.pipe(
|
|
865
|
+
Effect.flatMap((service) => service.reconcile({ cwd: root })),
|
|
866
|
+
),
|
|
867
|
+
)
|
|
868
|
+
expect((await Bun.file(callsPath).text()).trim().split("\n")).toHaveLength(
|
|
869
|
+
1,
|
|
870
|
+
)
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
test("queries pull request providers concurrently", async () => {
|
|
874
|
+
for (const id of ["first", "second", "third"]) {
|
|
875
|
+
await runTestEffect(
|
|
876
|
+
TaskService.pipe(
|
|
877
|
+
Effect.flatMap((service) =>
|
|
878
|
+
service.create(
|
|
879
|
+
{
|
|
880
|
+
id,
|
|
881
|
+
ticketUrl: null,
|
|
882
|
+
repo: "agency",
|
|
883
|
+
branch: `feat/${id}`,
|
|
884
|
+
base: "main",
|
|
885
|
+
},
|
|
886
|
+
root,
|
|
887
|
+
),
|
|
888
|
+
),
|
|
889
|
+
),
|
|
890
|
+
)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const barrier = join(root, "query-barrier")
|
|
894
|
+
await mkdir(barrier)
|
|
895
|
+
await Bun.write(
|
|
896
|
+
join(root, "bin", "gh"),
|
|
897
|
+
`#!/bin/sh
|
|
898
|
+
branch=""
|
|
899
|
+
while [ "$#" -gt 0 ]; do
|
|
900
|
+
if [ "$1" = "--head" ]; then branch="$2"; break; fi
|
|
901
|
+
shift
|
|
902
|
+
done
|
|
903
|
+
id="\${branch##*/}"
|
|
904
|
+
touch ${JSON.stringify(barrier)}/"\${id}"
|
|
905
|
+
attempt=0
|
|
906
|
+
while [ "$attempt" -lt 200 ]; do
|
|
907
|
+
set -- ${JSON.stringify(barrier)}/*
|
|
908
|
+
if [ -e "$1" ] && [ "$#" -ge 3 ]; then printf '[]\\n'; exit 0; fi
|
|
909
|
+
attempt=$((attempt + 1))
|
|
910
|
+
sleep 0.01
|
|
911
|
+
done
|
|
912
|
+
echo "provider queries were serialized" >&2
|
|
913
|
+
exit 9
|
|
914
|
+
`,
|
|
915
|
+
)
|
|
916
|
+
await chmod(join(root, "bin", "gh"), 0o755)
|
|
917
|
+
|
|
918
|
+
const result = await runTestEffect(
|
|
919
|
+
SyncService.pipe(
|
|
920
|
+
Effect.flatMap((service) => service.reconcile({ cwd: root })),
|
|
921
|
+
),
|
|
922
|
+
)
|
|
923
|
+
expect(
|
|
924
|
+
result.warnings.filter(
|
|
925
|
+
(warning) => warning.kind === "pr-discovery-unavailable",
|
|
926
|
+
),
|
|
927
|
+
).toEqual([])
|
|
928
|
+
})
|
|
799
929
|
})
|
|
@@ -48,6 +48,7 @@ interface RegisteredWorktree {
|
|
|
48
48
|
readonly path: string
|
|
49
49
|
readonly head: string | null
|
|
50
50
|
readonly branch: string | null
|
|
51
|
+
readonly dirty?: boolean
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
interface SyncChange {
|
|
@@ -106,6 +107,13 @@ interface SyncResult {
|
|
|
106
107
|
readonly repositories: RepositorySetupResult
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
export interface SyncProgress {
|
|
111
|
+
readonly stage: "repositories" | "pull-requests" | "executions"
|
|
112
|
+
readonly current: number
|
|
113
|
+
readonly total: number
|
|
114
|
+
readonly target?: string
|
|
115
|
+
}
|
|
116
|
+
|
|
109
117
|
const parseWorktrees = (output: string): RegisteredWorktree[] => {
|
|
110
118
|
const worktrees: RegisteredWorktree[] = []
|
|
111
119
|
let current: RegisteredWorktree | undefined
|
|
@@ -152,6 +160,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
152
160
|
readonly cwd?: string
|
|
153
161
|
readonly apply?: boolean
|
|
154
162
|
readonly now?: Date
|
|
163
|
+
readonly onProgress?: (progress: SyncProgress) => void
|
|
155
164
|
} = {},
|
|
156
165
|
) =>
|
|
157
166
|
Effect.gen(function* () {
|
|
@@ -177,6 +186,11 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
177
186
|
cwd: root,
|
|
178
187
|
apply: options.apply === true,
|
|
179
188
|
})
|
|
189
|
+
options.onProgress?.({
|
|
190
|
+
stage: "repositories",
|
|
191
|
+
current: repositorySetup.repositories.length,
|
|
192
|
+
total: repositorySetup.repositories.length,
|
|
193
|
+
})
|
|
180
194
|
|
|
181
195
|
const apply = options.apply === true
|
|
182
196
|
const now = options.now ?? new Date()
|
|
@@ -192,6 +206,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
192
206
|
})
|
|
193
207
|
}
|
|
194
208
|
const executions: ExecutionSyncState[] = []
|
|
209
|
+
const registeredByRepository = new Map<
|
|
210
|
+
string,
|
|
211
|
+
RegisteredWorktree[] | null
|
|
212
|
+
>()
|
|
195
213
|
const runExternal = (
|
|
196
214
|
args: readonly string[],
|
|
197
215
|
commandOptions?: {
|
|
@@ -214,8 +232,9 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
214
232
|
}),
|
|
215
233
|
),
|
|
216
234
|
)
|
|
235
|
+
const taskRecords = yield* tasks.list(root)
|
|
217
236
|
const records: ExecutionRecord[] = []
|
|
218
|
-
for (const task of
|
|
237
|
+
for (const task of taskRecords) {
|
|
219
238
|
if ("phases" in task.data) {
|
|
220
239
|
for (const phase of yield* phases.list(task.id, root)) {
|
|
221
240
|
records.push({
|
|
@@ -237,6 +256,164 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
237
256
|
})
|
|
238
257
|
}
|
|
239
258
|
}
|
|
259
|
+
const listRegistered = (repositoryPath: string) =>
|
|
260
|
+
Effect.gen(function* () {
|
|
261
|
+
if (registeredByRepository.has(repositoryPath))
|
|
262
|
+
return registeredByRepository.get(repositoryPath)!
|
|
263
|
+
const listed = yield* Effect.either(
|
|
264
|
+
backend.listWorkspaces(repositoryPath),
|
|
265
|
+
)
|
|
266
|
+
if (Either.isLeft(listed)) {
|
|
267
|
+
registeredByRepository.set(repositoryPath, null)
|
|
268
|
+
return null
|
|
269
|
+
}
|
|
270
|
+
const registered: RegisteredWorktree[] = []
|
|
271
|
+
for (const item of listed.right) {
|
|
272
|
+
registered.push({
|
|
273
|
+
head: backend.kind === "jj" ? (item.head ?? null) : item.commit,
|
|
274
|
+
branch: item.branch,
|
|
275
|
+
path: (yield* fs.exists(item.path))
|
|
276
|
+
? yield* fs.realPath(item.path)
|
|
277
|
+
: resolve(item.path),
|
|
278
|
+
...(item.dirty === undefined ? {} : { dirty: item.dirty }),
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
registeredByRepository.set(repositoryPath, registered)
|
|
282
|
+
return registered
|
|
283
|
+
})
|
|
284
|
+
const queryRecords = records.filter((record) => !record.data.completion)
|
|
285
|
+
let queriedPullRequests = 0
|
|
286
|
+
const remoteName = config.delivery?.remote ?? "origin"
|
|
287
|
+
const repositoryPaths = [
|
|
288
|
+
...new Set(
|
|
289
|
+
queryRecords.map((record) => join(root, "repos", record.data.repo)),
|
|
290
|
+
),
|
|
291
|
+
]
|
|
292
|
+
const remoteUrls = new Map(
|
|
293
|
+
yield* Effect.forEach(
|
|
294
|
+
repositoryPaths,
|
|
295
|
+
(repositoryPath) =>
|
|
296
|
+
backend
|
|
297
|
+
.remoteUrl(repositoryPath, remoteName)
|
|
298
|
+
.pipe(
|
|
299
|
+
Effect.map(
|
|
300
|
+
(remoteUrl) => [repositoryPath, remoteUrl] as const,
|
|
301
|
+
),
|
|
302
|
+
),
|
|
303
|
+
{ concurrency: 8 },
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
const prQueries = new Map(
|
|
307
|
+
yield* Effect.forEach(
|
|
308
|
+
queryRecords,
|
|
309
|
+
(record) =>
|
|
310
|
+
Effect.gen(function* () {
|
|
311
|
+
const data = record.data
|
|
312
|
+
const repositoryPath = join(root, "repos", data.repo)
|
|
313
|
+
const remoteUrl = remoteUrls.get(repositoryPath) ?? null
|
|
314
|
+
const remoteRepository = (remoteUrl ?? "")
|
|
315
|
+
.trim()
|
|
316
|
+
.replace(/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?[^/]+\//i, "")
|
|
317
|
+
.replace(/^[^:]+:/, "")
|
|
318
|
+
.replace(/\.git\/?$/, "")
|
|
319
|
+
.replace(/\/$/, "")
|
|
320
|
+
const existing = data.pr
|
|
321
|
+
? normalizePullRequestRecord(data.pr)
|
|
322
|
+
: null
|
|
323
|
+
let result
|
|
324
|
+
if (config.delivery && remoteUrl) {
|
|
325
|
+
const resolved = resolveDeliveryCommand(
|
|
326
|
+
config.delivery,
|
|
327
|
+
"query",
|
|
328
|
+
{
|
|
329
|
+
repository: remoteRepository,
|
|
330
|
+
branch: data.branch,
|
|
331
|
+
base: data.base,
|
|
332
|
+
draft: existing ? String(existing.draft) : "",
|
|
333
|
+
url: existing?.url ?? "",
|
|
334
|
+
identifier: existing?.identifier ?? "",
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
result = yield* runExternal(resolved.argv, {
|
|
338
|
+
cwd: repositoryPath,
|
|
339
|
+
env: resolved.environment,
|
|
340
|
+
})
|
|
341
|
+
} else if (!config.delivery && existing) {
|
|
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
|
+
)
|
|
359
|
+
} else if (!config.delivery) {
|
|
360
|
+
const environment = yield* Effect.either(
|
|
361
|
+
backend.gitEnvironment(repositoryPath),
|
|
362
|
+
)
|
|
363
|
+
result = yield* runExternal(
|
|
364
|
+
[
|
|
365
|
+
"gh",
|
|
366
|
+
"pr",
|
|
367
|
+
"list",
|
|
368
|
+
"--repo",
|
|
369
|
+
remoteRepository,
|
|
370
|
+
"--head",
|
|
371
|
+
data.branch,
|
|
372
|
+
"--state",
|
|
373
|
+
"all",
|
|
374
|
+
"--json",
|
|
375
|
+
"number,state,title,isDraft,headRefName,baseRefName,url,mergedAt,mergeCommit,mergeable",
|
|
376
|
+
],
|
|
377
|
+
{
|
|
378
|
+
cwd: repositoryPath,
|
|
379
|
+
env: Either.isRight(environment) ? environment.right : {},
|
|
380
|
+
},
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
return [
|
|
384
|
+
record.key,
|
|
385
|
+
{ remoteUrl, remoteRepository, result },
|
|
386
|
+
] as const
|
|
387
|
+
}).pipe(
|
|
388
|
+
Effect.tap(() =>
|
|
389
|
+
Effect.sync(() => {
|
|
390
|
+
queriedPullRequests += 1
|
|
391
|
+
options.onProgress?.({
|
|
392
|
+
stage: "pull-requests",
|
|
393
|
+
current: queriedPullRequests,
|
|
394
|
+
total: queryRecords.length,
|
|
395
|
+
target: record.key,
|
|
396
|
+
})
|
|
397
|
+
}),
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
{ concurrency: 8 },
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
const reviewRecords = taskRecords.filter(
|
|
404
|
+
(task) => "review" in task.data,
|
|
405
|
+
)
|
|
406
|
+
const executionTotal = records.length + reviewRecords.length
|
|
407
|
+
let reconciledExecutions = 0
|
|
408
|
+
const reportExecution = (target: string) => {
|
|
409
|
+
reconciledExecutions += 1
|
|
410
|
+
options.onProgress?.({
|
|
411
|
+
stage: "executions",
|
|
412
|
+
current: reconciledExecutions,
|
|
413
|
+
total: executionTotal,
|
|
414
|
+
target,
|
|
415
|
+
})
|
|
416
|
+
}
|
|
240
417
|
|
|
241
418
|
for (const record of records.sort((a, b) =>
|
|
242
419
|
a.key.localeCompare(b.key),
|
|
@@ -271,10 +448,8 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
271
448
|
workspaceConflict = true
|
|
272
449
|
continue
|
|
273
450
|
}
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
)
|
|
277
|
-
if (Either.isLeft(listed)) {
|
|
451
|
+
const registered = yield* listRegistered(repositoryPath)
|
|
452
|
+
if (registered === null) {
|
|
278
453
|
unresolved.push({
|
|
279
454
|
kind: "worktree-inspection-failed",
|
|
280
455
|
target: record.key,
|
|
@@ -284,26 +459,21 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
284
459
|
continue
|
|
285
460
|
}
|
|
286
461
|
const exists = yield* fs.isDirectory(checkoutPath)
|
|
287
|
-
const registered: RegisteredWorktree[] = []
|
|
288
|
-
for (const item of listed.right) {
|
|
289
|
-
registered.push({
|
|
290
|
-
head: item.commit,
|
|
291
|
-
branch:
|
|
292
|
-
backend.kind === "jj" && "branch" in checkout
|
|
293
|
-
? checkout.branch
|
|
294
|
-
: item.branch,
|
|
295
|
-
path: (yield* fs.exists(item.path))
|
|
296
|
-
? yield* fs.realPath(item.path)
|
|
297
|
-
: resolve(item.path),
|
|
298
|
-
})
|
|
299
|
-
}
|
|
300
462
|
const expectedPath = exists
|
|
301
463
|
? yield* fs.realPath(checkoutPath)
|
|
302
464
|
: (yield* fs.isDirectory(codePath))
|
|
303
465
|
? join(yield* fs.realPath(codePath), checkout.repo)
|
|
304
466
|
: resolve(checkoutPath)
|
|
305
467
|
let atPath = registered.find((item) => item.path === expectedPath)
|
|
306
|
-
if (backend.kind === "jj" && atPath &&
|
|
468
|
+
if (backend.kind === "jj" && atPath && "branch" in checkout) {
|
|
469
|
+
atPath = { ...atPath, branch: checkout.branch }
|
|
470
|
+
}
|
|
471
|
+
if (
|
|
472
|
+
backend.kind === "jj" &&
|
|
473
|
+
atPath &&
|
|
474
|
+
exists &&
|
|
475
|
+
atPath.head === null
|
|
476
|
+
) {
|
|
307
477
|
atPath = {
|
|
308
478
|
...atPath,
|
|
309
479
|
head: yield* backend.workspaceHead(checkoutPath),
|
|
@@ -378,14 +548,18 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
378
548
|
let resolvedCommit: string | null = null
|
|
379
549
|
if ("ref" in checkout) {
|
|
380
550
|
if (!isCommitId(checkout.ref)) {
|
|
381
|
-
const
|
|
382
|
-
"git",
|
|
383
|
-
"-C",
|
|
551
|
+
const remote = yield* backend.remoteUrl(
|
|
384
552
|
repositoryPath,
|
|
385
|
-
"ls-remote",
|
|
386
553
|
"origin",
|
|
387
|
-
|
|
388
|
-
|
|
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" }
|
|
389
563
|
resolvedCommit =
|
|
390
564
|
remoteRef.stdout.match(/^([0-9a-f]{40,64})\s/m)?.[1] ?? null
|
|
391
565
|
if (!resolvedCommit) {
|
|
@@ -415,7 +589,8 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
415
589
|
|
|
416
590
|
const dirty =
|
|
417
591
|
exists && atPath
|
|
418
|
-
?
|
|
592
|
+
? (atPath.dirty ??
|
|
593
|
+
(yield* backend.workspaceDirty(checkoutPath)))
|
|
419
594
|
: null
|
|
420
595
|
if (exists && atPath && dirty === null) {
|
|
421
596
|
warnings.push({
|
|
@@ -487,6 +662,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
487
662
|
{ silent: true },
|
|
488
663
|
)
|
|
489
664
|
for (const checkout of workspace.checkouts) {
|
|
665
|
+
const repositoryPath = join(root, "repos", checkout.repo)
|
|
490
666
|
const index = checkoutStates.findIndex(
|
|
491
667
|
(item) => item.repo === checkout.repo,
|
|
492
668
|
)
|
|
@@ -504,6 +680,20 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
504
680
|
: null,
|
|
505
681
|
dirty: false,
|
|
506
682
|
}
|
|
683
|
+
const cached = registeredByRepository.get(repositoryPath)
|
|
684
|
+
if (cached) {
|
|
685
|
+
cached.push({
|
|
686
|
+
path: yield* fs.realPath(checkout.path),
|
|
687
|
+
head: checkout.resolvedCommit,
|
|
688
|
+
branch:
|
|
689
|
+
checkout.kind === "writable"
|
|
690
|
+
? backend.kind === "jj"
|
|
691
|
+
? checkout.requestedRef
|
|
692
|
+
: `refs/heads/${checkout.requestedRef}`
|
|
693
|
+
: null,
|
|
694
|
+
dirty: false,
|
|
695
|
+
})
|
|
696
|
+
}
|
|
507
697
|
}
|
|
508
698
|
}
|
|
509
699
|
}
|
|
@@ -569,6 +759,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
569
759
|
checkouts: checkoutStates,
|
|
570
760
|
pr: { url: null, state: "none" },
|
|
571
761
|
})
|
|
762
|
+
reportExecution(record.key)
|
|
572
763
|
continue
|
|
573
764
|
}
|
|
574
765
|
|
|
@@ -579,15 +770,8 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
579
770
|
state: "none",
|
|
580
771
|
}
|
|
581
772
|
let prConflict = false
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
const remoteUrl = yield* backend.remoteUrl(repositoryPath, remoteName)
|
|
585
|
-
const remoteRepository = (remoteUrl ?? "")
|
|
586
|
-
.trim()
|
|
587
|
-
.replace(/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]+@)?[^/]+\//i, "")
|
|
588
|
-
.replace(/^[^:]+:/, "")
|
|
589
|
-
.replace(/\.git\/?$/, "")
|
|
590
|
-
.replace(/\/$/, "")
|
|
773
|
+
const query = prQueries.get(record.key)!
|
|
774
|
+
const { remoteUrl, remoteRepository } = query
|
|
591
775
|
|
|
592
776
|
if (
|
|
593
777
|
existing &&
|
|
@@ -609,18 +793,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
609
793
|
message: `Could not inspect delivery remote '${remoteName}'`,
|
|
610
794
|
})
|
|
611
795
|
} else if (config.delivery) {
|
|
612
|
-
const
|
|
613
|
-
repository: remoteRepository,
|
|
614
|
-
branch: data.branch,
|
|
615
|
-
base: data.base,
|
|
616
|
-
draft: existing ? String(existing.draft) : "",
|
|
617
|
-
url: existing?.url ?? "",
|
|
618
|
-
identifier: existing?.identifier ?? "",
|
|
619
|
-
})
|
|
620
|
-
const queried = yield* runExternal(resolved.argv, {
|
|
621
|
-
cwd: repositoryPath,
|
|
622
|
-
env: resolved.environment,
|
|
623
|
-
})
|
|
796
|
+
const queried = query.result!
|
|
624
797
|
if (queried.exitCode === 0) {
|
|
625
798
|
const parsed = yield* Effect.try({
|
|
626
799
|
try: () => parseOptionalPullRequestRecord(queried.stdout),
|
|
@@ -668,14 +841,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
668
841
|
})
|
|
669
842
|
}
|
|
670
843
|
} else if (existing) {
|
|
671
|
-
const viewed =
|
|
672
|
-
"gh",
|
|
673
|
-
"pr",
|
|
674
|
-
"view",
|
|
675
|
-
existing.url,
|
|
676
|
-
"--json",
|
|
677
|
-
"number,state,title,isDraft,headRefName,baseRefName,url,mergedAt,mergeCommit,mergeable",
|
|
678
|
-
])
|
|
844
|
+
const viewed = query.result!
|
|
679
845
|
if (viewed.exitCode === 0) {
|
|
680
846
|
const detail = parseJson<Record<string, unknown>>(
|
|
681
847
|
viewed.stdout,
|
|
@@ -704,20 +870,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
704
870
|
})
|
|
705
871
|
}
|
|
706
872
|
} else {
|
|
707
|
-
const listed =
|
|
708
|
-
[
|
|
709
|
-
"gh",
|
|
710
|
-
"pr",
|
|
711
|
-
"list",
|
|
712
|
-
"--head",
|
|
713
|
-
data.branch,
|
|
714
|
-
"--state",
|
|
715
|
-
"all",
|
|
716
|
-
"--json",
|
|
717
|
-
"number,state,title,isDraft,headRefName,baseRefName,url,mergedAt,mergeCommit,mergeable",
|
|
718
|
-
],
|
|
719
|
-
{ cwd: repositoryPath },
|
|
720
|
-
)
|
|
873
|
+
const listed = query.result!
|
|
721
874
|
if (listed.exitCode === 0) {
|
|
722
875
|
const matches = parseJson<Record<string, unknown>[]>(
|
|
723
876
|
listed.stdout,
|
|
@@ -816,11 +969,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
816
969
|
checkouts: checkoutStates,
|
|
817
970
|
pr,
|
|
818
971
|
})
|
|
972
|
+
reportExecution(record.key)
|
|
819
973
|
}
|
|
820
974
|
|
|
821
|
-
for (const task of
|
|
822
|
-
(task) => "review" in task.data,
|
|
823
|
-
)) {
|
|
975
|
+
for (const task of reviewRecords) {
|
|
824
976
|
if (!("review" in task.data)) continue
|
|
825
977
|
let data = task.data
|
|
826
978
|
let revision = task.revision
|
|
@@ -867,12 +1019,14 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
867
1019
|
})
|
|
868
1020
|
}
|
|
869
1021
|
const repositoryPath = join(root, "repos", data.review.repo)
|
|
1022
|
+
const reviewRemote = yield* backend.remoteUrl(
|
|
1023
|
+
repositoryPath,
|
|
1024
|
+
"origin",
|
|
1025
|
+
)
|
|
870
1026
|
const source = yield* runExternal([
|
|
871
1027
|
"git",
|
|
872
|
-
"-C",
|
|
873
|
-
repositoryPath,
|
|
874
1028
|
"ls-remote",
|
|
875
|
-
"origin",
|
|
1029
|
+
reviewRemote ?? "origin",
|
|
876
1030
|
data.review.source.kind === "pull-request"
|
|
877
1031
|
? data.review.source.fetchRef
|
|
878
1032
|
: originRef(data.review.source.ref),
|
|
@@ -915,6 +1069,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
915
1069
|
sourceAvailable: sourceCommit !== null,
|
|
916
1070
|
},
|
|
917
1071
|
})
|
|
1072
|
+
reportExecution(`task:${task.id}`)
|
|
918
1073
|
}
|
|
919
1074
|
|
|
920
1075
|
return {
|
|
@@ -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
|
})
|