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
11 changes: 9 additions & 2 deletions src/services/checkpoints/ShadowCheckpointService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import pWaitFor from "p-wait-for"
import * as vscode from "vscode"

import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual } from "../../utils/path"
import { executeRipgrep } from "../../services/search/file-search"
import { t } from "../../i18n"

Expand Down Expand Up @@ -155,9 +156,15 @@ export abstract class ShadowCheckpointService extends EventEmitter {
this.log(`[${this.constructor.name}#initShadowGit] shadow git repo already exists at ${this.dotGitDir}`)
const worktree = await this.getShadowGitConfigWorktree(git)

if (worktree !== this.workspaceDir) {
if (!worktree) {
throw new Error("Checkpoints require core.worktree to be set in the shadow git config")
}

const worktreeTrimmed = worktree.trim()

if (!arePathsEqual(worktreeTrimmed, this.workspaceDir)) {
throw new Error(
`Checkpoints can only be used in the original workspace: ${worktree} !== ${this.workspaceDir}`,
`Checkpoints can only be used in the original workspace: ${worktreeTrimmed} !== ${this.workspaceDir}`,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -915,3 +915,81 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
})
},
)

describe("worktree path comparison", () => {
it("accepts core.worktree with trailing newline from git output", async () => {
const shadowDir = path.join(tmpDir, `worktree-trim-${Date.now()}`)
const workspaceDir = path.join(tmpDir, `workspace-trim-${Date.now()}`)

try {
await fs.mkdir(workspaceDir, { recursive: true })
const mainGit = simpleGit(workspaceDir)
await mainGit.init()
await mainGit.addConfig("user.name", "Roo Code")
await mainGit.addConfig("user.email", "support@roocode.com")

await fs.writeFile(path.join(workspaceDir, "main.txt"), "main content")
await mainGit.add("main.txt")
await mainGit.commit("Initial commit")

vitest.spyOn(fileSearch, "executeRipgrep").mockImplementation(() => {
return Promise.resolve([])
})

// First init to create the shadow repo
const service1 = new RepoPerTaskCheckpointService("trim-test", shadowDir, workspaceDir, () => {})
await service1.initShadowGit()

// Second init with stubbed worktree returning a trailing newline
const service2 = new RepoPerTaskCheckpointService("trim-test-2", shadowDir, workspaceDir, () => {})
vitest
.spyOn(service2 as any, "getShadowGitConfigWorktree")
.mockResolvedValue(workspaceDir + "\n")

await service2.initShadowGit()
} finally {
vitest.restoreAllMocks()
await fs.rm(shadowDir, { recursive: true, force: true })
await fs.rm(workspaceDir, { recursive: true, force: true })
}
})

it("throws when core.worktree is missing", async () => {
const shadowDir = path.join(tmpDir, `worktree-missing-${Date.now()}`)
const workspaceDir = path.join(tmpDir, `workspace-missing-${Date.now()}`)

try {
await fs.mkdir(workspaceDir, { recursive: true })
const mainGit = simpleGit(workspaceDir)
await mainGit.init()
await mainGit.addConfig("user.name", "Roo Code")
await mainGit.addConfig("user.email", "support@roocode.com")

await fs.writeFile(path.join(workspaceDir, "main.txt"), "main content")
await mainGit.add("main.txt")
await mainGit.commit("Initial commit")

vitest.spyOn(fileSearch, "executeRipgrep").mockImplementation(() => {
return Promise.resolve([])
})

// First init to create the shadow repo
const service1 = new RepoPerTaskCheckpointService("missing-test", shadowDir, workspaceDir, () => {})
await service1.initShadowGit()

// Remove core.worktree from the shadow git config
const shadowGit = simpleGit(shadowDir)
await shadowGit.raw(["config", "--unset", "core.worktree"])

// Second init should throw because core.worktree is missing
const service2 = new RepoPerTaskCheckpointService("missing-test-2", shadowDir, workspaceDir, () => {})
await expect(service2.initShadowGit()).rejects.toThrowError(
/core\.worktree to be set/,
)
} finally {
vitest.restoreAllMocks()
await fs.rm(shadowDir, { recursive: true, force: true })
await fs.rm(workspaceDir, { recursive: true, force: true })
}
})
})
Loading