use std::collections::BTreeMap;
use gix::prelude::ObjectIdExt;
use gix::refs::transaction::PreviousValue;
use crate::error::{Error, Result};
use crate::session::Session;
use crate::tree::format::{build_merged_tree, parse_tree};
use crate::tree::merge::{
merge_list_tombstones, merge_set_member_tombstones, merge_tombstones, three_way_merge,
two_way_merge_no_common_ancestor, ConflictDecision,
};
use crate::tree::model::{Key, ParsedTree, Tombstone, TreeValue};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MaterializeStrategy {
FastForward,
ThreeWayMerge,
TwoWayMerge,
UpToDate,
}
#[must_use]
#[derive(Debug, Clone)]
pub struct MaterializeRefResult {
pub ref_name: String,
pub strategy: MaterializeStrategy,
pub changes: usize,
pub conflicts: Vec<ConflictDecision>,
}
#[must_use]
#[derive(Debug, Clone)]
pub struct MaterializeOutput {
pub results: Vec<MaterializeRefResult>,
}
pub fn run(session: &Session, remote: Option<&str>, now: i64) -> Result<MaterializeOutput> {
let repo = &session.repo;
let ns = session.namespace();
let local_ref_name = session.local_ref();
let email = session.email();
let remote_refs = find_remote_refs(repo, ns, remote)?;
if remote_refs.is_empty() {
return Ok(MaterializeOutput {
results: Vec::new(),
});
}
let mut results = Vec::new();
for (ref_name, remote_oid) in &remote_refs {
let remote_commit_obj = remote_oid
.attach(repo)
.object()
.map_err(|e| Error::Other(format!("{e}")))?
.into_commit();
let remote_tree_id = remote_commit_obj
.tree_id()
.map_err(|e| Error::Other(format!("{e}")))?
.detach();
let remote_entries = parse_tree(repo, remote_tree_id, "")?;
let local_commit_oid = repo
.find_reference(&local_ref_name)
.ok()
.and_then(|r| r.into_fully_peeled_id().ok())
.map(gix::Id::detach);
let can_fast_forward = match &local_commit_oid {
None => true,
Some(local_oid) => {
if *local_oid == *remote_oid {
results.push(MaterializeRefResult {
ref_name: ref_name.clone(),
strategy: MaterializeStrategy::UpToDate,
changes: 0,
conflicts: Vec::new(),
});
continue;
}
match repo.merge_base(*local_oid, *remote_oid) {
Ok(base_oid) => base_oid == *local_oid,
Err(_) => false,
}
}
};
if can_fast_forward {
let changes =
materialize_fast_forward(session, &local_commit_oid, &remote_entries, email, now)?;
repo.reference(
local_ref_name.as_str(),
*remote_oid,
PreviousValue::Any,
"fast-forward materialize",
)
.map_err(|e| Error::Other(format!("{e}")))?;
results.push(MaterializeRefResult {
ref_name: ref_name.clone(),
strategy: MaterializeStrategy::FastForward,
changes,
conflicts: Vec::new(),
});
} else {
let local_oid = local_commit_oid.as_ref().ok_or_else(|| {
Error::Other("expected local commit for merge but found None".into())
})?;
let (changes, conflict_decisions, strategy) = materialize_merge(
session,
local_oid,
remote_oid,
&remote_entries,
&remote_commit_obj,
email,
now,
&local_ref_name,
)?;
results.push(MaterializeRefResult {
ref_name: ref_name.clone(),
strategy,
changes,
conflicts: conflict_decisions,
});
}
}
session.store.set_last_materialized(now)?;
Ok(MaterializeOutput { results })
}
fn materialize_fast_forward(
session: &Session,
local_commit_oid: &Option<gix::ObjectId>,
remote_entries: &ParsedTree,
email: &str,
now: i64,
) -> Result<usize> {
let repo = &session.repo;
let local_entries = if let Some(local_oid) = local_commit_oid {
let lc = local_oid
.attach(repo)
.object()
.map_err(|e| Error::Other(format!("{e}")))?
.into_commit();
let lt = lc
.tree_id()
.map_err(|e| Error::Other(format!("{e}")))?
.detach();
parse_tree(repo, lt, "")?
} else {
ParsedTree::default()
};
let changes = remote_entries.values.len();
session.store.apply_tree(
&remote_entries.values,
&remote_entries.tombstones,
&remote_entries.set_tombstones,
&remote_entries.list_tombstones,
email,
now,
)?;
apply_legacy_deletes(session, &local_entries.values, remote_entries, email, now)?;
Ok(changes)
}
#[allow(clippy::too_many_arguments)]
fn materialize_merge(
session: &Session,
local_oid: &gix::ObjectId,
remote_oid: &gix::ObjectId,
remote_entries: &ParsedTree,
remote_commit_obj: &gix::Commit<'_>,
email: &str,
now: i64,
local_ref_name: &str,
) -> Result<(usize, Vec<ConflictDecision>, MaterializeStrategy)> {
let repo = &session.repo;
let local_commit_obj = local_oid
.attach(repo)
.object()
.map_err(|e| Error::Other(format!("{e}")))?
.into_commit();
let local_tree_id = local_commit_obj
.tree_id()
.map_err(|e| Error::Other(format!("{e}")))?
.detach();
let local_entries = parse_tree(repo, local_tree_id, "")?;
let local_timestamp = extract_author_timestamp(&local_commit_obj)?;
let remote_timestamp = extract_author_timestamp(remote_commit_obj)?;
let merge_base_oid = repo.merge_base(*local_oid, *remote_oid).ok();
let (
merged_values,
merged_tombstones,
merged_set_tombstones,
merged_list_tombstones,
conflict_decisions,
strategy,
legacy_base_values,
) = if let Some(base_oid) = merge_base_oid {
run_three_way_merge(
repo,
base_oid,
&local_entries,
remote_entries,
local_timestamp,
remote_timestamp,
)?
} else {
run_two_way_merge(&local_entries, remote_entries)?
};
let changes = merged_values.len();
session.store.apply_tree(
&merged_values,
&merged_tombstones,
&merged_set_tombstones,
&merged_list_tombstones,
email,
now,
)?;
if let Some(base_values) = &legacy_base_values {
for key in base_values.keys() {
if !merged_values.contains_key(key) && !merged_tombstones.contains_key(key) {
let target = key.to_target();
session
.store
.apply_tombstone(&target, &key.key, email, now)?;
}
}
}
let merged_tree_oid = build_merged_tree(
repo,
&merged_values,
&merged_tombstones,
&merged_set_tombstones,
&merged_list_tombstones,
)?;
let name = session.name();
let sig = gix::actor::Signature {
name: name.into(),
email: email.into(),
time: gix::date::Time::new(now / 1000, 0),
};
let commit = gix::objs::Commit {
message: "materialize".into(),
tree: merged_tree_oid,
author: sig.clone(),
committer: sig,
encoding: None,
parents: vec![*local_oid, *remote_oid].into(),
extra_headers: Default::default(),
};
let merge_commit_oid = repo
.write_object(&commit)
.map_err(|e| Error::Other(format!("{e}")))?
.detach();
repo.reference(
local_ref_name,
merge_commit_oid,
PreviousValue::Any,
"materialize merge",
)
.map_err(|e| Error::Other(format!("{e}")))?;
Ok((changes, conflict_decisions, strategy))
}
#[allow(clippy::type_complexity)]
fn run_three_way_merge(
repo: &gix::Repository,
base_oid: gix::Id<'_>,
local_entries: &ParsedTree,
remote_entries: &ParsedTree,
local_timestamp: i64,
remote_timestamp: i64,
) -> Result<(
BTreeMap<Key, TreeValue>,
BTreeMap<Key, Tombstone>,
BTreeMap<(Key, String), String>,
BTreeMap<(Key, String), Tombstone>,
Vec<ConflictDecision>,
MaterializeStrategy,
Option<BTreeMap<Key, TreeValue>>,
)> {
let base_commit_obj = base_oid
.object()
.map_err(|e| Error::Other(format!("{e}")))?
.into_commit();
let base_tree_id = base_commit_obj
.tree_id()
.map_err(|e| Error::Other(format!("{e}")))?
.detach();
let base_entries = parse_tree(repo, base_tree_id, "")?;
let legacy_base_values = Some(base_entries.values.clone());
let (merged_values, conflict_decisions) = three_way_merge(
&base_entries.values,
&local_entries.values,
&remote_entries.values,
local_timestamp,
remote_timestamp,
)?;
let merged_tombstones = merge_tombstones(
&base_entries.tombstones,
&local_entries.tombstones,
&remote_entries.tombstones,
&merged_values,
);
let merged_set_tombstones = merge_set_member_tombstones(
&local_entries.set_tombstones,
&remote_entries.set_tombstones,
&merged_values,
);
let merged_list_tombstones = merge_list_tombstones(
&local_entries.list_tombstones,
&remote_entries.list_tombstones,
&merged_values,
);
Ok((
merged_values,
merged_tombstones,
merged_set_tombstones,
merged_list_tombstones,
conflict_decisions,
MaterializeStrategy::ThreeWayMerge,
legacy_base_values,
))
}
#[allow(clippy::type_complexity)]
fn run_two_way_merge(
local_entries: &ParsedTree,
remote_entries: &ParsedTree,
) -> Result<(
BTreeMap<Key, TreeValue>,
BTreeMap<Key, Tombstone>,
BTreeMap<(Key, String), String>,
BTreeMap<(Key, String), Tombstone>,
Vec<ConflictDecision>,
MaterializeStrategy,
Option<BTreeMap<Key, TreeValue>>,
)> {
let (merged_values, merged_tombstones, conflict_decisions) = two_way_merge_no_common_ancestor(
&local_entries.values,
&local_entries.tombstones,
&remote_entries.values,
&remote_entries.tombstones,
);
let merged_set_tombstones = merge_set_member_tombstones(
&local_entries.set_tombstones,
&remote_entries.set_tombstones,
&merged_values,
);
let merged_list_tombstones = merge_list_tombstones(
&local_entries.list_tombstones,
&remote_entries.list_tombstones,
&merged_values,
);
Ok((
merged_values,
merged_tombstones,
merged_set_tombstones,
merged_list_tombstones,
conflict_decisions,
MaterializeStrategy::TwoWayMerge,
None,
))
}
fn apply_legacy_deletes(
session: &Session,
local_values: &BTreeMap<Key, TreeValue>,
remote_entries: &ParsedTree,
email: &str,
now: i64,
) -> Result<()> {
for key in local_values.keys() {
if !remote_entries.values.contains_key(key) {
let target = key.to_target();
session
.store
.apply_tombstone(&target, &key.key, email, now)?;
}
}
Ok(())
}
fn extract_author_timestamp(commit: &gix::Commit<'_>) -> Result<i64> {
let decoded = commit.decode().map_err(|e| Error::Other(format!("{e}")))?;
let time = decoded
.author()
.map_err(|e| Error::Other(format!("{e}")))?
.time()
.map_err(|e| Error::Other(format!("{e}")))?;
Ok(time.seconds)
}
pub fn find_remote_refs(
repo: &gix::Repository,
ns: &str,
remote: Option<&str>,
) -> Result<Vec<(String, gix::ObjectId)>> {
let mut results = Vec::new();
let prefix = match remote {
Some(r) => format!("refs/{ns}/{r}"),
None => format!("refs/{ns}/"),
};
let local_prefix = format!("refs/{ns}/local/");
let platform = repo
.references()
.map_err(|e| Error::Other(format!("{e}")))?;
for reference in platform.all().map_err(|e| Error::Other(format!("{e}")))? {
let reference = reference.map_err(|e| Error::Other(format!("{e}")))?;
let name = reference.name().as_bstr().to_string();
if name.starts_with(&prefix) && !name.starts_with(&local_prefix) {
if let Ok(id) = reference.into_fully_peeled_id() {
results.push((name, id.detach()));
}
}
}
Ok(results)
}