use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Write;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpEntry {
pub id: String,
pub timestamp: String,
pub command: String,
pub description: String,
pub before: RepoSnapshot,
pub after: RepoSnapshot,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoSnapshot {
pub head_oid: Option<String>,
pub head_ref: Option<String>,
pub branches: HashMap<String, String>,
pub tags: HashMap<String, String>,
pub index_tree_oid: Option<String>,
}
fn oplog_dir(path: &Path) -> PathBuf {
path.join(".git/securegit")
}
fn oplog_path(path: &Path) -> PathBuf {
oplog_dir(path).join("oplog.jsonl")
}
pub fn snapshot_repo(path: &Path) -> Result<RepoSnapshot> {
let repo = crate::ops::open_repo(path)?;
let head_oid = repo
.head()
.ok()
.and_then(|h| h.target())
.map(|oid| oid.to_string());
let head_ref = repo.head().ok().and_then(|h| {
if h.is_branch() {
h.name().map(|s| s.to_string())
} else {
None
}
});
let mut branches = HashMap::new();
if let Ok(branch_iter) = repo.branches(Some(git2::BranchType::Local)) {
for (branch, _) in branch_iter.flatten() {
if let (Ok(Some(name)), Some(oid)) = (branch.name(), branch.get().target()) {
branches.insert(name.to_string(), oid.to_string());
}
}
}
let mut tags = HashMap::new();
if let Ok(tag_names) = repo.tag_names(None) {
for tag_name in tag_names.iter().flatten() {
if let Ok(reference) = repo.find_reference(&format!("refs/tags/{}", tag_name)) {
if let Some(oid) = reference.target() {
tags.insert(tag_name.to_string(), oid.to_string());
}
}
}
}
let index_tree_oid = repo
.index()
.ok()
.and_then(|mut idx| idx.write_tree().ok())
.map(|oid| oid.to_string());
Ok(RepoSnapshot {
head_oid,
head_ref,
branches,
tags,
index_tree_oid,
})
}
pub fn with_oplog<F, T>(path: &Path, command: &str, description: &str, op: F) -> Result<T>
where
F: FnOnce() -> Result<T>,
{
let before = snapshot_repo(path).unwrap_or_else(|_| RepoSnapshot {
head_oid: None,
head_ref: None,
branches: HashMap::new(),
tags: HashMap::new(),
index_tree_oid: None,
});
let result = op()?;
let after = snapshot_repo(path).unwrap_or_else(|_| before.clone());
let entry = OpEntry {
id: generate_id(),
timestamp: now_iso8601(),
command: command.to_string(),
description: description.to_string(),
before,
after,
};
let _ = append_entry(path, &entry);
Ok(result)
}
pub fn read_oplog(path: &Path) -> Result<Vec<OpEntry>> {
let file_path = oplog_path(path);
if !file_path.exists() {
return Ok(Vec::new());
}
let content = std::fs::read_to_string(&file_path)?;
let mut entries: Vec<OpEntry> = content
.lines()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| serde_json::from_str(line).ok())
.collect();
entries.reverse(); Ok(entries)
}
const MAX_OPLOG_ENTRIES: usize = 500;
fn append_entry(path: &Path, entry: &OpEntry) -> Result<()> {
let dir = oplog_dir(path);
std::fs::create_dir_all(&dir)?;
let file_path = oplog_path(path);
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&file_path)?;
let json = serde_json::to_string(entry)?;
writeln!(file, "{}", json)?;
drop(file);
truncate_oplog(&file_path)?;
Ok(())
}
fn truncate_oplog(file_path: &Path) -> Result<()> {
let content = std::fs::read_to_string(file_path)?;
let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() > MAX_OPLOG_ENTRIES {
let keep = &lines[lines.len() - MAX_OPLOG_ENTRIES..];
let mut file = std::fs::File::create(file_path)?;
for line in keep {
writeln!(file, "{}", line)?;
}
}
Ok(())
}
fn generate_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
format!("{:x}", nanos)
}
pub fn now_iso8601_pub() -> String {
now_iso8601()
}
fn now_iso8601() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let days = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
let (year, month, day) = days_to_ymd(days);
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
year, month, day, hours, minutes, seconds
)
}
fn days_to_ymd(days_since_epoch: u64) -> (u64, u64, u64) {
let z = days_since_epoch + 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 };
(y, m, d)
}