Skip to main content

grit_lib/diff/
filter.rs

1//! Git `--diff-filter` selection over [`DiffStatus`](super::DiffStatus).
2
3use super::{DiffEntry, DiffStatus};
4
5/// Typed include/exclude set for [`DiffStatus`] using Git's letter syntax.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub struct DiffFilter {
8    include: u8,
9    exclude: u8,
10    has_include: bool,
11}
12
13/// Unknown or unsupported `--diff-filter` letter.
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15pub enum InvalidDiffFilter {
16    /// Character is not a recognized diff status letter.
17    #[error("unknown diff filter letter `{0}`")]
18    Unknown(char),
19    /// Letter is valid in Git but not produced by grit's diff (copy, broken, unknown).
20    #[error("diff filter letter `{0}` is not supported")]
21    Unsupported(char),
22}
23
24impl DiffFilter {
25    /// Apply this filter to tree diff entries (after rename detection).
26    #[must_use]
27    pub fn filter_entries(self, entries: Vec<DiffEntry>) -> Vec<DiffEntry> {
28        entries
29            .into_iter()
30            .filter(|e| self.matches(e.status))
31            .collect()
32    }
33
34    #[must_use]
35    fn matches(self, status: DiffStatus) -> bool {
36        let bit = status_bit(status);
37        if self.has_include && self.include & bit == 0 {
38            return false;
39        }
40        if self.exclude & bit != 0 {
41            return false;
42        }
43        true
44    }
45
46    fn from_letter(ch: char) -> Result<(bool, u8), InvalidDiffFilter> {
47        let upper = ch.to_ascii_uppercase();
48        let bit = match upper {
49            'A' => status_bit(DiffStatus::Added),
50            'D' => status_bit(DiffStatus::Deleted),
51            'M' => status_bit(DiffStatus::Modified),
52            'R' => status_bit(DiffStatus::Renamed),
53            'T' => status_bit(DiffStatus::TypeChanged),
54            'U' => status_bit(DiffStatus::Unmerged),
55            'C' | 'X' | 'B' => return Err(InvalidDiffFilter::Unsupported(ch)),
56            _ if ch.is_ascii_alphabetic() => return Err(InvalidDiffFilter::Unknown(ch)),
57            _ => return Err(InvalidDiffFilter::Unknown(ch)),
58        };
59        Ok((ch.is_ascii_uppercase(), bit))
60    }
61}
62
63fn status_bit(status: DiffStatus) -> u8 {
64    1 << match status {
65        DiffStatus::Added => 0,
66        DiffStatus::Deleted => 1,
67        DiffStatus::Modified => 2,
68        DiffStatus::Renamed => 3,
69        DiffStatus::Copied => 4,
70        DiffStatus::TypeChanged => 5,
71        DiffStatus::Unmerged => 6,
72    }
73}
74
75impl std::str::FromStr for DiffFilter {
76    type Err = InvalidDiffFilter;
77
78    fn from_str(spec: &str) -> Result<Self, Self::Err> {
79        let mut filter = Self::default();
80        for ch in spec.chars() {
81            if ch.is_whitespace() {
82                continue;
83            }
84            let (is_upper, bit) = Self::from_letter(ch)?;
85            if is_upper {
86                filter.include |= bit;
87                filter.has_include = true;
88            } else {
89                filter.exclude |= bit;
90            }
91        }
92        Ok(filter)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn uppercase_only_shows_matching_status() {
102        let f: DiffFilter = "AD".parse().expect("parse");
103        assert!(f.matches(DiffStatus::Added));
104        assert!(f.matches(DiffStatus::Deleted));
105        assert!(!f.matches(DiffStatus::Modified));
106    }
107
108    #[test]
109    fn lowercase_excludes_status() {
110        let f: DiffFilter = "a".parse().expect("parse");
111        assert!(!f.matches(DiffStatus::Added));
112        assert!(f.matches(DiffStatus::Deleted));
113    }
114
115    #[test]
116    fn rejects_unsupported_letters() {
117        assert!(matches!(
118            "C".parse::<DiffFilter>(),
119            Err(InvalidDiffFilter::Unsupported('C'))
120        ));
121        assert!(matches!(
122            "z".parse::<DiffFilter>(),
123            Err(InvalidDiffFilter::Unknown('z'))
124        ));
125    }
126}