Skip to content

Commit a995d0e

Browse files
committed
fix: allow checkpoints with nested git repos by excluding them
- Modified ShadowCheckpointService to exclude nested git repos instead of disabling checkpoints - Added getNestedGitRepositories method to find all nested repos - Updated writeExcludeFile to add nested repos to exclude patterns - Added info messages to notify users when nested repos are excluded - Updated tests to reflect new behavior - Fixes #8433
1 parent f8ed7a7 commit a995d0e

File tree

3 files changed

+59
-31
lines changed

3 files changed

+59
-31
lines changed

src/i18n/locales/en/common.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@
142142
"image_copied_to_clipboard": "Image data URI copied to clipboard",
143143
"image_saved": "Image saved to {{path}}",
144144
"mode_exported": "Mode '{{mode}}' exported successfully",
145-
"mode_imported": "Mode imported successfully"
145+
"mode_imported": "Mode imported successfully",
146+
"nested_git_repo_excluded": "Nested git repository at '{{path}}' will be excluded from checkpoints",
147+
"nested_git_repos_excluded": "{{count}} nested git repositories will be excluded from checkpoints"
146148
},
147149
"answers": {
148150
"yes": "Yes",

src/services/checkpoints/ShadowCheckpointService.ts

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,6 @@ export abstract class ShadowCheckpointService extends EventEmitter {
7070
throw new Error("Shadow git repo already initialized")
7171
}
7272

73-
const nestedGitPath = await this.getNestedGitRepository()
74-
75-
if (nestedGitPath) {
76-
// Show persistent error message with the offending path
77-
const relativePath = path.relative(this.workspaceDir, nestedGitPath)
78-
const message = t("common:errors.nested_git_repos_warning", { path: relativePath })
79-
vscode.window.showErrorMessage(message)
80-
81-
throw new Error(
82-
`Checkpoints are disabled because a nested git repository was detected at: ${relativePath}. ` +
83-
"Please remove or relocate nested git repositories to use the checkpoints feature.",
84-
)
85-
}
86-
8773
await fs.mkdir(this.checkpointsDir, { recursive: true })
8874
const git = simpleGit(this.checkpointsDir)
8975
const gitVersion = await git.version()
@@ -102,6 +88,7 @@ export abstract class ShadowCheckpointService extends EventEmitter {
10288
)
10389
}
10490

91+
// Write exclude file which will include nested git repos
10592
await this.writeExcludeFile()
10693
this.baseHash = await git.revparse(["HEAD"])
10794
} else {
@@ -111,6 +98,7 @@ export abstract class ShadowCheckpointService extends EventEmitter {
11198
await git.addConfig("commit.gpgSign", "false") // Disable commit signing for shadow repo.
11299
await git.addConfig("user.name", "Roo Code")
113100
await git.addConfig("user.email", "[email protected]")
101+
// Write exclude file which will include nested git repos
114102
await this.writeExcludeFile()
115103
await this.stageAll(git)
116104
const { commit } = await git.commit("initial commit", { "--allow-empty": null })
@@ -147,6 +135,16 @@ export abstract class ShadowCheckpointService extends EventEmitter {
147135
protected async writeExcludeFile() {
148136
await fs.mkdir(path.join(this.dotGitDir, "info"), { recursive: true })
149137
const patterns = await getExcludePatterns(this.workspaceDir)
138+
139+
// Add nested git repositories to exclude patterns
140+
const nestedGitPaths = await this.getNestedGitRepositories()
141+
for (const gitPath of nestedGitPaths) {
142+
const relativePath = path.relative(this.workspaceDir, gitPath)
143+
// Add the directory and all its contents to exclude patterns
144+
patterns.push(relativePath + "/")
145+
this.log(`[${this.constructor.name}#writeExcludeFile] excluding nested git repo: ${relativePath}`)
146+
}
147+
150148
await fs.writeFile(path.join(this.dotGitDir, "info", "exclude"), patterns.join("\n"))
151149
}
152150

@@ -160,7 +158,7 @@ export abstract class ShadowCheckpointService extends EventEmitter {
160158
}
161159
}
162160

163-
private async getNestedGitRepository(): Promise<string | null> {
161+
private async getNestedGitRepositories(): Promise<string[]> {
164162
try {
165163
// Find all .git/HEAD files that are not at the root level.
166164
const args = ["--files", "--hidden", "--follow", "-g", "**/.git/HEAD", this.workspaceDir]
@@ -182,32 +180,46 @@ export abstract class ShadowCheckpointService extends EventEmitter {
182180
)
183181
})
184182

185-
if (nestedGitPaths.length > 0) {
186-
// Get the first nested git repository path
183+
const repoDirs: string[] = []
184+
for (const gitPath of nestedGitPaths) {
187185
// Remove .git/HEAD from the path to get the repository directory
188-
const headPath = nestedGitPaths[0].path
186+
const headPath = gitPath.path
189187

190188
// Use path module to properly extract the repository directory
191189
// The HEAD file is at .git/HEAD, so we need to go up two directories
192190
const gitDir = path.dirname(headPath) // removes HEAD, gives us .git
193191
const repoDir = path.dirname(gitDir) // removes .git, gives us the repo directory
194192

195193
const absolutePath = path.join(this.workspaceDir, repoDir)
194+
repoDirs.push(absolutePath)
195+
}
196196

197+
if (repoDirs.length > 0) {
197198
this.log(
198-
`[${this.constructor.name}#getNestedGitRepository] found ${nestedGitPaths.length} nested git repositories, first at: ${repoDir}`,
199+
`[${this.constructor.name}#getNestedGitRepositories] found ${repoDirs.length} nested git repositories`,
199200
)
200-
return absolutePath
201+
202+
// Show informational message to user
203+
if (repoDirs.length === 1) {
204+
const relativePath = path.relative(this.workspaceDir, repoDirs[0])
205+
vscode.window.showInformationMessage(
206+
t("common:info.nested_git_repo_excluded", { path: relativePath }),
207+
)
208+
} else {
209+
vscode.window.showInformationMessage(
210+
t("common:info.nested_git_repos_excluded", { count: repoDirs.length }),
211+
)
212+
}
201213
}
202214

203-
return null
215+
return repoDirs
204216
} catch (error) {
205217
this.log(
206-
`[${this.constructor.name}#getNestedGitRepository] failed to check for nested git repos: ${error instanceof Error ? error.message : String(error)}`,
218+
`[${this.constructor.name}#getNestedGitRepositories] failed to check for nested git repos: ${error instanceof Error ? error.message : String(error)}`,
207219
)
208220

209-
// If we can't check, assume there are no nested repos to avoid blocking the feature.
210-
return null
221+
// If we can't check, return empty array to avoid blocking the feature.
222+
return []
211223
}
212224
}
213225

src/services/checkpoints/__tests__/ShadowCheckpointService.spec.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
379379
})
380380

381381
describe(`${klass.name}#hasNestedGitRepositories`, () => {
382-
it("throws error when nested git repositories are detected during initialization", async () => {
382+
it("excludes nested git repositories from checkpoints instead of throwing error", async () => {
383383
// Create a new temporary workspace and service for this test.
384384
const shadowDir = path.join(tmpDir, `${prefix}-nested-git-${Date.now()}`)
385385
const workspaceDir = path.join(tmpDir, `workspace-nested-git-${Date.now()}`)
@@ -437,11 +437,25 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
437437

438438
const service = new klass(taskId, shadowDir, workspaceDir, () => {})
439439

440-
// Verify that initialization throws an error when nested git repos are detected
441-
// The error message now includes the specific path of the nested repository
442-
await expect(service.initShadowGit()).rejects.toThrowError(
443-
/Checkpoints are disabled because a nested git repository was detected at:/,
444-
)
440+
// Verify that initialization succeeds and excludes nested git repos
441+
await expect(service.initShadowGit()).resolves.not.toThrow()
442+
expect(service.isInitialized).toBe(true)
443+
444+
// Verify that the nested git repo is excluded by checking the exclude file
445+
const excludesPath = path.join(service.checkpointsDir, ".git", "info", "exclude")
446+
const excludeContent = await fs.readFile(excludesPath, "utf-8")
447+
expect(excludeContent).toContain("nested-project/")
448+
449+
// Verify that changes in the nested repo are not tracked
450+
await fs.writeFile(nestedFile, "Modified content in nested repo")
451+
const checkpoint = await service.saveCheckpoint("Test checkpoint")
452+
// Should not create a checkpoint since nested repo changes are excluded
453+
expect(checkpoint?.commit).toBeFalsy()
454+
455+
// Verify that changes in the main repo are still tracked
456+
await fs.writeFile(mainFile, "Modified content in main repo")
457+
const mainCheckpoint = await service.saveCheckpoint("Main repo checkpoint")
458+
expect(mainCheckpoint?.commit).toBeTruthy()
445459

446460
// Clean up.
447461
vitest.restoreAllMocks()

0 commit comments

Comments
 (0)