eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Safe file I/O operations for rebase todo files.
//!
//! Provides atomic file operations to prevent corruption during rebase operations.

use crate::app::rebase::{RebaseEntry, RebaseAction};
use std::path::{Path, PathBuf};
use std::fs;
use std::io;

/// Errors that can occur during rebase I/O operations
#[derive(Debug, thiserror::Error)]
pub enum RebaseIOError {
    #[error("IO error: {0}")]
    Io(#[from] io::Error),

    #[error("Invalid todo file format: {0}")]
    InvalidFormat(String),

    #[error("File operation failed: {0}")]
    FileOperation(String),
}

/// Safe I/O operations for rebase todo files
pub struct RebaseIO;

impl RebaseIO {
    /// Format rebase entries to git-rebase-todo format
    pub fn format_todo_content(entries: &[RebaseEntry]) -> String {
        entries
            .iter()
            .map(|entry| format!("{} {}", entry.action.as_str(), entry.hash))
            .collect::<Vec<_>>()
            .join("\n")
    }

    /// Write todo content to file atomically (prevents corruption)
    pub fn write_todo_atomically(
        path: &Path,
        content: &str,
    ) -> Result<(), RebaseIOError> {
        // Create temp file in same directory for atomic rename
        let temp_path = path.with_extension("tmp");

        // Write to temp file first
        fs::write(&temp_path, content)?;

        // Atomic rename (this is atomic on POSIX filesystems)
        fs::rename(temp_path, path)?;

        Ok(())
    }

    /// Read and parse a git-rebase-todo file
    pub fn read_todo_file(path: &Path) -> Result<Vec<RebaseEntry>, RebaseIOError> {
        let content = fs::read_to_string(path)?;

        let mut entries = Vec::new();
        for line in content.lines() {
            let line = line.trim();

            // Skip comments and empty lines
            if line.starts_with('#') || line.is_empty() {
                continue;
            }

            // Parse action and hash
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() < 2 {
                continue; // Skip malformed lines
            }

            let action_str = parts[0];
            let hash = parts[1].to_string();

            // Parse message (everything after hash)
            let message = if parts.len() > 2 {
                parts[2..].join(" ")
            } else {
                "Unknown commit".to_string()
            };

            // Parse action
            let action = RebaseAction::from_str(action_str)
                .unwrap_or(RebaseAction::Pick); // Default to pick for unknown actions

            entries.push(RebaseEntry::new(hash, message, action));
        }

        Ok(entries)
    }

    /// Find the current rebase todo file path
    pub fn find_todo_file(repo_path: &str) -> Option<PathBuf> {
        let git_dir = Path::new(repo_path).join(".git");

        // Check for rebase-merge directory (newer git)
        let rebase_merge_todo = git_dir.join("rebase-merge").join("git-rebase-todo");
        if rebase_merge_todo.exists() {
            return Some(rebase_merge_todo);
        }

        // Check for rebase-apply directory (older git)
        let rebase_apply_todo = git_dir.join("rebase-apply").join("git-rebase-todo");
        if rebase_apply_todo.exists() {
            return Some(rebase_apply_todo);
        }

        None
    }

    /// Check if a rebase is currently in progress
    pub fn is_rebase_in_progress(repo_path: &str) -> bool {
        let git_dir = Path::new(repo_path).join(".git");
        git_dir.join("rebase-merge").exists() || git_dir.join("rebase-apply").exists()
    }

    /// Clean up rebase directories (for abort operations)
    pub fn cleanup_rebase_directories(repo_path: &str) -> Result<(), RebaseIOError> {
        let git_dir = Path::new(repo_path).join(".git");

        // Remove rebase directories if they exist
        let rebase_merge = git_dir.join("rebase-merge");
        if rebase_merge.exists() {
            fs::remove_dir_all(&rebase_merge)?;
        }

        let rebase_apply = git_dir.join("rebase-apply");
        if rebase_apply.exists() {
            fs::remove_dir_all(&rebase_apply)?;
        }

        Ok(())
    }
}