use std::{fmt::Formatter, io::Write}; use crate::{ file, store_impl::{packed, packed::Edit}, transaction::{Change, RefEdit}, Target, }; pub(crate) const HEADER_LINE: &[u8] = b"# pack-refs with: peeled fully-peeled sorted \n"; /// Access and instantiation impl packed::Transaction { pub(crate) fn new_from_pack_and_lock( buffer: Option, lock: gix_lock::File, ) -> Self { packed::Transaction { buffer, edits: None, lock: Some(lock), closed_lock: None, } } } impl std::fmt::Debug for packed::Transaction { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("packed::Transaction") .field("edits", &self.edits.as_ref().map(Vec::len)) .field("lock", &self.lock) .finish_non_exhaustive() } } /// Access impl packed::Transaction { /// Returns our packed buffer pub fn buffer(&self) -> Option<&packed::Buffer> { self.buffer.as_ref().map(|b| &***b) } } /// Lifecycle impl packed::Transaction { /// Prepare the transaction by checking all edits for applicability. /// Use `objects` to access objects for the purpose of peeling them - this is only used if packed-refs are involved. pub fn prepare( mut self, edits: &mut dyn Iterator, objects: &dyn gix_object::Find, ) -> Result { assert!(self.edits.is_none(), "BUG: cannot call prepare(…) more than once"); let buffer = &self.buffer; // Remove all edits which are deletions that aren't here in the first place let mut edits: Vec = edits .into_iter() .filter(|edit| { if let Change::Delete { .. } = edit.change { buffer.as_ref().map_or(true, |b| b.find(edit.name.as_ref()).is_ok()) } else { true } }) .map(|change| Edit { inner: change, peeled: None, }) .collect(); let mut buf = Vec::new(); for edit in &mut edits { if let Change::Update { new: Target::Peeled(new), .. } = edit.inner.change { let mut next_id = new; edit.peeled = loop { let kind = objects.try_find(&next_id, &mut buf)?.map(|d| d.kind); match kind { Some(gix_object::Kind::Tag) => { next_id = gix_object::TagRefIter::from_bytes(&buf).target_id().map_err(|_| { prepare::Error::Resolve( format!("Couldn't get target object id from tag {next_id}").into(), ) })?; } Some(_) => { break if next_id == new { None } else { Some(next_id) }; } None => { return Err(prepare::Error::Resolve( format!("Couldn't find object with id {next_id}").into(), )) } } }; } } if edits.is_empty() { self.closed_lock = self .lock .take() .map(gix_lock::File::close) .transpose() .map_err(prepare::Error::CloseLock)?; } else { // NOTE that we don't do any additional checks here but apply all edits unconditionally. // This is because this transaction system is internal and will be used correctly from the // loose ref store transactions, which do the necessary checking. } self.edits = Some(edits); Ok(self) } /// Commit the prepared transaction. /// /// Please note that actual edits invalidated existing packed buffers. /// Note: There is the potential to write changes into memory and return such a packed-refs buffer for reuse. pub fn commit(self) -> Result<(), commit::Error> { let mut edits = self.edits.expect("BUG: cannot call commit() before prepare(…)"); if edits.is_empty() { return Ok(()); } let mut file = self.lock.expect("a write lock for applying changes"); let refs_sorted: Box, packed::iter::Error>>> = match self.buffer.as_ref() { Some(buffer) => Box::new(buffer.iter()?), None => Box::new(std::iter::empty()), }; let mut refs_sorted = refs_sorted.peekable(); edits.sort_by(|l, r| l.inner.name.as_bstr().cmp(r.inner.name.as_bstr())); let mut peekable_sorted_edits = edits.iter().peekable(); file.with_mut(|f| f.write_all(HEADER_LINE))?; let mut num_written_lines = 0; loop { match (refs_sorted.peek(), peekable_sorted_edits.peek()) { (Some(Err(_)), _) => { let err = refs_sorted.next().expect("next").expect_err("err"); return Err(commit::Error::Iteration(err)); } (None, None) => { break; } (Some(Ok(_)), None) => { let pref = refs_sorted.next().expect("next").expect("no err"); num_written_lines += 1; file.with_mut(|out| write_packed_ref(out, pref))?; } (Some(Ok(pref)), Some(edit)) => { use std::cmp::Ordering::*; match pref.name.as_bstr().cmp(edit.inner.name.as_bstr()) { Less => { let pref = refs_sorted.next().expect("next").expect("valid"); num_written_lines += 1; file.with_mut(|out| write_packed_ref(out, pref))?; } Greater => { let edit = peekable_sorted_edits.next().expect("next"); file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?; } Equal => { let _pref = refs_sorted.next().expect("next").expect("valid"); let edit = peekable_sorted_edits.next().expect("next"); file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?; } } } (None, Some(_)) => { let edit = peekable_sorted_edits.next().expect("next"); file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?; } } } if num_written_lines == 0 { std::fs::remove_file(file.resource_path())?; } else { file.commit()?; } drop(refs_sorted); Ok(()) } } fn write_packed_ref(out: &mut dyn std::io::Write, pref: packed::Reference<'_>) -> std::io::Result<()> { write!(out, "{} ", pref.target)?; out.write_all(pref.name.as_bstr())?; out.write_all(b"\n")?; if let Some(object) = pref.object { writeln!(out, "^{object}")?; } Ok(()) } fn write_edit(out: &mut dyn std::io::Write, edit: &Edit, lines_written: &mut i32) -> std::io::Result<()> { match edit.inner.change { Change::Delete { .. } => {} Change::Update { new: Target::Peeled(target_oid), .. } => { write!(out, "{target_oid} ")?; out.write_all(edit.inner.name.as_bstr())?; out.write_all(b"\n")?; if let Some(object) = edit.peeled { writeln!(out, "^{object}")?; } *lines_written += 1; } Change::Update { new: Target::Symbolic(_), .. } => unreachable!("BUG: packed refs cannot contain symbolic refs, catch that in prepare(…)"), } Ok(()) } /// Convert this buffer to be used as the basis for a transaction. pub(crate) fn buffer_into_transaction( buffer: file::packed::SharedBufferSnapshot, lock_mode: gix_lock::acquire::Fail, ) -> Result { let lock = gix_lock::File::acquire_to_update_resource(&buffer.path, lock_mode, None)?; Ok(packed::Transaction { buffer: Some(buffer), lock: Some(lock), closed_lock: None, edits: None, }) } /// pub mod prepare { /// The error used in [`Transaction::prepare(…)`][crate::file::Transaction::prepare()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Could not close a lock which won't ever be committed")] CloseLock(#[from] std::io::Error), #[error("The lookup of an object failed while peeling it")] Resolve(#[from] Box), } } /// pub mod commit { use crate::store_impl::packed; /// The error used in [`Transaction::commit(…)`][crate::file::Transaction::commit()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Changes to the resource could not be committed")] Commit(#[from] gix_lock::commit::Error), #[error("Some references in the packed refs buffer could not be parsed")] Iteration(#[from] packed::iter::Error), #[error("Failed to write a ref line to the packed ref file")] Io(#[from] std::io::Error), } }