Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/code_trajectory/path_utils.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 5 additions & 2 deletions src/code_trajectory/recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}"
)
Expand Down
3 changes: 2 additions & 1 deletion src/code_trajectory/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .recorder import Recorder
from .watcher import Watcher
from .trajectory import Trajectory
from . import path_utils

# Configure logging
logging.basicConfig(
Expand Down Expand Up @@ -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}")

Expand Down
7 changes: 6 additions & 1 deletion src/code_trajectory/trajectory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os

from .recorder import Recorder
from . import path_utils

logger = logging.getLogger(__name__)

Expand All @@ -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 = {}
Expand Down
3 changes: 2 additions & 1 deletion src/code_trajectory/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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.
Expand Down
Loading