Skip to content

Latest commit

 

History

History
758 lines (628 loc) · 20.9 KB

File metadata and controls

758 lines (628 loc) · 20.9 KB

Implementation Examples: Code Sketches

This document provides concrete code examples for implementing the proposed features.

Part 1: Unicode Output - Code Examples

Example 1: Commit Graph Formatter

// src/ui/formatter.rs

use std::fmt::Write;

pub struct UnicodeFormatter {
    pub use_unicode: bool,
    pub use_colors: bool,
    pub width: usize,
}

impl UnicodeFormatter {
    pub fn format_commit_graph(&self, commits: &[CommitInfo]) -> String {
        let mut output = String::new();
        
        for (i, commit) in commits.iter().enumerate() {
            // Determine symbols
            let symbol = match commit.kind {
                CommitKind::Current => if self.use_unicode { "@" } else { "[*]" },
                CommitKind::MainBranch => if self.use_unicode { "◆" } else { "[+]" },
                CommitKind::Regular => if self.use_unicode { "◆" } else { "[*]" },
                CommitKind::Root => if self.use_unicode { "~" } else { "[~]" },
            };

            // Determine if there's a continuation
            let continuation = if i < commits.len() - 1 {
                if self.use_unicode { "│" } else { "|" }
            } else {
                if self.use_unicode { "~" } else { "~" }
            };

            // Format the line
            writeln!(
                &mut output,
                "{}  {}  {}  {}",
                symbol,
                &commit.hash[..8],
                if commit.is_current { "(HEAD)" } else { "" },
                commit.message
            ).unwrap();

            if i < commits.len() - 1 {
                writeln!(&mut output, "{}", continuation).unwrap();
            }
        }
        
        output
    }

    pub fn format_status(&self, status: &StatusInfo) -> String {
        let mut output = String::new();
        
        // Create a box
        let title = "Status";
        let width = self.width.min(60);
        
        writeln!(&mut output, "{}", self.box_top(title, width)).unwrap();
        
        // Branch info
        writeln!(
            &mut output,
            "│ {} On branch {}",
            if self.use_unicode { "📍" } else { "*" },
            status.branch
        ).unwrap();
        
        // Changes section
        if !status.changed.is_empty() {
            writeln!(&mut output, "│").unwrap();
            writeln!(
                &mut output,
                "│ {} Changes not staged:",
                if self.use_unicode { "📝" } else { "*" }
            ).unwrap();
            
            for (path, kind) in &status.changed {
                let icon = match kind {
                    ChangeKind::Modified => if self.use_unicode { "✏️" } else { "M" },
                    ChangeKind::Added => if self.use_unicode { "➕" } else { "A" },
                    ChangeKind::Deleted => if self.use_unicode { "🗑" } else { "D" },
                };
                writeln!(&mut output, "│   {} {}", icon, path).unwrap();
            }
        }
        
        writeln!(&mut output, "{}", self.box_bottom(width)).unwrap();
        output
    }

    pub fn format_progress_bar(&self, current: u64, total: u64) -> String {
        let percent = if total > 0 {
            (current as f64 / total as f64 * 100.0) as u64
        } else {
            0
        };
        
        let bar_width = 30;
        let filled = (percent as usize * bar_width) / 100;
        let empty = bar_width - filled;
        
        if self.use_unicode {
            format!(
                "{}{}  {}%",
                "█".repeat(filled),
                "░".repeat(empty),
                percent
            )
        } else {
            format!(
                "{}{}  {}%",
                "=".repeat(filled),
                " ".repeat(empty),
                percent
            )
        }
    }

    fn box_top(&self, title: &str, width: usize) -> String {
        if self.use_unicode {
            format!("┌─ {} {}", title, "─".repeat(width.saturating_sub(title.len() + 5)))
        } else {
            format!("+-- {} {}", title, "-".repeat(width.saturating_sub(title.len() + 6)))
        }
    }

    fn box_bottom(&self, width: usize) -> String {
        if self.use_unicode {
            format!("└{}", "─".repeat(width - 1))
        } else {
            format!("+{}", "-".repeat(width - 1))
        }
    }
}

// Test example
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_commit_graph_formatting() {
        let formatter = UnicodeFormatter {
            use_unicode: true,
            use_colors: false,
            width: 80,
        };

        let commits = vec![
            CommitInfo {
                hash: "abc1234567890".to_string(),
                message: "Update docs".to_string(),
                kind: CommitKind::Current,
                is_current: true,
                branch: Some("main".to_string()),
            },
            CommitInfo {
                hash: "def5678901234".to_string(),
                message: "Add feature".to_string(),
                kind: CommitKind::Regular,
                is_current: false,
                branch: None,
            },
        ];

        let output = formatter.format_commit_graph(&commits);
        assert!(output.contains("@"));  // Current indicator
        assert!(output.contains("◆"));  // Regular commit
        assert!(output.contains("│"));  // Continuation
        assert!(output.contains("Update docs"));
        assert!(output.contains("Add feature"));
    }

    #[test]
    fn test_progress_bar() {
        let formatter = UnicodeFormatter {
            use_unicode: true,
            use_colors: false,
            width: 80,
        };

        let bar = formatter.format_progress_bar(50, 100);
        assert!(bar.contains("50%"));
        assert!(bar.contains("█"));
        assert!(bar.contains("░"));
    }
}

Example 2: Status Command Enhanced

// src/commands/status.rs (modified)

use crate::ui::formatter::UnicodeFormatter;

pub fn cmd_status(repo: &Repository, args: StatusArgs) -> Result<()> {
    let status = repo.status()?;
    
    // Get config to check if Unicode is enabled
    let config = repo.config();
    let use_unicode = config.ui.use_unicode.unwrap_or(true);
    let use_colors = config.ui.use_colors.unwrap_or(true);
    
    let formatter = UnicodeFormatter {
        use_unicode,
        use_colors,
        width: term_size::dimensions().map(|(w, _)| w).unwrap_or(80),
    };

    let output = formatter.format_status(&status);
    println!("{}", output);

    println!("Happy Mugging!");
    Ok(())
}

Part 2: Snapshots - Code Examples

Example 1: Snapshot Core Structure

// src/core/snapshots.rs

use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use chrono::{DateTime, Local};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snapshot {
    pub id: String,
    pub timestamp: DateTime<Local>,
    pub source: SnapshotSource,
    pub description: Option<String>,
    pub changed_files: Vec<String>,
    pub total_files: usize,
    pub bytes_saved: u64,
    pub compression_ratio: f32,
    pub parent_id: Option<String>,
    pub is_full: bool,
    pub metadata: HashMap<String, String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SnapshotSource {
    Manual,
    Auto,
    PreOperation,
    Checkpoint,
}

#[derive(Debug)]
pub struct SnapshotConfig {
    pub enabled: bool,
    pub auto_interval_secs: u64,
    pub max_snapshots: usize,
    pub compression: bool,
    pub exclude_patterns: Vec<String>,
}

pub struct SnapshotManager {
    db: MugDb,
    snapshot_dir: PathBuf,
    config: SnapshotConfig,
}

impl SnapshotManager {
    pub fn new(db: MugDb, repo_root: &Path, config: SnapshotConfig) -> Self {
        let snapshot_dir = repo_root.join(".mug/snapshots");
        SnapshotManager {
            db,
            snapshot_dir,
            config,
        }
    }

    /// Create a new snapshot
    pub fn create(
        &mut self,
        source: SnapshotSource,
        description: Option<String>,
    ) -> Result<Snapshot> {
        if !self.config.enabled {
            return Err(Error::Custom("Snapshots disabled".to_string()));
        }

        let id = format!("snap-{}", uuid::Uuid::new_v4());
        let now = Local::now();

        // Get all modified files
        let changed_files = self.get_changed_files()?;
        let total_files = self.count_files()?;

        // Create tarball (incremental or full)
        let (bytes_saved, is_full, parent_id) = 
            self.create_snapshot_file(&id, &changed_files)?;

        // Calculate compression ratio
        let original_size = self.calculate_original_size(&changed_files)?;
        let compression_ratio = if original_size > 0 {
            bytes_saved as f32 / original_size as f32
        } else {
            1.0
        };

        let snapshot = Snapshot {
            id: id.clone(),
            timestamp: now,
            source,
            description,
            changed_files,
            total_files,
            bytes_saved,
            compression_ratio,
            parent_id,
            is_full,
            metadata: HashMap::new(),
        };

        // Store metadata
        let serialized = serde_json::to_vec(&snapshot)?;
        self.db.set("snapshots", &id, serialized)?;

        // Update manifest
        self.update_snapshot_manifest()?;

        // Cleanup old snapshots if necessary
        self.cleanup_excess_snapshots()?;

        Ok(snapshot)
    }

    /// Restore from a snapshot
    pub fn restore(&self, snapshot_id: &str) -> Result<()> {
        let snapshot = self.get(snapshot_id)?
            .ok_or_else(|| Error::Custom("Snapshot not found".to_string()))?;

        // Extract snapshot file
        let snapshot_path = self.snapshot_dir.join(format!("{}.tar.zst", snapshot_id));
        
        if !snapshot_path.exists() {
            // Try to reconstruct from incremental
            if let Some(parent_id) = &snapshot.parent_id {
                self.reconstruct_from_incremental(snapshot_id, parent_id)?;
            } else {
                return Err(Error::Custom("Snapshot file not found".to_string()));
            }
        }

        // Decompress and extract
        self.extract_snapshot(&snapshot_path)?;

        Ok(())
    }

    /// List all snapshots
    pub fn list(&self) -> Result<Vec<Snapshot>> {
        let entries = self.db.scan("snapshots", "")?;
        let mut snapshots = Vec::new();

        for (_, value) in entries {
            if let Ok(snap) = serde_json::from_slice::<Snapshot>(&value) {
                snapshots.push(snap);
            }
        }

        snapshots.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
        Ok(snapshots)
    }

    /// Get snapshot by ID
    pub fn get(&self, id: &str) -> Result<Option<Snapshot>> {
        match self.db.get("snapshots", id)? {
            Some(data) => {
                let snapshot: Snapshot = serde_json::from_slice(&data)?;
                Ok(Some(snapshot))
            }
            None => Ok(None),
        }
    }

    /// Delete a snapshot
    pub fn delete(&self, id: &str) -> Result<()> {
        let snapshot_path = self.snapshot_dir.join(format!("{}.tar.zst", id));
        if snapshot_path.exists() {
            std::fs::remove_file(snapshot_path)?;
        }
        self.db.delete("snapshots", id)?;
        self.update_snapshot_manifest()?;
        Ok(())
    }

    /// Cleanup old snapshots
    pub fn cleanup_old(&self, days: i64) -> Result<usize> {
        let snapshots = self.list()?;
        let cutoff = Local::now() - chrono::Duration::days(days);
        let mut deleted = 0;

        for snapshot in snapshots {
            if snapshot.timestamp < cutoff {
                self.delete(&snapshot.id)?;
                deleted += 1;
            }
        }

        Ok(deleted)
    }

    // Private helper methods
    
    fn create_snapshot_file(
        &self,
        snapshot_id: &str,
        changed_files: &[String],
    ) -> Result<(u64, bool, Option<String>)> {
        // Get last full snapshot for incremental
        let snapshots = self.list()?;
        let last_full = snapshots.iter()
            .find(|s| s.is_full)
            .map(|s| s.id.clone());

        let snapshot_path = self.snapshot_dir.join(format!("{}.tar.zst", snapshot_id));

        if let Some(last_full_id) = &last_full {
            // Create incremental snapshot
            let bytes = self.create_incremental_snapshot(
                snapshot_id,
                last_full_id,
                changed_files,
            )?;
            Ok((bytes, false, Some(last_full_id.clone())))
        } else {
            // Create full snapshot
            let bytes = self.create_full_snapshot(snapshot_id, changed_files)?;
            Ok((bytes, true, None))
        }
    }

    fn create_full_snapshot(
        &self,
        snapshot_id: &str,
        changed_files: &[String],
    ) -> Result<u64> {
        // Simplified: use tar + zstd
        use std::fs::File;
        use tar::Builder;
        use zstd::Encoder;

        let snapshot_path = self.snapshot_dir.join(format!("{}.tar.zst", snapshot_id));
        let encoder = Encoder::new(File::create(&snapshot_path)?, 10)?;
        let mut builder = Builder::new(encoder);

        let mut bytes_written = 0;
        for file_path in changed_files {
            if Path::new(file_path).exists() {
                let file = File::open(file_path)?;
                bytes_written += file.metadata()?.len();
                builder.append_path(file_path)?;
            }
        }

        builder.finish()?;
        Ok(bytes_written)
    }

    fn create_incremental_snapshot(
        &self,
        snapshot_id: &str,
        parent_id: &str,
        changed_files: &[String],
    ) -> Result<u64> {
        // Store only changed files
        // Similar to create_full_snapshot but smaller
        Ok(0) // Placeholder
    }

    fn extract_snapshot(&self, snapshot_path: &Path) -> Result<()> {
        // Extract tar.zst file to working directory
        Ok(())
    }

    fn reconstruct_from_incremental(
        &self,
        snapshot_id: &str,
        parent_id: &str,
    ) -> Result<()> {
        // Reconstruct full snapshot from incremental + parent
        Ok(())
    }

    fn get_changed_files(&self) -> Result<Vec<String>> {
        // Get list of modified files from repo status
        Ok(vec![])
    }

    fn count_files(&self) -> Result<usize> {
        Ok(0)
    }

    fn calculate_original_size(&self, files: &[String]) -> Result<u64> {
        Ok(0)
    }

    fn update_snapshot_manifest(&self) -> Result<()> {
        // Update .mug/snapshots/manifest.json
        Ok(())
    }

    fn cleanup_excess_snapshots(&self) -> Result<()> {
        let snapshots = self.list()?;
        if snapshots.len() > self.config.max_snapshots {
            let to_delete = snapshots.len() - self.config.max_snapshots;
            for snapshot in snapshots.iter().rev().take(to_delete) {
                self.delete(&snapshot.id)?;
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_snapshot_creation() {
        // Create test repo and snapshot
        // Verify snapshot is stored correctly
    }

    #[test]
    fn test_snapshot_restore() {
        // Create snapshot, modify files, restore
        // Verify files are restored to original state
    }

    #[test]
    fn test_incremental_snapshots() {
        // Create first full snapshot
        // Create incremental snapshot
        // Verify storage efficiency
    }

    #[test]
    fn test_cleanup() {
        // Create multiple snapshots
        // Run cleanup
        // Verify old ones removed, recent kept
    }
}

Example 2: Snapshot Commands

// src/main.rs (additions)

#[derive(Subcommand)]
enum Commands {
    // ... existing commands ...
    
    /// Manage work snapshots
    Snapshot {
        #[command(subcommand)]
        action: Option<SnapshotAction>,
    },
}

#[derive(Subcommand)]
enum SnapshotAction {
    /// Create a new snapshot
    Create {
        /// Description for the snapshot
        message: Option<String>,
    },
    
    /// List all snapshots
    List {
        /// Limit number of results
        #[arg(long, default_value = "20")]
        limit: usize,
    },
    
    /// Show snapshot details
    Show {
        /// Snapshot ID
        snapshot_id: String,
    },
    
    /// Restore from snapshot
    Restore {
        /// Snapshot ID to restore
        snapshot_id: String,
        
        /// Create a snapshot before restoring
        #[arg(long, default_value = "true")]
        create_snapshot_first: bool,
    },
    
    /// Delete a snapshot
    Delete {
        /// Snapshot ID
        snapshot_id: String,
    },
    
    /// Start auto-snapshots
    AutoStart,
    
    /// Stop auto-snapshots
    AutoStop,
    
    /// Clean old snapshots
    Cleanup {
        /// Delete snapshots older than N days
        #[arg(long, default_value = "30")]
        older_than_days: i64,
    },
}

// Handler implementation
Commands::Snapshot { action } => {
    let repo = Repository::open(".")?;
    let config = SnapshotConfig {
        enabled: repo.config().snapshots.enabled,
        auto_interval_secs: repo.config().snapshots.interval,
        max_snapshots: repo.config().snapshots.max_count,
        compression: true,
        exclude_patterns: vec![".git".to_string(), ".mug".to_string()],
    };
    
    let mut manager = SnapshotManager::new(
        repo.get_db().clone(),
        repo.path(),
        config,
    );

    match action {
        Some(SnapshotAction::Create { message }) => {
            let snapshot = manager.create(
                SnapshotSource::Manual,
                message,
            )?;
            println!("✓ Snapshot created: {}", snapshot.id);
            println!("  Files: {}", snapshot.changed_files.len());
            println!("  Size: {:.2}MB", snapshot.bytes_saved as f64 / 1_000_000.0);
        }
        
        Some(SnapshotAction::List { limit }) => {
            let snapshots = manager.list()?;
            println!("Snapshots (latest first):\n");
            for snapshot in snapshots.iter().take(limit) {
                println!("ID: {}", &snapshot.id[..16]);
                println!("  Time: {}", snapshot.timestamp);
                println!("  Desc: {}", snapshot.description.as_deref().unwrap_or(""));
                println!("  Size: {:.2}MB", snapshot.bytes_saved as f64 / 1_000_000.0);
                println!();
            }
        }
        
        Some(SnapshotAction::Restore { snapshot_id, create_snapshot_first }) => {
            if create_snapshot_first {
                println!("Creating backup snapshot...");
                let _backup = manager.create(SnapshotSource::PreOperation, None)?;
            }
            
            manager.restore(&snapshot_id)?;
            println!("✓ Restored from snapshot: {}", &snapshot_id[..16]);
        }
        
        Some(SnapshotAction::Cleanup { older_than_days }) => {
            let deleted = manager.cleanup_old(older_than_days)?;
            println!("✓ Deleted {} old snapshots (older than {} days)", deleted, older_than_days);
        }
        
        _ => {
            // Show help
            println!("mug snapshot create [message]");
            println!("mug snapshot list");
            println!("mug snapshot restore <id>");
        }
    }

    println!("Happy Mugging!");
}

Part 3: Integration Example

Auto-Snapshot Before Dangerous Operation

// src/commands/reset.rs (modified)

pub fn cmd_reset(repo: &Repository, args: ResetArgs) -> Result<()> {
    // Auto-create snapshot before dangerous operation
    if args.mode == "hard" {
        println!("⚠️  Creating safety snapshot before hard reset...");
        
        let config = SnapshotConfig {
            enabled: true,
            auto_interval_secs: 0,
            max_snapshots: 50,
            compression: true,
            exclude_patterns: vec![".git".to_string()],
        };
        
        let mut manager = SnapshotManager::new(
            repo.get_db().clone(),
            repo.path(),
            config,
        );
        
        let snapshot = manager.create(
            SnapshotSource::PreOperation,
            Some(format!("Before: reset --hard {}", args.commit)),
        )?;
        
        println!("✓ Snapshot created: {}", snapshot.id);
        println!("  Recover with: mug snapshot restore {}", &snapshot.id[..16]);
    }

    // Perform the reset
    let (old_head, new_head) = repo.reset(&args.commit, &args.mode)?;
    
    println!("Reset {} → {}", 
        &old_head[..8], 
        &new_head[..8]
    );
    println!("Happy Mugging!");
    Ok(())
}

Summary

These code examples show:

  1. Unicode Formatter: Clean abstraction for beautiful output
  2. Snapshot Manager: Core snapshot functionality with CRUD operations
  3. CLI Integration: Adding snapshot commands to MUG
  4. Safety Integration: Auto-snapshots before dangerous operations

All examples follow MUG's existing patterns and use already-available dependencies.