Skip to content
1 change: 1 addition & 0 deletions apps/server/src/git/GitWorkflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export class GitWorkflowService extends Context.Service<
readonly fetchRemote: (input: {
readonly cwd: string;
readonly remoteName: string;
readonly refName?: string;
}) => Effect.Effect<void, GitCommandError>;
readonly remoteExists: (input: {
readonly cwd: string;
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 @@ -10660,6 +10660,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
assert.deepEqual(fetchRemote.mock.calls[0]?.[0], {
cwd: "/tmp/project",
remoteName: "origin",
refName: "main",
});
assert.deepEqual(remoteBranchExists.mock.calls[0]?.[0], {
cwd: "/tmp/project",
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export interface GitFetchRemoteTrackingBranchInput {
export interface GitFetchRemoteInput {
cwd: string;
remoteName: string;
refName?: string;
}

export interface GitRemoteExistsInput {
Expand Down
134 changes: 130 additions & 4 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,64 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
});

describe("worktree operations", () => {
it.effect("uses parallel checkout without skipping filters or hooks", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const { initialBranch } = yield* initRepoWithCommit(cwd);
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* git(cwd, ["config", "filter.test.smudge", "sed s/original/filtered/g"]);
yield* writeTextFile(cwd, ".gitattributes", "asset.txt filter=test\n");
yield* writeTextFile(cwd, "asset.txt", "original\n");
yield* git(cwd, ["add", "."]);
yield* git(cwd, ["commit", "-m", "filtered asset"]);
yield* writeTextFile(
cwd,
".git/hooks/post-checkout",
"#!/bin/sh\ngit config checkout.workers > checkout-workers\nexit 0\n",
);
yield* fs.chmod(path.join(cwd, ".git/hooks/post-checkout"), 0o755);
const worktreePath = path.join(yield* makeTmpDir("git-worktrees-"), "parallel");

yield* driver.createWorktree({
cwd,
path: worktreePath,
refName: initialBranch,
newRefName: "feature/parallel",
baseRefName: initialBranch,
});

assert.notInclude(yield* git(cwd, ["worktree", "list", "--porcelain"]), "locked");
assert.equal(yield* fs.readFileString(path.join(worktreePath, "checkout-workers")), "0\n");
assert.equal(yield* fs.readFileString(path.join(worktreePath, "asset.txt")), "filtered\n");
assert.equal(
yield* git(worktreePath, ["rev-parse", "HEAD"]),
yield* git(cwd, ["rev-parse", "HEAD"]),
);
assert.equal(
yield* git(cwd, ["config", "branch.feature/parallel.gh-merge-base"]),
initialBranch,
);
for (const [configured, expected] of [
["1", "1"],
["", "0"],
] as const) {
yield* git(cwd, ["config", "checkout.workers", configured]);
const configuredPath = path.join(yield* makeTmpDir("git-worktrees-"), "configured");
yield* driver.createWorktree({
cwd,
path: configuredPath,
refName: initialBranch,
newRefName: `feature/configured-${expected}`,
});
assert.equal(
yield* fs.readFileString(path.join(configuredPath, "checkout-workers")),
`${expected}\n`,
);
}
}),
);
it("parses checkout progress lines from git's stderr", () => {
assert.deepStrictEqual(parseGitCheckoutProgressLine("Updating files: 78% (2104/2700)"), {
percent: 78,
Expand Down Expand Up @@ -1686,11 +1744,11 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
}),
);

it.effect("reports checkout progress while creating a worktree", () =>
it.effect("reports checkout progress during parallel worktree creation", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const { initialBranch } = yield* initRepoWithCommit(cwd);
for (let index = 0; index < 5; index += 1) {
for (let index = 0; index < 200; index += 1) {
yield* writeTextFile(cwd, `file-${index}.txt`, `${index}\n`);
}
yield* git(cwd, ["add", "."]);
Expand Down Expand Up @@ -1724,7 +1782,7 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
const updates = yield* Ref.get(seen);
assert.isAbove(updates.length, 1);
assert.equal(updates.at(-1)?.percent, 100);
assert.equal(updates.at(-1)?.total, 6);
assert.equal(updates.at(-1)?.total, 201);
const completed = updates.map((update) => update.completed);
assert.deepEqual(
completed,
Expand Down Expand Up @@ -1957,6 +2015,61 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
});

describe("remote operations", () => {
for (const failure of ["offline", "auth", "timeout"] as const) {
it.effect(`does not retry a scoped fetch after ${failure}`, () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const delegate = yield* ChildProcessSpawner.ChildProcessSpawner;
const started = yield* Deferred.make<void>();
const attempts: Array<ReadonlyArray<string>> = [];
const spawner = ChildProcessSpawner.make((command) =>
Effect.gen(function* () {
if (!ChildProcess.isStandardCommand(command))
return yield* Effect.die("unexpected command");
if (command.args[0] !== "fetch") return yield* delegate.spawn(command);
attempts.push(command.args);
yield* Deferred.succeed(started, undefined);
return ChildProcessSpawner.makeHandle({
...makeNonRepositoryHandle(),
exitCode:
failure === "timeout"
? Effect.never
: Effect.succeed(ChildProcessSpawner.ExitCode(128)),
stderr: Stream.encodeText(
Stream.make(
failure === "auth"
? "fatal: Authentication failed"
: "fatal: Could not resolve host",
),
),
});
}),
);
const driver = yield* makeGitVcsDriverCore().pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provide(ServerConfigLayer),
);
const fetching = yield* driver
.fetchRemote({ cwd, remoteName: "origin", refName: "main" })
.pipe(Effect.result, Effect.forkChild({ startImmediately: true }));
yield* Deferred.await(started);
if (failure === "timeout") {
yield* TestClock.adjust("31 seconds");
yield* TestClock.adjust("31 seconds");
}
const result = yield* Fiber.join(fetching);
assert.isTrue(Result.isFailure(result));
assert.equal(attempts.length, 1);
if (Result.isFailure(result)) {
assert.equal(
result.failure.detail,
failure === "timeout" ? "Git command timed out." : "git fetch origin failed",
);
}
}),
);
}

it.effect("creates a worktree from the latest fetched remote commit", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
Expand All @@ -1979,8 +2092,16 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
const remoteHead = yield* git(peer, ["rev-parse", "HEAD"]);
assert.notEqual(beforeFetch, remoteHead);

yield* git(peer, ["push", "origin", "HEAD:refs/heads/unrelated"]);
const driver = yield* GitVcsDriver.GitVcsDriver;
yield* driver.fetchRemote({ cwd, remoteName: "origin" });
yield* driver.fetchRemote({
cwd,
remoteName: "origin",
refName: `origin/${initialBranch}`,
});
assert.isFalse(
yield* driver.remoteBranchExists({ cwd, remoteName: "origin", refName: "unrelated" }),
);

assert.equal(
yield* driver.remoteBranchExists({
Expand Down Expand Up @@ -2042,6 +2163,11 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => {
const status = yield* driver.statusDetails(worktreePath);
assert.equal(status.aheadCount, 0);
assert.equal(status.aheadOfDefaultCount, 0);

yield* driver.fetchRemote({ cwd, remoteName: "origin", refName: "local-only" });
assert.isTrue(
yield* driver.remoteBranchExists({ cwd, remoteName: "origin", refName: "unrelated" }),
);
}),
);

Expand Down
82 changes: 60 additions & 22 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3042,23 +3042,29 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
const progress = options?.progress;
const onCheckoutProgress = progress?.onCheckoutProgress;

yield* executeGit("GitVcsDriver.createWorktree", input.cwd, args, {
fallbackErrorDetail: "git worktree add failed",
timeoutMs: WORKTREE_ADD_TIMEOUT_MS,
...(onCheckoutProgress
? {
// Git only prints checkout progress when stderr is a tty or the
// delay elapsed. GIT_PROGRESS_DELAY=0 forces it through the pipe.
env: { GIT_PROGRESS_DELAY: "0", LC_ALL: "C" },
progress: {
onStderrLine: (line) => {
const parsed = parseGitCheckoutProgressLine(line);
return parsed ? onCheckoutProgress(parsed) : Effect.void;
const checkoutWorkers = (yield* readConfigValue(input.cwd, "checkout.workers")) ?? "0";
yield* executeGit(
"GitVcsDriver.createWorktree",
input.cwd,
["-c", `checkout.workers=${checkoutWorkers}`, ...args],
{
fallbackErrorDetail: "git worktree add failed",
timeoutMs: WORKTREE_ADD_TIMEOUT_MS,
...(onCheckoutProgress
? {
// Git only prints checkout progress when stderr is a tty or the
// delay elapsed. GIT_PROGRESS_DELAY=0 forces it through the pipe.
env: { GIT_PROGRESS_DELAY: "0", LC_ALL: "C" },
progress: {
onStderrLine: (line) => {
const parsed = parseGitCheckoutProgressLine(line);
return parsed ? onCheckoutProgress(parsed) : Effect.void;
},
},
},
}
: {}),
});
}
: {}),
},
);

if (progress?.onWorktreeClaimed) {
yield* progress.onWorktreeClaimed(worktreePath);
Expand Down Expand Up @@ -3240,15 +3246,47 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*

const fetchRemote: GitVcsDriver.GitVcsDriver["Service"]["fetchRemote"] = Effect.fn("fetchRemote")(
function* (input) {
yield* executeGit(
const args = ["fetch", "--quiet", input.remoteName];
const options = {
env: STATUS_UPSTREAM_REFRESH_ENV,
fallbackErrorDetail: `git fetch ${input.remoteName} failed`,
};
const fetchAll = executeGit("GitVcsDriver.fetchRemote", input.cwd, args, options);
if (input.refName === undefined) {
return yield* fetchAll.pipe(Effect.asVoid);
}
const branch =
parseRemoteRefWithRemoteNames(input.refName, [input.remoteName])?.branchName ??
input.refName;
const scopedArgs = [
...args,
`+refs/heads/${branch}:refs/remotes/${input.remoteName}/${branch}`,
];
const result = yield* executeGitWithStableDiagnostics(
"GitVcsDriver.fetchRemote",
input.cwd,
["fetch", "--quiet", input.remoteName],
{
env: STATUS_UPSTREAM_REFRESH_ENV,
fallbackErrorDetail: `git fetch ${input.remoteName} failed`,
},
scopedArgs,
{ ...options, allowNonZeroExit: true },
);
if (result.exitCode === 0) return;
if (
result.stderr
.split(/\r?\n/)
.includes(`fatal: couldn't find remote ref refs/heads/${branch}`)
Comment thread
Bil0000 marked this conversation as resolved.
) {
return yield* fetchAll.pipe(Effect.asVoid);
}
return yield* new GitCommandError({
...gitCommandContext({
operation: "GitVcsDriver.fetchRemote",
cwd: input.cwd,
args: scopedArgs,
}),
detail: options.fallbackErrorDetail,
exitCode: result.exitCode,
stdoutLength: result.stdout.length,
stderrLength: result.stderr.length,
});
},
);

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 @@ -1221,6 +1221,7 @@ const makeWsRpcLayer = (
yield* gitWorkflow.fetchRemote({
cwd: prepareWorktree.projectCwd,
remoteName: "origin",
refName: prepareWorktree.baseBranch,
});
const remoteBaseExists = yield* gitWorkflow.remoteBranchExists({
cwd: prepareWorktree.projectCwd,
Expand Down
Loading