//! Copyright (c) 2022-2026 GitDataAi All rights reserved. //! Snapshot and move operations. //! //! Core operations for creating, restoring, and verifying repository snapshots //! using git bundle. use std::path::Path; use crate::bare::GitBare; use crate::error::{GitError, GitResult}; use crate::snapshot::storage::SnapshotStorageBackend; /// Create a git bundle snapshot of a repository. /// Returns the bundle data as raw bytes. pub fn create_snapshot(gb: &GitBare) -> GitResult> { tracing::info!(repo = %gb.bare_dir.display(), "creating snapshot bundle"); let bare_dir_str = gb.bare_dir.to_string_lossy().into_owned(); let tmp_file = tempfile::Builder::new() .prefix("gitks_snapshot_") .suffix(".bundle") .tempfile_in(&gb.bare_dir) .map_err(GitError::Io)?; let bundle_path_str = tmp_file.path().to_string_lossy().into_owned(); let result = duct::cmd!( "git", "--git-dir", &bare_dir_str, "bundle", "create", &bundle_path_str, "--all" ) .stdout_capture() .stderr_capture() .unchecked() .run()?; if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr); return Err(GitError::CommandFailed { status_code: result.status.code(), stderr: stderr.into_owned(), }); } let bundle_path = tmp_file.path().to_path_buf(); let data = std::fs::read(&bundle_path).map_err(GitError::Io)?; let head_oid = get_head_oid(gb)?; tracing::info!( repo = %gb.bare_dir.display(), size_bytes = data.len(), head_oid = %head_oid, "snapshot bundle created" ); Ok(data) } /// Create a snapshot and store it using the given backend. pub fn create_and_store_snapshot( gb: &GitBare, relative_path: &str, storage: &dyn SnapshotStorageBackend, ) -> GitResult { let data = create_snapshot(gb)?; let head_oid = get_head_oid(gb)?; let snapshot_id = generate_snapshot_id(relative_path, &head_oid); storage .write_snapshot(&snapshot_id, relative_path, &head_oid, &data) .map_err(GitError::Internal)?; Ok(snapshot_id) } /// Restore a snapshot to a repository path. pub fn restore_snapshot(repo_path: &Path, data: &[u8]) -> GitResult<()> { tracing::info!(path = %repo_path.display(), size_bytes = data.len(), "restoring snapshot"); let applicator = crate::snapshot::sync::BundleApplicator::new(repo_path.to_path_buf()); applicator.apply_bundle(data).map_err(GitError::Internal)?; tracing::info!(path = %repo_path.display(), "snapshot restored"); Ok(()) } /// Restore a snapshot from storage backend. pub fn restore_from_storage( repo_path: &Path, snapshot_id: &str, storage: &dyn SnapshotStorageBackend, ) -> GitResult<()> { let data = storage .read_snapshot(snapshot_id) .map_err(GitError::Internal)?; restore_snapshot(repo_path, &data)?; Ok(()) } /// Verify that a restored snapshot matches the expected HEAD OID. pub fn verify_snapshot(gb: &GitBare, expected_head_oid: &str) -> GitResult { let actual_head_oid = get_head_oid(gb)?; if actual_head_oid == expected_head_oid { tracing::info!( repo = %gb.bare_dir.display(), expected = %expected_head_oid, actual = %actual_head_oid, "snapshot verification passed" ); Ok(true) } else { tracing::warn!( repo = %gb.bare_dir.display(), expected = %expected_head_oid, actual = %actual_head_oid, "snapshot verification failed: HEAD mismatch" ); Ok(false) } } /// Get HEAD OID of a repository (public helper for RPC). pub fn get_head_oid_internal(gb: &GitBare) -> GitResult { get_head_oid(gb) } /// Get HEAD OID of a repository. fn get_head_oid(gb: &GitBare) -> GitResult { let bare_dir_str = gb.bare_dir.to_string_lossy().into_owned(); let result = duct::cmd!("git", "--git-dir", &bare_dir_str, "rev-parse", "HEAD") .stdout_capture() .stderr_capture() .unchecked() .run()?; if result.status.success() { Ok(String::from_utf8_lossy(&result.stdout).trim().to_string()) } else { // Repository may be empty (no HEAD) Ok(String::new()) } } /// Generate a snapshot ID from relative_path and head_oid. fn generate_snapshot_id(relative_path: &str, head_oid: &str) -> String { use std::time::{SystemTime, UNIX_EPOCH}; let ts = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); use sha2::Digest; let mut hasher = sha2::Sha256::new(); hasher.update(relative_path.as_bytes()); hasher.update(head_oid.as_bytes()); hasher.update(ts.to_le_bytes()); let hash = hasher.finalize(); let mut s = String::with_capacity(16); for byte in &hash[..8] { use std::fmt::Write; let _ = write!(s, "{byte:02x}"); } s }