mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-15 00:18:04 +00:00
Fix find_project_root hijacking git worktrees nested under a Serena project (#1550)
* Fix find_project_root hijacking git worktrees nested under a Serena project find_project_root searched all ancestor levels for .serena/project.yml before ever looking for .git. A git worktree whose own .git is a pointer file, nested under a directory that is an explicit Serena project, was resolved to the ancestor project instead of the worktree. With CLI agents that launch via --project-from-cwd (Claude Code, Codex, Gemini), this silently bound the server to the wrong working tree: reads returned stale symbols and edits landed in the parent repo. Walk up in a single pass so the nearest project boundary wins, whether it is a .serena/project.yml or a .git (dir or worktree/submodule pointer file). Same-level behavior is unchanged. Adds a regression test. * Document worktree project-root fix in changelog and usage docs
This commit is contained in:
1 parent
577931c8cf
commit
b5b9cc4ffc
5 files changed
+54
-9
No files matched your search
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
Status of the `main` branch. Changes prior to the next official version change will appear here.
|
Status of the `main` branch. Changes prior to the next official version change will appear here.
|
||||||
|
|
||||||
|
* General:
|
||||||
|
- Fix `--project-from-cwd` hijacking git worktrees nested under a Serena project. `find_project_root`
|
||||||
|
now walks up in a single pass so the nearest project boundary wins (either a `.serena/project.yml`
|
||||||
|
or a `.git`, including worktree/submodule pointer files), instead of preferring an ancestor's
|
||||||
|
`.serena/project.yml` over a closer `.git`. This previously bound CLI agents (Claude Code, Codex,
|
||||||
|
Gemini) launched from inside a worktree to the parent repo, causing stale reads and misdirected edits.
|
||||||
|
|
||||||
* Language Servers:
|
* Language Servers:
|
||||||
- `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`.
|
- `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`.
|
||||||
The dict is forwarded to vtsls via `initializationOptions`, `workspace/didChangeConfiguration`,
|
The dict is forwarded to vtsls via `initializationOptions`, `workspace/didChangeConfiguration`,
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ Some useful options include:
|
|||||||
|
|
||||||
* `--project <path|name>`: specify the project to work on by name or path.
|
* `--project <path|name>`: specify the project to work on by name or path.
|
||||||
* `--project-from-cwd`: auto-detect the project from current working directory
|
* `--project-from-cwd`: auto-detect the project from current working directory
|
||||||
(looking for a directory containing `.serena/project.yml` or `.git` in parent directories and activating the containing directory as the project root, if any).
|
(walking up the parent directories and activating the nearest one that contains either `.serena/project.yml`
|
||||||
|
or `.git`, if any). The nearest boundary wins, so a git worktree nested under another Serena project resolves
|
||||||
|
to the worktree itself rather than the ancestor project.
|
||||||
This option is intended for CLI-based agents like Claude Code, Gemini and Codex, which are typically started from within the project directory
|
This option is intended for CLI-based agents like Claude Code, Gemini and Codex, which are typically started from within the project directory
|
||||||
and which do not change directories during their operation.
|
and which do not change directories during their operation.
|
||||||
* `--language-backend JetBrains`: use the Serena JetBrains Plugin as the language backend (overriding the default backend configured in the central configuration)
|
* `--language-backend JetBrains`: use the Serena JetBrains Plugin as the language backend (overriding the default backend configured in the central configuration)
|
||||||
|
|||||||
@@ -14,3 +14,5 @@ having persisted the plan in a memory or dedicated file.
|
|||||||
[git-worktree](https://git-scm.com/docs/git-worktree) can be an excellent way to parallelize your work. More on this in [Anthropic: Run parallel Claude Code sessions with Git worktrees](https://docs.claude.com/en/docs/claude-code/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees).
|
[git-worktree](https://git-scm.com/docs/git-worktree) can be an excellent way to parallelize your work. More on this in [Anthropic: Run parallel Claude Code sessions with Git worktrees](https://docs.claude.com/en/docs/claude-code/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees).
|
||||||
|
|
||||||
Be sure to add the `.serena` folder to version control, such that your project-specific settings and memories are available across worktrees.
|
Be sure to add the `.serena` folder to version control, such that your project-specific settings and memories are available across worktrees.
|
||||||
|
|
||||||
|
When you launch a CLI agent from inside a worktree using `--project-from-cwd`, Serena activates the worktree itself, even if the worktree lives under another Serena project (for example `<repo>/.claude/worktrees/<name>`, where Claude Code creates them natively). The nearest project boundary wins: the worktree's own `.git` pointer file takes precedence over an ancestor's `.serena/project.yml`, so file operations always resolve against the correct working tree.
|
||||||
+15
-8
@@ -65,7 +65,16 @@ For details on mode configuration, see
|
|||||||
def find_project_root(root: str | Path | None = None) -> str | None:
|
def find_project_root(root: str | Path | None = None) -> str | None:
|
||||||
"""Find project root by walking up from CWD.
|
"""Find project root by walking up from CWD.
|
||||||
|
|
||||||
Checks for .serena/project.yml first (explicit Serena project), then .git (git root).
|
Returns the nearest ancestor that is either an explicit Serena project
|
||||||
|
(contains .serena/project.yml) or a git root (contains .git, which may be a
|
||||||
|
directory or, for git worktrees and submodules, a pointer file). The nearest
|
||||||
|
such directory wins; a .serena/project.yml at the same level takes priority
|
||||||
|
over .git only because they resolve to the same directory.
|
||||||
|
|
||||||
|
Walking up with a single pass (rather than searching all levels for
|
||||||
|
.serena/project.yml first and only then for .git) ensures a git worktree
|
||||||
|
nested under another Serena project resolves to the worktree itself, instead
|
||||||
|
of being hijacked by the ancestor project's .serena/project.yml.
|
||||||
|
|
||||||
:param root: If provided, constrains the search to this directory and below
|
:param root: If provided, constrains the search to this directory and below
|
||||||
(acts as a virtual filesystem root). Search stops at this boundary.
|
(acts as a virtual filesystem root). Search stops at this boundary.
|
||||||
@@ -82,14 +91,12 @@ def find_project_root(root: str | Path | None = None) -> str | None:
|
|||||||
if boundary is not None and parent == boundary:
|
if boundary is not None and parent == boundary:
|
||||||
return
|
return
|
||||||
|
|
||||||
# First pass: look for .serena
|
# Single pass: the nearest project boundary wins, whether it is an explicit
|
||||||
|
# Serena project (.serena/project.yml) or a git root (.git, a dir or a worktree
|
||||||
|
# pointer file). This keeps a nested git worktree from being hijacked by an
|
||||||
|
# ancestor's .serena/project.yml.
|
||||||
for directory in ancestors():
|
for directory in ancestors():
|
||||||
if (directory / ".serena" / "project.yml").is_file():
|
if (directory / ".serena" / "project.yml").is_file() or (directory / ".git").exists():
|
||||||
return str(directory)
|
|
||||||
|
|
||||||
# Second pass: look for .git
|
|
||||||
for directory in ancestors():
|
|
||||||
if (directory / ".git").exists(): # .git can be file (worktree) or dir
|
|
||||||
return str(directory)
|
return str(directory)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -328,6 +328,33 @@ class TestFindProjectRoot:
|
|||||||
finally:
|
finally:
|
||||||
os.chdir(original_cwd)
|
os.chdir(original_cwd)
|
||||||
|
|
||||||
|
def test_git_worktree_not_hijacked_by_ancestor_serena(self, temp_project_dir):
|
||||||
|
"""A git worktree nested under a Serena project must resolve to the worktree.
|
||||||
|
|
||||||
|
Regression test: when a git worktree (whose .git is a pointer *file*) lives
|
||||||
|
below a directory that is an explicit Serena project (.serena/project.yml),
|
||||||
|
the worktree's own .git boundary must win over the ancestor's project marker.
|
||||||
|
The old two-pass search returned the ancestor Serena project, causing reads
|
||||||
|
and edits to land in the wrong working tree.
|
||||||
|
"""
|
||||||
|
# Ancestor directory is an explicit Serena project.
|
||||||
|
serena_dir = os.path.join(temp_project_dir, ".serena")
|
||||||
|
os.makedirs(serena_dir)
|
||||||
|
Path(os.path.join(serena_dir, "project.yml")).touch()
|
||||||
|
# Nested git worktree: .git is a gitdir pointer file, as created by `git worktree add`.
|
||||||
|
worktree = os.path.join(temp_project_dir, "nested", "worktree")
|
||||||
|
os.makedirs(worktree)
|
||||||
|
Path(os.path.join(worktree, ".git")).write_text("gitdir: /repo/.git/worktrees/wt\n")
|
||||||
|
|
||||||
|
original_cwd = os.getcwd()
|
||||||
|
try:
|
||||||
|
os.chdir(worktree)
|
||||||
|
result = find_project_root(root=temp_project_dir)
|
||||||
|
assert result is not None
|
||||||
|
assert os.path.samefile(result, worktree)
|
||||||
|
finally:
|
||||||
|
os.chdir(original_cwd)
|
||||||
|
|
||||||
|
|
||||||
class TestProjectFromCwdMutualExclusivity:
|
class TestProjectFromCwdMutualExclusivity:
|
||||||
"""Tests for --project-from-cwd mutual exclusivity."""
|
"""Tests for --project-from-cwd mutual exclusivity."""
|
||||||
|
|||||||
Reference in new issue
Block a user