1use anyhow::{Context, Result};
10use fn_error_context::context;
11use gio::prelude::*;
12use ostree::gio;
13use std::collections::BTreeSet;
14use std::fmt;
15
16pub(crate) fn query_info_optional(
18 f: &gio::File,
19 queryattrs: &str,
20 queryflags: gio::FileQueryInfoFlags,
21) -> Result<Option<gio::FileInfo>> {
22 let cancellable = gio::Cancellable::NONE;
23 match f.query_info(queryattrs, queryflags, cancellable) {
24 Ok(i) => Ok(Some(i)),
25 Err(e) => {
26 if let Some(ref e2) = e.kind::<gio::IOErrorEnum>() {
27 match e2 {
28 gio::IOErrorEnum::NotFound => Ok(None),
29 _ => Err(e.into()),
30 }
31 } else {
32 Err(e.into())
33 }
34 }
35 }
36}
37
38pub type FileSet = BTreeSet<String>;
40
41#[derive(Debug, Default)]
43pub struct FileTreeDiff {
44 pub subdir: Option<String>,
46 pub added_files: FileSet,
48 pub added_dirs: FileSet,
50 pub removed_files: FileSet,
52 pub removed_dirs: FileSet,
54 pub changed_files: FileSet,
56 pub changed_dirs: FileSet,
58}
59
60impl fmt::Display for FileTreeDiff {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 write!(
63 f,
64 "files(added:{} removed:{} changed:{}) dirs(added:{} removed:{} changed:{})",
65 self.added_files.len(),
66 self.removed_files.len(),
67 self.changed_files.len(),
68 self.added_dirs.len(),
69 self.removed_dirs.len(),
70 self.changed_dirs.len()
71 )
72 }
73}
74
75fn diff_recurse(
76 prefix: &str,
77 diff: &mut FileTreeDiff,
78 from: &ostree::RepoFile,
79 to: &ostree::RepoFile,
80) -> Result<()> {
81 let cancellable = gio::Cancellable::NONE;
82 let queryattrs = "standard::name,standard::type";
83 let queryflags = gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS;
84 let from_iter = from.enumerate_children(queryattrs, queryflags, cancellable)?;
85
86 while let Some(from_info) = from_iter.next_file(cancellable)? {
89 let from_child = from_iter.child(&from_info);
90 let name = from_info.name();
91 let name = name.to_str().expect("UTF-8 ostree name");
92 let path = format!("{prefix}{name}");
93 let to_child = to.child(name);
94 let to_info = query_info_optional(&to_child, queryattrs, queryflags)
95 .context("querying optional to")?;
96 let is_dir = matches!(from_info.file_type(), gio::FileType::Directory);
97 if to_info.is_some() {
98 let to_child = to_child.downcast::<ostree::RepoFile>().expect("downcast");
99 to_child.ensure_resolved()?;
100 let from_child = from_child.downcast::<ostree::RepoFile>().expect("downcast");
101 from_child.ensure_resolved()?;
102
103 if is_dir {
104 let from_contents_checksum = from_child.tree_get_contents_checksum();
105 let to_contents_checksum = to_child.tree_get_contents_checksum();
106 if from_contents_checksum != to_contents_checksum {
107 let subpath = format!("{}/", path);
108 diff_recurse(&subpath, diff, &from_child, &to_child)?;
109 }
110 let from_meta_checksum = from_child.tree_get_metadata_checksum();
111 let to_meta_checksum = to_child.tree_get_metadata_checksum();
112 if from_meta_checksum != to_meta_checksum {
113 diff.changed_dirs.insert(path);
114 }
115 } else {
116 let from_checksum = from_child.checksum();
117 let to_checksum = to_child.checksum();
118 if from_checksum != to_checksum {
119 diff.changed_files.insert(path);
120 }
121 }
122 } else if is_dir {
123 diff.removed_dirs.insert(path);
124 } else {
125 diff.removed_files.insert(path);
126 }
127 }
128 let to_iter = to.enumerate_children(queryattrs, queryflags, cancellable)?;
131 while let Some(to_info) = to_iter.next_file(cancellable)? {
132 let name = to_info.name();
133 let name = name.to_str().expect("UTF-8 ostree name");
134 let path = format!("{prefix}{name}");
135 let from_child = from.child(name);
136 let from_info = query_info_optional(&from_child, queryattrs, queryflags)
137 .context("querying optional from")?;
138 if from_info.is_some() {
139 continue;
140 }
141 let is_dir = matches!(to_info.file_type(), gio::FileType::Directory);
142 if is_dir {
143 diff.added_dirs.insert(path);
144 } else {
145 diff.added_files.insert(path);
146 }
147 }
148 Ok(())
149}
150
151#[context("Computing ostree diff")]
153pub fn diff<P: AsRef<str>>(
154 repo: &ostree::Repo,
155 from: &str,
156 to: &str,
157 subdir: Option<P>,
158) -> Result<FileTreeDiff> {
159 let subdir = subdir.as_ref();
160 let subdir = subdir.map(|s| s.as_ref());
161 let (fromroot, _) = repo.read_commit(from, gio::Cancellable::NONE)?;
162 let (toroot, _) = repo.read_commit(to, gio::Cancellable::NONE)?;
163 let (fromroot, toroot) = if let Some(subdir) = subdir {
164 (
165 fromroot.resolve_relative_path(subdir),
166 toroot.resolve_relative_path(subdir),
167 )
168 } else {
169 (fromroot, toroot)
170 };
171 let fromroot = fromroot.downcast::<ostree::RepoFile>().expect("downcast");
172 fromroot.ensure_resolved()?;
173 let toroot = toroot.downcast::<ostree::RepoFile>().expect("downcast");
174 toroot.ensure_resolved()?;
175 let mut diff = FileTreeDiff {
176 subdir: subdir.map(|s| s.to_string()),
177 ..Default::default()
178 };
179 diff_recurse("/", &mut diff, &fromroot, &toroot)?;
180 Ok(diff)
181}