use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use tracing::{debug, info};
use crate::Error;
use crate::Res;
use crate::checksum::refresh_hash;
use crate::error::PackageOpError;
use crate::io::storage::Storage;
use crate::lineage::{CommitState, PackageLineage, PathState};
use crate::manifest::Manifest;
use crate::paths::DomainPaths;
use quilt_uri::Namespace;
use super::pull_outcome::{RemoteChange, remote_delta};
pub async fn undo_commit(
mut lineage: PackageLineage,
paths: &DomainPaths,
storage: &(impl Storage + Sync),
package_home: PathBuf,
namespace: Namespace,
) -> Res<(PackageLineage, CommitState)> {
let current = lineage.commit.clone().ok_or_else(|| {
Error::PackageOp(PackageOpError::Undo(
"package has no local commit to undo".to_string(),
))
})?;
let previous_hash = current.prev_hashes.first().cloned().ok_or_else(|| {
Error::PackageOp(PackageOpError::Undo(
"this is the package's first revision, so there is nothing to undo".to_string(),
))
})?;
let current_manifest = Manifest::from_path(
storage,
&paths.installed_manifest(&namespace, ¤t.hash),
)
.await?;
let previous_manifest_path = paths.installed_manifest(&namespace, &previous_hash);
let previous_manifest = Manifest::from_path(storage, &previous_manifest_path).await?;
let delta = remote_delta(¤t_manifest, &previous_manifest);
refuse_unless_clean(&lineage, storage, &package_home, ¤t_manifest, &delta).await?;
info!(
"⏳ Undoing commit {} of {}, restoring {}",
current.hash, namespace, previous_hash
);
let to_write: Vec<(&PathBuf, &crate::object_hash::ObjectHash)> = delta
.iter()
.filter_map(|(key, change)| match change {
RemoteChange::Modified(hash) | RemoteChange::Added(hash) => Some((key, hash)),
RemoteChange::Removed => None,
})
.collect();
let (blocking, trailing): (Vec<&PathBuf>, Vec<&PathBuf>) = delta
.iter()
.filter(|(_, change)| matches!(change, RemoteChange::Removed))
.map(|(key, _)| key)
.partition(|removed| to_write.iter().any(|(write, _)| nests(removed, write)));
let mut restored_paths = lineage.paths.clone();
for logical_key in blocking {
remove_working_file(storage, &package_home, logical_key).await?;
restored_paths.remove(logical_key);
}
for (logical_key, hash) in &to_write {
let working_path = package_home.join(logical_key);
if let Some(parent) = working_path.parent() {
storage.create_dir_all(parent).await?;
}
debug!("⏳ Restoring {}", logical_key.display());
storage
.copy(&paths.object(hash.digest()), &working_path)
.await?;
restored_paths.insert(
(*logical_key).clone(),
PathState {
timestamp: storage.modified_timestamp(&working_path).await?,
hash: (*hash).clone().into(),
},
);
}
for logical_key in trailing {
remove_working_file(storage, &package_home, logical_key).await?;
restored_paths.remove(logical_key);
}
let commit = CommitState {
timestamp: storage.modified_timestamp(&previous_manifest_path).await?,
hash: previous_hash,
prev_hashes: current.prev_hashes.into_iter().skip(1).collect(),
};
lineage.paths = restored_paths;
lineage.commit = Some(commit.clone());
info!("✔️ Undid commit of {}, now at {}", namespace, commit.hash);
Ok((lineage, commit))
}
fn nests(one: &Path, other: &Path) -> bool {
one.starts_with(other) || other.starts_with(one)
}
async fn remove_working_file(
storage: &(impl Storage + Sync),
package_home: &Path,
logical_key: &Path,
) -> Res {
let working_path = package_home.join(logical_key);
debug!("⏳ Removing {}", logical_key.display());
if let Err(err) = storage.remove_file(&working_path).await
&& err.kind() != ErrorKind::NotFound
{
return Err(Error::Io(err));
}
let mut dir = working_path.parent().map(Path::to_path_buf);
while let Some(candidate) = dir {
if candidate == package_home || !candidate.starts_with(package_home) {
break;
}
let Ok(mut entries) = storage.read_dir(&candidate).await else {
break;
};
if entries.next_entry().await?.is_some() {
break;
}
storage.remove_dir_all(&candidate).await?;
dir = candidate.parent().map(Path::to_path_buf);
}
Ok(())
}
async fn refuse_unless_clean(
lineage: &PackageLineage,
storage: &(impl Storage + Sync),
package_home: &Path,
current_manifest: &Manifest,
delta: &BTreeMap<PathBuf, RemoteChange>,
) -> Res {
let destinations = delta.iter().filter_map(|(key, change)| match change {
RemoteChange::Modified(_) | RemoteChange::Added(_) => Some(key),
RemoteChange::Removed => None,
});
let mut to_check: BTreeSet<&PathBuf> = lineage.paths.keys().collect();
to_check.extend(destinations);
let mut unexpected: Vec<String> = Vec::new();
for logical_key in to_check {
let working_path = package_home.join(logical_key);
let ok = match current_manifest.get_record(logical_key) {
Some(row) => matches!(
refresh_hash(storage, &working_path, row.clone()).await,
Ok(None)
),
None => {
!storage.exists(&working_path).await
|| storage.read_dir(&working_path).await.is_ok()
}
};
if !ok {
unexpected.push(logical_key.display().to_string());
}
}
if unexpected.is_empty() {
return Ok(());
}
unexpected.sort();
Err(Error::PackageOp(PackageOpError::Undo(format!(
"{} would be overwritten, and does not hold this revision's committed content. \
Commit or restore it by hand, then undo",
unexpected.join(", ")
))))
}