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
38 type GitManagerShape,38 type GitManagerShape,
39 type GitRunStackedActionOptions,39 type GitRunStackedActionOptions,
40} from "../Services/GitManager.ts";40} from "../Services/GitManager.ts";
41import type { GitStatusDetails } from "../Services/GitCore.ts";
42import { GitHubCli, type GitHubPullRequestSummary } from "../Services/GitHubCli.ts";41import { GitHubCli, type GitHubPullRequestSummary } from "../Services/GitHubCli.ts";
43import { TextGeneration } from "../Services/TextGeneration.ts";42import { TextGeneration } from "../Services/TextGeneration.ts";
44import { ProjectSetupScriptRunner } from "../../project/Services/ProjectSetupScriptRunner.ts";43import { ProjectSetupScriptRunner } from "../../project/Services/ProjectSetupScriptRunner.ts";
@@ -49,7 +48,7 @@ import { · 4 unmodified lines
49 decodeGitHubPullRequestListJson,48 decodeGitHubPullRequestListJson,
50 formatGitHubJsonDecodeError,49 formatGitHubJsonDecodeError,
51} from "../githubPullRequests.ts";50} from "../githubPullRequests.ts";
52import { GitVcsDriver } from "../../vcs/GitVcsDriver.ts";51import { GitVcsDriver, type GitStatusDetails } from "../../vcs/GitVcsDriver.ts";
53 52
54const COMMIT_TIMEOUT_MS = 10 * 60_000;53const COMMIT_TIMEOUT_MS = 10 * 60_000;
55const MAX_PROGRESS_TEXT_LENGTH = 500;54const 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 */
9import { Context } from "effect";
10import type { Effect } from "effect";
11import 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
27import type { GitCommandError } from "@t3tools/contracts";
28
29export 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
42export interface ExecuteGitResult {
43 readonly code: number;
44 readonly stdout: string;
45 readonly stderr: string;
46 readonly stdoutTruncated: boolean;
47 readonly stderrTruncated: boolean;
48}
49
50export interface GitStatusDetails extends Omit<GitStatusResult, "pr"> {
51 upstreamRef: string | null;
52}
53
54export interface GitPreparedCommitContext {
55 stagedSummary: string;
56 stagedPatch: string;
57}
58
59export 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
70export 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
83export interface GitCommitOptions {
84 readonly timeoutMs?: number;
85 readonly progress?: GitCommitProgress;
86}
87
88export interface GitPushResult {
89 status: "pushed" | "skipped_up_to_date";
90 branch: string;
91 upstreamBranch?: string | undefined;
92 setUpstream?: boolean | undefined;
93}
94
95export interface GitRangeContext {
96 commitSummary: string;
97 diffSummary: string;
98 diffPatch: string;
99}
100
101export interface GitRenameBranchInput {
102 cwd: string;
103 oldBranch: string;
104 newBranch: string;
105}
106
107export interface GitRenameBranchResult {
108 branch: string;
109}
110
111export interface GitFetchPullRequestBranchInput {
112 cwd: string;
113 prNumber: number;
114 branch: string;
115}
116
117export interface GitEnsureRemoteInput {
118 cwd: string;
119 preferredName: string;
120 url: string;
121}
122
123export interface GitFetchRemoteBranchInput {
124 cwd: string;
125 remoteName: string;
126 remoteBranch: string;
127 localBranch: string;
128}
129
130export 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 */
140export 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 */
288export class GitCore extends Context.Service<GitCore, GitCoreShape>()("t3/git/Services/GitCore") {}
apps/server/src/vcs/GitVcsDriver.ts+185−6
@@ -1,12 +1,191 @@
1import { Context, DateTime, Effect, Layer } from "effect";1import { Context, DateTime, Effect, Layer } from "effect";
2 2
3import { VcsProcessExitError } from "@t3tools/contracts";3import {
4import { makeGitCore } from "../git/Layers/GitCore.ts";4 GitCommandError,
5import type { GitCoreShape } from "../git/Services/GitCore.ts";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";
20import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts";
6import { VcsDriver, type VcsDriverShape } from "./VcsDriver.ts";21import { VcsDriver, type VcsDriverShape } from "./VcsDriver.ts";
7import { VcsProcess, type VcsProcessShape } from "./VcsProcess.ts";22import { VcsProcess, type VcsProcessShape } from "./VcsProcess.ts";
8 23
9export interface GitVcsDriverShape extends GitCoreShape {}24export 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
37export interface ExecuteGitResult {
38 readonly code: number;
39 readonly stdout: string;
40 readonly stderr: string;
41 readonly stdoutTruncated: boolean;
42 readonly stderrTruncated: boolean;
43}
44
45export interface GitStatusDetails extends Omit<GitStatusResult, "pr"> {
46 upstreamRef: string | null;
47}
48
49export interface GitPreparedCommitContext {
50 stagedSummary: string;
51 stagedPatch: string;
52}
53
54export 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
65export 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
78export interface GitCommitOptions {
79 readonly timeoutMs?: number;
80 readonly progress?: GitCommitProgress;
81}
82
83export interface GitPushResult {
84 status: "pushed" | "skipped_up_to_date";
85 branch: string;
86 upstreamBranch?: string | undefined;
87 setUpstream?: boolean | undefined;
88}
89
90export interface GitRangeContext {
91 commitSummary: string;
92 diffSummary: string;
93 diffPatch: string;
94}
95
96export interface GitRenameBranchInput {
97 cwd: string;
98 oldBranch: string;
99 newBranch: string;
100}
101
102export interface GitRenameBranchResult {
103 branch: string;
104}
105
106export interface GitFetchPullRequestBranchInput {
107 cwd: string;
108 prNumber: number;
109 branch: string;
110}
111
112export interface GitEnsureRemoteInput {
113 cwd: string;
114 preferredName: string;
115 url: string;
116}
117
118export interface GitFetchRemoteBranchInput {
119 cwd: string;
120 remoteName: string;
121 remoteBranch: string;
122 localBranch: string;
123}
124
125export interface GitSetBranchUpstreamInput {
126 cwd: string;
127 branch: string;
128 remoteName: string;
129 remoteBranch: string;
130}
131
132export 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}
10 189
11export class GitVcsDriver extends Context.Service<GitVcsDriver, GitVcsDriverShape>()(190export class GitVcsDriver extends Context.Service<GitVcsDriver, GitVcsDriverShape>()(
12 "t3/vcs/GitVcsDriver",191 "t3/vcs/GitVcsDriver",
@@ -259,8 +438,8 @@ export const makeVcsDriver = Effect.fn("makeGitVcsDriver")(function* () { · 246 unmodified lines
259});438});
260 439
261export const make = Effect.fn("makeGitVcsDriverService")(function* () {440export const make = Effect.fn("makeGitVcsDriverService")(function* () {
262 const legacyGit = yield* makeGitCore();441 const git = yield* makeGitVcsDriverCore();
263 return GitVcsDriver.of(legacyGit);442 return GitVcsDriver.of(git);
264});443});
265 444
266export const vcsLayer = Layer.effect(VcsDriver, makeVcsDriver());445export 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
6import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";6import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";
7import { describe, expect, vi } from "vitest";7import { describe, expect, vi } from "vitest";
8 8
9import { GitCoreLive, makeGitCore } from "./GitCore.ts";9import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts";
10import { GitCore, type GitCoreShape } from "../Services/GitCore.ts";10import * as GitVcsDriver from "./GitVcsDriver.ts";
11import { GitCommandError } from "@t3tools/contracts";11import { GitCommandError } from "@t3tools/contracts";
12import { type ProcessRunResult, runProcess } from "../../processRunner.ts";12import { type ProcessRunResult, runProcess } from "../processRunner.ts";
13import { ServerConfig } from "../../config.ts";13import { ServerConfig } from "../config.ts";
14 14
15// ── Helpers ──15// ── Helpers ──
16 16
17const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-core-test-" });17const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-core-test-" });
18const GitCoreTestLayer = GitCoreLive.pipe(18const GitVcsDriverTestLayer = GitVcsDriver.layer.pipe(
19 Layer.provide(ServerConfigLayer),19 Layer.provide(ServerConfigLayer),
20 Layer.provide(NodeServices.layer),20 Layer.provide(NodeServices.layer),
21);21);
22const TestLayer = Layer.mergeAll(NodeServices.layer, GitCoreTestLayer);22const TestLayer = Layer.mergeAll(NodeServices.layer, GitVcsDriverTestLayer);
23 23
24function makeTmpDir(24function makeTmpDir(
25 prefix = "git-test-",25 prefix = "git-test-",
@@ -63,11 +63,11 @@ function git( · 37 unmodified lines
63 cwd: string,63 cwd: string,
64 args: ReadonlyArray<string>,64 args: ReadonlyArray<string>,
65 env?: NodeJS.ProcessEnv,65 env?: NodeJS.ProcessEnv,
66): Effect.Effect<string, GitCommandError, GitCore> {66): Effect.Effect<string, GitCommandError, GitVcsDriver.GitVcsDriver> {
67 return Effect.gen(function* () {67 return Effect.gen(function* () {
68 const gitCore = yield* GitCore;68 const gitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
69 const result = yield* gitCore.execute({69 const result = yield* gitVcsDriver.execute({
70 operation: "GitCore.test.git",70 operation: "GitVcsDriver.test.git",
71 cwd,71 cwd,
72 args,72 args,
73 ...(env ? { env } : {}),73 ...(env ? { env } : {}),
@@ -82,7 +82,7 @@ function configureRemote( · 8 unmodified lines
82 remoteName: string,82 remoteName: string,
83 remotePath: string,83 remotePath: string,
84 fetchNamespace: string,84 fetchNamespace: string,
85): Effect.Effect<string, GitCommandError, GitCore> {85): Effect.Effect<string, GitCommandError, GitVcsDriver.GitVcsDriver> {
86 return Effect.gen(function* () {86 return Effect.gen(function* () {
87 yield* git(cwd, ["config", `remote.${remoteName}.url`, remotePath]);87 yield* git(cwd, ["config", `remote.${remoteName}.url`, remotePath]);
88 return yield* git(cwd, [88 return yield* git(cwd, [
@@ -119,8 +119,8 @@ function runShellCommand(input: { · 30 unmodified lines
119 });119 });
120}120}
121 121
122const makeIsolatedGitCore = (executeOverride: GitCoreShape["execute"]) =>122const makeIsolatedGitVcsDriver = (executeOverride: GitVcsDriver.GitVcsDriverShape["execute"]) =>
123 makeGitCore({ executeOverride }).pipe(123 makeGitVcsDriverCore({ executeOverride }).pipe(
124 Effect.provide(Layer.provideMerge(ServerConfigLayer, NodeServices.layer)),124 Effect.provide(Layer.provideMerge(ServerConfigLayer, NodeServices.layer)),
125 );125 );
126 126
@@ -130,10 +130,10 @@ function initRepoWithCommit( · 3 unmodified lines
130): Effect.Effect<130): Effect.Effect<
131 { initialBranch: string },131 { initialBranch: string },
132 GitCommandError | PlatformError.PlatformError,132 GitCommandError | PlatformError.PlatformError,
133 GitCore | FileSystem.FileSystem133 GitVcsDriver.GitVcsDriver | FileSystem.FileSystem
134> {134> {
135 return Effect.gen(function* () {135 return Effect.gen(function* () {
136 const core = yield* GitCore;136 const core = yield* GitVcsDriver.GitVcsDriver;
137 yield* core.initRepo({ cwd });137 yield* core.initRepo({ cwd });
138 yield* git(cwd, ["config", "user.email", "test@test.com"]);138 yield* git(cwd, ["config", "user.email", "test@test.com"]);
139 yield* git(cwd, ["config", "user.name", "Test"]);139 yield* git(cwd, ["config", "user.name", "Test"]);
@@ -154,7 +154,7 @@ function commitWithDate( · 14 unmodified lines
154): Effect.Effect<154): Effect.Effect<
155 void,155 void,
156 GitCommandError | PlatformError.PlatformError,156 GitCommandError | PlatformError.PlatformError,
157 GitCore | FileSystem.FileSystem157 GitVcsDriver.GitVcsDriver | FileSystem.FileSystem
158> {158> {
159 return Effect.gen(function* () {159 return Effect.gen(function* () {
160 yield* writeTextFile(path.join(cwd, fileName), fileContents);160 yield* writeTextFile(path.join(cwd, fileName), fileContents);
@@ -199,7 +199,7 @@ it.layer(TestLayer)("git integration", (it) => { · 38 unmodified lines
199 it.effect("creates a valid git repo", () =>199 it.effect("creates a valid git repo", () =>
200 Effect.gen(function* () {200 Effect.gen(function* () {
201 const tmp = yield* makeTmpDir();201 const tmp = yield* makeTmpDir();
202 yield* (yield* GitCore).initRepo({ cwd: tmp });202 yield* (yield* GitVcsDriver.GitVcsDriver).initRepo({ cwd: tmp });
203 expect(existsSync(path.join(tmp, ".git"))).toBe(true);203 expect(existsSync(path.join(tmp, ".git"))).toBe(true);
204 }),204 }),
205 );205 );
@@ -208,7 +208,7 @@ it.layer(TestLayer)("git integration", (it) => { · 2 unmodified lines
208 Effect.gen(function* () {208 Effect.gen(function* () {
209 const tmp = yield* makeTmpDir();209 const tmp = yield* makeTmpDir();
210 yield* initRepoWithCommit(tmp);210 yield* initRepoWithCommit(tmp);
211 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });211 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
212 expect(result.isRepo).toBe(true);212 expect(result.isRepo).toBe(true);
213 expect(result.hasOriginRemote).toBe(false);213 expect(result.hasOriginRemote).toBe(false);
214 expect(result.branches.length).toBeGreaterThanOrEqual(1);214 expect(result.branches.length).toBeGreaterThanOrEqual(1);
@@ -222,7 +222,7 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
222 it.effect("returns isRepo: false for non-git directory", () =>222 it.effect("returns isRepo: false for non-git directory", () =>
223 Effect.gen(function* () {223 Effect.gen(function* () {
224 const tmp = yield* makeTmpDir();224 const tmp = yield* makeTmpDir();
225 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });225 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
226 expect(result.isRepo).toBe(false);226 expect(result.isRepo).toBe(false);
227 expect(result.hasOriginRemote).toBe(false);227 expect(result.hasOriginRemote).toBe(false);
228 expect(result.branches).toEqual([]);228 expect(result.branches).toEqual([]);
@@ -236,7 +236,7 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
236 yield* makeDirectory(deletedDir);236 yield* makeDirectory(deletedDir);
237 yield* removePath(deletedDir);237 yield* removePath(deletedDir);
238 238
239 const result = yield* (yield* GitCore).listBranches({ cwd: deletedDir });239 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: deletedDir });
240 240
241 expect(result.isRepo).toBe(false);241 expect(result.isRepo).toBe(false);
242 expect(result.hasOriginRemote).toBe(false);242 expect(result.hasOriginRemote).toBe(false);
@@ -248,7 +248,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
248 Effect.gen(function* () {248 Effect.gen(function* () {
249 const tmp = yield* makeTmpDir();249 const tmp = yield* makeTmpDir();
250 yield* initRepoWithCommit(tmp);250 yield* initRepoWithCommit(tmp);
251 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });251 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
252 const current = result.branches.find((b) => b.current);252 const current = result.branches.find((b) => b.current);
253 expect(current).toBeDefined();253 expect(current).toBeDefined();
254 expect(current!.current).toBe(true);254 expect(current!.current).toBe(true);
@@ -261,7 +261,7 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
261 yield* initRepoWithCommit(tmp);261 yield* initRepoWithCommit(tmp);
262 yield* git(tmp, ["checkout", "--detach", "HEAD"]);262 yield* git(tmp, ["checkout", "--detach", "HEAD"]);
263 263
264 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });264 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
265 expect(result.branches.some((branch) => branch.name.startsWith("("))).toBe(false);265 expect(result.branches.some((branch) => branch.name.startsWith("("))).toBe(false);
266 expect(result.branches.some((branch) => branch.current)).toBe(false);266 expect(result.branches.some((branch) => branch.current)).toBe(false);
267 }),267 }),
@@ -271,12 +271,18 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
271 Effect.gen(function* () {271 Effect.gen(function* () {
272 const tmp = yield* makeTmpDir();272 const tmp = yield* makeTmpDir();
273 yield* initRepoWithCommit(tmp);273 yield* initRepoWithCommit(tmp);
274 const initialBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(274 const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
275 (branch) => branch.current,275 cwd: tmp,
276 )!.name;276 })).branches.find((branch) => branch.current)!.name;
277 277
278 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "older-branch" });278 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
279 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "older-branch" });279 cwd: tmp,
280 branch: "older-branch",
281 });
282 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
283 cwd: tmp,
284 branch: "older-branch",
285 });
280 yield* commitWithDate(286 yield* commitWithDate(
281 tmp,287 tmp,
282 "older.txt",288 "older.txt",
@@ -285,9 +291,18 @@ it.layer(TestLayer)("git integration", (it) => { · 2 unmodified lines
285 "older branch change",291 "older branch change",
286 );292 );
287 293
288 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: initialBranch });294 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
289 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "newer-branch" });295 cwd: tmp,
290 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "newer-branch" });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 });
291 yield* commitWithDate(306 yield* commitWithDate(
292 tmp,307 tmp,
293 "newer.txt",308 "newer.txt",
@@ -297,9 +312,12 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
297 );312 );
298 313
299 // Switch away to show current branch is pinned, then remaining branches are recency-sorted.314 // 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 });
301 319
302 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });320 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
303 expect(result.branches[0]!.name).toBe("older-branch");321 expect(result.branches[0]!.name).toBe("older-branch");
304 expect(result.branches[1]!.name).toBe("newer-branch");322 expect(result.branches[1]!.name).toBe("newer-branch");
305 }),323 }),
@@ -310,17 +328,23 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
310 const tmp = yield* makeTmpDir();328 const tmp = yield* makeTmpDir();
311 const remote = yield* makeTmpDir();329 const remote = yield* makeTmpDir();
312 yield* initRepoWithCommit(tmp);330 yield* initRepoWithCommit(tmp);
313 const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(331 const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
314 (branch) => branch.current,332 cwd: tmp,
315 )!.name;333 })).branches.find((branch) => branch.current)!.name;
316 334
317 yield* git(remote, ["init", "--bare"]);335 yield* git(remote, ["init", "--bare"]);
318 yield* git(tmp, ["remote", "add", "origin", remote]);336 yield* git(tmp, ["remote", "add", "origin", remote]);
319 yield* git(tmp, ["push", "-u", "origin", defaultBranch]);337 yield* git(tmp, ["push", "-u", "origin", defaultBranch]);
320 yield* git(tmp, ["remote", "set-head", "origin", defaultBranch]);338 yield* git(tmp, ["remote", "set-head", "origin", defaultBranch]);
321 339
322 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "current-branch" });340 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
323 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "current-branch" });341 cwd: tmp,
342 branch: "current-branch",
343 });
344 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
345 cwd: tmp,
346 branch: "current-branch",
347 });
324 yield* commitWithDate(348 yield* commitWithDate(
325 tmp,349 tmp,
326 "current.txt",350 "current.txt",
@@ -329,9 +353,18 @@ it.layer(TestLayer)("git integration", (it) => { · 2 unmodified lines
329 "current change",353 "current change",
330 );354 );
331 355
332 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: defaultBranch });356 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
333 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "newer-branch" });357 cwd: tmp,
334 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "newer-branch" });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 });
335 yield* commitWithDate(368 yield* commitWithDate(
336 tmp,369 tmp,
337 "newer.txt",370 "newer.txt",
@@ -340,9 +373,12 @@ it.layer(TestLayer)("git integration", (it) => { · 2 unmodified lines
340 "newer change",373 "newer change",
341 );374 );
342 375
343 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "current-branch" });376 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
377 cwd: tmp,
378 branch: "current-branch",
379 });
344 380
345 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });381 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
346 expect(result.branches[0]!.name).toBe("current-branch");382 expect(result.branches[0]!.name).toBe("current-branch");
347 expect(result.branches[1]!.name).toBe(defaultBranch);383 expect(result.branches[1]!.name).toBe(defaultBranch);
348 expect(result.branches[2]!.name).toBe("newer-branch");384 expect(result.branches[2]!.name).toBe("newer-branch");
@@ -353,10 +389,10 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
353 Effect.gen(function* () {389 Effect.gen(function* () {
354 const tmp = yield* makeTmpDir();390 const tmp = yield* makeTmpDir();
355 yield* initRepoWithCommit(tmp);391 yield* initRepoWithCommit(tmp);
356 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-a" });392 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature-a" });
357 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-b" });393 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature-b" });
358 394
359 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });395 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
360 const names = result.branches.map((b) => b.name);396 const names = result.branches.map((b) => b.name);
361 expect(names).toContain("feature-a");397 expect(names).toContain("feature-a");
362 expect(names).toContain("feature-b");398 expect(names).toContain("feature-b");
@@ -367,11 +403,14 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
367 Effect.gen(function* () {403 Effect.gen(function* () {
368 const tmp = yield* makeTmpDir();404 const tmp = yield* makeTmpDir();
369 const { initialBranch } = yield* initRepoWithCommit(tmp);405 const { initialBranch } = yield* initRepoWithCommit(tmp);
370 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-a" });406 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature-a" });
371 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-b" });407 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature-b" });
372 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-c" });408 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature-c" });
373 409
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 });
375 expect(firstPage.totalCount).toBe(4);414 expect(firstPage.totalCount).toBe(4);
376 expect(firstPage.nextCursor).toBe(2);415 expect(firstPage.nextCursor).toBe(2);
377 expect(firstPage.branches.map((branch) => branch.name)).toEqual([416 expect(firstPage.branches.map((branch) => branch.name)).toEqual([
@@ -379,7 +418,7 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
379 "feature-a",418 "feature-a",
380 ]);419 ]);
381 420
382 const secondPage = yield* (yield* GitCore).listBranches({421 const secondPage = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
383 cwd: tmp,422 cwd: tmp,
384 cursor: firstPage.nextCursor ?? 0,423 cursor: firstPage.nextCursor ?? 0,
385 limit: 2,424 limit: 2,
@@ -403,7 +442,7 @@ it.layer(TestLayer)("git integration", (it) => { · 17 unmodified lines
403 "copilot/rewrite-cli-in-rust",442 "copilot/rewrite-cli-in-rust",
404 ] as const;443 ] as const;
405 for (const branchName of createdBranchNames) {444 for (const branchName of createdBranchNames) {
406 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: branchName });445 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: branchName });
407 }446 }
408 yield* git(tmp, ["config", "column.ui", "always"]);447 yield* git(tmp, ["config", "column.ui", "always"]);
409 448
@@ -420,9 +459,9 @@ it.layer(TestLayer)("git integration", (it) => { · 10 unmodified lines
420 ),459 ),
421 ).toBe(true);460 ).toBe(true);
422 461
423 const realGitCore = yield* GitCore;462 const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
424 const core = yield* makeIsolatedGitCore((input) =>463 const core = yield* makeIsolatedGitVcsDriver((input) =>
425 realGitCore.execute(464 realGitVcsDriver.execute(
426 input.args[0] === "branch"465 input.args[0] === "branch"
427 ? {466 ? {
428 ...input,467 ...input,
@@ -455,7 +494,7 @@ it.layer(TestLayer)("git integration", (it) => { · 26 unmodified lines
455 Effect.gen(function* () {494 Effect.gen(function* () {
456 const tmp = yield* makeTmpDir();495 const tmp = yield* makeTmpDir();
457 yield* initRepoWithCommit(tmp);496 yield* initRepoWithCommit(tmp);
458 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });497 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
459 expect(result.branches.every((b) => b.isDefault === false)).toBe(true);498 expect(result.branches.every((b) => b.isDefault === false)).toBe(true);
460 }),499 }),
461 );500 );
@@ -467,23 +506,29 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
467 506
468 yield* git(remote, ["init", "--bare"]);507 yield* git(remote, ["init", "--bare"]);
469 yield* initRepoWithCommit(tmp);508 yield* initRepoWithCommit(tmp);
470 const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(509 const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
471 (branch) => branch.current,510 cwd: tmp,
472 )!.name;511 })).branches.find((branch) => branch.current)!.name;
473 512
474 yield* git(tmp, ["remote", "add", "origin", remote]);513 yield* git(tmp, ["remote", "add", "origin", remote]);
475 yield* git(tmp, ["push", "-u", "origin", defaultBranch]);514 yield* git(tmp, ["push", "-u", "origin", defaultBranch]);
476 515
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 });
478 520
479 const remoteOnlyBranch = "feature/remote-only";521 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 });
481 yield* git(tmp, ["checkout", "-b", remoteOnlyBranch]);526 yield* git(tmp, ["checkout", "-b", remoteOnlyBranch]);
482 yield* git(tmp, ["push", "-u", "origin", remoteOnlyBranch]);527 yield* git(tmp, ["push", "-u", "origin", remoteOnlyBranch]);
483 yield* git(tmp, ["checkout", defaultBranch]);528 yield* git(tmp, ["checkout", defaultBranch]);
484 yield* git(tmp, ["branch", "-D", remoteOnlyBranch]);529 yield* git(tmp, ["branch", "-D", remoteOnlyBranch]);
485 530
486 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });531 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
487 const firstRemoteIndex = result.branches.findIndex((branch) => branch.isRemote);532 const firstRemoteIndex = result.branches.findIndex((branch) => branch.isRemote);
488 533
489 expect(result.hasOriginRemote).toBe(true);534 expect(result.hasOriginRemote).toBe(true);
@@ -515,9 +560,9 @@ it.layer(TestLayer)("git integration", (it) => { · 25 unmodified lines
515 560
516 yield* git(remote, ["init", "--bare"]);561 yield* git(remote, ["init", "--bare"]);
517 yield* initRepoWithCommit(tmp);562 yield* initRepoWithCommit(tmp);
518 const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(563 const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
519 (branch) => branch.current,564 cwd: tmp,
520 )!.name;565 })).branches.find((branch) => branch.current)!.name;
521 566
522 yield* git(tmp, ["remote", "add", remoteName, remote]);567 yield* git(tmp, ["remote", "add", remoteName, remote]);
523 yield* git(tmp, ["push", "-u", remoteName, defaultBranch]);568 yield* git(tmp, ["push", "-u", remoteName, defaultBranch]);
@@ -528,7 +573,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
528 yield* git(tmp, ["checkout", defaultBranch]);573 yield* git(tmp, ["checkout", defaultBranch]);
529 yield* git(tmp, ["branch", "-D", remoteOnlyBranch]);574 yield* git(tmp, ["branch", "-D", remoteOnlyBranch]);
530 575
531 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });576 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
532 const remoteBranch = result.branches.find(577 const remoteBranch = result.branches.find(
533 (branch) => branch.name === `${remoteName}/${remoteOnlyBranch}`,578 (branch) => branch.name === `${remoteName}/${remoteOnlyBranch}`,
534 );579 );
@@ -551,7 +596,10 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
551 yield* git(tmp, ["remote", "add", "origin", remote]);596 yield* git(tmp, ["remote", "add", "origin", remote]);
552 yield* git(tmp, ["push", "-u", "origin", initialBranch]);597 yield* git(tmp, ["push", "-u", "origin", initialBranch]);
553 598
554 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature/demo" });599 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
600 cwd: tmp,
601 branch: "feature/demo",
602 });
555 yield* git(tmp, ["push", "-u", "origin", "feature/demo"]);603 yield* git(tmp, ["push", "-u", "origin", "feature/demo"]);
556 604
557 yield* git(tmp, ["checkout", "-b", "feature/remote-only"]);605 yield* git(tmp, ["checkout", "-b", "feature/remote-only"]);
@@ -559,7 +607,7 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
559 yield* git(tmp, ["checkout", initialBranch]);607 yield* git(tmp, ["checkout", initialBranch]);
560 yield* git(tmp, ["branch", "-D", "feature/remote-only"]);608 yield* git(tmp, ["branch", "-D", "feature/remote-only"]);
561 609
562 const result = yield* (yield* GitCore).listBranches({610 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
563 cwd: tmp,611 cwd: tmp,
564 query: "feature/",612 query: "feature/",
565 limit: 10,613 limit: 10,
@@ -582,11 +630,11 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
582 Effect.gen(function* () {630 Effect.gen(function* () {
583 const tmp = yield* makeTmpDir();631 const tmp = yield* makeTmpDir();
584 yield* initRepoWithCommit(tmp);632 yield* initRepoWithCommit(tmp);
585 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature" });633 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "feature" });
586 634
587 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "feature" });635 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "feature" });
588 636
589 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });637 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
590 const current = result.branches.find((b) => b.current);638 const current = result.branches.find((b) => b.current);
591 expect(current!.name).toBe("feature");639 expect(current!.name).toBe("feature");
592 }),640 }),
@@ -603,20 +651,29 @@ it.layer(TestLayer)("git integration", (it) => { · 10 unmodified lines
603 yield* git(remote, ["init", "--bare"]);651 yield* git(remote, ["init", "--bare"]);
604 652
605 yield* initRepoWithCommit(source);653 yield* initRepoWithCommit(source);
606 const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(654 const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
607 (branch) => branch.current,655 cwd: source,
608 )!.name;656 })).branches.find((branch) => branch.current)!.name;
609 yield* git(source, ["remote", "add", "origin", remote]);657 yield* git(source, ["remote", "add", "origin", remote]);
610 yield* git(source, ["push", "-u", "origin", defaultBranch]);658 yield* git(source, ["push", "-u", "origin", defaultBranch]);
611 659
612 const featureBranch = "feature-behind";660 const featureBranch = "feature-behind";
613 yield* (yield* GitCore).createBranch({ cwd: source, branch: featureBranch });661 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
614 yield* (yield* GitCore).checkoutBranch({ cwd: source, branch: featureBranch });662 cwd: source,
663 branch: featureBranch,
664 });
665 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
666 cwd: source,
667 branch: featureBranch,
668 });
615 yield* writeTextFile(path.join(source, "feature.txt"), "feature base\n");669 yield* writeTextFile(path.join(source, "feature.txt"), "feature base\n");
616 yield* git(source, ["add", "feature.txt"]);670 yield* git(source, ["add", "feature.txt"]);
617 yield* git(source, ["commit", "-m", "feature base"]);671 yield* git(source, ["commit", "-m", "feature base"]);
618 yield* git(source, ["push", "-u", "origin", featureBranch]);672 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 });
620 677
621 yield* git(clone, ["clone", remote, "."]);678 yield* git(clone, ["clone", remote, "."]);
622 yield* git(clone, ["config", "user.email", "test@test.com"]);679 yield* git(clone, ["config", "user.email", "test@test.com"]);
@@ -627,8 +684,11 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
627 yield* git(clone, ["commit", "-m", "remote feature update"]);684 yield* git(clone, ["commit", "-m", "remote feature update"]);
628 yield* git(clone, ["push", "origin", featureBranch]);685 yield* git(clone, ["push", "origin", featureBranch]);
629 686
630 yield* (yield* GitCore).checkoutBranch({ cwd: source, branch: featureBranch });687 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
631 const core = yield* GitCore;688 cwd: source,
689 branch: featureBranch,
690 });
691 const core = yield* GitVcsDriver.GitVcsDriver;
632 yield* Effect.promise(() =>692 yield* Effect.promise(() =>
633 vi.waitFor(693 vi.waitFor(
634 async () => {694 async () => {
@@ -653,9 +713,9 @@ it.layer(TestLayer)("git integration", (it) => { · 18 unmodified lines
653 yield* git(remote, ["init", "--bare"]);713 yield* git(remote, ["init", "--bare"]);
654 714
655 yield* initRepoWithCommit(source);715 yield* initRepoWithCommit(source);
656 const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(716 const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
657 (branch) => branch.current,717 cwd: source,
658 )!.name;718 })).branches.find((branch) => branch.current)!.name;
659 yield* git(source, ["remote", "add", "origin", remote]);719 yield* git(source, ["remote", "add", "origin", remote]);
660 yield* git(source, ["push", "-u", "origin", defaultBranch]);720 yield* git(source, ["push", "-u", "origin", defaultBranch]);
661 721
@@ -668,9 +728,9 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
668 yield* git(source, ["push", "-u", "origin", featureBranch]);728 yield* git(source, ["push", "-u", "origin", featureBranch]);
669 yield* git(source, ["checkout", defaultBranch]);729 yield* git(source, ["checkout", defaultBranch]);
670 730
671 const realGitCore = yield* GitCore;731 const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
672 let refreshFetchAttempts = 0;732 let refreshFetchAttempts = 0;
673 const core = yield* makeIsolatedGitCore((input) => {733 const core = yield* makeIsolatedGitVcsDriver((input) => {
674 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {734 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
675 refreshFetchAttempts += 1;735 refreshFetchAttempts += 1;
676 return Effect.fail(736 return Effect.fail(
@@ -682,7 +742,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
682 }),742 }),
683 );743 );
684 }744 }
685 return realGitCore.execute(input);745 return realGitVcsDriver.execute(input);
686 });746 });
687 yield* core.checkoutBranch({ cwd: source, branch: featureBranch });747 yield* core.checkoutBranch({ cwd: source, branch: featureBranch });
688 const status = yield* core.statusDetails(source);748 const status = yield* core.statusDetails(source);
@@ -700,9 +760,9 @@ it.layer(TestLayer)("git integration", (it) => { · 11 unmodified lines
700 yield* git(remote, ["init", "--bare"]);760 yield* git(remote, ["init", "--bare"]);
701 761
702 yield* initRepoWithCommit(source);762 yield* initRepoWithCommit(source);
703 const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(763 const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
704 (branch) => branch.current,764 cwd: source,
705 )!.name;765 })).branches.find((branch) => branch.current)!.name;
706 yield* git(source, ["remote", "add", "origin", remote]);766 yield* git(source, ["remote", "add", "origin", remote]);
707 yield* git(source, ["push", "-u", "origin", defaultBranch]);767 yield* git(source, ["push", "-u", "origin", defaultBranch]);
708 768
@@ -714,9 +774,9 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
714 yield* git(source, ["push", "-u", "origin", featureBranch]);774 yield* git(source, ["push", "-u", "origin", featureBranch]);
715 yield* git(source, ["checkout", defaultBranch]);775 yield* git(source, ["checkout", defaultBranch]);
716 776
717 const realGitCore = yield* GitCore;777 const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
718 let refreshFetchAttempts = 0;778 let refreshFetchAttempts = 0;
719 const core = yield* makeIsolatedGitCore((input) => {779 const core = yield* makeIsolatedGitVcsDriver((input) => {
720 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {780 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
721 refreshFetchAttempts += 1;781 refreshFetchAttempts += 1;
722 return Effect.succeed({782 return Effect.succeed({
@@ -727,7 +787,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
727 stderrTruncated: false,787 stderrTruncated: false,
728 });788 });
729 }789 }
730 return realGitCore.execute(input);790 return realGitVcsDriver.execute(input);
731 });791 });
732 yield* core.checkoutBranch({ cwd: source, branch: featureBranch });792 yield* core.checkoutBranch({ cwd: source, branch: featureBranch });
733 yield* Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, 50)));793 yield* Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, 50)));
@@ -750,7 +810,7 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
750 });810 });
751 811
752 let fetchCount = 0;812 let fetchCount = 0;
753 const core = yield* makeIsolatedGitCore((input) => {813 const core = yield* makeIsolatedGitVcsDriver((input) => {
754 if (814 if (
755 input.args[0] === "rev-parse" &&815 input.args[0] === "rev-parse" &&
756 input.args[1] === "--abbrev-ref" &&816 input.args[1] === "--abbrev-ref" &&
@@ -780,7 +840,7 @@ it.layer(TestLayer)("git integration", (it) => { · 23 unmodified lines
780 ]);840 ]);
781 return ok();841 return ok();
782 }842 }
783 if (input.operation === "GitCore.statusDetails.status") {843 if (input.operation === "GitVcsDriver.statusDetails.status") {
784 return ok(844 return ok(
785 input.cwd === "/repo/worktrees/pr-123"845 input.cwd === "/repo/worktrees/pr-123"
786 ? "# branch.head feature/pr-123\n# branch.upstream origin/feature/pr-123\n# branch.ab +0 -0\n"846 ? "# 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
788 );848 );
789 }849 }
790 if (850 if (
791 input.operation === "GitCore.statusDetails.unstagedNumstat" ||851 input.operation === "GitVcsDriver.statusDetails.unstagedNumstat" ||
792 input.operation === "GitCore.statusDetails.stagedNumstat"852 input.operation === "GitVcsDriver.statusDetails.stagedNumstat"
793 ) {853 ) {
794 return ok();854 return ok();
795 }855 }
796 if (input.operation === "GitCore.statusDetails.defaultRef") {856 if (input.operation === "GitVcsDriver.statusDetails.defaultRef") {
797 return ok("refs/remotes/origin/main\n");857 return ok("refs/remotes/origin/main\n");
798 }858 }
799 return Effect.fail(859 return Effect.fail(
@@ -826,7 +886,7 @@ it.layer(TestLayer)("git integration", (it) => { · 26 unmodified lines
826 });886 });
827 887
828 let fetchCount = 0;888 let fetchCount = 0;
829 const core = yield* makeIsolatedGitCore((input) => {889 const core = yield* makeIsolatedGitVcsDriver((input) => {
830 if (890 if (
831 input.args[0] === "rev-parse" &&891 input.args[0] === "rev-parse" &&
832 input.args[1] === "--abbrev-ref" &&892 input.args[1] === "--abbrev-ref" &&
@@ -856,7 +916,7 @@ it.layer(TestLayer)("git integration", (it) => { · 23 unmodified lines
856 }),916 }),
857 );917 );
858 }918 }
859 if (input.operation === "GitCore.statusDetails.status") {919 if (input.operation === "GitVcsDriver.statusDetails.status") {
860 return ok(920 return ok(
861 input.cwd === "/repo/worktrees/pr-123"921 input.cwd === "/repo/worktrees/pr-123"
862 ? "# branch.head feature/pr-123\n# branch.upstream origin/feature/pr-123\n# branch.ab +0 -0\n"922 ? "# 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
864 );924 );
865 }925 }
866 if (926 if (
867 input.operation === "GitCore.statusDetails.unstagedNumstat" ||927 input.operation === "GitVcsDriver.statusDetails.unstagedNumstat" ||
868 input.operation === "GitCore.statusDetails.stagedNumstat"928 input.operation === "GitVcsDriver.statusDetails.stagedNumstat"
869 ) {929 ) {
870 return ok();930 return ok();
871 }931 }
872 if (input.operation === "GitCore.statusDetails.defaultRef") {932 if (input.operation === "GitVcsDriver.statusDetails.defaultRef") {
873 return ok("refs/remotes/origin/main\n");933 return ok("refs/remotes/origin/main\n");
874 }934 }
875 return Effect.fail(935 return Effect.fail(
@@ -893,7 +953,7 @@ it.layer(TestLayer)("git integration", (it) => { · 17 unmodified lines
893 const tmp = yield* makeTmpDir();953 const tmp = yield* makeTmpDir();
894 yield* initRepoWithCommit(tmp);954 yield* initRepoWithCommit(tmp);
895 const result = yield* Effect.result(955 const result = yield* Effect.result(
896 (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "nonexistent" }),956 (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "nonexistent" }),
897 );957 );
898 expect(result._tag).toBe("Failure");958 expect(result._tag).toBe("Failure");
899 }),959 }),
@@ -906,16 +966,19 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
906 yield* git(remote, ["init", "--bare"]);966 yield* git(remote, ["init", "--bare"]);
907 967
908 yield* initRepoWithCommit(source);968 yield* initRepoWithCommit(source);
909 const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(969 const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
910 (branch) => branch.current,970 cwd: source,
911 )!.name;971 })).branches.find((branch) => branch.current)!.name;
912 yield* git(source, ["remote", "add", "origin", remote]);972 yield* git(source, ["remote", "add", "origin", remote]);
913 yield* git(source, ["push", "-u", "origin", defaultBranch]);973 yield* git(source, ["push", "-u", "origin", defaultBranch]);
914 974
915 yield* (yield* GitCore).createBranch({ cwd: source, branch: "feature" });975 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: source, branch: "feature" });
916 976
917 const checkoutResult = yield* Effect.result(977 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 }),
919 );982 );
920 expect(checkoutResult._tag).toBe("Failure");983 expect(checkoutResult._tag).toBe("Failure");
921 expect(yield* git(source, ["branch", "--show-current"])).toBe(defaultBranch);984 expect(yield* git(source, ["branch", "--show-current"])).toBe(defaultBranch);
@@ -935,9 +998,9 @@ it.layer(TestLayer)("git integration", (it) => { · 13 unmodified lines
935 yield* git(prefixRemote, ["init", "--bare"]);998 yield* git(prefixRemote, ["init", "--bare"]);
936 999
937 yield* initRepoWithCommit(source);1000 yield* initRepoWithCommit(source);
938 const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(1001 const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
939 (branch) => branch.current,1002 cwd: source,
940 )!.name;1003 })).branches.find((branch) => branch.current)!.name;
941 yield* configureRemote(source, prefixRemoteName, prefixRemote, prefixFetchNamespace);1004 yield* configureRemote(source, prefixRemoteName, prefixRemote, prefixFetchNamespace);
942 yield* configureRemote(source, remoteName, remote, remoteName);1005 yield* configureRemote(source, remoteName, remote, remoteName);
943 yield* git(source, ["push", "-u", remoteName, defaultBranch]);1006 yield* git(source, ["push", "-u", remoteName, defaultBranch]);
@@ -950,16 +1013,16 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
950 yield* git(source, ["checkout", defaultBranch]);1013 yield* git(source, ["checkout", defaultBranch]);
951 yield* git(source, ["branch", "-D", featureBranch]);1014 yield* git(source, ["branch", "-D", featureBranch]);
952 1015
953 const checkoutResult = yield* (yield* GitCore).checkoutBranch({1016 const checkoutResult = yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
954 cwd: source,1017 cwd: source,
955 branch: `${remoteName}/${featureBranch}`,1018 branch: `${remoteName}/${featureBranch}`,
956 });1019 });
957 1020
958 expect(checkoutResult.branch).toBe("upstream/feature");1021 expect(checkoutResult.branch).toBe("upstream/feature");
959 expect(yield* git(source, ["branch", "--show-current"])).toBe("upstream/feature");1022 expect(yield* git(source, ["branch", "--show-current"])).toBe("upstream/feature");
960 const realGitCore = yield* GitCore;1023 const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
961 let fetchArgs: readonly string[] | null = null;1024 let fetchArgs: readonly string[] | null = null;
962 const core = yield* makeIsolatedGitCore((input) => {1025 const core = yield* makeIsolatedGitVcsDriver((input) => {
963 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {1026 if (input.args[0] === "--git-dir" && input.args[2] === "fetch") {
964 fetchArgs = [...input.args];1027 fetchArgs = [...input.args];
965 return Effect.succeed({1028 return Effect.succeed({
@@ -970,7 +1033,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
970 stderrTruncated: false,1033 stderrTruncated: false,
971 });1034 });
972 }1035 }
973 return realGitCore.execute(input);1036 return realGitVcsDriver.execute(input);
974 });1037 });
975 1038
976 const status = yield* core.statusDetails(source);1039 const status = yield* core.statusDetails(source);
@@ -996,7 +1059,7 @@ it.layer(TestLayer)("git integration", (it) => { · 19 unmodified lines
996 yield* git(remote, ["init", "--bare"]);1059 yield* git(remote, ["init", "--bare"]);
997 1060
998 yield* initRepoWithCommit(source);1061 yield* initRepoWithCommit(source);
999 const defaultBranch = (yield* (yield* GitCore).listBranches({1062 const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1000 cwd: source,1063 cwd: source,
1001 })).branches.find((branch) => branch.current)!.name;1064 })).branches.find((branch) => branch.current)!.name;
1002 yield* git(source, ["remote", "add", "origin", remote]);1065 yield* git(source, ["remote", "add", "origin", remote]);
@@ -1006,12 +1069,12 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
1006 // would attempt to create an already-existing local branch.1069 // would attempt to create an already-existing local branch.
1007 yield* git(source, ["branch", "--unset-upstream"]);1070 yield* git(source, ["branch", "--unset-upstream"]);
1008 1071
1009 yield* (yield* GitCore).checkoutBranch({1072 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1010 cwd: source,1073 cwd: source,
1011 branch: `origin/${defaultBranch}`,1074 branch: `origin/${defaultBranch}`,
1012 });1075 });
1013 1076
1014 const core = yield* GitCore;1077 const core = yield* GitVcsDriver.GitVcsDriver;
1015 const status = yield* core.statusDetails(source);1078 const status = yield* core.statusDetails(source);
1016 expect(status.branch).toBeNull();1079 expect(status.branch).toBeNull();
1017 }),1080 }),
@@ -1021,7 +1084,7 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
1021 Effect.gen(function* () {1084 Effect.gen(function* () {
1022 const tmp = yield* makeTmpDir();1085 const tmp = yield* makeTmpDir();
1023 yield* initRepoWithCommit(tmp);1086 yield* initRepoWithCommit(tmp);
1024 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "other" });1087 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "other" });
1025 1088
1026 // Create a conflicting change: modify README on current branch1089 // Create a conflicting change: modify README on current branch
1027 yield* writeTextFile(path.join(tmp, "README.md"), "modified\n");1090 yield* writeTextFile(path.join(tmp, "README.md"), "modified\n");
@@ -1029,23 +1092,26 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
1029 1092
1030 // First, checkout other branch cleanly1093 // First, checkout other branch cleanly
1031 yield* git(tmp, ["stash"]);1094 yield* git(tmp, ["stash"]);
1032 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "other" });1095 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "other" });
1033 yield* writeTextFile(path.join(tmp, "README.md"), "other content\n");1096 yield* writeTextFile(path.join(tmp, "README.md"), "other content\n");
1034 yield* git(tmp, ["add", "."]);1097 yield* git(tmp, ["add", "."]);
1035 yield* git(tmp, ["commit", "-m", "other change"]);1098 yield* git(tmp, ["commit", "-m", "other change"]);
1036 1099
1037 // Go back to default branch1100 // Go back to default branch
1038 const defaultBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1101 const defaultBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1039 (b) => !b.current,1102 cwd: tmp,
1040 )!.name;1103 })).branches.find((b) => !b.current)!.name;
1041 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: defaultBranch });1104 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1105 cwd: tmp,
1106 branch: defaultBranch,
1107 });
1042 1108
1043 // Make uncommitted changes to the same file1109 // Make uncommitted changes to the same file
1044 yield* writeTextFile(path.join(tmp, "README.md"), "conflicting local\n");1110 yield* writeTextFile(path.join(tmp, "README.md"), "conflicting local\n");
1045 1111
1046 // Checkout should fail due to uncommitted changes1112 // Checkout should fail due to uncommitted changes
1047 const result = yield* Effect.result(1113 const result = yield* Effect.result(
1048 (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "other" }),1114 (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "other" }),
1049 );1115 );
1050 expect(result._tag).toBe("Failure");1116 expect(result._tag).toBe("Failure");
1051 }),1117 }),
@@ -1059,9 +1125,9 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
1059 Effect.gen(function* () {1125 Effect.gen(function* () {
1060 const tmp = yield* makeTmpDir();1126 const tmp = yield* makeTmpDir();
1061 yield* initRepoWithCommit(tmp);1127 yield* initRepoWithCommit(tmp);
1062 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "new-feature" });1128 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "new-feature" });
1063 1129
1064 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });1130 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
1065 expect(result.branches.some((b) => b.name === "new-feature")).toBe(true);1131 expect(result.branches.some((b) => b.name === "new-feature")).toBe(true);
1066 }),1132 }),
1067 );1133 );
@@ -1070,9 +1136,9 @@ it.layer(TestLayer)("git integration", (it) => { · 2 unmodified lines
1070 Effect.gen(function* () {1136 Effect.gen(function* () {
1071 const tmp = yield* makeTmpDir();1137 const tmp = yield* makeTmpDir();
1072 yield* initRepoWithCommit(tmp);1138 yield* initRepoWithCommit(tmp);
1073 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "dupe" });1139 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "dupe" });
1074 const result = yield* Effect.result(1140 const result = yield* Effect.result(
1075 (yield* GitCore).createBranch({ cwd: tmp, branch: "dupe" }),1141 (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "dupe" }),
1076 );1142 );
1077 expect(result._tag).toBe("Failure");1143 expect(result._tag).toBe("Failure");
1078 }),1144 }),
@@ -1086,10 +1152,16 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
1086 Effect.gen(function* () {1152 Effect.gen(function* () {
1087 const tmp = yield* makeTmpDir();1153 const tmp = yield* makeTmpDir();
1088 yield* initRepoWithCommit(tmp);1154 yield* initRepoWithCommit(tmp);
1089 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature/old-name" });1155 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1090 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "feature/old-name" });1156 cwd: tmp,
1157 branch: "feature/old-name",
1158 });
1159 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1160 cwd: tmp,
1161 branch: "feature/old-name",
1162 });
1091 1163
1092 const renamed = yield* (yield* GitCore).renameBranch({1164 const renamed = yield* (yield* GitVcsDriver.GitVcsDriver).renameBranch({
1093 cwd: tmp,1165 cwd: tmp,
1094 oldBranch: "feature/old-name",1166 oldBranch: "feature/old-name",
1095 newBranch: "feature/new-name",1167 newBranch: "feature/new-name",
@@ -1097,7 +1169,7 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
1097 1169
1098 expect(renamed.branch).toBe("feature/new-name");1170 expect(renamed.branch).toBe("feature/new-name");
1099 1171
1100 const branches = yield* (yield* GitCore).listBranches({ cwd: tmp });1172 const branches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
1101 expect(branches.branches.some((branch) => branch.name === "feature/old-name")).toBe(false);1173 expect(branches.branches.some((branch) => branch.name === "feature/old-name")).toBe(false);
1102 const current = branches.branches.find((branch) => branch.current);1174 const current = branches.branches.find((branch) => branch.current);
1103 expect(current?.name).toBe("feature/new-name");1175 expect(current?.name).toBe("feature/new-name");
@@ -1108,11 +1180,11 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
1108 Effect.gen(function* () {1180 Effect.gen(function* () {
1109 const tmp = yield* makeTmpDir();1181 const tmp = yield* makeTmpDir();
1110 yield* initRepoWithCommit(tmp);1182 yield* initRepoWithCommit(tmp);
1111 const current = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1183 const current = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1112 (b) => b.current,1184 cwd: tmp,
1113 )!;1185 })).branches.find((b) => b.current)!;
1114 1186
1115 const renamed = yield* (yield* GitCore).renameBranch({1187 const renamed = yield* (yield* GitVcsDriver.GitVcsDriver).renameBranch({
1116 cwd: tmp,1188 cwd: tmp,
1117 oldBranch: current.name,1189 oldBranch: current.name,
1118 newBranch: current.name,1190 newBranch: current.name,
@@ -1126,18 +1198,27 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
1126 Effect.gen(function* () {1198 Effect.gen(function* () {
1127 const tmp = yield* makeTmpDir();1199 const tmp = yield* makeTmpDir();
1128 yield* initRepoWithCommit(tmp);1200 yield* initRepoWithCommit(tmp);
1129 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "t3code/feat/session" });1201 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1130 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "t3code/tmp-working" });1202 cwd: tmp,
1131 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "t3code/tmp-working" });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 });
1132 1213
1133 const renamed = yield* (yield* GitCore).renameBranch({1214 const renamed = yield* (yield* GitVcsDriver.GitVcsDriver).renameBranch({
1134 cwd: tmp,1215 cwd: tmp,
1135 oldBranch: "t3code/tmp-working",1216 oldBranch: "t3code/tmp-working",
1136 newBranch: "t3code/feat/session",1217 newBranch: "t3code/feat/session",
1137 });1218 });
1138 1219
1139 expect(renamed.branch).toBe("t3code/feat/session-1");1220 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 });
1141 expect(branches.branches.some((branch) => branch.name === "t3code/feat/session")).toBe(1222 expect(branches.branches.some((branch) => branch.name === "t3code/feat/session")).toBe(
1142 true,1223 true,
1143 );1224 );
@@ -1153,12 +1234,24 @@ it.layer(TestLayer)("git integration", (it) => { · 9 unmodified lines
1153 Effect.gen(function* () {1234 Effect.gen(function* () {
1154 const tmp = yield* makeTmpDir();1235 const tmp = yield* makeTmpDir();
1155 yield* initRepoWithCommit(tmp);1236 yield* initRepoWithCommit(tmp);
1156 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "t3code/feat/session" });1237 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1157 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "t3code/feat/session-1" });1238 cwd: tmp,
1158 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "t3code/tmp-working" });1239 branch: "t3code/feat/session",
1159 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "t3code/tmp-working" });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 });
1160 1253
1161 const renamed = yield* (yield* GitCore).renameBranch({1254 const renamed = yield* (yield* GitVcsDriver.GitVcsDriver).renameBranch({
1162 cwd: tmp,1255 cwd: tmp,
1163 oldBranch: "t3code/tmp-working",1256 oldBranch: "t3code/tmp-working",
1164 newBranch: "t3code/feat/session",1257 newBranch: "t3code/feat/session",
@@ -1172,16 +1265,22 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
1172 Effect.gen(function* () {1265 Effect.gen(function* () {
1173 const tmp = yield* makeTmpDir();1266 const tmp = yield* makeTmpDir();
1174 yield* initRepoWithCommit(tmp);1267 yield* initRepoWithCommit(tmp);
1175 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature/old-name" });1268 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1176 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "feature/old-name" });1269 cwd: tmp,
1270 branch: "feature/old-name",
1271 });
1272 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1273 cwd: tmp,
1274 branch: "feature/old-name",
1275 });
1177 1276
1178 const realGitCore = yield* GitCore;1277 const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
1179 let renameArgs: ReadonlyArray<string> | null = null;1278 let renameArgs: ReadonlyArray<string> | null = null;
1180 const core = yield* makeIsolatedGitCore((input) => {1279 const core = yield* makeIsolatedGitVcsDriver((input) => {
1181 if (input.args[0] === "branch" && input.args[1] === "-m") {1280 if (input.args[0] === "branch" && input.args[1] === "-m") {
1182 renameArgs = [...input.args];1281 renameArgs = [...input.args];
1183 }1282 }
1184 return realGitCore.execute(input);1283 return realGitVcsDriver.execute(input);
1185 });1284 });
1186 1285
1187 const renamed = yield* core.renameBranch({1286 const renamed = yield* core.renameBranch({
@@ -1205,11 +1304,11 @@ it.layer(TestLayer)("git integration", (it) => { · 17 unmodified lines
1205 yield* initRepoWithCommit(tmp);1304 yield* initRepoWithCommit(tmp);
1206 1305
1207 const wtPath = path.join(tmp, "worktree-out");1306 const wtPath = path.join(tmp, "worktree-out");
1208 const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1307 const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1209 (b) => b.current,1308 cwd: tmp,
1210 )!.name;1309 })).branches.find((b) => b.current)!.name;
1211 1310
1212 const result = yield* (yield* GitCore).createWorktree({1311 const result = yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
1213 cwd: tmp,1312 cwd: tmp,
1214 branch: currentBranch,1313 branch: currentBranch,
1215 newBranch: "wt-branch",1314 newBranch: "wt-branch",
@@ -1222,7 +1321,7 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
1222 expect(existsSync(path.join(wtPath, "README.md"))).toBe(true);1321 expect(existsSync(path.join(wtPath, "README.md"))).toBe(true);
1223 1322
1224 // Clean up worktree before tmp dir disposal1323 // 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 });
1226 }),1325 }),
1227 );1326 );
1228 1327
@@ -1232,11 +1331,11 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
1232 yield* initRepoWithCommit(tmp);1331 yield* initRepoWithCommit(tmp);
1233 1332
1234 const wtPath = path.join(tmp, "wt-check-dir");1333 const wtPath = path.join(tmp, "wt-check-dir");
1235 const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1334 const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1236 (b) => b.current,1335 cwd: tmp,
1237 )!.name;1336 })).branches.find((b) => b.current)!.name;
1238 1337
1239 yield* (yield* GitCore).createWorktree({1338 yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
1240 cwd: tmp,1339 cwd: tmp,
1241 branch: currentBranch,1340 branch: currentBranch,
1242 newBranch: "wt-check",1341 newBranch: "wt-check",
@@ -1247,7 +1346,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
1247 const branchOutput = yield* git(wtPath, ["branch", "--show-current"]);1346 const branchOutput = yield* git(wtPath, ["branch", "--show-current"]);
1248 expect(branchOutput).toBe("wt-check");1347 expect(branchOutput).toBe("wt-check");
1249 1348
1250 yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });1349 yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
1251 }),1350 }),
1252 );1351 );
1253 1352
@@ -1255,10 +1354,13 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
1255 Effect.gen(function* () {1354 Effect.gen(function* () {
1256 const tmp = yield* makeTmpDir();1355 const tmp = yield* makeTmpDir();
1257 yield* initRepoWithCommit(tmp);1356 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 });
1259 1361
1260 const wtPath = path.join(tmp, "wt-existing");1362 const wtPath = path.join(tmp, "wt-existing");
1261 const result = yield* (yield* GitCore).createWorktree({1363 const result = yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
1262 cwd: tmp,1364 cwd: tmp,
1263 branch: "feature/existing-worktree",1365 branch: "feature/existing-worktree",
1264 path: wtPath,1366 path: wtPath,
@@ -1269,7 +1371,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
1269 const branchOutput = yield* git(wtPath, ["branch", "--show-current"]);1371 const branchOutput = yield* git(wtPath, ["branch", "--show-current"]);
1270 expect(branchOutput).toBe("feature/existing-worktree");1372 expect(branchOutput).toBe("feature/existing-worktree");
1271 1373
1272 yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });1374 yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
1273 }),1375 }),
1274 );1376 );
1275 1377
@@ -1277,15 +1379,15 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
1277 Effect.gen(function* () {1379 Effect.gen(function* () {
1278 const tmp = yield* makeTmpDir();1380 const tmp = yield* makeTmpDir();
1279 yield* initRepoWithCommit(tmp);1381 yield* initRepoWithCommit(tmp);
1280 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "existing" });1382 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "existing" });
1281 1383
1282 const wtPath = path.join(tmp, "wt-conflict");1384 const wtPath = path.join(tmp, "wt-conflict");
1283 const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1385 const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1284 (b) => b.current,1386 cwd: tmp,
1285 )!.name;1387 })).branches.find((b) => b.current)!.name;
1286 1388
1287 const result = yield* Effect.result(1389 const result = yield* Effect.result(
1288 (yield* GitCore).createWorktree({1390 (yield* GitVcsDriver.GitVcsDriver).createWorktree({
1289 cwd: tmp,1391 cwd: tmp,
1290 branch: currentBranch,1392 branch: currentBranch,
1291 newBranch: "existing",1393 newBranch: "existing",
@@ -1302,11 +1404,11 @@ it.layer(TestLayer)("git integration", (it) => { · 10 unmodified lines
1302 yield* initRepoWithCommit(tmp);1404 yield* initRepoWithCommit(tmp);
1303 1405
1304 const wtPath = path.join(tmp, "wt-list-dir");1406 const wtPath = path.join(tmp, "wt-list-dir");
1305 const mainBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1407 const mainBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1306 (b) => b.current,1408 cwd: tmp,
1307 )!.name;1409 })).branches.find((b) => b.current)!.name;
1308 1410
1309 yield* (yield* GitCore).createWorktree({1411 yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
1310 cwd: tmp,1412 cwd: tmp,
1311 branch: mainBranch,1413 branch: mainBranch,
1312 newBranch: "wt-list",1414 newBranch: "wt-list",
@@ -1314,17 +1416,17 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
1314 });1416 });
1315 1417
1316 // listGitBranches from the worktree should show wt-list as current1418 // 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 });
1318 expect(wtBranches.isRepo).toBe(true);1420 expect(wtBranches.isRepo).toBe(true);
1319 const wtCurrent = wtBranches.branches.find((b) => b.current);1421 const wtCurrent = wtBranches.branches.find((b) => b.current);
1320 expect(wtCurrent!.name).toBe("wt-list");1422 expect(wtCurrent!.name).toBe("wt-list");
1321 1423
1322 // Main repo should still show the original branch as current1424 // 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 });
1324 const mainCurrent = mainBranches.branches.find((b) => b.current);1426 const mainCurrent = mainBranches.branches.find((b) => b.current);
1325 expect(mainCurrent!.name).toBe(mainBranch);1427 expect(mainCurrent!.name).toBe(mainBranch);
1326 1428
1327 yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });1429 yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
1328 }),1430 }),
1329 );1431 );
1330 1432
@@ -1334,11 +1436,11 @@ it.layer(TestLayer)("git integration", (it) => { · 3 unmodified lines
1334 yield* initRepoWithCommit(tmp);1436 yield* initRepoWithCommit(tmp);
1335 1437
1336 const wtPath = path.join(tmp, "wt-remove-dir");1438 const wtPath = path.join(tmp, "wt-remove-dir");
1337 const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1439 const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1338 (b) => b.current,1440 cwd: tmp,
1339 )!.name;1441 })).branches.find((b) => b.current)!.name;
1340 1442
1341 yield* (yield* GitCore).createWorktree({1443 yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
1342 cwd: tmp,1444 cwd: tmp,
1343 branch: currentBranch,1445 branch: currentBranch,
1344 newBranch: "wt-remove",1446 newBranch: "wt-remove",
@@ -1346,7 +1448,7 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
1346 });1448 });
1347 expect(existsSync(wtPath)).toBe(true);1449 expect(existsSync(wtPath)).toBe(true);
1348 1450
1349 yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });1451 yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
1350 expect(existsSync(wtPath)).toBe(false);1452 expect(existsSync(wtPath)).toBe(false);
1351 }),1453 }),
1352 );1454 );
@@ -1357,11 +1459,11 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
1357 yield* initRepoWithCommit(tmp);1459 yield* initRepoWithCommit(tmp);
1358 1460
1359 const wtPath = path.join(tmp, "wt-dirty-dir");1461 const wtPath = path.join(tmp, "wt-dirty-dir");
1360 const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1462 const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1361 (b) => b.current,1463 cwd: tmp,
1362 )!.name;1464 })).branches.find((b) => b.current)!.name;
1363 1465
1364 yield* (yield* GitCore).createWorktree({1466 yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
1365 cwd: tmp,1467 cwd: tmp,
1366 branch: currentBranch,1468 branch: currentBranch,
1367 newBranch: "wt-dirty",1469 newBranch: "wt-dirty",
@@ -1372,12 +1474,16 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
1372 yield* writeTextFile(path.join(wtPath, "README.md"), "dirty change\n");1474 yield* writeTextFile(path.join(wtPath, "README.md"), "dirty change\n");
1373 1475
1374 const failedRemove = yield* Effect.result(1476 const failedRemove = yield* Effect.result(
1375 (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath }),1477 (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath }),
1376 );1478 );
1377 expect(failedRemove._tag).toBe("Failure");1479 expect(failedRemove._tag).toBe("Failure");
1378 expect(existsSync(wtPath)).toBe(true);1480 expect(existsSync(wtPath)).toBe(true);
1379 1481
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 });
1381 expect(existsSync(wtPath)).toBe(false);1487 expect(existsSync(wtPath)).toBe(false);
1382 }),1488 }),
1383 );1489 );
@@ -1390,10 +1496,16 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
1390 Effect.gen(function* () {1496 Effect.gen(function* () {
1391 const tmp = yield* makeTmpDir();1497 const tmp = yield* makeTmpDir();
1392 yield* initRepoWithCommit(tmp);1498 yield* initRepoWithCommit(tmp);
1393 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature-login" });1499 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
1394 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "feature-login" });1500 cwd: tmp,
1501 branch: "feature-login",
1502 });
1503 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
1504 cwd: tmp,
1505 branch: "feature-login",
1506 });
1395 1507
1396 const result = yield* (yield* GitCore).listBranches({ cwd: tmp });1508 const result = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
1397 const current = result.branches.find((b) => b.current);1509 const current = result.branches.find((b) => b.current);
1398 expect(current!.name).toBe("feature-login");1510 expect(current!.name).toBe("feature-login");
1399 }),1511 }),
@@ -1408,12 +1520,12 @@ it.layer(TestLayer)("git integration", (it) => { · 8 unmodified lines
1408 const tmp = yield* makeTmpDir();1520 const tmp = yield* makeTmpDir();
1409 yield* initRepoWithCommit(tmp);1521 yield* initRepoWithCommit(tmp);
1410 1522
1411 const currentBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1523 const currentBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1412 (b) => b.current,1524 cwd: tmp,
1413 )!.name;1525 })).branches.find((b) => b.current)!.name;
1414 1526
1415 const wtPath = path.join(tmp, "my-worktree");1527 const wtPath = path.join(tmp, "my-worktree");
1416 const result = yield* (yield* GitCore).createWorktree({1528 const result = yield* (yield* GitVcsDriver.GitVcsDriver).createWorktree({
1417 cwd: tmp,1529 cwd: tmp,
1418 branch: currentBranch,1530 branch: currentBranch,
1419 newBranch: "feature-wt",1531 newBranch: "feature-wt",
@@ -1424,7 +1536,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
1424 expect(existsSync(result.worktree.path)).toBe(true);1536 expect(existsSync(result.worktree.path)).toBe(true);
1425 1537
1426 // Main repo still on original branch1538 // 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 });
1428 const mainCurrent = mainBranches.branches.find((b) => b.current);1540 const mainCurrent = mainBranches.branches.find((b) => b.current);
1429 expect(mainCurrent!.name).toBe(currentBranch);1541 expect(mainCurrent!.name).toBe(currentBranch);
1430 1542
@@ -1432,7 +1544,7 @@ it.layer(TestLayer)("git integration", (it) => { · 1 unmodified line
1432 const wtBranch = yield* git(wtPath, ["branch", "--show-current"]);1544 const wtBranch = yield* git(wtPath, ["branch", "--show-current"]);
1433 expect(wtBranch).toBe("feature-wt");1545 expect(wtBranch).toBe("feature-wt");
1434 1546
1435 yield* (yield* GitCore).removeWorktree({ cwd: tmp, path: wtPath });1547 yield* (yield* GitVcsDriver.GitVcsDriver).removeWorktree({ cwd: tmp, path: wtPath });
1436 }),1548 }),
1437 );1549 );
1438 });1550 });
@@ -1454,7 +1566,7 @@ it.layer(TestLayer)("git integration", (it) => { · 15 unmodified lines
1454 yield* git(tmp, ["push", "origin", "HEAD:refs/pull/55/head"]);1566 yield* git(tmp, ["push", "origin", "HEAD:refs/pull/55/head"]);
1455 yield* git(tmp, ["checkout", initialBranch]);1567 yield* git(tmp, ["checkout", initialBranch]);
1456 1568
1457 yield* (yield* GitCore).fetchPullRequestBranch({1569 yield* (yield* GitVcsDriver.GitVcsDriver).fetchPullRequestBranch({
1458 cwd: tmp,1570 cwd: tmp,
1459 prNumber: 55,1571 prNumber: 55,
1460 branch: "feature/pr-fetch",1572 branch: "feature/pr-fetch",
@@ -1475,22 +1587,22 @@ it.layer(TestLayer)("git integration", (it) => { · 14 unmodified lines
1475 Effect.gen(function* () {1587 Effect.gen(function* () {
1476 const tmp = yield* makeTmpDir();1588 const tmp = yield* makeTmpDir();
1477 yield* initRepoWithCommit(tmp);1589 yield* initRepoWithCommit(tmp);
1478 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "branch-a" });1590 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "branch-a" });
1479 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "branch-b" });1591 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "branch-b" });
1480 1592
1481 // Simulate switching to thread A's branch1593 // Simulate switching to thread A's branch
1482 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "branch-a" });1594 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "branch-a" });
1483 let branches = yield* (yield* GitCore).listBranches({ cwd: tmp });1595 let branches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
1484 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a");1596 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a");
1485 1597
1486 // Simulate switching to thread B's branch1598 // Simulate switching to thread B's branch
1487 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "branch-b" });1599 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "branch-b" });
1488 branches = yield* (yield* GitCore).listBranches({ cwd: tmp });1600 branches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
1489 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b");1601 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b");
1490 1602
1491 // Switch back to thread A1603 // Switch back to thread A
1492 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "branch-a" });1604 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "branch-a" });
1493 branches = yield* (yield* GitCore).listBranches({ cwd: tmp });1605 branches = yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({ cwd: tmp });
1494 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a");1606 expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a");
1495 }),1607 }),
1496 );1608 );
@@ -1503,40 +1615,43 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
1503 Effect.gen(function* () {1615 Effect.gen(function* () {
1504 const tmp = yield* makeTmpDir();1616 const tmp = yield* makeTmpDir();
1505 yield* initRepoWithCommit(tmp);1617 yield* initRepoWithCommit(tmp);
1506 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "diverged" });1618 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({ cwd: tmp, branch: "diverged" });
1507 1619
1508 // Make diverged branch have different file content1620 // 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" });
1510 yield* writeTextFile(path.join(tmp, "README.md"), "diverged content\n");1622 yield* writeTextFile(path.join(tmp, "README.md"), "diverged content\n");
1511 yield* git(tmp, ["add", "."]);1623 yield* git(tmp, ["add", "."]);
1512 yield* git(tmp, ["commit", "-m", "diverge"]);1624 yield* git(tmp, ["commit", "-m", "diverge"]);
1513 1625
1514 // Actually, let's just get back to the initial branch explicitly1626 // 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 });
1516 const initialBranch = allBranches.branches.find((b) => b.name !== "diverged")!.name;1628 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 });
1518 1633
1519 // Make local uncommitted changes to the same file1634 // Make local uncommitted changes to the same file
1520 yield* writeTextFile(path.join(tmp, "README.md"), "local uncommitted\n");1635 yield* writeTextFile(path.join(tmp, "README.md"), "local uncommitted\n");
1521 1636
1522 // Attempt checkout should fail1637 // Attempt checkout should fail
1523 const failedCheckout = yield* Effect.result(1638 const failedCheckout = yield* Effect.result(
1524 (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "diverged" }),1639 (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({ cwd: tmp, branch: "diverged" }),
1525 );1640 );
1526 expect(failedCheckout._tag).toBe("Failure");1641 expect(failedCheckout._tag).toBe("Failure");
1527 1642
1528 // Current branch should still be the initial one1643 // 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 });
1530 expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch);1645 expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch);
1531 }),1646 }),
1532 );1647 );
1533 });1648 });
1534 1649
1535 describe("GitCore", () => {1650 describe("GitVcsDriver", () => {
1536 it.effect("supports branch lifecycle operations through the service API", () =>1651 it.effect("supports branch lifecycle operations through the service API", () =>
1537 Effect.gen(function* () {1652 Effect.gen(function* () {
1538 const tmp = yield* makeTmpDir();1653 const tmp = yield* makeTmpDir();
1539 const core = yield* GitCore;1654 const core = yield* GitVcsDriver.GitVcsDriver;
1540 1655
1541 yield* core.initRepo({ cwd: tmp });1656 yield* core.initRepo({ cwd: tmp });
1542 yield* git(tmp, ["config", "user.email", "test@test.com"]);1657 yield* git(tmp, ["config", "user.email", "test@test.com"]);
@@ -1563,7 +1678,7 @@ it.layer(TestLayer)("git integration", (it) => { · 20 unmodified lines
1563 Effect.gen(function* () {1678 Effect.gen(function* () {
1564 const tmp = yield* makeTmpDir();1679 const tmp = yield* makeTmpDir();
1565 yield* initRepoWithCommit(tmp);1680 yield* initRepoWithCommit(tmp);
1566 const core = yield* GitCore;1681 const core = yield* GitVcsDriver.GitVcsDriver;
1567 1682
1568 yield* git(tmp, ["remote", "add", "origin", "git@github.com:pingdotgg/t3code.git"]);1683 yield* git(tmp, ["remote", "add", "origin", "git@github.com:pingdotgg/t3code.git"]);
1569 1684
@@ -1582,7 +1697,7 @@ it.layer(TestLayer)("git integration", (it) => { · 12 unmodified lines
1582 Effect.gen(function* () {1697 Effect.gen(function* () {
1583 const tmp = yield* makeTmpDir();1698 const tmp = yield* makeTmpDir();
1584 yield* initRepoWithCommit(tmp);1699 yield* initRepoWithCommit(tmp);
1585 const core = yield* GitCore;1700 const core = yield* GitVcsDriver.GitVcsDriver;
1586 1701
1587 const clean = yield* core.status({ cwd: tmp });1702 const clean = yield* core.status({ cwd: tmp });
1588 expect(clean.hasWorkingTreeChanges).toBe(false);1703 expect(clean.hasWorkingTreeChanges).toBe(false);
@@ -1600,7 +1715,7 @@ it.layer(TestLayer)("git integration", (it) => { · 11 unmodified lines
1600 const deletedDir = path.join(tmp, "deleted-repo");1715 const deletedDir = path.join(tmp, "deleted-repo");
1601 yield* makeDirectory(deletedDir);1716 yield* makeDirectory(deletedDir);
1602 yield* removePath(deletedDir);1717 yield* removePath(deletedDir);
1603 const core = yield* GitCore;1718 const core = yield* GitVcsDriver.GitVcsDriver;
1604 1719
1605 const status = yield* core.statusDetails(deletedDir);1720 const status = yield* core.statusDetails(deletedDir);
1606 const localStatus = yield* core.statusDetailsLocal(deletedDir);1721 const localStatus = yield* core.statusDetailsLocal(deletedDir);
@@ -1629,7 +1744,7 @@ it.layer(TestLayer)("git integration", (it) => { · 22 unmodified lines
1629 Effect.gen(function* () {1744 Effect.gen(function* () {
1630 const tmp = yield* makeTmpDir();1745 const tmp = yield* makeTmpDir();
1631 yield* initRepoWithCommit(tmp);1746 yield* initRepoWithCommit(tmp);
1632 const core = yield* GitCore;1747 const core = yield* GitVcsDriver.GitVcsDriver;
1633 1748
1634 yield* core.createBranch({ cwd: tmp, branch: "feature/no-upstream-ahead" });1749 yield* core.createBranch({ cwd: tmp, branch: "feature/no-upstream-ahead" });
1635 yield* core.checkoutBranch({ cwd: tmp, branch: "feature/no-upstream-ahead" });1750 yield* core.checkoutBranch({ cwd: tmp, branch: "feature/no-upstream-ahead" });
@@ -1654,7 +1769,7 @@ it.layer(TestLayer)("git integration", (it) => { · 18 unmodified lines
1654 yield* git(remote, ["init", "--bare"]);1769 yield* git(remote, ["init", "--bare"]);
1655 1770
1656 yield* initRepoWithCommit(source);1771 yield* initRepoWithCommit(source);
1657 const initialBranch = (yield* (yield* GitCore).listBranches({1772 const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1658 cwd: source,1773 cwd: source,
1659 })).branches.find((branch) => branch.current)!.name;1774 })).branches.find((branch) => branch.current)!.name;
1660 yield* git(source, ["remote", "add", "origin", remote]);1775 yield* git(source, ["remote", "add", "origin", remote]);
@@ -1668,7 +1783,7 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
1668 yield* git(source, ["commit", "-m", "feature commit"]);1783 yield* git(source, ["commit", "-m", "feature commit"]);
1669 yield* git(source, ["branch", "-D", initialBranch]);1784 yield* git(source, ["branch", "-D", initialBranch]);
1670 1785
1671 const core = yield* GitCore;1786 const core = yield* GitVcsDriver.GitVcsDriver;
1672 const details = yield* core.statusDetails(source);1787 const details = yield* core.statusDetails(source);
1673 expect(details.branch).toBe("feature/remote-base-only");1788 expect(details.branch).toBe("feature/remote-base-only");
1674 expect(details.hasUpstream).toBe(false);1789 expect(details.hasUpstream).toBe(false);
@@ -1687,7 +1802,7 @@ it.layer(TestLayer)("git integration", (it) => { · 12 unmodified lines
1687 yield* git(remote, ["init", "--bare"]);1802 yield* git(remote, ["init", "--bare"]);
1688 1803
1689 yield* initRepoWithCommit(source);1804 yield* initRepoWithCommit(source);
1690 const initialBranch = (yield* (yield* GitCore).listBranches({1805 const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1691 cwd: source,1806 cwd: source,
1692 })).branches.find((branch) => branch.current)!.name;1807 })).branches.find((branch) => branch.current)!.name;
1693 yield* git(source, ["remote", "add", remoteName, remote]);1808 yield* git(source, ["remote", "add", remoteName, remote]);
@@ -1706,7 +1821,7 @@ it.layer(TestLayer)("git integration", (it) => { · 12 unmodified lines
1706 yield* git(source, ["commit", "-m", "feature commit"]);1821 yield* git(source, ["commit", "-m", "feature commit"]);
1707 yield* git(source, ["branch", "-D", initialBranch]);1822 yield* git(source, ["branch", "-D", initialBranch]);
1708 1823
1709 const core = yield* GitCore;1824 const core = yield* GitVcsDriver.GitVcsDriver;
1710 const details = yield* core.statusDetails(source);1825 const details = yield* core.statusDetails(source);
1711 expect(details.branch).toBe("feature/non-origin-merge-base");1826 expect(details.branch).toBe("feature/non-origin-merge-base");
1712 expect(details.hasUpstream).toBe(false);1827 expect(details.hasUpstream).toBe(false);
@@ -1719,7 +1834,7 @@ it.layer(TestLayer)("git integration", (it) => { · 6 unmodified lines
1719 Effect.gen(function* () {1834 Effect.gen(function* () {
1720 const tmp = yield* makeTmpDir();1835 const tmp = yield* makeTmpDir();
1721 yield* initRepoWithCommit(tmp);1836 yield* initRepoWithCommit(tmp);
1722 const core = yield* GitCore;1837 const core = yield* GitVcsDriver.GitVcsDriver;
1723 1838
1724 yield* core.createBranch({ cwd: tmp, branch: "feature/no-upstream-no-ahead" });1839 yield* core.createBranch({ cwd: tmp, branch: "feature/no-upstream-no-ahead" });
1725 yield* core.checkoutBranch({ cwd: tmp, branch: "feature/no-upstream-no-ahead" });1840 yield* core.checkoutBranch({ cwd: tmp, branch: "feature/no-upstream-no-ahead" });
@@ -1745,7 +1860,7 @@ it.layer(TestLayer)("git integration", (it) => { · 19 unmodified lines
1745 yield* git(tmp, ["remote", "add", "origin", remote]);1860 yield* git(tmp, ["remote", "add", "origin", remote]);
1746 yield* git(tmp, ["checkout", "-b", "feature/no-base"]);1861 yield* git(tmp, ["checkout", "-b", "feature/no-base"]);
1747 1862
1748 const core = yield* GitCore;1863 const core = yield* GitVcsDriver.GitVcsDriver;
1749 const pushed = yield* core.pushCurrentBranch(tmp, null);1864 const pushed = yield* core.pushCurrentBranch(tmp, null);
1750 expect(pushed.status).toBe("pushed");1865 expect(pushed.status).toBe("pushed");
1751 expect(pushed.setUpstream).toBe(true);1866 expect(pushed.setUpstream).toBe(true);
@@ -1770,7 +1885,7 @@ it.layer(TestLayer)("git integration", (it) => { · 18 unmodified lines
1770 yield* git(tmp, ["remote", "add", "fork", remote]);1885 yield* git(tmp, ["remote", "add", "fork", remote]);
1771 yield* git(tmp, ["checkout", "-b", "feature/fork-only"]);1886 yield* git(tmp, ["checkout", "-b", "feature/fork-only"]);
1772 1887
1773 const core = yield* GitCore;1888 const core = yield* GitVcsDriver.GitVcsDriver;
1774 const pushed = yield* core.pushCurrentBranch(tmp, null);1889 const pushed = yield* core.pushCurrentBranch(tmp, null);
1775 expect(pushed.status).toBe("pushed");1890 expect(pushed.status).toBe("pushed");
1776 expect(pushed.setUpstream).toBe(true);1891 expect(pushed.setUpstream).toBe(true);
@@ -1790,9 +1905,9 @@ it.layer(TestLayer)("git integration", (it) => { · 13 unmodified lines
1790 yield* git(remote, ["init", "--bare"]);1905 yield* git(remote, ["init", "--bare"]);
1791 1906
1792 yield* initRepoWithCommit(tmp);1907 yield* initRepoWithCommit(tmp);
1793 const initialBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1908 const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1794 (branch) => branch.current,1909 cwd: tmp,
1795 )!.name;1910 })).branches.find((branch) => branch.current)!.name;
1796 yield* git(tmp, ["remote", "add", "origin", remote]);1911 yield* git(tmp, ["remote", "add", "origin", remote]);
1797 yield* git(tmp, ["push", "-u", "origin", initialBranch]);1912 yield* git(tmp, ["push", "-u", "origin", initialBranch]);
1798 1913
@@ -1803,7 +1918,7 @@ it.layer(TestLayer)("git integration", (it) => { · 4 unmodified lines
1803 const featureBranch = "feature/publish-no-upstream";1918 const featureBranch = "feature/publish-no-upstream";
1804 yield* git(tmp, ["checkout", "-b", featureBranch]);1919 yield* git(tmp, ["checkout", "-b", featureBranch]);
1805 1920
1806 const core = yield* GitCore;1921 const core = yield* GitVcsDriver.GitVcsDriver;
1807 const pushed = yield* core.pushCurrentBranch(tmp, null);1922 const pushed = yield* core.pushCurrentBranch(tmp, null);
1808 expect(pushed.status).toBe("pushed");1923 expect(pushed.status).toBe("pushed");
1809 expect(pushed.setUpstream).toBe(true);1924 expect(pushed.setUpstream).toBe(true);
@@ -1826,9 +1941,9 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
1826 yield* git(fork, ["init", "--bare"]);1941 yield* git(fork, ["init", "--bare"]);
1827 1942
1828 yield* initRepoWithCommit(tmp);1943 yield* initRepoWithCommit(tmp);
1829 const initialBranch = (yield* (yield* GitCore).listBranches({ cwd: tmp })).branches.find(1944 const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1830 (branch) => branch.current,1945 cwd: tmp,
1831 )!.name;1946 })).branches.find((branch) => branch.current)!.name;
1832 yield* git(tmp, ["remote", "add", "origin", origin]);1947 yield* git(tmp, ["remote", "add", "origin", origin]);
1833 yield* git(tmp, ["remote", "add", "fork", fork]);1948 yield* git(tmp, ["remote", "add", "fork", fork]);
1834 yield* git(tmp, ["push", "-u", "origin", initialBranch]);1949 yield* git(tmp, ["push", "-u", "origin", initialBranch]);
@@ -1840,7 +1955,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
1840 yield* git(tmp, ["add", "feature.txt"]);1955 yield* git(tmp, ["add", "feature.txt"]);
1841 yield* git(tmp, ["commit", "-m", "feature commit"]);1956 yield* git(tmp, ["commit", "-m", "feature commit"]);
1842 1957
1843 const core = yield* GitCore;1958 const core = yield* GitVcsDriver.GitVcsDriver;
1844 const pushed = yield* core.pushCurrentBranch(tmp, null);1959 const pushed = yield* core.pushCurrentBranch(tmp, null);
1845 expect(pushed.status).toBe("pushed");1960 expect(pushed.status).toBe("pushed");
1846 expect(pushed.setUpstream).toBe(true);1961 expect(pushed.setUpstream).toBe(true);
@@ -1883,7 +1998,7 @@ it.layer(TestLayer)("git integration", (it) => { · 36 unmodified lines
1883 yield* git(tmp, ["add", "fork.txt"]);1998 yield* git(tmp, ["add", "fork.txt"]);
1884 yield* git(tmp, ["commit", "-m", "update reviewed PR branch"]);1999 yield* git(tmp, ["commit", "-m", "update reviewed PR branch"]);
1885 2000
1886 const core = yield* GitCore;2001 const core = yield* GitVcsDriver.GitVcsDriver;
1887 const pushed = yield* core.pushCurrentBranch(tmp, null);2002 const pushed = yield* core.pushCurrentBranch(tmp, null);
1888 2003
1889 expect(pushed.status).toBe("pushed");2004 expect(pushed.status).toBe("pushed");
@@ -1928,7 +2043,7 @@ it.layer(TestLayer)("git integration", (it) => { · 38 unmodified lines
1928 yield* git(tmp, ["add", "feature.txt"]);2043 yield* git(tmp, ["add", "feature.txt"]);
1929 yield* git(tmp, ["commit", "-m", "feature update"]);2044 yield* git(tmp, ["commit", "-m", "feature update"]);
1930 2045
1931 const core = yield* GitCore;2046 const core = yield* GitVcsDriver.GitVcsDriver;
1932 const pushed = yield* core.pushCurrentBranch(tmp, null);2047 const pushed = yield* core.pushCurrentBranch(tmp, null);
1933 expect(pushed.status).toBe("pushed");2048 expect(pushed.status).toBe("pushed");
1934 expect(pushed.setUpstream).toBe(false);2049 expect(pushed.setUpstream).toBe(false);
@@ -1946,7 +2061,7 @@ it.layer(TestLayer)("git integration", (it) => { · 11 unmodified lines
1946 Effect.gen(function* () {2061 Effect.gen(function* () {
1947 const tmp = yield* makeTmpDir();2062 const tmp = yield* makeTmpDir();
1948 yield* initRepoWithCommit(tmp);2063 yield* initRepoWithCommit(tmp);
1949 const core = yield* GitCore;2064 const core = yield* GitVcsDriver.GitVcsDriver;
1950 const missingWorktreePath = path.join(tmp, "missing-worktree");2065 const missingWorktreePath = path.join(tmp, "missing-worktree");
1951 2066
1952 const removeResult = yield* Effect.result(2067 const removeResult = yield* Effect.result(
@@ -1973,7 +2088,7 @@ it.layer(TestLayer)("git integration", (it) => { · 20 unmodified lines
1973 yield* git(remote, ["init", "--bare"]);2088 yield* git(remote, ["init", "--bare"]);
1974 2089
1975 yield* initRepoWithCommit(source);2090 yield* initRepoWithCommit(source);
1976 const initialBranch = (yield* (yield* GitCore).listBranches({2091 const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
1977 cwd: source,2092 cwd: source,
1978 })).branches.find((branch) => branch.current)!.name;2093 })).branches.find((branch) => branch.current)!.name;
1979 yield* git(source, ["remote", "add", "origin", remote]);2094 yield* git(source, ["remote", "add", "origin", remote]);
@@ -1994,7 +2109,7 @@ it.layer(TestLayer)("git integration", (it) => { · 14 unmodified lines
1994 yield* git(clone, ["commit", "-m", "remote update"]);2109 yield* git(clone, ["commit", "-m", "remote update"]);
1995 yield* git(clone, ["push", "origin", initialBranch]);2110 yield* git(clone, ["push", "origin", initialBranch]);
1996 2111
1997 const core = yield* GitCore;2112 const core = yield* GitVcsDriver.GitVcsDriver;
1998 const details = yield* core.statusDetails(source);2113 const details = yield* core.statusDetails(source);
1999 expect(details.branch).toBe(initialBranch);2114 expect(details.branch).toBe(initialBranch);
2000 expect(details.aheadCount).toBe(0);2115 expect(details.aheadCount).toBe(0);
@@ -2006,7 +2121,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
2006 Effect.gen(function* () {2121 Effect.gen(function* () {
2007 const tmp = yield* makeTmpDir();2122 const tmp = yield* makeTmpDir();
2008 yield* initRepoWithCommit(tmp);2123 yield* initRepoWithCommit(tmp);
2009 const core = yield* GitCore;2124 const core = yield* GitVcsDriver.GitVcsDriver;
2010 2125
2011 yield* writeTextFile(path.join(tmp, "README.md"), "new content\n");2126 yield* writeTextFile(path.join(tmp, "README.md"), "new content\n");
2012 const context = yield* core.prepareCommitContext(tmp);2127 const context = yield* core.prepareCommitContext(tmp);
@@ -2024,7 +2139,7 @@ it.layer(TestLayer)("git integration", (it) => { · 11 unmodified lines
2024 Effect.gen(function* () {2139 Effect.gen(function* () {
2025 const tmp = yield* makeTmpDir();2140 const tmp = yield* makeTmpDir();
2026 yield* initRepoWithCommit(tmp);2141 yield* initRepoWithCommit(tmp);
2027 const core = yield* GitCore;2142 const core = yield* GitVcsDriver.GitVcsDriver;
2028 2143
2029 yield* writeTextFile(path.join(tmp, "a.txt"), "file a\n");2144 yield* writeTextFile(path.join(tmp, "a.txt"), "file a\n");
2030 yield* writeTextFile(path.join(tmp, "b.txt"), "file b\n");2145 yield* writeTextFile(path.join(tmp, "b.txt"), "file b\n");
@@ -2047,7 +2162,7 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
2047 Effect.gen(function* () {2162 Effect.gen(function* () {
2048 const tmp = yield* makeTmpDir();2163 const tmp = yield* makeTmpDir();
2049 yield* initRepoWithCommit(tmp);2164 yield* initRepoWithCommit(tmp);
2050 const core = yield* GitCore;2165 const core = yield* GitVcsDriver.GitVcsDriver;
2051 2166
2052 yield* writeTextFile(path.join(tmp, "a.txt"), "file a\n");2167 yield* writeTextFile(path.join(tmp, "a.txt"), "file a\n");
2053 yield* writeTextFile(path.join(tmp, "b.txt"), "file b\n");2168 yield* writeTextFile(path.join(tmp, "b.txt"), "file b\n");
@@ -2063,7 +2178,7 @@ it.layer(TestLayer)("git integration", (it) => { · 9 unmodified lines
2063 Effect.gen(function* () {2178 Effect.gen(function* () {
2064 const tmp = yield* makeTmpDir();2179 const tmp = yield* makeTmpDir();
2065 yield* initRepoWithCommit(tmp);2180 yield* initRepoWithCommit(tmp);
2066 const core = yield* GitCore;2181 const core = yield* GitVcsDriver.GitVcsDriver;
2067 2182
2068 yield* writeTextFile(path.join(tmp, "README.md"), buildLargeText());2183 yield* writeTextFile(path.join(tmp, "README.md"), buildLargeText());
2069 2184
@@ -2078,7 +2193,7 @@ it.layer(TestLayer)("git integration", (it) => { · 8 unmodified lines
2078 Effect.gen(function* () {2193 Effect.gen(function* () {
2079 const tmp = yield* makeTmpDir();2194 const tmp = yield* makeTmpDir();
2080 const { initialBranch } = yield* initRepoWithCommit(tmp);2195 const { initialBranch } = yield* initRepoWithCommit(tmp);
2081 const core = yield* GitCore;2196 const core = yield* GitVcsDriver.GitVcsDriver;
2082 2197
2083 yield* core.createBranch({ cwd: tmp, branch: "feature/large-range-context" });2198 yield* core.createBranch({ cwd: tmp, branch: "feature/large-range-context" });
2084 yield* core.checkoutBranch({ cwd: tmp, branch: "feature/large-range-context" });2199 yield* core.checkoutBranch({ cwd: tmp, branch: "feature/large-range-context" });
@@ -2100,11 +2215,17 @@ it.layer(TestLayer)("git integration", (it) => { · 15 unmodified lines
2100 yield* initRepoWithCommit(tmp);2215 yield* initRepoWithCommit(tmp);
2101 yield* git(remote, ["init", "--bare"]);2216 yield* git(remote, ["init", "--bare"]);
2102 yield* git(tmp, ["remote", "add", "origin", remote]);2217 yield* git(tmp, ["remote", "add", "origin", remote]);
2103 yield* (yield* GitCore).createBranch({ cwd: tmp, branch: "feature/core-push" });2218 yield* (yield* GitVcsDriver.GitVcsDriver).createBranch({
2104 yield* (yield* GitCore).checkoutBranch({ cwd: tmp, branch: "feature/core-push" });2219 cwd: tmp,
2220 branch: "feature/core-push",
2221 });
2222 yield* (yield* GitVcsDriver.GitVcsDriver).checkoutBranch({
2223 cwd: tmp,
2224 branch: "feature/core-push",
2225 });
2105 2226
2106 yield* writeTextFile(path.join(tmp, "feature.txt"), "push me\n");2227 yield* writeTextFile(path.join(tmp, "feature.txt"), "push me\n");
2107 const core = yield* GitCore;2228 const core = yield* GitVcsDriver.GitVcsDriver;
2108 const context = yield* core.prepareCommitContext(tmp);2229 const context = yield* core.prepareCommitContext(tmp);
2109 expect(context).not.toBeNull();2230 expect(context).not.toBeNull();
2110 yield* core.commit(tmp, "Add feature file", "");2231 yield* core.commit(tmp, "Add feature file", "");
@@ -2129,9 +2250,9 @@ it.layer(TestLayer)("git integration", (it) => { · 18 unmodified lines
2129 yield* git(remote, ["init", "--bare"]);2250 yield* git(remote, ["init", "--bare"]);
2130 2251
2131 yield* initRepoWithCommit(source);2252 yield* initRepoWithCommit(source);
2132 const initialBranch = (yield* (yield* GitCore).listBranches({ cwd: source })).branches.find(2253 const initialBranch = (yield* (yield* GitVcsDriver.GitVcsDriver).listBranches({
2133 (branch) => branch.current,2254 cwd: source,
2134 )!.name;2255 })).branches.find((branch) => branch.current)!.name;
2135 yield* git(source, ["remote", "add", "origin", remote]);2256 yield* git(source, ["remote", "add", "origin", remote]);
2136 yield* git(source, ["push", "-u", "origin", initialBranch]);2257 yield* git(source, ["push", "-u", "origin", initialBranch]);
2137 2258
@@ -2143,7 +2264,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
2143 yield* git(clone, ["commit", "-m", "remote update"]);2264 yield* git(clone, ["commit", "-m", "remote update"]);
2144 yield* git(clone, ["push", "origin", initialBranch]);2265 yield* git(clone, ["push", "origin", initialBranch]);
2145 2266
2146 const core = yield* GitCore;2267 const core = yield* GitVcsDriver.GitVcsDriver;
2147 const pulled = yield* core.pullCurrentBranch(source);2268 const pulled = yield* core.pullCurrentBranch(source);
2148 expect(pulled.status).toBe("pulled");2269 expect(pulled.status).toBe("pulled");
2149 expect((yield* core.statusDetails(source)).behindCount).toBe(0);2270 expect((yield* core.statusDetails(source)).behindCount).toBe(0);
@@ -2157,7 +2278,9 @@ it.layer(TestLayer)("git integration", (it) => { · 7 unmodified lines
2157 Effect.gen(function* () {2278 Effect.gen(function* () {
2158 const tmp = yield* makeTmpDir();2279 const tmp = yield* makeTmpDir();
2159 yield* initRepoWithCommit(tmp);2280 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 );
2161 expect(result._tag).toBe("Failure");2284 expect(result._tag).toBe("Failure");
2162 if (result._tag === "Failure") {2285 if (result._tag === "Failure") {
2163 expect(result.failure.message.toLowerCase()).toContain("no upstream");2286 expect(result.failure.message.toLowerCase()).toContain("no upstream");
@@ -2169,9 +2292,9 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
2169 Effect.gen(function* () {2292 Effect.gen(function* () {
2170 const tmp = yield* makeTmpDir();2293 const tmp = yield* makeTmpDir();
2171 yield* initRepoWithCommit(tmp);2294 yield* initRepoWithCommit(tmp);
2172 const realGitCore = yield* GitCore;2295 const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
2173 let didFailRecency = false;2296 let didFailRecency = false;
2174 const core = yield* makeIsolatedGitCore((input) => {2297 const core = yield* makeIsolatedGitVcsDriver((input) => {
2175 if (!didFailRecency && input.args[0] === "for-each-ref") {2298 if (!didFailRecency && input.args[0] === "for-each-ref") {
2176 didFailRecency = true;2299 didFailRecency = true;
2177 return Effect.fail(2300 return Effect.fail(
@@ -2183,7 +2306,7 @@ it.layer(TestLayer)("git integration", (it) => { · 5 unmodified lines
2183 }),2306 }),
2184 );2307 );
2185 }2308 }
2186 return realGitCore.execute(input);2309 return realGitVcsDriver.execute(input);
2187 });2310 });
2188 2311
2189 const result = yield* core.listBranches({ cwd: tmp });2312 const result = yield* core.listBranches({ cwd: tmp });
@@ -2203,10 +2326,10 @@ it.layer(TestLayer)("git integration", (it) => { · 13 unmodified lines
2203 yield* git(remote, ["init", "--bare"]);2326 yield* git(remote, ["init", "--bare"]);
2204 yield* git(tmp, ["remote", "add", "origin", remote]);2327 yield* git(tmp, ["remote", "add", "origin", remote]);
2205 2328
2206 const realGitCore = yield* GitCore;2329 const realGitVcsDriver = yield* GitVcsDriver.GitVcsDriver;
2207 let didFailRemoteBranches = false;2330 let didFailRemoteBranches = false;
2208 let didFailRemoteNames = false;2331 let didFailRemoteNames = false;
2209 const core = yield* makeIsolatedGitCore((input) => {2332 const core = yield* makeIsolatedGitVcsDriver((input) => {
2210 if (input.args.join(" ") === "branch --no-color --no-column --remotes") {2333 if (input.args.join(" ") === "branch --no-color --no-column --remotes") {
2211 didFailRemoteBranches = true;2334 didFailRemoteBranches = true;
2212 return Effect.fail(2335 return Effect.fail(
@@ -2229,7 +2352,7 @@ it.layer(TestLayer)("git integration", (it) => { · 16 unmodified lines
2229 }),2352 }),
2230 );2353 );
2231 }2354 }
2232 return realGitCore.execute(input);2355 return realGitVcsDriver.execute(input);
2233 });2356 });
2234 2357
2235 const result = yield* core.listBranches({ cwd: tmp });2358 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
5 Effect,5 Effect,
6 Exit,6 Exit,
7 FileSystem,7 FileSystem,
8 Layer,
9 Option,8 Option,
10 Path,9 Path,
11 PlatformError,10 PlatformError,
@@ -20,23 +19,22 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; · 8 unmodified lines
20 19
21import { GitCommandError, type GitBranch } from "@t3tools/contracts";20import { GitCommandError, type GitBranch } from "@t3tools/contracts";
22import { dedupeRemoteBranchesWithLocalMatches } from "@t3tools/shared/git";21import { dedupeRemoteBranchesWithLocalMatches } from "@t3tools/shared/git";
23import { compactTraceAttributes } from "../../observability/Attributes.ts";22import { compactTraceAttributes } from "../observability/Attributes.ts";
24import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../../observability/Metrics.ts";23import { gitCommandDuration, gitCommandsTotal, withMetrics } from "../observability/Metrics.ts";
25import {24import type {
26 GitCore,25 ExecuteGitProgress,
27 type ExecuteGitProgress,26 GitCommitOptions,
28 type GitCommitOptions,27 GitVcsDriverShape,
29 type GitCoreShape,28 GitStatusDetails,
30 type GitStatusDetails,29 ExecuteGitInput,
31 type ExecuteGitInput,30 ExecuteGitResult,
32 type ExecuteGitResult,31} from "./GitVcsDriver.ts";
33} from "../Services/GitCore.ts";
34import {32import {
35 parseRemoteNames,33 parseRemoteNames,
36 parseRemoteNamesInGitOrder,34 parseRemoteNamesInGitOrder,
37 parseRemoteRefWithRemoteNames,35 parseRemoteRefWithRemoteNames,
38} from "../remoteRefs.ts";36} from "../git/remoteRefs.ts";
39import { ServerConfig } from "../../config.ts";37import { ServerConfig } from "../config.ts";
40import { decodeJsonResult } from "@t3tools/shared/schemaJson";38import { decodeJsonResult } from "@t3tools/shared/schemaJson";
41 39
42const DEFAULT_TIMEOUT_MS = 30_000;40const DEFAULT_TIMEOUT_MS = 30_000;
@@ -411,7 +409,7 @@ const createTrace2Monitor = Effect.fn("createTrace2Monitor")(function* ( · 368 unmodified lines
411 const traceRecord = decodeJsonResult(Trace2Record)(trimmedLine);409 const traceRecord = decodeJsonResult(Trace2Record)(trimmedLine);
412 if (Result.isFailure(traceRecord)) {410 if (Result.isFailure(traceRecord)) {
413 yield* Effect.logDebug(411 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}`,
415 traceRecord.failure,413 traceRecord.failure,
416 );414 );
417 return;415 return;
@@ -604,14 +602,14 @@ const collectOutput = Effect.fn("collectOutput")(function* <E>( · 186 unmodified lines
604 };602 };
605});603});
606 604
607export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {605export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* (options?: {
608 executeOverride?: GitCoreShape["execute"];606 executeOverride?: GitVcsDriverShape["execute"];
609}) {607}) {
610 const fileSystem = yield* FileSystem.FileSystem;608 const fileSystem = yield* FileSystem.FileSystem;
611 const path = yield* Path.Path;609 const path = yield* Path.Path;
612 const { worktreesDir } = yield* ServerConfig;610 const { worktreesDir } = yield* ServerConfig;
613 611
614 let executeRaw: GitCoreShape["execute"];612 let executeRaw: GitVcsDriverShape["execute"];
615 613
616 if (options?.executeOverride) {614 if (options?.executeOverride) {
617 executeRaw = options.executeOverride;615 executeRaw = options.executeOverride;
@@ -718,7 +716,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 100 unmodified lines
718 });716 });
719 }717 }
720 718
721 const execute: GitCoreShape["execute"] = (input) =>719 const execute: GitVcsDriverShape["execute"] = (input) =>
722 executeRaw(input).pipe(720 executeRaw(input).pipe(
723 withMetrics({721 withMetrics({
724 counter: gitCommandsTotal,722 counter: gitCommandsTotal,
@@ -812,7 +810,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 87 unmodified lines
812 810
813 const branchExists = (cwd: string, branch: string): Effect.Effect<boolean, GitCommandError> =>811 const branchExists = (cwd: string, branch: string): Effect.Effect<boolean, GitCommandError> =>
814 executeGit(812 executeGit(
815 "GitCore.branchExists",813 "GitVcsDriver.branchExists",
816 cwd,814 cwd,
817 ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],815 ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],
818 {816 {
@@ -839,7 +837,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 20 unmodified lines
839 }837 }
840 838
841 return yield* createGitCommandError(839 return yield* createGitCommandError(
842 "GitCore.renameBranch",840 "GitVcsDriver.renameBranch",
843 cwd,841 cwd,
844 ["branch", "-m", "--", desiredBranch],842 ["branch", "-m", "--", desiredBranch],
845 `Could not find an available branch name for '${desiredBranch}'.`,843 `Could not find an available branch name for '${desiredBranch}'.`,
@@ -848,7 +846,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
848 846
849 const resolveCurrentUpstream = Effect.fn("resolveCurrentUpstream")(function* (cwd: string) {847 const resolveCurrentUpstream = Effect.fn("resolveCurrentUpstream")(function* (cwd: string) {
850 const upstreamRef = yield* runGitStdout(848 const upstreamRef = yield* runGitStdout(
851 "GitCore.resolveCurrentUpstream",849 "GitVcsDriver.resolveCurrentUpstream",
852 cwd,850 cwd,
853 ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],851 ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
854 true,852 true,
@@ -858,7 +856,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 3 unmodified lines
858 return null;856 return null;
859 }857 }
860 858
861 const remoteNames = yield* runGitStdout("GitCore.listRemoteNames", cwd, ["remote"]).pipe(859 const remoteNames = yield* runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe(
862 Effect.map(parseRemoteNames),860 Effect.map(parseRemoteNames),
863 Effect.catch(() => Effect.succeed<ReadonlyArray<string>>([])),861 Effect.catch(() => Effect.succeed<ReadonlyArray<string>>([])),
864 );862 );
@@ -875,7 +873,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 10 unmodified lines
875 const fetchCwd =873 const fetchCwd =
876 path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;874 path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
877 return executeGit(875 return executeGit(
878 "GitCore.fetchRemoteForStatus",876 "GitVcsDriver.fetchRemoteForStatus",
879 fetchCwd,877 fetchCwd,
880 ["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", remoteName],878 ["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", remoteName],
881 {879 {
@@ -886,7 +884,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 4 unmodified lines
886 };884 };
887 885
888 const resolveGitCommonDir = Effect.fn("resolveGitCommonDir")(function* (cwd: string) {886 const resolveGitCommonDir = Effect.fn("resolveGitCommonDir")(function* (cwd: string) {
889 const gitCommonDir = yield* runGitStdout("GitCore.resolveGitCommonDir", cwd, [887 const gitCommonDir = yield* runGitStdout("GitVcsDriver.resolveGitCommonDir", cwd, [
890 "rev-parse",888 "rev-parse",
891 "--git-common-dir",889 "--git-common-dir",
892 ]).pipe(Effect.map((stdout) => stdout.trim()));890 ]).pipe(Effect.map((stdout) => stdout.trim()));
@@ -929,7 +927,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 36 unmodified lines
929 remoteName: string,927 remoteName: string,
930 ): Effect.Effect<string | null, GitCommandError> =>928 ): Effect.Effect<string | null, GitCommandError> =>
931 executeGit(929 executeGit(
932 "GitCore.resolveDefaultBranchName",930 "GitVcsDriver.resolveDefaultBranchName",
933 cwd,931 cwd,
934 ["symbolic-ref", `refs/remotes/${remoteName}/HEAD`],932 ["symbolic-ref", `refs/remotes/${remoteName}/HEAD`],
935 { allowNonZeroExit: true },933 { allowNonZeroExit: true },
@@ -948,7 +946,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 12 unmodified lines
948 branch: string,946 branch: string,
949 ): Effect.Effect<boolean, GitCommandError> =>947 ): Effect.Effect<boolean, GitCommandError> =>
950 executeGit(948 executeGit(
951 "GitCore.remoteBranchExists",949 "GitVcsDriver.remoteBranchExists",
952 cwd,950 cwd,
953 ["show-ref", "--verify", "--quiet", `refs/remotes/${remoteName}/${branch}`],951 ["show-ref", "--verify", "--quiet", `refs/remotes/${remoteName}/${branch}`],
954 {952 {
@@ -957,12 +955,12 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
957 ).pipe(Effect.map((result) => result.code === 0));955 ).pipe(Effect.map((result) => result.code === 0));
958 956
959 const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> =>957 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"], {
961 allowNonZeroExit: true,959 allowNonZeroExit: true,
962 }).pipe(Effect.map((result) => result.code === 0));960 }).pipe(Effect.map((result) => result.code === 0));
963 961
964 const listRemoteNames = (cwd: string): Effect.Effect<ReadonlyArray<string>, GitCommandError> =>962 const listRemoteNames = (cwd: string): Effect.Effect<ReadonlyArray<string>, GitCommandError> =>
965 runGitStdout("GitCore.listRemoteNames", cwd, ["remote"]).pipe(963 runGitStdout("GitVcsDriver.listRemoteNames", cwd, ["remote"]).pipe(
966 Effect.map(parseRemoteNamesInGitOrder),964 Effect.map(parseRemoteNamesInGitOrder),
967 );965 );
968 966
@@ -976,7 +974,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 7 unmodified lines
976 return firstRemote;974 return firstRemote;
977 }975 }
978 return yield* createGitCommandError(976 return yield* createGitCommandError(
979 "GitCore.resolvePrimaryRemoteName",977 "GitVcsDriver.resolvePrimaryRemoteName",
980 cwd,978 cwd,
981 ["remote"],979 ["remote"],
982 "No git remote is configured for this repository.",980 "No git remote is configured for this repository.",
@@ -988,7 +986,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 5 unmodified lines
988 branch: string,986 branch: string,
989 ) {987 ) {
990 const branchPushRemote = yield* runGitStdout(988 const branchPushRemote = yield* runGitStdout(
991 "GitCore.resolvePushRemoteName.branchPushRemote",989 "GitVcsDriver.resolvePushRemoteName.branchPushRemote",
992 cwd,990 cwd,
993 ["config", "--get", `branch.${branch}.pushRemote`],991 ["config", "--get", `branch.${branch}.pushRemote`],
994 true,992 true,
@@ -998,7 +996,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 3 unmodified lines
998 }996 }
999 997
1000 const pushDefaultRemote = yield* runGitStdout(998 const pushDefaultRemote = yield* runGitStdout(
1001 "GitCore.resolvePushRemoteName.remotePushDefault",999 "GitVcsDriver.resolvePushRemoteName.remotePushDefault",
1002 cwd,1000 cwd,
1003 ["config", "--get", "remote.pushDefault"],1001 ["config", "--get", "remote.pushDefault"],
1004 true,1002 true,
@@ -1010,37 +1008,45 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 5 unmodified lines
1010 return yield* resolvePrimaryRemoteName(cwd).pipe(Effect.catch(() => Effect.succeed(null)));1008 return yield* resolvePrimaryRemoteName(cwd).pipe(Effect.catch(() => Effect.succeed(null)));
1011 });1009 });
1012 1010
1013 const ensureRemote: GitCoreShape["ensureRemote"] = Effect.fn("ensureRemote")(function* (input) {1011 const ensureRemote: GitVcsDriverShape["ensureRemote"] = Effect.fn("ensureRemote")(
1014 const preferredName = sanitizeRemoteName(input.preferredName);1012 function* (input) {
1015 const normalizedTargetUrl = normalizeRemoteUrl(input.url);1013 const preferredName = sanitizeRemoteName(input.preferredName);
1016 const remoteFetchUrls = yield* runGitStdout("GitCore.ensureRemote.listRemoteUrls", input.cwd, [1014 const normalizedTargetUrl = normalizeRemoteUrl(input.url);
1017 "remote",1015 const remoteFetchUrls = yield* runGitStdout(
1018 "-v",1016 "GitVcsDriver.ensureRemote.listRemoteUrls",
1019 ]).pipe(Effect.map((stdout) => parseRemoteFetchUrls(stdout)));1017 input.cwd,
1020 1018 ["remote", "-v"],
1021 for (const [remoteName, remoteUrl] of remoteFetchUrls.entries()) {1019 ).pipe(Effect.map((stdout) => parseRemoteFetchUrls(stdout)));
1022 if (normalizeRemoteUrl(remoteUrl) === normalizedTargetUrl) {1020
1023 return remoteName;1021 for (const [remoteName, remoteUrl] of remoteFetchUrls.entries()) {
1022 if (normalizeRemoteUrl(remoteUrl) === normalizedTargetUrl) {
1023 return remoteName;
1024 }
1024 }1025 }
1025 }
1026 1026
1027 let remoteName = preferredName;1027 let remoteName = preferredName;
1028 let suffix = 1;1028 let suffix = 1;
1029 while (remoteFetchUrls.has(remoteName)) {1029 while (remoteFetchUrls.has(remoteName)) {
1030 remoteName = `${preferredName}-${suffix}`;1030 remoteName = `${preferredName}-${suffix}`;
1031 suffix += 1;1031 suffix += 1;
1032 }1032 }
1033 1033
1034 yield* runGit("GitCore.ensureRemote.add", input.cwd, ["remote", "add", remoteName, input.url]);1034 yield* runGit("GitVcsDriver.ensureRemote.add", input.cwd, [
1035 return remoteName;1035 "remote",
1036 });1036 "add",
1037 remoteName,
1038 input.url,
1039 ]);
1040 return remoteName;
1041 },
1042 );
1037 1043
1038 const resolveBaseBranchForNoUpstream = Effect.fn("resolveBaseBranchForNoUpstream")(function* (1044 const resolveBaseBranchForNoUpstream = Effect.fn("resolveBaseBranchForNoUpstream")(function* (
1039 cwd: string,1045 cwd: string,
1040 branch: string,1046 branch: string,
1041 ) {1047 ) {
1042 const configuredBaseBranch = yield* runGitStdout(1048 const configuredBaseBranch = yield* runGitStdout(
1043 "GitCore.resolveBaseBranchForNoUpstream.config",1049 "GitVcsDriver.resolveBaseBranchForNoUpstream.config",
1044 cwd,1050 cwd,
1045 ["config", "--get", `branch.${branch}.gh-merge-base`],1051 ["config", "--get", `branch.${branch}.gh-merge-base`],
1046 true,1052 true,
@@ -1098,7 +1104,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 51 unmodified lines
1098 }1104 }
1099 1105
1100 const result = yield* executeGit(1106 const result = yield* executeGit(
1101 "GitCore.computeAheadCountAgainstBase",1107 "GitVcsDriver.computeAheadCountAgainstBase",
1102 cwd,1108 cwd,
1103 ["rev-list", "--count", `${baseBranch}..HEAD`],1109 ["rev-list", "--count", `${baseBranch}..HEAD`],
1104 { allowNonZeroExit: true },1110 { allowNonZeroExit: true },
@@ -1113,7 +1119,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 8 unmodified lines
1113 1119
1114 const readBranchRecency = Effect.fn("readBranchRecency")(function* (cwd: string) {1120 const readBranchRecency = Effect.fn("readBranchRecency")(function* (cwd: string) {
1115 const branchRecency = yield* executeGit(1121 const branchRecency = yield* executeGit(
1116 "GitCore.readBranchRecency",1122 "GitVcsDriver.readBranchRecency",
1117 cwd,1123 cwd,
1118 [1124 [
1119 "for-each-ref",1125 "for-each-ref",
@@ -1149,7 +1155,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 29 unmodified lines
1149 1155
1150 const readStatusDetailsLocal = Effect.fn("readStatusDetailsLocal")(function* (cwd: string) {1156 const readStatusDetailsLocal = Effect.fn("readStatusDetailsLocal")(function* (cwd: string) {
1151 const statusResult = yield* executeGit(1157 const statusResult = yield* executeGit(
1152 "GitCore.statusDetails.status",1158 "GitVcsDriver.statusDetails.status",
1153 cwd,1159 cwd,
1154 ["status", "--porcelain=2", "--branch"],1160 ["status", "--porcelain=2", "--branch"],
1155 {1161 {
@@ -1164,7 +1170,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 8 unmodified lines
1164 if (statusResult.code !== 0) {1170 if (statusResult.code !== 0) {
1165 const stderr = statusResult.stderr.trim();1171 const stderr = statusResult.stderr.trim();
1166 return yield* createGitCommandError(1172 return yield* createGitCommandError(
1167 "GitCore.statusDetails.status",1173 "GitVcsDriver.statusDetails.status",
1168 cwd,1174 cwd,
1169 ["status", "--porcelain=2", "--branch"],1175 ["status", "--porcelain=2", "--branch"],
1170 stderr || "git status failed",1176 stderr || "git status failed",
@@ -1174,14 +1180,14 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 3 unmodified lines
1174 const [unstagedNumstatStdout, stagedNumstatStdout, defaultRefResult, hasOriginRemote] =1180 const [unstagedNumstatStdout, stagedNumstatStdout, defaultRefResult, hasOriginRemote] =
1175 yield* Effect.all(1181 yield* Effect.all(
1176 [1182 [
1177 runGitStdout("GitCore.statusDetails.unstagedNumstat", cwd, ["diff", "--numstat"]),1183 runGitStdout("GitVcsDriver.statusDetails.unstagedNumstat", cwd, ["diff", "--numstat"]),
1178 runGitStdout("GitCore.statusDetails.stagedNumstat", cwd, [1184 runGitStdout("GitVcsDriver.statusDetails.stagedNumstat", cwd, [
1179 "diff",1185 "diff",
1180 "--cached",1186 "--cached",
1181 "--numstat",1187 "--numstat",
1182 ]),1188 ]),
1183 executeGit(1189 executeGit(
1184 "GitCore.statusDetails.defaultRef",1190 "GitVcsDriver.statusDetails.defaultRef",
1185 cwd,1191 cwd,
1186 ["symbolic-ref", "refs/remotes/origin/HEAD"],1192 ["symbolic-ref", "refs/remotes/origin/HEAD"],
1187 {1193 {
@@ -1284,21 +1290,23 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 96 unmodified lines
1284 };1290 };
1285 });1291 });
1286 1292
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")(
1288 function* (cwd) {1300 function* (cwd) {
1301 yield* refreshStatusUpstreamIfStale(cwd).pipe(
1302 Effect.catchIf(isMissingGitCwdError, () => Effect.void),
1303 Effect.ignoreCause({ log: true }),
1304 );
1289 return yield* readStatusDetailsLocal(cwd);1305 return yield* readStatusDetailsLocal(cwd);
1290 },1306 },
1291 );1307 );
1292 1308
1293 const statusDetails: GitCoreShape["statusDetails"] = Effect.fn("statusDetails")(function* (cwd) {1309 const status: GitVcsDriverShape["status"] = (input) =>
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) =>
1302 statusDetails(input.cwd).pipe(1310 statusDetails(input.cwd).pipe(
1303 Effect.map((details) => ({1311 Effect.map((details) => ({
1304 isRepo: details.isRepo,1312 isRepo: details.isRepo,
@@ -1314,34 +1322,34 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 9 unmodified lines
1314 })),1322 })),
1315 );1323 );
1316 1324
1317 const prepareCommitContext: GitCoreShape["prepareCommitContext"] = Effect.fn(1325 const prepareCommitContext: GitVcsDriverShape["prepareCommitContext"] = Effect.fn(
1318 "prepareCommitContext",1326 "prepareCommitContext",
1319 )(function* (cwd, filePaths) {1327 )(function* (cwd, filePaths) {
1320 if (filePaths && filePaths.length > 0) {1328 if (filePaths && filePaths.length > 0) {
1321 yield* runGit("GitCore.prepareCommitContext.reset", cwd, ["reset"]).pipe(1329 yield* runGit("GitVcsDriver.prepareCommitContext.reset", cwd, ["reset"]).pipe(
1322 Effect.catch(() => Effect.void),1330 Effect.catch(() => Effect.void),
1323 );1331 );
1324 yield* runGit("GitCore.prepareCommitContext.addSelected", cwd, [1332 yield* runGit("GitVcsDriver.prepareCommitContext.addSelected", cwd, [
1325 "add",1333 "add",
1326 "-A",1334 "-A",
1327 "--",1335 "--",
1328 ...filePaths,1336 ...filePaths,
1329 ]);1337 ]);
1330 } else {1338 } else {
1331 yield* runGit("GitCore.prepareCommitContext.addAll", cwd, ["add", "-A"]);1339 yield* runGit("GitVcsDriver.prepareCommitContext.addAll", cwd, ["add", "-A"]);
1332 }1340 }
1333 1341
1334 const stagedSummary = yield* runGitStdout("GitCore.prepareCommitContext.stagedSummary", cwd, [1342 const stagedSummary = yield* runGitStdout(
1335 "diff",1343 "GitVcsDriver.prepareCommitContext.stagedSummary",
1336 "--cached",1344 cwd,
1337 "--name-status",1345 ["diff", "--cached", "--name-status"],
1338 ]).pipe(Effect.map((stdout) => stdout.trim()));1346 ).pipe(Effect.map((stdout) => stdout.trim()));
1339 if (stagedSummary.length === 0) {1347 if (stagedSummary.length === 0) {
1340 return null;1348 return null;
1341 }1349 }
1342 1350
1343 const stagedPatch = yield* runGitStdoutWithOptions(1351 const stagedPatch = yield* runGitStdoutWithOptions(
1344 "GitCore.prepareCommitContext.stagedPatch",1352 "GitVcsDriver.prepareCommitContext.stagedPatch",
1345 cwd,1353 cwd,
1346 ["diff", "--cached", "--patch", "--minimal"],1354 ["diff", "--cached", "--patch", "--minimal"],
1347 {1355 {
@@ -1356,7 +1364,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 8 unmodified lines
1356 };1364 };
1357 });1365 });
1358 1366
1359 const commit: GitCoreShape["commit"] = Effect.fn("commit")(function* (1367 const commit: GitVcsDriverShape["commit"] = Effect.fn("commit")(function* (
1360 cwd,1368 cwd,
1361 subject,1369 subject,
1362 body,1370 body,
@@ -1377,11 +1385,11 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 14 unmodified lines
1377 onStderrLine: (line: string) =>1385 onStderrLine: (line: string) =>
1378 options.progress?.onOutputLine?.({ stream: "stderr", text: line }) ?? Effect.void,1386 options.progress?.onOutputLine?.({ stream: "stderr", text: line }) ?? Effect.void,
1379 };1387 };
1380 yield* executeGit("GitCore.commit.commit", cwd, args, {1388 yield* executeGit("GitVcsDriver.commit.commit", cwd, args, {
1381 ...(options?.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),1389 ...(options?.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
1382 ...(progress ? { progress } : {}),1390 ...(progress ? { progress } : {}),
1383 }).pipe(Effect.asVoid);1391 }).pipe(Effect.asVoid);
1384 const commitSha = yield* runGitStdout("GitCore.commit.revParseHead", cwd, [1392 const commitSha = yield* runGitStdout("GitVcsDriver.commit.revParseHead", cwd, [
1385 "rev-parse",1393 "rev-parse",
1386 "HEAD",1394 "HEAD",
1387 ]).pipe(Effect.map((stdout) => stdout.trim()));1395 ]).pipe(Effect.map((stdout) => stdout.trim()));
@@ -1389,13 +1397,13 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 1 unmodified line
1389 return { commitSha };1397 return { commitSha };
1390 });1398 });
1391 1399
1392 const pushCurrentBranch: GitCoreShape["pushCurrentBranch"] = Effect.fn("pushCurrentBranch")(1400 const pushCurrentBranch: GitVcsDriverShape["pushCurrentBranch"] = Effect.fn("pushCurrentBranch")(
1393 function* (cwd, fallbackBranch) {1401 function* (cwd, fallbackBranch) {
1394 const details = yield* statusDetails(cwd);1402 const details = yield* statusDetails(cwd);
1395 const branch = details.branch ?? fallbackBranch;1403 const branch = details.branch ?? fallbackBranch;
1396 if (!branch) {1404 if (!branch) {
1397 return yield* createGitCommandError(1405 return yield* createGitCommandError(
1398 "GitCore.pushCurrentBranch",1406 "GitVcsDriver.pushCurrentBranch",
1399 cwd,1407 cwd,
1400 ["push"],1408 ["push"],
1401 "Cannot push from detached HEAD.",1409 "Cannot push from detached HEAD.",
@@ -1442,13 +1450,13 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 40 unmodified lines
1442 const publishRemoteName = yield* resolvePushRemoteName(cwd, branch);1450 const publishRemoteName = yield* resolvePushRemoteName(cwd, branch);
1443 if (!publishRemoteName) {1451 if (!publishRemoteName) {
1444 return yield* createGitCommandError(1452 return yield* createGitCommandError(
1445 "GitCore.pushCurrentBranch",1453 "GitVcsDriver.pushCurrentBranch",
1446 cwd,1454 cwd,
1447 ["push"],1455 ["push"],
1448 "Cannot push because no git remote is configured for this repository.",1456 "Cannot push because no git remote is configured for this repository.",
1449 );1457 );
1450 }1458 }
1451 yield* runGit("GitCore.pushCurrentBranch.pushWithUpstream", cwd, [1459 yield* runGit("GitVcsDriver.pushCurrentBranch.pushWithUpstream", cwd, [
1452 "push",1460 "push",
1453 "-u",1461 "-u",
1454 publishRemoteName,1462 publishRemoteName,
@@ -1466,7 +1474,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 11 unmodified lines
1466 Effect.catch(() => Effect.succeed(null)),1474 Effect.catch(() => Effect.succeed(null)),
1467 );1475 );
1468 if (currentUpstream) {1476 if (currentUpstream) {
1469 yield* runGit("GitCore.pushCurrentBranch.pushUpstream", cwd, [1477 yield* runGit("GitVcsDriver.pushCurrentBranch.pushUpstream", cwd, [
1470 "push",1478 "push",
1471 currentUpstream.remoteName,1479 currentUpstream.remoteName,
1472 `HEAD:${currentUpstream.upstreamBranch}`,1480 `HEAD:${currentUpstream.upstreamBranch}`,
@@ -1479,7 +1487,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 6 unmodified lines
1479 };1487 };
1480 }1488 }
1481 1489
1482 yield* runGit("GitCore.pushCurrentBranch.push", cwd, ["push"]);1490 yield* runGit("GitVcsDriver.pushCurrentBranch.push", cwd, ["push"]);
1483 return {1491 return {
1484 status: "pushed" as const,1492 status: "pushed" as const,
1485 branch,1493 branch,
@@ -1489,13 +1497,13 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 3 unmodified lines
1489 },1497 },
1490 );1498 );
1491 1499
1492 const pullCurrentBranch: GitCoreShape["pullCurrentBranch"] = Effect.fn("pullCurrentBranch")(1500 const pullCurrentBranch: GitVcsDriverShape["pullCurrentBranch"] = Effect.fn("pullCurrentBranch")(
1493 function* (cwd) {1501 function* (cwd) {
1494 const details = yield* statusDetails(cwd);1502 const details = yield* statusDetails(cwd);
1495 const branch = details.branch;1503 const branch = details.branch;
1496 if (!branch) {1504 if (!branch) {
1497 return yield* createGitCommandError(1505 return yield* createGitCommandError(
1498 "GitCore.pullCurrentBranch",1506 "GitVcsDriver.pullCurrentBranch",
1499 cwd,1507 cwd,
1500 ["pull", "--ff-only"],1508 ["pull", "--ff-only"],
1501 "Cannot pull from detached HEAD.",1509 "Cannot pull from detached HEAD.",
@@ -1503,24 +1511,24 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 1 unmodified line
1503 }1511 }
1504 if (!details.hasUpstream) {1512 if (!details.hasUpstream) {
1505 return yield* createGitCommandError(1513 return yield* createGitCommandError(
1506 "GitCore.pullCurrentBranch",1514 "GitVcsDriver.pullCurrentBranch",
1507 cwd,1515 cwd,
1508 ["pull", "--ff-only"],1516 ["pull", "--ff-only"],
1509 "Current branch has no upstream configured. Push with upstream first.",1517 "Current branch has no upstream configured. Push with upstream first.",
1510 );1518 );
1511 }1519 }
1512 const beforeSha = yield* runGitStdout(1520 const beforeSha = yield* runGitStdout(
1513 "GitCore.pullCurrentBranch.beforeSha",1521 "GitVcsDriver.pullCurrentBranch.beforeSha",
1514 cwd,1522 cwd,
1515 ["rev-parse", "HEAD"],1523 ["rev-parse", "HEAD"],
1516 true,1524 true,
1517 ).pipe(Effect.map((stdout) => stdout.trim()));1525 ).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"], {
1519 timeoutMs: 30_000,1527 timeoutMs: 30_000,
1520 fallbackErrorMessage: "git pull failed",1528 fallbackErrorMessage: "git pull failed",
1521 });1529 });
1522 const afterSha = yield* runGitStdout(1530 const afterSha = yield* runGitStdout(
1523 "GitCore.pullCurrentBranch.afterSha",1531 "GitVcsDriver.pullCurrentBranch.afterSha",
1524 cwd,1532 cwd,
1525 ["rev-parse", "HEAD"],1533 ["rev-parse", "HEAD"],
1526 true,1534 true,
@@ -1535,13 +1543,13 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 8 unmodified lines
1535 },1543 },
1536 );1544 );
1537 1545
1538 const readRangeContext: GitCoreShape["readRangeContext"] = Effect.fn("readRangeContext")(1546 const readRangeContext: GitVcsDriverShape["readRangeContext"] = Effect.fn("readRangeContext")(
1539 function* (cwd, baseBranch) {1547 function* (cwd, baseBranch) {
1540 const range = `${baseBranch}..HEAD`;1548 const range = `${baseBranch}..HEAD`;
1541 const [commitSummary, diffSummary, diffPatch] = yield* Effect.all(1549 const [commitSummary, diffSummary, diffPatch] = yield* Effect.all(
1542 [1550 [
1543 runGitStdoutWithOptions(1551 runGitStdoutWithOptions(
1544 "GitCore.readRangeContext.log",1552 "GitVcsDriver.readRangeContext.log",
1545 cwd,1553 cwd,
1546 ["log", "--oneline", range],1554 ["log", "--oneline", range],
1547 {1555 {
@@ -1550,7 +1558,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
1550 },1558 },
1551 ),1559 ),
1552 runGitStdoutWithOptions(1560 runGitStdoutWithOptions(
1553 "GitCore.readRangeContext.diffStat",1561 "GitVcsDriver.readRangeContext.diffStat",
1554 cwd,1562 cwd,
1555 ["diff", "--stat", range],1563 ["diff", "--stat", range],
1556 {1564 {
@@ -1559,7 +1567,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
1559 },1567 },
1560 ),1568 ),
1561 runGitStdoutWithOptions(1569 runGitStdoutWithOptions(
1562 "GitCore.readRangeContext.diffPatch",1570 "GitVcsDriver.readRangeContext.diffPatch",
1563 cwd,1571 cwd,
1564 ["diff", "--patch", "--minimal", range],1572 ["diff", "--patch", "--minimal", range],
1565 {1573 {
@@ -1579,228 +1587,230 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 13 unmodified lines
1579 },1587 },
1580 );1588 );
1581 1589
1582 const readConfigValue: GitCoreShape["readConfigValue"] = (cwd, key) =>1590 const readConfigValue: GitVcsDriverShape["readConfigValue"] = (cwd, key) =>
1583 runGitStdout("GitCore.readConfigValue", cwd, ["config", "--get", key], true).pipe(1591 runGitStdout("GitVcsDriver.readConfigValue", cwd, ["config", "--get", key], true).pipe(
1584 Effect.map((stdout) => stdout.trim()),1592 Effect.map((stdout) => stdout.trim()),
1585 Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)),1593 Effect.map((trimmed) => (trimmed.length > 0 ? trimmed : null)),
1586 );1594 );
1587 1595
1588 const listBranches: GitCoreShape["listBranches"] = Effect.fn("listBranches")(function* (input) {1596 const listBranches: GitVcsDriverShape["listBranches"] = Effect.fn("listBranches")(
1589 const branchRecencyPromise = readBranchRecency(input.cwd).pipe(1597 function* (input) {
1590 Effect.catch(() => Effect.succeed(new Map<string, number>())),1598 const branchRecencyPromise = readBranchRecency(input.cwd).pipe(
1591 );1599 Effect.catch(() => Effect.succeed(new Map<string, number>())),
1592 const localBranchResult = yield* executeGit(1600 );
1593 "GitCore.listBranches.branchNoColor",1601 const localBranchResult = yield* executeGit(
1594 input.cwd,1602 "GitVcsDriver.listBranches.branchNoColor",
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",
1625 input.cwd,1603 input.cwd,
1626 ["branch", "--no-color", "--no-column"],1604 ["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 ),
1628 );1619 );
1629 }
1630 1620
1631 const remoteBranchResultEffect = executeGit(1621 if (localBranchResult.code !== 0) {
1632 "GitCore.listBranches.remoteBranches",1622 const stderr = localBranchResult.stderr.trim();
1633 input.cwd,1623 if (stderr.toLowerCase().includes("not a git repository")) {
1634 ["branch", "--no-color", "--no-column", "--remotes"],1624 return {
1635 {1625 branches: [],
1636 timeoutMs: 10_000,1626 isRepo: false,
1637 allowNonZeroExit: true,1627 hasOriginRemote: false,
1638 },1628 nextCursor: null,
1639 ).pipe(1629 totalCount: 0,
1640 Effect.catch((error) =>1630 };
1641 Effect.logWarning(1631 }
1642 `GitCore.listBranches: remote branch lookup failed for ${input.cwd}: ${error.message}. Falling back to an empty remote branch list.`,1632 return yield* createGitCommandError(
1643 ).pipe(Effect.as({ code: 1, stdout: "", stderr: "" })),1633 "GitVcsDriver.listBranches",
1644 ),1634 input.cwd,
1645 );1635 ["branch", "--no-color", "--no-column"],
1646 1636 stderr || "git branch failed",
1647 const remoteNamesResultEffect = executeGit(1637 );
1648 "GitCore.listBranches.remoteNames",1638 }
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 );
1662 1639
1663 const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =1640 const remoteBranchResultEffect = executeGit(
1664 yield* Effect.all(1641 "GitVcsDriver.listBranches.remoteBranches",
1665 [1642 input.cwd,
1666 executeGit(1643 ["branch", "--no-color", "--no-column", "--remotes"],
1667 "GitCore.listBranches.defaultRef",1644 {
1668 input.cwd,1645 timeoutMs: 10_000,
1669 ["symbolic-ref", "refs/remotes/origin/HEAD"],1646 allowNonZeroExit: true,
1670 {1647 },
1671 timeoutMs: 5_000,1648 ).pipe(
1672 allowNonZeroExit: true,1649 Effect.catch((error) =>
1673 },1650 Effect.logWarning(
1674 ),1651 `GitVcsDriver.listBranches: remote branch lookup failed for ${input.cwd}: ${error.message}. Falling back to an empty remote branch list.`,
1675 executeGit(1652 ).pipe(Effect.as({ code: 1, stdout: "", stderr: "" })),
1676 "GitCore.listBranches.worktreeList",1653 ),
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" },
1689 );1654 );
1690 1655
1691 const remoteNames =1656 const remoteNamesResultEffect = executeGit(
1692 remoteNamesResult.code === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];1657 "GitVcsDriver.listBranches.remoteNames",
1693 if (remoteBranchResult.code !== 0 && remoteBranchResult.stderr.trim().length > 0) {1658 input.cwd,
1694 yield* Effect.logWarning(1659 ["remote"],
1695 `GitCore.listBranches: remote branch lookup returned code ${remoteBranchResult.code} for ${input.cwd}: ${remoteBranchResult.stderr.trim()}. Falling back to an empty remote branch list.`,1660 {
1696 );1661 timeoutMs: 5_000,
1697 }1662 allowNonZeroExit: true,
1698 if (remoteNamesResult.code !== 0 && remoteNamesResult.stderr.trim().length > 0) {1663 },
1699 yield* Effect.logWarning(1664 ).pipe(
1700 `GitCore.listBranches: remote name lookup returned code ${remoteNamesResult.code} for ${input.cwd}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,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 ),
1701 );1670 );
1702 }
1703 1671
1704 const defaultBranch =1672 const [defaultRef, worktreeList, remoteBranchResult, remoteNamesResult, branchLastCommit] =
1705 defaultRef.code === 01673 yield* Effect.all(
1706 ? defaultRef.stdout.trim().replace(/^refs\/remotes\/origin\//, "")1674 [
1707 : null;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 );
1708 1699
1709 const worktreeMap = new Map<string, string>();1700 const remoteNames =
1710 if (worktreeList.code === 0) {1701 remoteNamesResult.code === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];
1711 let currentPath: string | null = null;1702 if (remoteBranchResult.code !== 0 && remoteBranchResult.stderr.trim().length > 0) {
1712 for (const line of worktreeList.stdout.split("\n")) {1703 yield* Effect.logWarning(
1713 if (line.startsWith("worktree ")) {1704 `GitVcsDriver.listBranches: remote branch lookup returned code ${remoteBranchResult.code} for ${input.cwd}: ${remoteBranchResult.stderr.trim()}. Falling back to an empty remote branch list.`,
1714 const candidatePath = line.slice("worktree ".length);1705 );
1715 const exists = yield* fileSystem.stat(candidatePath).pipe(1706 }
1716 Effect.map(() => true),1707 if (remoteNamesResult.code !== 0 && remoteNamesResult.stderr.trim().length > 0) {
1717 Effect.catch(() => Effect.succeed(false)),1708 yield* Effect.logWarning(
1718 );1709 `GitVcsDriver.listBranches: remote name lookup returned code ${remoteNamesResult.code} for ${input.cwd}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,
1719 currentPath = exists ? candidatePath : null;1710 );
1720 } else if (line.startsWith("branch refs/heads/") && currentPath) {1711 }
1721 worktreeMap.set(line.slice("branch refs/heads/".length), currentPath);1712
1722 } else if (line === "") {1713 const defaultBranch =
1723 currentPath = null;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 }
1724 }1734 }
1725 }1735 }
1726 }
1727 1736
1728 const localBranches = localBranchResult.stdout1737 const localBranches = localBranchResult.stdout
1729 .split("\n")1738 .split("\n")
1730 .map(parseBranchLine)1739 .map(parseBranchLine)
1731 .filter((branch): branch is { name: string; current: boolean } => branch !== null)1740 .filter((branch): branch is { name: string; current: boolean } => branch !== null)
1732 .map((branch) => ({1741 .map((branch) => ({
1733 name: branch.name,1742 name: branch.name,
1734 current: branch.current,1743 current: branch.current,
1735 isRemote: false,1744 isRemote: false,
1736 isDefault: branch.name === defaultBranch,1745 isDefault: branch.name === defaultBranch,
1737 worktreePath: worktreeMap.get(branch.name) ?? null,1746 worktreePath: worktreeMap.get(branch.name) ?? null,
1738 }))1747 }))
1739 .toSorted((a, b) => {1748 .toSorted((a, b) => {
1740 const aPriority = a.current ? 0 : a.isDefault ? 1 : 2;1749 const aPriority = a.current ? 0 : a.isDefault ? 1 : 2;
1741 const bPriority = b.current ? 0 : b.isDefault ? 1 : 2;1750 const bPriority = b.current ? 0 : b.isDefault ? 1 : 2;
1742 if (aPriority !== bPriority) return aPriority - bPriority;1751 if (aPriority !== bPriority) return aPriority - bPriority;
1743 1752
1744 const aLastCommit = branchLastCommit.get(a.name) ?? 0;1753 const aLastCommit = branchLastCommit.get(a.name) ?? 0;
1745 const bLastCommit = branchLastCommit.get(b.name) ?? 0;1754 const bLastCommit = branchLastCommit.get(b.name) ?? 0;
1746 if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;1755 if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
1747 return a.name.localeCompare(b.name);1756 return a.name.localeCompare(b.name);
1748 });1757 });
1749 1758
1750 const remoteBranches =1759 const remoteBranches =
1751 remoteBranchResult.code === 01760 remoteBranchResult.code === 0
1752 ? remoteBranchResult.stdout1761 ? remoteBranchResult.stdout
1753 .split("\n")1762 .split("\n")
1754 .map(parseBranchLine)1763 .map(parseBranchLine)
1755 .filter((branch): branch is { name: string; current: boolean } => branch !== null)1764 .filter((branch): branch is { name: string; current: boolean } => branch !== null)
1756 .map((branch) => {1765 .map((branch) => {
1757 const parsedRemoteRef = parseRemoteRefWithRemoteNames(branch.name, remoteNames);1766 const parsedRemoteRef = parseRemoteRefWithRemoteNames(branch.name, remoteNames);
1758 const remoteBranch: {1767 const remoteBranch: {
1759 name: string;1768 name: string;
1760 current: boolean;1769 current: boolean;
1761 isRemote: boolean;1770 isRemote: boolean;
1762 remoteName?: string;1771 remoteName?: string;
1763 isDefault: boolean;1772 isDefault: boolean;
1764 worktreePath: string | null;1773 worktreePath: string | null;
1765 } = {1774 } = {
1766 name: branch.name,1775 name: branch.name,
1767 current: false,1776 current: false,
1768 isRemote: true,1777 isRemote: true,
1769 isDefault: false,1778 isDefault: false,
1770 worktreePath: null,1779 worktreePath: null,
1771 };1780 };
1772 if (parsedRemoteRef) {1781 if (parsedRemoteRef) {
1773 remoteBranch.remoteName = parsedRemoteRef.remoteName;1782 remoteBranch.remoteName = parsedRemoteRef.remoteName;
1774 }1783 }
1775 return remoteBranch;1784 return remoteBranch;
1776 })1785 })
1777 .toSorted((a, b) => {1786 .toSorted((a, b) => {
1778 const aLastCommit = branchLastCommit.get(a.name) ?? 0;1787 const aLastCommit = branchLastCommit.get(a.name) ?? 0;
1779 const bLastCommit = branchLastCommit.get(b.name) ?? 0;1788 const bLastCommit = branchLastCommit.get(b.name) ?? 0;
1780 if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;1789 if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
1781 return a.name.localeCompare(b.name);1790 return a.name.localeCompare(b.name);
1782 })1791 })
1783 : [];1792 : [];
1784 1793
1785 const branches = paginateBranches({1794 const branches = paginateBranches({
1786 branches: filterBranchesForListQuery(1795 branches: filterBranchesForListQuery(
1787 dedupeRemoteBranchesWithLocalMatches([...localBranches, ...remoteBranches]),1796 dedupeRemoteBranchesWithLocalMatches([...localBranches, ...remoteBranches]),
1788 input.query,1797 input.query,
1789 ),1798 ),
1790 cursor: input.cursor,1799 cursor: input.cursor,
1791 limit: input.limit,1800 limit: input.limit,
1792 });1801 });
1793 1802
1794 return {1803 return {
1795 branches: [...branches.branches],1804 branches: [...branches.branches],
1796 isRepo: true,1805 isRepo: true,
1797 hasOriginRemote: remoteNames.includes("origin"),1806 hasOriginRemote: remoteNames.includes("origin"),
1798 nextCursor: branches.nextCursor,1807 nextCursor: branches.nextCursor,
1799 totalCount: branches.totalCount,1808 totalCount: branches.totalCount,
1800 };1809 };
1801 });1810 },
1811 );
1802 1812
1803 const createWorktree: GitCoreShape["createWorktree"] = Effect.fn("createWorktree")(1813 const createWorktree: GitVcsDriverShape["createWorktree"] = Effect.fn("createWorktree")(
1804 function* (input) {1814 function* (input) {
1805 const targetBranch = input.newBranch ?? input.branch;1815 const targetBranch = input.newBranch ?? input.branch;
1806 const sanitizedBranch = targetBranch.replace(/\//g, "-");1816 const sanitizedBranch = targetBranch.replace(/\//g, "-");
@@ -1810,7 +1820,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 3 unmodified lines
1810 ? ["worktree", "add", "-b", input.newBranch, worktreePath, input.branch]1820 ? ["worktree", "add", "-b", input.newBranch, worktreePath, input.branch]
1811 : ["worktree", "add", worktreePath, input.branch];1821 : ["worktree", "add", worktreePath, input.branch];
1812 1822
1813 yield* executeGit("GitCore.createWorktree", input.cwd, args, {1823 yield* executeGit("GitVcsDriver.createWorktree", input.cwd, args, {
1814 fallbackErrorMessage: "git worktree add failed",1824 fallbackErrorMessage: "git worktree add failed",
1815 });1825 });
1816 1826
@@ -1823,12 +1833,12 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 6 unmodified lines
1823 },1833 },
1824 );1834 );
1825 1835
1826 const fetchPullRequestBranch: GitCoreShape["fetchPullRequestBranch"] = Effect.fn(1836 const fetchPullRequestBranch: GitVcsDriverShape["fetchPullRequestBranch"] = Effect.fn(
1827 "fetchPullRequestBranch",1837 "fetchPullRequestBranch",
1828 )(function* (input) {1838 )(function* (input) {
1829 const remoteName = yield* resolvePrimaryRemoteName(input.cwd);1839 const remoteName = yield* resolvePrimaryRemoteName(input.cwd);
1830 yield* executeGit(1840 yield* executeGit(
1831 "GitCore.fetchPullRequestBranch",1841 "GitVcsDriver.fetchPullRequestBranch",
1832 input.cwd,1842 input.cwd,
1833 [1843 [
1834 "fetch",1844 "fetch",
@@ -1843,9 +1853,9 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 8 unmodified lines
1843 );1853 );
1844 });1854 });
1845 1855
1846 const fetchRemoteBranch: GitCoreShape["fetchRemoteBranch"] = Effect.fn("fetchRemoteBranch")(1856 const fetchRemoteBranch: GitVcsDriverShape["fetchRemoteBranch"] = Effect.fn("fetchRemoteBranch")(
1847 function* (input) {1857 function* (input) {
1848 yield* runGit("GitCore.fetchRemoteBranch.fetch", input.cwd, [1858 yield* runGit("GitVcsDriver.fetchRemoteBranch.fetch", input.cwd, [
1849 "fetch",1859 "fetch",
1850 "--quiet",1860 "--quiet",
1851 "--no-tags",1861 "--no-tags",
@@ -1856,7 +1866,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 4 unmodified lines
1856 const localBranchAlreadyExists = yield* branchExists(input.cwd, input.localBranch);1866 const localBranchAlreadyExists = yield* branchExists(input.cwd, input.localBranch);
1857 const targetRef = `${input.remoteName}/${input.remoteBranch}`;1867 const targetRef = `${input.remoteName}/${input.remoteBranch}`;
1858 yield* runGit(1868 yield* runGit(
1859 "GitCore.fetchRemoteBranch.materialize",1869 "GitVcsDriver.fetchRemoteBranch.materialize",
1860 input.cwd,1870 input.cwd,
1861 localBranchAlreadyExists1871 localBranchAlreadyExists
1862 ? ["branch", "--force", input.localBranch, targetRef]1872 ? ["branch", "--force", input.localBranch, targetRef]
@@ -1865,28 +1875,28 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
1865 },1875 },
1866 );1876 );
1867 1877
1868 const setBranchUpstream: GitCoreShape["setBranchUpstream"] = (input) =>1878 const setBranchUpstream: GitVcsDriverShape["setBranchUpstream"] = (input) =>
1869 runGit("GitCore.setBranchUpstream", input.cwd, [1879 runGit("GitVcsDriver.setBranchUpstream", input.cwd, [
1870 "branch",1880 "branch",
1871 "--set-upstream-to",1881 "--set-upstream-to",
1872 `${input.remoteName}/${input.remoteBranch}`,1882 `${input.remoteName}/${input.remoteBranch}`,
1873 input.branch,1883 input.branch,
1874 ]);1884 ]);
1875 1885
1876 const removeWorktree: GitCoreShape["removeWorktree"] = Effect.fn("removeWorktree")(1886 const removeWorktree: GitVcsDriverShape["removeWorktree"] = Effect.fn("removeWorktree")(
1877 function* (input) {1887 function* (input) {
1878 const args = ["worktree", "remove"];1888 const args = ["worktree", "remove"];
1879 if (input.force) {1889 if (input.force) {
1880 args.push("--force");1890 args.push("--force");
1881 }1891 }
1882 args.push(input.path);1892 args.push(input.path);
1883 yield* executeGit("GitCore.removeWorktree", input.cwd, args, {1893 yield* executeGit("GitVcsDriver.removeWorktree", input.cwd, args, {
1884 timeoutMs: 15_000,1894 timeoutMs: 15_000,
1885 fallbackErrorMessage: "git worktree remove failed",1895 fallbackErrorMessage: "git worktree remove failed",
1886 }).pipe(1896 }).pipe(
1887 Effect.mapError((error) =>1897 Effect.mapError((error) =>
1888 createGitCommandError(1898 createGitCommandError(
1889 "GitCore.removeWorktree",1899 "GitVcsDriver.removeWorktree",
1890 input.cwd,1900 input.cwd,
1891 args,1901 args,
1892 `${commandLabel(args)} failed (cwd: ${input.cwd}): ${error.message}`,1902 `${commandLabel(args)} failed (cwd: ${input.cwd}): ${error.message}`,
@@ -1897,31 +1907,33 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 4 unmodified lines
1897 },1907 },
1898 );1908 );
1899 1909
1900 const renameBranch: GitCoreShape["renameBranch"] = Effect.fn("renameBranch")(function* (input) {1910 const renameBranch: GitVcsDriverShape["renameBranch"] = Effect.fn("renameBranch")(
1901 if (input.oldBranch === input.newBranch) {1911 function* (input) {
1902 return { branch: input.newBranch };1912 if (input.oldBranch === input.newBranch) {
1903 }1913 return { branch: input.newBranch };
1904 const targetBranch = yield* resolveAvailableBranchName(input.cwd, input.newBranch);1914 }
1915 const targetBranch = yield* resolveAvailableBranchName(input.cwd, input.newBranch);
1905 1916
1906 yield* executeGit(1917 yield* executeGit(
1907 "GitCore.renameBranch",1918 "GitVcsDriver.renameBranch",
1908 input.cwd,1919 input.cwd,
1909 ["branch", "-m", "--", input.oldBranch, targetBranch],1920 ["branch", "-m", "--", input.oldBranch, targetBranch],
1910 {1921 {
1911 timeoutMs: 10_000,1922 timeoutMs: 10_000,
1912 fallbackErrorMessage: "git branch rename failed",1923 fallbackErrorMessage: "git branch rename failed",
1913 },1924 },
1914 );1925 );
1915 1926
1916 return { branch: targetBranch };1927 return { branch: targetBranch };
1917 });1928 },
1929 );
1918 1930
1919 const checkoutBranch: GitCoreShape["checkoutBranch"] = Effect.fn("checkoutBranch")(1931 const checkoutBranch: GitVcsDriverShape["checkoutBranch"] = Effect.fn("checkoutBranch")(
1920 function* (input) {1932 function* (input) {
1921 const [localInputExists, remoteExists] = yield* Effect.all(1933 const [localInputExists, remoteExists] = yield* Effect.all(
1922 [1934 [
1923 executeGit(1935 executeGit(
1924 "GitCore.checkoutBranch.localInputExists",1936 "GitVcsDriver.checkoutBranch.localInputExists",
1925 input.cwd,1937 input.cwd,
1926 ["show-ref", "--verify", "--quiet", `refs/heads/${input.branch}`],1938 ["show-ref", "--verify", "--quiet", `refs/heads/${input.branch}`],
1927 {1939 {
@@ -1930,7 +1942,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
1930 },1942 },
1931 ).pipe(Effect.map((result) => result.code === 0)),1943 ).pipe(Effect.map((result) => result.code === 0)),
1932 executeGit(1944 executeGit(
1933 "GitCore.checkoutBranch.remoteExists",1945 "GitVcsDriver.checkoutBranch.remoteExists",
1934 input.cwd,1946 input.cwd,
1935 ["show-ref", "--verify", "--quiet", `refs/remotes/${input.branch}`],1947 ["show-ref", "--verify", "--quiet", `refs/remotes/${input.branch}`],
1936 {1948 {
@@ -1944,7 +1956,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 7 unmodified lines
1944 1956
1945 const localTrackingBranch = remoteExists1957 const localTrackingBranch = remoteExists
1946 ? yield* executeGit(1958 ? yield* executeGit(
1947 "GitCore.checkoutBranch.localTrackingBranch",1959 "GitVcsDriver.checkoutBranch.localTrackingBranch",
1948 input.cwd,1960 input.cwd,
1949 ["for-each-ref", "--format=%(refname:short)\t%(upstream:short)", "refs/heads"],1961 ["for-each-ref", "--format=%(refname:short)\t%(upstream:short)", "refs/heads"],
1950 {1962 {
@@ -1964,7 +1976,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 13 unmodified lines
1964 const localTrackedBranchTargetExists =1976 const localTrackedBranchTargetExists =
1965 remoteExists && localTrackedBranchCandidate1977 remoteExists && localTrackedBranchCandidate
1966 ? yield* executeGit(1978 ? yield* executeGit(
1967 "GitCore.checkoutBranch.localTrackedBranchTargetExists",1979 "GitVcsDriver.checkoutBranch.localTrackedBranchTargetExists",
1968 input.cwd,1980 input.cwd,
1969 ["show-ref", "--verify", "--quiet", `refs/heads/${localTrackedBranchCandidate}`],1981 ["show-ref", "--verify", "--quiet", `refs/heads/${localTrackedBranchCandidate}`],
1970 {1982 {
@@ -1984,12 +1996,12 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 13 unmodified lines
1984 ? ["checkout", localTrackingBranch]1996 ? ["checkout", localTrackingBranch]
1985 : ["checkout", input.branch];1997 : ["checkout", input.branch];
1986 1998
1987 yield* executeGit("GitCore.checkoutBranch.checkout", input.cwd, checkoutArgs, {1999 yield* executeGit("GitVcsDriver.checkoutBranch.checkout", input.cwd, checkoutArgs, {
1988 timeoutMs: 10_000,2000 timeoutMs: 10_000,
1989 fallbackErrorMessage: "git checkout failed",2001 fallbackErrorMessage: "git checkout failed",
1990 });2002 });
1991 2003
1992 const branch = yield* runGitStdout("GitCore.checkoutBranch.currentBranch", input.cwd, [2004 const branch = yield* runGitStdout("GitVcsDriver.checkoutBranch.currentBranch", input.cwd, [
1993 "branch",2005 "branch",
1994 "--show-current",2006 "--show-current",
1995 ]).pipe(Effect.map((stdout) => stdout.trim() || null));2007 ]).pipe(Effect.map((stdout) => stdout.trim() || null));
@@ -1998,26 +2010,28 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 2 unmodified lines
1998 },2010 },
1999 );2011 );
2000 2012
2001 const createBranch: GitCoreShape["createBranch"] = Effect.fn("createBranch")(function* (input) {2013 const createBranch: GitVcsDriverShape["createBranch"] = Effect.fn("createBranch")(
2002 yield* executeGit("GitCore.createBranch", input.cwd, ["branch", input.branch], {2014 function* (input) {
2003 timeoutMs: 10_000,2015 yield* executeGit("GitVcsDriver.createBranch", input.cwd, ["branch", input.branch], {
2004 fallbackErrorMessage: "git branch create failed",2016 timeoutMs: 10_000,
2005 });2017 fallbackErrorMessage: "git branch create failed",
2006 if (input.checkout) {2018 });
2007 yield* checkoutBranch({ cwd: input.cwd, branch: input.branch });2019 if (input.checkout) {
2008 }2020 yield* checkoutBranch({ cwd: input.cwd, branch: input.branch });
2021 }
2009 2022
2010 return { branch: input.branch };2023 return { branch: input.branch };
2011 });2024 },
2025 );
2012 2026
2013 const initRepo: GitCoreShape["initRepo"] = (input) =>2027 const initRepo: GitVcsDriverShape["initRepo"] = (input) =>
2014 executeGit("GitCore.initRepo", input.cwd, ["init"], {2028 executeGit("GitVcsDriver.initRepo", input.cwd, ["init"], {
2015 timeoutMs: 10_000,2029 timeoutMs: 10_000,
2016 fallbackErrorMessage: "git init failed",2030 fallbackErrorMessage: "git init failed",
2017 }).pipe(Effect.asVoid);2031 }).pipe(Effect.asVoid);
2018 2032
2019 const listLocalBranchNames: GitCoreShape["listLocalBranchNames"] = (cwd) =>2033 const listLocalBranchNames: GitVcsDriverShape["listLocalBranchNames"] = (cwd) =>
2020 runGitStdout("GitCore.listLocalBranchNames", cwd, [2034 runGitStdout("GitVcsDriver.listLocalBranchNames", cwd, [
2021 "branch",2035 "branch",
2022 "--list",2036 "--list",
2023 "--no-column",2037 "--no-column",
@@ -2054,7 +2068,5 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: { · 30 unmodified lines
2054 checkoutBranch,2068 checkoutBranch,
2055 initRepo,2069 initRepo,
2056 listLocalBranchNames,2070 listLocalBranchNames,
2057 } satisfies GitCoreShape;2071 } satisfies GitVcsDriverShape;
2058});2072});
2059
2060export const GitCoreLive = Layer.effect(GitCore, makeGitCore());