@markjaquith/agency 2.56.1 → 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 +1 -1
- package/src/commands/pr.test.ts +40 -1
- package/src/commands/pr.ts +6 -0
- 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/vcs-status-fast.ts +3 -8
- package/src/workbase/delivery-command.test.ts +11 -2
- package/src/workbase/delivery-command.ts +5 -1
package/README.md
CHANGED
|
@@ -142,9 +142,17 @@ or credential:
|
|
|
142
142
|
|
|
143
143
|
New workbases select `"jj"` when the `jj` executable is available and otherwise
|
|
144
144
|
select `"git"`. Existing version 2 workbases without `vcs` remain Git workbases.
|
|
145
|
-
The selected backend is a workbase-level invariant: jj workbases
|
|
146
|
-
managed clones
|
|
147
|
-
|
|
145
|
+
The selected backend is a workbase-level invariant: jj workbases create
|
|
146
|
+
non-colocated managed clones and jj workspaces, while Git workbases continue to
|
|
147
|
+
use bare repositories and Git worktrees. Existing colocated jj repositories are
|
|
148
|
+
also supported.
|
|
149
|
+
|
|
150
|
+
Agency uses jj-native commands for repository validity, workspace registration,
|
|
151
|
+
working-copy state, revisions, bookmarks, ancestry, fetch, and push. Raw Git is
|
|
152
|
+
limited to backing-store plumbing such as durable review refs and integrations
|
|
153
|
+
that require Git, including GitHub CLI commit discovery. For those operations,
|
|
154
|
+
Agency obtains the backing repository with `jj git root` and supplies it as
|
|
155
|
+
`GIT_DIR`; it does not use raw Git to infer jj workspace state.
|
|
148
156
|
|
|
149
157
|
Inspect the current backend and migration readiness with:
|
|
150
158
|
|
|
@@ -162,6 +170,9 @@ target backend, and updates `agency.json` last. Migration from jj to Git also
|
|
|
162
170
|
blocks jj-only heads that are not preserved by a bookmark or workspace. Failed
|
|
163
171
|
migrations restore the source repositories and workspaces when rollback is
|
|
164
172
|
possible and report explicit manual recovery paths otherwise.
|
|
173
|
+
Converting a non-colocated jj workbase to Git is currently blocked before any
|
|
174
|
+
mutation because its backing Git object store is inside `.jj`; first convert the
|
|
175
|
+
repositories to colocated jj so the Git store survives metadata removal.
|
|
165
176
|
|
|
166
177
|
Existing version 2 workbases without `repositories` remain valid. Run
|
|
167
178
|
`agency repo setup` to preview deterministic adoption of legacy local aliases;
|
package/package.json
CHANGED
package/src/commands/pr.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { chmod, mkdir, realpath } from "node:fs/promises"
|
|
3
|
+
import { chmod, mkdir, realpath, rm } from "node:fs/promises"
|
|
4
4
|
import { dirname, join } from "node:path"
|
|
5
5
|
import { PullRequestService } from "../services/PullRequestService"
|
|
6
6
|
import {
|
|
@@ -31,6 +31,7 @@ describe("pr command", () => {
|
|
|
31
31
|
join(bin, "gh"),
|
|
32
32
|
`#!/bin/sh
|
|
33
33
|
printf '%s\n' "$PWD" "$@" > "$GH_CAPTURE"
|
|
34
|
+
if [ -n "$GIT_DIR" ]; then printf 'GIT_DIR=%s\n' "$GIT_DIR" >> "$GH_CAPTURE"; fi
|
|
34
35
|
exit "\${GH_EXIT:-0}"
|
|
35
36
|
`,
|
|
36
37
|
)
|
|
@@ -109,6 +110,41 @@ status: open
|
|
|
109
110
|
# Build
|
|
110
111
|
`,
|
|
111
112
|
)
|
|
113
|
+
if (vcs === "jj") {
|
|
114
|
+
const repository = join(root, "repos/agency")
|
|
115
|
+
await rm(join(root, "tasks/single/code/agency"), { recursive: true })
|
|
116
|
+
await rm(join(root, "tasks/multi/phases/build/code/agency"), {
|
|
117
|
+
recursive: true,
|
|
118
|
+
})
|
|
119
|
+
const initialized = Bun.spawn(
|
|
120
|
+
["jj", "git", "init", "--no-colocate", repository],
|
|
121
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
122
|
+
)
|
|
123
|
+
if ((await initialized.exited) !== 0)
|
|
124
|
+
throw new Error(await new Response(initialized.stderr).text())
|
|
125
|
+
for (const [name, path] of [
|
|
126
|
+
["single", join(root, "tasks/single/code/agency")],
|
|
127
|
+
["build", join(root, "tasks/multi/phases/build/code/agency")],
|
|
128
|
+
] as const) {
|
|
129
|
+
const workspace = Bun.spawn(
|
|
130
|
+
[
|
|
131
|
+
"jj",
|
|
132
|
+
"-R",
|
|
133
|
+
repository,
|
|
134
|
+
"workspace",
|
|
135
|
+
"add",
|
|
136
|
+
"--name",
|
|
137
|
+
name,
|
|
138
|
+
"-r",
|
|
139
|
+
"root()",
|
|
140
|
+
path,
|
|
141
|
+
],
|
|
142
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
143
|
+
)
|
|
144
|
+
if ((await workspace.exited) !== 0)
|
|
145
|
+
throw new Error(await new Response(workspace.stderr).text())
|
|
146
|
+
}
|
|
147
|
+
}
|
|
112
148
|
return root
|
|
113
149
|
}
|
|
114
150
|
|
|
@@ -187,6 +223,7 @@ status: open
|
|
|
187
223
|
"--repo",
|
|
188
224
|
"example/agency",
|
|
189
225
|
"--web",
|
|
226
|
+
expect.stringMatching(/^GIT_DIR=.*\.jj/),
|
|
190
227
|
])
|
|
191
228
|
|
|
192
229
|
expect(
|
|
@@ -202,6 +239,7 @@ status: open
|
|
|
202
239
|
"example/agency",
|
|
203
240
|
"--title",
|
|
204
241
|
"Example",
|
|
242
|
+
expect.stringMatching(/^GIT_DIR=.*\.jj/),
|
|
205
243
|
])
|
|
206
244
|
})
|
|
207
245
|
|
|
@@ -215,6 +253,7 @@ status: open
|
|
|
215
253
|
await realpath(join(task, "code/agency")),
|
|
216
254
|
"pr",
|
|
217
255
|
...args,
|
|
256
|
+
expect.stringMatching(/^GIT_DIR=.*\.jj/),
|
|
218
257
|
])
|
|
219
258
|
})
|
|
220
259
|
|
package/src/commands/pr.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve } from "node:path"
|
|
|
3
3
|
import { ContextService } from "../services/ContextService"
|
|
4
4
|
import { FileSystemService } from "../services/FileSystemService"
|
|
5
5
|
import { PullRequestService } from "../services/PullRequestService"
|
|
6
|
+
import { VersionControlService } from "../services/VersionControlService"
|
|
6
7
|
import { WorkbaseService } from "../services/WorkbaseService"
|
|
7
8
|
import type { BaseCommandOptions } from "../utils/command"
|
|
8
9
|
import { createLoggers } from "../utils/effect"
|
|
@@ -84,6 +85,7 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
|
|
|
84
85
|
const contexts = yield* ContextService
|
|
85
86
|
const fs = yield* FileSystemService
|
|
86
87
|
const workbase = yield* WorkbaseService
|
|
88
|
+
const versionControl = yield* VersionControlService
|
|
87
89
|
const invocationCwd = resolve(cwd)
|
|
88
90
|
const context = yield* contexts
|
|
89
91
|
.get({ cwd: invocationCwd, target: ".", compact: true })
|
|
@@ -94,6 +96,7 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
|
|
|
94
96
|
: null
|
|
95
97
|
const focusedCwd = writableCheckout ?? invocationCwd
|
|
96
98
|
let forwardedArgs = args
|
|
99
|
+
let environment: Record<string, string> = {}
|
|
97
100
|
if (
|
|
98
101
|
writableCheckout &&
|
|
99
102
|
context?.workbase.vcs === "jj" &&
|
|
@@ -111,10 +114,13 @@ export const pr = (args: readonly string[], cwd: string = process.cwd()) =>
|
|
|
111
114
|
repositoryFromRemote(remote),
|
|
112
115
|
)
|
|
113
116
|
}
|
|
117
|
+
const backend = yield* versionControl.forWorkbase(context.workbase.root)
|
|
118
|
+
environment = yield* backend.gitEnvironment(writableCheckout)
|
|
114
119
|
}
|
|
115
120
|
const result = yield* fs.runCommand(["gh", "pr", ...forwardedArgs], {
|
|
116
121
|
cwd: focusedCwd,
|
|
117
122
|
passthrough: true,
|
|
123
|
+
env: environment,
|
|
118
124
|
})
|
|
119
125
|
return result.exitCode
|
|
120
126
|
})
|
|
@@ -973,17 +973,34 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
973
973
|
...(yield* inspectCheckout(repositoryPath, checkoutPath)),
|
|
974
974
|
})
|
|
975
975
|
}
|
|
976
|
-
const
|
|
977
|
-
?
|
|
978
|
-
"ls-remote",
|
|
979
|
-
"origin",
|
|
980
|
-
reviewData.source.kind === "pull-request"
|
|
981
|
-
? reviewData.source.fetchRef
|
|
982
|
-
: reviewData.source.ref
|
|
983
|
-
.replace(/^refs\/remotes\/origin\//, "")
|
|
984
|
-
.replace(/^origin\//, ""),
|
|
985
|
-
])
|
|
976
|
+
const reviewRepository = reviewData
|
|
977
|
+
? repositories.get(reviewData.repo)
|
|
986
978
|
: null
|
|
979
|
+
const reviewSourceCommit =
|
|
980
|
+
reviewData && reviewRepository?.remote
|
|
981
|
+
? yield* fs
|
|
982
|
+
.runCommand(
|
|
983
|
+
[
|
|
984
|
+
"git",
|
|
985
|
+
"ls-remote",
|
|
986
|
+
reviewRepository.remote,
|
|
987
|
+
reviewData.source.kind === "pull-request"
|
|
988
|
+
? reviewData.source.fetchRef
|
|
989
|
+
: reviewData.source.ref
|
|
990
|
+
.replace(/^refs\/remotes\/origin\//, "")
|
|
991
|
+
.replace(/^origin\//, ""),
|
|
992
|
+
],
|
|
993
|
+
{ captureOutput: true },
|
|
994
|
+
)
|
|
995
|
+
.pipe(
|
|
996
|
+
Effect.map((result) =>
|
|
997
|
+
result.exitCode === 0
|
|
998
|
+
? result.stdout.trim() || null
|
|
999
|
+
: null,
|
|
1000
|
+
),
|
|
1001
|
+
Effect.catchAll(() => Effect.succeed(null)),
|
|
1002
|
+
)
|
|
1003
|
+
: null
|
|
987
1004
|
|
|
988
1005
|
const checkoutStates = [writable, ...referenceCheckouts].filter(
|
|
989
1006
|
(value): value is NonNullable<typeof value> => value !== null,
|
|
@@ -396,17 +396,12 @@ export class DoctorService extends Effect.Service<DoctorService>()(
|
|
|
396
396
|
for (const source of reviewSources.filter(
|
|
397
397
|
(item) => item.repo === repository.alias,
|
|
398
398
|
)) {
|
|
399
|
-
const observed =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
"origin",
|
|
406
|
-
source.ref,
|
|
407
|
-
],
|
|
408
|
-
{ captureOutput: true },
|
|
409
|
-
)
|
|
399
|
+
const observed = repository.remote
|
|
400
|
+
? yield* fs.runCommand(
|
|
401
|
+
["git", "ls-remote", repository.remote, source.ref],
|
|
402
|
+
{ captureOutput: true },
|
|
403
|
+
)
|
|
404
|
+
: { exitCode: -1, stdout: "", stderr: "origin unavailable" }
|
|
410
405
|
const available =
|
|
411
406
|
observed.exitCode === 0 && Boolean(observed.stdout.trim())
|
|
412
407
|
add({
|
|
@@ -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 () => {
|