use std::collections::BTreeMap;
use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
use crate::lineage::Change;
use crate::lineage::InstalledPackageStatus;
use crate::manifest::Manifest;
use crate::object_hash::ObjectHash;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PullOutcome {
UpToDate,
CleanUpdate,
KeepsLocalChanges {
added: Vec<PathBuf>,
modified: Vec<PathBuf>,
removed: Vec<PathBuf>,
},
Blocked { conflicts: Vec<PathBuf> },
}
#[allow(clippy::large_enum_variant)]
pub(crate) enum RemoteChange {
Modified(ObjectHash),
Removed,
}
pub(crate) fn remote_delta(base: &Manifest, latest: &Manifest) -> BTreeMap<PathBuf, RemoteChange> {
let mut delta = BTreeMap::new();
for base_row in &base.rows {
match latest.get_record(&base_row.logical_key) {
Some(latest_row) if latest_row.hash == base_row.hash => {}
Some(latest_row) => {
delta.insert(
base_row.logical_key.clone(),
RemoteChange::Modified(latest_row.hash.clone()),
);
}
None => {
delta.insert(base_row.logical_key.clone(), RemoteChange::Removed);
}
}
}
delta
}
fn same_resulting_content(local: &Change, remote: &RemoteChange) -> bool {
match (local, remote) {
(Change::Modified(row), RemoteChange::Modified(hash)) => &row.hash == hash,
(Change::Removed(_), RemoteChange::Removed) => true,
_ => false,
}
}
#[must_use]
pub fn classify_pull(
status: &InstalledPackageStatus,
base: &Manifest,
latest: &Manifest,
) -> PullOutcome {
if base == latest {
return PullOutcome::UpToDate;
}
let delta = remote_delta(base, latest);
if status.changes.is_empty() {
return PullOutcome::CleanUpdate;
}
let mut conflicts = Vec::new();
let mut added = Vec::new();
let mut modified = Vec::new();
let mut removed = Vec::new();
for (path, change) in &status.changes {
if let Some(remote_change) = delta.get(path) {
if !same_resulting_content(change, remote_change) {
conflicts.push(path.clone());
}
continue;
}
match change {
Change::Added(row) => match latest.get_record(path) {
None => added.push(path.clone()),
Some(latest_row) if latest_row.hash == row.hash => {}
Some(_) => conflicts.push(path.clone()),
},
Change::Modified(_) => modified.push(path.clone()),
Change::Removed(_) => removed.push(path.clone()),
}
}
if conflicts.is_empty() {
PullOutcome::KeepsLocalChanges {
added,
modified,
removed,
}
} else {
PullOutcome::Blocked { conflicts }
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_log::test;
use multihash::Multihash;
use crate::Res;
use crate::lineage::Change;
use crate::lineage::ChangeSet;
use crate::lineage::InstalledPackageStatus;
use crate::lineage::UpstreamState;
use crate::manifest::Manifest;
use crate::manifest::ManifestRow;
fn row(key: &str, hash_seed: &[u8]) -> ManifestRow {
ManifestRow {
logical_key: PathBuf::from(key),
physical_key: format!("s3://b/{key}"),
hash: Multihash::<256>::wrap(0x12, hash_seed)
.unwrap()
.try_into()
.unwrap(),
size: hash_seed.len() as u64,
meta: None,
}
}
fn manifest_of(rows: Vec<ManifestRow>) -> Manifest {
Manifest {
rows,
..Manifest::default()
}
}
fn behind(changes: ChangeSet) -> InstalledPackageStatus {
InstalledPackageStatus::new(UpstreamState::Behind, changes)
}
#[test(tokio::test)]
async fn clean_tree_is_clean_update() -> Res {
let base = manifest_of(vec![row("a", b"1")]);
let latest = manifest_of(vec![row("a", b"2")]); let out = classify_pull(&behind(ChangeSet::default()), &base, &latest);
assert_eq!(out, PullOutcome::CleanUpdate);
Ok(())
}
#[test(tokio::test)]
async fn added_file_is_kept() -> Res {
let base = manifest_of(vec![row("a", b"1")]);
let latest = manifest_of(vec![row("a", b"2")]);
let mut changes = ChangeSet::new();
changes.insert(
PathBuf::from("new.txt"),
Change::Added(row("new.txt", b"x")),
);
let out = classify_pull(&behind(changes), &base, &latest);
assert_eq!(
out,
PullOutcome::KeepsLocalChanges {
added: vec![PathBuf::from("new.txt")],
modified: vec![],
removed: vec![],
}
);
Ok(())
}
#[test(tokio::test)]
async fn same_path_different_content_blocks() -> Res {
let base = manifest_of(vec![row("a", b"1")]);
let latest = manifest_of(vec![row("a", b"remote")]); let mut changes = ChangeSet::new();
changes.insert(PathBuf::from("a"), Change::Modified(row("a", b"local"))); let out = classify_pull(&behind(changes), &base, &latest);
assert_eq!(
out,
PullOutcome::Blocked {
conflicts: vec![PathBuf::from("a")]
}
);
Ok(())
}
#[test(tokio::test)]
async fn identical_edit_is_not_a_conflict() -> Res {
let base = manifest_of(vec![row("a", b"1")]);
let latest = manifest_of(vec![row("a", b"same")]);
let mut changes = ChangeSet::new();
changes.insert(PathBuf::from("a"), Change::Modified(row("a", b"same"))); let out = classify_pull(&behind(changes), &base, &latest);
assert_eq!(
out,
PullOutcome::KeepsLocalChanges {
added: vec![],
modified: vec![],
removed: vec![]
}
);
Ok(())
}
#[test(tokio::test)]
async fn local_remove_vs_remote_modify_blocks() -> Res {
let base = manifest_of(vec![row("a", b"1")]);
let latest = manifest_of(vec![row("a", b"2")]); let mut changes = ChangeSet::new();
changes.insert(PathBuf::from("a"), Change::Removed(row("a", b"1"))); let out = classify_pull(&behind(changes), &base, &latest);
assert_eq!(
out,
PullOutcome::Blocked {
conflicts: vec![PathBuf::from("a")]
}
);
Ok(())
}
#[test(tokio::test)]
async fn local_modify_vs_remote_remove_blocks() -> Res {
let base = manifest_of(vec![row("a", b"1"), row("b", b"2")]);
let latest = manifest_of(vec![row("b", b"2")]); let mut changes = ChangeSet::new();
changes.insert(PathBuf::from("a"), Change::Modified(row("a", b"local"))); let out = classify_pull(&behind(changes), &base, &latest);
assert_eq!(
out,
PullOutcome::Blocked {
conflicts: vec![PathBuf::from("a")]
}
);
Ok(())
}
#[test(tokio::test)]
async fn both_removed_is_not_a_conflict() -> Res {
let base = manifest_of(vec![row("a", b"1"), row("b", b"2")]);
let latest = manifest_of(vec![row("b", b"2")]); let mut changes = ChangeSet::new();
changes.insert(PathBuf::from("a"), Change::Removed(row("a", b"1"))); let out = classify_pull(&behind(changes), &base, &latest);
assert_eq!(
out,
PullOutcome::KeepsLocalChanges {
added: vec![],
modified: vec![],
removed: vec![]
}
);
Ok(())
}
#[test(tokio::test)]
async fn both_added_different_content_blocks() -> Res {
let base = manifest_of(vec![row("a", b"1")]);
let latest = manifest_of(vec![row("a", b"1"), row("new.txt", b"remote")]);
let mut changes = ChangeSet::new();
changes.insert(
PathBuf::from("new.txt"),
Change::Added(row("new.txt", b"local")),
);
let out = classify_pull(&behind(changes), &base, &latest);
assert_eq!(
out,
PullOutcome::Blocked {
conflicts: vec![PathBuf::from("new.txt")]
}
);
Ok(())
}
#[test(tokio::test)]
async fn both_added_same_content_is_trivially_resolved() -> Res {
let base = manifest_of(vec![row("a", b"1")]);
let latest = manifest_of(vec![row("a", b"1"), row("new.txt", b"same")]);
let mut changes = ChangeSet::new();
changes.insert(
PathBuf::from("new.txt"),
Change::Added(row("new.txt", b"same")),
);
let out = classify_pull(&behind(changes), &base, &latest);
assert_eq!(
out,
PullOutcome::KeepsLocalChanges {
added: vec![],
modified: vec![],
removed: vec![],
}
);
Ok(())
}
#[test(tokio::test)]
async fn no_remote_change_is_up_to_date() -> Res {
let base = manifest_of(vec![row("a", b"1")]);
let latest = manifest_of(vec![row("a", b"1")]); let out = classify_pull(&behind(ChangeSet::default()), &base, &latest);
assert_eq!(out, PullOutcome::UpToDate);
Ok(())
}
#[test(tokio::test)]
async fn metadata_only_change_is_clean_update() -> Res {
let base = manifest_of(vec![row("a", b"1")]);
let mut latest = manifest_of(vec![row("a", b"1")]);
latest.header.message = Some("newer revision message".to_string());
let out = classify_pull(&behind(ChangeSet::default()), &base, &latest);
assert_eq!(out, PullOutcome::CleanUpdate);
Ok(())
}
#[test(tokio::test)]
async fn metadata_only_change_keeps_local_changes() -> Res {
let base = manifest_of(vec![row("a", b"1")]);
let mut latest = manifest_of(vec![row("a", b"1")]);
latest.header.message = Some("newer revision message".to_string());
let mut changes = ChangeSet::new();
changes.insert(
PathBuf::from("new.txt"),
Change::Added(row("new.txt", b"x")),
);
let out = classify_pull(&behind(changes), &base, &latest);
assert_eq!(
out,
PullOutcome::KeepsLocalChanges {
added: vec![PathBuf::from("new.txt")],
modified: vec![],
removed: vec![],
}
);
Ok(())
}
}