use std::collections::{BTreeMap, BTreeSet};
use gix_hash::ObjectId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Blob {
pub mode: u32,
pub oid: ObjectId,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Listing {
pub blobs: BTreeMap<Vec<u8>, Blob>,
pub trees: BTreeMap<Vec<u8>, ObjectId>,
pub gitlinks: BTreeSet<Vec<u8>>,
}
pub const ATTRIBUTES_FILE: &[u8] = b".gitattributes";
impl Listing {
pub fn attribute_files(&self) -> BTreeMap<Vec<u8>, ObjectId> {
self.blobs
.iter()
.filter(|(path, _)| is_attributes_file(path))
.map(|(path, blob)| (path.clone(), blob.oid))
.collect()
}
}
pub fn is_attributes_file(path: &[u8]) -> bool {
path == ATTRIBUTES_FILE
|| (path.len() > ATTRIBUTES_FILE.len()
&& path.ends_with(ATTRIBUTES_FILE)
&& path[path.len() - ATTRIBUTES_FILE.len() - 1] == b'/')
}
pub fn directory_of(path: &[u8]) -> &[u8] {
match path.iter().rposition(|byte| *byte == b'/') {
Some(position) => &path[..position + 1],
None => &[],
}
}
pub fn parse(output: &[u8]) -> Option<Listing> {
let mut listing = Listing::default();
for record in output.split(|byte| *byte == 0) {
if record.is_empty() {
continue;
}
let tab = record.iter().position(|byte| *byte == b'\t')?;
let (meta, path) = record.split_at(tab);
let path = &path[1..];
let mut fields = meta.split(|byte| *byte == b' ');
let mode = std::str::from_utf8(fields.next()?).ok()?;
let mode = u32::from_str_radix(mode, 8).ok()?;
let kind = fields.next()?;
let oid = std::str::from_utf8(fields.next()?).ok()?;
let oid = ObjectId::from_hex(oid.as_bytes()).ok()?;
match kind {
b"blob" => {
listing.blobs.insert(path.to_vec(), Blob { mode, oid });
}
b"tree" => {
listing.trees.insert(path.to_vec(), oid);
}
b"commit" => {
listing.gitlinks.insert(path.to_vec());
}
_ => return None,
}
}
Some(listing)
}
#[cfg(test)]
mod tests {
use super::*;
fn record(mode: &str, kind: &str, oid: &str, path: &str) -> Vec<u8> {
format!("{mode} {kind} {oid}\t{path}\0").into_bytes()
}
const A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
#[test]
fn reads_blobs_trees_and_gitlinks() {
let mut output = record("040000", "tree", A, "src");
output.extend(record("100755", "blob", B, "src/run.sh"));
output.extend(record("160000", "commit", A, "vendor/lib"));
let listing = parse(&output).expect("parses");
assert_eq!(listing.trees.len(), 1);
assert_eq!(
listing.blobs[b"src/run.sh".as_slice()],
Blob {
mode: 0o100755,
oid: ObjectId::from_hex(B.as_bytes()).unwrap()
}
);
assert!(listing.gitlinks.contains(b"vendor/lib".as_slice()));
}
#[test]
fn keeps_paths_that_are_not_utf8() {
let mut output = b"100644 blob ".to_vec();
output.extend(A.as_bytes());
output.extend(b"\ta/\xff\0");
let listing = parse(&output).expect("parses");
assert!(listing.blobs.contains_key(b"a/\xff".as_slice()));
}
#[test]
fn recognises_attributes_files_at_any_depth() {
assert!(is_attributes_file(b".gitattributes"));
assert!(is_attributes_file(b"src/.gitattributes"));
assert!(!is_attributes_file(b"src/not.gitattributes"));
assert!(!is_attributes_file(b"gitattributes"));
}
#[test]
fn splits_the_directory_from_the_path() {
assert_eq!(directory_of(b"a/b/c.txt"), b"a/b/");
assert_eq!(directory_of(b"c.txt"), b"");
}
}