diff --git a/.changeset/agent-manager-subdir-worktree.md b/.changeset/agent-manager-subdir-worktree.md new file mode 100644 index 000000000000..36fbbd0ee547 --- /dev/null +++ b/.changeset/agent-manager-subdir-worktree.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix Agent Manager worktree creation when a VS Code window is opened on a subdirectory of a git repository. diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts index d81905205c18..833b6a3a35db 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts @@ -1349,7 +1349,7 @@ export class WorktreeManager { async repoUsesLfs(): Promise { // Check .git/lfs/ directory - const gitDir = await resolveGitDir(this.root) + const gitDir = await resolveGitDir(this.root, this.binary) if (fs.existsSync(path.join(gitDir, "lfs"))) return true // Check .gitattributes diff --git a/packages/kilo-vscode/src/agent-manager/constants.ts b/packages/kilo-vscode/src/agent-manager/constants.ts index 01979bf7d17b..d66b590a7def 100644 --- a/packages/kilo-vscode/src/agent-manager/constants.ts +++ b/packages/kilo-vscode/src/agent-manager/constants.ts @@ -1,5 +1,7 @@ import * as fs from "node:fs" import * as path from "node:path" +import { BUDGET } from "./command-budget" +import { execWithShellEnv } from "./shell-env" // TODO: Remove the legacy .kilocode -> .kilo migration helpers below after the // GA release cleanup tracked in https://github.com/Kilo-Org/kilocode/issues/6986. @@ -113,21 +115,21 @@ async function isDirectory(filepath: string): Promise { } /** - * Resolve the real git directory for a repository root. - * When root/.git is a directory, returns it directly. - * When root/.git is a file (worktree), follows the gitdir pointer - * up two levels to reach the shared git directory. + * Resolve the shared git directory for `root`. + * + * `git rev-parse --git-common-dir` works for a repository root, a linked + * worktree, and a subdirectory of either. A stat of `root/.git` does not: + * a subdirectory workspace has no `.git` entry. The path may be relative to + * `root`. Unknown flags are rejected because older Git prints them and exits 0. */ -export async function resolveGitDir(root: string): Promise { - const gitPath = path.join(root, ".git") - const stat = await fs.promises.stat(gitPath) - if (stat.isDirectory()) return gitPath - - const content = await fs.promises.readFile(gitPath, "utf-8") - const match = content.match(/^gitdir:\s*(.+)$/m) - if (!match) throw new Error("Invalid .git file format") - // gitdir points to e.g. /repo/.git/worktrees/foo — go up two levels to /repo/.git - return path.resolve(path.dirname(gitPath), match[1].trim(), "..", "..") +export async function resolveGitDir(root: string, binary = "git"): Promise { + const { stdout } = await execWithShellEnv(binary, ["rev-parse", "--git-common-dir"], { + cwd: root, + timeout: BUDGET.probe, + }) + const dir = stdout.trim() + if (!dir || dir.startsWith("--")) throw new Error("git rev-parse did not return a git directory") + return path.resolve(root, dir) } /** diff --git a/packages/kilo-vscode/tests/unit/worktree-manager.test.ts b/packages/kilo-vscode/tests/unit/worktree-manager.test.ts index c6bc54527b27..0f8a9d49423c 100644 --- a/packages/kilo-vscode/tests/unit/worktree-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/worktree-manager.test.ts @@ -1510,6 +1510,25 @@ describe("WorktreeManager helpers", () => { await fs.mkdir(path.join(root, ".git", "lfs"), { recursive: true }) expect(await mgr.repoUsesLfs()).toBe(true) }) + + it("creates a worktree from a subdirectory of the repository", async () => { + const root = await createTempRepo() + const sub = path.join(root, "web") + await fs.mkdir(sub, { recursive: true }) + const lfs = path.join(root, ".git", "lfs") + await fs.mkdir(lfs, { recursive: true }) + const mgr = createManager(sub) + + expect(await mgr.repoUsesLfs()).toBe(true) + await fs.rm(lfs, { recursive: true, force: true }) + expect(await mgr.repoUsesLfs()).toBe(false) + + const result = await mgr.createWorktree({ prompt: "subdir" }) + expect((await fs.stat(path.join(result.path, ".git"))).isFile()).toBe(true) + expect(result.path.startsWith(sub + path.sep)).toBe(true) + const status = await simpleGit(root).raw(["status", "--porcelain", "--untracked-files=all"]) + expect(status.trim()).toBe("") + }) }) describe("WorktreeManager.resolveStartPoint", () => {