git_policy_cli/
repo_read_diff.rs1#![cfg(unix)]
16
17use super::repo_read_tree::{FlatEntry, MODE_GITLINK};
18
19#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct Change {
22 pub path: Vec<u8>,
24 pub old_mode: u32,
26 pub new_mode: u32,
28 pub old_id: Vec<u8>,
30 pub new_id: Vec<u8>,
32 pub status: u8,
34}
35
36fn type_of(mode: u32) -> u32 {
38 return mode & 0o170_000;
39}
40
41fn has_entry_below(list: &[FlatEntry], path: &[u8]) -> bool {
43 let mut prefix: Vec<u8> = path.to_vec();
44 prefix.push(b'/');
45 let at: usize = list.partition_point(|entry| { return entry.path.as_slice() < prefix.as_slice(); });
46 return list.get(at).is_some_and(|entry| { return entry.path.starts_with(prefix.as_slice()); });
47}
48
49pub fn diff_sorted(old: &[FlatEntry], new: &[FlatEntry]) -> Option<Vec<Change>> {
60 let mut changes: Vec<Change> = Vec::new();
61 let (mut left, mut right): (usize, usize) = (0, 0);
62 while left < old.len() || right < new.len() {
63 let order = match (old.get(left), new.get(right)) {
64 (Some(before), Some(after)) => before.path.cmp(&after.path),
65 (Some(_), None) => std::cmp::Ordering::Less,
66 (None, _) => std::cmp::Ordering::Greater,
67 };
68 match order {
69 std::cmp::Ordering::Less => {
70 let gone: &FlatEntry = &old[left];
71 if has_entry_below(new, gone.path.as_slice()) {
72 return None;
73 }
74 changes.push(Change { path: gone.path.clone(), old_mode: gone.mode, new_mode: 0, old_id: gone.id.clone(), new_id: Vec::new(), status: b'D' });
75 left += 1;
76 }
77 std::cmp::Ordering::Greater => {
78 let added: &FlatEntry = &new[right];
79 if has_entry_below(old, added.path.as_slice()) {
80 return None;
81 }
82 changes.push(Change { path: added.path.clone(), old_mode: 0, new_mode: added.mode, old_id: Vec::new(), new_id: added.id.clone(), status: b'A' });
83 right += 1;
84 }
85 std::cmp::Ordering::Equal => {
86 let (before, after): (&FlatEntry, &FlatEntry) = (&old[left], &new[right]);
87 if before.mode != after.mode || before.id != after.id {
88 let status: u8 = if type_of(before.mode) == type_of(after.mode) { b'M' } else { b'T' };
89 changes.push(Change { path: before.path.clone(), old_mode: before.mode, new_mode: after.mode, old_id: before.id.clone(), new_id: after.id.clone(), status });
90 }
91 left += 1;
92 right += 1;
93 }
94 }
95 }
96 if changes.iter().any(|change| { return type_of(change.old_mode) == MODE_GITLINK || type_of(change.new_mode) == MODE_GITLINK; }) {
97 return None;
98 }
99 return Some(changes);
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct PathFilter {
115 names: Vec<Vec<u8>>,
117}
118
119impl PathFilter {
120 pub fn parse(specs: &[&[u8]], at_top: bool) -> Option<PathFilter> {
122 let mut names: Vec<Vec<u8>> = Vec::new();
123 let mut everything: bool = false;
124 for spec in specs {
125 if *spec == b":/" {
127 everything = true;
128 continue;
129 }
130 let (name, top_ok): (&[u8], bool) = match spec.strip_prefix(b":(top,literal)") {
131 Some(rest) => (rest, true),
132 None => (spec, at_top),
133 };
134 let plain: bool = !name.is_empty()
135 && !name.starts_with(b":")
136 && !name.starts_with(b"/")
137 && !name.ends_with(b"/")
138 && !name.windows(2).any(|pair| { return pair == b"//"; })
139 && !name.split(|byte| { return *byte == b'/'; }).any(|part| { return part == b"." || part == b".."; })
140 && !name.iter().any(|byte| { return matches!(byte, b'*' | b'?' | b'[' | b'\\' | 0); });
141 if !plain || !top_ok {
142 return None;
143 }
144 names.push(name.to_vec());
145 }
146 if everything {
147 names.clear();
148 }
149 return Some(PathFilter { names });
150 }
151
152 pub fn matches(&self, path: &[u8]) -> bool {
154 if self.names.is_empty() {
155 return true;
156 }
157 return self.names.iter().any(|name| {
158 return path == name.as_slice() || (path.starts_with(name.as_slice()) && path.get(name.len()) == Some(&b'/'));
159 });
160 }
161}
162
163pub fn parse_diff_filter(text: &str) -> Option<Vec<u8>> {
165 if text.is_empty() || !text.bytes().all(|byte| { return matches!(byte, b'A' | b'D' | b'M' | b'T'); }) {
166 return None;
167 }
168 return Some(text.bytes().collect());
169}
170
171pub fn select(changes: Vec<Change>, filter: &PathFilter, statuses: Option<&[u8]>) -> Vec<Change> {
173 return changes
174 .into_iter()
175 .filter(|change| { return filter.matches(change.path.as_slice()) && statuses.is_none_or(|kept| { return kept.contains(&change.status); }); })
176 .collect();
177}
178
179pub fn raw_lines(changes: &[Change], zero_id: &str) -> Vec<u8> {
181 let mut out: Vec<u8> = Vec::new();
182 for change in changes {
183 let old_id: String = if change.old_id.is_empty() { zero_id.to_string() } else { super::repo_read_object::to_hex(change.old_id.as_slice()) };
184 let new_id: String = if change.new_id.is_empty() { zero_id.to_string() } else { super::repo_read_object::to_hex(change.new_id.as_slice()) };
185 out.extend_from_slice(format!(":{:06o} {:06o} {old_id} {new_id} {}", change.old_mode, change.new_mode, char::from(change.status)).as_bytes());
186 out.push(0);
187 out.extend_from_slice(change.path.as_slice());
188 out.push(0);
189 }
190 return out;
191}
192
193pub fn name_lines(changes: &[Change]) -> Vec<u8> {
195 let mut out: Vec<u8> = Vec::new();
196 for change in changes {
197 out.extend_from_slice(change.path.as_slice());
198 out.push(0);
199 }
200 return out;
201}
202
203#[cfg(test)]
204#[path = "repo_read_diff_tests.rs"]
205mod tests;