fix: allow checkpoints when workspace root has no .git but subdirs have git repos

This fixes issue #10125 where users could not enable checkpoints when
opening a parent folder containing multiple sibling projects (e.g.,
frontend and backend), each with their own git repositories.

The previous behavior blocked checkpoints whenever any nested .git
directory was detected. This change modifies the detection logic to:

1. Check if the workspace root has its own .git directory first
2. If the root has NO .git, allow checkpoints since subdirectory git
   repos are independent projects (not truly nested)
3. Only block checkpoints when the root HAS a .git AND there are nested
   .git directories (true nesting scenario like git submodules)

The subdirectory .git folders are already excluded from tracking via
the existing .git/ pattern in the exclude patterns.

Closes #10125
This commit is contained in:
Roo Code 2025-12-16 12:58:05 +00:00
parent 596783d365
commit ae669364a9
2 changed files with 77 additions and 0 deletions

View file

@ -221,6 +221,24 @@ export abstract class ShadowCheckpointService extends EventEmitter {
private async getNestedGitRepository(): Promise<string | null> {
try {
// First, check if the workspace root has its own .git directory.
// If the root does NOT have a .git, we allow checkpoints even if subdirectories
// have their own git repos. This supports the common use case where users open
// a parent folder containing multiple sibling projects (e.g., frontend and backend),
// each with their own git repository.
const rootGitPath = path.join(this.workspaceDir, ".git")
const rootHasGit = await fileExistsAtPath(rootGitPath)
if (!rootHasGit) {
this.log(
`[${this.constructor.name}#getNestedGitRepository] workspace root has no .git, allowing checkpoints even with subdirectory git repos`,
)
// No root .git means subdirectory git repos are independent projects, not nested repos.
// The shadow repo will exclude them via .git/ pattern in excludes.
return null
}
// Root has a .git, so we need to check for truly nested git repos (submodules, etc.)
// Find all .git/HEAD files that are not at the root level.
const args = ["--files", "--hidden", "--follow", "-g", "**/.git/HEAD", this.workspaceDir]

View file

@ -483,6 +483,65 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
await fs.rm(shadowDir, { recursive: true, force: true })
await fs.rm(workspaceDir, { recursive: true, force: true })
})
it("succeeds when workspace root has no .git but subdirectories have git repos (sibling projects)", async () => {
// Create a new temporary workspace and service for this test.
// This simulates a user opening a parent folder containing multiple sibling projects.
const shadowDir = path.join(tmpDir, `${prefix}-sibling-projects-${Date.now()}`)
const workspaceDir = path.join(tmpDir, `workspace-sibling-projects-${Date.now()}`)
// Create the parent workspace WITHOUT initializing git (no root .git)
await fs.mkdir(workspaceDir, { recursive: true })
// Create a "frontend" project with its own git repo
const frontendDir = path.join(workspaceDir, "frontend")
await fs.mkdir(frontendDir, { recursive: true })
const frontendGit = simpleGit(frontendDir)
await frontendGit.init()
await frontendGit.addConfig("user.name", "Roo Code")
await frontendGit.addConfig("user.email", "support@roocode.com")
const frontendFile = path.join(frontendDir, "index.js")
await fs.writeFile(frontendFile, "// Frontend code")
await frontendGit.add(".")
await frontendGit.commit("Initial frontend commit")
// Create a "backend" project with its own git repo
const backendDir = path.join(workspaceDir, "backend")
await fs.mkdir(backendDir, { recursive: true })
const backendGit = simpleGit(backendDir)
await backendGit.init()
await backendGit.addConfig("user.name", "Roo Code")
await backendGit.addConfig("user.email", "support@roocode.com")
const backendFile = path.join(backendDir, "server.js")
await fs.writeFile(backendFile, "// Backend code")
await backendGit.add(".")
await backendGit.commit("Initial backend commit")
// Create a test file in the root (outside any git repo)
const rootFile = path.join(workspaceDir, "README.md")
await fs.writeFile(rootFile, "# Project Documentation")
const service = new klass(taskId, shadowDir, workspaceDir, () => {})
// Initialization should succeed because root has no .git
// Even though subdirectories have their own git repos, they are independent projects
await expect(service.initShadowGit()).resolves.not.toThrow()
expect(service.isInitialized).toBe(true)
// Verify checkpoints work correctly
await fs.writeFile(rootFile, "# Updated Documentation")
const checkpoint = await service.saveCheckpoint("Update readme")
expect(checkpoint?.commit).toBeTruthy()
// Verify we can restore
await fs.writeFile(rootFile, "# Modified again")
await service.restoreCheckpoint(checkpoint!.commit)
expect(await fs.readFile(rootFile, "utf-8")).toBe("# Updated Documentation")
// Clean up.
await fs.rm(shadowDir, { recursive: true, force: true })
await fs.rm(workspaceDir, { recursive: true, force: true })
})
})
describe(`${klass.name}#events`, () => {