Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/agent-manager-subdir-worktree.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion packages/kilo-vscode/src/agent-manager/WorktreeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1349,7 +1349,7 @@ export class WorktreeManager {

async repoUsesLfs(): Promise<boolean> {
// 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
Expand Down
30 changes: 16 additions & 14 deletions packages/kilo-vscode/src/agent-manager/constants.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -113,21 +115,21 @@ async function isDirectory(filepath: string): Promise<boolean> {
}

/**
* 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<string> {
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<string> {
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)
}

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/kilo-vscode/tests/unit/worktree-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading