use std::collections::HashMap;
use crate::inventory::Entry;
use crate::inventory_delta::{InventoryDelta, InventoryDeltaEntry};
use crate::repository::{Repository, RepositoryError};
use crate::workingtree::{EntryKind, WorkingTreeChange};
use crate::{FileId, RevisionId};
pub struct CommitBuilder<'a> {
repository: &'a mut dyn Repository,
parents: Vec<Vec<u8>>,
new_revision_id: Vec<u8>,
committer: String,
timestamp: u64,
timezone: i32,
properties: HashMap<String, Vec<u8>>,
delta: Vec<InventoryDeltaEntry>,
inventory_sha1: Option<Vec<u8>>,
}
impl<'a> CommitBuilder<'a> {
pub(super) fn new(
repository: &'a mut dyn Repository,
parents: Vec<Vec<u8>>,
new_revision_id: Vec<u8>,
committer: String,
timestamp: u64,
timezone: i32,
) -> Self {
CommitBuilder {
repository,
parents,
new_revision_id,
committer,
timestamp,
timezone,
properties: HashMap::new(),
delta: Vec::new(),
inventory_sha1: None,
}
}
pub fn with_properties(mut self, properties: HashMap<String, Vec<u8>>) -> Self {
self.properties = properties;
self
}
pub fn any_changes(&self) -> bool {
if self.parents.len() > 1 {
return true;
}
let basis_is_null = self.basis_revision_id() == crate::branch::NULL_REVISION;
if basis_is_null {
self.delta.len() > 1
} else {
!self.delta.is_empty()
}
}
fn basis_revision_id(&self) -> Vec<u8> {
self.parents
.first()
.cloned()
.unwrap_or_else(|| crate::branch::NULL_REVISION.to_vec())
}
pub fn record_iter_changes<F>(
&mut self,
changes: &[WorkingTreeChange],
mut get_file_text: F,
) -> Result<(), RepositoryError>
where
F: FnMut(&str) -> Result<Vec<u8>, RepositoryError>,
{
let new_rev = RevisionId::from(self.new_revision_id.as_slice());
for change in changes {
match (&change.old_path, &change.new_path) {
(old @ Some(_), None) => {
self.delta.push(InventoryDeltaEntry {
old_path: old.clone(),
new_path: None,
file_id: FileId::from(change.file_id.as_slice()),
new_entry: None,
});
}
(_, Some(new_path)) => {
let kind = change.new_kind.ok_or_else(|| {
RepositoryError::Corrupt(format!(
"change for {new_path} has a new path but no kind"
))
})?;
let merges_parents = change.text_parents.len() > 1;
let carried_over = !change.content_change
&& !merges_parents
&& change.basis_revision.is_some();
let entry_revision = if carried_over {
RevisionId::from(change.basis_revision.as_deref().unwrap())
} else {
new_rev.clone()
};
let entry =
self.build_entry(change, kind, &entry_revision, &mut get_file_text)?;
self.delta.push(InventoryDeltaEntry {
old_path: change.old_path.clone(),
new_path: Some(new_path.clone()),
file_id: FileId::from(change.file_id.as_slice()),
new_entry: Some(entry),
});
}
(None, None) => {}
}
}
Ok(())
}
fn build_entry<F>(
&mut self,
change: &WorkingTreeChange,
kind: EntryKind,
revision: &RevisionId,
get_file_text: &mut F,
) -> Result<Entry, RepositoryError>
where
F: FnMut(&str) -> Result<Vec<u8>, RepositoryError>,
{
let file_id = FileId::from(change.file_id.as_slice());
let new_path = change.new_path.as_deref().unwrap_or("");
let merges_parents = change.text_parents.len() > 1;
let writes_text =
change.content_change || change.basis_revision.is_none() || merges_parents;
if new_path.is_empty() && change.new_parent_id.is_none() {
if writes_text && self.repository.format().rich_root_data {
self.repository
.add_text(&change.file_id, &self.new_revision_id, &[], b"")?;
}
return Ok(Entry::root(file_id, Some(revision.clone())));
}
let name = change.new_name.clone().ok_or_else(|| {
RepositoryError::Corrupt(format!("change for {new_path} has no name"))
})?;
let parent_id = FileId::from(
change
.new_parent_id
.as_deref()
.unwrap_or(crate::inventory::ROOT_ID),
);
let text_parents: Vec<(Vec<u8>, Vec<u8>)> = change
.text_parents
.iter()
.map(|rev| (change.file_id.clone(), rev.clone()))
.collect();
match kind {
EntryKind::File => {
let content = get_file_text(new_path)?;
let sha1 = crate::weave::sha_strings(&[content.as_slice()]);
let size = content.len() as u64;
if writes_text {
self.repository.add_text(
&change.file_id,
&self.new_revision_id,
&text_parents,
&content,
)?;
}
Ok(Entry::file(
file_id,
name,
parent_id,
Some(revision.clone()),
Some(sha1),
Some(size),
Some(change.new_executable),
None,
))
}
EntryKind::Directory => {
if writes_text {
self.repository.add_text(
&change.file_id,
&self.new_revision_id,
&text_parents,
b"",
)?;
}
Ok(Entry::directory(
file_id,
name,
parent_id,
Some(revision.clone()),
))
}
EntryKind::Symlink => {
if writes_text {
self.repository.add_text(
&change.file_id,
&self.new_revision_id,
&text_parents,
b"",
)?;
}
let target = String::from_utf8_lossy(&get_file_text(new_path)?).into_owned();
Ok(Entry::link(
file_id,
name,
parent_id,
Some(revision.clone()),
Some(target),
))
}
EntryKind::TreeReference => Err(RepositoryError::Corrupt(
"tree references are not supported in commit".to_string(),
)),
}
}
pub fn finish_inventory(&mut self) -> Result<Vec<u8>, RepositoryError> {
let delta = InventoryDelta(std::mem::take(&mut self.delta));
let basis = self.basis_revision_id();
let sha1 = self.repository.add_inventory_by_delta(
&basis,
&delta,
&self.new_revision_id,
&self.parents,
)?;
self.inventory_sha1 = Some(sha1.clone());
Ok(sha1)
}
pub fn commit(&mut self, message: &str) -> Result<Vec<u8>, RepositoryError> {
let inventory_sha1 = self.inventory_sha1.clone().ok_or_else(|| {
RepositoryError::Corrupt("commit() called before finish_inventory()".to_string())
})?;
let revision = crate::revision::Revision::new(
RevisionId::from(self.new_revision_id.as_slice()),
self.parents
.iter()
.map(|p| RevisionId::from(p.as_slice()))
.collect(),
Some(self.committer.clone()),
message.to_string(),
self.properties.clone(),
Some(inventory_sha1),
self.timestamp as f64,
Some(self.timezone),
);
self.repository.add_revision(&revision, &self.parents)?;
Ok(self.new_revision_id.clone())
}
}