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
1 change: 1 addition & 0 deletions apps/server/src/git/GitWorkflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class GitWorkflowService extends Context.Service<
readonly fetchWorktreeBaseCommit: (input: {
readonly cwd: string;
readonly refName: string;
readonly remote?: GitVcsDriver.GitWorktreeBaseRemote;
}) => Effect.Effect<
{ readonly commitSha: string; readonly remoteRefName: string } | null,
GitCommandError
Expand Down
23 changes: 23 additions & 0 deletions apps/server/src/orchestration-v2/ThreadLaunchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,29 @@ it.effect.each(["stable", "upstream/release"])(
),
);

it.effect("fetches the origin base from the remote it already resolved", () => {
let fetchInput: Deferred.Deferred<unknown> | null = null;
const harness = makeHarness({
fetchWorktreeBaseCommit: (input) => Deferred.succeed(fetchInput!, input).pipe(Effect.as(null)),
});
return Effect.gen(function* () {
fetchInput = yield* Deferred.make<unknown>();
const launches = yield* ThreadLaunch.ThreadLaunchService;
yield* launches.launch(
launchInput({
command: "command:launch:resolved-remote",
thread: "thread:launch:resolved-remote",
workspace: { type: "worktree", baseRef: "main", startFromOrigin: true },
}),
);
assert.deepEqual(yield* Deferred.await(fetchInput), {
cwd: project.workspaceRoot,
refName: "main",
remote: { remoteName: "origin", branchName: "main" },
});
}).pipe(Effect.provide(harness.layer));
});

it.effect("shows the fetch diagnosis when preparing a worktree from origin fails", () => {
const detail =
"Git could not authenticate with the remote. Check Git credentials or SSH access on the server, then retry.";
Expand Down
21 changes: 12 additions & 9 deletions apps/server/src/orchestration-v2/ThreadLaunchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,20 +396,23 @@ const make = Effect.gen(function* () {
}
let startRef = input.workspaceStrategy.baseRef;
let baseRefName = input.workspaceStrategy.baseRef;
const shouldFetch =
input.workspaceStrategy.startFromOrigin === true &&
(yield* git
.resolveWorktreeBaseRemote({
cwd: project.workspaceRoot,
refName: input.workspaceStrategy.baseRef,
})
.pipe(Effect.mapError(mapError(input, "provision-worktree", threadId)))) !== null;
const remote =
input.workspaceStrategy.startFromOrigin === true
? yield* git
.resolveWorktreeBaseRemote({
cwd: project.workspaceRoot,
refName: input.workspaceStrategy.baseRef,
})
.pipe(Effect.mapError(mapError(input, "provision-worktree", threadId)))
: null;
const shouldFetch = remote !== null;
yield* setupTracker.stageStatus(threadId, "fetch", shouldFetch ? "running" : "skipped");
if (shouldFetch) {
if (remote !== null) {
const remoteBase = yield* git
.fetchWorktreeBaseCommit({
cwd: project.workspaceRoot,
refName: input.workspaceStrategy.baseRef,
remote,
})
.pipe(Effect.mapError(mapError(input, "provision-worktree", threadId)));
if (remoteBase !== null) {
Expand Down
14 changes: 11 additions & 3 deletions apps/server/src/vcs/GitVcsDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ export interface GitWriteConfigValueInput {
readonly value: string;
}

export interface GitWorktreeBaseRemote {
remoteName: string;
branchName: string;
}

export interface GitSetBranchUpstreamInput {
cwd: string;
branch: string;
Expand Down Expand Up @@ -392,10 +397,13 @@ export class GitVcsDriver extends Context.Service<
) => Effect.Effect<GitResolveRemoteTrackingCommitResult, GitCommandError>;
readonly resolveWorktreeBaseRemote: (
input: GitFetchWorktreeBaseCommitInput,
) => Effect.Effect<{ remoteName: string; branchName: string } | null, GitCommandError>;
/** Fetches the configured upstream, explicit remote ref, or origin; null uses the local base. */
) => Effect.Effect<GitWorktreeBaseRemote | null, GitCommandError>;
/**
* Fetches the configured upstream, explicit remote ref, or origin; null uses the local base.
* A caller that already ran `resolveWorktreeBaseRemote` passes its result as `remote`.
*/
readonly fetchWorktreeBaseCommit: (
input: GitFetchWorktreeBaseCommitInput,
input: GitFetchWorktreeBaseCommitInput & { readonly remote?: GitWorktreeBaseRemote },
) => Effect.Effect<GitResolveRemoteTrackingCommitResult | null, GitCommandError>;
readonly fetchRemoteBranch: (
input: GitFetchRemoteBranchInput,
Expand Down
49 changes: 49 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5413,6 +5413,55 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
}),
);

it.effect("fetches a worktree base without downloading tags", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const remote = yield* makeTmpDir("git-remote-");
const { initialBranch } = yield* initRepoWithCommit(cwd);
yield* git(remote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "origin", remote]);
yield* git(cwd, ["push", "-u", "origin", initialBranch]);
const localHead = yield* git(cwd, ["rev-parse", "HEAD"]);
yield* git(cwd, ["commit", "--allow-empty", "-m", "tagged release"]);
const remoteHead = yield* git(cwd, ["rev-parse", "HEAD"]);
yield* git(cwd, ["tag", "v1"]);
yield* git(cwd, ["push", "origin", initialBranch, "v1"]);
yield* git(cwd, ["tag", "-d", "v1"]);
yield* git(cwd, ["reset", "--hard", localHead]);
yield* git(cwd, ["update-ref", "-d", `refs/remotes/origin/${initialBranch}`]);
const driver = yield* GitVcsDriver.GitVcsDriver;

assert.deepEqual(yield* driver.fetchWorktreeBaseCommit({ cwd, refName: initialBranch }), {
commitSha: remoteHead,
remoteRefName: `origin/${initialBranch}`,
});
assert.equal(yield* git(cwd, ["tag", "--list", "v1"]), "");
}),
);

it.effect("fetches the remote a caller already resolved for the worktree base", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
const remote = yield* makeTmpDir("git-remote-");
const { initialBranch } = yield* initRepoWithCommit(cwd);
yield* git(remote, ["init", "--bare"]);
yield* git(cwd, ["remote", "add", "origin", remote]);
yield* git(cwd, ["push", "origin", initialBranch]);
const head = yield* git(cwd, ["rev-parse", "HEAD"]);
const driver = yield* GitVcsDriver.GitVcsDriver;

assert.isNull(yield* driver.fetchWorktreeBaseCommit({ cwd, refName: "unpublished" }));
assert.deepEqual(
yield* driver.fetchWorktreeBaseCommit({
cwd,
refName: "unpublished",
remote: { remoteName: "origin", branchName: initialBranch },
}),
{ commitSha: head, remoteRefName: `origin/${initialBranch}` },
);
}),
);

it.effect("fails instead of falling back when the worktree base remote is unreachable", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
Expand Down
61 changes: 35 additions & 26 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4147,7 +4147,17 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
const progress = options?.progress;
const onCheckoutProgress = progress?.onCheckoutProgress;

const checkoutWorkers = (yield* readConfigValue(input.cwd, "checkout.workers")) ?? "0";
// Both reads leave the repository alone, so they share one round trip before the checkout.
const [configuredCheckoutWorkers, base] = yield* Effect.all(
[
readConfigValue(input.cwd, "checkout.workers"),
input.newRefName && input.baseRefName
? resolveBranchBase(input.cwd, input.baseRefName)
: Effect.succeed(null),
],
{ concurrency: "unbounded" },
);
const checkoutWorkers = configuredCheckoutWorkers ?? "0";
yield* executeGitWithStableDiagnostics(
"GitVcsDriver.createWorktree",
input.cwd,
Expand Down Expand Up @@ -4253,32 +4263,27 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
}

const newRefName = input.newRefName;
if (newRefName && input.baseRefName) {
const base = yield* resolveBranchBase(input.cwd, input.baseRefName);
if (base) {
yield* Effect.gen(function* () {
if (newRefName && base) {
yield* Effect.gen(function* () {
yield* writeConfigValue({
cwd: input.cwd,
key: branchBaseConfigKey(newRefName),
value: base.branchName,
});
if (base.baseRef.startsWith("refs/remotes/")) {
// The base key names a branch on the host; this one keeps the remote it came from.
yield* writeConfigValue({
cwd: input.cwd,
key: branchBaseConfigKey(newRefName),
value: base.branchName,
key: `branch.${newRefName}.t3-merge-base-ref`,
value: base.baseRef,
});
if (base.baseRef.startsWith("refs/remotes/")) {
// The base key names a branch on the host; this one keeps the remote it came from.
yield* writeConfigValue({
cwd: input.cwd,
key: `branch.${input.newRefName}.t3-merge-base-ref`,
value: base.baseRef,
});
}
}).pipe(
Effect.catchTags({
GitCommandError: () =>
Effect.logWarning(
"worktree base-ref configuration failed; using base-branch fallback",
),
}),
);
}
}
}).pipe(
Effect.catchTags({
GitCommandError: () =>
Effect.logWarning("worktree base-ref configuration failed; using base-branch fallback"),
}),
);
}

return {
Expand Down Expand Up @@ -4444,7 +4449,11 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
const branch =
parseRemoteRefWithRemoteNames(input.refName, [input.remoteName])?.branchName ?? input.refName;
const scopedArgs = [
...args,
"fetch",
"--quiet",
"--no-tags",
"--",
input.remoteName,
`+refs/heads/${branch}:refs/remotes/${input.remoteName}/${branch}`,
];
const result = yield* runFetch(scopedArgs);
Expand Down Expand Up @@ -4524,7 +4533,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*

const fetchWorktreeBaseCommit: GitVcsDriver.GitVcsDriver["Service"]["fetchWorktreeBaseCommit"] =
Effect.fn("fetchWorktreeBaseCommit")(function* (input) {
const target = yield* resolveWorktreeBaseRemote(input);
const target = input.remote ?? (yield* resolveWorktreeBaseRemote(input));
if (target === null) return null;
const { remoteName, branchName } = target;
const remoteRefName = `${remoteName}/${branchName}`;
Expand Down
Loading