use crate::cli::UI;
use crate::ops::utils::short_oid as fmt_short_oid;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotInfo {
pub ref_name: String,
pub oid: String,
pub timestamp: i64,
pub message: String,
}
pub fn create(path: &Path, message: Option<&str>, ui: &UI) -> Result<SnapshotInfo> {
let repo = crate::ops::open_repo(path)?;
let mut index = repo.index()?;
index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
let tree_oid = index.write_tree()?;
if let Ok(head) = repo.head() {
if let Ok(tree) = head.peel_to_tree() {
let mut original_index = repo.index()?;
original_index.read_tree(&tree)?;
}
}
let tree = repo.find_tree(tree_oid)?;
let sig = repo
.signature()
.unwrap_or_else(|_| git2::Signature::now("securegit", "snapshot@securegit.local").unwrap());
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let msg = message.unwrap_or("auto-snapshot");
let full_msg = format!("securegit snapshot: {}", msg);
let oid = repo.commit(None, &sig, &sig, &full_msg, &tree, &[])?;
let ref_name = format!("refs/snapshots/{}", timestamp);
repo.reference(&ref_name, oid, true, "securegit snapshot")?;
let hash = fmt_short_oid(&oid);
let info = SnapshotInfo {
ref_name: ref_name.clone(),
oid: oid.to_string(),
timestamp,
message: msg.to_string(),
};
ui.success(format!("Snapshot created: {} ({})", hash, msg));
Ok(info)
}
pub fn list(path: &Path, count: usize, ui: &UI) -> Result<Vec<SnapshotInfo>> {
let repo = crate::ops::open_repo(path)?;
let mut snapshots = Vec::new();
let refs = crate::ops::refstore::list_refs_with_prefix(&repo, "refs/snapshots/")?;
for ref_name in &refs {
if let Ok(reference) = repo.find_reference(ref_name) {
if let Some(oid) = reference.target() {
if let Ok(commit) = repo.find_commit(oid) {
let ts_str = ref_name.strip_prefix("refs/snapshots/").unwrap_or("0");
let timestamp: i64 = ts_str.parse().unwrap_or(0);
let msg = commit
.message()
.unwrap_or("")
.strip_prefix("securegit snapshot: ")
.unwrap_or(commit.message().unwrap_or(""))
.to_string();
snapshots.push(SnapshotInfo {
ref_name: ref_name.clone(),
oid: oid.to_string(),
timestamp,
message: msg,
});
}
}
}
}
snapshots.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
snapshots.truncate(count);
if snapshots.is_empty() {
ui.info("No snapshots found");
} else {
ui.section("Snapshots (newest first)");
for snap in &snapshots {
let short_oid = &snap.oid[..7.min(snap.oid.len())];
ui.list_item(format!(
"{} {} — {}",
short_oid,
format_timestamp(snap.timestamp),
snap.message
));
}
}
Ok(snapshots)
}
pub fn restore(path: &Path, id: &str, ui: &UI) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let commit = if let Ok(oid) = git2::Oid::from_str(id) {
repo.find_commit(oid)?
} else {
let ref_name = format!("refs/snapshots/{}", id);
if let Ok(reference) = repo.find_reference(&ref_name) {
let oid = reference
.target()
.ok_or_else(|| anyhow::anyhow!("Invalid snapshot ref"))?;
repo.find_commit(oid)?
} else {
let refs = crate::ops::refstore::list_refs_with_prefix(&repo, "refs/snapshots/")?;
let mut found = None;
for ref_name in &refs {
if let Ok(reference) = repo.find_reference(ref_name) {
if let Some(oid) = reference.target() {
if oid.to_string().starts_with(id) {
found = Some(repo.find_commit(oid)?);
break;
}
}
}
}
found.ok_or_else(|| anyhow::anyhow!("Snapshot '{}' not found", id))?
}
};
let mut checkout = git2::build::CheckoutBuilder::new();
checkout.force();
repo.checkout_tree(commit.as_object(), Some(&mut checkout))?;
let hash = fmt_short_oid(&commit.id());
ui.success(format!("Restored working tree to snapshot {}", hash));
Ok(())
}
pub fn prune(path: &Path, older_than: &str, dry_run: bool, ui: &UI) -> Result<()> {
let retention_secs = parse_retention(older_than)?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let cutoff = now - retention_secs as i64;
let repo = crate::ops::open_repo(path)?;
let refs = crate::ops::refstore::list_refs_with_prefix(&repo, "refs/snapshots/")?;
let mut pruned = 0;
for ref_name in &refs {
let ts_str = ref_name.strip_prefix("refs/snapshots/").unwrap_or("0");
let timestamp: i64 = ts_str.parse().unwrap_or(0);
if timestamp < cutoff {
if dry_run {
ui.list_item(format!(
"Would prune: {} ({})",
ref_name,
format_timestamp(timestamp)
));
} else {
crate::ops::refstore::delete_ref(&repo, ref_name)?;
ui.success(format!("Pruned: {}", ref_name));
}
pruned += 1;
}
}
if pruned == 0 {
ui.info("No snapshots to prune");
} else if dry_run {
ui.info(format!("{} snapshot(s) would be pruned", pruned));
} else {
ui.success(format!("Pruned {} snapshot(s)", pruned));
}
Ok(())
}
fn parse_retention(s: &str) -> Result<u64> {
let s = s.trim();
if let Some(days) = s.strip_suffix('d') {
Ok(days.parse::<u64>()? * 86400)
} else if let Some(hours) = s.strip_suffix('h') {
Ok(hours.parse::<u64>()? * 3600)
} else if let Some(mins) = s.strip_suffix('m') {
Ok(mins.parse::<u64>()? * 60)
} else {
Ok(s.parse::<u64>()?)
}
}
fn format_timestamp(ts: i64) -> String {
let secs = ts as u64;
let days = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let z = days + 719468;
let era = z / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{:04}-{:02}-{:02} {:02}:{:02}Z", y, m, d, hours, minutes)
}