@markjaquith/agency 3.2.4 → 3.2.5
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { mkdir, realpath } from "node:fs/promises"
|
|
3
|
+
import { mkdir, realpath, stat } from "node:fs/promises"
|
|
4
4
|
import { dirname, join } from "node:path"
|
|
5
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
6
|
import { RepositoryService } from "./RepositoryService"
|
|
@@ -136,6 +136,30 @@ describe("RepositoryService", () => {
|
|
|
136
136
|
})
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
+
test("inspects only the requested repository alias", async () => {
|
|
140
|
+
const requested = join(root, "repos/requested")
|
|
141
|
+
const unrelated = join(root, "repos/unrelated")
|
|
142
|
+
await mkdir(requested, { recursive: true })
|
|
143
|
+
await mkdir(unrelated, { recursive: true })
|
|
144
|
+
await runGit(["init", "--initial-branch=main", requested])
|
|
145
|
+
await runGit(["init", "--initial-branch=main", unrelated])
|
|
146
|
+
|
|
147
|
+
const spawn = spyOn(Bun, "spawn")
|
|
148
|
+
try {
|
|
149
|
+
const repository = await runTestEffect(
|
|
150
|
+
RepositoryService.pipe(
|
|
151
|
+
Effect.flatMap((service) => service.show("requested", root)),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
expect(repository.alias).toBe("requested")
|
|
155
|
+
expect(spawn.mock.calls).toHaveLength(1)
|
|
156
|
+
expect(spawn.mock.calls[0]?.[0]).toContain(requested)
|
|
157
|
+
expect(spawn.mock.calls[0]?.[0]).not.toContain(unrelated)
|
|
158
|
+
} finally {
|
|
159
|
+
spawn.mockRestore()
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
139
163
|
test("materializes a linked alias without invalidating active worktrees", async () => {
|
|
140
164
|
const target = join(root, "linked-repository")
|
|
141
165
|
const checkout = join(root, "tasks/active/code/linked")
|
|
@@ -146,6 +170,13 @@ describe("RepositoryService", () => {
|
|
|
146
170
|
await Bun.write(join(target, "README.md"), "linked\n")
|
|
147
171
|
await runGit(["-C", target, "add", "README.md"])
|
|
148
172
|
await runGit(["-C", target, "commit", "-m", "initial"])
|
|
173
|
+
const commit = await gitOutput(["-C", target, "rev-parse", "HEAD"])
|
|
174
|
+
const sourceObject = join(
|
|
175
|
+
target,
|
|
176
|
+
".git/objects",
|
|
177
|
+
commit.slice(0, 2),
|
|
178
|
+
commit.slice(2),
|
|
179
|
+
)
|
|
149
180
|
await setPortableOrigin(target, "materialized")
|
|
150
181
|
await runTestEffect(
|
|
151
182
|
RepositoryService.pipe(
|
|
@@ -218,6 +249,15 @@ status: working
|
|
|
218
249
|
"refs/agency/reviews/active",
|
|
219
250
|
]),
|
|
220
251
|
).toBe(await gitOutput(["-C", target, "rev-parse", "main"]))
|
|
252
|
+
const materializedObject = join(
|
|
253
|
+
root,
|
|
254
|
+
"repos/linked/objects",
|
|
255
|
+
commit.slice(0, 2),
|
|
256
|
+
commit.slice(2),
|
|
257
|
+
)
|
|
258
|
+
expect((await stat(materializedObject)).ino).not.toBe(
|
|
259
|
+
(await stat(sourceObject)).ino,
|
|
260
|
+
)
|
|
221
261
|
})
|
|
222
262
|
|
|
223
263
|
test("preserves detached registered worktree commits from a shallow linked repository", async () => {
|
|
@@ -186,18 +186,71 @@ const portableRemote = (path: string, backend?: VersionControlBackend) =>
|
|
|
186
186
|
return yield* validateRemote(remote)
|
|
187
187
|
})
|
|
188
188
|
|
|
189
|
-
const
|
|
189
|
+
const inspectRepository = (
|
|
190
|
+
alias: string,
|
|
191
|
+
state: Effect.Effect.Success<ReturnType<typeof configState>>,
|
|
192
|
+
backend: VersionControlBackend,
|
|
193
|
+
) =>
|
|
190
194
|
Effect.gen(function* () {
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
195
|
+
const fs = yield* FileSystemService
|
|
196
|
+
const path = join(state.root, "repos", alias)
|
|
197
|
+
const declaredRemote = state.config.repositories?.[alias]?.remote ?? null
|
|
198
|
+
const entry = yield* fs.inspectFile(path)
|
|
199
|
+
if (entry.kind === "missing") {
|
|
200
|
+
if (declaredRemote) {
|
|
201
|
+
return {
|
|
202
|
+
alias,
|
|
203
|
+
path,
|
|
204
|
+
kind: null,
|
|
205
|
+
remote: null,
|
|
206
|
+
declaredRemote,
|
|
207
|
+
target: null,
|
|
208
|
+
states: ["declared", "missing"],
|
|
209
|
+
} as RepositoryInfo
|
|
210
|
+
}
|
|
196
211
|
return yield* new RepositoryError({
|
|
197
|
-
message: `Unknown repository alias '${
|
|
212
|
+
message: `Unknown repository alias '${alias}'`,
|
|
198
213
|
})
|
|
199
214
|
}
|
|
200
|
-
|
|
215
|
+
const isSymlink = entry.kind === "symlink"
|
|
216
|
+
if (!isSymlink && !(yield* fs.isDirectory(path))) {
|
|
217
|
+
return {
|
|
218
|
+
alias,
|
|
219
|
+
path,
|
|
220
|
+
kind: null,
|
|
221
|
+
remote: null,
|
|
222
|
+
declaredRemote,
|
|
223
|
+
target: null,
|
|
224
|
+
states: [...(declaredRemote ? (["declared"] as const) : []), "invalid"],
|
|
225
|
+
} as RepositoryInfo
|
|
226
|
+
}
|
|
227
|
+
const target = isSymlink ? yield* fs.readSymlinkTarget(path) : null
|
|
228
|
+
const inspection = yield* backend.inspectRepository(path)
|
|
229
|
+
const remote = inspection?.remote ?? null
|
|
230
|
+
const states: RepositoryState[] = []
|
|
231
|
+
if (declaredRemote) states.push("declared")
|
|
232
|
+
states.push(isSymlink ? "linked" : "materialized")
|
|
233
|
+
if (!inspection) states.push("invalid")
|
|
234
|
+
if (declaredRemote && remote !== declaredRemote)
|
|
235
|
+
states.push("remote-drifted")
|
|
236
|
+
return {
|
|
237
|
+
alias,
|
|
238
|
+
path,
|
|
239
|
+
kind: isSymlink ? "symlink" : (inspection?.kind ?? "repository"),
|
|
240
|
+
remote,
|
|
241
|
+
declaredRemote,
|
|
242
|
+
target,
|
|
243
|
+
states,
|
|
244
|
+
} as RepositoryInfo
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
const find = (alias: string, startPath: string) =>
|
|
248
|
+
Effect.gen(function* () {
|
|
249
|
+
const versionControl = yield* VersionControlService
|
|
250
|
+
const validAlias = yield* validateAlias(alias)
|
|
251
|
+
const state = yield* configState(startPath)
|
|
252
|
+
const backend = yield* versionControl.forWorkbase(state.root)
|
|
253
|
+
return yield* inspectRepository(validAlias, state, backend)
|
|
201
254
|
})
|
|
202
255
|
|
|
203
256
|
const requireMaterialized = (repository: RepositoryInfo) =>
|
|
@@ -450,14 +503,11 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
450
503
|
const backend = yield* versionControl.forWorkbase(state.root)
|
|
451
504
|
const destination = join(state.root, "repos", validAlias)
|
|
452
505
|
const resolvedTarget = resolve(startPath, target)
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
),
|
|
459
|
-
)
|
|
460
|
-
const current = yield* existing
|
|
506
|
+
const current =
|
|
507
|
+
state.config.repositories?.[validAlias] ||
|
|
508
|
+
(yield* fs.exists(destination))
|
|
509
|
+
? yield* inspectRepository(validAlias, state, backend)
|
|
510
|
+
: undefined
|
|
461
511
|
const localCurrent =
|
|
462
512
|
current && !current.states.includes("missing") ? current : undefined
|
|
463
513
|
if (localCurrent?.kind === "symlink") {
|
|
@@ -521,10 +571,17 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
521
571
|
Effect.gen(function* () {
|
|
522
572
|
const fs = yield* FileSystemService
|
|
523
573
|
const versionControl = yield* VersionControlService
|
|
524
|
-
const
|
|
574
|
+
const validAlias = yield* validateAlias(alias)
|
|
575
|
+
const state = yield* configState(startPath)
|
|
576
|
+
const backend = yield* versionControl.forWorkbase(state.root)
|
|
577
|
+
const repository = yield* inspectRepository(
|
|
578
|
+
validAlias,
|
|
579
|
+
state,
|
|
580
|
+
backend,
|
|
581
|
+
)
|
|
525
582
|
if (repository.kind !== "symlink" || !repository.target) {
|
|
526
583
|
return yield* new RepositoryError({
|
|
527
|
-
message: `Repository alias '${
|
|
584
|
+
message: `Repository alias '${validAlias}' is not linked`,
|
|
528
585
|
})
|
|
529
586
|
}
|
|
530
587
|
if (repository.states.includes("invalid")) {
|
|
@@ -543,9 +600,6 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
543
600
|
})
|
|
544
601
|
}
|
|
545
602
|
|
|
546
|
-
const state = yield* configState(startPath)
|
|
547
|
-
const backend = yield* versionControl.forWorkbase(state.root)
|
|
548
|
-
|
|
549
603
|
const source = yield* fs.realPath(repository.path)
|
|
550
604
|
const registered = yield* backend.listWorkspaces(repository.path)
|
|
551
605
|
const registeredState = registered
|
|
@@ -854,78 +908,22 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
854
908
|
list: (startPath: string = process.cwd()) =>
|
|
855
909
|
Effect.gen(function* () {
|
|
856
910
|
const fs = yield* FileSystemService
|
|
857
|
-
const { root, config } = yield* WorkbaseService.pipe(
|
|
858
|
-
Effect.flatMap((service) => service.loadConfig(startPath)),
|
|
859
|
-
)
|
|
860
911
|
const versionControl = yield* VersionControlService
|
|
861
|
-
const
|
|
862
|
-
const
|
|
912
|
+
const state = yield* configState(startPath)
|
|
913
|
+
const backend = yield* versionControl.forWorkbase(state.root)
|
|
914
|
+
const reposPath = join(state.root, "repos")
|
|
863
915
|
const entries = (yield* fs.isDirectory(reposPath))
|
|
864
916
|
? (yield* fs.readDirectory(reposPath)).filter(
|
|
865
917
|
(entry) => !entry.name.startsWith(".agency-"),
|
|
866
918
|
)
|
|
867
919
|
: []
|
|
868
|
-
const local = new Map(entries.map((entry) => [entry.name, entry]))
|
|
869
920
|
const aliases = new Set([
|
|
870
|
-
...Object.keys(config.repositories ?? {}),
|
|
871
|
-
...
|
|
921
|
+
...Object.keys(state.config.repositories ?? {}),
|
|
922
|
+
...entries.map((entry) => entry.name),
|
|
872
923
|
])
|
|
873
924
|
return yield* Effect.forEach(
|
|
874
925
|
[...aliases].sort(),
|
|
875
|
-
(alias) =>
|
|
876
|
-
Effect.gen(function* () {
|
|
877
|
-
const path = join(reposPath, alias)
|
|
878
|
-
const entry = local.get(alias)
|
|
879
|
-
const declaredRemote =
|
|
880
|
-
config.repositories?.[alias]?.remote ?? null
|
|
881
|
-
if (!entry) {
|
|
882
|
-
return {
|
|
883
|
-
alias,
|
|
884
|
-
path,
|
|
885
|
-
kind: null,
|
|
886
|
-
remote: null,
|
|
887
|
-
declaredRemote,
|
|
888
|
-
target: null,
|
|
889
|
-
states: ["declared", "missing"] as RepositoryState[],
|
|
890
|
-
} satisfies RepositoryInfo
|
|
891
|
-
}
|
|
892
|
-
if (!entry.isDirectory && !entry.isSymlink) {
|
|
893
|
-
return {
|
|
894
|
-
alias,
|
|
895
|
-
path,
|
|
896
|
-
kind: null,
|
|
897
|
-
remote: null,
|
|
898
|
-
declaredRemote,
|
|
899
|
-
target: null,
|
|
900
|
-
states: [
|
|
901
|
-
...(declaredRemote ? (["declared"] as const) : []),
|
|
902
|
-
"invalid",
|
|
903
|
-
] as RepositoryState[],
|
|
904
|
-
} satisfies RepositoryInfo
|
|
905
|
-
}
|
|
906
|
-
const target = entry.isSymlink
|
|
907
|
-
? yield* fs.readSymlinkTarget(path)
|
|
908
|
-
: null
|
|
909
|
-
const inspection = yield* backend.inspectRepository(path)
|
|
910
|
-
const remote = inspection?.remote ?? null
|
|
911
|
-
const states: RepositoryState[] = []
|
|
912
|
-
if (declaredRemote) states.push("declared")
|
|
913
|
-
states.push(entry.isSymlink ? "linked" : "materialized")
|
|
914
|
-
if (!inspection) states.push("invalid")
|
|
915
|
-
if (declaredRemote && remote !== declaredRemote)
|
|
916
|
-
states.push("remote-drifted")
|
|
917
|
-
return {
|
|
918
|
-
alias,
|
|
919
|
-
path,
|
|
920
|
-
kind: entry.isSymlink
|
|
921
|
-
? "symlink"
|
|
922
|
-
: (inspection?.kind ?? "repository"),
|
|
923
|
-
remote,
|
|
924
|
-
declaredRemote,
|
|
925
|
-
target,
|
|
926
|
-
states,
|
|
927
|
-
} satisfies RepositoryInfo
|
|
928
|
-
}),
|
|
926
|
+
(alias) => inspectRepository(alias, state, backend),
|
|
929
927
|
{ concurrency: 8 },
|
|
930
928
|
)
|
|
931
929
|
}),
|
|
@@ -171,7 +171,15 @@ export class GitVersionControlService extends Effect.Service<GitVersionControlSe
|
|
|
171
171
|
yield* requireSuccess(
|
|
172
172
|
"Failed to clone Git repository",
|
|
173
173
|
fs.runCommand(
|
|
174
|
-
[
|
|
174
|
+
[
|
|
175
|
+
"git",
|
|
176
|
+
"clone",
|
|
177
|
+
"--bare",
|
|
178
|
+
"--no-hardlinks",
|
|
179
|
+
"--",
|
|
180
|
+
source,
|
|
181
|
+
destination,
|
|
182
|
+
],
|
|
175
183
|
{
|
|
176
184
|
captureOutput: true,
|
|
177
185
|
},
|