1use super::{DiffEntry, DiffStatus};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub struct DiffFilter {
8 include: u8,
9 exclude: u8,
10 has_include: bool,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15pub enum InvalidDiffFilter {
16 #[error("unknown diff filter letter `{0}`")]
18 Unknown(char),
19 #[error("diff filter letter `{0}` is not supported")]
21 Unsupported(char),
22}
23
24impl DiffFilter {
25 #[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}