Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2239,6 +2239,62 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
}),
);

it.effect("generates PR content against the remote base when the local base is stale", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
const remoteDir = yield* createBareRemote();
yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
yield* runGit(repoDir, ["push", "-u", "origin", "main"]);
yield* runGit(remoteDir, ["symbolic-ref", "HEAD", "refs/heads/main"]);

const peerDir = yield* makeTempDir("t3code-git-peer-");
yield* runGit(peerDir, ["clone", remoteDir, "."]);
yield* runGit(peerDir, ["config", "user.email", "peer@example.com"]);
yield* runGit(peerDir, ["config", "user.name", "Peer User"]);
fs.writeFileSync(path.join(peerDir, "remote.txt"), "remote\n");
yield* runGit(peerDir, ["add", "remote.txt"]);
yield* runGit(peerDir, ["commit", "-m", "Remote base commit"]);
yield* runGit(peerDir, ["push", "origin", "main"]);

yield* runGit(repoDir, ["fetch", "origin"]);
yield* runGit(repoDir, [
"checkout",
"--no-track",
"-b",
"feature/remote-base",
"origin/main",
]);
fs.writeFileSync(path.join(repoDir, "feature.txt"), "feature\n");
yield* runGit(repoDir, ["add", "feature.txt"]);
yield* runGit(repoDir, ["commit", "-m", "Feature commit"]);
yield* runGit(repoDir, ["push", "-u", "origin", "feature/remote-base"]);
yield* runGit(repoDir, ["config", "branch.feature/remote-base.gh-merge-base", "main"]);

let generatedCommitSummary = "";
const { manager } = yield* makeManager({
ghScenario: {
prListSequence: ["[]", "[]"],
},
textGeneration: {
generatePrContent: (input) => {
generatedCommitSummary = input.commitSummary;
return Effect.succeed({ title: "Feature PR", body: "Feature body" });
},
},
});

const result = yield* runStackedAction(manager, {
cwd: repoDir,
action: "create_pr",
});

expect(result.pr.status).toBe("created");
expect(generatedCommitSummary).toContain("Feature commit");
expect(generatedCommitSummary).not.toContain("Remote base commit");
}),
);

it.effect(
"creates a new PR instead of reusing an unrelated fork PR with the same head branch",
() =>
Expand Down
24 changes: 23 additions & 1 deletion apps/server/src/git/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,27 @@ export const make = Effect.gen(function* () {
return "main";
});

const resolveBaseRangeRef = Effect.fn("resolveBaseRangeRef")(function* (
cwd: string,
baseBranch: string,
) {
const remoteName = yield* gitCore
.resolvePrimaryRemoteName(cwd)
.pipe(Effect.orElseSucceed(() => null));
if (!remoteName) return baseBranch;

return yield* gitCore
.resolveRemoteTrackingCommit({
cwd,
refName: baseBranch,
fallbackRemoteName: remoteName,
})
.pipe(
Effect.map((resolved) => resolved.commitSha),
Effect.orElseSucceed(() => baseBranch),
);
});

const resolveCommitAndBranchSuggestion = Effect.fn("resolveCommitAndBranchSuggestion")(
function* (input: {
cwd: string;
Expand Down Expand Up @@ -1298,7 +1319,8 @@ export const make = Effect.gen(function* () {
phase: "pr",
label: `Generating ${terms.shortLabel} content...`,
});
const rangeContext = yield* gitCore.readRangeContext(cwd, baseBranch);
const baseRangeRef = yield* resolveBaseRangeRef(cwd, baseBranch);
const rangeContext = yield* gitCore.readRangeContext(cwd, baseRangeRef);

const generated = yield* textGeneration.generatePrContent({
cwd,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6041,6 +6041,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
cwd: "/tmp/project",
refName: fetchedOriginCommit,
newRefName: "t3code/bootstrap-refName",
baseRefName: "main",
path: null,
});
assert.deepEqual(fetchRemote.mock.calls[0]?.[0], {
Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,13 +568,21 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
path: worktreePath,
refName: resolvedBase.commitSha,
newRefName: "t3code/fetched-origin",
baseRefName: resolvedBase.remoteRefName,
});

assert.equal(yield* git(worktreePath, ["rev-parse", "HEAD"]), remoteHead);
assert.equal(
yield* driver.readConfigValue(worktreePath, "branch.t3code/fetched-origin.gh-merge-base"),
initialBranch,
);
assert.equal(
yield* driver.readConfigValue(worktreePath, "branch.t3code/fetched-origin.remote"),
null,
);
const status = yield* driver.statusDetails(worktreePath);
assert.equal(status.aheadCount, 0);
assert.equal(status.aheadOfDefaultCount, 0);
}),
);

Expand Down
22 changes: 18 additions & 4 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,16 +1130,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
continue;
}

if (yield* branchExists(cwd, normalizedCandidate)) {
return normalizedCandidate;
}

if (
primaryRemoteName &&
(yield* remoteBranchExists(cwd, primaryRemoteName, normalizedCandidate))
) {
return `${primaryRemoteName}/${normalizedCandidate}`;
}

if (yield* branchExists(cwd, normalizedCandidate)) {
return normalizedCandidate;
}
Comment thread
juliusmarminge marked this conversation as resolved.
}

return null;
Expand Down Expand Up @@ -2178,6 +2178,20 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
fallbackErrorMessage: "git worktree add failed",
});

if (input.newRefName && input.baseRefName) {
const remoteNames = yield* listRemoteNames(input.cwd).pipe(Effect.orElseSucceed(() => []));
const parsedBaseRef = parseRemoteRefWithRemoteNames(
input.baseRefName,
remoteNames.toSorted((left, right) => right.length - left.length),
);
const baseBranch = parsedBaseRef?.branchName ?? input.baseRefName;
yield* runGit("GitVcsDriver.createWorktree.configureBaseRef", input.cwd, [
"config",
`branch.${input.newRefName}.gh-merge-base`,
baseBranch,
]);
}

return {
worktree: {
path: worktreePath,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,7 @@ const makeWsRpcLayer = (currentSession: EnvironmentAuth.AuthenticatedSession) =>
cwd: bootstrap.prepareWorktree.projectCwd,
refName: worktreeBaseRef,
newRefName: bootstrap.prepareWorktree.branch,
baseRefName: bootstrap.prepareWorktree.baseBranch,
path: null,
});
targetWorktreePath = worktree.worktree.path;
Expand Down
12 changes: 12 additions & 0 deletions packages/contracts/src/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ describe("VcsCreateWorktreeInput", () => {
expect(parsed.newRefName).toBeUndefined();
expect(parsed.refName).toBe("feature/existing");
});

it("accepts baseRefName metadata for a new worktree ref", () => {
const parsed = decodeCreateWorktreeInput({
cwd: "/repo",
refName: "0123456789abcdef",
newRefName: "feature/new",
baseRefName: "origin/main",
path: "/tmp/worktree",
});

expect(parsed.baseRefName).toBe("origin/main");
});
});

describe("GitPreparePullRequestThreadInput", () => {
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const VcsCreateWorktreeInput = Schema.Struct({
cwd: TrimmedNonEmptyStringSchema,
refName: TrimmedNonEmptyStringSchema,
newRefName: Schema.optional(TrimmedNonEmptyStringSchema),
baseRefName: Schema.optional(TrimmedNonEmptyStringSchema),
path: Schema.NullOr(TrimmedNonEmptyStringSchema),
});
export type VcsCreateWorktreeInput = typeof VcsCreateWorktreeInput.Type;
Expand Down
Loading