use std::collections::{HashMap, HashSet};
use super::{Repository, RepositoryError};
pub fn fetch(
source: &dyn Repository,
target: &mut dyn Repository,
revision_id: Option<&[u8]>,
) -> Result<usize, RepositoryError> {
let missing = missing_revisions(source, target, revision_id)?;
if missing.is_empty() {
return Ok(0);
}
let ordered = toposort(source, &missing)?;
let copied = ordered.len();
if target.try_fetch_from(source, &ordered)? {
return Ok(copied);
}
target.start_write_group()?;
for rev_id in &ordered {
copy_revision(source, target, rev_id)?;
}
target.commit_write_group()?;
Ok(copied)
}
fn missing_revisions(
source: &dyn Repository,
target: &dyn Repository,
revision_id: Option<&[u8]>,
) -> Result<HashSet<Vec<u8>>, RepositoryError> {
let wanted = match revision_id {
None => source.all_revision_ids()?.into_iter().collect(),
Some(id) => ancestry_closure(source, id)?,
};
let present: HashSet<Vec<u8>> = target.all_revision_ids()?.into_iter().collect();
Ok(wanted.difference(&present).cloned().collect())
}
fn ancestry_closure(
source: &dyn Repository,
revision_id: &[u8],
) -> Result<HashSet<Vec<u8>>, RepositoryError> {
let mut seen = HashSet::new();
let mut pending = vec![revision_id.to_vec()];
while let Some(id) = pending.pop() {
if id == crate::branch::NULL_REVISION || !seen.insert(id.clone()) {
continue;
}
let parent_map = source.get_parent_map(std::slice::from_ref(&id))?;
if let Some(parents) = parent_map.get(&id) {
for p in parents {
if p != crate::branch::NULL_REVISION && !seen.contains(p) {
pending.push(p.clone());
}
}
}
}
Ok(seen)
}
fn toposort(
source: &dyn Repository,
revisions: &HashSet<Vec<u8>>,
) -> Result<Vec<Vec<u8>>, RepositoryError> {
let all: Vec<Vec<u8>> = revisions.iter().cloned().collect();
let parent_map = source.get_parent_map(&all)?;
let deps: HashMap<Vec<u8>, Vec<Vec<u8>>> = all
.iter()
.map(|id| {
let parents = parent_map
.get(id)
.map(|ps| {
ps.iter()
.filter(|p| revisions.contains(*p))
.cloned()
.collect()
})
.unwrap_or_default();
(id.clone(), parents)
})
.collect();
let mut remaining: HashMap<Vec<u8>, usize> =
deps.iter().map(|(k, v)| (k.clone(), v.len())).collect();
let mut children: HashMap<Vec<u8>, Vec<Vec<u8>>> = HashMap::new();
for (id, parents) in &deps {
for p in parents {
children.entry(p.clone()).or_default().push(id.clone());
}
}
let mut ready: Vec<Vec<u8>> = remaining
.iter()
.filter(|(_, &n)| n == 0)
.map(|(k, _)| k.clone())
.collect();
ready.sort();
let mut order = Vec::with_capacity(all.len());
while let Some(id) = ready.pop() {
order.push(id.clone());
if let Some(kids) = children.get(&id) {
let mut newly_ready = Vec::new();
for child in kids {
let count = remaining.get_mut(child).expect("child tracked");
*count -= 1;
if *count == 0 {
newly_ready.push(child.clone());
}
}
ready.extend(newly_ready);
ready.sort();
}
}
if order.len() != all.len() {
return Err(RepositoryError::Corrupt(
"revision graph has a cycle or a missing parent within the fetch set".to_string(),
));
}
Ok(order)
}
fn copy_revision(
source: &dyn Repository,
target: &mut dyn Repository,
rev_id: &[u8],
) -> Result<(), RepositoryError> {
let revision = source.get_revision(rev_id)?;
let parents: Vec<Vec<u8>> = revision
.parent_ids
.iter()
.map(|p| p.as_bytes().to_vec())
.collect();
let tree = source.revision_tree(rev_id)?;
let root = tree
.inventory()
.root_entry()
.map_err(|e| RepositoryError::Corrupt(format!("reading root entry: {e:?}")))?;
let root_id = root
.as_ref()
.map(|r| r.file_id().as_bytes().to_vec())
.unwrap_or_else(|| crate::inventory::ROOT_ID.to_vec());
let mut entries: Vec<crate::inventory::Entry> = Vec::new();
entries.extend(root);
entries.extend(tree.iter_entries().into_iter().map(|(_, e)| e));
use crate::osutils::Kind;
for entry in &entries {
let introduced = entry
.revision()
.map(|r| r.as_bytes() == rev_id)
.unwrap_or(false);
if !introduced {
continue;
}
let file_id = entry.file_id().as_bytes().to_vec();
let bytes: Vec<u8> = match entry.kind() {
Kind::File => source.get_file_text(&file_id, rev_id)?,
Kind::Symlink => entry
.symlink_target()
.map(|t| t.as_bytes().to_vec())
.unwrap_or_default(),
Kind::Directory | Kind::TreeReference => Vec::new(),
};
target.add_text(&file_id, rev_id, &[], &bytes)?;
}
target.add_inventory_from_entries(rev_id, &parents, &root_id, &entries)?;
target.add_revision(&revision, &parents)?;
if let Some(sig) = source.get_signature_text(rev_id)? {
target.add_signature_text(rev_id, &sig)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::repository::Pack2aRepository;
use crate::transport::{LocalTransport, SharedTransport};
use std::sync::Arc;
fn temp_repo() -> (tempfile::TempDir, SharedTransport) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("repository");
std::fs::create_dir_all(&path).unwrap();
(dir, Arc::new(LocalTransport::new(&path)))
}
fn revision(id: &[u8], parents: Vec<&[u8]>) -> crate::revision::Revision {
crate::revision::Revision::new(
crate::RevisionId::from(id),
parents.into_iter().map(crate::RevisionId::from).collect(),
Some("T <t@e>".to_string()),
"m".to_string(),
std::collections::HashMap::new(),
None,
1577880000.0,
Some(0),
)
}
fn make_chain(t: &SharedTransport, n: usize) -> Vec<Vec<u8>> {
let mut repo = Pack2aRepository::create(t.clone()).unwrap();
let root = crate::inventory::ROOT_ID;
let mut ids: Vec<Vec<u8>> = Vec::new();
for i in 1..=n {
let rev = format!("rev-{i}").into_bytes();
let parents: Vec<&[u8]> = if i == 1 {
vec![]
} else {
vec![ids[i - 2].as_slice()]
};
let parent_vecs: Vec<Vec<u8>> = parents.iter().map(|p| p.to_vec()).collect();
repo.start_write_group().unwrap();
let text = format!("hello {i}\n").into_bytes();
repo.add_text(b"file-1", &rev, &[], &text).unwrap();
let entries = vec![
crate::inventory::Entry::root(
crate::FileId::from(root),
Some(crate::RevisionId::from(rev.as_slice())),
),
crate::inventory::Entry::file(
crate::FileId::from(&b"file-1"[..]),
"a.txt".into(),
crate::FileId::from(root),
Some(crate::RevisionId::from(rev.as_slice())),
Some(crate::weave::sha_strings(&[text.as_slice()])),
Some(text.len() as u64),
Some(false),
None,
),
];
repo.add_inventory_from_entries(&rev, &parent_vecs, root, &entries)
.unwrap();
repo.add_revision(&revision(&rev, parents), &parent_vecs)
.unwrap();
repo.commit_write_group().unwrap();
ids.push(rev);
}
ids
}
#[test]
fn fetch_copies_ancestry() {
let (_sd, st) = temp_repo();
let ids = make_chain(&st, 3);
let source = Pack2aRepository::open(st).unwrap();
let (_td, tt) = temp_repo();
let mut target = Pack2aRepository::create(tt.clone()).unwrap();
let copied = fetch(&source, &mut target, Some(&ids[2])).unwrap();
assert_eq!(copied, 3);
let target = Pack2aRepository::open(tt).unwrap();
let mut got = target.all_revision_ids().unwrap();
got.sort();
assert_eq!(got, ids);
assert_eq!(
target.get_file_text(b"file-1", &ids[2]).unwrap(),
b"hello 3\n"
);
}
#[test]
fn fetch_everything_then_noop() {
let (_sd, st) = temp_repo();
let ids = make_chain(&st, 2);
let source = Pack2aRepository::open(st).unwrap();
let (_td, tt) = temp_repo();
let mut target = Pack2aRepository::create(tt.clone()).unwrap();
assert_eq!(fetch(&source, &mut target, None).unwrap(), 2);
let mut target = Pack2aRepository::open(tt).unwrap();
assert_eq!(fetch(&source, &mut target, None).unwrap(), 0);
let _ = ids;
}
#[test]
fn fetch_only_missing() {
let (_sd, st) = temp_repo();
let ids = make_chain(&st, 3);
let source = Pack2aRepository::open(st).unwrap();
let (_td, tt) = temp_repo();
let mut target = Pack2aRepository::create(tt.clone()).unwrap();
assert_eq!(fetch(&source, &mut target, Some(&ids[0])).unwrap(), 1);
let mut target = Pack2aRepository::open(tt).unwrap();
assert_eq!(fetch(&source, &mut target, Some(&ids[2])).unwrap(), 2);
}
#[cfg(feature = "knitpack")]
#[test]
fn fetch_across_formats_knitpack_to_2a() {
use crate::repository::KnitPackRepository;
let (_sd, st) = temp_repo();
let knitpack6 =
crate::repository::find_format(b"Bazaar RepositoryFormatKnitPack6 (bzr 1.9)\n")
.unwrap();
let mut src = KnitPackRepository::create(st.clone(), knitpack6).unwrap();
let root = crate::inventory::ROOT_ID;
let rev = b"rev-1";
src.start_write_group().unwrap();
let text = b"hello\n";
src.add_text(b"file-1", rev, &[], text).unwrap();
let entries = vec![
crate::inventory::Entry::root(
crate::FileId::from(root),
Some(crate::RevisionId::from(&rev[..])),
),
crate::inventory::Entry::file(
crate::FileId::from(&b"file-1"[..]),
"a.txt".into(),
crate::FileId::from(root),
Some(crate::RevisionId::from(&rev[..])),
Some(crate::weave::sha_strings(&[text.as_slice()])),
Some(text.len() as u64),
Some(false),
None,
),
];
src.add_inventory_from_entries(rev, &[], root, &entries)
.unwrap();
src.add_revision(&revision(rev, vec![]), &[]).unwrap();
src.commit_write_group().unwrap();
let source = KnitPackRepository::open(st).unwrap();
let (_td, tt) = temp_repo();
let mut target = Pack2aRepository::create(tt.clone()).unwrap();
assert_eq!(fetch(&source, &mut target, Some(rev)).unwrap(), 1);
let target = Pack2aRepository::open(tt).unwrap();
assert!(target.has_revision(rev).unwrap());
assert_eq!(target.get_revision(rev).unwrap().message, "m");
assert_eq!(target.get_file_text(b"file-1", rev).unwrap(), b"hello\n");
let inv = target.get_inventory(rev).unwrap();
let paths: Vec<String> = inv.entries().unwrap().into_iter().map(|(p, _)| p).collect();
assert_eq!(paths, vec!["a.txt".to_string()]);
assert_eq!(
target.get_file_text_at_path("a.txt", rev).unwrap(),
b"hello\n"
);
}
#[test]
fn streaming_matches_generic() {
let (_sd, st) = temp_repo();
let ids = make_chain(&st, 4);
let source = Pack2aRepository::open(st).unwrap();
let (_fd, ft) = temp_repo();
let mut fast = Pack2aRepository::create(ft.clone()).unwrap();
fetch(&source, &mut fast, Some(&ids[3])).unwrap();
let fast = Pack2aRepository::open(ft).unwrap();
let (_gd, gt) = temp_repo();
let mut generic = Pack2aRepository::create(gt.clone()).unwrap();
generic.start_write_group().unwrap();
for rev in &ids {
copy_revision(&source, &mut generic, rev).unwrap();
}
generic.commit_write_group().unwrap();
let generic = Pack2aRepository::open(gt).unwrap();
let mut a = fast.all_revision_ids().unwrap();
let mut b = generic.all_revision_ids().unwrap();
a.sort();
b.sort();
assert_eq!(a, ids);
assert_eq!(b, ids);
for rev in &ids {
assert_eq!(
fast.get_revision(rev).unwrap().message,
generic.get_revision(rev).unwrap().message
);
assert_eq!(
fast.get_file_text(b"file-1", rev).unwrap(),
generic.get_file_text(b"file-1", rev).unwrap()
);
let fp: Vec<String> = fast
.get_inventory(rev)
.unwrap()
.entries()
.unwrap()
.into_iter()
.map(|(p, _)| p)
.collect();
let gp: Vec<String> = generic
.get_inventory(rev)
.unwrap()
.entries()
.unwrap()
.into_iter()
.map(|(p, _)| p)
.collect();
assert_eq!(fp, gp);
}
}
#[test]
fn streaming_incremental_into_nonempty() {
let (_sd, st) = temp_repo();
let ids = make_chain(&st, 4);
let source = Pack2aRepository::open(st).unwrap();
let (_td, tt) = temp_repo();
let mut target = Pack2aRepository::create(tt.clone()).unwrap();
assert_eq!(fetch(&source, &mut target, Some(&ids[1])).unwrap(), 2);
let mut target = Pack2aRepository::open(tt.clone()).unwrap();
assert_eq!(fetch(&source, &mut target, Some(&ids[3])).unwrap(), 2);
let target = Pack2aRepository::open(tt).unwrap();
let mut got = target.all_revision_ids().unwrap();
got.sort();
assert_eq!(got, ids);
for (i, rev) in ids.iter().enumerate() {
assert_eq!(
target.get_file_text(b"file-1", rev).unwrap(),
format!("hello {}\n", i + 1).into_bytes()
);
}
}
#[cfg(feature = "knitpack")]
fn make_knitpack_chain(t: &SharedTransport, n: usize) -> Vec<Vec<u8>> {
use crate::repository::KnitPackRepository;
let knitpack6 =
crate::repository::find_format(b"Bazaar RepositoryFormatKnitPack6 (bzr 1.9)\n")
.unwrap();
let mut repo = KnitPackRepository::create(t.clone(), knitpack6).unwrap();
let root = crate::inventory::ROOT_ID;
let mut ids: Vec<Vec<u8>> = Vec::new();
for i in 1..=n {
let rev = format!("rev-{i}").into_bytes();
let parents: Vec<&[u8]> = if i == 1 {
vec![]
} else {
vec![ids[i - 2].as_slice()]
};
let parent_vecs: Vec<Vec<u8>> = parents.iter().map(|p| p.to_vec()).collect();
repo.start_write_group().unwrap();
let text = format!("hello {i}\n").into_bytes();
repo.add_text(b"file-1", &rev, &[], &text).unwrap();
let entries = vec![
crate::inventory::Entry::root(
crate::FileId::from(root),
Some(crate::RevisionId::from(rev.as_slice())),
),
crate::inventory::Entry::file(
crate::FileId::from(&b"file-1"[..]),
"a.txt".into(),
crate::FileId::from(root),
Some(crate::RevisionId::from(rev.as_slice())),
Some(crate::weave::sha_strings(&[text.as_slice()])),
Some(text.len() as u64),
Some(false),
None,
),
];
repo.add_inventory_from_entries(&rev, &parent_vecs, root, &entries)
.unwrap();
repo.add_revision(&revision(&rev, parents), &parent_vecs)
.unwrap();
repo.commit_write_group().unwrap();
ids.push(rev);
}
ids
}
#[cfg(feature = "knitpack")]
#[test]
fn fetch_knitpack_to_knitpack_streams() {
use crate::repository::KnitPackRepository;
let knitpack6 =
crate::repository::find_format(b"Bazaar RepositoryFormatKnitPack6 (bzr 1.9)\n")
.unwrap();
let (_sd, st) = temp_repo();
let ids = make_knitpack_chain(&st, 3);
let source = KnitPackRepository::open(st).unwrap();
let (_td, tt) = temp_repo();
let mut target = KnitPackRepository::create(tt.clone(), knitpack6).unwrap();
assert_eq!(fetch(&source, &mut target, Some(&ids[2])).unwrap(), 3);
let target = KnitPackRepository::open(tt).unwrap();
let mut got = target.all_revision_ids().unwrap();
got.sort();
assert_eq!(got, ids);
for (i, rev) in ids.iter().enumerate() {
assert_eq!(
target.get_file_text(b"file-1", rev).unwrap(),
format!("hello {}\n", i + 1).into_bytes()
);
}
}
#[cfg(feature = "knitpack")]
#[test]
fn knitpack_streaming_matches_generic() {
use crate::repository::KnitPackRepository;
let knitpack6 =
crate::repository::find_format(b"Bazaar RepositoryFormatKnitPack6 (bzr 1.9)\n")
.unwrap();
let (_sd, st) = temp_repo();
let ids = make_knitpack_chain(&st, 3);
let source = KnitPackRepository::open(st).unwrap();
let (_fd, ft) = temp_repo();
let mut fast = KnitPackRepository::create(ft.clone(), knitpack6).unwrap();
fetch(&source, &mut fast, Some(&ids[2])).unwrap();
let fast = KnitPackRepository::open(ft).unwrap();
let (_gd, gt) = temp_repo();
let mut generic = KnitPackRepository::create(gt.clone(), knitpack6).unwrap();
generic.start_write_group().unwrap();
for rev in &ids {
copy_revision(&source, &mut generic, rev).unwrap();
}
generic.commit_write_group().unwrap();
let generic = KnitPackRepository::open(gt).unwrap();
for rev in &ids {
assert_eq!(
fast.get_revision(rev).unwrap().message,
generic.get_revision(rev).unwrap().message
);
assert_eq!(
fast.get_file_text(b"file-1", rev).unwrap(),
generic.get_file_text(b"file-1", rev).unwrap()
);
}
}
}