Unified · Split
| @@ -2239,6 +2239,62 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { · 2238 unmodified lines | ||
| 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 | () => |
| @@ -1092,6 +1092,27 @@ export const make = Effect.gen(function* () { · 1091 unmodified lines | ||
| 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* () { · 200 unmodified lines | ||
| 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, |
| @@ -6041,6 +6041,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 6040 unmodified lines | ||
| 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], { |
| @@ -568,13 +568,21 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { · 567 unmodified lines | ||
| 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 | |
| @@ -1130,16 +1130,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 1129 unmodified lines | ||
| 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* · 1032 unmodified lines | ||
| 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, |
| @@ -712,6 +712,7 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => · 711 unmodified lines | ||
| 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; |
| @@ -28,6 +28,18 @@ describe("VcsCreateWorktreeInput", () => { · 27 unmodified lines | ||
| 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", () => { |
| @@ -137,6 +137,7 @@ export const VcsCreateWorktreeInput = Schema.Struct({ · 136 unmodified lines | ||
| 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; |