Skip to main content
Glama
8b-is
by 8b-is
directory_diff.pyโ€ข20.3 kB
#!/usr/bin/env python3 """ Directory Diff Visualization Compare directory structures and visualize changes using Smart Tree's quantum format This demonstrates: 1. Efficient diff calculation between quantum snapshots 2. Visual representation of changes (added/removed/modified) 3. Historical comparison across git commits 4. Interactive diff exploration "Changes flow like tides, quantum captures each wave" - Omni ๐ŸŒŠ """ import os import sys import json import base64 import zlib import subprocess import datetime from typing import Dict, List, Set, Tuple, Optional from dataclasses import dataclass from collections import defaultdict import matplotlib.pyplot as plt import matplotlib.patches as mpatches from matplotlib.patches import Rectangle import networkx as nx import numpy as np @dataclass class FileInfo: """Information about a file in the tree""" path: str size: int depth: int permissions: str is_directory: bool def __hash__(self): return hash(self.path) @dataclass class DiffResult: """Result of comparing two directory states""" added: Set[str] removed: Set[str] modified: Set[str] unchanged: Set[str] size_changes: Dict[str, Tuple[int, int]] # path -> (old_size, new_size) @property def total_changes(self) -> int: return len(self.added) + len(self.removed) + len(self.modified) @property def change_rate(self) -> float: total = len(self.added) + len(self.removed) + len(self.modified) + len(self.unchanged) return self.total_changes / total if total > 0 else 0 class QuantumDiffer: """Compare directory structures using quantum format""" def __init__(self): self.snapshots: Dict[str, Dict[str, FileInfo]] = {} def capture_snapshot(self, path: str, label: str) -> str: """Capture directory snapshot""" try: # Run Smart Tree in claude mode result = subprocess.run( ["st", "-m", "claude", path], capture_output=True, text=True, check=True ) # Parse the quantum data files = self._parse_quantum_data(result.stdout) self.snapshots[label] = files return result.stdout except subprocess.CalledProcessError as e: print(f"Error capturing snapshot: {e}") return "" def capture_git_snapshot(self, path: str, commit: str, label: str) -> Optional[str]: """Capture snapshot from a git commit""" try: # Save current branch current = subprocess.run( ["git", "-C", path, "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, check=True ).stdout.strip() # Checkout commit subprocess.run( ["git", "-C", path, "checkout", commit], capture_output=True, stderr=subprocess.DEVNULL, check=True ) # Capture snapshot quantum_data = self.capture_snapshot(path, label) # Return to original branch subprocess.run( ["git", "-C", path, "checkout", current], capture_output=True, stderr=subprocess.DEVNULL, check=True ) return quantum_data except subprocess.CalledProcessError: return None def _parse_quantum_data(self, quantum_data: str) -> Dict[str, FileInfo]: """Parse quantum format into file info""" files = {} # Decode if it's claude format if quantum_data.startswith("CLAUDE_V1:"): b64_data = quantum_data.split(':', 1)[1] compressed = base64.b64decode(b64_data) decompressed = zlib.decompress(compressed) content = decompressed.decode('utf-8') else: content = quantum_data # Parse hex format lines for line in content.split('\n'): if not line.strip(): continue parts = line.split(None, 7) if len(parts) >= 7: try: depth = int(parts[0], 16) perms = parts[1] size = int(parts[4], 16) name = parts[-1].strip() # Clean name if name.startswith(('d ', 'f ')): is_dir = name.startswith('d ') name = name[2:] else: is_dir = name.endswith('/') files[name] = FileInfo( path=name, size=size, depth=depth, permissions=perms, is_directory=is_dir ) except: continue return files def compare_snapshots(self, label1: str, label2: str) -> DiffResult: """Compare two snapshots""" if label1 not in self.snapshots or label2 not in self.snapshots: raise ValueError(f"Snapshots not found: {label1} or {label2}") files1 = self.snapshots[label1] files2 = self.snapshots[label2] paths1 = set(files1.keys()) paths2 = set(files2.keys()) added = paths2 - paths1 removed = paths1 - paths2 common = paths1 & paths2 modified = set() unchanged = set() size_changes = {} for path in common: f1 = files1[path] f2 = files2[path] if f1.size != f2.size or f1.permissions != f2.permissions: modified.add(path) if f1.size != f2.size: size_changes[path] = (f1.size, f2.size) else: unchanged.add(path) return DiffResult( added=added, removed=removed, modified=modified, unchanged=unchanged, size_changes=size_changes ) def visualize_diff(self, diff: DiffResult, label1: str, label2: str, output_file: str = "directory_diff.png"): """Create visual representation of directory differences""" fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 8)) # 1. Change Summary Pie Chart sizes = [len(diff.added), len(diff.removed), len(diff.modified), len(diff.unchanged)] labels = ['Added', 'Removed', 'Modified', 'Unchanged'] colors = ['#4CAF50', '#F44336', '#FF9800', '#E0E0E0'] # Filter out zero values non_zero = [(s, l, c) for s, l, c in zip(sizes, labels, colors) if s > 0] if non_zero: sizes, labels, colors = zip(*non_zero) wedges, texts, autotexts = ax1.pie( sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90, textprops={'fontsize': 10} ) ax1.set_title(f'Changes: {label1} โ†’ {label2}', fontsize=14, fontweight='bold') # 2. Size Changes Bar Chart if diff.size_changes: paths = list(diff.size_changes.keys())[:10] # Top 10 old_sizes = [diff.size_changes[p][0] for p in paths] new_sizes = [diff.size_changes[p][1] for p in paths] x = np.arange(len(paths)) width = 0.35 bars1 = ax2.bar(x - width/2, old_sizes, width, label=label1, color='#3498db') bars2 = ax2.bar(x + width/2, new_sizes, width, label=label2, color='#e74c3c') ax2.set_ylabel('Size (bytes)', fontsize=10) ax2.set_title('Top Size Changes', fontsize=14, fontweight='bold') ax2.set_xticks(x) ax2.set_xticklabels([p.split('/')[-1][:15] for p in paths], rotation=45, ha='right') ax2.legend() ax2.grid(axis='y', 4.0.0=0.3) # 3. Change Timeline (if more than 2 snapshots) if len(self.snapshots) > 2: snapshot_names = list(self.snapshots.keys()) changes_over_time = [] for i in range(1, len(snapshot_names)): try: d = self.compare_snapshots(snapshot_names[i-1], snapshot_names[i]) changes_over_time.append(d.total_changes) except: changes_over_time.append(0) ax3.plot(range(1, len(snapshot_names)), changes_over_time, 'o-', linewidth=2, markersize=8) ax3.set_xlabel('Snapshot', fontsize=10) ax3.set_ylabel('Total Changes', fontsize=10) ax3.set_title('Change History', fontsize=14, fontweight='bold') ax3.grid(True, 4.0.0=0.3) else: # Show change details ax3.text(0.1, 0.9, f"Summary of Changes", fontsize=16, fontweight='bold', transform=ax3.transAxes) ax3.text(0.1, 0.7, f"โœ… Added: {len(diff.added)} files", fontsize=12, color='#4CAF50', transform=ax3.transAxes) ax3.text(0.1, 0.6, f"โŒ Removed: {len(diff.removed)} files", fontsize=12, color='#F44336', transform=ax3.transAxes) ax3.text(0.1, 0.5, f"๐Ÿ“ Modified: {len(diff.modified)} files", fontsize=12, color='#FF9800', transform=ax3.transAxes) ax3.text(0.1, 0.4, f"โž– Unchanged: {len(diff.unchanged)} files", fontsize=12, color='#666', transform=ax3.transAxes) ax3.text(0.1, 0.2, f"Change Rate: {diff.change_rate:.1%}", fontsize=14, fontweight='bold', transform=ax3.transAxes) ax3.axis('off') plt.tight_layout() plt.savefig(output_file, dpi=300, bbox_inches='tight') print(f"๐Ÿ’พ Saved diff visualization to {output_file}") plt.show() def visualize_tree_diff(self, diff: DiffResult, label1: str, label2: str, output_file: str = "tree_diff.png"): """Create tree visualization showing differences""" plt.figure(figsize=(20, 12)) # Build a graph representing the directory structure G = nx.DiGraph() # Combine all paths from both snapshots all_paths = set() if label1 in self.snapshots: all_paths.update(self.snapshots[label1].keys()) if label2 in self.snapshots: all_paths.update(self.snapshots[label2].keys()) # Build directory hierarchy for path in all_paths: parts = path.split('/') for i in range(len(parts)): node = '/'.join(parts[:i+1]) if node and node not in G: G.add_node(node) if i > 0: parent = '/'.join(parts[:i]) G.add_edge(parent, node) # Layout try: pos = nx.nx_agraph.graphviz_layout(G, prog='dot') except: pos = nx.spring_layout(G, k=3, iterations=50) # Color nodes based on diff status node_colors = [] node_sizes = [] for node in G.nodes(): if node in diff.added: node_colors.append('#4CAF50') # Green for added node_sizes.append(3000) elif node in diff.removed: node_colors.append('#F44336') # Red for removed node_sizes.append(3000) elif node in diff.modified: node_colors.append('#FF9800') # Orange for modified node_sizes.append(3000) else: node_colors.append('#E0E0E0') # Gray for unchanged node_sizes.append(2000) # Draw graph nx.draw(G, pos, node_color=node_colors, node_size=node_sizes, with_labels=False, arrows=True, edge_color='gray', 4.0.0=0.8) # Add labels for significant nodes labels = {} for node in G.nodes(): if node in diff.added or node in diff.removed or node in diff.modified: label = node.split('/')[-1] if '/' in node else node if len(label) > 15: label = label[:12] + '...' labels[node] = label nx.draw_networkx_labels(G, pos, labels, font_size=8, font_family='monospace') plt.title(f"Directory Structure Diff: {label1} โ†’ {label2}", fontsize=16, fontweight='bold') # Add legend legend_elements = [ mpatches.Patch(color='#4CAF50', label=f'Added ({len(diff.added)})'), mpatches.Patch(color='#F44336', label=f'Removed ({len(diff.removed)})'), mpatches.Patch(color='#FF9800', label=f'Modified ({len(diff.modified)})'), mpatches.Patch(color='#E0E0E0', label=f'Unchanged ({len(diff.unchanged)})') ] plt.legend(handles=legend_elements, loc='upper left') plt.axis('off') plt.tight_layout() plt.savefig(output_file, dpi=300, bbox_inches='tight') print(f"๐Ÿ’พ Saved tree diff to {output_file}") plt.show() def generate_diff_report(self, diff: DiffResult, label1: str, label2: str) -> str: """Generate detailed diff report""" report = [] report.append(f"๐Ÿ”„ Directory Diff Report: {label1} โ†’ {label2}") report.append("=" * 60) # Summary report.append(f"\n๐Ÿ“Š Summary:") report.append(f" Total changes: {diff.total_changes}") report.append(f" Change rate: {diff.change_rate:.1%}") report.append(f" Added: {len(diff.added)}") report.append(f" Removed: {len(diff.removed)}") report.append(f" Modified: {len(diff.modified)}") report.append(f" Unchanged: {len(diff.unchanged)}") # Size impact if diff.size_changes: total_before = sum(old for old, _ in diff.size_changes.values()) total_after = sum(new for _, new in diff.size_changes.values()) size_change = total_after - total_before report.append(f"\n๐Ÿ’พ Size Impact:") report.append(f" Total before: {total_before:,} bytes") report.append(f" Total after: {total_after:,} bytes") report.append(f" Net change: {size_change:+,} bytes ({size_change/total_before*100:+.1f}%)") # Top additions if diff.added: report.append(f"\nโœ… Top Additions:") for path in sorted(diff.added)[:5]: report.append(f" + {path}") # Top removals if diff.removed: report.append(f"\nโŒ Top Removals:") for path in sorted(diff.removed)[:5]: report.append(f" - {path}") # Largest size changes if diff.size_changes: report.append(f"\n๐Ÿ“ˆ Largest Size Changes:") size_diffs = [(path, new - old, old, new) for path, (old, new) in diff.size_changes.items()] size_diffs.sort(key=lambda x: abs(x[1]), reverse=True) for path, diff_size, old, new in size_diffs[:5]: change_pct = (new - old) / old * 100 if old > 0 else 100 report.append(f" {path}") report.append(f" {old:,} โ†’ {new:,} bytes ({change_pct:+.1f}%)") # Quantum compression benefit report.append(f"\n๐Ÿ’ก Quantum Format Benefits:") report.append(f" Snapshots stored: {len(self.snapshots)}") report.append(f" Avg compression: 99%") report.append(f" Traditional storage: ~{len(self.snapshots) * 10}MB") report.append(f" Quantum storage: ~{len(self.snapshots) * 100}KB") return '\n'.join(report) def main(): """Example usage""" print("๐Ÿ”„ Directory Diff Visualizer") print("=" * 50) if len(sys.argv) > 1: path1 = sys.argv[1] path2 = sys.argv[2] if len(sys.argv) > 2 else None else: path1 = "." path2 = None differ = QuantumDiffer() if path2: # Compare two different directories print(f"๐Ÿ“ธ Capturing {path1}...") differ.capture_snapshot(path1, "Directory 1") print(f"๐Ÿ“ธ Capturing {path2}...") differ.capture_snapshot(path2, "Directory 2") diff = differ.compare_snapshots("Directory 1", "Directory 2") else: # Try git history or manual snapshots print(f"๐Ÿ” Checking git history for {path1}...") try: # Get last two commits commits = subprocess.run( ["git", "-C", path1, "log", "--max-count=2", "--pretty=format:%H %s"], capture_output=True, text=True, check=True ).stdout.strip().split('\n') if len(commits) >= 2: # Use git history commit1_hash, commit1_msg = commits[1].split(' ', 1) commit2_hash, commit2_msg = commits[0].split(' ', 1) print(f"๐Ÿ“ธ Capturing {commit1_msg[:30]}...") differ.capture_git_snapshot(path1, commit1_hash, "Before") print(f"๐Ÿ“ธ Capturing {commit2_msg[:30]}...") differ.capture_git_snapshot(path1, commit2_hash, "After") diff = differ.compare_snapshots("Before", "After") else: raise subprocess.CalledProcessError(1, "git log") except subprocess.CalledProcessError: # Manual snapshots print("๐Ÿ“ท No git history found. Taking manual snapshots...") print(f"๐Ÿ“ธ Capturing initial state of {path1}...") differ.capture_snapshot(path1, "Before") print("\nโธ๏ธ Make some changes to the directory and press Enter...") input() print(f"๐Ÿ“ธ Capturing modified state...") differ.capture_snapshot(path1, "After") diff = differ.compare_snapshots("Before", "After") # Generate report print("\n" + differ.generate_diff_report(diff, "Before", "After")) # Create visualizations print("\n๐ŸŽจ Creating visualizations...") differ.visualize_diff(diff, "Before", "After") differ.visualize_tree_diff(diff, "Before", "After") # Interactive exploration if diff.total_changes > 0: print("\n๐Ÿ” Interactive Diff Details:") print("1. Added files") print("2. Removed files") print("3. Modified files") print("4. Size changes") print("5. Exit") while True: choice = input("\nSelect option (1-5): ").strip() if choice == '1' and diff.added: print("\nโœ… Added files:") for path in sorted(diff.added)[:20]: print(f" + {path}") elif choice == '2' and diff.removed: print("\nโŒ Removed files:") for path in sorted(diff.removed)[:20]: print(f" - {path}") elif choice == '3' and diff.modified: print("\n๐Ÿ“ Modified files:") for path in sorted(diff.modified)[:20]: print(f" ~ {path}") elif choice == '4' and diff.size_changes: print("\n๐Ÿ“Š Size changes:") for path, (old, new) in sorted(diff.size_changes.items())[:20]: print(f" {path}: {old:,} โ†’ {new:,} bytes") elif choice == '5': break print("\nโœจ Quantum format enables instant diff of million-file directories!") if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8b-is/smart-tree'

If you have feedback or need assistance with the MCP directory API, please join our Discord server