//! OSTree Commit Management for APT-OSTree //! //! This module implements OSTree commit management for package layering, //! providing atomic operations, rollback support, and commit history tracking. use std::path::{Path, PathBuf}; use tracing::{info, warn, debug}; use serde::{Serialize, Deserialize}; use chrono::{DateTime, Utc}; use crate::error::{AptOstreeError, AptOstreeResult}; use crate::dependency_resolver::DebPackageMetadata; /// OSTree commit metadata #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OstreeCommitMetadata { pub commit_id: String, pub parent_commit: Option, pub timestamp: DateTime, pub subject: String, pub body: String, pub author: String, pub packages_added: Vec, pub packages_removed: Vec, pub packages_modified: Vec, pub layer_level: usize, pub deployment_type: DeploymentType, pub checksum: String, } /// Deployment type #[derive(Debug, Clone, Serialize, Deserialize)] pub enum DeploymentType { Base, PackageLayer, SystemUpdate, Rollback, Custom, } /// OSTree commit manager pub struct OstreeCommitManager { repo_path: PathBuf, branch_name: String, current_commit: Option, commit_history: Vec, layer_counter: usize, } /// Commit creation options #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CommitOptions { pub subject: String, pub body: Option, pub author: Option, pub layer_level: Option, pub deployment_type: DeploymentType, pub dry_run: bool, } /// Commit result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CommitResult { pub success: bool, pub commit_id: Option, pub parent_commit: Option, pub metadata: Option, pub error_message: Option, } impl Default for CommitOptions { fn default() -> Self { Self { subject: "Package layer update".to_string(), body: None, author: Some("apt-ostree ".to_string()), layer_level: None, deployment_type: DeploymentType::PackageLayer, dry_run: false, } } } impl OstreeCommitManager { /// Create a new OSTree commit manager pub fn new(repo_path: PathBuf, branch_name: String) -> AptOstreeResult { info!("Creating OSTree commit manager for branch: {} at {}", branch_name, repo_path.display()); // Ensure repository exists if !repo_path.exists() { return Err(AptOstreeError::Ostree( format!("OSTree repository not found: {}", repo_path.display()) )); } Ok(Self { repo_path, branch_name, current_commit: None, commit_history: Vec::new(), layer_counter: 0, }) } /// Initialize commit manager pub async fn initialize(&mut self) -> AptOstreeResult<()> { info!("Initializing OSTree commit manager"); // Get current commit self.current_commit = self.get_current_commit().await?; // Load commit history self.load_commit_history().await?; // Initialize layer counter self.layer_counter = self.get_next_layer_level(); info!("OSTree commit manager initialized. Current commit: {:?}, Layer counter: {}", self.current_commit, self.layer_counter); Ok(()) } /// Get current commit pub async fn get_current_commit(&self) -> AptOstreeResult> { let output = std::process::Command::new("ostree") .args(&["rev-parse", &self.branch_name]) .current_dir(&self.repo_path) .output(); match output { Ok(output) => { if output.status.success() { let commit_id = String::from_utf8_lossy(&output.stdout).trim().to_string(); Ok(Some(commit_id)) } else { warn!("No current commit found for branch: {}", self.branch_name); Ok(None) } } Err(e) => { warn!("Failed to get current commit: {}", e); Ok(None) } } } /// Load commit history async fn load_commit_history(&mut self) -> AptOstreeResult<()> { debug!("Loading commit history"); if let Some(current_commit) = &self.current_commit { let output = std::process::Command::new("ostree") .args(&["log", current_commit]) .current_dir(&self.repo_path) .output(); if let Ok(output) = output { if output.status.success() { self.parse_commit_log(&output.stdout)?; } } } info!("Loaded {} commits from history", self.commit_history.len()); Ok(()) } /// Parse commit log fn parse_commit_log(&mut self, log_output: &[u8]) -> AptOstreeResult<()> { let log_text = String::from_utf8_lossy(log_output); let lines: Vec<&str> = log_text.lines().collect(); let mut current_commit: Option = None; for line in lines { if line.starts_with("commit ") { // Save previous commit if exists if let Some(commit) = current_commit.take() { self.commit_history.push(commit); } // Start new commit let commit_id = line[7..].trim(); current_commit = Some(OstreeCommitMetadata { commit_id: commit_id.to_string(), parent_commit: None, timestamp: Utc::now(), subject: String::new(), body: String::new(), author: String::new(), packages_added: Vec::new(), packages_removed: Vec::new(), packages_modified: Vec::new(), layer_level: 0, deployment_type: DeploymentType::Custom, checksum: String::new(), }); } else if let Some(ref mut commit) = current_commit { if line.starts_with("Subject: ") { commit.subject = line[9..].trim().to_string(); } else if line.starts_with("Author: ") { commit.author = line[8..].trim().to_string(); } else if line.starts_with("Date: ") { // Parse date if needed } else if !line.is_empty() && !line.starts_with(" ") { // Body content commit.body.push_str(line); commit.body.push('\n'); } } } // Save last commit if let Some(commit) = current_commit { self.commit_history.push(commit); } Ok(()) } /// Create a new commit with package changes pub async fn create_package_commit( &mut self, packages_added: &[DebPackageMetadata], packages_removed: &[String], options: CommitOptions, ) -> AptOstreeResult { info!("Creating package commit with {} added, {} removed packages", packages_added.len(), packages_removed.len()); if options.dry_run { info!("DRY RUN: Would create commit with subject: {}", options.subject); return Ok(CommitResult { success: true, commit_id: None, parent_commit: self.current_commit.clone(), metadata: None, error_message: Some("Dry run mode".to_string()), }); } // Prepare commit metadata let layer_level = options.layer_level.unwrap_or_else(|| { self.layer_counter += 1; self.layer_counter }); let packages_added_names: Vec = packages_added.iter() .map(|pkg| pkg.name.clone()) .collect(); let metadata = OstreeCommitMetadata { commit_id: String::new(), // Will be set after commit parent_commit: self.current_commit.clone(), timestamp: Utc::now(), subject: options.subject, body: options.body.unwrap_or_default(), author: options.author.unwrap_or_else(|| "apt-ostree ".to_string()), packages_added: packages_added_names, packages_removed: packages_removed.to_vec(), packages_modified: Vec::new(), layer_level, deployment_type: options.deployment_type, checksum: String::new(), }; // Create OSTree commit let commit_id = self.create_ostree_commit(&metadata).await?; // Update metadata with commit ID let mut final_metadata = metadata.clone(); final_metadata.commit_id = commit_id.clone(); // Add to history self.commit_history.push(final_metadata.clone()); // Update current commit self.current_commit = Some(commit_id.clone()); info!("Created package commit: {} (layer: {})", commit_id, layer_level); Ok(CommitResult { success: true, commit_id: Some(commit_id), parent_commit: metadata.parent_commit, metadata: Some(final_metadata), error_message: None, }) } /// Create OSTree commit pub async fn create_ostree_commit(&self, metadata: &OstreeCommitMetadata) -> AptOstreeResult { debug!("Creating OSTree commit with subject: {}", metadata.subject); // Prepare commit message let commit_message = self.format_commit_message(metadata); // Create temporary commit message file let temp_dir = std::env::temp_dir(); let message_file = temp_dir.join(format!("apt-ostree-commit-{}.msg", chrono::Utc::now().timestamp())); std::fs::write(&message_file, commit_message)?; // Build ostree commit command let mut cmd = std::process::Command::new("/usr/bin/ostree"); cmd.args(&["commit", "--branch", &self.branch_name]); if let Some(parent) = &metadata.parent_commit { cmd.args(&["--parent", parent]); } cmd.args(&["--body-file", message_file.to_str().unwrap()]); cmd.current_dir(&self.repo_path); // Execute commit let output = cmd.output() .map_err(|e| AptOstreeError::Ostree(format!("Failed to create OSTree commit: {}", e)))?; // Clean up message file let _ = std::fs::remove_file(&message_file); if !output.status.success() { let error_msg = String::from_utf8_lossy(&output.stderr); return Err(AptOstreeError::Ostree( format!("OSTree commit failed: {}", error_msg) )); } // Get commit ID from output let commit_id = String::from_utf8_lossy(&output.stdout).trim().to_string(); Ok(commit_id) } /// Format commit message fn format_commit_message(&self, metadata: &OstreeCommitMetadata) -> String { let mut message = format!("{}\n\n", metadata.subject); if !metadata.body.is_empty() { message.push_str(&metadata.body); message.push_str("\n\n"); } message.push_str("Package Changes:\n"); if !metadata.packages_added.is_empty() { message.push_str("Added:\n"); for package in &metadata.packages_added { message.push_str(&format!(" + {}\n", package)); } message.push('\n'); } if !metadata.packages_removed.is_empty() { message.push_str("Removed:\n"); for package in &metadata.packages_removed { message.push_str(&format!(" - {}\n", package)); } message.push('\n'); } if !metadata.packages_modified.is_empty() { message.push_str("Modified:\n"); for package in &metadata.packages_modified { message.push_str(&format!(" ~ {}\n", package)); } message.push('\n'); } message.push_str(&format!("Layer Level: {}\n", metadata.layer_level)); message.push_str(&format!("Deployment Type: {:?}\n", metadata.deployment_type)); message.push_str(&format!("Timestamp: {}\n", metadata.timestamp)); message.push_str(&format!("Author: {}\n", metadata.author)); message } /// Rollback to previous commit pub async fn rollback_to_commit(&mut self, commit_id: &str) -> AptOstreeResult { info!("Rolling back to commit: {}", commit_id); // Verify commit exists if !self.commit_exists(commit_id).await? { return Err(AptOstreeError::Ostree( format!("Commit not found: {}", commit_id) )); } // Create rollback commit let options = CommitOptions { subject: format!("Rollback to commit {}", commit_id), body: Some(format!("Rolling back from {} to {}", self.current_commit.as_deref().unwrap_or("none"), commit_id)), author: Some("apt-ostree ".to_string()), layer_level: Some(self.layer_counter + 1), deployment_type: DeploymentType::Rollback, dry_run: false, }; let rollback_metadata = OstreeCommitMetadata { commit_id: String::new(), parent_commit: self.current_commit.clone(), timestamp: Utc::now(), subject: options.subject.clone(), body: options.body.clone().unwrap_or_default(), author: options.author.clone().unwrap_or_default(), packages_added: Vec::new(), packages_removed: Vec::new(), packages_modified: Vec::new(), layer_level: options.layer_level.unwrap_or(0), deployment_type: DeploymentType::Rollback, checksum: String::new(), }; // Create rollback commit let new_commit_id = self.create_ostree_commit(&rollback_metadata).await?; // Update current commit self.current_commit = Some(new_commit_id.clone()); // Add to history let parent_commit = rollback_metadata.parent_commit.clone(); let mut final_metadata = rollback_metadata; final_metadata.commit_id = new_commit_id.clone(); self.commit_history.push(final_metadata.clone()); info!("Rollback completed to commit: {}", new_commit_id); Ok(CommitResult { success: true, commit_id: Some(new_commit_id), parent_commit, metadata: Some(final_metadata), error_message: None, }) } /// Check if commit exists async fn commit_exists(&self, commit_id: &str) -> AptOstreeResult { let output = std::process::Command::new("/usr/bin/ostree") .args(&["show", commit_id]) .current_dir(&self.repo_path) .output(); match output { Ok(output) => Ok(output.status.success()), Err(_) => Ok(false), } } /// Get commit history pub fn get_commit_history(&self) -> &[OstreeCommitMetadata] { &self.commit_history } /// Get next layer level fn get_next_layer_level(&self) -> usize { self.commit_history.iter() .map(|commit| commit.layer_level) .max() .unwrap_or(0) + 1 } /// Get commits by layer level pub fn get_commits_by_layer(&self, layer_level: usize) -> Vec<&OstreeCommitMetadata> { self.commit_history.iter() .filter(|commit| commit.layer_level == layer_level) .collect() } /// Get commits by deployment type pub fn get_commits_by_type(&self, deployment_type: &DeploymentType) -> Vec<&OstreeCommitMetadata> { self.commit_history.iter() .filter(|commit| std::mem::discriminant(&commit.deployment_type) == std::mem::discriminant(deployment_type)) .collect() } /// Get commit metadata pub fn get_commit_metadata(&self, commit_id: &str) -> Option<&OstreeCommitMetadata> { self.commit_history.iter() .find(|commit| commit.commit_id == commit_id) } /// Get repository path pub fn get_repo_path(&self) -> &Path { &self.repo_path } /// Get branch name pub fn get_branch_name(&self) -> &str { &self.branch_name } /// Get layer counter pub fn get_layer_counter(&self) -> usize { self.layer_counter } }