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:
182
src/diff.rs
Normal file
182
src/diff.rs
Normal 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)
|
||||
}
|
||||
BIN
tests/it/fixtures/exampleos-v1.tar.zst
Normal file
BIN
tests/it/fixtures/exampleos-v1.tar.zst
Normal file
Binary file not shown.
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user