diff --git a/.python-version b/.python-version index 6324d40..763b626 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14 +3.12.12 diff --git a/src/code_trajectory/path_utils.py b/src/code_trajectory/path_utils.py new file mode 100644 index 0000000..46f6015 --- /dev/null +++ b/src/code_trajectory/path_utils.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: MIT +import os +import pathlib +from typing import Optional + +def to_posix_path(path: str) -> str: + """Converts a path to POSIX format (forward slashes). + + This is useful for interacting with tools that expect POSIX paths, + such as Git, even when running on Windows. + """ + return path.replace("\\", "/") + +def normalize_path(path: str) -> str: + """Returns the absolute, normalized path.""" + return os.path.abspath(path) + +def is_subpath(path: str, parent: str) -> bool: + """Checks if path is a subpath of parent. + + Handles platform-specific case sensitivity and normalization. + """ + try: + # Resolve paths to handle symlinks and relative paths + # We use pathlib for robust comparison + p_path = pathlib.Path(path).resolve() + p_parent = pathlib.Path(parent).resolve() + + # Check if p_path is relative to p_parent + p_path.relative_to(p_parent) + return True + except (ValueError, RuntimeError): + return False + +def get_relative_path(path: str, start: str) -> str: + """Returns a relative path from start to path. + + Wraps os.path.relpath but ensures consistent behavior or error handling if needed. + """ + return os.path.relpath(path, start) + +def is_git_directory(path: str) -> bool: + """Checks if the path is a .git directory or inside one.""" + # Robust check for .git in path components + p = pathlib.Path(path) + return ".git" in p.parts diff --git a/src/code_trajectory/recorder.py b/src/code_trajectory/recorder.py index a32d683..0de5b87 100644 --- a/src/code_trajectory/recorder.py +++ b/src/code_trajectory/recorder.py @@ -5,13 +5,14 @@ import logging import os from typing import Optional +from . import path_utils logger = logging.getLogger(__name__) class Recorder: def __init__(self, repo_path: str): - self.project_root = os.path.abspath(repo_path) + self.project_root = path_utils.normalize_path(repo_path) self.shadow_repo_path = os.path.join(self.project_root, ".trajectory") self.current_intent: Optional[str] = None self.current_intent: Optional[str] = None @@ -116,8 +117,10 @@ def get_history(self, filepath: str, max_count: int = 5): else: abs_path = filepath + abs_path = path_utils.normalize_path(abs_path) + # Check if path is within project root - if not abs_path.startswith(self.project_root): + if not path_utils.is_subpath(abs_path, self.project_root): logger.error( f"Path {filepath} is not within project root {self.project_root}" ) diff --git a/src/code_trajectory/server.py b/src/code_trajectory/server.py index 4f73a02..4e3e750 100644 --- a/src/code_trajectory/server.py +++ b/src/code_trajectory/server.py @@ -6,6 +6,7 @@ from .recorder import Recorder from .watcher import Watcher from .trajectory import Trajectory +from . import path_utils # Configure logging logging.basicConfig( @@ -47,7 +48,7 @@ def _check_configured() -> str | None: def _initialize_components(path: str) -> str: - target_path = os.path.abspath(path) + target_path = path_utils.normalize_path(path) if not os.path.exists(target_path): raise ValueError(f"Target path does not exist: {target_path}") diff --git a/src/code_trajectory/trajectory.py b/src/code_trajectory/trajectory.py index a6b0052..5643649 100644 --- a/src/code_trajectory/trajectory.py +++ b/src/code_trajectory/trajectory.py @@ -4,6 +4,7 @@ import os from .recorder import Recorder +from . import path_utils logger = logging.getLogger(__name__) @@ -29,14 +30,18 @@ def get_file_trajectory(self, filepath: str, depth: int = 5) -> str: trajectory = [f"# Trajectory for {filepath}"] # Normalize filepath for tree access (must be relative to project root). + # We need a POSIX path for git tree traversal. if os.path.isabs(filepath): try: - rel_filepath = os.path.relpath(filepath, self.recorder.project_root) + rel_filepath = path_utils.get_relative_path(filepath, self.recorder.project_root) except ValueError: rel_filepath = filepath # Fallback else: rel_filepath = filepath + # Ensure it is posix style for git + rel_filepath = path_utils.to_posix_path(rel_filepath) + # Track content hashes to detect reverts. # Map: content_hash -> (timestamp, message) seen_states = {} diff --git a/src/code_trajectory/watcher.py b/src/code_trajectory/watcher.py index 17560cf..868b4fd 100644 --- a/src/code_trajectory/watcher.py +++ b/src/code_trajectory/watcher.py @@ -4,6 +4,7 @@ from watchdog.events import FileSystemEventHandler from threading import Timer from .recorder import Recorder +from . import path_utils logger = logging.getLogger(__name__) @@ -27,7 +28,7 @@ def on_modified(self, event): filepath = event.src_path # Ignore .git directory. - if ".git" in filepath: + if path_utils.is_git_directory(filepath): return # Check if ignored by git.