use std::collections::HashSet;
use std::fs;
use std::io;
use std::io::Error as IoError;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use toml::from_str;
use toml::to_string_pretty;
use crate::fingerprint::Fingerprint;
use crate::grouper::DuplicateGroup;
use crate::suppression::ignore_entry::IgnoreEntry;
const IGNORE_FILE_NAME: &str = ".dry4rust-ignore.toml";
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct IgnoreFile {
#[serde(default)]
pub ignore: Vec<IgnoreEntry>,
}
impl IgnoreFile {
#[must_use]
pub fn path_in(root: &Path) -> PathBuf {
root.join(IGNORE_FILE_NAME)
}
#[must_use]
pub fn load(root: &Path) -> Self {
let path = Self::path_in(root);
if !path.exists() {
return Self::default();
}
fs::read_to_string(&path).map_or_else(
|_| Self::default(),
|content| from_str(&content).unwrap_or_default(),
)
}
pub fn save(&self, root: &Path) -> io::Result<()> {
if self.ignore.is_empty() {
return Self::remove_from(root);
}
let content = to_string_pretty(self)
.map_err(|e| IoError::other(format!("Failed to serialize ignore file: {e}")))?;
fs::write(Self::path_in(root), content)
}
pub fn remove_from(root: &Path) -> io::Result<()> {
match fs::remove_file(Self::path_in(root)) {
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
outcome => outcome,
}
}
#[must_use]
pub fn with_ignored(
self,
fingerprint: &Fingerprint,
reason: Option<String>,
members: Vec<String>,
) -> Self {
let hex = fingerprint.to_hex();
if self.ignore.iter().any(|entry| entry.fingerprint == hex) {
return self;
}
let mut file = self;
file.ignore.push(IgnoreEntry {
fingerprint: hex,
reason,
members,
});
file
}
#[must_use]
pub fn without(self, fingerprint: &str) -> (Self, bool) {
let mut file = self;
let before = file.ignore.len();
file.ignore.retain(|entry| entry.fingerprint != fingerprint);
let removed = file.ignore.len() < before;
(file, removed)
}
#[must_use]
pub fn contains(&self, fingerprint: &Fingerprint) -> bool {
let hex = fingerprint.to_hex();
self.ignore.iter().any(|entry| entry.fingerprint == hex)
}
#[must_use]
pub fn retain_unsuppressed(&self, groups: Vec<DuplicateGroup>) -> Vec<DuplicateGroup> {
groups
.into_iter()
.filter(|group| !self.contains(&group.fingerprint))
.collect()
}
#[must_use]
pub fn stale(&self, live: &HashSet<Fingerprint>) -> Vec<&IgnoreEntry> {
self.ignore
.iter()
.filter(|entry| !Self::is_live(entry, live))
.collect()
}
#[must_use]
pub fn without_stale(self, live: &HashSet<Fingerprint>) -> (Self, Vec<IgnoreEntry>) {
let (kept, stale) = self
.ignore
.into_iter()
.partition(|entry| Self::is_live(entry, live));
(Self { ignore: kept }, stale)
}
fn is_live(entry: &IgnoreEntry, live: &HashSet<Fingerprint>) -> bool {
Fingerprint::from_hex(&entry.fingerprint).is_some_and(|fp| live.contains(&fp))
}
}