@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
|
@@ -33,6 +33,10 @@ import {
|
|
|
33
33
|
type WorkStatus,
|
|
34
34
|
} from "../workbase/schemas"
|
|
35
35
|
import { FileSystemService } from "./FileSystemService"
|
|
36
|
+
import {
|
|
37
|
+
VersionControlService,
|
|
38
|
+
type VersionControlBackend,
|
|
39
|
+
} from "./VersionControlService"
|
|
36
40
|
import { WorkbaseService, type ValidationReport } from "./WorkbaseService"
|
|
37
41
|
|
|
38
42
|
class GraphError extends Data.TaggedError("GraphError")<{
|
|
@@ -68,6 +72,7 @@ export interface GraphOptions {
|
|
|
68
72
|
readonly repositories?: readonly string[]
|
|
69
73
|
readonly kinds?: readonly GraphNodeKind[]
|
|
70
74
|
readonly include?: readonly GraphInclude[]
|
|
75
|
+
readonly backend?: VersionControlBackend
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
const epicNodeId = (id: string) => `epic:${id}`
|
|
@@ -127,6 +132,11 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
127
132
|
const fs = yield* FileSystemService
|
|
128
133
|
const workbase = yield* WorkbaseService
|
|
129
134
|
const { root, config } = yield* workbase.loadConfig(options.cwd)
|
|
135
|
+
const backend =
|
|
136
|
+
options.backend ??
|
|
137
|
+
(yield* VersionControlService.pipe(
|
|
138
|
+
Effect.flatMap((service) => service.forWorkbase(root)),
|
|
139
|
+
))
|
|
130
140
|
const includes = [...new Set(options.include ?? [])].sort()
|
|
131
141
|
const include = new Set(includes)
|
|
132
142
|
const epics = new Map<string, Document<EpicData>>()
|
|
@@ -623,38 +633,20 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
623
633
|
|
|
624
634
|
const inspectGit = (path: string) =>
|
|
625
635
|
Effect.gen(function* () {
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
636
|
+
const inspection = yield* backend.inspectRepository(path)
|
|
637
|
+
const workspaces = inspection
|
|
638
|
+
? yield* backend
|
|
639
|
+
.listWorkspaces(path)
|
|
640
|
+
.pipe(Effect.catchAll(() => Effect.succeed([])))
|
|
641
|
+
: []
|
|
642
|
+
const primary = workspaces.find(
|
|
643
|
+
(workspace) => workspace.path === path,
|
|
644
|
+
)
|
|
633
645
|
return {
|
|
634
|
-
kind:
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
? "bare"
|
|
639
|
-
: "repository",
|
|
640
|
-
remote: yield* run(fs, [
|
|
641
|
-
"git",
|
|
642
|
-
"-C",
|
|
643
|
-
path,
|
|
644
|
-
"remote",
|
|
645
|
-
"get-url",
|
|
646
|
-
"origin",
|
|
647
|
-
]),
|
|
648
|
-
head: yield* run(fs, ["git", "-C", path, "rev-parse", "HEAD"]),
|
|
649
|
-
branch: yield* run(fs, [
|
|
650
|
-
"git",
|
|
651
|
-
"-C",
|
|
652
|
-
path,
|
|
653
|
-
"symbolic-ref",
|
|
654
|
-
"--quiet",
|
|
655
|
-
"--short",
|
|
656
|
-
"HEAD",
|
|
657
|
-
]),
|
|
646
|
+
kind: inspection?.kind ?? null,
|
|
647
|
+
remote: inspection?.remote ?? null,
|
|
648
|
+
head: inspection ? yield* backend.workspaceHead(path) : null,
|
|
649
|
+
branch: primary?.branch?.replace(/^refs\/heads\//, "") ?? null,
|
|
658
650
|
} satisfies GraphRepositoryGit
|
|
659
651
|
})
|
|
660
652
|
|
|
@@ -678,6 +670,27 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
678
670
|
}
|
|
679
671
|
}
|
|
680
672
|
if (include.has("git")) {
|
|
673
|
+
const remote = yield* backend.remoteUrl(
|
|
674
|
+
repositoryPath,
|
|
675
|
+
"origin",
|
|
676
|
+
)
|
|
677
|
+
const canonicalCheckoutPath = materialized
|
|
678
|
+
? yield* fs.realPath(checkoutPath)
|
|
679
|
+
: checkoutPath
|
|
680
|
+
const actualBranch = materialized
|
|
681
|
+
? yield* backend.listWorkspaces(repositoryPath).pipe(
|
|
682
|
+
Effect.map(
|
|
683
|
+
(workspaces) =>
|
|
684
|
+
workspaces
|
|
685
|
+
.find(
|
|
686
|
+
(workspace) =>
|
|
687
|
+
workspace.path === canonicalCheckoutPath,
|
|
688
|
+
)
|
|
689
|
+
?.branch?.replace(/^refs\/heads\//, "") ?? null,
|
|
690
|
+
),
|
|
691
|
+
Effect.catchAll(() => Effect.succeed(null)),
|
|
692
|
+
)
|
|
693
|
+
: null
|
|
681
694
|
result.git =
|
|
682
695
|
"review" in data
|
|
683
696
|
? {
|
|
@@ -686,10 +699,8 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
686
699
|
value?.split(/\s+/)[0] ?? null)(
|
|
687
700
|
yield* run(fs, [
|
|
688
701
|
"git",
|
|
689
|
-
"-C",
|
|
690
|
-
repositoryPath,
|
|
691
702
|
"ls-remote",
|
|
692
|
-
"origin",
|
|
703
|
+
remote ?? "origin",
|
|
693
704
|
data.review.source.kind === "pull-request"
|
|
694
705
|
? data.review.source.fetchRef
|
|
695
706
|
: data.review.source.ref
|
|
@@ -698,86 +709,33 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
698
709
|
]),
|
|
699
710
|
),
|
|
700
711
|
checkoutCommit: materialized
|
|
701
|
-
? yield*
|
|
702
|
-
"git",
|
|
703
|
-
"-C",
|
|
704
|
-
checkoutPath,
|
|
705
|
-
"rev-parse",
|
|
706
|
-
"HEAD",
|
|
707
|
-
])
|
|
708
|
-
: null,
|
|
709
|
-
checkoutBranch: materialized
|
|
710
|
-
? yield* run(fs, [
|
|
711
|
-
"git",
|
|
712
|
-
"-C",
|
|
713
|
-
checkoutPath,
|
|
714
|
-
"symbolic-ref",
|
|
715
|
-
"--quiet",
|
|
716
|
-
"--short",
|
|
717
|
-
"HEAD",
|
|
718
|
-
])
|
|
712
|
+
? yield* backend.workspaceHead(checkoutPath)
|
|
719
713
|
: null,
|
|
714
|
+
checkoutBranch: null,
|
|
720
715
|
dirty: materialized
|
|
721
|
-
? (
|
|
722
|
-
status === null ? null : status.length > 0)(
|
|
723
|
-
yield* runText(fs, [
|
|
724
|
-
"git",
|
|
725
|
-
"-C",
|
|
726
|
-
checkoutPath,
|
|
727
|
-
"status",
|
|
728
|
-
"--porcelain",
|
|
729
|
-
]),
|
|
730
|
-
)
|
|
716
|
+
? yield* backend.workspaceDirty(checkoutPath)
|
|
731
717
|
: null,
|
|
732
718
|
}
|
|
733
719
|
: {
|
|
734
720
|
branch: data.branch,
|
|
735
721
|
base: data.base,
|
|
736
|
-
branchCommit: yield*
|
|
737
|
-
"git",
|
|
738
|
-
"-C",
|
|
722
|
+
branchCommit: yield* backend.resolveRevision(
|
|
739
723
|
repositoryPath,
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
baseCommit: yield* run(fs, [
|
|
744
|
-
"git",
|
|
745
|
-
"-C",
|
|
724
|
+
data.branch,
|
|
725
|
+
),
|
|
726
|
+
baseCommit: yield* backend.resolveRevision(
|
|
746
727
|
repositoryPath,
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
]),
|
|
728
|
+
data.base,
|
|
729
|
+
),
|
|
750
730
|
checkoutCommit: materialized
|
|
751
|
-
? yield*
|
|
752
|
-
"git",
|
|
753
|
-
"-C",
|
|
754
|
-
checkoutPath,
|
|
755
|
-
"rev-parse",
|
|
756
|
-
"HEAD",
|
|
757
|
-
])
|
|
758
|
-
: null,
|
|
759
|
-
checkoutBranch: materialized
|
|
760
|
-
? yield* run(fs, [
|
|
761
|
-
"git",
|
|
762
|
-
"-C",
|
|
763
|
-
checkoutPath,
|
|
764
|
-
"symbolic-ref",
|
|
765
|
-
"--quiet",
|
|
766
|
-
"--short",
|
|
767
|
-
"HEAD",
|
|
768
|
-
])
|
|
731
|
+
? yield* backend.workspaceHead(checkoutPath)
|
|
769
732
|
: null,
|
|
733
|
+
checkoutBranch:
|
|
734
|
+
backend.kind === "jj" && materialized
|
|
735
|
+
? data.branch
|
|
736
|
+
: actualBranch,
|
|
770
737
|
dirty: materialized
|
|
771
|
-
? (
|
|
772
|
-
status === null ? null : status.length > 0)(
|
|
773
|
-
yield* runText(fs, [
|
|
774
|
-
"git",
|
|
775
|
-
"-C",
|
|
776
|
-
checkoutPath,
|
|
777
|
-
"status",
|
|
778
|
-
"--porcelain",
|
|
779
|
-
]),
|
|
780
|
-
)
|
|
738
|
+
? yield* backend.workspaceDirty(checkoutPath)
|
|
781
739
|
: null,
|
|
782
740
|
}
|
|
783
741
|
}
|
|
@@ -97,7 +97,7 @@ describe("PullRequestService", () => {
|
|
|
97
97
|
await Bun.write(
|
|
98
98
|
path,
|
|
99
99
|
`#!/usr/bin/env bun
|
|
100
|
-
await Bun.write(${JSON.stringify(ghCallPath)}, JSON.stringify({ args: Bun.argv.slice(2), cwd: process.cwd() }))
|
|
100
|
+
await Bun.write(${JSON.stringify(ghCallPath)}, JSON.stringify({ args: Bun.argv.slice(2), cwd: process.cwd(), ...(process.env.GIT_DIR ? { gitDir: process.env.GIT_DIR } : {}) }))
|
|
101
101
|
process.stdout.write(${JSON.stringify(stdout)})
|
|
102
102
|
process.stderr.write(${JSON.stringify(stderr)})
|
|
103
103
|
process.exit(${exitCode})
|
|
@@ -110,6 +110,7 @@ process.exit(${exitCode})
|
|
|
110
110
|
(await Bun.file(ghCallPath).json()) as {
|
|
111
111
|
args: string[]
|
|
112
112
|
cwd: string
|
|
113
|
+
gitDir?: string
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
const expectRemoteBranch = async (branch: string, exists = true) => {
|
|
@@ -241,6 +242,49 @@ process.exit(${exitCode})
|
|
|
241
242
|
expect(updated.endsWith(`${body}\n`)).toBe(true)
|
|
242
243
|
})
|
|
243
244
|
|
|
245
|
+
test("creates a PR from a non-colocated jj workspace with backing Git context", async () => {
|
|
246
|
+
if (!Bun.which("jj")) return
|
|
247
|
+
await rm(join(root, "repos/agency"), { recursive: true, force: true })
|
|
248
|
+
await requireCommand([
|
|
249
|
+
"jj",
|
|
250
|
+
"git",
|
|
251
|
+
"clone",
|
|
252
|
+
"--no-colocate",
|
|
253
|
+
remotePath,
|
|
254
|
+
join(root, "repos/agency"),
|
|
255
|
+
])
|
|
256
|
+
await requireCommand(
|
|
257
|
+
["jj", "config", "set", "--repo", "user.name", "Agency Test"],
|
|
258
|
+
join(root, "repos/agency"),
|
|
259
|
+
)
|
|
260
|
+
await requireCommand(
|
|
261
|
+
["jj", "config", "set", "--repo", "user.email", "agency@example.com"],
|
|
262
|
+
join(root, "repos/agency"),
|
|
263
|
+
)
|
|
264
|
+
await Bun.write(
|
|
265
|
+
join(root, "agency.json"),
|
|
266
|
+
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
267
|
+
)
|
|
268
|
+
await createTask("jj-example", "task/jj-example")
|
|
269
|
+
const workspace = await materialize("jj-example")
|
|
270
|
+
await Bun.write(join(workspace.writablePath!, "CHANGE.md"), "jj change\n")
|
|
271
|
+
await requireCommand(
|
|
272
|
+
["jj", "describe", "-m", "feat: add jj example"],
|
|
273
|
+
workspace.writablePath!,
|
|
274
|
+
)
|
|
275
|
+
await requireCommand(["jj", "new"], workspace.writablePath!)
|
|
276
|
+
const url = "https://github.com/example/agency/pull/44"
|
|
277
|
+
await writeFakeGh({ stdout: `${url}\n` })
|
|
278
|
+
|
|
279
|
+
expect(await createPullRequest("jj-example")).toBe(url)
|
|
280
|
+
await expectRemoteBranch("task/jj-example")
|
|
281
|
+
const ghCall = await readGhCall()
|
|
282
|
+
expect(ghCall.args).toContain("--head")
|
|
283
|
+
expect(ghCall.args).toContain("--repo")
|
|
284
|
+
expect(ghCall.gitDir).toContain(".jj")
|
|
285
|
+
expect(await Bun.file(join(root, "repos/agency/.git")).exists()).toBe(false)
|
|
286
|
+
})
|
|
287
|
+
|
|
244
288
|
test("passes --draft to gh", async () => {
|
|
245
289
|
await createTask()
|
|
246
290
|
await writeFakeGh({
|
|
@@ -172,6 +172,19 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
yield* backend.push(workspace.writablePath, remote, execution.branch)
|
|
175
|
+
const defaults =
|
|
176
|
+
backend.kind === "jj"
|
|
177
|
+
? yield* backend.pullRequestDefaults(
|
|
178
|
+
workspace.writablePath,
|
|
179
|
+
execution.base,
|
|
180
|
+
)
|
|
181
|
+
: null
|
|
182
|
+
if (backend.kind === "jj" && !defaults) {
|
|
183
|
+
return yield* new PullRequestError({
|
|
184
|
+
message:
|
|
185
|
+
"Failed to derive pull request title and body from jj commits",
|
|
186
|
+
})
|
|
187
|
+
}
|
|
175
188
|
|
|
176
189
|
const remoteUrl = yield* backend.remoteUrl(
|
|
177
190
|
workspace.writablePath,
|
|
@@ -198,11 +211,15 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
|
|
|
198
211
|
repository,
|
|
199
212
|
draft,
|
|
200
213
|
vcs: config.vcs ?? "git",
|
|
214
|
+
...(defaults ? { defaults } : {}),
|
|
201
215
|
})
|
|
216
|
+
const gitEnvironment = config.delivery
|
|
217
|
+
? {}
|
|
218
|
+
: yield* backend.gitEnvironment(workspace.writablePath)
|
|
202
219
|
const created = yield* fs.runCommand(resolved.argv, {
|
|
203
220
|
cwd: workspace.writablePath,
|
|
204
221
|
captureOutput: true,
|
|
205
|
-
env: resolved.environment,
|
|
222
|
+
env: { ...gitEnvironment, ...resolved.environment },
|
|
206
223
|
})
|
|
207
224
|
if (created.exitCode !== 0) {
|
|
208
225
|
return yield* new PullRequestError({
|
|
@@ -74,21 +74,16 @@ describe("PushService", () => {
|
|
|
74
74
|
await requireCommand(["git", "commit", "-m", "Initial commit"], seed)
|
|
75
75
|
await requireCommand(["git", "remote", "add", "origin", remote], seed)
|
|
76
76
|
await requireCommand(["git", "push", "-u", "origin", "main"], seed)
|
|
77
|
-
await requireCommand(["git", "clone", "--bare", remote, repository])
|
|
78
77
|
if (vcs === "jj") {
|
|
79
|
-
await requireCommand([
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
["jj", "git", "fetch", "--remote", "origin"],
|
|
86
|
-
repository,
|
|
87
|
-
)
|
|
88
|
-
await requireCommand(
|
|
89
|
-
["jj", "bookmark", "create", "main", "-r", "main@origin"],
|
|
78
|
+
await requireCommand([
|
|
79
|
+
"jj",
|
|
80
|
+
"git",
|
|
81
|
+
"clone",
|
|
82
|
+
"--no-colocate",
|
|
83
|
+
remote,
|
|
90
84
|
repository,
|
|
91
|
-
)
|
|
85
|
+
])
|
|
86
|
+
expect(await Bun.file(join(repository, ".git")).exists()).toBe(false)
|
|
92
87
|
await requireCommand(
|
|
93
88
|
["jj", "config", "set", "--repo", "user.name", "Agency Test"],
|
|
94
89
|
repository,
|
|
@@ -97,6 +92,8 @@ describe("PushService", () => {
|
|
|
97
92
|
["jj", "config", "set", "--repo", "user.email", "agency@example.com"],
|
|
98
93
|
repository,
|
|
99
94
|
)
|
|
95
|
+
} else {
|
|
96
|
+
await requireCommand(["git", "clone", "--bare", remote, repository])
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
await runTestEffect(
|
|
@@ -119,13 +119,18 @@ describe("RepositoryService", () => {
|
|
|
119
119
|
),
|
|
120
120
|
)
|
|
121
121
|
expect((await stat(join(destination, ".jj"))).isDirectory()).toBe(true)
|
|
122
|
+
expect(await Bun.file(join(destination, ".git")).exists()).toBe(false)
|
|
122
123
|
expect(
|
|
123
124
|
await runTestEffect(
|
|
124
125
|
RepositoryService.pipe(
|
|
125
126
|
Effect.flatMap((service) => service.show("agency", root)),
|
|
126
127
|
),
|
|
127
128
|
),
|
|
128
|
-
).toMatchObject({
|
|
129
|
+
).toMatchObject({
|
|
130
|
+
kind: "repository",
|
|
131
|
+
remote: portableRemote("jj-agency"),
|
|
132
|
+
states: ["declared", "materialized"],
|
|
133
|
+
})
|
|
129
134
|
})
|
|
130
135
|
|
|
131
136
|
test("links an existing repository", async () => {
|
|
@@ -171,9 +171,12 @@ const inspectRemote = (path: string) =>
|
|
|
171
171
|
return result.exitCode === 0 ? result.stdout.trim() : null
|
|
172
172
|
})
|
|
173
173
|
|
|
174
|
-
const portableRemote = (path: string) =>
|
|
174
|
+
const portableRemote = (path: string, backend?: VersionControlBackend) =>
|
|
175
175
|
Effect.gen(function* () {
|
|
176
|
-
const
|
|
176
|
+
const backendRemote = backend
|
|
177
|
+
? yield* backend.remoteUrl(path, "origin")
|
|
178
|
+
: null
|
|
179
|
+
const remote = backendRemote ?? (yield* inspectRemote(path))
|
|
177
180
|
if (!remote) {
|
|
178
181
|
return yield* new RepositoryError({
|
|
179
182
|
message: `Repository '${path}' has no portable origin remote`,
|
|
@@ -219,7 +222,7 @@ const removalBlockers = (
|
|
|
219
222
|
Effect.gen(function* () {
|
|
220
223
|
const fs = yield* FileSystemService
|
|
221
224
|
const graph = yield* GraphService
|
|
222
|
-
const report = yield* graph.get({ cwd: startPath })
|
|
225
|
+
const report = yield* graph.get({ cwd: startPath, backend })
|
|
223
226
|
const repositoryId = `repository:${repository.alias}`
|
|
224
227
|
const references = report.edges
|
|
225
228
|
.filter(
|
|
@@ -373,50 +376,30 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
373
376
|
: resolve(startPath, remote)
|
|
374
377
|
const declaredRemote = inputIsPortable
|
|
375
378
|
? yield* validateRemote(remote)
|
|
376
|
-
: yield* portableRemote(cloneSource)
|
|
379
|
+
: yield* portableRemote(cloneSource, backend)
|
|
377
380
|
const staging = join(
|
|
378
381
|
state.root,
|
|
379
382
|
"repos",
|
|
380
383
|
`.agency-clone-${validAlias}-${process.pid}-${Date.now()}`,
|
|
381
384
|
)
|
|
382
385
|
yield* fs.createDirectory(join(state.root, "repos"))
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
386
|
+
yield* backend.cloneRepository(cloneSource, staging).pipe(
|
|
387
|
+
Effect.catchAll((cause) =>
|
|
388
|
+
fs.deleteDirectory(staging).pipe(
|
|
389
|
+
Effect.ignore,
|
|
390
|
+
Effect.zipRight(
|
|
391
|
+
Effect.fail(
|
|
392
|
+
new RepositoryError({
|
|
393
|
+
message: `Failed to clone repository '${remote}': ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
394
|
+
cause,
|
|
395
|
+
}),
|
|
396
|
+
),
|
|
397
|
+
),
|
|
398
|
+
),
|
|
399
|
+
),
|
|
393
400
|
)
|
|
394
|
-
if (cloned.exitCode !== 0) {
|
|
395
|
-
yield* fs.deleteDirectory(staging).pipe(Effect.ignore)
|
|
396
|
-
return yield* new RepositoryError({
|
|
397
|
-
message: `Failed to clone repository '${remote}': ${cloned.stderr.trim()}`,
|
|
398
|
-
})
|
|
399
|
-
}
|
|
400
|
-
yield* backend.initializeRepository(staging)
|
|
401
401
|
if (declaredRemote !== remote) {
|
|
402
|
-
|
|
403
|
-
[
|
|
404
|
-
"git",
|
|
405
|
-
"-C",
|
|
406
|
-
staging,
|
|
407
|
-
"remote",
|
|
408
|
-
"set-url",
|
|
409
|
-
"origin",
|
|
410
|
-
declaredRemote,
|
|
411
|
-
],
|
|
412
|
-
{ captureOutput: true },
|
|
413
|
-
)
|
|
414
|
-
if (setRemote.exitCode !== 0) {
|
|
415
|
-
yield* fs.deleteDirectory(staging).pipe(Effect.ignore)
|
|
416
|
-
return yield* new RepositoryError({
|
|
417
|
-
message: `Failed to record portable remote for repository '${validAlias}': ${setRemote.stderr.trim()}`,
|
|
418
|
-
})
|
|
419
|
-
}
|
|
402
|
+
yield* backend.setRemoteUrl(staging, "origin", declaredRemote)
|
|
420
403
|
}
|
|
421
404
|
const config = withDeclarations(state.config, {
|
|
422
405
|
...(state.config.repositories ?? {}),
|
|
@@ -465,19 +448,21 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
465
448
|
message: `Repository path does not exist: ${resolvedTarget}`,
|
|
466
449
|
})
|
|
467
450
|
}
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
{ captureOutput: true },
|
|
471
|
-
)
|
|
472
|
-
if (git.exitCode !== 0) {
|
|
451
|
+
const inspection = yield* backend.inspectRepository(resolvedTarget)
|
|
452
|
+
if (!inspection && backend.kind === "git") {
|
|
473
453
|
return yield* new RepositoryError({
|
|
474
|
-
message: `Path is not a
|
|
454
|
+
message: `Path is not a ${backend.kind} repository: ${resolvedTarget}`,
|
|
475
455
|
})
|
|
476
456
|
}
|
|
477
457
|
yield* backend.initializeRepository(resolvedTarget)
|
|
458
|
+
if (!(yield* backend.inspectRepository(resolvedTarget))) {
|
|
459
|
+
return yield* new RepositoryError({
|
|
460
|
+
message: `Path is not a ${backend.kind} repository: ${resolvedTarget}`,
|
|
461
|
+
})
|
|
462
|
+
}
|
|
478
463
|
const declaredRemote =
|
|
479
464
|
state.config.repositories?.[validAlias]?.remote ??
|
|
480
|
-
(yield* portableRemote(resolvedTarget))
|
|
465
|
+
(yield* portableRemote(resolvedTarget, backend))
|
|
481
466
|
const staging = join(
|
|
482
467
|
state.root,
|
|
483
468
|
"repos",
|
|
@@ -522,6 +507,8 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
522
507
|
const { root, config } = yield* WorkbaseService.pipe(
|
|
523
508
|
Effect.flatMap((service) => service.loadConfig(startPath)),
|
|
524
509
|
)
|
|
510
|
+
const versionControl = yield* VersionControlService
|
|
511
|
+
const backend = yield* versionControl.forWorkbase(root)
|
|
525
512
|
const reposPath = join(root, "repos")
|
|
526
513
|
const entries = (yield* fs.isDirectory(reposPath))
|
|
527
514
|
? (yield* fs.readDirectory(reposPath)).filter(
|
|
@@ -569,20 +556,12 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
569
556
|
const target = entry.isSymlink
|
|
570
557
|
? yield* fs.readSymlinkTarget(path)
|
|
571
558
|
: null
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
{ captureOutput: true },
|
|
575
|
-
)
|
|
576
|
-
const bare = yield* fs.runCommand(
|
|
577
|
-
["git", "-C", path, "rev-parse", "--is-bare-repository"],
|
|
578
|
-
{ captureOutput: true },
|
|
579
|
-
)
|
|
580
|
-
const remote =
|
|
581
|
-
git.exitCode === 0 ? yield* inspectRemote(path) : null
|
|
559
|
+
const inspection = yield* backend.inspectRepository(path)
|
|
560
|
+
const remote = inspection?.remote ?? null
|
|
582
561
|
const states: RepositoryState[] = []
|
|
583
562
|
if (declaredRemote) states.push("declared")
|
|
584
563
|
states.push(entry.isSymlink ? "linked" : "materialized")
|
|
585
|
-
if (
|
|
564
|
+
if (!inspection) states.push("invalid")
|
|
586
565
|
if (declaredRemote && remote !== declaredRemote)
|
|
587
566
|
states.push("remote-drifted")
|
|
588
567
|
repositories.push({
|
|
@@ -590,9 +569,7 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
590
569
|
path,
|
|
591
570
|
kind: entry.isSymlink
|
|
592
571
|
? "symlink"
|
|
593
|
-
:
|
|
594
|
-
? "bare"
|
|
595
|
-
: "repository",
|
|
572
|
+
: (inspection?.kind ?? "repository"),
|
|
596
573
|
remote,
|
|
597
574
|
declaredRemote,
|
|
598
575
|
target,
|
|
@@ -760,10 +737,12 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
760
737
|
) =>
|
|
761
738
|
Effect.gen(function* () {
|
|
762
739
|
const fs = yield* FileSystemService
|
|
740
|
+
const versionControl = yield* VersionControlService
|
|
763
741
|
const repository = yield* find(alias, startPath)
|
|
764
742
|
if (remote === undefined) return repository
|
|
765
743
|
const portable = yield* validateRemote(remote)
|
|
766
744
|
const state = yield* configState(startPath)
|
|
745
|
+
const backend = yield* versionControl.forWorkbase(state.root)
|
|
767
746
|
const config = withDeclarations(state.config, {
|
|
768
747
|
...(state.config.repositories ?? {}),
|
|
769
748
|
[repository.alias]: { remote: portable },
|
|
@@ -777,30 +756,12 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
777
756
|
const previous = repository.remote
|
|
778
757
|
const update = (value: string | null) =>
|
|
779
758
|
Effect.runPromise(
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
"remote",
|
|
787
|
-
"remove",
|
|
788
|
-
"origin",
|
|
789
|
-
]
|
|
790
|
-
: [
|
|
791
|
-
"git",
|
|
792
|
-
"-C",
|
|
793
|
-
repository.path,
|
|
794
|
-
"remote",
|
|
795
|
-
previous === null ? "add" : "set-url",
|
|
796
|
-
"origin",
|
|
797
|
-
value,
|
|
798
|
-
],
|
|
799
|
-
{ captureOutput: true },
|
|
800
|
-
),
|
|
801
|
-
).then((result) => {
|
|
802
|
-
if (result.exitCode !== 0) throw new Error(result.stderr.trim())
|
|
803
|
-
})
|
|
759
|
+
backend
|
|
760
|
+
.setRemoteUrl(repository.path, "origin", value)
|
|
761
|
+
.pipe(
|
|
762
|
+
Effect.provideService(FileSystemService, fs),
|
|
763
|
+
) as Effect.Effect<void, unknown, never>,
|
|
764
|
+
)
|
|
804
765
|
steps.push({
|
|
805
766
|
label: `update origin for repos/${repository.alias}`,
|
|
806
767
|
preflight: async () => {
|
|
@@ -810,21 +771,11 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
810
771
|
`Repository alias '${repository.alias}' changed to a linked checkout; retry the remote update`,
|
|
811
772
|
)
|
|
812
773
|
}
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
"-C",
|
|
818
|
-
repository.path,
|
|
819
|
-
"remote",
|
|
820
|
-
"get-url",
|
|
821
|
-
"origin",
|
|
822
|
-
],
|
|
823
|
-
{ captureOutput: true },
|
|
824
|
-
),
|
|
774
|
+
const currentRemote = await Effect.runPromise(
|
|
775
|
+
backend
|
|
776
|
+
.remoteUrl(repository.path, "origin")
|
|
777
|
+
.pipe(Effect.provideService(FileSystemService, fs)),
|
|
825
778
|
)
|
|
826
|
-
const currentRemote =
|
|
827
|
-
current.exitCode === 0 ? current.stdout.trim() : null
|
|
828
779
|
if (currentRemote !== previous) {
|
|
829
780
|
throw new Error(
|
|
830
781
|
`Origin for repository '${repository.alias}' changed; retry the remote update`,
|
|
@@ -847,7 +798,7 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
847
798
|
if (repository.states.includes("missing"))
|
|
848
799
|
issues.push("Local materialization is missing")
|
|
849
800
|
if (repository.states.includes("invalid"))
|
|
850
|
-
issues.push("Path is not a
|
|
801
|
+
issues.push("Path is not a valid repository")
|
|
851
802
|
if (!repository.declaredRemote)
|
|
852
803
|
issues.push("Portable remote is not declared")
|
|
853
804
|
if (repository.states.includes("remote-drifted"))
|
|
@@ -877,7 +828,7 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
877
828
|
unresolved.push({
|
|
878
829
|
alias: repository.alias,
|
|
879
830
|
state: "invalid",
|
|
880
|
-
message: `Local path for '${repository.alias}' is not a valid
|
|
831
|
+
message: `Local path for '${repository.alias}' is not a valid repository`,
|
|
881
832
|
action: `Repair the path or run 'agency repo remove ${repository.alias}' before setup`,
|
|
882
833
|
})
|
|
883
834
|
continue
|
|
@@ -936,25 +887,17 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
936
887
|
`.agency-setup-${action.alias}-${process.pid}-${Date.now()}`,
|
|
937
888
|
)
|
|
938
889
|
yield* fs.createDirectory(join(state.root, "repos"))
|
|
939
|
-
const cloned = yield*
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
...(backend.kind === "git" ? ["--bare"] : []),
|
|
944
|
-
"--",
|
|
945
|
-
action.remote,
|
|
946
|
-
from,
|
|
947
|
-
],
|
|
948
|
-
{ captureOutput: true },
|
|
949
|
-
)
|
|
950
|
-
if (cloned.exitCode !== 0) {
|
|
890
|
+
const cloned = yield* backend
|
|
891
|
+
.cloneRepository(action.remote, from)
|
|
892
|
+
.pipe(Effect.either)
|
|
893
|
+
if (Either.isLeft(cloned)) {
|
|
951
894
|
for (const item of staging)
|
|
952
895
|
yield* fs.deleteDirectory(item.from).pipe(Effect.ignore)
|
|
953
896
|
return yield* new RepositoryError({
|
|
954
|
-
message: `Failed to materialize repository '${action.alias}': ${cloned.
|
|
897
|
+
message: `Failed to materialize repository '${action.alias}': ${cloned.left instanceof Error ? cloned.left.message : String(cloned.left)}`,
|
|
898
|
+
cause: cloned.left,
|
|
955
899
|
})
|
|
956
900
|
}
|
|
957
|
-
yield* backend.initializeRepository(from)
|
|
958
901
|
staging.push({
|
|
959
902
|
alias: action.alias,
|
|
960
903
|
from,
|