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
}
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));
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');
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');
}
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())
}
}