#![cfg(unix)]
use super::repo_read_tree::{FlatEntry, MODE_GITLINK};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Change {
pub path: Vec<u8>,
pub old_mode: u32,
pub new_mode: u32,
pub old_id: Vec<u8>,
pub new_id: Vec<u8>,
pub status: u8,
}
fn type_of(mode: u32) -> u32 {
return mode & 0o170_000;
}
fn has_entry_below(list: &[FlatEntry], path: &[u8]) -> bool {
let mut prefix: Vec<u8> = path.to_vec();
prefix.push(b'/');
let at: usize = list.partition_point(|entry| { return entry.path.as_slice() < prefix.as_slice(); });
return list.get(at).is_some_and(|entry| { return entry.path.starts_with(prefix.as_slice()); });
}
pub fn diff_sorted(old: &[FlatEntry], new: &[FlatEntry]) -> Option<Vec<Change>> {
let mut changes: Vec<Change> = Vec::new();
let (mut left, mut right): (usize, usize) = (0, 0);
while left < old.len() || right < new.len() {
let order = match (old.get(left), new.get(right)) {
(Some(before), Some(after)) => before.path.cmp(&after.path),
(Some(_), None) => std::cmp::Ordering::Less,
(None, _) => std::cmp::Ordering::Greater,
};
match order {
std::cmp::Ordering::Less => {
let gone: &FlatEntry = &old[left];
if has_entry_below(new, gone.path.as_slice()) {
return None;
}
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' });
left += 1;
}
std::cmp::Ordering::Greater => {
let added: &FlatEntry = &new[right];
if has_entry_below(old, added.path.as_slice()) {
return None;
}
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' });
right += 1;
}
std::cmp::Ordering::Equal => {
let (before, after): (&FlatEntry, &FlatEntry) = (&old[left], &new[right]);
if before.mode != after.mode || before.id != after.id {
let status: u8 = if type_of(before.mode) == type_of(after.mode) { b'M' } else { b'T' };
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 });
}
left += 1;
right += 1;
}
}
}
if changes.iter().any(|change| { return type_of(change.old_mode) == MODE_GITLINK || type_of(change.new_mode) == MODE_GITLINK; }) {
return None;
}
return Some(changes);
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PathFilter {
names: Vec<Vec<u8>>,
}
impl PathFilter {
pub fn parse(specs: &[&[u8]], at_top: bool) -> Option<PathFilter> {
let mut names: Vec<Vec<u8>> = Vec::new();
let mut everything: bool = false;
for spec in specs {
if *spec == b":/" {
everything = true;
continue;
}
let (name, top_ok): (&[u8], bool) = match spec.strip_prefix(b":(top,literal)") {
Some(rest) => (rest, true),
None => (spec, at_top),
};
let plain: bool = !name.is_empty()
&& !name.starts_with(b":")
&& !name.starts_with(b"/")
&& !name.ends_with(b"/")
&& !name.windows(2).any(|pair| { return pair == b"//"; })
&& !name.split(|byte| { return *byte == b'/'; }).any(|part| { return part == b"." || part == b".."; })
&& !name.iter().any(|byte| { return matches!(byte, b'*' | b'?' | b'[' | b'\\' | 0); });
if !plain || !top_ok {
return None;
}
names.push(name.to_vec());
}
if everything {
names.clear();
}
return Some(PathFilter { names });
}
pub fn matches(&self, path: &[u8]) -> bool {
if self.names.is_empty() {
return true;
}
return self.names.iter().any(|name| {
return path == name.as_slice() || (path.starts_with(name.as_slice()) && path.get(name.len()) == Some(&b'/'));
});
}
}
pub fn parse_diff_filter(text: &str) -> Option<Vec<u8>> {
if text.is_empty() || !text.bytes().all(|byte| { return matches!(byte, b'A' | b'D' | b'M' | b'T'); }) {
return None;
}
return Some(text.bytes().collect());
}
pub fn select(changes: Vec<Change>, filter: &PathFilter, statuses: Option<&[u8]>) -> Vec<Change> {
return changes
.into_iter()
.filter(|change| { return filter.matches(change.path.as_slice()) && statuses.is_none_or(|kept| { return kept.contains(&change.status); }); })
.collect();
}
pub fn raw_lines(changes: &[Change], zero_id: &str) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
for change in changes {
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()) };
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()) };
out.extend_from_slice(format!(":{:06o} {:06o} {old_id} {new_id} {}", change.old_mode, change.new_mode, char::from(change.status)).as_bytes());
out.push(0);
out.extend_from_slice(change.path.as_slice());
out.push(0);
}
return out;
}
pub fn name_lines(changes: &[Change]) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
for change in changes {
out.extend_from_slice(change.path.as_slice());
out.push(0);
}
return out;
}
#[cfg(test)]
#[path = "repo_read_diff_tests.rs"]
mod tests;