Skip to content
101 changes: 99 additions & 2 deletions apps/server/src/orchestration-v2/ThreadLaunchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ function makeHarness(options: HarnessOptions = {}) {
fetchWorktreeBaseCommit:
options.fetchWorktreeBaseCommit ??
(() => Effect.succeed({ commitSha: "remote-main-sha", remoteRefName: "origin/main" })),
hasCommit: options.hasCommit ?? (() => Effect.succeed(false)),
hasCommit: options.hasCommit ?? (({ refName }) => Effect.succeed(refName === "HEAD")),
currentBranch: () => Effect.succeed(null),
resolveWorktreeBaseRemote: () => Effect.succeed({ remoteName: "origin", branchName: "main" }),
remoteExists: options.remoteExists ?? (() => Effect.succeed(true)),
Expand Down Expand Up @@ -990,7 +990,8 @@ it.effect.each([
[projectId]: { branchNamingMode: "static", branchNamePrefix: scenario.prefix },
},
},
hasCommit: ({ refName }) => Effect.succeed(refName === `refs/heads/${scenario.collision}`),
hasCommit: ({ refName }) =>
Effect.succeed(refName === "HEAD" || refName === `refs/heads/${scenario.collision}`),
generateBranchName: () =>
Deferred.succeed(namingAttempted, undefined).pipe(
Effect.andThen(
Expand Down Expand Up @@ -1288,6 +1289,102 @@ it.effect.each(["stable", "upstream/release"])(
),
);

it.effect.each([
{ name: "a repository with no commits", setup: [], baseRef: "main", worktree: false },
{
name: "an orphan branch beside remote-tracking branches",
setup: [
["commit", "--allow-empty", "-m", "initial"],
["update-ref", "refs/remotes/origin/main", "HEAD"],
["update-ref", "refs/remotes/origin/other", "HEAD"],
["checkout", "--orphan", "empty"],
["branch", "-D", "main"],
],
baseRef: "origin/main...origin/other",
worktree: true,
},
])("launches a new worktree thread from $name", (scenario) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const git = yield* GitVcsDriver.GitVcsDriver;
const directory = yield* fs.makeTempDirectoryScoped({ prefix: "launch-unborn-" });
const cwd = `${directory}/repo`;
yield* fs.makeDirectory(cwd);
for (const args of [
["init", "-b", "main"],
["config", "user.email", "test@example.com"],
["config", "user.name", "Test"],
...scenario.setup,
]) {
yield* git.execute({ operation: "launch-test", cwd, args });
}

const harness = makeHarness({
hasCommit: ({ refName }) =>
git
.execute({
operation: "launch-test",
cwd,
args: ["rev-parse", "--verify", `${refName}^{commit}`],
})
.pipe(
Effect.as(true),
Effect.orElseSucceed(() => false),
),
git: {
currentBranch: () =>
git
.execute({ operation: "launch-test", cwd, args: ["symbolic-ref", "--short", "HEAD"] })
.pipe(Effect.map((result) => result.stdout.trim())),
},
createWorktree: (input, options) =>
git.createWorktree({ ...input, cwd, path: `${directory}/worktree` }, options),
});
yield* Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
const launched = yield* launches.launch(
launchInput({
command: "command:launch:unborn",
thread: "thread:launch:unborn",
message: "Say hello",
workspace: { type: "worktree", baseRef: scenario.baseRef, branch: "feature" },
}),
);
const projection = yield* preparationSettled(launched.threadId);
assert.equal(
projection.thread.worktreePath,
scenario.worktree ? `${directory}/worktree` : null,
);
assert.equal(projection.runs[0]?.status, "starting");
}).pipe(Effect.provide(harness.layer));
}).pipe(
Effect.provide(
GitVcsDriver.layer.pipe(
Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "launch-unborn-git-" })),
Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
),
),
),
);

it.effect("checks out a commit-search base when the current branch has no commits", () => {
const harness = makeHarness({ hasCommit: () => Effect.succeed(false) });
return Effect.gen(function* () {
const launches = yield* ThreadLaunch.ThreadLaunchService;
const launched = yield* launches.launch(
launchInput({
command: "command:launch:commit-search",
thread: "thread:launch:commit-search",
message: "Say hello",
workspace: { type: "worktree", baseRef: ":/initial", branch: "feature" },
}),
);
const projection = yield* preparationSettled(launched.threadId);
assert.equal(projection.thread.worktreePath, "/repo-worktrees/feature");
assert.equal(harness.createWorktree.mock.calls[0]?.[0]?.refName, ":/initial");
}).pipe(Effect.provide(harness.layer));
});

it.effect("fetches the origin base from the remote it already resolved", () => {
let fetchInput: Deferred.Deferred<unknown> | null = null;
const harness = makeHarness({
Expand Down
191 changes: 104 additions & 87 deletions apps/server/src/orchestration-v2/ThreadLaunchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,99 +554,116 @@ const make = Effect.gen(function* () {
}
}
if (shouldFetch) yield* setupTracker.stageStatus(threadId, "fetch", "done");
if (branch !== null && (generatedTemporaryBranch || isTemporaryWorktreeBranch(branch))) {
const parts = branch.split("/");
for (let depth = 1; depth < parts.length; depth++) {
if (
yield* git
.hasCommit({
cwd: project.workspaceRoot,
refName: `refs/heads/${parts.slice(0, depth).join("/")}`,
})
.pipe(Effect.mapError(mapError(input, "provision-worktree", threadId)))
) {
branch = generatedTemporaryBranch
? branch.replaceAll("/", "-")
: flattenTemporaryWorktreeBranchName(branch);
break;
const unborn = yield* Effect.gen(function* () {
const cwd = project.workspaceRoot;
if (yield* git.hasCommit({ cwd, refName: "HEAD" })) return null;
// Only the unborn branch itself has nothing to check out; Git resolves any other base.
const current = yield* git.currentBranch(cwd);
if (startRef !== "HEAD" && startRef !== current) return null;
if (yield* git.hasCommit({ cwd, refName: startRef })) return null;
return { branch: current };
}).pipe(Effect.mapError(mapError(input, "provision-worktree", threadId)));
if (unborn !== null) {
// A branch with no commits has nothing to check out; run in the project folder.
branch = unborn.branch;
yield* setupTracker.update(threadId, (snapshot) => ({ ...snapshot, branch }));
yield* setupTracker.stageStatus(threadId, "checkout", "skipped");
yield* setupTracker.stageStatus(threadId, "submodules", "skipped");
} else {
if (branch !== null && (generatedTemporaryBranch || isTemporaryWorktreeBranch(branch))) {
const parts = branch.split("/");
for (let depth = 1; depth < parts.length; depth++) {
if (
yield* git
.hasCommit({
cwd: project.workspaceRoot,
refName: `refs/heads/${parts.slice(0, depth).join("/")}`,
})
.pipe(Effect.mapError(mapError(input, "provision-worktree", threadId)))
) {
branch = generatedTemporaryBranch
? branch.replaceAll("/", "-")
: flattenTemporaryWorktreeBranchName(branch);
break;
}
}
}
}
yield* setupTracker.stageStatus(threadId, "checkout", "running");
let submoduleProgressReportedAt = Number.NEGATIVE_INFINITY;
const worktree = yield* git
.createWorktree(
{
cwd: project.workspaceRoot,
refName: startRef,
newRefName: branch!,
baseRefName,
path: null,
},
{
progress: {
onWorktreeClaimed: (path) =>
Effect.sync(() => {
createdWorktreePath = path;
createdBranch = branch;
}),
onCheckoutProgress: (progress) =>
setupTracker.stage(threadId, "checkout", { percent: progress.percent }),
onSubmodulesStarted: () =>
setupTracker
.stageStatus(threadId, "checkout", "done")
.pipe(
Effect.andThen(setupTracker.stageStatus(threadId, "submodules", "running")),
yield* setupTracker.stageStatus(threadId, "checkout", "running");
let submoduleProgressReportedAt = Number.NEGATIVE_INFINITY;
const worktree = yield* git
.createWorktree(
{
cwd: project.workspaceRoot,
refName: startRef,
newRefName: branch!,
baseRefName,
path: null,
},
{
progress: {
onWorktreeClaimed: (path) =>
Effect.sync(() => {
createdWorktreePath = path;
createdBranch = branch;
}),
onCheckoutProgress: (progress) =>
setupTracker.stage(threadId, "checkout", { percent: progress.percent }),
onSubmodulesStarted: () =>
setupTracker
.stageStatus(threadId, "checkout", "done")
.pipe(
Effect.andThen(setupTracker.stageStatus(threadId, "submodules", "running")),
),
onSubmodulesDisabled: ({ source }) =>
setupTracker.stageStatus(
threadId,
"submodules",
"skipped",
`disabled in ${source}`,
),
onSubmodulesDisabled: ({ source }) =>
setupTracker.stageStatus(
threadId,
"submodules",
"skipped",
`disabled in ${source}`,
),
onSubmoduleLine: (line) => {
// Paths may contain quotes; git closes the path with "': ".
const submodulePath = /^\s*Submodule path '(.+)': /.exec(line)?.[1];
if (submodulePath !== undefined) {
return setupTracker.stage(threadId, "submodules", { detail: submodulePath });
}
if (parseGitCloneProgressLine(line) === null) return Effect.void;
// Git redraws on every percent; hold the websocket to one update a second.
return Effect.flatMap(Clock.currentTimeMillis, (now) => {
if (now - submoduleProgressReportedAt < 1000) return Effect.void;
submoduleProgressReportedAt = now;
return setupTracker.stage(threadId, "submodules", {
detail: line.trim().replace(/\s+/g, " "),
onSubmoduleLine: (line) => {
// Paths may contain quotes; git closes the path with "': ".
const submodulePath = /^\s*Submodule path '(.+)': /.exec(line)?.[1];
if (submodulePath !== undefined) {
return setupTracker.stage(threadId, "submodules", { detail: submodulePath });
}
if (parseGitCloneProgressLine(line) === null) return Effect.void;
// Git redraws on every percent; hold the websocket to one update a second.
return Effect.flatMap(Clock.currentTimeMillis, (now) => {
if (now - submoduleProgressReportedAt < 1000) return Effect.void;
submoduleProgressReportedAt = now;
return setupTracker.stage(threadId, "submodules", {
detail: line.trim().replace(/\s+/g, " "),
});
});
});
},
onSubmodulesFinished: ({ ok, detail }) =>
setupTracker.stageStatus(
threadId,
"submodules",
ok ? "done" : "warning",
ok ? undefined : (detail ?? "submodule checkout failed"),
),
},
onSubmodulesFinished: ({ ok, detail }) =>
setupTracker.stageStatus(
threadId,
"submodules",
ok ? "done" : "warning",
ok ? undefined : (detail ?? "submodule checkout failed"),
),
},
},
)
.pipe(Effect.mapError(mapError(input, "provision-worktree", threadId)));
worktreePath = worktree.worktree.path;
branch = worktree.worktree.refName;
createdWorktreePath = worktreePath;
createdBranch = branch;
yield* setupTracker.update(threadId, (snapshot) => ({
...snapshot,
worktreePath,
branch,
stages: snapshot.stages.map((stage) =>
stage.id === "submodules" && stage.status === "pending"
? { ...stage, status: "skipped", detail: "none" }
: stage,
),
}));
yield* setupTracker.stageStatus(threadId, "checkout", "done");
)
.pipe(Effect.mapError(mapError(input, "provision-worktree", threadId)));
worktreePath = worktree.worktree.path;
branch = worktree.worktree.refName;
createdWorktreePath = worktreePath;
createdBranch = branch;
yield* setupTracker.update(threadId, (snapshot) => ({
...snapshot,
worktreePath,
branch,
stages: snapshot.stages.map((stage) =>
stage.id === "submodules" && stage.status === "pending"
? { ...stage, status: "skipped", detail: "none" }
: stage,
),
}));
yield* setupTracker.stageStatus(threadId, "checkout", "done");
}
}

// Do not rewrite reused worktrees; that could undo the original branch rename.
Expand Down
Loading