@markjaquith/agency 2.56.0 → 2.56.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/package.json +2 -1
- package/src/commands/sync.test.ts +27 -0
- package/src/commands/sync.ts +40 -5
- package/src/services/SyncService.test.ts +115 -0
- package/src/services/SyncService.ts +198 -66
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.56.
|
|
3
|
+
"version": "2.56.1",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"tag": "latest"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
|
+
"benchmark:sync": "bun scripts/benchmark-sync.ts",
|
|
61
62
|
"test": "find src \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print0 | xargs -0 -n 1 -P 4 bun test",
|
|
62
63
|
"test:opencode": "AGENCY_TEST_OPENCODE=1 bun test src/cli.test.ts --test-name-pattern 'provides effective whole-workbase OpenCode access'",
|
|
63
64
|
"format": "oxfmt",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createTempDir,
|
|
10
10
|
runTestEffect,
|
|
11
11
|
} from "../test-utils"
|
|
12
|
+
import type { Progress } from "../utils/progress"
|
|
12
13
|
import { sync } from "./sync"
|
|
13
14
|
|
|
14
15
|
const git = async (args: string[], cwd: string) => {
|
|
@@ -97,4 +98,30 @@ describe("sync command", () => {
|
|
|
97
98
|
],
|
|
98
99
|
})
|
|
99
100
|
})
|
|
101
|
+
|
|
102
|
+
test("reports human-readable progress without polluting JSON output", async () => {
|
|
103
|
+
const updates: string[] = []
|
|
104
|
+
const progress: Progress = {
|
|
105
|
+
start: (message) => updates.push(`start:${message}`),
|
|
106
|
+
succeed: (message) => updates.push(`succeed:${message}`),
|
|
107
|
+
fail: (message) => updates.push(`fail:${message}`),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await captureLogs(() =>
|
|
111
|
+
runTestEffect(sync({ cwd: root, silent: false }, progress)),
|
|
112
|
+
)
|
|
113
|
+
expect(updates).toEqual([
|
|
114
|
+
"start:Validating workbase",
|
|
115
|
+
"start:Inspected 1 repositories",
|
|
116
|
+
"start:Queried pull requests 1/1 (task:example)",
|
|
117
|
+
"start:Reconciled execution units 1/1 (task:example)",
|
|
118
|
+
"succeed:Synchronized 1 execution units",
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
updates.length = 0
|
|
122
|
+
await captureLogs(() =>
|
|
123
|
+
runTestEffect(sync({ cwd: root, dryRun: true, json: true }, progress)),
|
|
124
|
+
)
|
|
125
|
+
expect(updates).toEqual([])
|
|
126
|
+
})
|
|
100
127
|
})
|
package/src/commands/sync.ts
CHANGED
|
@@ -2,19 +2,54 @@ import { Effect } from "effect"
|
|
|
2
2
|
import { SyncService } from "../services/SyncService"
|
|
3
3
|
import type { BaseCommandOptions } from "../utils/command"
|
|
4
4
|
import { createLoggers } from "../utils/effect"
|
|
5
|
+
import { createProgress, type Progress } from "../utils/progress"
|
|
5
6
|
|
|
6
7
|
interface SyncCommandOptions extends BaseCommandOptions {
|
|
7
8
|
readonly dryRun?: boolean
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export const sync = (
|
|
11
|
+
export const sync = (
|
|
12
|
+
options: SyncCommandOptions = {},
|
|
13
|
+
progress: Progress = createProgress({
|
|
14
|
+
silent: options.silent || options.json,
|
|
15
|
+
}),
|
|
16
|
+
) =>
|
|
11
17
|
Effect.gen(function* () {
|
|
12
18
|
const service = yield* SyncService
|
|
13
19
|
const { log } = createLoggers(options)
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
const showProgress = !options.silent && !options.json
|
|
21
|
+
if (showProgress) progress.start("Validating workbase")
|
|
22
|
+
const result = yield* service
|
|
23
|
+
.reconcile({
|
|
24
|
+
cwd: options.cwd,
|
|
25
|
+
apply: options.dryRun !== true,
|
|
26
|
+
onProgress: showProgress
|
|
27
|
+
? ({ stage, current, total, target }) => {
|
|
28
|
+
if (stage === "repositories") {
|
|
29
|
+
progress.start(`Inspected ${total} repositories`)
|
|
30
|
+
} else if (stage === "pull-requests") {
|
|
31
|
+
progress.start(
|
|
32
|
+
`Queried pull requests ${current}/${total}${target ? ` (${target})` : ""}`,
|
|
33
|
+
)
|
|
34
|
+
} else {
|
|
35
|
+
progress.start(
|
|
36
|
+
`Reconciled execution units ${current}/${total}${target ? ` (${target})` : ""}`,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
: undefined,
|
|
41
|
+
})
|
|
42
|
+
.pipe(
|
|
43
|
+
Effect.tapError(() =>
|
|
44
|
+
Effect.sync(() => {
|
|
45
|
+
if (showProgress) progress.fail("Workbase sync failed")
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
if (showProgress)
|
|
50
|
+
progress.succeed(
|
|
51
|
+
`Synchronized ${result.executions.length} execution units`,
|
|
52
|
+
)
|
|
18
53
|
if (options.json) return log(JSON.stringify(result, null, 2))
|
|
19
54
|
|
|
20
55
|
for (const action of result.repositories.actions) {
|
|
@@ -796,4 +796,119 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
|
|
|
796
796
|
),
|
|
797
797
|
).toMatchObject({ valid: true, issues: [] })
|
|
798
798
|
})
|
|
799
|
+
|
|
800
|
+
test("reads each repository workspace inventory once", async () => {
|
|
801
|
+
for (const id of ["first", "second"]) {
|
|
802
|
+
await runTestEffect(
|
|
803
|
+
TaskService.pipe(
|
|
804
|
+
Effect.flatMap((service) =>
|
|
805
|
+
service.create(
|
|
806
|
+
{
|
|
807
|
+
id,
|
|
808
|
+
ticketUrl: null,
|
|
809
|
+
repo: "agency",
|
|
810
|
+
branch: `feat/${id}`,
|
|
811
|
+
base: "main",
|
|
812
|
+
},
|
|
813
|
+
root,
|
|
814
|
+
),
|
|
815
|
+
),
|
|
816
|
+
),
|
|
817
|
+
)
|
|
818
|
+
await runTestEffect(
|
|
819
|
+
WorktreeService.pipe(
|
|
820
|
+
Effect.flatMap((service) => service.materialize(id, undefined, root)),
|
|
821
|
+
),
|
|
822
|
+
)
|
|
823
|
+
await runTestEffect(
|
|
824
|
+
TaskService.pipe(
|
|
825
|
+
Effect.flatMap((service) =>
|
|
826
|
+
service.setStatus(id, "done", root, {
|
|
827
|
+
summary: `Completed ${id}`,
|
|
828
|
+
}),
|
|
829
|
+
),
|
|
830
|
+
),
|
|
831
|
+
)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const callsPath = join(root, "workspace-list-calls")
|
|
835
|
+
const realGit = Bun.which("git")!
|
|
836
|
+
const gitWrapper = join(root, "bin", "git")
|
|
837
|
+
await Bun.write(
|
|
838
|
+
gitWrapper,
|
|
839
|
+
`#!/bin/sh
|
|
840
|
+
case "$*" in
|
|
841
|
+
*"worktree list --porcelain -z"*) printf 'call\\n' >> ${JSON.stringify(callsPath)} ;;
|
|
842
|
+
esac
|
|
843
|
+
exec ${JSON.stringify(realGit)} "$@"
|
|
844
|
+
`,
|
|
845
|
+
)
|
|
846
|
+
await chmod(gitWrapper, 0o755)
|
|
847
|
+
|
|
848
|
+
await runTestEffect(
|
|
849
|
+
SyncService.pipe(
|
|
850
|
+
Effect.flatMap((service) => service.reconcile({ cwd: root })),
|
|
851
|
+
),
|
|
852
|
+
)
|
|
853
|
+
expect((await Bun.file(callsPath).text()).trim().split("\n")).toHaveLength(
|
|
854
|
+
1,
|
|
855
|
+
)
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
test("queries pull request providers concurrently", async () => {
|
|
859
|
+
for (const id of ["first", "second", "third"]) {
|
|
860
|
+
await runTestEffect(
|
|
861
|
+
TaskService.pipe(
|
|
862
|
+
Effect.flatMap((service) =>
|
|
863
|
+
service.create(
|
|
864
|
+
{
|
|
865
|
+
id,
|
|
866
|
+
ticketUrl: null,
|
|
867
|
+
repo: "agency",
|
|
868
|
+
branch: `feat/${id}`,
|
|
869
|
+
base: "main",
|
|
870
|
+
},
|
|
871
|
+
root,
|
|
872
|
+
),
|
|
873
|
+
),
|
|
874
|
+
),
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const barrier = join(root, "query-barrier")
|
|
879
|
+
await mkdir(barrier)
|
|
880
|
+
await Bun.write(
|
|
881
|
+
join(root, "bin", "gh"),
|
|
882
|
+
`#!/bin/sh
|
|
883
|
+
branch=""
|
|
884
|
+
while [ "$#" -gt 0 ]; do
|
|
885
|
+
if [ "$1" = "--head" ]; then branch="$2"; break; fi
|
|
886
|
+
shift
|
|
887
|
+
done
|
|
888
|
+
id="\${branch##*/}"
|
|
889
|
+
touch ${JSON.stringify(barrier)}/"\${id}"
|
|
890
|
+
attempt=0
|
|
891
|
+
while [ "$attempt" -lt 200 ]; do
|
|
892
|
+
set -- ${JSON.stringify(barrier)}/*
|
|
893
|
+
if [ -e "$1" ] && [ "$#" -ge 3 ]; then printf '[]\\n'; exit 0; fi
|
|
894
|
+
attempt=$((attempt + 1))
|
|
895
|
+
sleep 0.01
|
|
896
|
+
done
|
|
897
|
+
echo "provider queries were serialized" >&2
|
|
898
|
+
exit 9
|
|
899
|
+
`,
|
|
900
|
+
)
|
|
901
|
+
await chmod(join(root, "bin", "gh"), 0o755)
|
|
902
|
+
|
|
903
|
+
const result = await runTestEffect(
|
|
904
|
+
SyncService.pipe(
|
|
905
|
+
Effect.flatMap((service) => service.reconcile({ cwd: root })),
|
|
906
|
+
),
|
|
907
|
+
)
|
|
908
|
+
expect(
|
|
909
|
+
result.warnings.filter(
|
|
910
|
+
(warning) => warning.kind === "pr-discovery-unavailable",
|
|
911
|
+
),
|
|
912
|
+
).toEqual([])
|
|
913
|
+
})
|
|
799
914
|
})
|
|
@@ -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,147 @@ 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
|
+
result = yield* runExternal([
|
|
343
|
+
"gh",
|
|
344
|
+
"pr",
|
|
345
|
+
"view",
|
|
346
|
+
existing.url,
|
|
347
|
+
"--json",
|
|
348
|
+
"number,state,title,isDraft,headRefName,baseRefName,url,mergedAt,mergeCommit,mergeable",
|
|
349
|
+
])
|
|
350
|
+
} else if (!config.delivery) {
|
|
351
|
+
result = yield* runExternal(
|
|
352
|
+
[
|
|
353
|
+
"gh",
|
|
354
|
+
"pr",
|
|
355
|
+
"list",
|
|
356
|
+
"--head",
|
|
357
|
+
data.branch,
|
|
358
|
+
"--state",
|
|
359
|
+
"all",
|
|
360
|
+
"--json",
|
|
361
|
+
"number,state,title,isDraft,headRefName,baseRefName,url,mergedAt,mergeCommit,mergeable",
|
|
362
|
+
],
|
|
363
|
+
{ cwd: repositoryPath },
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
return [
|
|
367
|
+
record.key,
|
|
368
|
+
{ remoteUrl, remoteRepository, result },
|
|
369
|
+
] as const
|
|
370
|
+
}).pipe(
|
|
371
|
+
Effect.tap(() =>
|
|
372
|
+
Effect.sync(() => {
|
|
373
|
+
queriedPullRequests += 1
|
|
374
|
+
options.onProgress?.({
|
|
375
|
+
stage: "pull-requests",
|
|
376
|
+
current: queriedPullRequests,
|
|
377
|
+
total: queryRecords.length,
|
|
378
|
+
target: record.key,
|
|
379
|
+
})
|
|
380
|
+
}),
|
|
381
|
+
),
|
|
382
|
+
),
|
|
383
|
+
{ concurrency: 8 },
|
|
384
|
+
),
|
|
385
|
+
)
|
|
386
|
+
const reviewRecords = taskRecords.filter(
|
|
387
|
+
(task) => "review" in task.data,
|
|
388
|
+
)
|
|
389
|
+
const executionTotal = records.length + reviewRecords.length
|
|
390
|
+
let reconciledExecutions = 0
|
|
391
|
+
const reportExecution = (target: string) => {
|
|
392
|
+
reconciledExecutions += 1
|
|
393
|
+
options.onProgress?.({
|
|
394
|
+
stage: "executions",
|
|
395
|
+
current: reconciledExecutions,
|
|
396
|
+
total: executionTotal,
|
|
397
|
+
target,
|
|
398
|
+
})
|
|
399
|
+
}
|
|
240
400
|
|
|
241
401
|
for (const record of records.sort((a, b) =>
|
|
242
402
|
a.key.localeCompare(b.key),
|
|
@@ -271,10 +431,8 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
271
431
|
workspaceConflict = true
|
|
272
432
|
continue
|
|
273
433
|
}
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
)
|
|
277
|
-
if (Either.isLeft(listed)) {
|
|
434
|
+
const registered = yield* listRegistered(repositoryPath)
|
|
435
|
+
if (registered === null) {
|
|
278
436
|
unresolved.push({
|
|
279
437
|
kind: "worktree-inspection-failed",
|
|
280
438
|
target: record.key,
|
|
@@ -284,26 +442,21 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
284
442
|
continue
|
|
285
443
|
}
|
|
286
444
|
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
445
|
const expectedPath = exists
|
|
301
446
|
? yield* fs.realPath(checkoutPath)
|
|
302
447
|
: (yield* fs.isDirectory(codePath))
|
|
303
448
|
? join(yield* fs.realPath(codePath), checkout.repo)
|
|
304
449
|
: resolve(checkoutPath)
|
|
305
450
|
let atPath = registered.find((item) => item.path === expectedPath)
|
|
306
|
-
if (backend.kind === "jj" && atPath &&
|
|
451
|
+
if (backend.kind === "jj" && atPath && "branch" in checkout) {
|
|
452
|
+
atPath = { ...atPath, branch: checkout.branch }
|
|
453
|
+
}
|
|
454
|
+
if (
|
|
455
|
+
backend.kind === "jj" &&
|
|
456
|
+
atPath &&
|
|
457
|
+
exists &&
|
|
458
|
+
atPath.head === null
|
|
459
|
+
) {
|
|
307
460
|
atPath = {
|
|
308
461
|
...atPath,
|
|
309
462
|
head: yield* backend.workspaceHead(checkoutPath),
|
|
@@ -415,7 +568,8 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
415
568
|
|
|
416
569
|
const dirty =
|
|
417
570
|
exists && atPath
|
|
418
|
-
?
|
|
571
|
+
? (atPath.dirty ??
|
|
572
|
+
(yield* backend.workspaceDirty(checkoutPath)))
|
|
419
573
|
: null
|
|
420
574
|
if (exists && atPath && dirty === null) {
|
|
421
575
|
warnings.push({
|
|
@@ -487,6 +641,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
487
641
|
{ silent: true },
|
|
488
642
|
)
|
|
489
643
|
for (const checkout of workspace.checkouts) {
|
|
644
|
+
const repositoryPath = join(root, "repos", checkout.repo)
|
|
490
645
|
const index = checkoutStates.findIndex(
|
|
491
646
|
(item) => item.repo === checkout.repo,
|
|
492
647
|
)
|
|
@@ -504,6 +659,20 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
504
659
|
: null,
|
|
505
660
|
dirty: false,
|
|
506
661
|
}
|
|
662
|
+
const cached = registeredByRepository.get(repositoryPath)
|
|
663
|
+
if (cached) {
|
|
664
|
+
cached.push({
|
|
665
|
+
path: yield* fs.realPath(checkout.path),
|
|
666
|
+
head: checkout.resolvedCommit,
|
|
667
|
+
branch:
|
|
668
|
+
checkout.kind === "writable"
|
|
669
|
+
? backend.kind === "jj"
|
|
670
|
+
? checkout.requestedRef
|
|
671
|
+
: `refs/heads/${checkout.requestedRef}`
|
|
672
|
+
: null,
|
|
673
|
+
dirty: false,
|
|
674
|
+
})
|
|
675
|
+
}
|
|
507
676
|
}
|
|
508
677
|
}
|
|
509
678
|
}
|
|
@@ -569,6 +738,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
569
738
|
checkouts: checkoutStates,
|
|
570
739
|
pr: { url: null, state: "none" },
|
|
571
740
|
})
|
|
741
|
+
reportExecution(record.key)
|
|
572
742
|
continue
|
|
573
743
|
}
|
|
574
744
|
|
|
@@ -579,15 +749,8 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
579
749
|
state: "none",
|
|
580
750
|
}
|
|
581
751
|
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(/\/$/, "")
|
|
752
|
+
const query = prQueries.get(record.key)!
|
|
753
|
+
const { remoteUrl, remoteRepository } = query
|
|
591
754
|
|
|
592
755
|
if (
|
|
593
756
|
existing &&
|
|
@@ -609,18 +772,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
609
772
|
message: `Could not inspect delivery remote '${remoteName}'`,
|
|
610
773
|
})
|
|
611
774
|
} 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
|
-
})
|
|
775
|
+
const queried = query.result!
|
|
624
776
|
if (queried.exitCode === 0) {
|
|
625
777
|
const parsed = yield* Effect.try({
|
|
626
778
|
try: () => parseOptionalPullRequestRecord(queried.stdout),
|
|
@@ -668,14 +820,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
668
820
|
})
|
|
669
821
|
}
|
|
670
822
|
} 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
|
-
])
|
|
823
|
+
const viewed = query.result!
|
|
679
824
|
if (viewed.exitCode === 0) {
|
|
680
825
|
const detail = parseJson<Record<string, unknown>>(
|
|
681
826
|
viewed.stdout,
|
|
@@ -704,20 +849,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
704
849
|
})
|
|
705
850
|
}
|
|
706
851
|
} 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
|
-
)
|
|
852
|
+
const listed = query.result!
|
|
721
853
|
if (listed.exitCode === 0) {
|
|
722
854
|
const matches = parseJson<Record<string, unknown>[]>(
|
|
723
855
|
listed.stdout,
|
|
@@ -816,11 +948,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
816
948
|
checkouts: checkoutStates,
|
|
817
949
|
pr,
|
|
818
950
|
})
|
|
951
|
+
reportExecution(record.key)
|
|
819
952
|
}
|
|
820
953
|
|
|
821
|
-
for (const task of
|
|
822
|
-
(task) => "review" in task.data,
|
|
823
|
-
)) {
|
|
954
|
+
for (const task of reviewRecords) {
|
|
824
955
|
if (!("review" in task.data)) continue
|
|
825
956
|
let data = task.data
|
|
826
957
|
let revision = task.revision
|
|
@@ -915,6 +1046,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
915
1046
|
sourceAvailable: sourceCommit !== null,
|
|
916
1047
|
},
|
|
917
1048
|
})
|
|
1049
|
+
reportExecution(`task:${task.id}`)
|
|
918
1050
|
}
|
|
919
1051
|
|
|
920
1052
|
return {
|