Files
artcraft-services/frontend/python/compare.py
T
2025-04-16 20:16:12 -04:00

315 lines
11 KiB
Python

import os
import hashlib
import filecmp
from pathlib import Path
from datetime import datetime
import difflib
def get_file_hash(filepath):
"""Calculate MD5 hash of a file."""
md5_hash = hashlib.md5()
try:
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
md5_hash.update(chunk)
return md5_hash.hexdigest()
except Exception:
return "Error calculating hash"
def format_size(size_bytes):
"""Format file size nicely."""
if size_bytes < 1000:
return f"{size_bytes}B"
elif size_bytes < 1000000:
return f"{size_bytes / 1000:.1f}KB"
else:
return f"{size_bytes / 1000000:.1f}MB"
def count_lines(filepath):
"""Count lines in a file safely."""
try:
with open(filepath, "rb") as f:
return sum(1 for _ in f)
except Exception:
return 0
def calculate_diff_ratio(file1, file2):
"""Calculate how different two files are using difflib."""
try:
with (
open(file1, "r", errors="replace") as f1,
open(file2, "r", errors="replace") as f2,
):
text1 = f1.readlines()
text2 = f2.readlines()
matcher = difflib.SequenceMatcher(None, text1, text2)
return 1.0 - matcher.ratio() # Higher value means more different
except Exception:
return 1.0 # If we can't compare, assume completely different
def get_relative_path(file_path, base_path):
"""Get relative path from base directory."""
return str(Path(file_path).relative_to(base_path))
def collect_files_recursive(folder_path):
"""Recursively collect all files in a folder."""
folder_path = Path(folder_path)
file_dict = {}
for path in folder_path.rglob("*"):
if path.is_file():
rel_path = get_relative_path(path, folder_path)
file_dict[rel_path] = path
return file_dict
def compare_folders_recursive(folder1, folder2, output_file=None):
"""Compare two folders recursively and generate a detailed report."""
# Automatically generate output filename if not specified
if output_file is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = f"folder_comparison_{timestamp}.md"
# Get folder paths
folder1_path = Path(folder1)
folder2_path = Path(folder2)
# Get shorthand names for display
folder1_name = folder1_path.name
folder2_name = folder2_path.name
# Get all files recursively
print(f"Collecting files from {folder1_name}...")
files1 = collect_files_recursive(folder1_path)
print(f"Collecting files from {folder2_name}...")
files2 = collect_files_recursive(folder2_path)
# All unique file paths across both folders
all_files = sorted(set(files1.keys()) | set(files2.keys()))
# Calculate column widths for alignment
max_filepath_len = max(len(file) for file in all_files) + 2
hash_col_width = 10 # For hash comparison
size_col_width = 20 # For size and line count
diff_col_width = 30 # For difference description
# Initialize report
report = [
f"# Recursive Folder Comparison Report: {folder1_name} vs {folder2_name}\n",
f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n",
"## File Comparison Table\n",
f"| {'File Path'.ljust(max_filepath_len)} | {'Status'.ljust(hash_col_width)} | {folder1_name.ljust(size_col_width)} | {folder2_name.ljust(size_col_width)} | {'Difference'.ljust(diff_col_width)} |",
f"|{'-' * max_filepath_len}|{'-' * hash_col_width}|{'-' * size_col_width}|{'-' * size_col_width}|{'-' * diff_col_width}|",
]
# Track file categories
identical_files = []
different_files = []
only_in_folder1 = []
only_in_folder2 = []
# Store detailed file comparisons for sorting later
file_comparisons = []
# Compare each file
print(f"Comparing {len(all_files)} unique files...")
for rel_path in all_files:
in_folder1 = rel_path in files1
in_folder2 = rel_path in files2
# Default values
status = "—"
size1_str = "—"
size2_str = "—"
diff_str = "—"
diff_ratio = 0
# Get details for folder1
if in_folder1:
file1 = files1[rel_path]
size1 = file1.stat().st_size
lines1 = count_lines(file1)
hash1 = get_file_hash(file1)
size1_str = f"{format_size(size1)} ({lines1} lines)"
else:
hash1 = "—"
size1 = 0
lines1 = 0
# Get details for folder2
if in_folder2:
file2 = files2[rel_path]
size2 = file2.stat().st_size
lines2 = count_lines(file2)
hash2 = get_file_hash(file2)
size2_str = f"{format_size(size2)} ({lines2} lines)"
else:
hash2 = "—"
size2 = 0
lines2 = 0
# Calculate difference and determine status
if not in_folder1:
status = "MISSING"
diff_str = f"Only in {folder2_name}"
only_in_folder2.append(rel_path)
diff_ratio = 1.0 # Completely different (missing)
elif not in_folder2:
status = "MISSING"
diff_str = f"Only in {folder1_name}"
only_in_folder1.append(rel_path)
diff_ratio = 1.0 # Completely different (missing)
else:
# Both files exist, compare them
if hash1 == hash2 and hash1 != "Error calculating hash":
status = "IDENTICAL"
diff_str = "Identical content"
identical_files.append(rel_path)
diff_ratio = 0.0
else:
status = "DIFFERENT"
size_diff = size2 - size1
lines_diff = lines2 - lines1
sign = "+" if size_diff > 0 else ""
sign_lines = "+" if lines_diff > 0 else ""
# Calculate how different the files are
diff_ratio = calculate_diff_ratio(file1, file2)
different_files.append((rel_path, diff_ratio))
# Format difference description
diff_pct = int(diff_ratio * 100)
diff_str = f"{sign}{format_size(size_diff)} ({sign_lines}{lines_diff} lines) {diff_pct}% different"
# Store comparison for sorting later
file_comparisons.append(
{
"rel_path": rel_path,
"status": status,
"size1_str": size1_str,
"size2_str": size2_str,
"diff_str": diff_str,
"diff_ratio": diff_ratio,
"in_folder1": in_folder1,
"in_folder2": in_folder2,
"hash1": hash1[:8] if len(hash1) > 8 else hash1,
"hash2": hash2[:8] if len(hash2) > 8 else hash2,
}
)
# Sort files by difference ratio (most different first)
file_comparisons.sort(key=lambda x: x["diff_ratio"], reverse=True)
# Generate the sorted table
for comp in file_comparisons:
hash_display = (
f"{comp['hash1']}:{comp['hash2']}"
if comp["status"] == "DIFFERENT"
else comp["status"]
)
report.append(
f"| {comp['rel_path'].ljust(max_filepath_len)} | "
f"{hash_display.ljust(hash_col_width)} | "
f"{comp['size1_str'].ljust(size_col_width)} | "
f"{comp['size2_str'].ljust(size_col_width)} | "
f"{comp['diff_str'].ljust(diff_col_width)} |"
)
# Sort different files by difference ratio for the detailed section
different_files.sort(key=lambda x: x[1], reverse=True)
# Add Files Missing From Each Folder sections
report.append("\n## Files Present in One Folder But Missing in the Other\n")
if only_in_folder1:
report.append(
f"\n### Files Only in {folder1_name} ({len(only_in_folder1)} files)"
)
for rel_path in sorted(only_in_folder1):
file1 = files1[rel_path]
size1 = file1.stat().st_size
lines1 = count_lines(file1)
hash1 = get_file_hash(file1)[:8]
report.append(
f"- `{rel_path}` ({format_size(size1)}, {lines1} lines, hash: {hash1})"
)
if only_in_folder2:
report.append(
f"\n### Files Only in {folder2_name} ({len(only_in_folder2)} files)"
)
for rel_path in sorted(only_in_folder2):
file2 = files2[rel_path]
size2 = file2.stat().st_size
lines2 = count_lines(file2)
hash2 = get_file_hash(file2)[:8]
report.append(
f"- `{rel_path}` ({format_size(size2)}, {lines2} lines, hash: {hash2})"
)
# Add detailed section for different files
if different_files:
report.append(f"\n## Files with Differences (Sorted by Most Different First)\n")
for rel_path, diff_ratio in different_files:
file1 = files1[rel_path]
file2 = files2[rel_path]
size1 = file1.stat().st_size
size2 = file2.stat().st_size
lines1 = count_lines(file1)
lines2 = count_lines(file2)
hash1 = get_file_hash(file1)[:8]
hash2 = get_file_hash(file2)[:8]
size_diff = size2 - size1
lines_diff = lines2 - lines1
diff_pct = int(diff_ratio * 100)
report.append(f"### `{rel_path}` ({diff_pct}% different)")
report.append(
f"- {folder1_name}: {format_size(size1)}, {lines1} lines, hash: {hash1}"
)
report.append(
f"- {folder2_name}: {format_size(size2)}, {lines2} lines, hash: {hash2}"
)
report.append(
f"- Difference: {size_diff:+d} bytes, {lines_diff:+d} lines\n"
)
# Add summary section
report.extend(
[
"\n## Summary",
f"- **Total Unique Files**: {len(all_files)}",
f"- **Files in {folder1_name}**: {len(files1)}",
f"- **Files in {folder2_name}**: {len(files2)}",
f"- **Identical Files**: {len(identical_files)}",
f"- **Different Files**: {len(different_files)}",
f"- **Files Only in {folder1_name}**: {len(only_in_folder1)}",
f"- **Files Only in {folder2_name}**: {len(only_in_folder2)}",
f"\n## Path Information",
f"- {folder1_name}: `{folder1}`",
f"- {folder2_name}: `{folder2}`",
]
)
# Write report to file
with open(output_file, "w") as f:
f.write("\n".join(report))
print(f"Recursive comparison report written to {output_file}")
return "\n".join(report)
# Execute the comparison
if __name__ == "__main__":
folder1 = "/Users/michaelchung/storyteller-rust/frontend/apps/editor2d/src/Classes/"
folder2 = "/Users/michaelchung/storyteller-rust/frontend/apps/editor3d/app/Classes/"
report = compare_folders_recursive(folder1, folder2)