1import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
2import type { VcsRef } from "@t3tools/client-runtime/state/vcs";
3import {
4 type AtomCommandResult,
5 mapAtomCommandResult,
6} from "@t3tools/client-runtime/state/runtime";
7import type {
8 VcsListRefsInput,
9 VcsListRefsResult,
10 VcsSwitchRefInput,
11 VcsSwitchRefResult,
12} from "@t3tools/contracts";
13import { deriveLocalBranchNameFromRemoteRef } from "@t3tools/shared/git";
14import * as Cause from "effect/Cause";
15import { AsyncResult } from "effect/reactivity";
16
17const VCS_REF_QUERY_MAX_LENGTH = 256;
18
19export async function checkoutNewTaskBranch<E>(input: {
20 readonly branch: VcsRef;
21 readonly project: Pick<EnvironmentProject, "environmentId" | "workspaceRoot"> | null;
22 readonly workspaceMode: "local" | "worktree";
23 readonly readRefs: (request: {
24 readonly environmentId: EnvironmentProject["environmentId"];
25 readonly input: VcsListRefsInput;
26 }) => Promise<AtomCommandResult<VcsListRefsResult, E>>;
27 readonly switchRef: (request: {
28 readonly environmentId: EnvironmentProject["environmentId"];
29 readonly input: VcsSwitchRefInput;
30 }) => Promise<AtomCommandResult<VcsSwitchRefResult, E>>;
31}): Promise<AtomCommandResult<VcsRef, E | Error>> {
32 if (!input.project) {
33 return AsyncResult.failure(
34 Cause.fail(new Error("The selected project is unavailable. Reconnect and try again.")),
35 );
36 }
37 if (input.workspaceMode === "worktree" || input.branch.current) {
38 return AsyncResult.success(
39 input.branch.current ? { ...input.branch, worktreePath: null } : input.branch,
40 );
41 }
42
43 // Remote refs omit local worktrees, and historical thread metadata can be stale.
44 const localName = localBranchName(input.branch);
45 let cursor: number | undefined;
46 for (;;) {
47 const refs = await input.readRefs({
48 environmentId: input.project.environmentId,
49 input: {
50 cwd: input.project.workspaceRoot,
51 // Bound the substring query; compare the full name across every page.
52 query: localName.slice(0, VCS_REF_QUERY_MAX_LENGTH),
53 refKind: "local",
54 ...(cursor === undefined ? { refresh: true } : { cursor }),
55 },
56 });
57 if (refs._tag === "Failure") return AsyncResult.failure(refs.cause);
58 const local = refs.value.refs.find((ref) => ref.name === localName);
59 if (
60 local &&
61 (!input.branch.isRemote || local.upstreamRef === `refs/remotes/${input.branch.name}`) &&
62 (local.current || local.worktreePath)
63 ) {
64 if (local.current) return AsyncResult.success({ ...local, worktreePath: null });
65 if (refs.value.isAtRepositoryRoot === undefined) {
66 return AsyncResult.failure(
67 Cause.fail(
68 new Error(
69 "The server did not report whether this project is at the repository root, so this worktree cannot be reused safely. Update the server or choose another branch.",
70 ),
71 ),
72 );
73 }
74 if (!refs.value.isAtRepositoryRoot) {
75 return AsyncResult.failure(
76 Cause.fail(
77 new Error(
78 "This project is a repository subdirectory. Choose another branch or add the repository root as a project to reuse this worktree.",
79 ),
80 ),
81 );
82 }
83 return AsyncResult.success(local);
84 }
85 if (local || refs.value.nextCursor === null) break;
86 cursor = refs.value.nextCursor;
87 }
88
89 const result = await input.switchRef({
90 environmentId: input.project.environmentId,
91 input: { cwd: input.project.workspaceRoot, refName: input.branch.name },
92 });
93 return mapAtomCommandResult(result, (value) => ({
94 ...input.branch,
95 current: true,
96 isRemote: false,
97 name: value.refName ?? input.branch.name,
98 worktreePath: null,
99 }));
100}
101
102// A remote name can itself contain slashes; prefer the authoritative prefix.
103function localBranchName(branch: VcsRef): string {
104 if (!branch.isRemote) return branch.name;
105 const prefix = branch.remoteName ? `${branch.remoteName}/` : null;
106 return prefix && branch.name.startsWith(prefix)
107 ? branch.name.slice(prefix.length)
108 : deriveLocalBranchNameFromRemoteRef(branch.name);
109}