Skip to content
Draft
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
18 changes: 18 additions & 0 deletions src/services/checkpoints/ShadowCheckpointService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", "[email protected]")
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", "[email protected]")
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`, () => {
Expand Down
Loading