branch_party_core 0.1.1

Core library for branch-party CLI tool
Documentation
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunReport {
    pub run_id: String,
    
    #[serde(with = "time::serde::rfc3339")]
    pub timestamp: OffsetDateTime,
    
    pub destination: String,
    pub base: String,
    pub sources: Vec<String>,
    pub merge_order: String,
    
    pub results: Vec<MergeResult>,
    
    pub created_branch: bool,
    pub pushed: bool,
    
    pub tag: Option<String>,
    pub duration_ms: u64,
    
    pub environment: EnvironmentInfo,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MergeResult {
    pub source: String,
    pub status: MergeStatus,
    pub commit: Option<String>,
    pub notes: String,
    pub conflicts: Vec<ConflictDetail>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MergeStatus {
    Merged,
    Failed,
    Skipped,
    Conflicted,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictDetail {
    pub path: String,
    pub resolution: String,
    pub details: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentInfo {
    pub tool_version: String,
    pub git_version: String,
    pub repository_path: String,
    pub user: String,
    pub host: String,
}

impl RunReport {
    pub fn new(
        destination: String,
        base: String,
        sources: Vec<String>,
        merge_order: String,
    ) -> Self {
        Self {
            run_id: Uuid::new_v4().to_string(),
            timestamp: OffsetDateTime::now_utc(),
            destination,
            base,
            sources,
            merge_order,
            results: Vec::new(),
            created_branch: false,
            pushed: false,
            tag: None,
            duration_ms: 0,
            environment: EnvironmentInfo::gather(),
        }
    }

    pub fn add_result(&mut self, result: MergeResult) {
        self.results.push(result);
    }

    pub fn set_duration(&mut self, duration_ms: u64) {
        self.duration_ms = duration_ms;
    }

    pub fn set_tag(&mut self, tag: String) {
        self.tag = Some(tag);
    }

    pub fn mark_branch_created(&mut self) {
        self.created_branch = true;
    }

    pub fn mark_pushed(&mut self) {
        self.pushed = true;
    }

    pub fn success_count(&self) -> usize {
        self.results.iter()
            .filter(|r| matches!(r.status, MergeStatus::Merged))
            .count()
    }

    pub fn failure_count(&self) -> usize {
        self.results.iter()
            .filter(|r| matches!(r.status, MergeStatus::Failed | MergeStatus::Conflicted))
            .count()
    }

    pub fn skip_count(&self) -> usize {
        self.results.iter()
            .filter(|r| matches!(r.status, MergeStatus::Skipped))
            .count()
    }

    pub fn is_success(&self) -> bool {
        self.failure_count() == 0
    }

    /// Generate a Markdown summary of the run report
    pub fn to_markdown(&self) -> String {
        let mut md = String::new();
        
        md.push_str(&format!("# Branch Party Run Report\n\n"));
        md.push_str(&format!("**Run ID:** `{}`\n", self.run_id));
        md.push_str(&format!("**Timestamp:** {}\n", self.timestamp.format(&time::format_description::well_known::Rfc3339).unwrap_or_else(|_| "Invalid timestamp".to_string())));
        md.push_str(&format!("**Duration:** {}ms\n\n", self.duration_ms));
        
        md.push_str(&format!("**Destination:** `{}`\n", self.destination));
        md.push_str(&format!("**Base Branch:** `{}`\n", self.base));
        md.push_str(&format!("**Merge Order:** {}\n\n", self.merge_order));

        // Summary
        let total = self.results.len();
        let success = self.success_count();
        let failed = self.failure_count();
        let skipped = self.skip_count();

        md.push_str("## Summary\n\n");
        md.push_str(&format!("- **Total sources:** {}\n", total));
        md.push_str(&format!("- **Successful merges:** {}\n", success));
        md.push_str(&format!("- **Failed merges:** {}\n", failed));
        md.push_str(&format!("- **Skipped:** {}\n", skipped));
        
        if self.created_branch {
            md.push_str("- **Branch created:** Yes\n");
        }
        if self.pushed {
            md.push_str("- **Pushed to remote:** Yes\n");
        }
        if let Some(ref tag) = self.tag {
            md.push_str(&format!("- **Tag:** `{}`\n", tag));
        }
        md.push('\n');

        // Results
        if !self.results.is_empty() {
            md.push_str("## Merge Results\n\n");
            md.push_str("| Source | Status | Commit | Notes |\n");
            md.push_str("|--------|--------|--------|-------|\n");

            for result in &self.results {
                let commit = result.commit.as_deref().unwrap_or("N/A");
                let commit_short = if commit.len() > 7 { &commit[..7] } else { commit };
                let status_icon = match result.status {
                    MergeStatus::Merged => "",
                    MergeStatus::Failed => "",
                    MergeStatus::Skipped => "⏭️",
                    MergeStatus::Conflicted => "⚠️",
                };

                md.push_str(&format!(
                    "| `{}` | {} {:?} | `{}` | {} |\n",
                    result.source,
                    status_icon,
                    result.status,
                    commit_short,
                    result.notes
                ));
            }
            md.push('\n');
        }

        // Environment
        md.push_str("## Environment\n\n");
        md.push_str(&format!("- **Tool Version:** {}\n", self.environment.tool_version));
        md.push_str(&format!("- **Git Version:** {}\n", self.environment.git_version));
        md.push_str(&format!("- **Repository:** {}\n", self.environment.repository_path));
        md.push_str(&format!("- **User:** {}\n", self.environment.user));
        md.push_str(&format!("- **Host:** {}\n", self.environment.host));

        md
    }
}

impl MergeResult {
    pub fn success(source: String, commit: String) -> Self {
        Self {
            source,
            status: MergeStatus::Merged,
            commit: Some(commit),
            notes: String::new(),
            conflicts: Vec::new(),
        }
    }

    pub fn failed(source: String, error: String) -> Self {
        Self {
            source,
            status: MergeStatus::Failed,
            commit: None,
            notes: error,
            conflicts: Vec::new(),
        }
    }

    pub fn skipped(source: String, reason: String) -> Self {
        Self {
            source,
            status: MergeStatus::Skipped,
            commit: None,
            notes: reason,
            conflicts: Vec::new(),
        }
    }

    pub fn conflicted(source: String, conflicts: Vec<ConflictDetail>) -> Self {
        let notes = format!("{} conflicts", conflicts.len());
        Self {
            source,
            status: MergeStatus::Conflicted,
            commit: None,
            notes,
            conflicts,
        }
    }

    pub fn with_notes(mut self, notes: String) -> Self {
        self.notes = notes;
        self
    }
}

impl EnvironmentInfo {
    pub fn gather() -> Self {
        let tool_version = env!("CARGO_PKG_VERSION").to_string();
        let git_version = Self::get_git_version();
        let repository_path = std::env::current_dir()
            .map(|p| p.to_string_lossy().to_string())
            .unwrap_or_else(|_| "unknown".to_string());
        let user = std::env::var("USER")
            .or_else(|_| std::env::var("USERNAME"))
            .unwrap_or_else(|_| "unknown".to_string());
        let host = std::env::var("HOSTNAME")
            .or_else(|_| std::env::var("COMPUTERNAME"))
            .unwrap_or_else(|_| "unknown".to_string());

        Self {
            tool_version,
            git_version,
            repository_path,
            user,
            host,
        }
    }

    fn get_git_version() -> String {
        std::process::Command::new("git")
            .args(&["--version"])
            .output()
            .ok()
            .and_then(|output| String::from_utf8(output.stdout).ok())
            .map(|s| s.trim().to_string())
            .unwrap_or_else(|| "unknown".to_string())
    }
}