use std::{ io::Write, path::{Path, PathBuf}, time::SystemTime, }; use gix_error::{Exn, Message, ResultExt, message}; use gix_hash::ObjectId; use gix_object::Find; use super::{Options, Outcome, Split, write}; use crate::Graph; /// Write the commit-graph of the objects whose `info` directory is `info_dir`. /// /// `graph` must be the graph in `info_dir`, if any. It's closed before files are replaced. /// See [`write()`] for `objects`, `tips` and `options`. /// /// * With [`Split::No`], `info_dir/commit-graph` is written, and the chain in `info_dir/commit-graphs` removed. /// * Otherwise, a layer is written to `info_dir/commit-graphs` and `commit-graph-chain` is updated to end with it. /// If `info_dir/commit-graph` stays below it, it becomes the bottom layer of the chain, otherwise it's removed. /// /// Afterwards, `.graph` files in `info_dir/commit-graphs` that aren't in the chain and weren't modified after /// `expire_time` are deleted. Merged layers count as modified now and `expire_time` defaults to now, /// so by default they are deleted right away. /// /// Returns `None` and changes nothing if there was nothing to write. pub fn write_to_info_dir( objects: &(impl Find + Clone + Send), info_dir: &Path, graph: Option, tips: impl IntoIterator, options: Options, expire_time: Option, ) -> Result, Exn> { let graphs_dir = info_dir.join("commit-graphs"); let layers: Vec<(PathBuf, ObjectId)> = graph .iter() .flat_map(|graph| graph.files.iter()) .map(|file| (file.path().to_owned(), file.checksum().to_owned())) .collect(); let lock = |path: &Path| { gix_lock::File::acquire_to_update_resource(path, gix_lock::acquire::Fail::Immediately, None) .or_raise(|| message!("Could not lock '{}'", path.display())) }; let (outcome, chain) = if options.split == Split::No { let path = info_dir.join("commit-graph"); let mut out = std::io::BufWriter::new(lock(&path)?); let Some(outcome) = write(objects, graph.as_ref(), tips, options, &mut out)? else { return Ok(None); }; drop(graph); commit(into_inner(out)?)?; make_read_only(&path); remove(&graphs_dir.join("commit-graph-chain"))?; (outcome, Vec::new()) } else { std::fs::create_dir_all(&graphs_dir) .or_raise(|| message!("Could not create directory '{}'", graphs_dir.display()))?; // Hold the chain's lock while writing the layer. let chain_path = graphs_dir.join("commit-graph-chain"); let mut chain_lock = lock(&chain_path)?; let layer = gix_tempfile::new( &graphs_dir, gix_tempfile::ContainingDirectory::Exists, gix_tempfile::AutoRemove::Tempfile, ) .or_raise(|| { message!( "Could not create a temporary file in '{}'", graphs_dir.display() ) })?; let mut out = std::io::BufWriter::new(layer); let Some(outcome) = write(objects, graph.as_ref(), tips, options, &mut out)? else { return Ok(None); }; drop(graph); let layer = into_inner(out)?; let layer_path = |checksum: &ObjectId| graphs_dir.join(format!("graph-{checksum}.graph")); let base_layers = &layers[..outcome.num_base_layers]; match base_layers.last() { // A monolithic `commit-graph` kept below the new layer moves into the chain. Some((path, checksum)) if *path != layer_path(checksum) => { std::fs::rename(path, layer_path(checksum)).or_raise(|| { message!("Could not rename '{}' into the chain", path.display()) })?; } Some(_) => {} None => remove(&info_dir.join("commit-graph"))?, } let path = layer_path(&outcome.checksum); layer .persist(&path) .map_err(|err| err.error) .or_raise(|| message!("Could not write commit-graph layer '{}'", path.display()))?; make_read_only(&path); let chain: Vec = base_layers .iter() .map(|(_, checksum)| *checksum) .chain(Some(outcome.checksum)) .collect(); for checksum in &chain { writeln!(chain_lock, "{checksum}") .or_raise(|| message!("Could not write commit-graph chain"))?; } commit(chain_lock)?; make_read_only(&chain_path); let now = SystemTime::now(); for (path, _) in &layers[outcome.num_base_layers..] { if let Ok(file) = std::fs::File::open(path) { file.set_modified(now).ok(); } } (outcome, chain.iter().map(layer_path).collect()) }; expire( &graphs_dir, &chain, expire_time.unwrap_or_else(SystemTime::now), )?; Ok(Some(outcome)) } fn into_inner(out: std::io::BufWriter) -> Result> { out.into_inner() .map_err(std::io::IntoInnerError::into_error) .or_raise(|| message!("Could not write commit-graph")) } fn commit(lock: gix_lock::File) -> Result<(), Exn> { let path = lock.resource_path(); lock.commit() .map_err(|err| err.error) .or_raise(|| message!("Could not write '{}'", path.display()))?; Ok(()) } /// Delete the `.graph` files in `graphs_dir` that aren't in `chain` and weren't modified after `expire_time`. fn expire( graphs_dir: &Path, chain: &[PathBuf], expire_time: SystemTime, ) -> Result<(), Exn> { let Ok(entries) = std::fs::read_dir(graphs_dir) else { return Ok(()); }; for entry in entries.flatten() { let path = entry.path(); let expired = entry .metadata() .and_then(|meta| meta.modified()) .is_ok_and(|modified| modified <= expire_time); if expired && path.extension().is_some_and(|ext| ext == "graph") && !chain.contains(&path) { std::fs::remove_file(&path).ok(); } } Ok(()) } fn remove(path: &Path) -> Result<(), Exn> { match std::fs::remove_file(path) { Err(err) if err.kind() != std::io::ErrorKind::NotFound => { Err(err).or_raise(|| message!("Could not remove '{}'", path.display())) } _ => Ok(()), } } fn make_read_only(path: &Path) { #[cfg(unix)] if let Ok(meta) = std::fs::metadata(path) { let mut permissions = meta.permissions(); permissions.set_readonly(true); std::fs::set_permissions(path, permissions).ok(); } #[cfg(not(unix))] let _ = path; }