| author | |
| committer | |
| log | 335e0b59ea8a1bbc381402d1fbea268e05095f74 |
| tree | d9963e730eca030ae1441b6112ce21ca024090b8 |
| parent | 2fbbe68c24892c4de9bd50450e7363376475ad7d |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius@mac.lan>8 files changed, 120 insertions(+), 5 deletions(-)
apps/server/src/git/GitManager.test.ts+56| ... | ... | @@ -2239,6 +2239,62 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { |
| 2239 | 2239 | }), |
| 2240 | 2240 | ); |
| 2241 | 2241 | |
| 2242 | it.effect("generates PR content against the remote base when the local base is stale", () => | |
| 2243 | Effect.gen(function* () { | |
| 2244 | const repoDir = yield* makeTempDir("t3code-git-manager-"); | |
| 2245 | yield* initRepo(repoDir); | |
| 2246 | const remoteDir = yield* createBareRemote(); | |
| 2247 | yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]); | |
| 2248 | yield* runGit(repoDir, ["push", "-u", "origin", "main"]); | |
| 2249 | yield* runGit(remoteDir, ["symbolic-ref", "HEAD", "refs/heads/main"]); | |
| 2250 | ||
| 2251 | const peerDir = yield* makeTempDir("t3code-git-peer-"); | |
| 2252 | yield* runGit(peerDir, ["clone", remoteDir, "."]); | |
| 2253 | yield* runGit(peerDir, ["config", "user.email", "peer@example.com"]); | |
| 2254 | yield* runGit(peerDir, ["config", "user.name", "Peer User"]); | |
| 2255 | fs.writeFileSync(path.join(peerDir, "remote.txt"), "remote\n"); | |
| 2256 | yield* runGit(peerDir, ["add", "remote.txt"]); | |
| 2257 | yield* runGit(peerDir, ["commit", "-m", "Remote base commit"]); | |
| 2258 | yield* runGit(peerDir, ["push", "origin", "main"]); | |
| 2259 | ||
| 2260 | yield* runGit(repoDir, ["fetch", "origin"]); | |
| 2261 | yield* runGit(repoDir, [ | |
| 2262 | "checkout", | |
| 2263 | "--no-track", | |
| 2264 | "-b", | |
| 2265 | "feature/remote-base", | |
| 2266 | "origin/main", | |
| 2267 | ]); | |
| 2268 | fs.writeFileSync(path.join(repoDir, "feature.txt"), "feature\n"); | |
| 2269 | yield* runGit(repoDir, ["add", "feature.txt"]); | |
| 2270 | yield* runGit(repoDir, ["commit", "-m", "Feature commit"]); | |
| 2271 | yield* runGit(repoDir, ["push", "-u", "origin", "feature/remote-base"]); | |
| 2272 | yield* runGit(repoDir, ["config", "branch.feature/remote-base.gh-merge-base", "main"]); | |
| 2273 | ||
| 2274 | let generatedCommitSummary = ""; | |
| 2275 | const { manager } = yield* makeManager({ | |
| 2276 | ghScenario: { | |
| 2277 | prListSequence: ["[]", "[]"], | |
| 2278 | }, | |
| 2279 | textGeneration: { | |
| 2280 | generatePrContent: (input) => { | |
| 2281 | generatedCommitSummary = input.commitSummary; | |
| 2282 | return Effect.succeed({ title: "Feature PR", body: "Feature body" }); | |
| 2283 | }, | |
| 2284 | }, | |
| 2285 | }); | |
| 2286 | ||
| 2287 | const result = yield* runStackedAction(manager, { | |
| 2288 | cwd: repoDir, | |
| 2289 | action: "create_pr", | |
| 2290 | }); | |
| 2291 | ||
| 2292 | expect(result.pr.status).toBe("created"); | |
| 2293 | expect(generatedCommitSummary).toContain("Feature commit"); | |
| 2294 | expect(generatedCommitSummary).not.toContain("Remote base commit"); | |
| 2295 | }), | |
| 2296 | ); | |
| 2297 | ||
| 2242 | 2298 | it.effect( |
| 2243 | 2299 | "creates a new PR instead of reusing an unrelated fork PR with the same head branch", |
| 2244 | 2300 | () => |
apps/server/src/git/GitManager.ts+23-1| ... | ... | @@ -1092,6 +1092,27 @@ export const make = Effect.gen(function* () { |
| 1092 | 1092 | return "main"; |
| 1093 | 1093 | }); |
| 1094 | 1094 | |
| 1095 | const resolveBaseRangeRef = Effect.fn("resolveBaseRangeRef")(function* ( | |
| 1096 | cwd: string, | |
| 1097 | baseBranch: string, | |
| 1098 | ) { | |
| 1099 | const remoteName = yield* gitCore | |
| 1100 | .resolvePrimaryRemoteName(cwd) | |
| 1101 | .pipe(Effect.orElseSucceed(() => null)); | |
| 1102 | if (!remoteName) return baseBranch; | |
| 1103 | ||
| 1104 | return yield* gitCore | |
| 1105 | .resolveRemoteTrackingCommit({ | |
| 1106 | cwd, | |
| 1107 | refName: baseBranch, | |
| 1108 | fallbackRemoteName: remoteName, | |
| 1109 | }) | |
| 1110 | .pipe( | |
| 1111 | Effect.map((resolved) => resolved.commitSha), | |
| 1112 | Effect.orElseSucceed(() => baseBranch), | |
| 1113 | ); | |
| 1114 | }); | |
| 1115 | ||
| 1095 | 1116 | const resolveCommitAndBranchSuggestion = Effect.fn("resolveCommitAndBranchSuggestion")( |
| 1096 | 1117 | function* (input: { |
| 1097 | 1118 | cwd: string; |
| ... | ... | @@ -1298,7 +1319,8 @@ export const make = Effect.gen(function* () { |
| 1298 | 1319 | phase: "pr", |
| 1299 | 1320 | label: `Generating ${terms.shortLabel} content...`, |
| 1300 | 1321 | }); |
| 1301 | const rangeContext = yield* gitCore.readRangeContext(cwd, baseBranch); | |
| 1322 | const baseRangeRef = yield* resolveBaseRangeRef(cwd, baseBranch); | |
| 1323 | const rangeContext = yield* gitCore.readRangeContext(cwd, baseRangeRef); | |
| 1302 | 1324 | |
| 1303 | 1325 | const generated = yield* textGeneration.generatePrContent({ |
| 1304 | 1326 | cwd, |
apps/server/src/server.test.ts+1| ... | ... | @@ -6041,6 +6041,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { |
| 6041 | 6041 | cwd: "/tmp/project", |
| 6042 | 6042 | refName: fetchedOriginCommit, |
| 6043 | 6043 | newRefName: "t3code/bootstrap-refName", |
| 6044 | baseRefName: "main", | |
| 6044 | 6045 | path: null, |
| 6045 | 6046 | }); |
| 6046 | 6047 | assert.deepEqual(fetchRemote.mock.calls[0]?.[0], { |
apps/server/src/vcs/GitVcsDriverCore.test.ts+8| ... | ... | @@ -568,13 +568,21 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { |
| 568 | 568 | path: worktreePath, |
| 569 | 569 | refName: resolvedBase.commitSha, |
| 570 | 570 | newRefName: "t3code/fetched-origin", |
| 571 | baseRefName: resolvedBase.remoteRefName, | |
| 571 | 572 | }); |
| 572 | 573 | |
| 573 | 574 | assert.equal(yield* git(worktreePath, ["rev-parse", "HEAD"]), remoteHead); |
| 575 | assert.equal( | |
| 576 | yield* driver.readConfigValue(worktreePath, "branch.t3code/fetched-origin.gh-merge-base"), | |
| 577 | initialBranch, | |
| 578 | ); | |
| 574 | 579 | assert.equal( |
| 575 | 580 | yield* driver.readConfigValue(worktreePath, "branch.t3code/fetched-origin.remote"), |
| 576 | 581 | null, |
| 577 | 582 | ); |
| 583 | const status = yield* driver.statusDetails(worktreePath); | |
| 584 | assert.equal(status.aheadCount, 0); | |
| 585 | assert.equal(status.aheadOfDefaultCount, 0); | |
| 578 | 586 | }), |
| 579 | 587 | ); |
| 580 | 588 |
apps/server/src/vcs/GitVcsDriverCore.ts+18-4| ... | ... | @@ -1130,16 +1130,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 1130 | 1130 | continue; |
| 1131 | 1131 | } |
| 1132 | 1132 | |
| 1133 | if (yield* branchExists(cwd, normalizedCandidate)) { | |
| 1134 | return normalizedCandidate; | |
| 1135 | } | |
| 1136 | ||
| 1137 | 1133 | if ( |
| 1138 | 1134 | primaryRemoteName && |
| 1139 | 1135 | (yield* remoteBranchExists(cwd, primaryRemoteName, normalizedCandidate)) |
| 1140 | 1136 | ) { |
| 1141 | 1137 | return `${primaryRemoteName}/${normalizedCandidate}`; |
| 1142 | 1138 | } |
| 1139 | ||
| 1140 | if (yield* branchExists(cwd, normalizedCandidate)) { | |
| 1141 | return normalizedCandidate; | |
| 1142 | } | |
| 1143 | 1143 | } |
| 1144 | 1144 | |
| 1145 | 1145 | return null; |
| ... | ... | @@ -2178,6 +2178,20 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* |
| 2178 | 2178 | fallbackErrorMessage: "git worktree add failed", |
| 2179 | 2179 | }); |
| 2180 | 2180 | |
| 2181 | if (input.newRefName && input.baseRefName) { | |
| 2182 | const remoteNames = yield* listRemoteNames(input.cwd).pipe(Effect.orElseSucceed(() => [])); | |
| 2183 | const parsedBaseRef = parseRemoteRefWithRemoteNames( | |
| 2184 | input.baseRefName, | |
| 2185 | remoteNames.toSorted((left, right) => right.length - left.length), | |
| 2186 | ); | |
| 2187 | const baseBranch = parsedBaseRef?.branchName ?? input.baseRefName; | |
| 2188 | yield* runGit("GitVcsDriver.createWorktree.configureBaseRef", input.cwd, [ | |
| 2189 | "config", | |
| 2190 | `branch.${input.newRefName}.gh-merge-base`, | |
| 2191 | baseBranch, | |
| 2192 | ]); | |
| 2193 | } | |
| 2194 | ||
| 2181 | 2195 | return { |
| 2182 | 2196 | worktree: { |
| 2183 | 2197 | path: worktreePath, |
apps/server/src/ws.ts+1| ... | ... | @@ -712,6 +712,7 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => |
| 712 | 712 | cwd: bootstrap.prepareWorktree.projectCwd, |
| 713 | 713 | refName: worktreeBaseRef, |
| 714 | 714 | newRefName: bootstrap.prepareWorktree.branch, |
| 715 | baseRefName: bootstrap.prepareWorktree.baseBranch, | |
| 715 | 716 | path: null, |
| 716 | 717 | }); |
| 717 | 718 | targetWorktreePath = worktree.worktree.path; |
packages/contracts/src/git.test.ts+12| ... | ... | @@ -28,6 +28,18 @@ describe("VcsCreateWorktreeInput", () => { |
| 28 | 28 | expect(parsed.newRefName).toBeUndefined(); |
| 29 | 29 | expect(parsed.refName).toBe("feature/existing"); |
| 30 | 30 | }); |
| 31 | ||
| 32 | it("accepts baseRefName metadata for a new worktree ref", () => { | |
| 33 | const parsed = decodeCreateWorktreeInput({ | |
| 34 | cwd: "/repo", | |
| 35 | refName: "0123456789abcdef", | |
| 36 | newRefName: "feature/new", | |
| 37 | baseRefName: "origin/main", | |
| 38 | path: "/tmp/worktree", | |
| 39 | }); | |
| 40 | ||
| 41 | expect(parsed.baseRefName).toBe("origin/main"); | |
| 42 | }); | |
| 31 | 43 | }); |
| 32 | 44 | |
| 33 | 45 | describe("GitPreparePullRequestThreadInput", () => { |
packages/contracts/src/git.ts+1| ... | ... | @@ -137,6 +137,7 @@ export const VcsCreateWorktreeInput = Schema.Struct({ |
| 137 | 137 | cwd: TrimmedNonEmptyStringSchema, |
| 138 | 138 | refName: TrimmedNonEmptyStringSchema, |
| 139 | 139 | newRefName: Schema.optional(TrimmedNonEmptyStringSchema), |
| 140 | baseRefName: Schema.optional(TrimmedNonEmptyStringSchema), | |
| 140 | 141 | path: Schema.NullOr(TrimmedNonEmptyStringSchema), |
| 141 | 142 | }); |
| 142 | 143 | export type VcsCreateWorktreeInput = typeof VcsCreateWorktreeInput.Type; |