#![cfg(unix)]
use crate::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 (mut low, mut high): (usize, usize) = (0, list.len());
while low < high {
let middle: usize = low + (high - low) / 2;
if list[middle].path.as_slice() < prefix.as_slice() {
low = middle + 1;
} else {
high = middle;
}
}
return match list.get(low) {
Some(entry) => entry.path.starts_with(prefix.as_slice()),
None => false,
};
}
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;
}
}
}
for change in &changes {
if 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(is_double_slash)
&& !name.split(is_slash).any(is_dot_part)
&& !name.iter().any(is_pattern_byte);
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;
}
for name in &self.names {
if path == name.as_slice() || (path.starts_with(name.as_slice()) && path.get(name.len()) == Some(&b'/')) {
return true;
}
}
return false;
}
}
fn is_double_slash(pair: &[u8]) -> bool {
return pair == b"//";
}
fn is_slash(byte: &u8) -> bool {
return *byte == b'/';
}
fn is_dot_part(part: &[u8]) -> bool {
return part == b"." || part == b"..";
}
fn is_pattern_byte(byte: &u8) -> bool {
return matches!(byte, b'*' | b'?' | b'[' | b'\\' | 0);
}
fn is_status_letter(byte: u8) -> bool {
return matches!(byte, b'A' | b'D' | b'M' | b'T');
}
pub fn parse_diff_filter(text: &str) -> Option<Vec<u8>> {
if text.is_empty() || !text.bytes().all(is_status_letter) {
return None;
}
return Some(text.bytes().collect());
}
pub fn select(changes: Vec<Change>, filter: &PathFilter, statuses: Option<&[u8]>) -> Vec<Change> {
let mut selected: Vec<Change> = Vec::new();
for change in changes {
if !filter.matches(change.path.as_slice()) {
continue;
}
let status_kept: bool = match statuses {
Some(kept) => kept.contains(&change.status),
None => true,
};
if status_kept {
selected.push(change);
}
}
return selected;
}
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 { crate::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 { crate::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;