1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 15:45:53 +01:00

Add diff module

Taken from https://github.com/coreos/rpm-ostree/blob/master/rust/src/ostree_diff.rs
This commit is contained in:
Colin Walters
2021-04-04 18:00:29 +00:00
parent ab9126dc86
commit 4f7f43c97b
3 changed files with 244 additions and 13 deletions

182
src/diff.rs Normal file
View File

@@ -0,0 +1,182 @@
//! Compute the difference between two OSTree commits.
/*
* Copyright (C) 2020 Red Hat, Inc.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*/
use anyhow::{Context, Result};
use fn_error_context::context;
use gio::prelude::*;
use ostree::RepoFileExt;
use std::collections::BTreeSet;
use std::fmt;
/// Like `g_file_query_info()`, but return None if the target doesn't exist.
fn query_info_optional(
f: &gio::File,
queryattrs: &str,
queryflags: gio::FileQueryInfoFlags,
) -> Result<Option<gio::FileInfo>> {
let cancellable = gio::NONE_CANCELLABLE;
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())
}
}
}
}
/// A set of file paths.
pub type FileSet = BTreeSet<String>;
/// Diff between two ostree commits.
#[derive(Debug, Default)]
pub struct FileTreeDiff {
/// The prefix passed for diffing, e.g. /usr
pub subdir: Option<String>,
/// Files that are new in an existing directory
pub added_files: FileSet,
/// New directories
pub added_dirs: FileSet,
/// Files removed
pub removed_files: FileSet,
/// Directories removed (recursively)
pub removed_dirs: FileSet,
/// Files that changed (in any way, metadata or content)
pub changed_files: FileSet,
/// Directories that changed mode/permissions
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::NONE_CANCELLABLE;
let queryattrs = "standard::name,standard::type";
let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
let from_iter = from.enumerate_children(queryattrs, queryflags, cancellable)?;
// Iterate over the source (from) directory, and compare with the
// target (to) directory. This generates removals and changes.
while let Some(from_info) = from_iter.next_file(cancellable)? {
let from_child = from_iter.get_child(&from_info).expect("file");
let name = from_info.get_name().expect("name");
let name = name.to_str().expect("UTF-8 ostree name");
let path = format!("{}{}", prefix, name);
let to_child = to.get_child(&name).expect("child");
let to_info = query_info_optional(&to_child, queryattrs, queryflags)
.context("querying optional to")?;
let is_dir = matches!(from_info.get_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.get_checksum().expect("checksum");
let to_checksum = to_child.get_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);
}
}
// Iterate over the target (to) directory, and find any
// files/directories which were not present in the source.
let to_iter = to.enumerate_children(queryattrs, queryflags, cancellable)?;
while let Some(to_info) = to_iter.next_file(cancellable)? {
let name = to_info.get_name().expect("name");
let name = name.to_str().expect("UTF-8 ostree name");
let path = format!("{}{}", prefix, name);
let from_child = from.get_child(name).expect("child");
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.get_file_type(), gio::FileType::Directory);
if is_dir {
diff.added_dirs.insert(path);
} else {
diff.added_files.insert(path);
}
}
Ok(())
}
/// Given two ostree commits, compute the diff between them.
#[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::NONE_CANCELLABLE)?;
let (toroot, _) = repo.read_commit(to, gio::NONE_CANCELLABLE)?;
let (fromroot, toroot) = if let Some(subdir) = subdir {
(
fromroot.resolve_relative_path(subdir).expect("path"),
toroot.resolve_relative_path(subdir).expect("path"),
)
} 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)
}

Binary file not shown.

View File

@@ -5,28 +5,48 @@ use indoc::indoc;
use sh_inline::bash;
use std::io::Write;
const EXAMPLEOS_TAR: &[u8] = include_bytes!("fixtures/exampleos.tar.zst");
const EXAMPLEOS_V0: &[u8] = include_bytes!("fixtures/exampleos.tar.zst");
const EXAMPLEOS_V1: &[u8] = include_bytes!("fixtures/exampleos-v1.tar.zst");
const TESTREF: &str = "exampleos/x86_64/stable";
const CONTENT_CHECKSUM: &str = "0ef7461f9db15e1d8bd8921abf20694225fbaa4462cadf7deed8ea0e43162120";
const EXAMPLEOS_CONTENT_CHECKSUM: &str =
"0ef7461f9db15e1d8bd8921abf20694225fbaa4462cadf7deed8ea0e43162120";
fn generate_test_repo(dir: &Utf8Path) -> Result<Utf8PathBuf> {
let src_tarpath = &dir.join("exampleos.tar.zst");
std::fs::write(src_tarpath, EXAMPLEOS_TAR)?;
std::fs::write(src_tarpath, EXAMPLEOS_V0)?;
bash!(
indoc! {"
cd {path}
ostree --repo=repo-archive init --mode=archive
ostree --repo=repo-archive commit -b {testref} --tree=tar=exampleos.tar.zst
ostree --repo=repo-archive show {testref}
cd {dir}
ostree --repo=repo init --mode=archive
ostree --repo=repo commit -b {testref} --tree=tar=exampleos.tar.zst
ostree --repo=repo show {testref}
"},
testref = TESTREF,
path = path.as_str()
dir = dir.as_str()
)?;
std::fs::remove_file(src_tarpath)?;
Ok(dir.join("repo"))
}
#[context("Generating test OCI")]
fn update_repo(repopath: &Utf8Path) -> Result<()> {
let repotmp = &repopath.join("tmp");
let srcpath = &repotmp.join("exampleos-v1.tar.zst");
std::fs::write(srcpath, EXAMPLEOS_V1)?;
let srcpath = srcpath.as_str();
let repopath = repopath.as_str();
let testref = TESTREF;
bash!(
"ostree --repo={repopath} commit -b {testref} --tree=tar={srcpath}",
testref,
repopath,
srcpath
)?;
std::fs::remove_file(srcpath)?;
Ok(())
}
#[context("Generating test tarball")]
fn generate_test_tarball(dir: &Utf8Path) -> Result<Utf8PathBuf> {
let cancellable = gio::NONE_CANCELLABLE;
let repopath = generate_test_repo(dir)?;
@@ -37,9 +57,9 @@ fn generate_test_tarball(dir: &Utf8Path) -> Result<Utf8PathBuf> {
ostree::commit_get_content_checksum(&commitv)
.unwrap()
.as_str(),
CONTENT_CHECKSUM
EXAMPLEOS_CONTENT_CHECKSUM
);
let destpath = path.join("exampleos-export.tar");
let destpath = dir.join("exampleos-export.tar");
let mut outf = std::io::BufWriter::new(std::fs::File::create(&destpath)?);
ostree_ext::tar::export_commit(repo, rev.as_str(), &mut outf)?;
outf.flush()?;
@@ -47,7 +67,7 @@ fn generate_test_tarball(dir: &Utf8Path) -> Result<Utf8PathBuf> {
}
#[test]
fn test_e2e() -> Result<()> {
fn test_tar_import_export() -> Result<()> {
let cancellable = gio::NONE_CANCELLABLE;
let tempdir = tempfile::tempdir()?;
@@ -65,7 +85,7 @@ fn test_e2e() -> Result<()> {
let imported_commit: String = ostree_ext::tar::import_tar(&destrepo, src_tar)?;
let (commitdata, _) = destrepo.load_commit(&imported_commit)?;
assert_eq!(
CONTENT_CHECKSUM,
EXAMPLEOS_CONTENT_CHECKSUM,
ostree::commit_get_content_checksum(&commitdata)
.unwrap()
.as_str()
@@ -77,3 +97,32 @@ fn test_e2e() -> Result<()> {
)?;
Ok(())
}
#[test]
fn test_diff() -> Result<()> {
let cancellable = gio::NONE_CANCELLABLE;
let tempdir = tempfile::tempdir()?;
let tempdir = Utf8Path::from_path(tempdir.path()).unwrap();
let repopath = &generate_test_repo(tempdir)?;
update_repo(repopath)?;
let from = &format!("{}^", TESTREF);
let repo = &ostree::Repo::open_at(libc::AT_FDCWD, repopath.as_str(), cancellable)?;
let subdir: Option<&str> = None;
let diff = ostree_ext::diff::diff(repo, from, TESTREF, subdir)?;
assert!(diff.subdir.is_none());
assert_eq!(diff.added_dirs.len(), 1);
assert_eq!(diff.added_dirs.iter().nth(0).unwrap(), "/usr/share");
assert_eq!(diff.added_files.len(), 1);
assert_eq!(diff.added_files.iter().nth(0).unwrap(), "/usr/bin/newbin");
assert_eq!(diff.removed_files.len(), 1);
assert_eq!(diff.removed_files.iter().nth(0).unwrap(), "/usr/bin/foo");
let diff = ostree_ext::diff::diff(repo, from, TESTREF, Some("/usr"))?;
assert_eq!(diff.subdir.as_ref().unwrap(), "/usr");
assert_eq!(diff.added_dirs.len(), 1);
assert_eq!(diff.added_dirs.iter().nth(0).unwrap(), "/share");
assert_eq!(diff.added_files.len(), 1);
assert_eq!(diff.added_files.iter().nth(0).unwrap(), "/bin/newbin");
assert_eq!(diff.removed_files.len(), 1);
assert_eq!(diff.removed_files.iter().nth(0).unwrap(), "/bin/foo");
Ok(())
}