use std::collections::BTreeMap;
use crate::error::Result;
use crate::objects::{parse_tree, serialize_tree, tree_entry_cmp, ObjectId, ObjectKind, TreeEntry};
use crate::odb::Odb;
pub(crate) const TREE_MODE: u32 = 0o040000;
pub(crate) type Entry = (u32, ObjectId);
pub(crate) type Edit = (Vec<u8>, Option<Entry>);
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Change {
pub(crate) path: Vec<u8>,
pub(crate) old: Option<Entry>,
pub(crate) new: Option<Entry>,
}
fn entries(odb: &Odb, tree: Option<ObjectId>) -> Result<Vec<TreeEntry>> {
match tree {
Some(t) => parse_tree(&odb.read(&t)?.data),
None => Ok(Vec::new()),
}
}
pub(crate) fn diff(odb: &Odb, old: Option<ObjectId>, new: Option<ObjectId>) -> Result<Vec<Change>> {
let mut out = Vec::new();
diff_into(odb, old, new, b"", &mut out)?;
Ok(out)
}
fn diff_into(
odb: &Odb,
old: Option<ObjectId>,
new: Option<ObjectId>,
prefix: &[u8],
out: &mut Vec<Change>,
) -> Result<()> {
if old == new {
return Ok(());
}
let mut by_name: BTreeMap<Vec<u8>, (Option<Entry>, Option<Entry>)> = BTreeMap::new();
for e in entries(odb, old)? {
by_name.entry(e.name).or_default().0 = Some((e.mode, e.oid));
}
for e in entries(odb, new)? {
by_name.entry(e.name).or_default().1 = Some((e.mode, e.oid));
}
for (name, (a, b)) in by_name {
if a == b {
continue;
}
let path = if prefix.is_empty() {
name
} else {
[prefix, b"/", &name].concat()
};
let a_tree = a.filter(|e| e.0 == TREE_MODE).map(|e| e.1);
let b_tree = b.filter(|e| e.0 == TREE_MODE).map(|e| e.1);
if a_tree.is_some() || b_tree.is_some() {
diff_into(odb, a_tree, b_tree, &path, out)?;
}
let a_file = a.filter(|e| e.0 != TREE_MODE);
let b_file = b.filter(|e| e.0 != TREE_MODE);
if a_file != b_file {
out.push(Change {
path,
old: a_file,
new: b_file,
});
}
}
Ok(())
}
pub(crate) fn lookup(odb: &Odb, tree: ObjectId, path: &[u8]) -> Result<Option<Entry>> {
Ok(odb
.tree_entry_at_path(&tree, path)?
.map(|e| (e.mode, e.oid)))
}
pub(crate) fn edit(
odb: &Odb,
base: Option<ObjectId>,
edits: &[(Vec<u8>, Option<Entry>)],
) -> Result<ObjectId> {
let mut grouped: BTreeMap<Vec<u8>, Vec<Edit>> = BTreeMap::new();
let mut here: Vec<Edit> = Vec::new();
for (path, entry) in edits {
match path.iter().position(|b| *b == b'/') {
Some(i) => grouped
.entry(path[..i].to_vec())
.or_default()
.push((path[i + 1..].to_vec(), *entry)),
None => here.push((path.clone(), *entry)),
}
}
let mut current: BTreeMap<Vec<u8>, Entry> = entries(odb, base)?
.into_iter()
.map(|e| (e.name, (e.mode, e.oid)))
.collect();
for (name, entry) in here {
match entry {
Some(e) => {
current.insert(name, e);
}
None => {
if current.get(&name).is_some_and(|e| e.0 != TREE_MODE) {
current.remove(&name);
}
}
}
}
for (dir, sub) in grouped {
let sub_base = current.get(&dir).filter(|e| e.0 == TREE_MODE).map(|e| e.1);
let tree = edit(odb, sub_base, &sub)?;
if parse_tree(&odb.read(&tree)?.data)?.is_empty() {
if sub_base.is_some() {
current.remove(&dir);
}
} else {
current.insert(dir, (TREE_MODE, tree));
}
}
let mut list: Vec<TreeEntry> = current
.into_iter()
.map(|(name, (mode, oid))| TreeEntry { mode, name, oid })
.collect();
list.sort_by(|a, b| tree_entry_cmp(&a.name, a.mode == TREE_MODE, &b.name, b.mode == TREE_MODE));
odb.write(ObjectKind::Tree, &serialize_tree(&list))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::repo::{InitOptions, Repository};
fn blob(odb: &Odb, text: &str) -> Entry {
match odb.write(ObjectKind::Blob, text.as_bytes()) {
Ok(oid) => (0o100644, oid),
Err(e) => panic!("write blob: {e}"),
}
}
#[test]
fn edits_and_diffs_round_trip() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
let odb = repo.odb();
let (a, b, c) = (blob(odb, "a"), blob(odb, "b"), blob(odb, "c"));
let base = edit(
odb,
None,
&[
(b"top".to_vec(), Some(a)),
(b"src/x".to_vec(), Some(b)),
(b"src/deep/y".to_vec(), Some(c)),
],
)
.expect("base");
assert_eq!(lookup(odb, base, b"src/deep/y").expect("lookup"), Some(c));
let next = edit(
odb,
Some(base),
&[(b"src/deep/y".to_vec(), None), (b"src/x".to_vec(), Some(a))],
)
.expect("edit");
assert_eq!(
lookup(odb, next, b"src/deep").expect("lookup"),
None,
"empty dirs go"
);
let changes = diff(odb, Some(base), Some(next)).expect("diff");
assert_eq!(
changes,
vec![
Change {
path: b"src/deep/y".to_vec(),
old: Some(c),
new: None
},
Change {
path: b"src/x".to_vec(),
old: Some(b),
new: Some(a)
},
]
);
assert!(diff(odb, Some(next), Some(next)).expect("diff").is_empty());
}
#[test]
fn a_directory_and_a_file_replace_each_other_in_one_batch() {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
let odb = repo.odb();
let (x, y, f) = (blob(odb, "x"), blob(odb, "y"), blob(odb, "f"));
let with_dir = edit(
odb,
None,
&[(b"lib/x".to_vec(), Some(x)), (b"lib/y".to_vec(), Some(y))],
)
.expect("tree");
let with_file = edit(odb, None, &[(b"lib".to_vec(), Some(f))]).expect("tree");
for edits in [
vec![
(b"lib/x".to_vec(), None),
(b"lib".to_vec(), Some(f)),
(b"lib/y".to_vec(), None),
],
vec![
(b"lib".to_vec(), Some(f)),
(b"lib/x".to_vec(), None),
(b"lib/y".to_vec(), None),
],
] {
assert_eq!(edit(odb, Some(with_dir), &edits).expect("edit"), with_file);
}
let back = edit(
odb,
Some(with_file),
&[
(b"lib/x".to_vec(), Some(x)),
(b"lib".to_vec(), None),
(b"lib/y".to_vec(), Some(y)),
],
)
.expect("edit");
assert_eq!(back, with_dir);
let changes = diff(odb, Some(with_dir), Some(with_file)).expect("diff");
let edits: Vec<Edit> = changes.into_iter().map(|c| (c.path, c.new)).collect();
assert_eq!(edit(odb, Some(with_dir), &edits).expect("edit"), with_file);
}
}