#![cfg(unix)]
use crate::lfs_pointer::{Verdict, decode_pointer};
use crate::repo_read_layout::ObjectFormat;
use crate::repo_read_object::{ObjectKind, name_length};
use crate::repo_read_odb::ObjectDatabase;
use crate::repo_read_tree::{CommitFields, MODE_GITLINK, is_tree_mode, parse_commit, parse_tree};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::time::{Duration, Instant};
pub const MAX_COMMITS: usize = 100_000;
pub const SCAN_BUDGET: Duration = Duration::from_secs(3);
struct Scan<'a> {
database: &'a ObjectDatabase,
format: ObjectFormat,
commits: HashMap<Vec<u8>, CommitFields>,
times: HashMap<Vec<u8>, i64>,
seen_trees: HashSet<Vec<Vec<u8>>>,
candidates: BTreeSet<Vec<u8>>,
deadline: Instant,
}
impl Scan<'_> {
fn commit(&mut self, name: &[u8]) -> Option<CommitFields> {
if let Some(known) = self.commits.get(name) {
return Some(known.clone());
}
let object = self.database.read(name)?;
if object.kind != ObjectKind::Commit {
return None;
}
let parsed: CommitFields = parse_commit(object.bytes.as_slice(), self.format)?;
self.times.insert(name.to_vec(), committer_time(object.bytes.as_slice())?);
self.commits.insert(name.to_vec(), parsed.clone());
return Some(parsed);
}
fn time(&self, name: &[u8]) -> i64 {
return self.times.get(name).copied().unwrap_or_default();
}
fn tree(&self, name: &[u8]) -> Option<Vec<crate::repo_read_tree::TreeEntry>> {
let object = self.database.read(name)?;
if object.kind != ObjectKind::Tree {
return None;
}
return parse_tree(object.bytes.as_slice(), name_length(self.format));
}
fn compare(&mut self, tree: &[u8], parents: &[Vec<u8>]) -> Option<()> {
if Instant::now() >= self.deadline {
return None;
}
if parents.iter().any(|parent| {
return parent.as_slice() == tree;
}) || !self.seen_trees.insert([vec![tree.to_vec()], parents.to_vec()].concat())
{
return Some(());
}
let entries = self.tree(tree)?;
let mut parent_entries = Vec::with_capacity(parents.len());
for parent in parents {
parent_entries.push(self.tree(parent)?);
}
for entry in &entries {
if entry.mode == MODE_GITLINK {
continue;
}
let counterparts: Vec<&crate::repo_read_tree::TreeEntry> = parent_entries
.iter()
.filter_map(|list| {
return list.iter().find(|candidate| {
return candidate.name == entry.name;
});
})
.collect();
if is_tree_mode(entry.mode) {
let below: Vec<Vec<u8>> = counterparts
.iter()
.filter(|other| {
return is_tree_mode(other.mode);
})
.map(|other| {
return other.id.clone();
})
.collect();
self.compare(entry.id.as_slice(), below.as_slice())?;
} else if !counterparts.iter().any(|other| {
return other.id == entry.id && !is_tree_mode(other.mode);
}) {
self.candidates.insert(entry.id.clone());
}
}
return Some(());
}
}
const CLOCK_SKEW_SLOP: usize = 5;
fn committer_time(bytes: &[u8]) -> Option<i64> {
let header_end: usize = bytes.windows(2).position(|pair| {
return pair == b"\n\n";
})?;
let line: &[u8] = bytes[..header_end].split(|byte| {
return *byte == b'\n';
}).find(|line| {
return line.starts_with(b"committer ");
})?;
let after_email: &[u8] = &line[line.iter().rposition(|byte| {
return *byte == b'>';
})? + 1..];
let text: &str = std::str::from_utf8(after_email).ok()?;
return text.split_whitespace().next()?.parse::<i64>().ok();
}
struct Walk {
queue: std::collections::BinaryHeap<(i64, Vec<u8>)>,
known: HashMap<Vec<u8>, bool>,
queued: HashSet<Vec<u8>>,
unknown_queued: usize,
expanded: HashSet<Vec<u8>>,
}
impl Walk {
fn mark(&mut self, scan: &mut Scan<'_>, name: &[u8], known: bool) -> Option<()> {
let mut pending: Vec<Vec<u8>> = vec![name.to_vec()];
while let Some(current) = pending.pop() {
match self.known.get(current.as_slice()).copied() {
None => {
scan.commit(current.as_slice())?;
self.known.insert(current.clone(), known);
self.queue.push((scan.time(current.as_slice()), current.clone()));
self.queued.insert(current);
if !known {
self.unknown_queued += 1;
}
}
Some(false) if known => {
self.known.insert(current.clone(), true);
if self.queued.contains(current.as_slice()) {
self.unknown_queued -= 1;
} else if self.expanded.contains(current.as_slice()) {
pending.extend(scan.commit(current.as_slice())?.parents);
}
}
Some(_) => {}
}
}
return Some(());
}
}
fn new_commits(scan: &mut Scan<'_>, include: &[Vec<u8>], stop: &[Vec<u8>]) -> Option<Vec<Vec<u8>>> {
let mut walk: Walk = Walk {
queue: std::collections::BinaryHeap::new(),
known: HashMap::new(),
queued: HashSet::new(),
unknown_queued: 0,
expanded: HashSet::new(),
};
for name in stop {
walk.mark(scan, name.as_slice(), true)?;
}
for name in include {
walk.mark(scan, name.as_slice(), false)?;
}
let mut listed: Vec<Vec<u8>> = Vec::new();
let mut slop: usize = 0;
let mut popped: usize = 0;
while let Some((_, name)) = walk.queue.pop() {
walk.queued.remove(name.as_slice());
let known: bool = walk.known.get(name.as_slice()).copied().unwrap_or(true);
if !known {
walk.unknown_queued -= 1;
listed.push(name.clone());
}
popped += 1;
if popped > MAX_COMMITS || Instant::now() >= scan.deadline {
return None;
}
let parents: Vec<Vec<u8>> = scan.commit(name.as_slice())?.parents;
walk.expanded.insert(name);
for parent in &parents {
walk.mark(scan, parent.as_slice(), known)?;
}
if walk.unknown_queued == 0 {
slop += 1;
if slop > CLOCK_SKEW_SLOP {
break;
}
} else {
slop = 0;
}
}
return Some(
listed
.into_iter()
.filter(|name| {
return !walk.known.get(name.as_slice()).copied().unwrap_or(false);
})
.collect(),
);
}
pub fn new_pointer_objects(
database: &ObjectDatabase,
format: ObjectFormat,
include: &[Vec<u8>],
stop: &[Vec<u8>],
budget: Duration,
) -> Option<Vec<String>> {
let mut scan: Scan<'_> = Scan {
database,
format,
commits: HashMap::new(),
times: HashMap::new(),
seen_trees: HashSet::new(),
candidates: BTreeSet::new(),
deadline: Instant::now() + budget,
};
for name in new_commits(&mut scan, include, stop)? {
let commit: CommitFields = scan.commit(name.as_slice())?;
let mut parent_trees: Vec<Vec<u8>> = Vec::with_capacity(commit.parents.len());
for parent in &commit.parents {
parent_trees.push(scan.commit(parent.as_slice())?.tree);
}
scan.compare(commit.tree.as_slice(), parent_trees.as_slice())?;
}
let mut objects: BTreeSet<String> = BTreeSet::new();
for candidate in &scan.candidates {
if Instant::now() >= scan.deadline {
return None;
}
let blob = database.read(candidate.as_slice())?;
if blob.kind != ObjectKind::Blob {
return None;
}
match decode_pointer(blob.bytes.as_slice()) {
Verdict::NotUploadable => {}
Verdict::Pointer(name) => {
objects.insert(name);
}
Verdict::Decline => return None,
}
}
return Some(objects.into_iter().collect());
}
#[cfg(test)]
#[path = "lfs_pointer_scan_tests.rs"]
mod tests;