Skip to content
Open
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
6 changes: 3 additions & 3 deletions mem_mcp_server/server/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,11 +675,11 @@ def mem_jump(commit_hash: str) -> str:
if not target_entry:
return f"[ERROR] Commit '{commit_hash}' not found. Use mem_history() to see available commits."

# Perform the jump (now returns tuple)
jump_status, new_branch = memov_manager.jump(commit_hash.strip())
# Perform the jump (returns 3-tuple: status, branch, error_detail)
jump_status, new_branch, error_detail = memov_manager.jump(commit_hash.strip())

if jump_status is not MemStatus.SUCCESS:
return f"[ERROR] Failed to jump to commit '{commit_hash}': {jump_status}"
return f"[ERROR] Failed to jump to commit '{commit_hash}': {error_detail or jump_status}"

# Build success message
lines = []
Expand Down
48 changes: 36 additions & 12 deletions memov/core/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1060,14 +1060,14 @@ def _parse_diff_by_file(self, diff_output: str) -> dict[str, dict]:

return diffs_by_file

def jump(self, commit_hash: str) -> tuple[MemStatus, str]:
def jump(self, commit_hash: str) -> tuple[MemStatus, str, str]:
"""Jump to a specific snapshot and auto-create a new branch.

Args:
commit_hash: The commit hash to jump to (full or short form)

Returns:
Tuple of (MemStatus, branch_name) - branch_name is the auto-created branch
Tuple of (MemStatus, branch_name, error_detail) - branch_name is the auto-created branch
"""
try:
# Validate commit hash exists
Expand All @@ -1076,39 +1076,63 @@ def jump(self, commit_hash: str) -> tuple[MemStatus, str]:
)
if not full_hash:
LOGGER.error(f"Commit '{commit_hash}' not found.")
return MemStatus.UNKNOWN_ERROR, ""
return MemStatus.UNKNOWN_ERROR, "", f"Commit '{commit_hash}' not found"

# Get all files that have ever been tracked
all_tracked_files = set()
branches = self._load_branches()
if branches is None or "branches" not in branches:
LOGGER.error("No branches configuration found.")
return MemStatus.UNKNOWN_ERROR, ""
return MemStatus.UNKNOWN_ERROR, "", "No branches configuration found"

for branch_tip in branches["branches"].values():
rev_list = GitManager.get_commit_history(self.bare_repo_path, branch_tip)
for commit in rev_list:
_, file_abs_paths = GitManager.get_files_by_commit(self.bare_repo_path, commit)
all_tracked_files.update(file_abs_paths)
# Normalize paths for cross-platform compatibility
all_tracked_files.update(
os.path.normpath(os.path.abspath(p)) for p in file_abs_paths
)

# Verify archive can be created BEFORE deleting files
archive = GitManager.git_archive(self.bare_repo_path, full_hash)
if archive is None:
LOGGER.error(f"Failed to create archive for commit {commit_hash}.")
return MemStatus.UNKNOWN_ERROR, ""
return (
MemStatus.UNKNOWN_ERROR,
"",
f"Failed to create archive for commit {commit_hash}",
)

# Now safe to remove files that are not in the snapshot
snapshot_files, _ = GitManager.get_files_by_commit(self.bare_repo_path, full_hash)
# Use absolute paths for comparison (normalize for cross-platform compatibility)
_, snapshot_abs_files = GitManager.get_files_by_commit(self.bare_repo_path, full_hash)
snapshot_abs_set = set(os.path.normpath(os.path.abspath(p)) for p in snapshot_abs_files)
for file_path in all_tracked_files:
if file_path not in snapshot_files and os.path.exists(file_path):
normalized_path = os.path.normpath(os.path.abspath(file_path))
if normalized_path not in snapshot_abs_set and os.path.exists(file_path):
try:
os.remove(file_path)
except OSError as e:
LOGGER.warning(f"Failed to delete {file_path}: {e}")

# Extract the snapshot content to the workspace
with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as tar:
tar.extractall(self.project_path)
try:
with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as tar:
# Use extractall with filter for Python 3.12+ compatibility
# Also ensure directory creation on Windows
for member in tar.getmembers():
# Normalize path separators for Windows
member.name = member.name.replace("/", os.sep)
# Ensure parent directories exist
target_path = os.path.join(self.project_path, member.name)
parent_dir = os.path.dirname(target_path)
if parent_dir and not os.path.exists(parent_dir):
os.makedirs(parent_dir, exist_ok=True)
tar.extract(member, self.project_path)
except (tarfile.TarError, OSError, PermissionError) as e:
LOGGER.error(f"Failed to extract archive to {self.project_path}: {e}")
return MemStatus.UNKNOWN_ERROR, "", f"Failed to extract archive: {e}"

# Auto-create a new branch at this commit
# Record where we jumped from (the previous HEAD)
Expand Down Expand Up @@ -1142,10 +1166,10 @@ def jump(self, commit_hash: str) -> tuple[MemStatus, str]:
)

LOGGER.info(f"Jumped to commit {full_hash[:7]} and created branch '{new_branch}'.")
return MemStatus.SUCCESS, new_branch
return MemStatus.SUCCESS, new_branch, ""
except Exception as e:
LOGGER.error(f"Error jumping to commit in memov repo: {e}", exc_info=True)
return MemStatus.UNKNOWN_ERROR, ""
return MemStatus.UNKNOWN_ERROR, "", f"Exception: {type(e).__name__}: {e}"

def _generate_jump_branch_name(self, branches: dict) -> str:
"""Generate a unique branch name for jump operation."""
Expand Down
9 changes: 8 additions & 1 deletion memov/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,15 @@ def jump(
loc: LocOption = ".",
) -> None:
"""Jump to a specific snapshot, restoring the project state."""
from memov.core.manager import MemStatus

manager = get_manager(loc)
manager.jump(snapshot_id)
console = get_console()
status, new_branch, error_detail = manager.jump(snapshot_id)
if status is MemStatus.SUCCESS:
console.print(f"[green]✓ Jumped to {snapshot_id}, created branch '{new_branch}'[/green]")
else:
console.print(f"[red]✗ Jump failed: {error_detail or status}[/red]")


@app.command()
Expand Down
4 changes: 2 additions & 2 deletions memov/web/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ def jump_to_commit(commit_hash: str):
if manager.check() is not MemStatus.SUCCESS:
raise HTTPException(status_code=400, detail="Memov not initialized")

status, new_branch = manager.jump(commit_hash)
status, new_branch, error_detail = manager.jump(commit_hash)
if status is not MemStatus.SUCCESS:
raise HTTPException(status_code=400, detail=f"Jump failed: {status}")
raise HTTPException(status_code=400, detail=f"Jump failed: {error_detail or status}")

return {"status": "success", "new_branch": new_branch}

Expand Down