History

Refactor git core into pluggable VCS driver

- Move low-level git service into `GitVcsDriverCore` - Update server wiring and tests to use the new driver

Julius Marminge committed 6 months ago

Commit a11d6b91 parent 82a83945 files · +929 −904Browse files

Unified · Split

5 files
  1. apps/server/src/git/Layers/GitManager.ts +1 −2
  2. apps/server/src/git/Services/GitCore.ts +0 −288
  3. apps/server/src/vcs/GitVcsDriver.ts +185 −6
  4. apps/server/src/vcs/GitVcsDriverCore.test.ts +378 −255
  5. apps/server/src/vcs/GitVcsDriverCore.ts +365 −353
apps/server/src/git/Layers/GitManager.ts+1−2
@@ -38,7 +38,6 @@ import { · 37 unmodified lines
3838 type GitManagerShape,
3939 type GitRunStackedActionOptions,
4040 } from "../Services/GitManager.ts";
41−import type { GitStatusDetails } from "../Services/GitCore.ts";
4241 import { GitHubCli, type GitHubPullRequestSummary } from "../Services/GitHubCli.ts";
4342 import { TextGeneration } from "../Services/TextGeneration.ts";
4443 import { ProjectSetupScriptRunner } from "../../project/Services/ProjectSetupScriptRunner.ts";
@@ -49,7 +48,7 @@ import { · 4 unmodified lines
4948 decodeGitHubPullRequestListJson,
5049 formatGitHubJsonDecodeError,
5150 } from "../githubPullRequests.ts";
52−import { GitVcsDriver } from "../../vcs/GitVcsDriver.ts";
51+import { GitVcsDriver, type GitStatusDetails } from "../../vcs/GitVcsDriver.ts";
5352
5453 const COMMIT_TIMEOUT_MS = 10 * 60_000;
5554 const MAX_PROGRESS_TEXT_LENGTH = 500;
apps/server/src/git/Services/GitCore.ts+0−288
@@ -1,288 +0,0 @@
1−/**
2− * GitCore - Effect service contract for low-level Git operations.
3− *
4− * Wraps core repository primitives used by higher-level orchestration
5− * services and WebSocket routes.
6− *
7− * @module GitCore
8− */
9−import { Context } from "effect";
10−import type { Effect } from "effect";
11−import type {
12− GitCheckoutInput,
13− GitCheckoutResult,
14− GitCreateBranchInput,
15− GitCreateBranchResult,
16− GitCreateWorktreeInput,
17− GitCreateWorktreeResult,
18− GitInitInput,
19− GitListBranchesInput,
20− GitListBranchesResult,
21− GitPullResult,
22− GitRemoveWorktreeInput,
23− GitStatusInput,
24− GitStatusResult,
25−} from "@t3tools/contracts";
26−
27−import type { GitCommandError } from "@t3tools/contracts";
28−
29−export interface ExecuteGitInput {
30− readonly operation: string;
31− readonly cwd: string;
32− readonly args: ReadonlyArray<string>;
33− readonly stdin?: string;
34− readonly env?: NodeJS.ProcessEnv;
35− readonly allowNonZeroExit?: boolean;
36− readonly timeoutMs?: number;
37− readonly maxOutputBytes?: number;
38− readonly truncateOutputAtMaxBytes?: boolean;
39− readonly progress?: ExecuteGitProgress;
40−}
41−
42−export interface ExecuteGitResult {
43− readonly code: number;
44− readonly stdout: string;
45− readonly stderr: string;
46− readonly stdoutTruncated: boolean;
47− readonly stderrTruncated: boolean;
48−}
49−
50−export interface GitStatusDetails extends Omit<GitStatusResult, "pr"> {
51− upstreamRef: string | null;
52−}
53−
54−export interface GitPreparedCommitContext {
55− stagedSummary: string;
56− stagedPatch: string;
57−}
58−
59−export interface ExecuteGitProgress {
60− readonly onStdoutLine?: (line: string) => Effect.Effect<void, never>;
61− readonly onStderrLine?: (line: string) => Effect.Effect<void, never>;
62− readonly onHookStarted?: (hookName: string) => Effect.Effect<void, never>;
63− readonly onHookFinished?: (input: {
64− hookName: string;
65− exitCode: number | null;
66− durationMs: number | null;
67− }) => Effect.Effect<void, never>;
68−}
69−
70−export interface GitCommitProgress {
71− readonly onOutputLine?: (input: {
72− stream: "stdout" | "stderr";
73− text: string;
74− }) => Effect.Effect<void, never>;
75− readonly onHookStarted?: (hookName: string) => Effect.Effect<void, never>;
76− readonly onHookFinished?: (input: {
77− hookName: string;
78− exitCode: number | null;
79− durationMs: number | null;
80− }) => Effect.Effect<void, never>;
81−}
82−
83−export interface GitCommitOptions {
84− readonly timeoutMs?: number;
85− readonly progress?: GitCommitProgress;
86−}
87−
88−export interface GitPushResult {
89− status: "pushed" | "skipped_up_to_date";
90− branch: string;
91− upstreamBranch?: string | undefined;
92− setUpstream?: boolean | undefined;
93−}
94−
95−export interface GitRangeContext {
96− commitSummary: string;
97− diffSummary: string;
98− diffPatch: string;
99−}
100−
101−export interface GitRenameBranchInput {
102− cwd: string;
103− oldBranch: string;
104− newBranch: string;
105−}
106−
107−export interface GitRenameBranchResult {
108− branch: string;
109−}
110−
111−export interface GitFetchPullRequestBranchInput {
112− cwd: string;
113− prNumber: number;
114− branch: string;
115−}
116−
117−export interface GitEnsureRemoteInput {
118− cwd: string;
119− preferredName: string;
120− url: string;
121−}
122−
123−export interface GitFetchRemoteBranchInput {
124− cwd: string;
125− remoteName: string;
126− remoteBranch: string;
127− localBranch: string;
128−}
129−
130−export interface GitSetBranchUpstreamInput {
131− cwd: string;
132− branch: string;
133− remoteName: string;
134− remoteBranch: string;
135−}
136−
137−/**
138− * GitCoreShape - Service API for low-level Git repository interactions.
139− */
140−export interface GitCoreShape {
141− /**
142− * Execute a raw Git command.
143− */
144− readonly execute: (input: ExecuteGitInput) => Effect.Effect<ExecuteGitResult, GitCommandError>;
145−
146− /**
147− * Read Git status for a repository.
148− */
149− readonly status: (input: GitStatusInput) => Effect.Effect<GitStatusResult, GitCommandError>;
150−
151− /**
152− * Read detailed working tree / branch status for a repository.
153− */
154− readonly statusDetails: (cwd: string) => Effect.Effect<GitStatusDetails, GitCommandError>;
155−
156− /**
157− * Read detailed working tree / branch status without refreshing remote tracking refs.
158− */
159− readonly statusDetailsLocal: (cwd: string) => Effect.Effect<GitStatusDetails, GitCommandError>;
160−
161− /**
162− * Build staged change context for commit generation.
163− */
164− readonly prepareCommitContext: (
165− cwd: string,
166− filePaths?: readonly string[],
167− ) => Effect.Effect<GitPreparedCommitContext | null, GitCommandError>;
168−
169− /**
170− * Create a commit with provided subject/body.
171− */
172− readonly commit: (
173− cwd: string,
174− subject: string,
175− body: string,
176− options?: GitCommitOptions,
177− ) => Effect.Effect<{ commitSha: string }, GitCommandError>;
178−
179− /**
180− * Push current branch, setting upstream if needed.
181− */
182− readonly pushCurrentBranch: (
183− cwd: string,
184− fallbackBranch: string | null,
185− ) => Effect.Effect<GitPushResult, GitCommandError>;
186−
187− /**
188− * Collect commit/diff context between base branch and current HEAD.
189− */
190− readonly readRangeContext: (
191− cwd: string,
192− baseBranch: string,
193− ) => Effect.Effect<GitRangeContext, GitCommandError>;
194−
195− /**
196− * Read a Git config value from the local repository.
197− */
198− readonly readConfigValue: (
199− cwd: string,
200− key: string,
201− ) => Effect.Effect<string | null, GitCommandError>;
202−
203− /**
204− * List local + remote branches and branch metadata.
205− */
206− readonly listBranches: (
207− input: GitListBranchesInput,
208− ) => Effect.Effect<GitListBranchesResult, GitCommandError>;
209−
210− /**
211− * Pull current branch from upstream using fast-forward only.
212− */
213− readonly pullCurrentBranch: (cwd: string) => Effect.Effect<GitPullResult, GitCommandError>;
214−
215− /**
216− * Create a worktree and branch from a base branch.
217− */
218− readonly createWorktree: (
219− input: GitCreateWorktreeInput,
220− ) => Effect.Effect<GitCreateWorktreeResult, GitCommandError>;
221−
222− /**
223− * Materialize a GitHub pull request head as a local branch without switching checkout.
224− */
225− readonly fetchPullRequestBranch: (
226− input: GitFetchPullRequestBranchInput,
227− ) => Effect.Effect<void, GitCommandError>;
228−
229− /**
230− * Ensure a named remote exists for the provided URL, returning the reused or created remote name.
231− */
232− readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect<string, GitCommandError>;
233−
234− /**
235− * Fetch a remote branch into a local branch without checkout.
236− */
237− readonly fetchRemoteBranch: (
238− input: GitFetchRemoteBranchInput,
239− ) => Effect.Effect<void, GitCommandError>;
240−
241− /**
242− * Set the upstream tracking branch for a local branch.
243− */
244− readonly setBranchUpstream: (
245− input: GitSetBranchUpstreamInput,
246− ) => Effect.Effect<void, GitCommandError>;
247−
248− /**
249− * Remove an existing worktree.
250− */
251− readonly removeWorktree: (input: GitRemoveWorktreeInput) => Effect.Effect<void, GitCommandError>;
252−
253− /**
254− * Rename an existing local branch.
255− */
256− readonly renameBranch: (
257− input: GitRenameBranchInput,
258− ) => Effect.Effect<GitRenameBranchResult, GitCommandError>;
259−
260− /**
261− * Create a local branch.
262− */
263− readonly createBranch: (
264− input: GitCreateBranchInput,
265− ) => Effect.Effect<GitCreateBranchResult, GitCommandError>;
266−
267− /**
268− * Checkout an existing branch and refresh its upstream metadata in background.
269− */
270− readonly checkoutBranch: (
271− input: GitCheckoutInput,
272− ) => Effect.Effect<GitCheckoutResult, GitCommandError>;
273−
274− /**
275− * Initialize a repository in the provided directory.
276− */
277− readonly initRepo: (input: GitInitInput) => Effect.Effect<void, GitCommandError>;
278−
279− /**
280− * List local branch names (short format).
281− */
282− readonly listLocalBranchNames: (cwd: string) => Effect.Effect<string[], GitCommandError>;
283−}
284−
285−/**
286− * GitCore - Service tag for low-level Git repository operations.
287− */
288−export class GitCore extends Context.Service<GitCore, GitCoreShape>()("t3/git/Services/GitCore") {}
apps/server/src/vcs/GitVcsDriver.ts+185−6
@@ -1,12 +1,191 @@
11 import { Context, DateTime, Effect, Layer } from "effect";
22
3−import { VcsProcessExitError } from "@t3tools/contracts";
4−import { makeGitCore } from "../git/Layers/GitCore.ts";
5−import type { GitCoreShape } from "../git/Services/GitCore.ts";
3+import {
4+ GitCommandError,
5+ VcsProcessExitError,
6+ type GitCheckoutInput,
7+ type GitCheckoutResult,
8+ type GitCreateBranchInput,
9+ type GitCreateBranchResult,
10+ type GitCreateWorktreeInput,
11+ type GitCreateWorktreeResult,
12+ type GitInitInput,
13+ type GitListBranchesInput,
14+ type GitListBranchesResult,
15+ type GitPullResult,
16+ type GitRemoveWorktreeInput,
17+ type GitStatusInput,
18+ type GitStatusResult,
19+} from "@t3tools/contracts";
20+import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts";
621 import { VcsDriver, type VcsDriverShape } from "./VcsDriver.ts";
722 import { VcsProcess, type VcsProcessShape } from "./VcsProcess.ts";
823
9−export interface GitVcsDriverShape extends GitCoreShape {}
24+export interface ExecuteGitInput {
25+ readonly operation: string;
26+ readonly cwd: string;
27+ readonly args: ReadonlyArray<string>;
28+ readonly stdin?: string;
29+ readonly env?: NodeJS.ProcessEnv;
30+ readonly allowNonZeroExit?: boolean;
31+ readonly timeoutMs?: number;
32+ readonly maxOutputBytes?: number;
33+ readonly truncateOutputAtMaxBytes?: boolean;
34+ readonly progress?: ExecuteGitProgress;
35+}
36+
37+export interface ExecuteGitResult {
38+ readonly code: number;
39+ readonly stdout: string;
40+ readonly stderr: string;
41+ readonly stdoutTruncated: boolean;
42+ readonly stderrTruncated: boolean;
43+}
44+
45+export interface GitStatusDetails extends Omit<GitStatusResult, "pr"> {
46+ upstreamRef: string | null;
47+}
48+
49+export interface GitPreparedCommitContext {
50+ stagedSummary: string;
51+ stagedPatch: string;
52+}
53+
54+export interface ExecuteGitProgress {
55+ readonly onStdoutLine?: (line: string) => Effect.Effect<void, never>;
56+ readonly onStderrLine?: (line: string) => Effect.Effect<void, never>;
57+ readonly onHookStarted?: (hookName: string) => Effect.Effect<void, never>;
58+ readonly onHookFinished?: (input: {
59+ hookName: string;
60+ exitCode: number | null;
61+ durationMs: number | null;
62+ }) => Effect.Effect<void, never>;
63+}
64+
65+export interface GitCommitProgress {
66+ readonly onOutputLine?: (input: {
67+ stream: "stdout" | "stderr";
68+ text: string;
69+ }) => Effect.Effect<void, never>;
70+ readonly onHookStarted?: (hookName: string) => Effect.Effect<void, never>;
71+ readonly onHookFinished?: (input: {
72+ hookName: string;
73+ exitCode: number | null;
74+ durationMs: number | null;
75+ }) => Effect.Effect<void, never>;
76+}
77+
78+export interface GitCommitOptions {
79+ readonly timeoutMs?: number;
80+ readonly progress?: GitCommitProgress;
81+}
82+
83+export interface GitPushResult {
84+ status: "pushed" | "skipped_up_to_date";
85+ branch: string;
86+ upstreamBranch?: string | undefined;
87+ setUpstream?: boolean | undefined;
88+}
89+
90+export interface GitRangeContext {
91+ commitSummary: string;
92+ diffSummary: string;
93+ diffPatch: string;
94+}
95+
96+export interface GitRenameBranchInput {
97+ cwd: string;
98+ oldBranch: string;
99+ newBranch: string;
100+}
101+
102+export interface GitRenameBranchResult {
103+ branch: string;
104+}
105+
106+export interface GitFetchPullRequestBranchInput {
107+ cwd: string;
108+ prNumber: number;
109+ branch: string;
110+}
111+
112+export interface GitEnsureRemoteInput {
113+ cwd: string;
114+ preferredName: string;
115+ url: string;
116+}
117+
118+export interface GitFetchRemoteBranchInput {
119+ cwd: string;
120+ remoteName: string;
121+ remoteBranch: string;
122+ localBranch: string;
123+}
124+
125+export interface GitSetBranchUpstreamInput {
126+ cwd: string;
127+ branch: string;
128+ remoteName: string;
129+ remoteBranch: string;
130+}
131+
132+export interface GitVcsDriverShape {
133+ readonly execute: (input: ExecuteGitInput) => Effect.Effect<ExecuteGitResult, GitCommandError>;
134+ readonly status: (input: GitStatusInput) => Effect.Effect<GitStatusResult, GitCommandError>;
135+ readonly statusDetails: (cwd: string) => Effect.Effect<GitStatusDetails, GitCommandError>;
136+ readonly statusDetailsLocal: (cwd: string) => Effect.Effect<GitStatusDetails, GitCommandError>;
137+ readonly prepareCommitContext: (
138+ cwd: string,
139+ filePaths?: readonly string[],
140+ ) => Effect.Effect<GitPreparedCommitContext | null, GitCommandError>;
141+ readonly commit: (
142+ cwd: string,
143+ subject: string,
144+ body: string,
145+ options?: GitCommitOptions,
146+ ) => Effect.Effect<{ commitSha: string }, GitCommandError>;
147+ readonly pushCurrentBranch: (
148+ cwd: string,
149+ fallbackBranch: string | null,
150+ ) => Effect.Effect<GitPushResult, GitCommandError>;
151+ readonly readRangeContext: (
152+ cwd: string,
153+ baseBranch: string,
154+ ) => Effect.Effect<GitRangeContext, GitCommandError>;
155+ readonly readConfigValue: (
156+ cwd: string,
157+ key: string,
158+ ) => Effect.Effect<string | null, GitCommandError>;
159+ readonly listBranches: (
160+ input: GitListBranchesInput,
161+ ) => Effect.Effect<GitListBranchesResult, GitCommandError>;
162+ readonly pullCurrentBranch: (cwd: string) => Effect.Effect<GitPullResult, GitCommandError>;
163+ readonly createWorktree: (
164+ input: GitCreateWorktreeInput,
165+ ) => Effect.Effect<GitCreateWorktreeResult, GitCommandError>;
166+ readonly fetchPullRequestBranch: (
167+ input: GitFetchPullRequestBranchInput,
168+ ) => Effect.Effect<void, GitCommandError>;
169+ readonly ensureRemote: (input: GitEnsureRemoteInput) => Effect.Effect<string, GitCommandError>;
170+ readonly fetchRemoteBranch: (
171+ input: GitFetchRemoteBranchInput,
172+ ) => Effect.Effect<void, GitCommandError>;
173+ readonly setBranchUpstream: (
174+ input: GitSetBranchUpstreamInput,
175+ ) => Effect.Effect<void, GitCommandError>;
176+ readonly removeWorktree: (input: GitRemoveWorktreeInput) => Effect.Effect<void, GitCommandError>;
177+ readonly renameBranch: (
178+ input: GitRenameBranchInput,
179+ ) => Effect.Effect<GitRenameBranchResult, GitCommandError>;
180+ readonly createBranch: (
181+ input: GitCreateBranchInput,
182+ ) => Effect.Effect<GitCreateBranchResult, GitCommandError>;
183+ readonly checkoutBranch: (
184+ input: GitCheckoutInput,
185+ ) => Effect.Effect<GitCheckoutResult, GitCommandError>;
186+ readonly initRepo: (input: GitInitInput) => Effect.Effect<void, GitCommandError>;
187+ readonly listLocalBranchNames: (cwd: string) => Effect.Effect<string[], GitCommandError>;
188+}
10189
11190 export class GitVcsDriver extends Context.Service<GitVcsDriver, GitVcsDriverShape>()(
12191 "t3/vcs/GitVcsDriver",
@@ -259,8 +438,8 @@ export const makeVcsDriver = Effect.fn("makeGitVcsDriver")(function* () { · 246 unmodified lines
259438 });
260439
261440 export const make = Effect.fn("makeGitVcsDriverService")(function* () {
262− const legacyGit = yield* makeGitCore();
263− return GitVcsDriver.of(legacyGit);
441+ const git = yield* makeGitVcsDriverCore();
442+ return GitVcsDriver.of(git);
264443 });
265444
266445 export const vcsLayer = Layer.effect(VcsDriver, makeVcsDriver());
apps/server/src/vcs/GitVcsDriverCore.test.tsrenamed from apps/server/src/git/Layers/GitCore.test.ts+378−255
@@ -6,20 +6,20 @@ import { it } from "@effect/vitest"; · 5 unmodified lines
66 import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";
77 import { describe, expect, vi } from "vitest";
88
9−import { GitCoreLive, makeGitCore } from "./GitCore.ts";
10−import { GitCore, type GitCoreShape } from "../Services/GitCore.ts";
9+import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts";
10+import * as GitVcsDriver from "./GitVcsDriver.ts";
1111 import { GitCommandError } from "@t3tools/contracts";
12−import { type ProcessRunResult, runProcess } from "../../processRunner.ts";
13−import { ServerConfig } from "../../config.ts";
12+import { type ProcessRunResult, runProcess } from "../processRunner.ts";
13+import { ServerConfig } from "../config.ts";
1414
1515 // ── Helpers ──
1616
1717 const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-core-test-" });
18−const GitCoreTestLayer = GitCoreLive.pipe(
18+const GitVcsDriverTestLayer = GitVcsDriver.layer.pipe(
1919 Layer.provide(ServerConfigLayer),
2020 Layer.provide(NodeServices.layer),
2121 );
22−const TestLayer = Layer.mergeAll(NodeServices.layer, GitCoreTestLayer);
22+const TestLayer = Layer.mergeAll(NodeServices.layer, GitVcsDriverTestLayer);
2323
2424 function makeTmpDir(
2525 prefix = "git-test-",
@@ -63,11 +63,11 @@ function git( · 37 unmodified lines
6363 cwd: string,
6464 args: ReadonlyArray<string>,
6565 env?: NodeJS.ProcessEnv,
66−): Effect.Effect<string, GitCommandError, GitCore> {
66+): Effect.Effect<string, GitCommandError, GitVcsDriver.GitVcsDriver> {
6767 return Effect.gen(function* () {
68− const gitCore = yield* GitCore;
69− const result = yield* gitCore.execute({
70− operation: "GitCore.test.git",
68+ const gitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
69+ const result = yield* gitVcsDriver.execute({
70+ operation: "GitVcsDriver.test.git",
7171 cwd,
7272 args,
7373 ...(env ? { env } : {}),
@@ -82,7 +82,7 @@ function configureRemote( · 8 unmodified lines
8282 remoteName: string,
8383 remotePath: string,
8484 fetchNamespace: string,
85−): Effect.Effect<string, GitCommandError, GitCore> {
85+): Effect.Effect<string, GitCommandError, GitVcsDriver.GitVcsDriver> {
8686 return Effect.gen(function* () {
8787 yield* git(cwd, ["config", `remote.${remoteName}.url`, remotePath]);
8888 return yield* git(cwd, [
@@ -119,8 +119,8 @@ function runShellCommand(input: { · 30 unmodified lines
119119 });
120120 }
121121
122−const makeIsolatedGitCore = (executeOverride: GitCoreShape["execute"]) =>
123− makeGitCore({ executeOverride }).pipe(
122+const makeIsolatedGitVcsDriver = (executeOverride: GitVcsDriver.GitVcsDriverShape["execute"]) =>
123+ makeGitVcsDriverCore({ executeOverride }).pipe(
124124 Effect.provide(Layer.provideMerge(ServerConfigLayer, NodeServices.layer)),
125125 );
126126
@@ -130,10 +130,10 @@ function initRepoWithCommit( · 3 unmodified lines
130130 ): Effect.Effect<
131131 { initialBranch: string },
132132 GitCommandError | PlatformError.PlatformError,
133− GitCore | FileSystem.FileSystem
133+ GitVcsDriver.GitVcsDriver | FileSystem.FileSystem
134134 > {
135135 return Effect.gen(function* () {
136− const core = yield* GitCore;
136+ const core = yield* GitVcsDriver.GitVcsDriver;
137137 yield* core.initRepo({ cwd });
138138 yield* git(cwd, ["config", "user.email", "test@test.com"]);
139139 yield* git(cwd, ["config", "user.name", "Test"]);
@@ -154,7 +154,7 @@ function commitWithDate( · 14 unmodified lines
154154 ): Effect.Effect<
155155 void,
156156 GitCommandError | PlatformError.PlatformError,
157− GitCore | FileSystem.FileSystem
157+ GitVcsDriver.GitVcsDriver | FileSystem.FileSystem
158158 > {
159159 return Effect.gen(function* () {
160160 yield* writeTextFile(path.join(cwd, fileName), fileContents);
@@ -199,7 +199,7 @@ it.layer(TestLayer)("git integration", (it) => { · 38 unmodified lines
199199 it.effect("creates a valid git repo", () =>
200200 Effect.gen(function* () {
201201 const tmp = yield* makeTmpDir();
202− yield* (yield* GitCore).initRepo({ cwd: tmp });
202+ yield* (yield* GitVcsDriver.GitVcsDriver).initRepo({ cwd: tmp });
203203 expect(existsSync(path.join(tmp, ".git"))).toBe(true);
204204 }),
205205 );
@@ -208,7 +208,7 @@ it.layer(TestLayer)("git integration", (it) => { · 2 unmodified lines
208208 Effect.gen(function* () {
209209 const tmp = yield* makeTmpDir();
210210 yield* initRepoWithCommit(tmp);
211− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
211+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
212212 expect(result.isRepo).toBe(true);
213213 expect(result.hasOriginRemote).toBe(false);
214214 expect(result.branches.length).toBeGreaterThanOrEqual(1);
@@ -222,7 +222,7 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
222222 it.effect("returns isRepo: false for non-git directory", () =>
223223 Effect.gen(function* () {
224224 const tmp = yield* makeTmpDir();
225− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
225+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
226226 expect(result.isRepo).toBe(false);
227227 expect(result.hasOriginRemote).toBe(false);
228228 expect(result.branches).toEqual([]);
@@ -236,7 +236,7 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
236236 yield* makeDirectory(deletedDir);
237237 yield* removePath(deletedDir);
238238
239− const result = yield* (yield* GitCore).listBranches({ cwd: deletedDir });
239+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: deletedDir });
240240
241241 expect(result.isRepo).toBe(false);
242242 expect(result.hasOriginRemote).toBe(false);
@@ -248,7 +248,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
248248 Effect.gen(function* () {
249249 const tmp = yield* makeTmpDir();
250250 yield* initRepoWithCommit(tmp);
251− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
251+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
252252 const current = result.branches.find((b) => b.current);
253253 expect(current).toBeDefined();
254254 expect(current!.current).toBe(true);
@@ -261,7 +261,7 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
261261 yield* initRepoWithCommit(tmp);
262262 yield* git(tmp, ["checkout", "--detach", "HEAD"]);
263263
264− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
264+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
265265 expect(result.branches.some((branch) => branch.name.startsWith("("))).toBe(false);
266266 expect(result.branches.some((branch) => branch.current)).toBe(false);
267267 }),
@@ -271,12 +271,18 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
271271 Effect.gen(function* () {
272272 const tmp = yield* makeTmpDir();
273273 yield* initRepoWithCommit(tmp);
274− const initialBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
275− (branch) => branch.current,
276− )!.name;
274+ const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
275+ cwd: tmp,
276+ })).branches.find((branch) => branch.current)!.name;
277277
278− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "older-branch" });
279− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "older-branch" });
278+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
279+ cwd: tmp,
280+ branch: "older-branch",
281+ });
282+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
283+ cwd: tmp,
284+ branch: "older-branch",
285+ });
280286 yield* commitWithDate(
281287 tmp,
282288 "older.txt",
@@ -285,9 +291,18 @@ it.layer(TestLayer)("git integration", (it) => { · 2 unmodified lines
285291 "older branch change",
286292 );
287293
288− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: initialBranch });
289− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "newer-branch" });
290− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "newer-branch" });
294+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
295+ cwd: tmp,
296+ branch: initialBranch,
297+ });
298+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
299+ cwd: tmp,
300+ branch: "newer-branch",
301+ });
302+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
303+ cwd: tmp,
304+ branch: "newer-branch",
305+ });
291306 yield* commitWithDate(
292307 tmp,
293308 "newer.txt",
@@ -297,9 +312,12 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
297312 );
298313
299314 // Switch away to show current branch is pinned, then remaining branches are recency-sorted.
300− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "older-branch" });
315+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
316+ cwd: tmp,
317+ branch: "older-branch",
318+ });
301319
302− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
320+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
303321 expect(result.branches[0]!.name).toBe("older-branch");
304322 expect(result.branches[1]!.name).toBe("newer-branch");
305323 }),
@@ -310,17 +328,23 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
310328 const tmp = yield* makeTmpDir();
311329 const remote = yield* makeTmpDir();
312330 yield* initRepoWithCommit(tmp);
313− const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
314− (branch) => branch.current,
315− )!.name;
331+ const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
332+ cwd: tmp,
333+ })).branches.find((branch) => branch.current)!.name;
316334
317335 yield* git(remote, ["init", "--bare"]);
318336 yield* git(tmp, ["remote", "add", "origin", remote]);
319337 yield* git(tmp, ["push", "-u", "origin", defaultBranch]);
320338 yield* git(tmp, ["remote", "set-head", "origin", defaultBranch]);
321339
322− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "current-branch" });
323− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "current-branch" });
340+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
341+ cwd: tmp,
342+ branch: "current-branch",
343+ });
344+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
345+ cwd: tmp,
346+ branch: "current-branch",
347+ });
324348 yield* commitWithDate(
325349 tmp,
326350 "current.txt",
@@ -329,9 +353,18 @@ it.layer(TestLayer)("git integration", (it) => { · 2 unmodified lines
329353 "current change",
330354 );
331355
332− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: defaultBranch });
333− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "newer-branch" });
334− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "newer-branch" });
356+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
357+ cwd: tmp,
358+ branch: defaultBranch,
359+ });
360+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
361+ cwd: tmp,
362+ branch: "newer-branch",
363+ });
364+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
365+ cwd: tmp,
366+ branch: "newer-branch",
367+ });
335368 yield* commitWithDate(
336369 tmp,
337370 "newer.txt",
@@ -340,9 +373,12 @@ it.layer(TestLayer)("git integration", (it) => { · 2 unmodified lines
340373 "newer change",
341374 );
342375
343− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "current-branch" });
376+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
377+ cwd: tmp,
378+ branch: "current-branch",
379+ });
344380
345− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
381+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
346382 expect(result.branches[0]!.name).toBe("current-branch");
347383 expect(result.branches[1]!.name).toBe(defaultBranch);
348384 expect(result.branches[2]!.name).toBe("newer-branch");
@@ -353,10 +389,10 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
353389 Effect.gen(function* () {
354390 const tmp = yield* makeTmpDir();
355391 yield* initRepoWithCommit(tmp);
356− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-a" });
357− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-b" });
392+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature-a" });
393+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature-b" });
358394
359− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
395+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
360396 const names = result.branches.map((b) => b.name);
361397 expect(names).toContain("feature-a");
362398 expect(names).toContain("feature-b");
@@ -367,11 +403,14 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
367403 Effect.gen(function* () {
368404 const tmp = yield* makeTmpDir();
369405 const { initialBranch } = yield* initRepoWithCommit(tmp);
370− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-a" });
371− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-b" });
372− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-c" });
406+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature-a" });
407+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature-b" });
408+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature-c" });
373409
374− const firstPage = yield* (yield* GitCore).listBranches({ cwd: tmp, limit: 2 });
410+ const firstPage = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
411+ cwd: tmp,
412+ limit: 2,
413+ });
375414 expect(firstPage.totalCount).toBe(4);
376415 expect(firstPage.nextCursor).toBe(2);
377416 expect(firstPage.branches.map((branch) => branch.name)).toEqual([
@@ -379,7 +418,7 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
379418 "feature-a",
380419 ]);
381420
382− const secondPage = yield* (yield* GitCore).listBranches({
421+ const secondPage = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
383422 cwd: tmp,
384423 cursor: firstPage.nextCursor ?? 0,
385424 limit: 2,
@@ -403,7 +442,7 @@ it.layer(TestLayer)("git integration", (it) => { · 17 unmodified lines
403442 "copilot/rewrite-cli-in-rust",
404443 ] as const;
405444 for (const branchName of createdBranchNames) {
406− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: branchName });
445+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: branchName });
407446 }
408447 yield* git(tmp, ["config", "column.ui", "always"]);
409448
@@ -420,9 +459,9 @@ it.layer(TestLayer)("git integration", (it) => { · 10 unmodified lines
420459 ),
421460 ).toBe(true);
422461
423− const realGitCore = yield* GitCore;
424− const core = yield* makeIsolatedGitCore((input) =>
425− realGitCore.execute(
462+ const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
463+ const core = yield* makeIsolatedGitVcsDriver((input) =>
464+ realGitVcsDriver.execute(
426465 input.args[0] === "branch"
427466 ? {
428467 ...input,
@@ -455,7 +494,7 @@ it.layer(TestLayer)("git integration", (it) => { · 26 unmodified lines
455494 Effect.gen(function* () {
456495 const tmp = yield* makeTmpDir();
457496 yield* initRepoWithCommit(tmp);
458− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
497+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
459498 expect(result.branches.every((b) => b.isDefault === false)).toBe(true);
460499 }),
461500 );
@@ -467,23 +506,29 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
467506
468507 yield* git(remote, ["init", "--bare"]);
469508 yield* initRepoWithCommit(tmp);
470− const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
471− (branch) => branch.current,
472− )!.name;
509+ const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
510+ cwd: tmp,
511+ })).branches.find((branch) => branch.current)!.name;
473512
474513 yield* git(tmp, ["remote", "add", "origin", remote]);
475514 yield* git(tmp, ["push", "-u", "origin", defaultBranch]);
476515
477− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature/local-only" });
516+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
517+ cwd: tmp,
518+ branch: "feature/local-only",
519+ });
478520
479521 const remoteOnlyBranch = "feature/remote-only";
480− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: defaultBranch });
522+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
523+ cwd: tmp,
524+ branch: defaultBranch,
525+ });
481526 yield* git(tmp, ["checkout", "-b", remoteOnlyBranch]);
482527 yield* git(tmp, ["push", "-u", "origin", remoteOnlyBranch]);
483528 yield* git(tmp, ["checkout", defaultBranch]);
484529 yield* git(tmp, ["branch", "-D", remoteOnlyBranch]);
485530
486− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
531+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
487532 const firstRemoteIndex = result.branches.findIndex((branch) => branch.isRemote);
488533
489534 expect(result.hasOriginRemote).toBe(true);
@@ -515,9 +560,9 @@ it.layer(TestLayer)("git integration", (it) => { · 25 unmodified lines
515560
516561 yield* git(remote, ["init", "--bare"]);
517562 yield* initRepoWithCommit(tmp);
518− const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
519− (branch) => branch.current,
520− )!.name;
563+ const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
564+ cwd: tmp,
565+ })).branches.find((branch) => branch.current)!.name;
521566
522567 yield* git(tmp, ["remote", "add", remoteName, remote]);
523568 yield* git(tmp, ["push", "-u", remoteName, defaultBranch]);
@@ -528,7 +573,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
528573 yield* git(tmp, ["checkout", defaultBranch]);
529574 yield* git(tmp, ["branch", "-D", remoteOnlyBranch]);
530575
531− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
576+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
532577 const remoteBranch = result.branches.find(
533578 (branch) => branch.name === `${remoteName}/${remoteOnlyBranch}`,
534579 );
@@ -551,7 +596,10 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
551596 yield* git(tmp, ["remote", "add", "origin", remote]);
552597 yield* git(tmp, ["push", "-u", "origin", initialBranch]);
553598
554− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature/demo" });
599+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
600+ cwd: tmp,
601+ branch: "feature/demo",
602+ });
555603 yield* git(tmp, ["push", "-u", "origin", "feature/demo"]);
556604
557605 yield* git(tmp, ["checkout", "-b", "feature/remote-only"]);
@@ -559,7 +607,7 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
559607 yield* git(tmp, ["checkout", initialBranch]);
560608 yield* git(tmp, ["branch", "-D", "feature/remote-only"]);
561609
562− const result = yield* (yield* GitCore).listBranches({
610+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
563611 cwd: tmp,
564612 query: "feature/",
565613 limit: 10,
@@ -582,11 +630,11 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
582630 Effect.gen(function* () {
583631 const tmp = yield* makeTmpDir();
584632 yield* initRepoWithCommit(tmp);
585− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature" });
633+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature" });
586634
587− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "feature" });
635+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "feature" });
588636
589− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
637+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
590638 const current = result.branches.find((b) => b.current);
591639 expect(current!.name).toBe("feature");
592640 }),
@@ -603,20 +651,29 @@ it.layer(TestLayer)("git integration", (it) => { · 10 unmodified lines
603651 yield* git(remote, ["init", "--bare"]);
604652
605653 yield* initRepoWithCommit(source);
606− const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(
607− (branch) => branch.current,
608− )!.name;
654+ const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
655+ cwd: source,
656+ })).branches.find((branch) => branch.current)!.name;
609657 yield* git(source, ["remote", "add", "origin", remote]);
610658 yield* git(source, ["push", "-u", "origin", defaultBranch]);
611659
612660 const featureBranch = "feature-behind";
613− yield* (yield* GitCore).createBranch({ cwd: source, branch: featureBranch });
614− yield* (yield* GitCore).checkoutBranch({ cwd: source, branch: featureBranch });
661+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
662+ cwd: source,
663+ branch: featureBranch,
664+ });
665+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
666+ cwd: source,
667+ branch: featureBranch,
668+ });
615669 yield* writeTextFile(path.join(source, "feature.txt"), "feature base\n");
616670 yield* git(source, ["add", "feature.txt"]);
617671 yield* git(source, ["commit", "-m", "feature base"]);
618672 yield* git(source, ["push", "-u", "origin", featureBranch]);
619− yield* (yield* GitCore).checkoutBranch({ cwd: source, branch: defaultBranch });
673+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
674+ cwd: source,
675+ branch: defaultBranch,
676+ });
620677
621678 yield* git(clone, ["clone", remote, "."]);
622679 yield* git(clone, ["config", "user.email", "test@test.com"]);
@@ -627,8 +684,11 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
627684 yield* git(clone, ["commit", "-m", "remote feature update"]);
628685 yield* git(clone, ["push", "origin", featureBranch]);
629686
630− yield* (yield* GitCore).checkoutBranch({ cwd: source, branch: featureBranch });
631− const core = yield* GitCore;
687+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
688+ cwd: source,
689+ branch: featureBranch,
690+ });
691+ const core = yield* GitVcsDriver.GitVcsDriver;
632692 yield* Effect.promise(() =>
633693 vi.waitFor(
634694 async () => {
@@ -653,9 +713,9 @@ it.layer(TestLayer)("git integration", (it) => { · 18 unmodified lines
653713 yield* git(remote, ["init", "--bare"]);
654714
655715 yield* initRepoWithCommit(source);
656− const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(
657− (branch) => branch.current,
658− )!.name;
716+ const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
717+ cwd: source,
718+ })).branches.find((branch) => branch.current)!.name;
659719 yield* git(source, ["remote", "add", "origin", remote]);
660720 yield* git(source, ["push", "-u", "origin", defaultBranch]);
661721
@@ -668,9 +728,9 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
668728 yield* git(source, ["push", "-u", "origin", featureBranch]);
669729 yield* git(source, ["checkout", defaultBranch]);
670730
671− const realGitCore = yield* GitCore;
731+ const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
672732 let refreshFetchAttempts = 0;
673− const core = yield* makeIsolatedGitCore((input) => {
733+ const core = yield* makeIsolatedGitVcsDriver((input) => {
674734 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
675735 refreshFetchAttempts += 1;
676736 return Effect.fail(
@@ -682,7 +742,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
682742 }),
683743 );
684744 }
685− return realGitCore.execute(input);
745+ return realGitVcsDriver.execute(input);
686746 });
687747 yield* core.checkoutBranch({ cwd: source, branch: featureBranch });
688748 const status = yield* core.statusDetails(source);
@@ -700,9 +760,9 @@ it.layer(TestLayer)("git integration", (it) => { · 11 unmodified lines
700760 yield* git(remote, ["init", "--bare"]);
701761
702762 yield* initRepoWithCommit(source);
703− const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(
704− (branch) => branch.current,
705− )!.name;
763+ const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
764+ cwd: source,
765+ })).branches.find((branch) => branch.current)!.name;
706766 yield* git(source, ["remote", "add", "origin", remote]);
707767 yield* git(source, ["push", "-u", "origin", defaultBranch]);
708768
@@ -714,9 +774,9 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
714774 yield* git(source, ["push", "-u", "origin", featureBranch]);
715775 yield* git(source, ["checkout", defaultBranch]);
716776
717− const realGitCore = yield* GitCore;
777+ const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
718778 let refreshFetchAttempts = 0;
719− const core = yield* makeIsolatedGitCore((input) => {
779+ const core = yield* makeIsolatedGitVcsDriver((input) => {
720780 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
721781 refreshFetchAttempts += 1;
722782 return Effect.succeed({
@@ -727,7 +787,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
727787 stderrTruncated: false,
728788 });
729789 }
730− return realGitCore.execute(input);
790+ return realGitVcsDriver.execute(input);
731791 });
732792 yield* core.checkoutBranch({ cwd: source, branch: featureBranch });
733793 yield* Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, 50)));
@@ -750,7 +810,7 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
750810 });
751811
752812 let fetchCount = 0;
753− const core = yield* makeIsolatedGitCore((input) => {
813+ const core = yield* makeIsolatedGitVcsDriver((input) => {
754814 if (
755815 input.args[0] === "rev-parse" &&
756816 input.args[1] === "--abbrev-ref" &&
@@ -780,7 +840,7 @@ it.layer(TestLayer)("git integration", (it) => { · 23 unmodified lines
780840 ]);
781841 return ok();
782842 }
783− if (input.operation === "GitCore.statusDetails.status") {
843+ if (input.operation === "GitVcsDriver.statusDetails.status") {
784844 return ok(
785845 input.cwd === "/repo/worktrees/pr-123"
786846 ? "# branch.head feature/pr-123\n# branch.upstream origin/feature/pr-123\n# branch.ab +0 -0\n"
@@ -788,12 +848,12 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
788848 );
789849 }
790850 if (
791− input.operation === "GitCore.statusDetails.unstagedNumstat" ||
792− input.operation === "GitCore.statusDetails.stagedNumstat"
851+ input.operation === "GitVcsDriver.statusDetails.unstagedNumstat" ||
852+ input.operation === "GitVcsDriver.statusDetails.stagedNumstat"
793853 ) {
794854 return ok();
795855 }
796− if (input.operation === "GitCore.statusDetails.defaultRef") {
856+ if (input.operation === "GitVcsDriver.statusDetails.defaultRef") {
797857 return ok("refs/remotes/origin/main\n");
798858 }
799859 return Effect.fail(
@@ -826,7 +886,7 @@ it.layer(TestLayer)("git integration", (it) => { · 26 unmodified lines
826886 });
827887
828888 let fetchCount = 0;
829− const core = yield* makeIsolatedGitCore((input) => {
889+ const core = yield* makeIsolatedGitVcsDriver((input) => {
830890 if (
831891 input.args[0] === "rev-parse" &&
832892 input.args[1] === "--abbrev-ref" &&
@@ -856,7 +916,7 @@ it.layer(TestLayer)("git integration", (it) => { · 23 unmodified lines
856916 }),
857917 );
858918 }
859− if (input.operation === "GitCore.statusDetails.status") {
919+ if (input.operation === "GitVcsDriver.statusDetails.status") {
860920 return ok(
861921 input.cwd === "/repo/worktrees/pr-123"
862922 ? "# branch.head feature/pr-123\n# branch.upstream origin/feature/pr-123\n# branch.ab +0 -0\n"
@@ -864,12 +924,12 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
864924 );
865925 }
866926 if (
867− input.operation === "GitCore.statusDetails.unstagedNumstat" ||
868− input.operation === "GitCore.statusDetails.stagedNumstat"
927+ input.operation === "GitVcsDriver.statusDetails.unstagedNumstat" ||
928+ input.operation === "GitVcsDriver.statusDetails.stagedNumstat"
869929 ) {
870930 return ok();
871931 }
872− if (input.operation === "GitCore.statusDetails.defaultRef") {
932+ if (input.operation === "GitVcsDriver.statusDetails.defaultRef") {
873933 return ok("refs/remotes/origin/main\n");
874934 }
875935 return Effect.fail(
@@ -893,7 +953,7 @@ it.layer(TestLayer)("git integration", (it) => { · 17 unmodified lines
893953 const tmp = yield* makeTmpDir();
894954 yield* initRepoWithCommit(tmp);
895955 const result = yield* Effect.result(
896− (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "nonexistent" }),
956+ (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "nonexistent" }),
897957 );
898958 expect(result._tag).toBe("Failure");
899959 }),
@@ -906,16 +966,19 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
906966 yield* git(remote, ["init", "--bare"]);
907967
908968 yield* initRepoWithCommit(source);
909− const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(
910− (branch) => branch.current,
911− )!.name;
969+ const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
970+ cwd: source,
971+ })).branches.find((branch) => branch.current)!.name;
912972 yield* git(source, ["remote", "add", "origin", remote]);
913973 yield* git(source, ["push", "-u", "origin", defaultBranch]);
914974
915− yield* (yield* GitCore).createBranch({ cwd: source, branch: "feature" });
975+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: source, branch: "feature" });
916976
917977 const checkoutResult = yield* Effect.result(
918− (yield* GitCore).checkoutBranch({ cwd: source, branch: "origin/feature" }),
978+ (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
979+ cwd: source,
980+ branch: "origin/feature",
981+ }),
919982 );
920983 expect(checkoutResult._tag).toBe("Failure");
921984 expect(yield* git(source, ["branch", "--show-current"])).toBe(defaultBranch);
@@ -935,9 +998,9 @@ it.layer(TestLayer)("git integration", (it) => { · 13 unmodified lines
935998 yield* git(prefixRemote, ["init", "--bare"]);
936999
9371000 yield* initRepoWithCommit(source);
938− const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(
939− (branch) => branch.current,
940− )!.name;
1001+ const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1002+ cwd: source,
1003+ })).branches.find((branch) => branch.current)!.name;
9411004 yield* configureRemote(source, prefixRemoteName, prefixRemote, prefixFetchNamespace);
9421005 yield* configureRemote(source, remoteName, remote, remoteName);
9431006 yield* git(source, ["push", "-u", remoteName, defaultBranch]);
@@ -950,16 +1013,16 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
9501013 yield* git(source, ["checkout", defaultBranch]);
9511014 yield* git(source, ["branch", "-D", featureBranch]);
9521015
953− const checkoutResult = yield* (yield* GitCore).checkoutBranch({
1016+ const checkoutResult = yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
9541017 cwd: source,
9551018 branch: `${remoteName}/${featureBranch}`,
9561019 });
9571020
9581021 expect(checkoutResult.branch).toBe("upstream/feature");
9591022 expect(yield* git(source, ["branch", "--show-current"])).toBe("upstream/feature");
960− const realGitCore = yield* GitCore;
1023+ const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
9611024 let fetchArgs: readonly string[] | null = null;
962− const core = yield* makeIsolatedGitCore((input) => {
1025+ const core = yield* makeIsolatedGitVcsDriver((input) => {
9631026 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
9641027 fetchArgs = [...input.args];
9651028 return Effect.succeed({
@@ -970,7 +1033,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
9701033 stderrTruncated: false,
9711034 });
9721035 }
973− return realGitCore.execute(input);
1036+ return realGitVcsDriver.execute(input);
9741037 });
9751038
9761039 const status = yield* core.statusDetails(source);
@@ -996,7 +1059,7 @@ it.layer(TestLayer)("git integration", (it) => { · 19 unmodified lines
9961059 yield* git(remote, ["init", "--bare"]);
9971060
9981061 yield* initRepoWithCommit(source);
999− const defaultBranch = (yield* (yield* GitCore).listBranches({
1062+ const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
10001063 cwd: source,
10011064 })).branches.find((branch) => branch.current)!.name;
10021065 yield* git(source, ["remote", "add", "origin", remote]);
@@ -1006,12 +1069,12 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
10061069 // would attempt to create an already-existing local branch.
10071070 yield* git(source, ["branch", "--unset-upstream"]);
10081071
1009− yield* (yield* GitCore).checkoutBranch({
1072+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
10101073 cwd: source,
10111074 branch: `origin/${defaultBranch}`,
10121075 });
10131076
1014− const core = yield* GitCore;
1077+ const core = yield* GitVcsDriver.GitVcsDriver;
10151078 const status = yield* core.statusDetails(source);
10161079 expect(status.branch).toBeNull();
10171080 }),
@@ -1021,7 +1084,7 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
10211084 Effect.gen(function* () {
10221085 const tmp = yield* makeTmpDir();
10231086 yield* initRepoWithCommit(tmp);
1024− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "other" });
1087+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "other" });
10251088
10261089 // Create a conflicting change: modify README on current branch
10271090 yield* writeTextFile(path.join(tmp, "README.md"), "modified\n");
@@ -1029,23 +1092,26 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
10291092
10301093 // First, checkout other branch cleanly
10311094 yield* git(tmp, ["stash"]);
1032− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "other" });
1095+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "other" });
10331096 yield* writeTextFile(path.join(tmp, "README.md"), "other content\n");
10341097 yield* git(tmp, ["add", "."]);
10351098 yield* git(tmp, ["commit", "-m", "other change"]);
10361099
10371100 // Go back to default branch
1038− const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1039− (b) => !b.current,
1040− )!.name;
1041− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: defaultBranch });
1101+ const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1102+ cwd: tmp,
1103+ })).branches.find((b) => !b.current)!.name;
1104+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1105+ cwd: tmp,
1106+ branch: defaultBranch,
1107+ });
10421108
10431109 // Make uncommitted changes to the same file
10441110 yield* writeTextFile(path.join(tmp, "README.md"), "conflicting local\n");
10451111
10461112 // Checkout should fail due to uncommitted changes
10471113 const result = yield* Effect.result(
1048− (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "other" }),
1114+ (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "other" }),
10491115 );
10501116 expect(result._tag).toBe("Failure");
10511117 }),
@@ -1059,9 +1125,9 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
10591125 Effect.gen(function* () {
10601126 const tmp = yield* makeTmpDir();
10611127 yield* initRepoWithCommit(tmp);
1062− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "new-feature" });
1128+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "new-feature" });
10631129
1064− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
1130+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
10651131 expect(result.branches.some((b) => b.name === "new-feature")).toBe(true);
10661132 }),
10671133 );
@@ -1070,9 +1136,9 @@ it.layer(TestLayer)("git integration", (it) => { · 2 unmodified lines
10701136 Effect.gen(function* () {
10711137 const tmp = yield* makeTmpDir();
10721138 yield* initRepoWithCommit(tmp);
1073− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "dupe" });
1139+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "dupe" });
10741140 const result = yield* Effect.result(
1075− (yield* GitCore).createBranch({ cwd: tmp, branch: "dupe" }),
1141+ (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "dupe" }),
10761142 );
10771143 expect(result._tag).toBe("Failure");
10781144 }),
@@ -1086,10 +1152,16 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
10861152 Effect.gen(function* () {
10871153 const tmp = yield* makeTmpDir();
10881154 yield* initRepoWithCommit(tmp);
1089− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature/old-name" });
1090− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "feature/old-name" });
1155+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1156+ cwd: tmp,
1157+ branch: "feature/old-name",
1158+ });
1159+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1160+ cwd: tmp,
1161+ branch: "feature/old-name",
1162+ });
10911163
1092− const renamed = yield* (yield* GitCore).renameBranch({
1164+ const renamed = yield* (yield* GitVcsDriver.GitVcsDriver).renameBranch({
10931165 cwd: tmp,
10941166 oldBranch: "feature/old-name",
10951167 newBranch: "feature/new-name",
@@ -1097,7 +1169,7 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
10971169
10981170 expect(renamed.branch).toBe("feature/new-name");
10991171
1100− const branches = yield* (yield* GitCore).listBranches({ cwd: tmp });
1172+ const branches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
11011173 expect(branches.branches.some((branch) => branch.name === "feature/old-name")).toBe(false);
11021174 const current = branches.branches.find((branch) => branch.current);
11031175 expect(current?.name).toBe("feature/new-name");
@@ -1108,11 +1180,11 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
11081180 Effect.gen(function* () {
11091181 const tmp = yield* makeTmpDir();
11101182 yield* initRepoWithCommit(tmp);
1111− const current = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1112− (b) => b.current,
1113− )!;
1183+ const current = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1184+ cwd: tmp,
1185+ })).branches.find((b) => b.current)!;
11141186
1115− const renamed = yield* (yield* GitCore).renameBranch({
1187+ const renamed = yield* (yield* GitVcsDriver.GitVcsDriver).renameBranch({
11161188 cwd: tmp,
11171189 oldBranch: current.name,
11181190 newBranch: current.name,
@@ -1126,18 +1198,27 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
11261198 Effect.gen(function* () {
11271199 const tmp = yield* makeTmpDir();
11281200 yield* initRepoWithCommit(tmp);
1129− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "t3code/feat/session" });
1130− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "t3code/tmp-working" });
1131− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "t3code/tmp-working" });
1201+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1202+ cwd: tmp,
1203+ branch: "t3code/feat/session",
1204+ });
1205+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1206+ cwd: tmp,
1207+ branch: "t3code/tmp-working",
1208+ });
1209+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1210+ cwd: tmp,
1211+ branch: "t3code/tmp-working",
1212+ });
11321213
1133− const renamed = yield* (yield* GitCore).renameBranch({
1214+ const renamed = yield* (yield* GitVcsDriver.GitVcsDriver).renameBranch({
11341215 cwd: tmp,
11351216 oldBranch: "t3code/tmp-working",
11361217 newBranch: "t3code/feat/session",
11371218 });
11381219
11391220 expect(renamed.branch).toBe("t3code/feat/session-1");
1140− const branches = yield* (yield* GitCore).listBranches({ cwd: tmp });
1221+ const branches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
11411222 expect(branches.branches.some((branch) => branch.name === "t3code/feat/session")).toBe(
11421223 true,
11431224 );
@@ -1153,12 +1234,24 @@ it.layer(TestLayer)("git integration", (it) => { · 9 unmodified lines
11531234 Effect.gen(function* () {
11541235 const tmp = yield* makeTmpDir();
11551236 yield* initRepoWithCommit(tmp);
1156− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "t3code/feat/session" });
1157− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "t3code/feat/session-1" });
1158− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "t3code/tmp-working" });
1159− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "t3code/tmp-working" });
1237+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1238+ cwd: tmp,
1239+ branch: "t3code/feat/session",
1240+ });
1241+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1242+ cwd: tmp,
1243+ branch: "t3code/feat/session-1",
1244+ });
1245+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1246+ cwd: tmp,
1247+ branch: "t3code/tmp-working",
1248+ });
1249+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1250+ cwd: tmp,
1251+ branch: "t3code/tmp-working",
1252+ });
11601253
1161− const renamed = yield* (yield* GitCore).renameBranch({
1254+ const renamed = yield* (yield* GitVcsDriver.GitVcsDriver).renameBranch({
11621255 cwd: tmp,
11631256 oldBranch: "t3code/tmp-working",
11641257 newBranch: "t3code/feat/session",
@@ -1172,16 +1265,22 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
11721265 Effect.gen(function* () {
11731266 const tmp = yield* makeTmpDir();
11741267 yield* initRepoWithCommit(tmp);
1175− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature/old-name" });
1176− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "feature/old-name" });
1268+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1269+ cwd: tmp,
1270+ branch: "feature/old-name",
1271+ });
1272+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1273+ cwd: tmp,
1274+ branch: "feature/old-name",
1275+ });
11771276
1178− const realGitCore = yield* GitCore;
1277+ const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
11791278 let renameArgs: ReadonlyArray<string> | null = null;
1180− const core = yield* makeIsolatedGitCore((input) => {
1279+ const core = yield* makeIsolatedGitVcsDriver((input) => {
11811280 if (input.args[0] === "branch" && input.args[1] === "-m") {
11821281 renameArgs = [...input.args];
11831282 }
1184− return realGitCore.execute(input);
1283+ return realGitVcsDriver.execute(input);
11851284 });
11861285
11871286 const renamed = yield* core.renameBranch({
@@ -1205,11 +1304,11 @@ it.layer(TestLayer)("git integration", (it) => { · 17 unmodified lines
12051304 yield* initRepoWithCommit(tmp);
12061305
12071306 const wtPath = path.join(tmp, "worktree-out");
1208− const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1209− (b) => b.current,
1210− )!.name;
1307+ const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1308+ cwd: tmp,
1309+ })).branches.find((b) => b.current)!.name;
12111310
1212− const result = yield* (yield* GitCore).createWorktree({
1311+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
12131312 cwd: tmp,
12141313 branch: currentBranch,
12151314 newBranch: "wt-branch",
@@ -1222,7 +1321,7 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
12221321 expect(existsSync(path.join(wtPath, "README.md"))).toBe(true);
12231322
12241323 // Clean up worktree before tmp dir disposal
1225− yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });
1324+ yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
12261325 }),
12271326 );
12281327
@@ -1232,11 +1331,11 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
12321331 yield* initRepoWithCommit(tmp);
12331332
12341333 const wtPath = path.join(tmp, "wt-check-dir");
1235− const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1236− (b) => b.current,
1237− )!.name;
1334+ const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1335+ cwd: tmp,
1336+ })).branches.find((b) => b.current)!.name;
12381337
1239− yield* (yield* GitCore).createWorktree({
1338+ yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
12401339 cwd: tmp,
12411340 branch: currentBranch,
12421341 newBranch: "wt-check",
@@ -1247,7 +1346,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
12471346 const branchOutput = yield* git(wtPath, ["branch", "--show-current"]);
12481347 expect(branchOutput).toBe("wt-check");
12491348
1250− yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });
1349+ yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
12511350 }),
12521351 );
12531352
@@ -1255,10 +1354,13 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
12551354 Effect.gen(function* () {
12561355 const tmp = yield* makeTmpDir();
12571356 yield* initRepoWithCommit(tmp);
1258− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature/existing-worktree" });
1357+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1358+ cwd: tmp,
1359+ branch: "feature/existing-worktree",
1360+ });
12591361
12601362 const wtPath = path.join(tmp, "wt-existing");
1261− const result = yield* (yield* GitCore).createWorktree({
1363+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
12621364 cwd: tmp,
12631365 branch: "feature/existing-worktree",
12641366 path: wtPath,
@@ -1269,7 +1371,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
12691371 const branchOutput = yield* git(wtPath, ["branch", "--show-current"]);
12701372 expect(branchOutput).toBe("feature/existing-worktree");
12711373
1272− yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });
1374+ yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
12731375 }),
12741376 );
12751377
@@ -1277,15 +1379,15 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
12771379 Effect.gen(function* () {
12781380 const tmp = yield* makeTmpDir();
12791381 yield* initRepoWithCommit(tmp);
1280− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "existing" });
1382+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "existing" });
12811383
12821384 const wtPath = path.join(tmp, "wt-conflict");
1283− const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1284− (b) => b.current,
1285− )!.name;
1385+ const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1386+ cwd: tmp,
1387+ })).branches.find((b) => b.current)!.name;
12861388
12871389 const result = yield* Effect.result(
1288− (yield* GitCore).createWorktree({
1390+ (yield* GitVcsDriver.GitVcsDriver).createWorktree({
12891391 cwd: tmp,
12901392 branch: currentBranch,
12911393 newBranch: "existing",
@@ -1302,11 +1404,11 @@ it.layer(TestLayer)("git integration", (it) => { · 10 unmodified lines
13021404 yield* initRepoWithCommit(tmp);
13031405
13041406 const wtPath = path.join(tmp, "wt-list-dir");
1305− const mainBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1306− (b) => b.current,
1307− )!.name;
1407+ const mainBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1408+ cwd: tmp,
1409+ })).branches.find((b) => b.current)!.name;
13081410
1309− yield* (yield* GitCore).createWorktree({
1411+ yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
13101412 cwd: tmp,
13111413 branch: mainBranch,
13121414 newBranch: "wt-list",
@@ -1314,17 +1416,17 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
13141416 });
13151417
13161418 // listGitBranches from the worktree should show wt-list as current
1317− const wtBranches = yield* (yield* GitCore).listBranches({ cwd: wtPath });
1419+ const wtBranches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: wtPath });
13181420 expect(wtBranches.isRepo).toBe(true);
13191421 const wtCurrent = wtBranches.branches.find((b) => b.current);
13201422 expect(wtCurrent!.name).toBe("wt-list");
13211423
13221424 // Main repo should still show the original branch as current
1323− const mainBranches = yield* (yield* GitCore).listBranches({ cwd: tmp });
1425+ const mainBranches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
13241426 const mainCurrent = mainBranches.branches.find((b) => b.current);
13251427 expect(mainCurrent!.name).toBe(mainBranch);
13261428
1327− yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });
1429+ yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
13281430 }),
13291431 );
13301432
@@ -1334,11 +1436,11 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
13341436 yield* initRepoWithCommit(tmp);
13351437
13361438 const wtPath = path.join(tmp, "wt-remove-dir");
1337− const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1338− (b) => b.current,
1339− )!.name;
1439+ const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1440+ cwd: tmp,
1441+ })).branches.find((b) => b.current)!.name;
13401442
1341− yield* (yield* GitCore).createWorktree({
1443+ yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
13421444 cwd: tmp,
13431445 branch: currentBranch,
13441446 newBranch: "wt-remove",
@@ -1346,7 +1448,7 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
13461448 });
13471449 expect(existsSync(wtPath)).toBe(true);
13481450
1349− yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });
1451+ yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
13501452 expect(existsSync(wtPath)).toBe(false);
13511453 }),
13521454 );
@@ -1357,11 +1459,11 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
13571459 yield* initRepoWithCommit(tmp);
13581460
13591461 const wtPath = path.join(tmp, "wt-dirty-dir");
1360− const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1361− (b) => b.current,
1362− )!.name;
1462+ const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1463+ cwd: tmp,
1464+ })).branches.find((b) => b.current)!.name;
13631465
1364− yield* (yield* GitCore).createWorktree({
1466+ yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
13651467 cwd: tmp,
13661468 branch: currentBranch,
13671469 newBranch: "wt-dirty",
@@ -1372,12 +1474,16 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
13721474 yield* writeTextFile(path.join(wtPath, "README.md"), "dirty change\n");
13731475
13741476 const failedRemove = yield* Effect.result(
1375− (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath }),
1477+ (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath }),
13761478 );
13771479 expect(failedRemove._tag).toBe("Failure");
13781480 expect(existsSync(wtPath)).toBe(true);
13791481
1380− yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath, force: true });
1482+ yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({
1483+ cwd: tmp,
1484+ path: wtPath,
1485+ force: true,
1486+ });
13811487 expect(existsSync(wtPath)).toBe(false);
13821488 }),
13831489 );
@@ -1390,10 +1496,16 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
13901496 Effect.gen(function* () {
13911497 const tmp = yield* makeTmpDir();
13921498 yield* initRepoWithCommit(tmp);
1393− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-login" });
1394− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "feature-login" });
1499+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1500+ cwd: tmp,
1501+ branch: "feature-login",
1502+ });
1503+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1504+ cwd: tmp,
1505+ branch: "feature-login",
1506+ });
13951507
1396− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
1508+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
13971509 const current = result.branches.find((b) => b.current);
13981510 expect(current!.name).toBe("feature-login");
13991511 }),
@@ -1408,12 +1520,12 @@ it.layer(TestLayer)("git integration", (it) => { · 8 unmodified lines
14081520 const tmp = yield* makeTmpDir();
14091521 yield* initRepoWithCommit(tmp);
14101522
1411− const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1412− (b) => b.current,
1413− )!.name;
1523+ const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1524+ cwd: tmp,
1525+ })).branches.find((b) => b.current)!.name;
14141526
14151527 const wtPath = path.join(tmp, "my-worktree");
1416− const result = yield* (yield* GitCore).createWorktree({
1528+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
14171529 cwd: tmp,
14181530 branch: currentBranch,
14191531 newBranch: "feature-wt",
@@ -1424,7 +1536,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
14241536 expect(existsSync(result.worktree.path)).toBe(true);
14251537
14261538 // Main repo still on original branch
1427− const mainBranches = yield* (yield* GitCore).listBranches({ cwd: tmp });
1539+ const mainBranches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
14281540 const mainCurrent = mainBranches.branches.find((b) => b.current);
14291541 expect(mainCurrent!.name).toBe(currentBranch);
14301542
@@ -1432,7 +1544,7 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
14321544 const wtBranch = yield* git(wtPath, ["branch", "--show-current"]);
14331545 expect(wtBranch).toBe("feature-wt");
14341546
1435− yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });
1547+ yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
14361548 }),
14371549 );
14381550 });
@@ -1454,7 +1566,7 @@ it.layer(TestLayer)("git integration", (it) => { · 15 unmodified lines
14541566 yield* git(tmp, ["push", "origin", "HEAD:refs/pull/55/head"]);
14551567 yield* git(tmp, ["checkout", initialBranch]);
14561568
1457− yield* (yield* GitCore).fetchPullRequestBranch({
1569+ yield* (yield* GitVcsDriver.GitVcsDriver).fetchPullRequestBranch({
14581570 cwd: tmp,
14591571 prNumber: 55,
14601572 branch: "feature/pr-fetch",
@@ -1475,22 +1587,22 @@ it.layer(TestLayer)("git integration", (it) => { · 14 unmodified lines
14751587 Effect.gen(function* () {
14761588 const tmp = yield* makeTmpDir();
14771589 yield* initRepoWithCommit(tmp);
1478− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "branch-a" });
1479− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "branch-b" });
1590+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "branch-a" });
1591+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "branch-b" });
14801592
14811593 // Simulate switching to thread A's branch
1482− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "branch-a" });
1483− let branches = yield* (yield* GitCore).listBranches({ cwd: tmp });
1594+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "branch-a" });
1595+ let branches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
14841596 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a");
14851597
14861598 // Simulate switching to thread B's branch
1487− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "branch-b" });
1488− branches = yield* (yield* GitCore).listBranches({ cwd: tmp });
1599+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "branch-b" });
1600+ branches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
14891601 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b");
14901602
14911603 // Switch back to thread A
1492− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "branch-a" });
1493− branches = yield* (yield* GitCore).listBranches({ cwd: tmp });
1604+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "branch-a" });
1605+ branches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
14941606 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a");
14951607 }),
14961608 );
@@ -1503,40 +1615,43 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
15031615 Effect.gen(function* () {
15041616 const tmp = yield* makeTmpDir();
15051617 yield* initRepoWithCommit(tmp);
1506− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "diverged" });
1618+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "diverged" });
15071619
15081620 // Make diverged branch have different file content
1509− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "diverged" });
1621+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "diverged" });
15101622 yield* writeTextFile(path.join(tmp, "README.md"), "diverged content\n");
15111623 yield* git(tmp, ["add", "."]);
15121624 yield* git(tmp, ["commit", "-m", "diverge"]);
15131625
15141626 // Actually, let's just get back to the initial branch explicitly
1515− const allBranches = yield* (yield* GitCore).listBranches({ cwd: tmp });
1627+ const allBranches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
15161628 const initialBranch = allBranches.branches.find((b) => b.name !== "diverged")!.name;
1517− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: initialBranch });
1629+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1630+ cwd: tmp,
1631+ branch: initialBranch,
1632+ });
15181633
15191634 // Make local uncommitted changes to the same file
15201635 yield* writeTextFile(path.join(tmp, "README.md"), "local uncommitted\n");
15211636
15221637 // Attempt checkout should fail
15231638 const failedCheckout = yield* Effect.result(
1524− (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "diverged" }),
1639+ (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "diverged" }),
15251640 );
15261641 expect(failedCheckout._tag).toBe("Failure");
15271642
15281643 // Current branch should still be the initial one
1529− const result = yield* (yield* GitCore).listBranches({ cwd: tmp });
1644+ const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
15301645 expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch);
15311646 }),
15321647 );
15331648 });
15341649
1535− describe("GitCore", () => {
1650+ describe("GitVcsDriver", () => {
15361651 it.effect("supports branch lifecycle operations through the service API", () =>
15371652 Effect.gen(function* () {
15381653 const tmp = yield* makeTmpDir();
1539− const core = yield* GitCore;
1654+ const core = yield* GitVcsDriver.GitVcsDriver;
15401655
15411656 yield* core.initRepo({ cwd: tmp });
15421657 yield* git(tmp, ["config", "user.email", "test@test.com"]);
@@ -1563,7 +1678,7 @@ it.layer(TestLayer)("git integration", (it) => { · 20 unmodified lines
15631678 Effect.gen(function* () {
15641679 const tmp = yield* makeTmpDir();
15651680 yield* initRepoWithCommit(tmp);
1566− const core = yield* GitCore;
1681+ const core = yield* GitVcsDriver.GitVcsDriver;
15671682
15681683 yield* git(tmp, ["remote", "add", "origin", "git@github.com:pingdotgg/t3code.git"]);
15691684
@@ -1582,7 +1697,7 @@ it.layer(TestLayer)("git integration", (it) => { · 12 unmodified lines
15821697 Effect.gen(function* () {
15831698 const tmp = yield* makeTmpDir();
15841699 yield* initRepoWithCommit(tmp);
1585− const core = yield* GitCore;
1700+ const core = yield* GitVcsDriver.GitVcsDriver;
15861701
15871702 const clean = yield* core.status({ cwd: tmp });
15881703 expect(clean.hasWorkingTreeChanges).toBe(false);
@@ -1600,7 +1715,7 @@ it.layer(TestLayer)("git integration", (it) => { · 11 unmodified lines
16001715 const deletedDir = path.join(tmp, "deleted-repo");
16011716 yield* makeDirectory(deletedDir);
16021717 yield* removePath(deletedDir);
1603− const core = yield* GitCore;
1718+ const core = yield* GitVcsDriver.GitVcsDriver;
16041719
16051720 const status = yield* core.statusDetails(deletedDir);
16061721 const localStatus = yield* core.statusDetailsLocal(deletedDir);
@@ -1629,7 +1744,7 @@ it.layer(TestLayer)("git integration", (it) => { · 22 unmodified lines
16291744 Effect.gen(function* () {
16301745 const tmp = yield* makeTmpDir();
16311746 yield* initRepoWithCommit(tmp);
1632− const core = yield* GitCore;
1747+ const core = yield* GitVcsDriver.GitVcsDriver;
16331748
16341749 yield* core.createBranch({ cwd: tmp, branch: "feature/no-upstream-ahead" });
16351750 yield* core.checkoutBranch({ cwd: tmp, branch: "feature/no-upstream-ahead" });
@@ -1654,7 +1769,7 @@ it.layer(TestLayer)("git integration", (it) => { · 18 unmodified lines
16541769 yield* git(remote, ["init", "--bare"]);
16551770
16561771 yield* initRepoWithCommit(source);
1657− const initialBranch = (yield* (yield* GitCore).listBranches({
1772+ const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
16581773 cwd: source,
16591774 })).branches.find((branch) => branch.current)!.name;
16601775 yield* git(source, ["remote", "add", "origin", remote]);
@@ -1668,7 +1783,7 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
16681783 yield* git(source, ["commit", "-m", "feature commit"]);
16691784 yield* git(source, ["branch", "-D", initialBranch]);
16701785
1671− const core = yield* GitCore;
1786+ const core = yield* GitVcsDriver.GitVcsDriver;
16721787 const details = yield* core.statusDetails(source);
16731788 expect(details.branch).toBe("feature/remote-base-only");
16741789 expect(details.hasUpstream).toBe(false);
@@ -1687,7 +1802,7 @@ it.layer(TestLayer)("git integration", (it) => { · 12 unmodified lines
16871802 yield* git(remote, ["init", "--bare"]);
16881803
16891804 yield* initRepoWithCommit(source);
1690− const initialBranch = (yield* (yield* GitCore).listBranches({
1805+ const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
16911806 cwd: source,
16921807 })).branches.find((branch) => branch.current)!.name;
16931808 yield* git(source, ["remote", "add", remoteName, remote]);
@@ -1706,7 +1821,7 @@ it.layer(TestLayer)("git integration", (it) => { · 12 unmodified lines
17061821 yield* git(source, ["commit", "-m", "feature commit"]);
17071822 yield* git(source, ["branch", "-D", initialBranch]);
17081823
1709− const core = yield* GitCore;
1824+ const core = yield* GitVcsDriver.GitVcsDriver;
17101825 const details = yield* core.statusDetails(source);
17111826 expect(details.branch).toBe("feature/non-origin-merge-base");
17121827 expect(details.hasUpstream).toBe(false);
@@ -1719,7 +1834,7 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
17191834 Effect.gen(function* () {
17201835 const tmp = yield* makeTmpDir();
17211836 yield* initRepoWithCommit(tmp);
1722− const core = yield* GitCore;
1837+ const core = yield* GitVcsDriver.GitVcsDriver;
17231838
17241839 yield* core.createBranch({ cwd: tmp, branch: "feature/no-upstream-no-ahead" });
17251840 yield* core.checkoutBranch({ cwd: tmp, branch: "feature/no-upstream-no-ahead" });
@@ -1745,7 +1860,7 @@ it.layer(TestLayer)("git integration", (it) => { · 19 unmodified lines
17451860 yield* git(tmp, ["remote", "add", "origin", remote]);
17461861 yield* git(tmp, ["checkout", "-b", "feature/no-base"]);
17471862
1748− const core = yield* GitCore;
1863+ const core = yield* GitVcsDriver.GitVcsDriver;
17491864 const pushed = yield* core.pushCurrentBranch(tmp, null);
17501865 expect(pushed.status).toBe("pushed");
17511866 expect(pushed.setUpstream).toBe(true);
@@ -1770,7 +1885,7 @@ it.layer(TestLayer)("git integration", (it) => { · 18 unmodified lines
17701885 yield* git(tmp, ["remote", "add", "fork", remote]);
17711886 yield* git(tmp, ["checkout", "-b", "feature/fork-only"]);
17721887
1773− const core = yield* GitCore;
1888+ const core = yield* GitVcsDriver.GitVcsDriver;
17741889 const pushed = yield* core.pushCurrentBranch(tmp, null);
17751890 expect(pushed.status).toBe("pushed");
17761891 expect(pushed.setUpstream).toBe(true);
@@ -1790,9 +1905,9 @@ it.layer(TestLayer)("git integration", (it) => { · 13 unmodified lines
17901905 yield* git(remote, ["init", "--bare"]);
17911906
17921907 yield* initRepoWithCommit(tmp);
1793− const initialBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1794− (branch) => branch.current,
1795− )!.name;
1908+ const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1909+ cwd: tmp,
1910+ })).branches.find((branch) => branch.current)!.name;
17961911 yield* git(tmp, ["remote", "add", "origin", remote]);
17971912 yield* git(tmp, ["push", "-u", "origin", initialBranch]);
17981913
@@ -1803,7 +1918,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
18031918 const featureBranch = "feature/publish-no-upstream";
18041919 yield* git(tmp, ["checkout", "-b", featureBranch]);
18051920
1806− const core = yield* GitCore;
1921+ const core = yield* GitVcsDriver.GitVcsDriver;
18071922 const pushed = yield* core.pushCurrentBranch(tmp, null);
18081923 expect(pushed.status).toBe("pushed");
18091924 expect(pushed.setUpstream).toBe(true);
@@ -1826,9 +1941,9 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
18261941 yield* git(fork, ["init", "--bare"]);
18271942
18281943 yield* initRepoWithCommit(tmp);
1829− const initialBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(
1830− (branch) => branch.current,
1831− )!.name;
1944+ const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1945+ cwd: tmp,
1946+ })).branches.find((branch) => branch.current)!.name;
18321947 yield* git(tmp, ["remote", "add", "origin", origin]);
18331948 yield* git(tmp, ["remote", "add", "fork", fork]);
18341949 yield* git(tmp, ["push", "-u", "origin", initialBranch]);
@@ -1840,7 +1955,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
18401955 yield* git(tmp, ["add", "feature.txt"]);
18411956 yield* git(tmp, ["commit", "-m", "feature commit"]);
18421957
1843− const core = yield* GitCore;
1958+ const core = yield* GitVcsDriver.GitVcsDriver;
18441959 const pushed = yield* core.pushCurrentBranch(tmp, null);
18451960 expect(pushed.status).toBe("pushed");
18461961 expect(pushed.setUpstream).toBe(true);
@@ -1883,7 +1998,7 @@ it.layer(TestLayer)("git integration", (it) => { · 36 unmodified lines
18831998 yield* git(tmp, ["add", "fork.txt"]);
18841999 yield* git(tmp, ["commit", "-m", "update reviewed PR branch"]);
18852000
1886− const core = yield* GitCore;
2001+ const core = yield* GitVcsDriver.GitVcsDriver;
18872002 const pushed = yield* core.pushCurrentBranch(tmp, null);
18882003
18892004 expect(pushed.status).toBe("pushed");
@@ -1928,7 +2043,7 @@ it.layer(TestLayer)("git integration", (it) => { · 38 unmodified lines
19282043 yield* git(tmp, ["add", "feature.txt"]);
19292044 yield* git(tmp, ["commit", "-m", "feature update"]);
19302045
1931− const core = yield* GitCore;
2046+ const core = yield* GitVcsDriver.GitVcsDriver;
19322047 const pushed = yield* core.pushCurrentBranch(tmp, null);
19332048 expect(pushed.status).toBe("pushed");
19342049 expect(pushed.setUpstream).toBe(false);
@@ -1946,7 +2061,7 @@ it.layer(TestLayer)("git integration", (it) => { · 11 unmodified lines
19462061 Effect.gen(function* () {
19472062 const tmp = yield* makeTmpDir();
19482063 yield* initRepoWithCommit(tmp);
1949− const core = yield* GitCore;
2064+ const core = yield* GitVcsDriver.GitVcsDriver;
19502065 const missingWorktreePath = path.join(tmp, "missing-worktree");
19512066
19522067 const removeResult = yield* Effect.result(
@@ -1973,7 +2088,7 @@ it.layer(TestLayer)("git integration", (it) => { · 20 unmodified lines
19732088 yield* git(remote, ["init", "--bare"]);
19742089
19752090 yield* initRepoWithCommit(source);
1976− const initialBranch = (yield* (yield* GitCore).listBranches({
2091+ const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
19772092 cwd: source,
19782093 })).branches.find((branch) => branch.current)!.name;
19792094 yield* git(source, ["remote", "add", "origin", remote]);
@@ -1994,7 +2109,7 @@ it.layer(TestLayer)("git integration", (it) => { · 14 unmodified lines
19942109 yield* git(clone, ["commit", "-m", "remote update"]);
19952110 yield* git(clone, ["push", "origin", initialBranch]);
19962111
1997− const core = yield* GitCore;
2112+ const core = yield* GitVcsDriver.GitVcsDriver;
19982113 const details = yield* core.statusDetails(source);
19992114 expect(details.branch).toBe(initialBranch);
20002115 expect(details.aheadCount).toBe(0);
@@ -2006,7 +2121,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
20062121 Effect.gen(function* () {
20072122 const tmp = yield* makeTmpDir();
20082123 yield* initRepoWithCommit(tmp);
2009− const core = yield* GitCore;
2124+ const core = yield* GitVcsDriver.GitVcsDriver;
20102125
20112126 yield* writeTextFile(path.join(tmp, "README.md"), "new content\n");
20122127 const context = yield* core.prepareCommitContext(tmp);
@@ -2024,7 +2139,7 @@ it.layer(TestLayer)("git integration", (it) => { · 11 unmodified lines
20242139 Effect.gen(function* () {
20252140 const tmp = yield* makeTmpDir();
20262141 yield* initRepoWithCommit(tmp);
2027− const core = yield* GitCore;
2142+ const core = yield* GitVcsDriver.GitVcsDriver;
20282143
20292144 yield* writeTextFile(path.join(tmp, "a.txt"), "file a\n");
20302145 yield* writeTextFile(path.join(tmp, "b.txt"), "file b\n");
@@ -2047,7 +2162,7 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
20472162 Effect.gen(function* () {
20482163 const tmp = yield* makeTmpDir();
20492164 yield* initRepoWithCommit(tmp);
2050− const core = yield* GitCore;
2165+ const core = yield* GitVcsDriver.GitVcsDriver;
20512166
20522167 yield* writeTextFile(path.join(tmp, "a.txt"), "file a\n");
20532168 yield* writeTextFile(path.join(tmp, "b.txt"), "file b\n");
@@ -2063,7 +2178,7 @@ it.layer(TestLayer)("git integration", (it) => { · 9 unmodified lines
20632178 Effect.gen(function* () {
20642179 const tmp = yield* makeTmpDir();
20652180 yield* initRepoWithCommit(tmp);
2066− const core = yield* GitCore;
2181+ const core = yield* GitVcsDriver.GitVcsDriver;
20672182
20682183 yield* writeTextFile(path.join(tmp, "README.md"), buildLargeText());
20692184
@@ -2078,7 +2193,7 @@ it.layer(TestLayer)("git integration", (it) => { · 8 unmodified lines
20782193 Effect.gen(function* () {
20792194 const tmp = yield* makeTmpDir();
20802195 const { initialBranch } = yield* initRepoWithCommit(tmp);
2081− const core = yield* GitCore;
2196+ const core = yield* GitVcsDriver.GitVcsDriver;
20822197
20832198 yield* core.createBranch({ cwd: tmp, branch: "feature/large-range-context" });
20842199 yield* core.checkoutBranch({ cwd: tmp, branch: "feature/large-range-context" });
@@ -2100,11 +2215,17 @@ it.layer(TestLayer)("git integration", (it) => { · 15 unmodified lines
21002215 yield* initRepoWithCommit(tmp);
21012216 yield* git(remote, ["init", "--bare"]);
21022217 yield* git(tmp, ["remote", "add", "origin", remote]);
2103− yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature/core-push" });
2104− yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "feature/core-push" });
2218+ yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
2219+ cwd: tmp,
2220+ branch: "feature/core-push",
2221+ });
2222+ yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
2223+ cwd: tmp,
2224+ branch: "feature/core-push",
2225+ });
21052226
21062227 yield* writeTextFile(path.join(tmp, "feature.txt"), "push me\n");
2107− const core = yield* GitCore;
2228+ const core = yield* GitVcsDriver.GitVcsDriver;
21082229 const context = yield* core.prepareCommitContext(tmp);
21092230 expect(context).not.toBeNull();
21102231 yield* core.commit(tmp, "Add feature file", "");
@@ -2129,9 +2250,9 @@ it.layer(TestLayer)("git integration", (it) => { · 18 unmodified lines
21292250 yield* git(remote, ["init", "--bare"]);
21302251
21312252 yield* initRepoWithCommit(source);
2132− const initialBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(
2133− (branch) => branch.current,
2134− )!.name;
2253+ const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
2254+ cwd: source,
2255+ })).branches.find((branch) => branch.current)!.name;
21352256 yield* git(source, ["remote", "add", "origin", remote]);
21362257 yield* git(source, ["push", "-u", "origin", initialBranch]);
21372258
@@ -2143,7 +2264,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
21432264 yield* git(clone, ["commit", "-m", "remote update"]);
21442265 yield* git(clone, ["push", "origin", initialBranch]);
21452266
2146− const core = yield* GitCore;
2267+ const core = yield* GitVcsDriver.GitVcsDriver;
21472268 const pulled = yield* core.pullCurrentBranch(source);
21482269 expect(pulled.status).toBe("pulled");
21492270 expect((yield* core.statusDetails(source)).behindCount).toBe(0);
@@ -2157,7 +2278,9 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
21572278 Effect.gen(function* () {
21582279 const tmp = yield* makeTmpDir();
21592280 yield* initRepoWithCommit(tmp);
2160− const result = yield* Effect.result((yield* GitCore).pullCurrentBranch(tmp));
2281+ const result = yield* Effect.result(
2282+ (yield* GitVcsDriver.GitVcsDriver).pullCurrentBranch(tmp),
2283+ );
21612284 expect(result._tag).toBe("Failure");
21622285 if (result._tag === "Failure") {
21632286 expect(result.failure.message.toLowerCase()).toContain("no upstream");
@@ -2169,9 +2292,9 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
21692292 Effect.gen(function* () {
21702293 const tmp = yield* makeTmpDir();
21712294 yield* initRepoWithCommit(tmp);
2172− const realGitCore = yield* GitCore;
2295+ const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
21732296 let didFailRecency = false;
2174− const core = yield* makeIsolatedGitCore((input) => {
2297+ const core = yield* makeIsolatedGitVcsDriver((input) => {
21752298 if (!didFailRecency && input.args[0] === "for-each-ref") {
21762299 didFailRecency = true;
21772300 return Effect.fail(
@@ -2183,7 +2306,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
21832306 }),
21842307 );
21852308 }
2186− return realGitCore.execute(input);
2309+ return realGitVcsDriver.execute(input);
21872310 });
21882311
21892312 const result = yield* core.listBranches({ cwd: tmp });
@@ -2203,10 +2326,10 @@ it.layer(TestLayer)("git integration", (it) => { · 13 unmodified lines
22032326 yield* git(remote, ["init", "--bare"]);
22042327 yield* git(tmp, ["remote", "add", "origin", remote]);
22052328
2206− const realGitCore = yield* GitCore;
2329+ const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
22072330 let didFailRemoteBranches = false;
22082331 let didFailRemoteNames = false;
2209− const core = yield* makeIsolatedGitCore((input) => {
2332+ const core = yield* makeIsolatedGitVcsDriver((input) => {
22102333 if (input.args.join(" ") === "branch --no-color --no-column --remotes") {
22112334 didFailRemoteBranches = true;
22122335 return Effect.fail(
@@ -2229,7 +2352,7 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
22292352 }),
22302353 );
22312354 }
2232− return realGitCore.execute(input);
2355+ return realGitVcsDriver.execute(input);
22332356 });
22342357
22352358 const result = yield* core.listBranches({ cwd: tmp });
apps/server/src/vcs/GitVcsDriverCore.tsrenamed from apps/server/src/git/Layers/GitCore.ts+365−353
@@ -5,7 +5,6 @@ import { · 4 unmodified lines
55 Effect,
66 Exit,
77 FileSystem,
8− Layer,
98 Option,
109 Path,
1110 PlatformError,
@@ -20,23 +19,22 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; · 8 unmodified lines
2019
2120 import { GitCommandError, type GitBranch } from "@t3tools/contracts";
2221 import { dedupeRemoteBranchesWithLocalMatches } from "@t3tools/shared/git";
23−import { compactTraceAttributes } from "../../observability/Attributes.ts";
24−import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../../observability/Metrics.ts";
25−import {
26− GitCore,
27− type ExecuteGitProgress,
28− type GitCommitOptions,
29− type GitCoreShape,
30− type GitStatusDetails,
31− type ExecuteGitInput,
32− type ExecuteGitResult,
33−} from "../Services/GitCore.ts";
22+import { compactTraceAttributes } from "../observability/Attributes.ts";
23+import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../observability/Metrics.ts";
24+import type {
25+ ExecuteGitProgress,
26+ GitCommitOptions,
27+ GitVcsDriverShape,
28+ GitStatusDetails,
29+ ExecuteGitInput,
30+ ExecuteGitResult,
31+} from "./GitVcsDriver.ts";
3432 import {
3533 parseRemoteNames,
3634 parseRemoteNamesInGitOrder,
3735 parseRemoteRefWithRemoteNames,
38−} from "../remoteRefs.ts";
39−import { ServerConfig } from "../../config.ts";
36+} from "../git/remoteRefs.ts";
37+import { ServerConfig } from "../config.ts";
4038 import { decodeJsonResult } from "@t3tools/shared/schemaJson";
4139
4240 const DEFAULT_TIMEOUT_MS = 30_000;
@@ -411,7 +409,7 @@ const createTrace2Monitor = Effect.fn("createTrace2Monitor")(function* ( · 368 unmodified lines
411409 const traceRecord = decodeJsonResult(Trace2Record)(trimmedLine);
412410 if (Result.isFailure(traceRecord)) {
413411 yield* Effect.logDebug(
414− `GitCore.trace2: failed to parse trace line for ${quoteGitCommand(input.args)} in ${input.cwd}`,
412+ `GitVcsDriver.trace2: failed to parse trace line for ${quoteGitCommand(input.args)} in ${input.cwd}`,
415413 traceRecord.failure,
416414 );
417415 return;
@@ -604,14 +602,14 @@ const collectOutput = Effect.fn("collectOutput")(function* <E>( · 186 unmodified lines
604602 };
605603 });
606604
607−export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
608− executeOverride?: GitCoreShape["execute"];
605+export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* (options?: {
606+ executeOverride?: GitVcsDriverShape["execute"];
609607 }) {
610608 const fileSystem = yield* FileSystem.FileSystem;
611609 const path = yield* Path.Path;
612610 const { worktreesDir } = yield* ServerConfig;
613611
614− let executeRaw: GitCoreShape["execute"];
612+ let executeRaw: GitVcsDriverShape["execute"];
615613
616614 if (options?.executeOverride) {
617615 executeRaw = options.executeOverride;
@@ -718,7 +716,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 100 unmodified lines
718716 });
719717 }
720718
721− const execute: GitCoreShape["execute"] = (input) =>
719+ const execute: GitVcsDriverShape["execute"] = (input) =>
722720 executeRaw(input).pipe(
723721 withMetrics({
724722 counter: gitCommandsTotal,
@@ -812,7 +810,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 87 unmodified lines
812810
813811 const branchExists = (cwd: string, branch: string): Effect.Effect<boolean, GitCommandError> =>
814812 executeGit(
815− "GitCore.branchExists",
813+ "GitVcsDriver.branchExists",
816814 cwd,
817815 ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],
818816 {
@@ -839,7 +837,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 20 unmodified lines
839837 }
840838
841839 return yield* createGitCommandError(
842− "GitCore.renameBranch",
840+ "GitVcsDriver.renameBranch",
843841 cwd,
844842 ["branch", "-m", "--", desiredBranch],
845843 `Could not find an available branch name for '${desiredBranch}'.`,
@@ -848,7 +846,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
848846
849847 const resolveCurrentUpstream = Effect.fn("resolveCurrentUpstream")(function* (cwd: string) {
850848 const upstreamRef = yield* runGitStdout(
851− "GitCore.resolveCurrentUpstream",
849+ "GitVcsDriver.resolveCurrentUpstream",
852850 cwd,
853851 ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
854852 true,
@@ -858,7 +856,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 3 unmodified lines
858856 return null;
859857 }
860858
861− const remoteNames = yield* runGitStdout("GitCore.listRemoteNames", cwd, ["remote"]).pipe(
859+ const remoteNames = yield* runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe(
862860 Effect.map(parseRemoteNames),
863861 Effect.catch(() => Effect.succeed<ReadonlyArray<string>>([])),
864862 );
@@ -875,7 +873,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 10 unmodified lines
875873 const fetchCwd =
876874 path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
877875 return executeGit(
878− "GitCore.fetchRemoteForStatus",
876+ "GitVcsDriver.fetchRemoteForStatus",
879877 fetchCwd,
880878 ["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", remoteName],
881879 {
@@ -886,7 +884,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 4 unmodified lines
886884 };
887885
888886 const resolveGitCommonDir = Effect.fn("resolveGitCommonDir")(function* (cwd: string) {
889− const gitCommonDir = yield* runGitStdout("GitCore.resolveGitCommonDir", cwd, [
887+ const gitCommonDir = yield* runGitStdout("GitVcsDriver.resolveGitCommonDir", cwd, [
890888 "rev-parse",
891889 "--git-common-dir",
892890 ]).pipe(Effect.map((stdout) => stdout.trim()));
@@ -929,7 +927,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 36 unmodified lines
929927 remoteName: string,
930928 ): Effect.Effect<string | null, GitCommandError> =>
931929 executeGit(
932− "GitCore.resolveDefaultBranchName",
930+ "GitVcsDriver.resolveDefaultBranchName",
933931 cwd,
934932 ["symbolic-ref", `refs/remotes/${remoteName}/HEAD`],
935933 { allowNonZeroExit: true },
@@ -948,7 +946,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 12 unmodified lines
948946 branch: string,
949947 ): Effect.Effect<boolean, GitCommandError> =>
950948 executeGit(
951− "GitCore.remoteBranchExists",
949+ "GitVcsDriver.remoteBranchExists",
952950 cwd,
953951 ["show-ref", "--verify", "--quiet", `refs/remotes/${remoteName}/${branch}`],
954952 {
@@ -957,12 +955,12 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
957955 ).pipe(Effect.map((result) => result.code === 0));
958956
959957 const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> =>
960− executeGit("GitCore.originRemoteExists", cwd, ["remote", "get-url", "origin"], {
958+ executeGit("GitVcsDriver.originRemoteExists", cwd, ["remote", "get-url", "origin"], {
961959 allowNonZeroExit: true,
962960 }).pipe(Effect.map((result) => result.code === 0));
963961
964962 const listRemoteNames = (cwd: string): Effect.Effect<ReadonlyArray<string>, GitCommandError> =>
965− runGitStdout("GitCore.listRemoteNames", cwd, ["remote"]).pipe(
963+ runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe(
966964 Effect.map(parseRemoteNamesInGitOrder),
967965 );
968966
@@ -976,7 +974,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 7 unmodified lines
976974 return firstRemote;
977975 }
978976 return yield* createGitCommandError(
979− "GitCore.resolvePrimaryRemoteName",
977+ "GitVcsDriver.resolvePrimaryRemoteName",
980978 cwd,
981979 ["remote"],
982980 "No git remote is configured for this repository.",
@@ -988,7 +986,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 5 unmodified lines
988986 branch: string,
989987 ) {
990988 const branchPushRemote = yield* runGitStdout(
991− "GitCore.resolvePushRemoteName.branchPushRemote",
989+ "GitVcsDriver.resolvePushRemoteName.branchPushRemote",
992990 cwd,
993991 ["config", "--get", `branch.${branch}.pushRemote`],
994992 true,
@@ -998,7 +996,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 3 unmodified lines
998996 }
999997
1000998 const pushDefaultRemote = yield* runGitStdout(
1001− "GitCore.resolvePushRemoteName.remotePushDefault",
999+ "GitVcsDriver.resolvePushRemoteName.remotePushDefault",
10021000 cwd,
10031001 ["config", "--get", "remote.pushDefault"],
10041002 true,
@@ -1010,37 +1008,45 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 5 unmodified lines
10101008 return yield* resolvePrimaryRemoteName(cwd).pipe(Effect.catch(() => Effect.succeed(null)));
10111009 });
10121010
1013− const ensureRemote: GitCoreShape["ensureRemote"] = Effect.fn("ensureRemote")(function* (input) {
1014− const preferredName = sanitizeRemoteName(input.preferredName);
1015− const normalizedTargetUrl = normalizeRemoteUrl(input.url);
1016− const remoteFetchUrls = yield* runGitStdout("GitCore.ensureRemote.listRemoteUrls", input.cwd, [
1017− "remote",
1018− "-v",
1019− ]).pipe(Effect.map((stdout) => parseRemoteFetchUrls(stdout)));
1020−
1021− for (const [remoteName, remoteUrl] of remoteFetchUrls.entries()) {
1022− if (normalizeRemoteUrl(remoteUrl) === normalizedTargetUrl) {
1023− return remoteName;
1011+ const ensureRemote: GitVcsDriverShape["ensureRemote"] = Effect.fn("ensureRemote")(
1012+ function* (input) {
1013+ const preferredName = sanitizeRemoteName(input.preferredName);
1014+ const normalizedTargetUrl = normalizeRemoteUrl(input.url);
1015+ const remoteFetchUrls = yield* runGitStdout(
1016+ "GitVcsDriver.ensureRemote.listRemoteUrls",
1017+ input.cwd,
1018+ ["remote", "-v"],
1019+ ).pipe(Effect.map((stdout) => parseRemoteFetchUrls(stdout)));
1020+
1021+ for (const [remoteName, remoteUrl] of remoteFetchUrls.entries()) {
1022+ if (normalizeRemoteUrl(remoteUrl) === normalizedTargetUrl) {
1023+ return remoteName;
1024+ }
10241025 }
1025− }
10261026
1027− let remoteName = preferredName;
1028− let suffix = 1;
1029− while (remoteFetchUrls.has(remoteName)) {
1030− remoteName = `${preferredName}-${suffix}`;
1031− suffix += 1;
1032− }
1027+ let remoteName = preferredName;
1028+ let suffix = 1;
1029+ while (remoteFetchUrls.has(remoteName)) {
1030+ remoteName = `${preferredName}-${suffix}`;
1031+ suffix += 1;
1032+ }
10331033
1034− yield* runGit("GitCore.ensureRemote.add", input.cwd, ["remote", "add", remoteName, input.url]);
1035− return remoteName;
1036− });
1034+ yield* runGit("GitVcsDriver.ensureRemote.add", input.cwd, [
1035+ "remote",
1036+ "add",
1037+ remoteName,
1038+ input.url,
1039+ ]);
1040+ return remoteName;
1041+ },
1042+ );
10371043
10381044 const resolveBaseBranchForNoUpstream = Effect.fn("resolveBaseBranchForNoUpstream")(function* (
10391045 cwd: string,
10401046 branch: string,
10411047 ) {
10421048 const configuredBaseBranch = yield* runGitStdout(
1043− "GitCore.resolveBaseBranchForNoUpstream.config",
1049+ "GitVcsDriver.resolveBaseBranchForNoUpstream.config",
10441050 cwd,
10451051 ["config", "--get", `branch.${branch}.gh-merge-base`],
10461052 true,
@@ -1098,7 +1104,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 51 unmodified lines
10981104 }
10991105
11001106 const result = yield* executeGit(
1101− "GitCore.computeAheadCountAgainstBase",
1107+ "GitVcsDriver.computeAheadCountAgainstBase",
11021108 cwd,
11031109 ["rev-list", "--count", `${baseBranch}..HEAD`],
11041110 { allowNonZeroExit: true },
@@ -1113,7 +1119,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 8 unmodified lines
11131119
11141120 const readBranchRecency = Effect.fn("readBranchRecency")(function* (cwd: string) {
11151121 const branchRecency = yield* executeGit(
1116− "GitCore.readBranchRecency",
1122+ "GitVcsDriver.readBranchRecency",
11171123 cwd,
11181124 [
11191125 "for-each-ref",
@@ -1149,7 +1155,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 29 unmodified lines
11491155
11501156 const readStatusDetailsLocal = Effect.fn("readStatusDetailsLocal")(function* (cwd: string) {
11511157 const statusResult = yield* executeGit(
1152− "GitCore.statusDetails.status",
1158+ "GitVcsDriver.statusDetails.status",
11531159 cwd,
11541160 ["status", "--porcelain=2", "--branch"],
11551161 {
@@ -1164,7 +1170,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 8 unmodified lines
11641170 if (statusResult.code !== 0) {
11651171 const stderr = statusResult.stderr.trim();
11661172 return yield* createGitCommandError(
1167− "GitCore.statusDetails.status",
1173+ "GitVcsDriver.statusDetails.status",
11681174 cwd,
11691175 ["status", "--porcelain=2", "--branch"],
11701176 stderr || "git status failed",
@@ -1174,14 +1180,14 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 3 unmodified lines
11741180 const [unstagedNumstatStdout, stagedNumstatStdout, defaultRefResult, hasOriginRemote] =
11751181 yield* Effect.all(
11761182 [
1177− runGitStdout("GitCore.statusDetails.unstagedNumstat", cwd, ["diff", "--numstat"]),
1178− runGitStdout("GitCore.statusDetails.stagedNumstat", cwd, [
1183+ runGitStdout("GitVcsDriver.statusDetails.unstagedNumstat", cwd, ["diff", "--numstat"]),
1184+ runGitStdout("GitVcsDriver.statusDetails.stagedNumstat", cwd, [
11791185 "diff",
11801186 "--cached",
11811187 "--numstat",
11821188 ]),
11831189 executeGit(
1184− "GitCore.statusDetails.defaultRef",
1190+ "GitVcsDriver.statusDetails.defaultRef",
11851191 cwd,
11861192 ["symbolic-ref", "refs/remotes/origin/HEAD"],
11871193 {
@@ -1284,21 +1290,23 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 96 unmodified lines
12841290 };
12851291 });
12861292
1287− const statusDetailsLocal: GitCoreShape["statusDetailsLocal"] = Effect.fn("statusDetailsLocal")(
1293+ const statusDetailsLocal: GitVcsDriverShape["statusDetailsLocal"] = Effect.fn(
1294+ "statusDetailsLocal",
1295+ )(function* (cwd) {
1296+ return yield* readStatusDetailsLocal(cwd);
1297+ });
1298+
1299+ const statusDetails: GitVcsDriverShape["statusDetails"] = Effect.fn("statusDetails")(
12881300 function* (cwd) {
1301+ yield* refreshStatusUpstreamIfStale(cwd).pipe(
1302+ Effect.catchIf(isMissingGitCwdError, () => Effect.void),
1303+ Effect.ignoreCause({ log: true }),
1304+ );
12891305 return yield* readStatusDetailsLocal(cwd);
12901306 },
12911307 );
12921308
1293− const statusDetails: GitCoreShape["statusDetails"] = Effect.fn("statusDetails")(function* (cwd) {
1294− yield* refreshStatusUpstreamIfStale(cwd).pipe(
1295− Effect.catchIf(isMissingGitCwdError, () => Effect.void),
1296− Effect.ignoreCause({ log: true }),
1297− );
1298− return yield* readStatusDetailsLocal(cwd);
1299− });
1300−
1301− const status: GitCoreShape["status"] = (input) =>
1309+ const status: GitVcsDriverShape["status"] = (input) =>
13021310 statusDetails(input.cwd).pipe(
13031311 Effect.map((details) => ({
13041312 isRepo: details.isRepo,
@@ -1314,34 +1322,34 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 9 unmodified lines
13141322 })),
13151323 );
13161324
1317− const prepareCommitContext: GitCoreShape["prepareCommitContext"] = Effect.fn(
1325+ const prepareCommitContext: GitVcsDriverShape["prepareCommitContext"] = Effect.fn(
13181326 "prepareCommitContext",
13191327 )(function* (cwd, filePaths) {
13201328 if (filePaths && filePaths.length > 0) {
1321− yield* runGit("GitCore.prepareCommitContext.reset", cwd, ["reset"]).pipe(
1329+ yield* runGit("GitVcsDriver.prepareCommitContext.reset", cwd, ["reset"]).pipe(
13221330 Effect.catch(() => Effect.void),
13231331 );
1324− yield* runGit("GitCore.prepareCommitContext.addSelected", cwd, [
1332+ yield* runGit("GitVcsDriver.prepareCommitContext.addSelected", cwd, [
13251333 "add",
13261334 "-A",
13271335 "--",
13281336 ...filePaths,
13291337 ]);
13301338 } else {
1331− yield* runGit("GitCore.prepareCommitContext.addAll", cwd, ["add", "-A"]);
1339+ yield* runGit("GitVcsDriver.prepareCommitContext.addAll", cwd, ["add", "-A"]);
13321340 }
13331341
1334− const stagedSummary = yield* runGitStdout("GitCore.prepareCommitContext.stagedSummary", cwd, [
1335− "diff",
1336− "--cached",
1337− "--name-status",
1338− ]).pipe(Effect.map((stdout) => stdout.trim()));
1342+ const stagedSummary = yield* runGitStdout(
1343+ "GitVcsDriver.prepareCommitContext.stagedSummary",
1344+ cwd,
1345+ ["diff", "--cached", "--name-status"],
1346+ ).pipe(Effect.map((stdout) => stdout.trim()));
13391347 if (stagedSummary.length === 0) {
13401348 return null;
13411349 }
13421350
13431351 const stagedPatch = yield* runGitStdoutWithOptions(
1344− "GitCore.prepareCommitContext.stagedPatch",
1352+ "GitVcsDriver.prepareCommitContext.stagedPatch",
13451353 cwd,
13461354 ["diff", "--cached", "--patch", "--minimal"],
13471355 {
@@ -1356,7 +1364,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 8 unmodified lines
13561364 };
13571365 });
13581366
1359− const commit: GitCoreShape["commit"] = Effect.fn("commit")(function* (
1367+ const commit: GitVcsDriverShape["commit"] = Effect.fn("commit")(function* (
13601368 cwd,
13611369 subject,
13621370 body,
@@ -1377,11 +1385,11 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 14 unmodified lines
13771385 onStderrLine: (line: string) =>
13781386 options.progress?.onOutputLine?.({ stream: "stderr", text: line }) ?? Effect.void,
13791387 };
1380− yield* executeGit("GitCore.commit.commit", cwd, args, {
1388+ yield* executeGit("GitVcsDriver.commit.commit", cwd, args, {
13811389 ...(options?.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
13821390 ...(progress ? { progress } : {}),
13831391 }).pipe(Effect.asVoid);
1384− const commitSha = yield* runGitStdout("GitCore.commit.revParseHead", cwd, [
1392+ const commitSha = yield* runGitStdout("GitVcsDriver.commit.revParseHead", cwd, [
13851393 "rev-parse",
13861394 "HEAD",
13871395 ]).pipe(Effect.map((stdout) => stdout.trim()));
@@ -1389,13 +1397,13 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 1 unmodified line
13891397 return { commitSha };
13901398 });
13911399
1392− const pushCurrentBranch: GitCoreShape["pushCurrentBranch"] = Effect.fn("pushCurrentBranch")(
1400+ const pushCurrentBranch: GitVcsDriverShape["pushCurrentBranch"] = Effect.fn("pushCurrentBranch")(
13931401 function* (cwd, fallbackBranch) {
13941402 const details = yield* statusDetails(cwd);
13951403 const branch = details.branch ?? fallbackBranch;
13961404 if (!branch) {
13971405 return yield* createGitCommandError(
1398− "GitCore.pushCurrentBranch",
1406+ "GitVcsDriver.pushCurrentBranch",
13991407 cwd,
14001408 ["push"],
14011409 "Cannot push from detached HEAD.",
@@ -1442,13 +1450,13 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 40 unmodified lines
14421450 const publishRemoteName = yield* resolvePushRemoteName(cwd, branch);
14431451 if (!publishRemoteName) {
14441452 return yield* createGitCommandError(
1445− "GitCore.pushCurrentBranch",
1453+ "GitVcsDriver.pushCurrentBranch",
14461454 cwd,
14471455 ["push"],
14481456 "Cannot push because no git remote is configured for this repository.",
14491457 );
14501458 }
1451− yield* runGit("GitCore.pushCurrentBranch.pushWithUpstream", cwd, [
1459+ yield* runGit("GitVcsDriver.pushCurrentBranch.pushWithUpstream", cwd, [
14521460 "push",
14531461 "-u",
14541462 publishRemoteName,
@@ -1466,7 +1474,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 11 unmodified lines
14661474 Effect.catch(() => Effect.succeed(null)),
14671475 );
14681476 if (currentUpstream) {
1469− yield* runGit("GitCore.pushCurrentBranch.pushUpstream", cwd, [
1477+ yield* runGit("GitVcsDriver.pushCurrentBranch.pushUpstream", cwd, [
14701478 "push",
14711479 currentUpstream.remoteName,
14721480 `HEAD:${currentUpstream.upstreamBranch}`,
@@ -1479,7 +1487,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 6 unmodified lines
14791487 };
14801488 }
14811489
1482− yield* runGit("GitCore.pushCurrentBranch.push", cwd, ["push"]);
1490+ yield* runGit("GitVcsDriver.pushCurrentBranch.push", cwd, ["push"]);
14831491 return {
14841492 status: "pushed" as const,
14851493 branch,
@@ -1489,13 +1497,13 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 3 unmodified lines
14891497 },
14901498 );
14911499
1492− const pullCurrentBranch: GitCoreShape["pullCurrentBranch"] = Effect.fn("pullCurrentBranch")(
1500+ const pullCurrentBranch: GitVcsDriverShape["pullCurrentBranch"] = Effect.fn("pullCurrentBranch")(
14931501 function* (cwd) {
14941502 const details = yield* statusDetails(cwd);
14951503 const branch = details.branch;
14961504 if (!branch) {
14971505 return yield* createGitCommandError(
1498− "GitCore.pullCurrentBranch",
1506+ "GitVcsDriver.pullCurrentBranch",
14991507 cwd,
15001508 ["pull", "--ff-only"],
15011509 "Cannot pull from detached HEAD.",
@@ -1503,24 +1511,24 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 1 unmodified line
15031511 }
15041512 if (!details.hasUpstream) {
15051513 return yield* createGitCommandError(
1506− "GitCore.pullCurrentBranch",
1514+ "GitVcsDriver.pullCurrentBranch",
15071515 cwd,
15081516 ["pull", "--ff-only"],
15091517 "Current branch has no upstream configured. Push with upstream first.",
15101518 );
15111519 }
15121520 const beforeSha = yield* runGitStdout(
1513− "GitCore.pullCurrentBranch.beforeSha",
1521+ "GitVcsDriver.pullCurrentBranch.beforeSha",
15141522 cwd,
15151523 ["rev-parse", "HEAD"],
15161524 true,
15171525 ).pipe(Effect.map((stdout) => stdout.trim()));
1518− yield* executeGit("GitCore.pullCurrentBranch.pull", cwd, ["pull", "--ff-only"], {
1526+ yield* executeGit("GitVcsDriver.pullCurrentBranch.pull", cwd, ["pull", "--ff-only"], {
15191527 timeoutMs: 30_000,
15201528 fallbackErrorMessage: "git pull failed",
15211529 });
15221530 const afterSha = yield* runGitStdout(
1523− "GitCore.pullCurrentBranch.afterSha",
1531+ "GitVcsDriver.pullCurrentBranch.afterSha",
15241532 cwd,
15251533 ["rev-parse", "HEAD"],
15261534 true,
@@ -1535,13 +1543,13 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 8 unmodified lines
15351543 },
15361544 );
15371545
1538− const readRangeContext: GitCoreShape["readRangeContext"] = Effect.fn("readRangeContext")(
1546+ const readRangeContext: GitVcsDriverShape["readRangeContext"] = Effect.fn("readRangeContext")(
15391547 function* (cwd, baseBranch) {
15401548 const range = `${baseBranch}..HEAD`;
15411549 const [commitSummary, diffSummary, diffPatch] = yield* Effect.all(
15421550 [
15431551 runGitStdoutWithOptions(
1544− "GitCore.readRangeContext.log",
1552+ "GitVcsDriver.readRangeContext.log",
15451553 cwd,
15461554 ["log", "--oneline", range],
15471555 {
@@ -1550,7 +1558,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
15501558 },
15511559 ),
15521560 runGitStdoutWithOptions(
1553− "GitCore.readRangeContext.diffStat",
1561+ "GitVcsDriver.readRangeContext.diffStat",
15541562 cwd,
15551563 ["diff", "--stat", range],
15561564 {
@@ -1559,7 +1567,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
15591567 },
15601568 ),
15611569 runGitStdoutWithOptions(
1562− "GitCore.readRangeContext.diffPatch",
1570+ "GitVcsDriver.readRangeContext.diffPatch",
15631571 cwd,
15641572 ["diff", "--patch", "--minimal", range],
15651573 {
@@ -1579,228 +1587,230 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 13 unmodified lines
15791587 },
15801588 );
15811589
1582− const readConfigValue: GitCoreShape["readConfigValue"] = (cwd, key) =>
1583− runGitStdout("GitCore.readConfigValue", cwd, ["config", "--get", key], true).pipe(
1590+ const readConfigValue: GitVcsDriverShape["readConfigValue"] = (cwd, key) =>
1591+ runGitStdout("GitVcsDriver.readConfigValue", cwd, ["config", "--get", key], true).pipe(
15841592 Effect.map((stdout) => stdout.trim()),
15851593 Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)),
15861594 );
15871595
1588− const listBranches: GitCoreShape["listBranches"] = Effect.fn("listBranches")(function* (input) {
1589− const branchRecencyPromise = readBranchRecency(input.cwd).pipe(
1590− Effect.catch(() => Effect.succeed(new Map<string, number>())),
1591− );
1592− const localBranchResult = yield* executeGit(
1593− "GitCore.listBranches.branchNoColor",
1594− input.cwd,
1595− ["branch", "--no-color", "--no-column"],
1596− {
1597− timeoutMs: 10_000,
1598− allowNonZeroExit: true,
1599− },
1600− ).pipe(
1601− Effect.catchIf(isMissingGitCwdError, () =>
1602− Effect.succeed({
1603− code: 128,
1604− stdout: "",
1605− stderr: "fatal: not a git repository",
1606− stdoutTruncated: false,
1607− stderrTruncated: false,
1608− }),
1609− ),
1610− );
1611−
1612− if (localBranchResult.code !== 0) {
1613− const stderr = localBranchResult.stderr.trim();
1614− if (stderr.toLowerCase().includes("not a git repository")) {
1615− return {
1616− branches: [],
1617− isRepo: false,
1618− hasOriginRemote: false,
1619− nextCursor: null,
1620− totalCount: 0,
1621− };
1622− }
1623− return yield* createGitCommandError(
1624− "GitCore.listBranches",
1596+ const listBranches: GitVcsDriverShape["listBranches"] = Effect.fn("listBranches")(
1597+ function* (input) {
1598+ const branchRecencyPromise = readBranchRecency(input.cwd).pipe(
1599+ Effect.catch(() => Effect.succeed(new Map<string, number>())),
1600+ );
1601+ const localBranchResult = yield* executeGit(
1602+ "GitVcsDriver.listBranches.branchNoColor",
16251603 input.cwd,
16261604 ["branch", "--no-color", "--no-column"],
1627− stderr || "git branch failed",
1605+ {
1606+ timeoutMs: 10_000,
1607+ allowNonZeroExit: true,
1608+ },
1609+ ).pipe(
1610+ Effect.catchIf(isMissingGitCwdError, () =>
1611+ Effect.succeed({
1612+ code: 128,
1613+ stdout: "",
1614+ stderr: "fatal: not a git repository",
1615+ stdoutTruncated: false,
1616+ stderrTruncated: false,
1617+ }),
1618+ ),
16281619 );
1629− }
16301620
1631− const remoteBranchResultEffect = executeGit(
1632− "GitCore.listBranches.remoteBranches",
1633− input.cwd,
1634− ["branch", "--no-color", "--no-column", "--remotes"],
1635− {
1636− timeoutMs: 10_000,
1637− allowNonZeroExit: true,
1638− },
1639− ).pipe(
1640− Effect.catch((error) =>
1641− Effect.logWarning(
1642− `GitCore.listBranches: remote branch lookup failed for ${input.cwd}: ${error.message}. Falling back to an empty remote branch list.`,
1643− ).pipe(Effect.as({ code: 1, stdout: "", stderr: "" })),
1644− ),
1645− );
1646−
1647− const remoteNamesResultEffect = executeGit(
1648− "GitCore.listBranches.remoteNames",
1649− input.cwd,
1650− ["remote"],
1651− {
1652− timeoutMs: 5_000,
1653− allowNonZeroExit: true,
1654− },
1655− ).pipe(
1656− Effect.catch((error) =>
1657− Effect.logWarning(
1658− `GitCore.listBranches: remote name lookup failed for ${input.cwd}: ${error.message}. Falling back to an empty remote name list.`,
1659− ).pipe(Effect.as({ code: 1, stdout: "", stderr: "" })),
1660− ),
1661− );
1621+ if (localBranchResult.code !== 0) {
1622+ const stderr = localBranchResult.stderr.trim();
1623+ if (stderr.toLowerCase().includes("not a git repository")) {
1624+ return {
1625+ branches: [],
1626+ isRepo: false,
1627+ hasOriginRemote: false,
1628+ nextCursor: null,
1629+ totalCount: 0,
1630+ };
1631+ }
1632+ return yield* createGitCommandError(
1633+ "GitVcsDriver.listBranches",
1634+ input.cwd,
1635+ ["branch", "--no-color", "--no-column"],
1636+ stderr || "git branch failed",
1637+ );
1638+ }
16621639
1663− const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =
1664− yield* Effect.all(
1665− [
1666− executeGit(
1667− "GitCore.listBranches.defaultRef",
1668− input.cwd,
1669− ["symbolic-ref", "refs/remotes/origin/HEAD"],
1670− {
1671− timeoutMs: 5_000,
1672− allowNonZeroExit: true,
1673− },
1674− ),
1675− executeGit(
1676− "GitCore.listBranches.worktreeList",
1677− input.cwd,
1678− ["worktree", "list", "--porcelain"],
1679− {
1680− timeoutMs: 5_000,
1681− allowNonZeroExit: true,
1682− },
1683− ),
1684− remoteBranchResultEffect,
1685− remoteNamesResultEffect,
1686− branchRecencyPromise,
1687− ],
1688− { concurrency: "unbounded" },
1640+ const remoteBranchResultEffect = executeGit(
1641+ "GitVcsDriver.listBranches.remoteBranches",
1642+ input.cwd,
1643+ ["branch", "--no-color", "--no-column", "--remotes"],
1644+ {
1645+ timeoutMs: 10_000,
1646+ allowNonZeroExit: true,
1647+ },
1648+ ).pipe(
1649+ Effect.catch((error) =>
1650+ Effect.logWarning(
1651+ `GitVcsDriver.listBranches: remote branch lookup failed for ${input.cwd}: ${error.message}. Falling back to an empty remote branch list.`,
1652+ ).pipe(Effect.as({ code: 1, stdout: "", stderr: "" })),
1653+ ),
16891654 );
16901655
1691− const remoteNames =
1692− remoteNamesResult.code === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
1693− if (remoteBranchResult.code !== 0 && remoteBranchResult.stderr.trim().length > 0) {
1694− yield* Effect.logWarning(
1695− `GitCore.listBranches: remote branch lookup returned code ${remoteBranchResult.code} for ${input.cwd}: ${remoteBranchResult.stderr.trim()}. Falling back to an empty remote branch list.`,
1696− );
1697− }
1698− if (remoteNamesResult.code !== 0 && remoteNamesResult.stderr.trim().length > 0) {
1699− yield* Effect.logWarning(
1700− `GitCore.listBranches: remote name lookup returned code ${remoteNamesResult.code} for ${input.cwd}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
1656+ const remoteNamesResultEffect = executeGit(
1657+ "GitVcsDriver.listBranches.remoteNames",
1658+ input.cwd,
1659+ ["remote"],
1660+ {
1661+ timeoutMs: 5_000,
1662+ allowNonZeroExit: true,
1663+ },
1664+ ).pipe(
1665+ Effect.catch((error) =>
1666+ Effect.logWarning(
1667+ `GitVcsDriver.listBranches: remote name lookup failed for ${input.cwd}: ${error.message}. Falling back to an empty remote name list.`,
1668+ ).pipe(Effect.as({ code: 1, stdout: "", stderr: "" })),
1669+ ),
17011670 );
1702− }
17031671
1704− const defaultBranch =
1705− defaultRef.code === 0
1706− ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "")
1707− : null;
1672+ const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =
1673+ yield* Effect.all(
1674+ [
1675+ executeGit(
1676+ "GitVcsDriver.listBranches.defaultRef",
1677+ input.cwd,
1678+ ["symbolic-ref", "refs/remotes/origin/HEAD"],
1679+ {
1680+ timeoutMs: 5_000,
1681+ allowNonZeroExit: true,
1682+ },
1683+ ),
1684+ executeGit(
1685+ "GitVcsDriver.listBranches.worktreeList",
1686+ input.cwd,
1687+ ["worktree", "list", "--porcelain"],
1688+ {
1689+ timeoutMs: 5_000,
1690+ allowNonZeroExit: true,
1691+ },
1692+ ),
1693+ remoteBranchResultEffect,
1694+ remoteNamesResultEffect,
1695+ branchRecencyPromise,
1696+ ],
1697+ { concurrency: "unbounded" },
1698+ );
17081699
1709− const worktreeMap = new Map<string, string>();
1710− if (worktreeList.code === 0) {
1711− let currentPath: string | null = null;
1712− for (const line of worktreeList.stdout.split("\n")) {
1713− if (line.startsWith("worktree ")) {
1714− const candidatePath = line.slice("worktree ".length);
1715− const exists = yield* fileSystem.stat(candidatePath).pipe(
1716− Effect.map(() => true),
1717− Effect.catch(() => Effect.succeed(false)),
1718− );
1719− currentPath = exists ? candidatePath : null;
1720− } else if (line.startsWith("branch refs/heads/") && currentPath) {
1721− worktreeMap.set(line.slice("branch refs/heads/".length), currentPath);
1722− } else if (line === "") {
1723− currentPath = null;
1700+ const remoteNames =
1701+ remoteNamesResult.code === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
1702+ if (remoteBranchResult.code !== 0 && remoteBranchResult.stderr.trim().length > 0) {
1703+ yield* Effect.logWarning(
1704+ `GitVcsDriver.listBranches: remote branch lookup returned code ${remoteBranchResult.code} for ${input.cwd}: ${remoteBranchResult.stderr.trim()}. Falling back to an empty remote branch list.`,
1705+ );
1706+ }
1707+ if (remoteNamesResult.code !== 0 && remoteNamesResult.stderr.trim().length > 0) {
1708+ yield* Effect.logWarning(
1709+ `GitVcsDriver.listBranches: remote name lookup returned code ${remoteNamesResult.code} for ${input.cwd}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
1710+ );
1711+ }
1712+
1713+ const defaultBranch =
1714+ defaultRef.code === 0
1715+ ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "")
1716+ : null;
1717+
1718+ const worktreeMap = new Map<string, string>();
1719+ if (worktreeList.code === 0) {
1720+ let currentPath: string | null = null;
1721+ for (const line of worktreeList.stdout.split("\n")) {
1722+ if (line.startsWith("worktree ")) {
1723+ const candidatePath = line.slice("worktree ".length);
1724+ const exists = yield* fileSystem.stat(candidatePath).pipe(
1725+ Effect.map(() => true),
1726+ Effect.catch(() => Effect.succeed(false)),
1727+ );
1728+ currentPath = exists ? candidatePath : null;
1729+ } else if (line.startsWith("branch refs/heads/") && currentPath) {
1730+ worktreeMap.set(line.slice("branch refs/heads/".length), currentPath);
1731+ } else if (line === "") {
1732+ currentPath = null;
1733+ }
17241734 }
17251735 }
1726− }
17271736
1728− const localBranches = localBranchResult.stdout
1729− .split("\n")
1730− .map(parseBranchLine)
1731− .filter((branch): branch is { name: string; current: boolean } => branch !== null)
1732− .map((branch) => ({
1733− name: branch.name,
1734− current: branch.current,
1735− isRemote: false,
1736− isDefault: branch.name === defaultBranch,
1737− worktreePath: worktreeMap.get(branch.name) ?? null,
1738− }))
1739− .toSorted((a, b) => {
1740− const aPriority = a.current ? 0 : a.isDefault ? 1 : 2;
1741− const bPriority = b.current ? 0 : b.isDefault ? 1 : 2;
1742− if (aPriority !== bPriority) return aPriority - bPriority;
1743−
1744− const aLastCommit = branchLastCommit.get(a.name) ?? 0;
1745− const bLastCommit = branchLastCommit.get(b.name) ?? 0;
1746− if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
1747− return a.name.localeCompare(b.name);
1748− });
1737+ const localBranches = localBranchResult.stdout
1738+ .split("\n")
1739+ .map(parseBranchLine)
1740+ .filter((branch): branch is { name: string; current: boolean } => branch !== null)
1741+ .map((branch) => ({
1742+ name: branch.name,
1743+ current: branch.current,
1744+ isRemote: false,
1745+ isDefault: branch.name === defaultBranch,
1746+ worktreePath: worktreeMap.get(branch.name) ?? null,
1747+ }))
1748+ .toSorted((a, b) => {
1749+ const aPriority = a.current ? 0 : a.isDefault ? 1 : 2;
1750+ const bPriority = b.current ? 0 : b.isDefault ? 1 : 2;
1751+ if (aPriority !== bPriority) return aPriority - bPriority;
1752+
1753+ const aLastCommit = branchLastCommit.get(a.name) ?? 0;
1754+ const bLastCommit = branchLastCommit.get(b.name) ?? 0;
1755+ if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
1756+ return a.name.localeCompare(b.name);
1757+ });
17491758
1750− const remoteBranches =
1751− remoteBranchResult.code === 0
1752− ? remoteBranchResult.stdout
1753− .split("\n")
1754− .map(parseBranchLine)
1755− .filter((branch): branch is { name: string; current: boolean } => branch !== null)
1756− .map((branch) => {
1757− const parsedRemoteRef = parseRemoteRefWithRemoteNames(branch.name, remoteNames);
1758− const remoteBranch: {
1759− name: string;
1760− current: boolean;
1761− isRemote: boolean;
1762− remoteName?: string;
1763− isDefault: boolean;
1764− worktreePath: string | null;
1765− } = {
1766− name: branch.name,
1767− current: false,
1768− isRemote: true,
1769− isDefault: false,
1770− worktreePath: null,
1771− };
1772− if (parsedRemoteRef) {
1773− remoteBranch.remoteName = parsedRemoteRef.remoteName;
1774− }
1775− return remoteBranch;
1776− })
1777− .toSorted((a, b) => {
1778− const aLastCommit = branchLastCommit.get(a.name) ?? 0;
1779− const bLastCommit = branchLastCommit.get(b.name) ?? 0;
1780− if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
1781− return a.name.localeCompare(b.name);
1782− })
1783− : [];
1784−
1785− const branches = paginateBranches({
1786− branches: filterBranchesForListQuery(
1787− dedupeRemoteBranchesWithLocalMatches([...localBranches, ...remoteBranches]),
1788− input.query,
1789− ),
1790− cursor: input.cursor,
1791− limit: input.limit,
1792− });
1759+ const remoteBranches =
1760+ remoteBranchResult.code === 0
1761+ ? remoteBranchResult.stdout
1762+ .split("\n")
1763+ .map(parseBranchLine)
1764+ .filter((branch): branch is { name: string; current: boolean } => branch !== null)
1765+ .map((branch) => {
1766+ const parsedRemoteRef = parseRemoteRefWithRemoteNames(branch.name, remoteNames);
1767+ const remoteBranch: {
1768+ name: string;
1769+ current: boolean;
1770+ isRemote: boolean;
1771+ remoteName?: string;
1772+ isDefault: boolean;
1773+ worktreePath: string | null;
1774+ } = {
1775+ name: branch.name,
1776+ current: false,
1777+ isRemote: true,
1778+ isDefault: false,
1779+ worktreePath: null,
1780+ };
1781+ if (parsedRemoteRef) {
1782+ remoteBranch.remoteName = parsedRemoteRef.remoteName;
1783+ }
1784+ return remoteBranch;
1785+ })
1786+ .toSorted((a, b) => {
1787+ const aLastCommit = branchLastCommit.get(a.name) ?? 0;
1788+ const bLastCommit = branchLastCommit.get(b.name) ?? 0;
1789+ if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
1790+ return a.name.localeCompare(b.name);
1791+ })
1792+ : [];
1793+
1794+ const branches = paginateBranches({
1795+ branches: filterBranchesForListQuery(
1796+ dedupeRemoteBranchesWithLocalMatches([...localBranches, ...remoteBranches]),
1797+ input.query,
1798+ ),
1799+ cursor: input.cursor,
1800+ limit: input.limit,
1801+ });
17931802
1794− return {
1795− branches: [...branches.branches],
1796− isRepo: true,
1797− hasOriginRemote: remoteNames.includes("origin"),
1798− nextCursor: branches.nextCursor,
1799− totalCount: branches.totalCount,
1800− };
1801− });
1803+ return {
1804+ branches: [...branches.branches],
1805+ isRepo: true,
1806+ hasOriginRemote: remoteNames.includes("origin"),
1807+ nextCursor: branches.nextCursor,
1808+ totalCount: branches.totalCount,
1809+ };
1810+ },
1811+ );
18021812
1803− const createWorktree: GitCoreShape["createWorktree"] = Effect.fn("createWorktree")(
1813+ const createWorktree: GitVcsDriverShape["createWorktree"] = Effect.fn("createWorktree")(
18041814 function* (input) {
18051815 const targetBranch = input.newBranch ?? input.branch;
18061816 const sanitizedBranch = targetBranch.replace(/\//g, "-");
@@ -1810,7 +1820,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 3 unmodified lines
18101820 ? ["worktree", "add", "-b", input.newBranch, worktreePath, input.branch]
18111821 : ["worktree", "add", worktreePath, input.branch];
18121822
1813− yield* executeGit("GitCore.createWorktree", input.cwd, args, {
1823+ yield* executeGit("GitVcsDriver.createWorktree", input.cwd, args, {
18141824 fallbackErrorMessage: "git worktree add failed",
18151825 });
18161826
@@ -1823,12 +1833,12 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 6 unmodified lines
18231833 },
18241834 );
18251835
1826− const fetchPullRequestBranch: GitCoreShape["fetchPullRequestBranch"] = Effect.fn(
1836+ const fetchPullRequestBranch: GitVcsDriverShape["fetchPullRequestBranch"] = Effect.fn(
18271837 "fetchPullRequestBranch",
18281838 )(function* (input) {
18291839 const remoteName = yield* resolvePrimaryRemoteName(input.cwd);
18301840 yield* executeGit(
1831− "GitCore.fetchPullRequestBranch",
1841+ "GitVcsDriver.fetchPullRequestBranch",
18321842 input.cwd,
18331843 [
18341844 "fetch",
@@ -1843,9 +1853,9 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 8 unmodified lines
18431853 );
18441854 });
18451855
1846− const fetchRemoteBranch: GitCoreShape["fetchRemoteBranch"] = Effect.fn("fetchRemoteBranch")(
1856+ const fetchRemoteBranch: GitVcsDriverShape["fetchRemoteBranch"] = Effect.fn("fetchRemoteBranch")(
18471857 function* (input) {
1848− yield* runGit("GitCore.fetchRemoteBranch.fetch", input.cwd, [
1858+ yield* runGit("GitVcsDriver.fetchRemoteBranch.fetch", input.cwd, [
18491859 "fetch",
18501860 "--quiet",
18511861 "--no-tags",
@@ -1856,7 +1866,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 4 unmodified lines
18561866 const localBranchAlreadyExists = yield* branchExists(input.cwd, input.localBranch);
18571867 const targetRef = `${input.remoteName}/${input.remoteBranch}`;
18581868 yield* runGit(
1859− "GitCore.fetchRemoteBranch.materialize",
1869+ "GitVcsDriver.fetchRemoteBranch.materialize",
18601870 input.cwd,
18611871 localBranchAlreadyExists
18621872 ? ["branch", "--force", input.localBranch, targetRef]
@@ -1865,28 +1875,28 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
18651875 },
18661876 );
18671877
1868− const setBranchUpstream: GitCoreShape["setBranchUpstream"] = (input) =>
1869− runGit("GitCore.setBranchUpstream", input.cwd, [
1878+ const setBranchUpstream: GitVcsDriverShape["setBranchUpstream"] = (input) =>
1879+ runGit("GitVcsDriver.setBranchUpstream", input.cwd, [
18701880 "branch",
18711881 "--set-upstream-to",
18721882 `${input.remoteName}/${input.remoteBranch}`,
18731883 input.branch,
18741884 ]);
18751885
1876− const removeWorktree: GitCoreShape["removeWorktree"] = Effect.fn("removeWorktree")(
1886+ const removeWorktree: GitVcsDriverShape["removeWorktree"] = Effect.fn("removeWorktree")(
18771887 function* (input) {
18781888 const args = ["worktree", "remove"];
18791889 if (input.force) {
18801890 args.push("--force");
18811891 }
18821892 args.push(input.path);
1883− yield* executeGit("GitCore.removeWorktree", input.cwd, args, {
1893+ yield* executeGit("GitVcsDriver.removeWorktree", input.cwd, args, {
18841894 timeoutMs: 15_000,
18851895 fallbackErrorMessage: "git worktree remove failed",
18861896 }).pipe(
18871897 Effect.mapError((error) =>
18881898 createGitCommandError(
1889− "GitCore.removeWorktree",
1899+ "GitVcsDriver.removeWorktree",
18901900 input.cwd,
18911901 args,
18921902 `${commandLabel(args)} failed (cwd: ${input.cwd}): ${error.message}`,
@@ -1897,31 +1907,33 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 4 unmodified lines
18971907 },
18981908 );
18991909
1900− const renameBranch: GitCoreShape["renameBranch"] = Effect.fn("renameBranch")(function* (input) {
1901− if (input.oldBranch === input.newBranch) {
1902− return { branch: input.newBranch };
1903− }
1904− const targetBranch = yield* resolveAvailableBranchName(input.cwd, input.newBranch);
1910+ const renameBranch: GitVcsDriverShape["renameBranch"] = Effect.fn("renameBranch")(
1911+ function* (input) {
1912+ if (input.oldBranch === input.newBranch) {
1913+ return { branch: input.newBranch };
1914+ }
1915+ const targetBranch = yield* resolveAvailableBranchName(input.cwd, input.newBranch);
19051916
1906− yield* executeGit(
1907− "GitCore.renameBranch",
1908− input.cwd,
1909− ["branch", "-m", "--", input.oldBranch, targetBranch],
1910− {
1911− timeoutMs: 10_000,
1912− fallbackErrorMessage: "git branch rename failed",
1913− },
1914− );
1917+ yield* executeGit(
1918+ "GitVcsDriver.renameBranch",
1919+ input.cwd,
1920+ ["branch", "-m", "--", input.oldBranch, targetBranch],
1921+ {
1922+ timeoutMs: 10_000,
1923+ fallbackErrorMessage: "git branch rename failed",
1924+ },
1925+ );
19151926
1916− return { branch: targetBranch };
1917− });
1927+ return { branch: targetBranch };
1928+ },
1929+ );
19181930
1919− const checkoutBranch: GitCoreShape["checkoutBranch"] = Effect.fn("checkoutBranch")(
1931+ const checkoutBranch: GitVcsDriverShape["checkoutBranch"] = Effect.fn("checkoutBranch")(
19201932 function* (input) {
19211933 const [localInputExists, remoteExists] = yield* Effect.all(
19221934 [
19231935 executeGit(
1924− "GitCore.checkoutBranch.localInputExists",
1936+ "GitVcsDriver.checkoutBranch.localInputExists",
19251937 input.cwd,
19261938 ["show-ref", "--verify", "--quiet", `refs/heads/${input.branch}`],
19271939 {
@@ -1930,7 +1942,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
19301942 },
19311943 ).pipe(Effect.map((result) => result.code === 0)),
19321944 executeGit(
1933− "GitCore.checkoutBranch.remoteExists",
1945+ "GitVcsDriver.checkoutBranch.remoteExists",
19341946 input.cwd,
19351947 ["show-ref", "--verify", "--quiet", `refs/remotes/${input.branch}`],
19361948 {
@@ -1944,7 +1956,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 7 unmodified lines
19441956
19451957 const localTrackingBranch = remoteExists
19461958 ? yield* executeGit(
1947− "GitCore.checkoutBranch.localTrackingBranch",
1959+ "GitVcsDriver.checkoutBranch.localTrackingBranch",
19481960 input.cwd,
19491961 ["for-each-ref", "--format=%(refname:short)\t%(upstream:short)", "refs/heads"],
19501962 {
@@ -1964,7 +1976,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 13 unmodified lines
19641976 const localTrackedBranchTargetExists =
19651977 remoteExists && localTrackedBranchCandidate
19661978 ? yield* executeGit(
1967− "GitCore.checkoutBranch.localTrackedBranchTargetExists",
1979+ "GitVcsDriver.checkoutBranch.localTrackedBranchTargetExists",
19681980 input.cwd,
19691981 ["show-ref", "--verify", "--quiet", `refs/heads/${localTrackedBranchCandidate}`],
19701982 {
@@ -1984,12 +1996,12 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 13 unmodified lines
19841996 ? ["checkout", localTrackingBranch]
19851997 : ["checkout", input.branch];
19861998
1987− yield* executeGit("GitCore.checkoutBranch.checkout", input.cwd, checkoutArgs, {
1999+ yield* executeGit("GitVcsDriver.checkoutBranch.checkout", input.cwd, checkoutArgs, {
19882000 timeoutMs: 10_000,
19892001 fallbackErrorMessage: "git checkout failed",
19902002 });
19912003
1992− const branch = yield* runGitStdout("GitCore.checkoutBranch.currentBranch", input.cwd, [
2004+ const branch = yield* runGitStdout("GitVcsDriver.checkoutBranch.currentBranch", input.cwd, [
19932005 "branch",
19942006 "--show-current",
19952007 ]).pipe(Effect.map((stdout) => stdout.trim() || null));
@@ -1998,26 +2010,28 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
19982010 },
19992011 );
20002012
2001− const createBranch: GitCoreShape["createBranch"] = Effect.fn("createBranch")(function* (input) {
2002− yield* executeGit("GitCore.createBranch", input.cwd, ["branch", input.branch], {
2003− timeoutMs: 10_000,
2004− fallbackErrorMessage: "git branch create failed",
2005− });
2006− if (input.checkout) {
2007− yield* checkoutBranch({ cwd: input.cwd, branch: input.branch });
2008− }
2013+ const createBranch: GitVcsDriverShape["createBranch"] = Effect.fn("createBranch")(
2014+ function* (input) {
2015+ yield* executeGit("GitVcsDriver.createBranch", input.cwd, ["branch", input.branch], {
2016+ timeoutMs: 10_000,
2017+ fallbackErrorMessage: "git branch create failed",
2018+ });
2019+ if (input.checkout) {
2020+ yield* checkoutBranch({ cwd: input.cwd, branch: input.branch });
2021+ }
20092022
2010− return { branch: input.branch };
2011− });
2023+ return { branch: input.branch };
2024+ },
2025+ );
20122026
2013− const initRepo: GitCoreShape["initRepo"] = (input) =>
2014− executeGit("GitCore.initRepo", input.cwd, ["init"], {
2027+ const initRepo: GitVcsDriverShape["initRepo"] = (input) =>
2028+ executeGit("GitVcsDriver.initRepo", input.cwd, ["init"], {
20152029 timeoutMs: 10_000,
20162030 fallbackErrorMessage: "git init failed",
20172031 }).pipe(Effect.asVoid);
20182032
2019− const listLocalBranchNames: GitCoreShape["listLocalBranchNames"] = (cwd) =>
2020− runGitStdout("GitCore.listLocalBranchNames", cwd, [
2033+ const listLocalBranchNames: GitVcsDriverShape["listLocalBranchNames"] = (cwd) =>
2034+ runGitStdout("GitVcsDriver.listLocalBranchNames", cwd, [
20212035 "branch",
20222036 "--list",
20232037 "--no-column",
@@ -2054,7 +2068,5 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 30 unmodified lines
20542068 checkoutBranch,
20552069 initRepo,
20562070 listLocalBranchNames,
2057− } satisfies GitCoreShape;
2071+ } satisfies GitVcsDriverShape;
20582072 });
2059−
2060−export const GitCoreLive = Layer.effect(GitCore, makeGitCore());