fix(gitstore): preserve staged symlinks

This commit is contained in:
chengyongru 2026-07-19 14:53:41 +08:00 committed by chengyongru
parent cea8617096
commit b1232fdaf4
2 changed files with 36 additions and 8 deletions

View File

@ -168,8 +168,8 @@ class GitStore:
# -- internal helpers ------------------------------------------------------ # -- internal helpers ------------------------------------------------------
def _staging_paths(self, *paths: str) -> list[str]: def _staging_paths(self, *paths: str) -> list[str]:
"""Return absolute paths so Dulwich resolves them inside this workspace.""" """Return absolute paths without resolving tracked-file symlinks."""
return [str((self._workspace / path).resolve()) for path in paths] return [str((self._workspace / path).absolute()) for path in paths]
def _resolve_sha(self, short_sha: str) -> bytes | None: def _resolve_sha(self, short_sha: str) -> bytes | None:
"""Resolve a short SHA prefix to the full SHA bytes.""" """Resolve a short SHA prefix to the full SHA bytes."""

View File

@ -2,6 +2,7 @@
import subprocess import subprocess
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@ -225,20 +226,47 @@ class TestNestedRepoProtection:
assert result is True assert result is True
assert (workspace / ".git").is_dir() assert (workspace / ".git").is_dir()
def test_staging_paths_are_resolved_from_workspace(self, tmp_path, monkeypatch): def test_staging_paths_are_absolute_from_workspace(self, tmp_path, monkeypatch):
"""Git operations should not depend on the process working directory.""" """Git operations should not depend on the process working directory."""
from dulwich import porcelain
workspace = tmp_path / "workspace" workspace = tmp_path / "workspace"
workspace.mkdir() workspace.mkdir()
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
git = GitStore(workspace, tracked_files=["MEMORY.md"]) git = GitStore(workspace, tracked_files=["MEMORY.md"])
assert git.init() is True with patch.object(porcelain, "add", wraps=porcelain.add) as mock_add:
assert len(git.log()) == 1 assert git.init() is True
assert len(git.log()) == 1
(workspace / "MEMORY.md").write_text("updated\n", encoding="utf-8") (workspace / "MEMORY.md").write_text("updated\n", encoding="utf-8")
assert git.auto_commit("update memory") is not None assert git.auto_commit("update memory") is not None
assert len(git.log()) == 2 assert len(git.log()) == 2
assert len(mock_add.call_args_list) == 2
for call in mock_add.call_args_list:
staging_paths = [Path(path) for path in call.kwargs["paths"]]
assert all(path.is_absolute() for path in staging_paths)
assert all(path.is_relative_to(workspace) for path in staging_paths)
def test_staging_paths_preserve_symlinks(self, tmp_path):
"""Absolute staging paths should still identify the tracked symlink itself."""
workspace = tmp_path / "workspace"
workspace.mkdir()
target = tmp_path / "shared-memory.md"
target.write_text("shared\n", encoding="utf-8")
link = workspace / "MEMORY.md"
try:
link.symlink_to(target)
except OSError as exc:
pytest.skip(f"symlinks unavailable: {exc}")
git = GitStore(workspace, tracked_files=["MEMORY.md"])
staging_path = Path(git._staging_paths("MEMORY.md")[0])
assert staging_path == link.absolute()
assert staging_path.is_symlink()
def test_init_refuses_inside_git_worktree(self, tmp_path): def test_init_refuses_inside_git_worktree(self, tmp_path):
"""init() should refuse when the parent checkout is a git worktree.""" """init() should refuse when the parent checkout is a git worktree."""