History

fix(server): skip origin fetch when creating worktrees in repos without an origin remote (#5556)

Theo Browne committed 3 months ago

Commit 331c6dc1 parent 4f5834b5 files · +143 −3Browse files

Unified · Split

5 files
  1. apps/server/src/git/GitWorkflowService.ts +8 −0
  2. apps/server/src/server.test.ts +113 −0
  3. apps/server/src/vcs/GitVcsDriver.ts +6 −0
  4. apps/server/src/vcs/GitVcsDriverCore.ts +6 −2
  5. apps/server/src/ws.ts +10 −1
apps/server/src/git/GitWorkflowService.ts+8−0
@@ -69,6 +69,10 @@ export class GitWorkflowService extends Context.Service< · 68 unmodified lines
6969 readonly cwd: string;
7070 readonly remoteName: string;
7171 }) => Effect.Effect<void, GitCommandError>;
72+ readonly remoteExists: (input: {
73+ readonly cwd: string;
74+ readonly remoteName: string;
75+ }) => Effect.Effect<boolean, GitCommandError>;
7276 readonly resolveRemoteTrackingCommit: (input: {
7377 readonly cwd: string;
7478 readonly refName: string;
@@ -303,6 +307,10 @@ export const make = Effect.gen(function* () { · 228 unmodified lines
303307 ensureGitCommand("GitWorkflowService.fetchRemote", input.cwd).pipe(
304308 Effect.andThen(git.fetchRemote(input)),
305309 ),
310+ remoteExists: (input) =>
311+ ensureGitCommand("GitWorkflowService.remoteExists", input.cwd).pipe(
312+ Effect.andThen(git.remoteExists(input)),
313+ ),
306314 resolveRemoteTrackingCommit: (input) =>
307315 ensureGitCommand("GitWorkflowService.resolveRemoteTrackingCommit", input.cwd).pipe(
308316 Effect.andThen(git.resolveRemoteTrackingCommit(input)),
apps/server/src/server.test.ts+113−0
@@ -7134,6 +7134,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 7133 unmodified lines
71347134 pr: null,
71357135 }),
71367136 );
7137+ const remoteExists = vi.fn(
7138+ (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["remoteExists"]>[0]) =>
7139+ Effect.sync(() => {
7140+ bootstrapGitOperations.push("remote-exists");
7141+ return true;
7142+ }),
7143+ );
71377144 const fetchRemote = vi.fn(
71387145 (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) =>
71397146 Effect.sync(() => {
@@ -7181,6 +7188,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 41 unmodified lines
71817188 yield* buildAppUnderTest({
71827189 layers: {
71837190 gitVcsDriver: {
7191+ remoteExists,
71847192 fetchRemote,
71857193 resolveRemoteTrackingCommit,
71867194 createWorktree,
@@ -7271,6 +7279,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 84 unmodified lines
72717279 fallbackRemoteName: "origin",
72727280 });
72737281 assert.deepEqual(bootstrapGitOperations, [
7282+ "remote-exists",
72747283 "fetch",
72757284 "resolve-remote-commit",
72767285 "create-worktree",
@@ -7299,6 +7308,110 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 22 unmodified lines
72997308 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
73007309 );
73017310
7311+ it.effect(
7312+ "falls back to the local base branch when startFromOrigin is set but no origin remote exists",
7313+ () =>
7314+ Effect.gen(function* () {
7315+ const dispatchedCommands: Array<OrchestrationCommand> = [];
7316+ const remoteExists = vi.fn(
7317+ (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["remoteExists"]>[0]) =>
7318+ Effect.succeed(false),
7319+ );
7320+ const fetchRemote = vi.fn(
7321+ (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) => Effect.void,
7322+ );
7323+ const resolveRemoteTrackingCommit = vi.fn(
7324+ (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["resolveRemoteTrackingCommit"]>[0]) =>
7325+ Effect.succeed({
7326+ commitSha: "0123456789abcdef0123456789abcdef01234567",
7327+ remoteRefName: "origin/main",
7328+ }),
7329+ );
7330+ const createWorktree = vi.fn(
7331+ (_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
7332+ Effect.succeed({
7333+ worktree: {
7334+ refName: "t3code/bootstrap-refName",
7335+ path: "/tmp/bootstrap-worktree",
7336+ },
7337+ }),
7338+ );
7339+
7340+ yield* buildAppUnderTest({
7341+ layers: {
7342+ gitVcsDriver: {
7343+ remoteExists,
7344+ fetchRemote,
7345+ resolveRemoteTrackingCommit,
7346+ createWorktree,
7347+ },
7348+ orchestrationEngine: {
7349+ dispatch: (command) =>
7350+ Effect.sync(() => {
7351+ dispatchedCommands.push(command);
7352+ return { sequence: dispatchedCommands.length };
7353+ }),
7354+ readEvents: () => Stream.empty,
7355+ },
7356+ },
7357+ });
7358+
7359+ const createdAt = "2026-01-01T00:00:00.000Z";
7360+ const wsUrl = yield* getWsServerUrl("/ws");
7361+ yield* Effect.scoped(
7362+ withWsRpcClient(wsUrl, (client) =>
7363+ client[ORCHESTRATION_WS_METHODS.dispatchCommand]({
7364+ type: "thread.turn.start",
7365+ commandId: CommandId.make("cmd-bootstrap-turn-start-no-origin"),
7366+ threadId: ThreadId.make("thread-bootstrap-no-origin"),
7367+ message: {
7368+ messageId: MessageId.make("msg-bootstrap-no-origin"),
7369+ role: "user",
7370+ text: "hello",
7371+ attachments: [],
7372+ },
7373+ modelSelection: defaultModelSelection,
7374+ runtimeMode: "full-access",
7375+ interactionMode: "default",
7376+ bootstrap: {
7377+ createThread: {
7378+ projectId: defaultProjectId,
7379+ title: "Bootstrap Thread",
7380+ modelSelection: defaultModelSelection,
7381+ runtimeMode: "full-access",
7382+ interactionMode: "default",
7383+ branch: "main",
7384+ worktreePath: null,
7385+ createdAt,
7386+ },
7387+ prepareWorktree: {
7388+ projectCwd: "/tmp/project",
7389+ baseBranch: "main",
7390+ branch: "t3code/bootstrap-refName",
7391+ startFromOrigin: true,
7392+ },
7393+ },
7394+ createdAt,
7395+ }),
7396+ ),
7397+ );
7398+
7399+ assert.deepEqual(remoteExists.mock.calls[0]?.[0], {
7400+ cwd: "/tmp/project",
7401+ remoteName: "origin",
7402+ });
7403+ assert.equal(fetchRemote.mock.calls.length, 0);
7404+ assert.equal(resolveRemoteTrackingCommit.mock.calls.length, 0);
7405+ assert.deepEqual(createWorktree.mock.calls[0]?.[0], {
7406+ cwd: "/tmp/project",
7407+ refName: "main",
7408+ newRefName: "t3code/bootstrap-refName",
7409+ baseRefName: "main",
7410+ path: null,
7411+ });
7412+ }).pipe(Effect.provide(NodeHttpServer.layerTest)),
7413+ );
7414+
73027415 it.effect("records setup-script failures without aborting bootstrap turn start", () =>
73037416 Effect.gen(function* () {
73047417 const dispatchedCommands: Array<OrchestrationCommand> = [];
apps/server/src/vcs/GitVcsDriver.ts+6−0
@@ -168,6 +168,11 @@ export interface GitFetchRemoteInput { · 167 unmodified lines
168168 remoteName: string;
169169 }
170170
171+export interface GitRemoteExistsInput {
172+ cwd: string;
173+ remoteName: string;
174+}
175+
171176 export interface GitResolveRemoteTrackingCommitInput {
172177 cwd: string;
173178 refName: string;
@@ -243,6 +248,7 @@ export class GitVcsDriver extends Context.Service< · 69 unmodified lines
243248 readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect<string, GitCommandError>;
244249 readonly resolvePrimaryRemoteName: (cwd: string) => Effect.Effect<string, GitCommandError>;
245250 readonly fetchRemote: (input: GitFetchRemoteInput) => Effect.Effect<void, GitCommandError>;
251+ readonly remoteExists: (input: GitRemoteExistsInput) => Effect.Effect<boolean, GitCommandError>;
246252 readonly resolveRemoteTrackingCommit: (
247253 input: GitResolveRemoteTrackingCommitInput,
248254 ) => Effect.Effect<GitResolveRemoteTrackingCommitResult, GitCommandError>;
apps/server/src/vcs/GitVcsDriverCore.ts+6−2
@@ -1284,11 +1284,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 1283 unmodified lines
12841284 },
12851285 ).pipe(Effect.map((result) => result.exitCode === 0));
12861286
1287− const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> =>
1288− executeGit("GitVcsDriver.originRemoteExists", cwd, ["remote", "get-url", "origin"], {
1287+ const remoteExists: GitVcsDriver.GitVcsDriver["Service"]["remoteExists"] = (input) =>
1288+ executeGit("GitVcsDriver.remoteExists", input.cwd, ["remote", "get-url", input.remoteName], {
12891289 allowNonZeroExit: true,
12901290 }).pipe(Effect.map((result) => result.exitCode === 0));
12911291
1292+ const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> =>
1293+ remoteExists({ cwd, remoteName: "origin" });
1294+
12921295 const listRemoteNames = (cwd: string): Effect.Effect<ReadonlyArray<string>, GitCommandError> =>
12931296 runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe(
12941297 Effect.map(parseRemoteNamesInGitOrder),
@@ -3071,6 +3074,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* · 1776 unmodified lines
30713074 ensureRemote: (input) => withListRefsInvalidation(input.cwd, ensureRemote(input)),
30723075 resolvePrimaryRemoteName,
30733076 fetchRemote: (input) => withListRefsInvalidation(input.cwd, fetchRemote(input)),
3077+ remoteExists,
30743078 resolveRemoteTrackingCommit,
30753079 fetchRemoteBranch: (input) => withListRefsInvalidation(input.cwd, fetchRemoteBranch(input)),
30763080 fetchRemoteTrackingBranch: (input) =>
apps/server/src/ws.ts+10−1
@@ -908,7 +908,16 @@ const makeWsRpcLayer = ( · 907 unmodified lines
908908
909909 if (bootstrap?.prepareWorktree) {
910910 let worktreeBaseRef = bootstrap.prepareWorktree.baseBranch;
911− if (bootstrap.prepareWorktree.startFromOrigin) {
911+ // "Start from origin" is a stored default; repos without an
912+ // origin remote fall back to the local base branch instead of
913+ // failing the whole bootstrap on `git fetch origin`.
914+ const startFromOrigin =
915+ bootstrap.prepareWorktree.startFromOrigin === true &&
916+ (yield* gitWorkflow.remoteExists({
917+ cwd: bootstrap.prepareWorktree.projectCwd,
918+ remoteName: "origin",
919+ }));
920+ if (startFromOrigin) {
912921 yield* gitWorkflow.fetchRemote({
913922 cwd: bootstrap.prepareWorktree.projectCwd,
914923 remoteName: "origin",