Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/fix-pr-branch-refspec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Fix Agent Manager PR and base-branch worktrees when a repository uses a restrictive Git fetch refspec.
21 changes: 18 additions & 3 deletions packages/kilo-vscode/src/agent-manager/WorktreeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,8 @@ export class WorktreeManager {
private async refreshBase(branch: string, requested?: string): Promise<void> {
const remote = requested ?? (await this.resolveRemote())
if (!remote) return
validateGitRef(remote, "remote")
validateGitRef(branch, "branch")
const key = `${this.root}:${remote}:${branch}`
const cached = WorktreeManager.fetchCache.get(key)
if (cached && Date.now() - cached < WorktreeManager.FETCH_CACHE_TTL) return
Expand All @@ -849,7 +851,7 @@ export class WorktreeManager {
const env = nonInteractiveEnv()
await simpleGit(this.root, { unsafe: { allowUnsafeSshCommand: isKiloOwnedSshCommand(env) } })
.env(env)
.fetch(remote, branch, { "--quiet": null, "--no-tags": null })
.raw(["fetch", "--quiet", "--no-tags", remote, `+refs/heads/${branch}:refs/remotes/${remote}/${branch}`])
WorktreeManager.fetchCache.set(key, Date.now())
}

Expand Down Expand Up @@ -1107,17 +1109,30 @@ export class WorktreeManager {
if (!remotes.some((r) => r.name === forkOwner)) {
await this.git.addRemote(forkOwner, `https://github.com/${forkOwner}/${parsed.repo}.git`)
}
await this.gitExec(["fetch", forkOwner, info.headRefName])
await this.gitExec([
"fetch",
"--quiet",
"--no-tags",
forkOwner,
`+refs/heads/${info.headRefName}:refs/remotes/${forkOwner}/${info.headRefName}`,
])
} else {
validateGitRef(info.headRefName, "branch name")
const ok = await this.gitTry(["fetch", "origin", info.headRefName])
const ref = `+refs/heads/${info.headRefName}:refs/remotes/origin/${info.headRefName}`
const ok = await this.gitTry(["fetch", "--quiet", "--no-tags", "origin", ref])
if (!ok) {
await this.gitExec([
"fetch",
"origin",
`+refs/pull/${parsed.number}/head:refs/remotes/origin/${info.headRefName}`,
])
}
if (!(await this.gitTry(["show-ref", "--verify", "--quiet", `refs/heads/${info.headRefName}`]))) {
const ref = `refs/remotes/origin/${info.headRefName}`
await this.gitExec(["branch", info.headRefName, ref])
await this.gitExec(["config", `branch.${info.headRefName}.remote`, "origin"])
await this.gitExec(["config", `branch.${info.headRefName}.merge`, `refs/heads/${info.headRefName}`])
Comment thread
marius-kilocode marked this conversation as resolved.
Resolved
}
}
}

Expand Down
53 changes: 53 additions & 0 deletions packages/kilo-vscode/tests/unit/worktree-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
versionedName,
} from "../../src/agent-manager/branch-name"
import { WorktreeStateManager } from "../../src/agent-manager/WorktreeStateManager"
import type { PRInfo } from "../../src/agent-manager/git-import"
import simpleGit from "simple-git"

// Each test gets its own temp directory -- no shared state, safe to run in parallel.
Expand Down Expand Up @@ -1138,6 +1139,58 @@ describe("WorktreeManager.createWorktree advanced", () => {
const devParams = await git.log(["-1"])
expect(headParams.latest?.hash).toBe(devParams.latest?.hash)
})

it("creates from a base branch excluded by the remote fetch refspec", async () => {
const { clone } = await createTempRepoWithOrigin()
const git = simpleGit(clone)
await git.checkoutLocalBranch("topic")
await fs.writeFile(path.join(clone, "topic.txt"), "topic")
await git.add(".")
await git.commit("topic commit")
await git.push("origin", "topic")
await git.checkout("main")

await git.raw(["config", "remote.origin.fetch", "+refs/heads/main:refs/remotes/origin/main"])
await git.raw(["update-ref", "-d", "refs/remotes/origin/topic"])

const result = await createManager(clone).createWorktree({ baseBranch: "topic", prompt: "from topic" })
const remoteHead = (await git.revparse(["refs/remotes/origin/topic"])).trim()
const worktreeHead = (await simpleGit(result.path).revparse(["HEAD"])).trim()

expect(worktreeHead).toBe(remoteHead)
expect(result.parentBranch).toBe("topic")
})

it("creates from a same-repository PR branch excluded by the remote fetch refspec", async () => {
const { clone } = await createTempRepoWithOrigin()
const git = simpleGit(clone)
await git.checkoutLocalBranch("topic")
await fs.writeFile(path.join(clone, "topic.txt"), "topic")
await git.add(".")
await git.commit("topic commit")
await git.push("origin", "topic")
await git.checkout("main")
await git.raw(["config", "remote.origin.fetch", "+refs/heads/main:refs/remotes/origin/main"])
await git.raw(["update-ref", "-d", "refs/remotes/origin/topic"])
await git.branch(["-D", "topic"])

const manager = createManager(clone)
const internal = manager as unknown as {
fetchPRInfo: (parsed: { owner: string; repo: string; number: number }) => Promise<PRInfo>
}
internal.fetchPRInfo = async () => ({
headRefName: "topic",
isCrossRepository: false,
title: "Topic PR",
})

const result = await manager.createFromPR("https://github.com/org/repo/pull/1")
const remoteHead = (await git.revparse(["refs/remotes/origin/topic"])).trim()
const worktreeHead = (await simpleGit(result.path).revparse(["HEAD"])).trim()

expect(worktreeHead).toBe(remoteHead)
expect(result.parentBranch).toBe("topic")
})
})

// ---------------------------------------------------------------------------
Expand Down
Loading