mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix(checkpoints): validate gitdir: content in .git files before flagging as nested repo
Addresses feedback from #11340: .git files without gitdir: content (e.g. stray files) are no longer false-positived as nested repos. - findNestedGitEntry now checks .git file content for gitdir: prefix - .git directories are still always treated as nested repos - Adds regression test for stray .git file without gitdir: pointer
This commit is contained in:
parent
88a7d3b6df
commit
c042bebf2d
2 changed files with 56 additions and 7 deletions
|
|
@ -236,7 +236,10 @@ export abstract class ShadowCheckpointService extends EventEmitter {
|
|||
*
|
||||
* This detects both:
|
||||
* - `.git` directories (standard nested repos)
|
||||
* - `.git` files (submodule/worktree pointer files containing `gitdir: ...`)
|
||||
* - `.git` files containing a `gitdir:` pointer (submodules/worktrees)
|
||||
*
|
||||
* A `.git` file that does NOT begin with `gitdir:` is ignored to avoid
|
||||
* false positives from stray files.
|
||||
*
|
||||
* It uses `lstat` semantics via `withFileTypes` so symbolic links are never
|
||||
* followed, preventing false positives from symlinks pointing outside the
|
||||
|
|
@ -257,13 +260,30 @@ export abstract class ShadowCheckpointService extends EventEmitter {
|
|||
|
||||
// Look for a .git entry in this directory (skip root level).
|
||||
if (!isRoot) {
|
||||
const hasGitEntry = entries.some((e) => e.name === ".git")
|
||||
const gitEntry = entries.find((e) => e.name === ".git")
|
||||
|
||||
if (hasGitEntry) {
|
||||
this.log(
|
||||
`[${this.constructor.name}#getNestedGitRepository] found nested git repository at: ${path.relative(this.workspaceDir, dir)}`,
|
||||
)
|
||||
return dir
|
||||
if (gitEntry) {
|
||||
// .git directories are always valid nested repos.
|
||||
// .git files are only valid if they contain a "gitdir:" pointer
|
||||
// (used by submodules and worktrees). Stray .git files without
|
||||
// "gitdir:" are not treated as nested repos to avoid false positives.
|
||||
let isNestedRepo = gitEntry.isDirectory()
|
||||
|
||||
if (!isNestedRepo && !gitEntry.isSymbolicLink()) {
|
||||
try {
|
||||
const content = await fs.readFile(path.join(dir, ".git"), "utf-8")
|
||||
isNestedRepo = content.trimStart().toLowerCase().startsWith("gitdir:")
|
||||
} catch {
|
||||
// Unreadable .git file -- skip gracefully.
|
||||
}
|
||||
}
|
||||
|
||||
if (isNestedRepo) {
|
||||
this.log(
|
||||
`[${this.constructor.name}#getNestedGitRepository] found nested git repository at: ${path.relative(this.workspaceDir, dir)}`,
|
||||
)
|
||||
return dir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -456,6 +456,35 @@ describe.each([[RepoPerTaskCheckpointService, "RepoPerTaskCheckpointService"]])(
|
|||
await fs.rm(workspaceDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("ignores stray .git file without gitdir: content (no false positive)", async () => {
|
||||
// Create a new temporary workspace and service for this test.
|
||||
const shadowDir = path.join(tmpDir, `${prefix}-stray-gitfile-${Date.now()}`)
|
||||
const workspaceDir = path.join(tmpDir, `workspace-stray-gitfile-${Date.now()}`)
|
||||
|
||||
// Create a primary workspace repo.
|
||||
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")
|
||||
|
||||
// Create a stray .git file that does NOT contain "gitdir:" --
|
||||
// this should NOT be treated as a nested repo.
|
||||
const strayDir = path.join(workspaceDir, "some-tool-output")
|
||||
await fs.mkdir(strayDir, { recursive: true })
|
||||
await fs.writeFile(path.join(strayDir, ".git"), "not a gitdir pointer\n")
|
||||
|
||||
const service = new klass(taskId, shadowDir, workspaceDir, () => {})
|
||||
|
||||
// Initialization should succeed because the .git file is not a valid pointer.
|
||||
await expect(service.initShadowGit()).resolves.not.toThrow()
|
||||
expect(service.isInitialized).toBe(true)
|
||||
|
||||
// Clean up.
|
||||
await fs.rm(shadowDir, { recursive: true, force: true })
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("does not follow symlinks outside the workspace", async () => {
|
||||
// Create a new temporary workspace and service for this test.
|
||||
const shadowDir = path.join(tmpDir, `${prefix}-symlink-${Date.now()}`)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue