Files
AIPass/src/aipass/backup/apps/handlers/diff/vscode_integration.py
T
2026-03-17 21:33:10 -07:00

224 lines
9.3 KiB
Python
Executable File

# =================== AIPass ====================
# Name: vscode_integration.py
# Description: VS Code diff viewer integration
# Version: 2.0.0
# Created: 2025-11-16
# Modified: 2026-03-09
# =============================================
"""
VS Code Integration Handler
Opens file diffs in VS Code editor for visual comparison.
Supports baseline vs current and version-to-version comparisons.
"""
# =============================================
# IMPORTS
# =============================================
import sys
import os
import subprocess
from aipass.prax import logger
from pathlib import Path
# logger imported from aipass.prax
# Import from handlers
from aipass.backup.apps.handlers.utils.system_utils import safe_print
from aipass.backup.apps.handlers.diff.version_manager import get_versioned_files
from aipass.backup.apps.handlers.json import json_handler
# =============================================
# VS CODE INTEGRATION
# =============================================
def show_file_diff(backup_path: Path, source_dir: Path, file_path: str, version1: str | None = None, version2: str | None = None) -> bool:
"""Show diff between two versions of a file in VS Code.
Args:
backup_path: Path to backup root directory
source_dir: Path to source directory (for current file comparison)
file_path: File path to show diff for
version1: First version timestamp (optional)
version2: Second version timestamp (optional)
Returns:
True if diff was opened successfully, False otherwise
"""
try:
json_handler.log_operation("vscode_diff_opened")
versioned_files = get_versioned_files(backup_path, file_path)
if not versioned_files:
safe_print(f"No versioned files found for: {file_path}")
return False
# Find the matching file with normalized path comparison
matching_file = None
normalized_search_path = str(Path(file_path)).replace('\\', '/').lower()
for base_path in versioned_files:
normalized_base_path = str(base_path).replace('\\', '/').lower()
if normalized_search_path in normalized_base_path:
matching_file = base_path
break
if not matching_file:
safe_print(f"File not found: {file_path}")
return False
versions = versioned_files[matching_file]
if len(versions) < 1:
safe_print(f"Only one version found for: {file_path}")
return False
# Determine which versions to compare
if version1 and version2:
if version1 not in versions or version2 not in versions:
safe_print(f"Version not found. Available: {', '.join(versions)}")
return False
else:
# Compare latest version with current file, or two most recent versions
current_file = backup_path / matching_file
if current_file.exists() and len(versions) >= 1:
version1 = "current"
version2 = versions[0] # Most recent version
elif len(versions) >= 2:
version1 = versions[1] # Second most recent
version2 = versions[0] # Most recent
else:
safe_print(f"Need at least 2 versions to compare")
return False
# Determine file paths for comparison using NEW STRUCTURE
# For VS Code diff, we need actual files, not diff patches
try:
# NEW STRUCTURE: matching_file is like "root/CLAUDE.local.md/CLAUDE.local.md"
# We need to find the file folder and look for baseline + current file
# Extract the base filename for pattern matching
base_filename = Path(matching_file).name
file_folder_path = backup_path / Path(matching_file).parent
# Look for baseline file in the file folder
# Handle files with extensions like CLAUDE.local.md -> CLAUDE.local-baseline-*
name_without_ext = base_filename.rsplit('.', 1)[0] if '.' in base_filename else base_filename
baseline_pattern = f"{name_without_ext}-baseline-*"
baseline_files = list(file_folder_path.glob(baseline_pattern))
# Current backup file path (in the organized structure)
current_backup_file = file_folder_path / base_filename
# Source file path (live file in source directory)
# Convert backup path back to source path
matching_file_normalized = str(Path(matching_file)).replace('\\', '/')
if matching_file_normalized.startswith("root/"):
# Root level file: root/filename/filename -> filename
source_relative_path = base_filename
else:
# Nested file: folder/filename/filename -> folder/filename
source_relative_path = str(Path(matching_file).parent.parent / base_filename)
source_file_path = source_dir / source_relative_path
if baseline_files:
# ALWAYS prefer baseline comparison when available
file1_path = baseline_files[0] # Use first baseline found
file2_path = source_file_path # Current source
label1 = f"{base_filename} (baseline)"
label2 = f"{base_filename} (current)"
elif current_backup_file.exists():
# No baseline, compare backup against current source
file1_path = current_backup_file # Backup version
file2_path = source_file_path # Live source
label1 = f"{base_filename} (backup)"
label2 = f"{base_filename} (current)"
# Check if they're the same
if file1_path.exists() and file2_path.exists():
if file1_path.stat().st_mtime == file2_path.stat().st_mtime:
safe_print(f"Note: Backup is identical to current file (no changes)")
safe_print(f" This happens when Google Drive sync updates the backup")
else:
safe_print(f"Could not find backup file in new structure: {matching_file}")
return False
# Verify files exist
if not file1_path.exists():
safe_print(f"File not found: {file1_path}")
return False
if not file2_path.exists():
safe_print(f"File not found: {file2_path}")
return False
except Exception as e:
safe_print(f"Error locating files: {e}")
logger.error(f"[vscode_integration] Error locating files for diff: {e}")
return False
# Open diff in VS Code
safe_print(f"\n{'='*70}")
safe_print(f"Opening diff in VS Code: {matching_file}")
safe_print(f"Comparing: {label2} -> {label1}")
safe_print('='*70)
# Launch VS Code with diff view
try:
# Try to find VS Code command
code_cmd = 'code'
if sys.platform == 'win32':
# On Windows, try common VS Code paths if 'code' not in PATH
import shutil
if not shutil.which('code'):
# Platform-specific VS Code detection
if sys.platform == 'win32':
# Windows VS Code path
vscode_path = Path(r"C:\Users") / os.environ.get('USERNAME', 'input') / r"AppData\Local\Programs\Microsoft VS Code\bin\code.cmd"
if vscode_path.exists():
code_cmd = str(vscode_path)
else:
code_cmd = 'code.cmd'
else:
# Linux - VS Code is usually in PATH or use snap
if Path('/snap/bin/code').exists():
code_cmd = '/snap/bin/code'
elif Path('/usr/bin/code').exists():
code_cmd = '/usr/bin/code'
else:
code_cmd = 'code'
# Use subprocess for better error handling
# Swap order so baseline (older) is on left, current (newer) is on right
result = subprocess.run(
[code_cmd, '--diff', str(file1_path), str(file2_path)],
capture_output=True,
text=True,
shell=True if sys.platform == 'win32' else False
)
if result.returncode == 0:
safe_print(f"Diff opened in VS Code")
else:
safe_print(f"VS Code returned code {result.returncode}")
if result.stderr:
safe_print(f"Error: {result.stderr}")
except FileNotFoundError:
safe_print(f"VS Code command not found. Make sure 'code' is in your PATH")
safe_print(f"You can manually compare:")
safe_print(f" File 1: {file2_path}")
safe_print(f" File 2: {file1_path}")
return False
except Exception as e:
safe_print(f"Error launching VS Code: {e}")
logger.error(f"[vscode_integration] Error launching VS Code: {e}")
return False
safe_print('='*70)
return True
except Exception as e:
safe_print(f"Error generating diff: {e}")
logger.error(f"[vscode_integration] Error in show_file_diff: {e}")
return False