From e08507f3ce4c8462224bd1b63b93da3b0b201a7c Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Sat, 18 Apr 2026 19:35:06 +0000 Subject: [PATCH] fix: handle git worktrees in GitStore nested repo protection Treat `.git` files the same as `.git` directories so GitStore refuses to initialize inside git worktrees, and add a focused regression test for that checkout shape. Made-with: Cursor --- nanobot/utils/gitstore.py | 7 ++++-- tests/utils/test_gitstore.py | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/nanobot/utils/gitstore.py b/nanobot/utils/gitstore.py index ffa241e7..d9b528c9 100644 --- a/nanobot/utils/gitstore.py +++ b/nanobot/utils/gitstore.py @@ -180,11 +180,14 @@ class GitStore: """Check if self._workspace is already inside a git repository. Walks up from self._workspace to the filesystem root, returning True - if any parent directory contains a .git directory. + if any parent directory contains a .git entry. + + Git worktrees and submodules can use a ``.git`` file instead of a + directory, so we must treat either form as "already inside a repo". """ current = self._workspace.resolve() while current != current.parent: - if (current / ".git").is_dir(): + if (current / ".git").exists(): return True current = current.parent return False diff --git a/tests/utils/test_gitstore.py b/tests/utils/test_gitstore.py index b7ee0ef2..b431bf71 100644 --- a/tests/utils/test_gitstore.py +++ b/tests/utils/test_gitstore.py @@ -1,5 +1,6 @@ """Tests for GitStore — line_ages() and core git operations.""" +import subprocess import time from datetime import datetime, timedelta, timezone from unittest.mock import patch @@ -172,3 +173,44 @@ class TestNestedRepoProtection: assert result is True assert (workspace / ".git").is_dir() + + def test_init_refuses_inside_git_worktree(self, tmp_path): + """init() should refuse when the parent checkout is a git worktree.""" + repo = tmp_path / "repo" + repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + (repo / "README.md").write_text("x\n", encoding="utf-8") + subprocess.run(["git", "-C", str(repo), "add", "README.md"], check=True) + subprocess.run( + [ + "git", + "-C", + str(repo), + "-c", + "user.name=test", + "-c", + "user.email=test@example.com", + "commit", + "-q", + "-m", + "init", + ], + check=True, + ) + subprocess.run(["git", "-C", str(repo), "branch", "wt-branch"], check=True) + + worktree = tmp_path / "worktree" + subprocess.run( + ["git", "-C", str(repo), "worktree", "add", "-q", str(worktree), "wt-branch"], + check=True, + ) + assert (worktree / ".git").is_file() + + workspace = worktree / "workspace" + workspace.mkdir() + + g = GitStore(workspace, tracked_files=["MEMORY.md"]) + result = g.init() + + assert result is False + assert not (workspace / ".git").exists()