History

Fix PR creation from origin-based worktrees (#3218)

Julius Marminge committed 4 months ago

Commit 335e0b51 parent 2fbbe688 files · +120 −5Browse files

Unified · Split

8 files
  1. apps/server/src/git/GitManager.test.ts +56 −0
  2. apps/server/src/git/GitManager.ts +23 −1
  3. apps/server/src/server.test.ts +1 −0
  4. apps/server/src/vcs/GitVcsDriverCore.test.ts +8 −0
  5. apps/server/src/vcs/GitVcsDriverCore.ts +18 −4
  6. apps/server/src/ws.ts +1 −0
  7. packages/contracts/src/git.test.ts +12 −0
  8. packages/contracts/src/git.ts +1 −0
apps/server/src/git/GitManager.test.ts+56−0
@@ -2239,6 +2239,62 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { · 2238 unmodified lines
22392239 }),
22402240 );
22412241
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+
22422298 it.effect(
22432299 "creates a new PR instead of reusing an unrelated fork PR with the same head branch",
22442300 () =>
apps/server/src/git/GitManager.ts+23−1
@@ -1092,6 +1092,27 @@ export const make = Effect.gen(function* () { · 1091 unmodified lines
10921092 return "main";
10931093 });
10941094
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+
10951116 const resolveCommitAndBranchSuggestion = Effect.fn("resolveCommitAndBranchSuggestion")(
10961117 function* (input: {
10971118 cwd: string;
@@ -1298,7 +1319,8 @@ export const make = Effect.gen(function* () { · 200 unmodified lines
12981319 phase: "pr",
12991320 label: `Generating ${terms.shortLabel} content...`,
13001321 });
1301− const rangeContext = yield* gitCore.readRangeContext(cwd, baseBranch);
1322+ const baseRangeRef = yield* resolveBaseRangeRef(cwd, baseBranch);
1323+ const rangeContext = yield* gitCore.readRangeContext(cwd, baseRangeRef);
13021324
13031325 const generated = yield* textGeneration.generatePrContent({
13041326 cwd,
apps/server/src/server.test.ts+1−0
@@ -6041,6 +6041,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 6040 unmodified lines
60416041 cwd: "/tmp/project",
60426042 refName: fetchedOriginCommit,
60436043 newRefName: "t3code/bootstrap-refName",
6044+ baseRefName: "main",
60446045 path: null,
60456046 });
60466047 assert.deepEqual(fetchRemote.mock.calls[0]?.[0], {
apps/server/src/vcs/GitVcsDriverCore.test.ts+8−0
@@ -568,13 +568,21 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { · 567 unmodified lines
568568 path: worktreePath,
569569 refName: resolvedBase.commitSha,
570570 newRefName: "t3code/fetched-origin",
571+ baseRefName: resolvedBase.remoteRefName,
571572 });
572573
573574 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+ );
574579 assert.equal(
575580 yield* driver.readConfigValue(worktreePath, "branch.t3code/fetched-origin.remote"),
576581 null,
577582 );
583+ const status = yield* driver.statusDetails(worktreePath);
584+ assert.equal(status.aheadCount, 0);
585+ assert.equal(status.aheadOfDefaultCount, 0);
578586 }),
579587 );
580588
apps/server/src/vcs/GitVcsDriverCore.ts+18−4
@@ -1130,16 +1130,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 1129 unmodified lines
11301130 continue;
11311131 }
11321132
1133− if (yield* branchExists(cwd, normalizedCandidate)) {
1134− return normalizedCandidate;
1135− }
1136−
11371133 if (
11381134 primaryRemoteName &&
11391135 (yield* remoteBranchExists(cwd, primaryRemoteName, normalizedCandidate))
11401136 ) {
11411137 return `${primaryRemoteName}/${normalizedCandidate}`;
11421138 }
1139+
1140+ if (yield* branchExists(cwd, normalizedCandidate)) {
1141+ return normalizedCandidate;
1142+ }
11431143 }
11441144
11451145 return null;
@@ -2178,6 +2178,20 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 1032 unmodified lines
21782178 fallbackErrorMessage: "git worktree add failed",
21792179 });
21802180
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+
21812195 return {
21822196 worktree: {
21832197 path: worktreePath,
apps/server/src/ws.ts+1−0
@@ -712,6 +712,7 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) => · 711 unmodified lines
712712 cwd: bootstrap.prepareWorktree.projectCwd,
713713 refName: worktreeBaseRef,
714714 newRefName: bootstrap.prepareWorktree.branch,
715+ baseRefName: bootstrap.prepareWorktree.baseBranch,
715716 path: null,
716717 });
717718 targetWorktreePath = worktree.worktree.path;
packages/contracts/src/git.test.ts+12−0
@@ -28,6 +28,18 @@ describe("VcsCreateWorktreeInput", () => { · 27 unmodified lines
2828 expect(parsed.newRefName).toBeUndefined();
2929 expect(parsed.refName).toBe("feature/existing");
3030 });
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+ });
3143 });
3244
3345 describe("GitPreparePullRequestThreadInput", () => {
packages/contracts/src/git.ts+1−0
@@ -137,6 +137,7 @@ export const VcsCreateWorktreeInput = Schema.Struct({ · 136 unmodified lines
137137 cwd: TrimmedNonEmptyStringSchema,
138138 refName: TrimmedNonEmptyStringSchema,
139139 newRefName: Schema.optional(TrimmedNonEmptyStringSchema),
140+ baseRefName: Schema.optional(TrimmedNonEmptyStringSchema),
140141 path: Schema.NullOr(TrimmedNonEmptyStringSchema),
141142 });
142143 export type VcsCreateWorktreeInput = typeof VcsCreateWorktreeInput.Type;