Skip to content

Commit 942ef55

Browse files
t3dotggclaude
authored andcommitted
fix(server): skip origin fetch when creating worktrees in repos without an origin remote (pingdotgg#5556)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent f0b03e8 commit 942ef55

5 files changed

Lines changed: 143 additions & 3 deletions

File tree

apps/server/src/git/GitWorkflowService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export class GitWorkflowService extends Context.Service<
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* () {
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

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7140,6 +7140,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
71407140
pr: null,
71417141
}),
71427142
);
7143+
const remoteExists = vi.fn(
7144+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["remoteExists"]>[0]) =>
7145+
Effect.sync(() => {
7146+
bootstrapGitOperations.push("remote-exists");
7147+
return true;
7148+
}),
7149+
);
71437150
const fetchRemote = vi.fn(
71447151
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) =>
71457152
Effect.sync(() => {
@@ -7187,6 +7194,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
71877194
yield* buildAppUnderTest({
71887195
layers: {
71897196
gitVcsDriver: {
7197+
remoteExists,
71907198
fetchRemote,
71917199
resolveRemoteTrackingCommit,
71927200
createWorktree,
@@ -7277,6 +7285,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
72777285
fallbackRemoteName: "origin",
72787286
});
72797287
assert.deepEqual(bootstrapGitOperations, [
7288+
"remote-exists",
72807289
"fetch",
72817290
"resolve-remote-commit",
72827291
"create-worktree",
@@ -7305,6 +7314,110 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
73057314
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
73067315
);
73077316

7317+
it.effect(
7318+
"falls back to the local base branch when startFromOrigin is set but no origin remote exists",
7319+
() =>
7320+
Effect.gen(function* () {
7321+
const dispatchedCommands: Array<OrchestrationCommand> = [];
7322+
const remoteExists = vi.fn(
7323+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["remoteExists"]>[0]) =>
7324+
Effect.succeed(false),
7325+
);
7326+
const fetchRemote = vi.fn(
7327+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"]>[0]) => Effect.void,
7328+
);
7329+
const resolveRemoteTrackingCommit = vi.fn(
7330+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["resolveRemoteTrackingCommit"]>[0]) =>
7331+
Effect.succeed({
7332+
commitSha: "0123456789abcdef0123456789abcdef01234567",
7333+
remoteRefName: "origin/main",
7334+
}),
7335+
);
7336+
const createWorktree = vi.fn(
7337+
(_: Parameters<GitVcsDriver.GitVcsDriver["Service"]["createWorktree"]>[0]) =>
7338+
Effect.succeed({
7339+
worktree: {
7340+
refName: "t3code/bootstrap-refName",
7341+
path: "/tmp/bootstrap-worktree",
7342+
},
7343+
}),
7344+
);
7345+
7346+
yield* buildAppUnderTest({
7347+
layers: {
7348+
gitVcsDriver: {
7349+
remoteExists,
7350+
fetchRemote,
7351+
resolveRemoteTrackingCommit,
7352+
createWorktree,
7353+
},
7354+
orchestrationEngine: {
7355+
dispatch: (command) =>
7356+
Effect.sync(() => {
7357+
dispatchedCommands.push(command);
7358+
return { sequence: dispatchedCommands.length };
7359+
}),
7360+
readEvents: () => Stream.empty,
7361+
},
7362+
},
7363+
});
7364+
7365+
const createdAt = "2026-01-01T00:00:00.000Z";
7366+
const wsUrl = yield* getWsServerUrl("/ws");
7367+
yield* Effect.scoped(
7368+
withWsRpcClient(wsUrl, (client) =>
7369+
client[ORCHESTRATION_WS_METHODS.dispatchCommand]({
7370+
type: "thread.turn.start",
7371+
commandId: CommandId.make("cmd-bootstrap-turn-start-no-origin"),
7372+
threadId: ThreadId.make("thread-bootstrap-no-origin"),
7373+
message: {
7374+
messageId: MessageId.make("msg-bootstrap-no-origin"),
7375+
role: "user",
7376+
text: "hello",
7377+
attachments: [],
7378+
},
7379+
modelSelection: defaultModelSelection,
7380+
runtimeMode: "full-access",
7381+
interactionMode: "default",
7382+
bootstrap: {
7383+
createThread: {
7384+
projectId: defaultProjectId,
7385+
title: "Bootstrap Thread",
7386+
modelSelection: defaultModelSelection,
7387+
runtimeMode: "full-access",
7388+
interactionMode: "default",
7389+
branch: "main",
7390+
worktreePath: null,
7391+
createdAt,
7392+
},
7393+
prepareWorktree: {
7394+
projectCwd: "/tmp/project",
7395+
baseBranch: "main",
7396+
branch: "t3code/bootstrap-refName",
7397+
startFromOrigin: true,
7398+
},
7399+
},
7400+
createdAt,
7401+
}),
7402+
),
7403+
);
7404+
7405+
assert.deepEqual(remoteExists.mock.calls[0]?.[0], {
7406+
cwd: "/tmp/project",
7407+
remoteName: "origin",
7408+
});
7409+
assert.equal(fetchRemote.mock.calls.length, 0);
7410+
assert.equal(resolveRemoteTrackingCommit.mock.calls.length, 0);
7411+
assert.deepEqual(createWorktree.mock.calls[0]?.[0], {
7412+
cwd: "/tmp/project",
7413+
refName: "main",
7414+
newRefName: "t3code/bootstrap-refName",
7415+
baseRefName: "main",
7416+
path: null,
7417+
});
7418+
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
7419+
);
7420+
73087421
it.effect("records setup-script failures without aborting bootstrap turn start", () =>
73097422
Effect.gen(function* () {
73107423
const dispatchedCommands: Array<OrchestrationCommand> = [];

apps/server/src/vcs/GitVcsDriver.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ export interface GitFetchRemoteInput {
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<
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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,11 +1286,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
12861286
},
12871287
).pipe(Effect.map((result) => result.exitCode === 0));
12881288

1289-
const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> =>
1290-
executeGit("GitVcsDriver.originRemoteExists", cwd, ["remote", "get-url", "origin"], {
1289+
const remoteExists: GitVcsDriver.GitVcsDriver["Service"]["remoteExists"] = (input) =>
1290+
executeGit("GitVcsDriver.remoteExists", input.cwd, ["remote", "get-url", input.remoteName], {
12911291
allowNonZeroExit: true,
12921292
}).pipe(Effect.map((result) => result.exitCode === 0));
12931293

1294+
const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> =>
1295+
remoteExists({ cwd, remoteName: "origin" });
1296+
12941297
const listRemoteNames = (cwd: string): Effect.Effect<ReadonlyArray<string>, GitCommandError> =>
12951298
runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe(
12961299
Effect.map(parseRemoteNamesInGitOrder),
@@ -3073,6 +3076,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
30733076
ensureRemote: (input) => withListRefsInvalidation(input.cwd, ensureRemote(input)),
30743077
resolvePrimaryRemoteName,
30753078
fetchRemote: (input) => withListRefsInvalidation(input.cwd, fetchRemote(input)),
3079+
remoteExists,
30763080
resolveRemoteTrackingCommit,
30773081
fetchRemoteBranch: (input) => withListRefsInvalidation(input.cwd, fetchRemoteBranch(input)),
30783082
fetchRemoteTrackingBranch: (input) =>

apps/server/src/ws.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,16 @@ const makeWsRpcLayer = (
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",

0 commit comments

Comments
 (0)