use anyhow::{Context, Result};
use fn_error_context::context;
use gio::prelude::*;
use ostree::gio;
use std::collections::BTreeSet;
use std::fmt;
fn query_info_optional(
f: &gio::File,
queryattrs: &str,
queryflags: gio::FileQueryInfoFlags,
) -> Result<Option<gio::FileInfo>> {
let cancellable = gio::Cancellable::NONE;
match f.query_info(queryattrs, queryflags, cancellable) {
Ok(i) => Ok(Some(i)),
Err(e) => {
if let Some(ref e2) = e.kind::<gio::IOErrorEnum>() {
match e2 {
gio::IOErrorEnum::NotFound => Ok(None),
_ => Err(e.into()),
}
} else {
Err(e.into())
}
}
}
}
pub type FileSet = BTreeSet<String>;
#[derive(Debug, Default)]
pub struct FileTreeDiff {
pub subdir: Option<String>,
pub added_files: FileSet,
pub added_dirs: FileSet,
pub removed_files: FileSet,
pub removed_dirs: FileSet,
pub changed_files: FileSet,
pub changed_dirs: FileSet,
}
impl fmt::Display for FileTreeDiff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"files(added:{} removed:{} changed:{}) dirs(added:{} removed:{} changed:{})",
self.added_files.len(),
self.removed_files.len(),
self.changed_files.len(),
self.added_dirs.len(),
self.removed_dirs.len(),
self.changed_dirs.len()
)
}
}
fn diff_recurse(
prefix: &str,
diff: &mut FileTreeDiff,
from: &ostree::RepoFile,
to: &ostree::RepoFile,
) -> Result<()> {
let cancellable = gio::Cancellable::NONE;
let queryattrs = "standard::name,standard::type";
let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
let from_iter = from.enumerate_children(queryattrs, queryflags, cancellable)?;
while let Some(from_info) = from_iter.next_file(cancellable)? {
let from_child = from_iter.child(&from_info);
let name = from_info.name();
let name = name.to_str().expect("UTF-8 ostree name");
let path = format!("{prefix}{name}");
let to_child = to.child(&name);
let to_info = query_info_optional(&to_child, queryattrs, queryflags)
.context("querying optional to")?;
let is_dir = matches!(from_info.file_type(), gio::FileType::Directory);
if to_info.is_some() {
let to_child = to_child.downcast::<ostree::RepoFile>().expect("downcast");
to_child.ensure_resolved()?;
let from_child = from_child.downcast::<ostree::RepoFile>().expect("downcast");
from_child.ensure_resolved()?;
if is_dir {
let from_contents_checksum =
from_child.tree_get_contents_checksum().expect("checksum");
let to_contents_checksum = to_child.tree_get_contents_checksum().expect("checksum");
if from_contents_checksum != to_contents_checksum {
let subpath = format!("{}/", path);
diff_recurse(&subpath, diff, &from_child, &to_child)?;
}
let from_meta_checksum = from_child.tree_get_metadata_checksum().expect("checksum");
let to_meta_checksum = to_child.tree_get_metadata_checksum().expect("checksum");
if from_meta_checksum != to_meta_checksum {
diff.changed_dirs.insert(path);
}
} else {
let from_checksum = from_child.checksum().expect("checksum");
let to_checksum = to_child.checksum().expect("checksum");
if from_checksum != to_checksum {
diff.changed_files.insert(path);
}
}
} else if is_dir {
diff.removed_dirs.insert(path);
} else {
diff.removed_files.insert(path);
}
}
let to_iter = to.enumerate_children(queryattrs, queryflags, cancellable)?;
while let Some(to_info) = to_iter.next_file(cancellable)? {
let name = to_info.name();
let name = name.to_str().expect("UTF-8 ostree name");
let path = format!("{prefix}{name}");
let from_child = from.child(name);
let from_info = query_info_optional(&from_child, queryattrs, queryflags)
.context("querying optional from")?;
if from_info.is_some() {
continue;
}
let is_dir = matches!(to_info.file_type(), gio::FileType::Directory);
if is_dir {
diff.added_dirs.insert(path);
} else {
diff.added_files.insert(path);
}
}
Ok(())
}
#[context("Computing ostree diff")]
pub fn diff<P: AsRef<str>>(
repo: &ostree::Repo,
from: &str,
to: &str,
subdir: Option<P>,
) -> Result<FileTreeDiff> {
let subdir = subdir.as_ref();
let subdir = subdir.map(|s| s.as_ref());
let (fromroot, _) = repo.read_commit(from, gio::Cancellable::NONE)?;
let (toroot, _) = repo.read_commit(to, gio::Cancellable::NONE)?;
let (fromroot, toroot) = if let Some(subdir) = subdir {
(
fromroot.resolve_relative_path(subdir),
toroot.resolve_relative_path(subdir),
)
} else {
(fromroot, toroot)
};
let fromroot = fromroot.downcast::<ostree::RepoFile>().expect("downcast");
fromroot.ensure_resolved()?;
let toroot = toroot.downcast::<ostree::RepoFile>().expect("downcast");
toroot.ensure_resolved()?;
let mut diff = FileTreeDiff {
subdir: subdir.map(|s| s.to_string()),
..Default::default()
};
diff_recurse("/", &mut diff, &fromroot, &toroot)?;
Ok(diff)
}