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<Graph>,
tips: impl IntoIterator<Item = ObjectId>,
options: Options,
expire_time: Option<SystemTime>,
) -> Result<Option<Outcome>, Exn<Message>> {
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<ObjectId> = 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<T: Write>(out: std::io::BufWriter<T>) -> Result<T, Exn<Message>> {
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<Message>> {
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<Message>> {
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<Message>> {
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;
}