use std::collections::HashSet;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use regex::Regex;
use glob::Pattern;
fn normalise_dir(p: &Path) -> &Path {
p.strip_prefix("/").unwrap_or(p)
}
fn has_prefix(rel: &Path, prefix: &Path) -> bool {
rel == prefix || rel.strip_prefix(prefix).is_ok()
}
#[derive(Debug, Clone, Default)]
pub struct CommitFilter {
skip_dirs: HashSet<PathBuf>,
skip_files: HashSet<String>,
skip_empty_files_in: HashSet<PathBuf>,
skip_zero_permissions: bool,
skip_regexes: Vec<Regex>,
skip_globs: Vec<Pattern>,
}
impl CommitFilter {
pub fn new() -> Self {
Self::default()
}
pub fn rootfs() -> Self {
const ROOTFS_SKIP_DIRS: &[&str] =
&["dev", "proc", "sys", "run", "tmp", "mnt", "media", "home"];
let mut filter = Self::new();
filter.skip_zero_permissions = true;
for dir in ROOTFS_SKIP_DIRS {
filter.skip_dirs.insert(PathBuf::from(dir));
}
filter
}
pub fn skip_dir(mut self, path: impl AsRef<Path>) -> Self {
self.skip_dirs
.insert(normalise_dir(path.as_ref()).to_path_buf());
self
}
pub fn skip_dirs<I, P>(mut self, paths: I) -> Self
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
for p in paths {
self.skip_dirs
.insert(normalise_dir(p.as_ref()).to_path_buf());
}
self
}
pub fn skip_file(mut self, name: impl Into<String>) -> Self {
self.skip_files.insert(name.into());
self
}
pub fn skip_files<I, S>(mut self, names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for n in names {
self.skip_files.insert(n.into());
}
self
}
pub fn skip_empty_files_in(mut self, dir: impl AsRef<Path>) -> Self {
self.skip_empty_files_in
.insert(normalise_dir(dir.as_ref()).to_path_buf());
self
}
pub fn skip_empty_files_in_dirs<I, P>(mut self, dirs: I) -> Self
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
for d in dirs {
self.skip_empty_files_in
.insert(normalise_dir(d.as_ref()).to_path_buf());
}
self
}
pub fn skip_zero_permissions(mut self, enabled: bool) -> Self {
self.skip_zero_permissions = enabled;
self
}
pub fn skip_regex(mut self, pattern: &str) -> Self {
if let Ok(re) = Regex::new(pattern) {
self.skip_regexes.push(re);
}
self
}
pub fn skip_glob(mut self, pattern: &str) -> Self {
if let Ok(glob) = Pattern::new(pattern) {
self.skip_globs.push(glob);
}
self
}
pub(crate) fn should_skip(&self, rel: &Path, abs_upper: &Path) -> bool {
let rel_str = rel.to_string_lossy();
for re in &self.skip_regexes {
if re.is_match(&rel_str) {
return true;
}
}
for glob in &self.skip_globs {
if glob.matches(&rel_str) {
return true;
}
}
if let Some(name) = rel.file_name() {
if self.skip_files.contains(name.to_string_lossy().as_ref()) {
return true;
}
}
for skipped in &self.skip_dirs {
if has_prefix(rel, skipped) {
return true;
}
}
if self.skip_zero_permissions || !self.skip_empty_files_in.is_empty() {
if let Ok(meta) = fs::symlink_metadata(abs_upper) {
let ft = meta.file_type();
if self.skip_zero_permissions && !ft.is_symlink() {
if meta.permissions().mode() & 0o777 == 0 {
return true;
}
}
if ft.is_file() && meta.len() == 0 && !self.skip_empty_files_in.is_empty() {
let mut ancestor = rel.parent();
while let Some(dir) = ancestor {
if self.skip_empty_files_in.contains(dir) {
return true;
}
ancestor = dir.parent();
}
}
}
}
false
}
pub(crate) fn is_skipped_dir(&self, rel: &Path) -> bool {
self.skip_dirs.iter().any(|d| has_prefix(rel, d))
}
}