eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Error recovery utilities for graceful degradation.
//!
//! Provides helpers for recovering from errors without crashing,
//! with proper logging and user feedback.

use std::fmt;

/// Result type with recovery capabilities.
pub type RecoverableResult<T> = Result<T, RecoverableError>;

/// An error that can potentially be recovered from.
#[derive(Debug, Clone)]
pub struct RecoverableError {
    /// Original error message
    pub message: String,
    /// Error category for handling
    pub category: ErrorCategory,
    /// Whether the operation can be retried
    pub retryable: bool,
    /// Suggested user action
    pub guidance: Option<String>,
}

/// Categories of errors for different handling strategies.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
    /// Git operation failed (command, network, etc.)
    Git,
    /// File system operation failed
    FileSystem,
    /// Configuration error
    Config,
    /// UI rendering error
    Render,
    /// Network error
    Network,
    /// Unknown/other error
    Unknown,
}

impl RecoverableError {
    /// Create a new recoverable error.
    pub fn new(message: impl Into<String>, category: ErrorCategory) -> Self {
        Self {
            message: message.into(),
            category,
            retryable: false,
            guidance: None,
        }
    }
    
    /// Mark this error as retryable.
    pub fn retryable(mut self) -> Self {
        self.retryable = true;
        self
    }
    
    /// Add guidance for the user.
    pub fn with_guidance(mut self, guidance: impl Into<String>) -> Self {
        self.guidance = Some(guidance.into());
        self
    }
    
    /// Create a Git error.
    pub fn git(message: impl Into<String>) -> Self {
        Self::new(message, ErrorCategory::Git).retryable()
    }
    
    /// Create a file system error.
    pub fn fs(message: impl Into<String>) -> Self {
        Self::new(message, ErrorCategory::FileSystem)
    }
    
    /// Create a config error.
    pub fn config(message: impl Into<String>) -> Self {
        Self::new(message, ErrorCategory::Config)
    }
    
    /// Convert to Msg for TEA pattern.
    pub fn to_msg(&self) -> crate::core::Msg {
        crate::core::Msg::SetError {
            error: Some(self.message.clone()),
            guidance: self.guidance.clone(),
        }
    }
}

impl fmt::Display for RecoverableError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.message)?;
        if let Some(guidance) = &self.guidance {
            write!(f, " ({})", guidance)?;
        }
        Ok(())
    }
}

impl std::error::Error for RecoverableError {}

impl From<std::io::Error> for RecoverableError {
    fn from(err: std::io::Error) -> Self {
        Self::fs(err.to_string())
    }
}

impl From<anyhow::Error> for RecoverableError {
    fn from(err: anyhow::Error) -> Self {
        Self::new(err.to_string(), ErrorCategory::Unknown)
    }
}

/// Retry an operation with exponential backoff.
pub fn retry<T, E, F>(mut f: F, max_attempts: usize) -> Result<T, E>
where
    F: FnMut() -> Result<T, E>,
{
    let mut attempt = 0;
    loop {
        match f() {
            Ok(v) => return Ok(v),
            Err(e) if attempt + 1 >= max_attempts => return Err(e),
            Err(_) => {
                attempt += 1;
                std::thread::sleep(std::time::Duration::from_millis(100 * (1 << attempt)));
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_error_creation() {
        let err = RecoverableError::git("Command failed")
            .with_guidance("Try running git status manually");
        
        assert_eq!(err.category, ErrorCategory::Git);
        assert!(err.retryable);
        assert!(err.guidance.is_some());
    }
    
    #[test]
    fn test_retry_success() {
        let mut counter = 0;
        let result: Result<i32, &str> = retry(|| {
            counter += 1;
            if counter < 3 {
                Err("not yet")
            } else {
                Ok(42)
            }
        }, 5);
        
        assert_eq!(result, Ok(42));
        assert_eq!(counter, 3);
    }
}