Skip to content

Commit 809911b

Browse files
committed
fix: Fixing Git clones
1 parent 77a282b commit 809911b

4 files changed

Lines changed: 74 additions & 28 deletions

File tree

electron/main/ipc/git.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import { Effect, Stream } from "effect"
99
import { ipcMain } from "electron"
1010
import { runtime, sessionManager } from "./runtime.ts"
11+
import { ProcessSpawner } from "../../../src/services/ProcessSpawner.ts"
1112
import {
12-
cloneRepository,
1313
resolveClonePaths,
1414
countFiles,
1515
deleteBranch,
@@ -19,6 +19,8 @@ import {
1919
} from "../../../src/domain/git/operations.ts"
2020
import type { CloneOptions, PushOptions } from "../../../src/services/GitClient.ts"
2121
import { isContainedIn } from "../../../src/path-validation.ts"
22+
import * as fs from "node:fs"
23+
function debugLog(msg: string) { fs.appendFileSync("/tmp/runbooks-git-debug.log", new Date().toISOString() + " " + msg + "\n") }
2224
import { PathTraversalError } from "../../../src/errors/index.ts"
2325
import { validateSessionPath } from "./path-guard.ts"
2426

@@ -35,6 +37,7 @@ export function registerGitHandlers(): void {
3537
},
3638
) => {
3739
return runtime.runPromise(
40+
Effect.scoped(
3841
Effect.gen(function* () {
3942
// Resolve clone destination paths
4043
const session = yield* sessionManager.getSession()
@@ -59,35 +62,64 @@ export function registerGitHandlers(): void {
5962
token: params.credentials?.token,
6063
}
6164

62-
// Get the progress stream
63-
const progressStream = yield* cloneRepository(
64-
params.url,
65-
paths.absolutePath,
66-
options,
67-
)
65+
// Clone the repository using direct process spawning.
66+
// We avoid the GitClient's stream-based API because
67+
// Stream.runCollect hangs in Electron's runtime.runPromise.
68+
const spawner = yield* ProcessSpawner
69+
const cloneArgs = ["clone", "--progress"]
70+
if (options.ref) cloneArgs.push("--branch", options.ref)
71+
72+
const effectiveUrl = options.token
73+
? (() => { try { const u = new URL(params.url); u.username = "x-access-token"; u.password = options.token!; return u.toString(); } catch { return params.url; } })()
74+
: params.url
6875

69-
// Stream progress events to the renderer
70-
yield* Stream.runForEach(progressStream, (progress) =>
76+
cloneArgs.push(effectiveUrl, paths.absolutePath)
77+
78+
// debugLog("[git:clone] spawning git process...")
79+
const proc = yield* spawner.spawn("git", cloneArgs, {})
80+
81+
// debugLog("[git:clone] draining output stream...")
82+
yield* Stream.runForEach(proc.output, (line) =>
7183
Effect.sync(() => {
7284
event.sender.send("git:clone-progress", {
73-
line: progress.line,
74-
timestamp: progress.timestamp,
85+
line: line.line,
86+
timestamp: new Date().toISOString(),
7587
})
7688
}),
7789
)
7890

79-
// Count files in the cloned repo
91+
// debugLog("[git:clone] getting exit code...")
92+
const exitCode = yield* proc.exitCode
93+
// debugLog("[git:clone] exit code: " + exitCode)
94+
if (exitCode !== 0) {
95+
return yield* Effect.fail(
96+
new PathTraversalError({
97+
path: paths.absolutePath,
98+
message: `git clone failed with exit code ${exitCode}`,
99+
}),
100+
)
101+
}
102+
103+
event.sender.send("git:clone-progress", {
104+
line: "Clone complete. Counting files...",
105+
timestamp: new Date().toISOString(),
106+
})
107+
108+
// Count tracked files using `git ls-files` (fast, ~10ms)
80109
const fileCount = yield* countFiles(paths.absolutePath)
81110

82111
// Register the worktree path
83112
sessionManager.registerWorkTreePath(paths.absolutePath)
113+
// debugLog("[git:clone] registered worktree, returning result")
84114

85115
return {
86116
absolutePath: paths.absolutePath,
87117
relativePath: paths.relativePath,
88118
fileCount,
119+
status: "success" as const,
89120
}
90121
}),
122+
),
91123
)
92124
},
93125
)

electron/main/ipc/runbook.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
runtime,
1111
runbookConfig,
1212
executableRegistry,
13+
sessionManager,
1314
setExecutableRegistry,
1415
setRunbookConfig,
1516
} from "./runtime.ts"
@@ -45,6 +46,12 @@ export function registerRunbookHandlers(): void {
4546
}
4647
setRunbookConfig(config)
4748

49+
// Update the session's working directory to the runbook's parent dir.
50+
// The session may have been created with '.' before the runbook path
51+
// was known.
52+
const runbookDir = path.dirname(runbookPath)
53+
sessionManager.setWorkingDir(runbookDir)
54+
4855
// Read the runbook file content
4956
const fileData = await runtime.runPromise(readFileMetadata(runbookPath))
5057

src/domain/git/operations.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Port of api/git_clone.go and api/github_pull_request.go.
44
*/
55
import path from "path"
6-
import { Effect, Stream } from "effect"
6+
import { Effect, Stream, Chunk } from "effect"
77
import { GitClient } from "../../services/GitClient.ts"
88
import type {
99
CloneOptions,
@@ -12,6 +12,7 @@ import type {
1212
import { GitHubClient } from "../../services/GitHubClient.ts"
1313
import type { CreatePRParams } from "../../services/GitHubClient.ts"
1414
import { FileSystem } from "../../services/FileSystem.ts"
15+
import { ProcessSpawner } from "../../services/ProcessSpawner.ts"
1516
import { GitError } from "../../errors/index.ts"
1617

1718
// ---------------------------------------------------------------------------
@@ -226,24 +227,19 @@ export const resolveClonePaths = (
226227
// ---------------------------------------------------------------------------
227228

228229
/**
229-
* Count files in a directory, excluding the .git directory.
230+
* Count tracked files in a git repository using `git ls-files`.
231+
* Falls back to 0 if the command fails (e.g., not a git repo).
230232
*/
231233
export const countFiles = (dir: string) =>
232234
Effect.gen(function* () {
233-
const fs = yield* FileSystem
234-
235-
let count = 0
236-
yield* fs.walk(dir).pipe(
237-
Stream.filter((entry) => entry.isFile && !entry.relativePath.startsWith(".git/") && entry.relativePath !== ".git"),
238-
Stream.runForEach(() =>
239-
Effect.sync(() => {
240-
count++
241-
}),
242-
),
243-
)
244-
245-
return count
246-
})
235+
const spawner = yield* ProcessSpawner
236+
const proc = yield* spawner.spawn("git", ["ls-files"], { cwd: dir })
237+
const chunks = yield* Stream.runCollect(proc.output)
238+
const lines = Chunk.toArray(chunks)
239+
const code = yield* proc.exitCode
240+
if (code !== 0) return 0
241+
return lines.filter((l) => l.source === "stdout" && l.line.trim() !== "").length
242+
}).pipe(Effect.catchAll(() => Effect.succeed(0)))
247243

248244
// ---------------------------------------------------------------------------
249245
// URL Parsing

src/domain/session/manager.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,17 @@ export class SessionManager {
221221
return this.session !== null
222222
}
223223

224+
/**
225+
* Update the session's working directory.
226+
* Called when the runbook loads and we know the actual path.
227+
*/
228+
setWorkingDir(dir: string): void {
229+
if (this.session) {
230+
this.session.workingDir = dir
231+
this.session.initialWorkDir = dir
232+
}
233+
}
234+
224235
/**
225236
* Reset the session to its initial environment and working directory.
226237
*/

0 commit comments

Comments
 (0)