use std::collections::HashMap;
use crate::knit::{
parse_knit_index_value, KnitAccess, KnitError, KnitIndex, KnitIndexMemo, KnitKey, KnitMethod,
KnitPlainFactory, KnitRecordDetails, KnitVersionedFiles,
};
use crate::pack_repo::{index_extension, IndexKind};
use crate::transport::{SharedTransport, Transport, TransportError};
use super::format::RepositoryFormat;
use super::pack_2a::RepositoryError;
use super::unkey_knit_parent_map;
use crate::declare_repository_format;
use crate::xml_serializer::{
XMLInventorySerializer5, XMLInventorySerializer6, XMLInventorySerializer7,
XMLRevisionSerializer5,
};
declare_repository_format! {
FORMAT_KNIT_PACK_1 {
format_string: b"Bazaar pack repository format 1 (needs bzr 0.92)\n",
description: "Pack repository format 1",
revision_serializer: &XMLRevisionSerializer5,
inventory_serializer: &XMLInventorySerializer5,
open: open_knit_pack,
create: create_knit_pack,
supported: true,
uses_btree_index: false,
}
}
declare_repository_format! {
FORMAT_KNIT_PACK_3 {
format_string: b"Bazaar pack repository format 1 with subtree support (needs bzr 0.92)\n",
description: "Pack repository format 1 with subtree support",
revision_serializer: &XMLRevisionSerializer5,
inventory_serializer: &XMLInventorySerializer7,
open: open_knit_pack,
create: create_knit_pack,
rich_root_data: true,
supports_tree_reference: true,
supported: true,
uses_btree_index: false,
}
}
declare_repository_format! {
FORMAT_KNIT_PACK_4 {
format_string: b"Bazaar pack repository format 1 with rich root (needs bzr 1.0)\n",
description: "Pack repository format 1 with rich root",
revision_serializer: &XMLRevisionSerializer5,
inventory_serializer: &XMLInventorySerializer6,
open: open_knit_pack,
create: create_knit_pack,
rich_root_data: true,
supported: true,
uses_btree_index: false,
}
}
declare_repository_format! {
FORMAT_KNIT_PACK_5 {
format_string: b"Bazaar RepositoryFormatKnitPack5 (bzr 1.6)\n",
description: "Pack repository format 5 (stackable)",
revision_serializer: &XMLRevisionSerializer5,
inventory_serializer: &XMLInventorySerializer5,
open: open_knit_pack,
create: create_knit_pack,
supports_external_lookups: true,
supported: true,
uses_btree_index: false,
}
}
declare_repository_format! {
FORMAT_KNIT_PACK_5_RICH_ROOT {
format_string: b"Bazaar RepositoryFormatKnitPack5RichRoot (bzr 1.6.1)\n",
description: "Pack repository format 5 with rich root (stackable)",
revision_serializer: &XMLRevisionSerializer5,
inventory_serializer: &XMLInventorySerializer6,
open: open_knit_pack,
create: create_knit_pack,
rich_root_data: true,
supports_external_lookups: true,
supported: true,
uses_btree_index: false,
}
}
declare_repository_format! {
FORMAT_KNIT_PACK_5_RICH_ROOT_BROKEN {
format_string: b"Bazaar RepositoryFormatKnitPack5RichRoot (bzr 1.6)\n",
description: "Pack repository format 5 with rich root (broken)",
revision_serializer: &XMLRevisionSerializer5,
inventory_serializer: &XMLInventorySerializer6,
open: open_knit_pack,
create: create_knit_pack,
rich_root_data: true,
supports_external_lookups: true,
deprecated: true,
uses_btree_index: false,
}
}
declare_repository_format! {
FORMAT_KNIT_PACK_6 {
format_string: b"Bazaar RepositoryFormatKnitPack6 (bzr 1.9)\n",
description: "Pack repository format 6 (btree indexes, stackable)",
revision_serializer: &XMLRevisionSerializer5,
inventory_serializer: &XMLInventorySerializer5,
open: open_knit_pack,
create: create_knit_pack,
supports_external_lookups: true,
supported: true,
}
}
declare_repository_format! {
FORMAT_KNIT_PACK_6_RICH_ROOT {
format_string: b"Bazaar RepositoryFormatKnitPack6RichRoot (bzr 1.9)\n",
description: "Pack repository format 6 with rich root (btree, stackable)",
revision_serializer: &XMLRevisionSerializer5,
inventory_serializer: &XMLInventorySerializer6,
open: open_knit_pack,
create: create_knit_pack,
rich_root_data: true,
supports_external_lookups: true,
supported: true,
}
}
type PackName = String;
#[derive(Clone, Copy)]
enum RepackTarget {
Revisions,
Inventories,
Texts,
Signatures,
}
struct PackKnitIndex {
entries: HashMap<KnitKey, KnitRecordDetails<PackName>>,
has_graph: bool,
}
impl PackKnitIndex {
fn load(
transport: &dyn Transport,
packs: &[PackName],
kind: IndexKind,
) -> Result<Self, RepositoryError> {
let ext = index_extension(kind);
let mut entries: HashMap<KnitKey, KnitRecordDetails<PackName>> = HashMap::new();
let mut has_graph = false;
for pack in packs {
let name = format!("indices/{pack}{ext}");
let index = super::pack_index::PackIndex::open(transport, &name)?;
if index.node_ref_lists() > 0 {
has_graph = true;
}
for (key, value, refs) in index.iter_all_entries() {
let parsed = parse_knit_index_value(value)
.map_err(|e| RepositoryError::Corrupt(format!("knit index value: {e}")))?;
let parents: Vec<KnitKey> = refs.first().cloned().unwrap_or_default();
let compression_parent: Option<KnitKey> =
refs.get(1).and_then(|cp| cp.first().cloned());
let method = if compression_parent.is_some() {
KnitMethod::LineDelta
} else {
KnitMethod::Fulltext
};
entries.insert(
key.clone(),
KnitRecordDetails {
method,
noeol: parsed.noeol,
index_memo: KnitIndexMemo {
file_ref: pack.clone(),
offset: parsed.pos,
length: parsed.size as usize,
},
compression_parent,
parents,
},
);
}
}
Ok(PackKnitIndex { entries, has_graph })
}
}
impl KnitIndex for PackKnitIndex {
type F = PackName;
fn get_build_details(
&self,
keys: &[KnitKey],
) -> Result<HashMap<KnitKey, KnitRecordDetails<Self::F>>, KnitError> {
let mut out = HashMap::new();
for key in keys {
if let Some(d) = self.entries.get(key) {
out.insert(key.clone(), d.clone());
}
}
Ok(out)
}
fn keys(&self) -> Result<Vec<KnitKey>, KnitError> {
Ok(self.entries.keys().cloned().collect())
}
fn get_parent_map(
&self,
keys: &[KnitKey],
) -> Result<HashMap<KnitKey, Vec<KnitKey>>, KnitError> {
let mut out = HashMap::new();
for key in keys {
if let Some(d) = self.entries.get(key) {
out.insert(key.clone(), d.parents.clone());
}
}
Ok(out)
}
fn get_method(&self, key: &KnitKey) -> Result<KnitMethod, KnitError> {
self.entries
.get(key)
.map(|d| d.method)
.ok_or_else(|| KnitError::RevisionNotPresent(key.clone()))
}
fn get_total_build_size(
&self,
keys: &[KnitKey],
positions: &HashMap<KnitKey, KnitRecordDetails<Self::F>>,
) -> usize {
keys.iter()
.filter_map(|k| positions.get(k))
.map(|d| d.index_memo.length)
.sum()
}
fn sort_keys_by_io(
&self,
keys: &mut [KnitKey],
positions: &HashMap<KnitKey, KnitRecordDetails<Self::F>>,
) {
keys.sort_by(|a, b| {
let ka = positions
.get(a)
.map(|d| (&d.index_memo.file_ref, d.index_memo.offset));
let kb = positions
.get(b)
.map(|d| (&d.index_memo.file_ref, d.index_memo.offset));
ka.cmp(&kb)
});
}
fn has_graph(&self) -> bool {
self.has_graph
}
fn contains(&self, key: &KnitKey) -> Result<bool, KnitError> {
Ok(self.entries.contains_key(key))
}
fn get_missing_compression_parents(&self) -> Result<Vec<KnitKey>, KnitError> {
Ok(Vec::new())
}
fn check_write_ok(&self) -> Result<(), KnitError> {
Err(KnitError::Corrupt("read-only index".to_string()))
}
fn add_records(
&self,
_records: &[(
KnitKey,
Vec<KnitMethod>,
KnitIndexMemo<Self::F>,
Vec<KnitKey>,
)],
_random_id: bool,
_missing_compression_parents: bool,
) -> Result<(), KnitError> {
Err(KnitError::Corrupt("read-only index".to_string()))
}
}
struct PackKnitAccess {
transport: SharedTransport,
cache: std::sync::Mutex<HashMap<PackName, std::sync::Arc<Vec<u8>>>>,
}
impl PackKnitAccess {
fn new(transport: SharedTransport) -> Self {
PackKnitAccess {
transport,
cache: std::sync::Mutex::new(HashMap::new()),
}
}
fn pack_bytes(&self, pack: &str) -> Result<std::sync::Arc<Vec<u8>>, KnitError> {
if let Some(b) = self.cache.lock().unwrap().get(pack) {
return Ok(b.clone());
}
let path = format!("packs/{pack}.pack");
let bytes = self
.transport
.get_bytes(&path)
.map_err(|e| KnitError::Corrupt(format!("reading {path}: {e}")))?;
let arc = std::sync::Arc::new(bytes);
self.cache
.lock()
.unwrap()
.insert(pack.to_string(), arc.clone());
Ok(arc)
}
fn record_body(&self, memo: &KnitIndexMemo<PackName>) -> Result<Vec<u8>, KnitError> {
let bytes = self.pack_bytes(&memo.file_ref)?;
let start = memo.offset as usize;
let stop = start + memo.length;
if stop > bytes.len() {
return Err(KnitError::Corrupt(format!(
"record range {start}..{stop} outside pack {} (len {})",
memo.file_ref,
bytes.len()
)));
}
crate::pack::read_bytes_record_body(&bytes[start..stop])
.map_err(|e| KnitError::Corrupt(format!("reading pack record: {e}")))
}
}
impl KnitAccess for PackKnitAccess {
type F = PackName;
fn get_raw_record(&self, memo: &KnitIndexMemo<Self::F>) -> Result<Vec<u8>, KnitError> {
self.record_body(memo)
}
fn get_raw_records(&self, memos: &[KnitIndexMemo<Self::F>]) -> Result<Vec<Vec<u8>>, KnitError> {
memos.iter().map(|m| self.record_body(m)).collect()
}
fn add_raw_record(
&self,
_key: &KnitKey,
_size: usize,
_chunks: Vec<Vec<u8>>,
) -> Result<KnitIndexMemo<Self::F>, KnitError> {
Err(KnitError::Corrupt("read-only access".to_string()))
}
fn flush(&self) -> Result<(), KnitError> {
Ok(())
}
fn reload_or_raise(&self, err: KnitError) -> Result<(), KnitError> {
Err(err)
}
}
type Store = KnitVersionedFiles<PackKnitIndex, PackKnitAccess, KnitPlainFactory>;
fn build_store(
transport: &SharedTransport,
packs: &[PackName],
kind: IndexKind,
) -> Result<Store, RepositoryError> {
let index = PackKnitIndex::load(transport.as_ref(), packs, kind)?;
let access = PackKnitAccess::new(transport.clone());
Ok(KnitVersionedFiles::new(
index,
access,
KnitPlainFactory,
200,
))
}
pub struct KnitPackRepository {
format: &'static RepositoryFormat,
transport: SharedTransport,
revisions: Store,
inventories: Store,
texts: Store,
signatures: Store,
write_group: Option<WriteGroup>,
}
impl KnitPackRepository {
pub fn open(transport: SharedTransport) -> Result<Self, RepositoryError> {
let format = check_format(transport.as_ref())?;
let packs = read_pack_names(transport.as_ref())?;
Ok(KnitPackRepository {
format,
revisions: build_store(&transport, &packs, IndexKind::Revision)?,
inventories: build_store(&transport, &packs, IndexKind::Inventory)?,
texts: build_store(&transport, &packs, IndexKind::Text)?,
signatures: build_store(&transport, &packs, IndexKind::Signature)?,
transport,
write_group: None,
})
}
pub fn create(
transport: SharedTransport,
format: &'static RepositoryFormat,
) -> Result<Self, RepositoryError> {
if !std::ptr::fn_addr_eq(format.open, open_knit_pack as super::format::OpenFn) {
return Err(RepositoryError::UnsupportedFormat(
format.get_format_description(),
));
}
transport.mkdir("")?;
transport.mkdir("indices")?;
transport.mkdir("packs")?;
transport.put_bytes("format", format.format_string(), None)?;
let empty = super::pack_index::IndexBuilder::new(format.uses_btree_index, 0, 1)
.finish()
.map_err(|e| RepositoryError::Corrupt(format!("empty pack-names: {e}")))?;
transport.put_bytes("pack-names", &empty, None)?;
Self::open(transport)
}
pub fn start_write_group(&mut self) -> Result<(), RepositoryError> {
if self.write_group.is_some() {
return Err(RepositoryError::Corrupt(
"a write group is already open".to_string(),
));
}
self.write_group = Some(WriteGroup::new(
&new_pack_name(),
self.format.uses_btree_index,
)?);
Ok(())
}
fn group(&self) -> Result<&WriteGroup, RepositoryError> {
self.write_group
.as_ref()
.ok_or_else(|| RepositoryError::Corrupt("no write group is open".to_string()))
}
pub fn add_revision(
&mut self,
revision: &crate::revision::Revision,
parents: &[Vec<u8>],
) -> Result<(), RepositoryError> {
use crate::serializer::RevisionSerializer;
let bytes = crate::xml_serializer::XMLRevisionSerializer5
.write_revision_to_string(revision)
.map_err(|e| RepositoryError::Corrupt(format!("write revision: {e:?}")))?;
let key: KnitKey = vec![revision.revision_id.as_bytes().to_vec()];
let parent_keys: Vec<KnitKey> = parents.iter().map(|p| vec![p.clone()]).collect();
self.group()?
.revisions
.add_lines(key, parent_keys, split_lines(&bytes), false)
.map_err(|e| RepositoryError::Corrupt(format!("add revision: {e}")))?;
Ok(())
}
pub fn add_inventory_xml(
&mut self,
revision_id: &[u8],
parents: &[Vec<u8>],
xml: &[u8],
) -> Result<(), RepositoryError> {
let key: KnitKey = vec![revision_id.to_vec()];
let parent_keys: Vec<KnitKey> = parents.iter().map(|p| vec![p.clone()]).collect();
self.group()?
.inventories
.add_lines(key, parent_keys, split_lines(xml), false)
.map_err(|e| RepositoryError::Corrupt(format!("add inventory: {e}")))?;
Ok(())
}
pub fn add_text(
&mut self,
file_id: &[u8],
revision: &[u8],
parents: &[(Vec<u8>, Vec<u8>)],
bytes: &[u8],
) -> Result<(), RepositoryError> {
let key: KnitKey = vec![file_id.to_vec(), revision.to_vec()];
let parent_keys: Vec<KnitKey> = parents
.iter()
.map(|(f, r)| vec![f.clone(), r.clone()])
.collect();
self.group()?
.texts
.add_lines(key, parent_keys, split_lines(bytes), false)
.map_err(|e| RepositoryError::Corrupt(format!("add text: {e}")))?;
Ok(())
}
pub fn add_signature(
&mut self,
revision_id: &[u8],
signature: &[u8],
) -> Result<(), RepositoryError> {
let key: KnitKey = vec![revision_id.to_vec()];
self.group()?
.signatures
.add_lines(key, Vec::new(), split_lines(signature), false)
.map_err(|e| RepositoryError::Corrupt(format!("add signature: {e}")))?;
Ok(())
}
pub fn get_signature_text(
&self,
revision_id: &[u8],
) -> Result<Option<Vec<u8>>, RepositoryError> {
let key: KnitKey = vec![revision_id.to_vec()];
match self.signatures.get_text(&key) {
Ok(bytes) => Ok(Some(bytes)),
Err(crate::knit::KnitError::RevisionNotPresent(_)) => Ok(None),
Err(e) => Err(RepositoryError::Corrupt(format!("signature {e}"))),
}
}
pub fn commit_write_group(&mut self) -> Result<(), RepositoryError> {
let group = self
.write_group
.take()
.ok_or_else(|| RepositoryError::Corrupt("no write group is open".to_string()))?;
let existing = read_pack_names_with_values(self.transport.as_ref())?;
group.finish(self.transport.as_ref(), &existing)?;
self.autopack()?;
Ok(())
}
pub fn stream_fetch_from(
&mut self,
source: &KnitPackRepository,
missing: &[Vec<u8>],
) -> Result<(), RepositoryError> {
if self.write_group.is_some() {
return Err(RepositoryError::Corrupt(
"cannot fetch with an open write group".to_string(),
));
}
if missing.is_empty() {
return Ok(());
}
let rev_keys: Vec<KnitKey> = missing.iter().map(|r| vec![r.clone()]).collect();
let mut text_keys: Vec<KnitKey> = Vec::new();
for rev in missing {
let inv = source.get_inventory(rev)?;
for (_, entry) in inv.entries() {
if entry.revision().map(|r| r.as_bytes()) == Some(rev.as_slice()) {
text_keys.push(vec![entry.file_id().as_bytes().to_vec(), rev.clone()]);
}
}
}
self.start_write_group()?;
let group = self.write_group.as_ref().expect("just opened");
group.copy_store_keys(&source.revisions, RepackTarget::Revisions, &rev_keys)?;
group.copy_store_keys(&source.inventories, RepackTarget::Inventories, &rev_keys)?;
group.copy_store_keys(&source.signatures, RepackTarget::Signatures, &rev_keys)?;
group.copy_store_keys(&source.texts, RepackTarget::Texts, &text_keys)?;
self.commit_write_group()?;
Ok(())
}
pub fn reconcile(&mut self) -> Result<super::ReconcileResult, RepositoryError> {
if self.write_group.is_some() {
return Err(RepositoryError::Corrupt(
"cannot reconcile with an open write group".to_string(),
));
}
let old_packs = read_pack_names(self.transport.as_ref())?;
let reachable = self.all_revision_ids()?;
let stored_inventories = self.inventories.keys()?.len();
let garbage_inventories = stored_inventories.saturating_sub(reachable.len());
if old_packs.is_empty() || reachable.is_empty() {
if !old_packs.is_empty() {
self.write_empty_pack_names()?;
self.obsolete_packs(&old_packs)?;
}
return Ok(super::ReconcileResult {
garbage_inventories,
repacked: !old_packs.is_empty(),
});
}
let rev_keys: Vec<KnitKey> = reachable.iter().map(|r| vec![r.clone()]).collect();
let mut text_keys: Vec<KnitKey> = Vec::new();
for rev in &reachable {
let inv = self.get_inventory(rev)?;
for (_, entry) in inv.entries() {
if entry.revision().map(|r| r.as_bytes()) == Some(rev.as_slice()) {
text_keys.push(vec![entry.file_id().as_bytes().to_vec(), rev.clone()]);
}
}
}
let group = WriteGroup::new(&new_pack_name(), self.format.uses_btree_index)?;
group.copy_store_keys(&self.revisions, RepackTarget::Revisions, &rev_keys)?;
group.copy_store_keys(&self.inventories, RepackTarget::Inventories, &rev_keys)?;
group.copy_store_keys(&self.signatures, RepackTarget::Signatures, &rev_keys)?;
group.copy_store_keys(&self.texts, RepackTarget::Texts, &text_keys)?;
group.finish(self.transport.as_ref(), &[])?;
self.obsolete_packs(&old_packs)?;
Ok(super::ReconcileResult {
garbage_inventories,
repacked: true,
})
}
fn write_empty_pack_names(&self) -> Result<(), RepositoryError> {
let names = super::pack_index::IndexBuilder::new(self.format.uses_btree_index, 0, 1);
let bytes = names
.finish()
.map_err(|e| RepositoryError::Corrupt(format!("empty pack-names: {e}")))?;
self.transport.put_bytes("pack-names", &bytes, None)?;
Ok(())
}
pub fn pack(&mut self) -> Result<(), RepositoryError> {
if self.write_group.is_some() {
return Err(RepositoryError::Corrupt(
"cannot pack with an open write group".to_string(),
));
}
let old_packs = read_pack_names(self.transport.as_ref())?;
if old_packs.len() <= 1 {
return Ok(());
}
self.repack(&old_packs, &[])
}
pub fn autopack(&mut self) -> Result<bool, RepositoryError> {
if self.write_group.is_some() {
return Err(RepositoryError::Corrupt(
"cannot autopack with an open write group".to_string(),
));
}
let all_packs = read_pack_names(self.transport.as_ref())?;
if all_packs.len() <= 1 {
return Ok(false);
}
let mut counts = Vec::with_capacity(all_packs.len());
for name in &all_packs {
let ext = index_extension(IndexKind::Revision);
let index = super::pack_index::PackIndex::open(
self.transport.as_ref(),
&format!("indices/{name}{ext}"),
)?;
counts.push(index.iter_all_entries().count() as u64);
}
let selected = super::pack_collection::plan_autopack_combinations(&counts);
if selected.is_empty() {
return Ok(false);
}
let to_combine: Vec<PackName> = selected.iter().map(|&i| all_packs[i].clone()).collect();
let survivors: Vec<(PackName, Vec<u8>)> = {
let with_values = read_pack_names_with_values(self.transport.as_ref())?;
let combine: std::collections::HashSet<&PackName> = to_combine.iter().collect();
with_values
.into_iter()
.filter(|(n, _)| !combine.contains(n))
.collect()
};
self.repack(&to_combine, &survivors)?;
Ok(true)
}
fn repack(
&mut self,
to_combine: &[PackName],
survivors: &[(PackName, Vec<u8>)],
) -> Result<(), RepositoryError> {
let revisions = build_store(&self.transport, to_combine, IndexKind::Revision)?;
let inventories = build_store(&self.transport, to_combine, IndexKind::Inventory)?;
let texts = build_store(&self.transport, to_combine, IndexKind::Text)?;
let signatures = build_store(&self.transport, to_combine, IndexKind::Signature)?;
let group = WriteGroup::new(&new_pack_name(), self.format.uses_btree_index)?;
group.copy_store(&revisions, RepackTarget::Revisions)?;
group.copy_store(&inventories, RepackTarget::Inventories)?;
group.copy_store(&texts, RepackTarget::Texts)?;
group.copy_store(&signatures, RepackTarget::Signatures)?;
group.finish(self.transport.as_ref(), survivors)?;
self.obsolete_packs(to_combine)?;
Ok(())
}
fn obsolete_packs(&self, packs: &[PackName]) -> Result<(), RepositoryError> {
let _ = self.transport.mkdir("obsolete_packs");
for name in packs {
self.move_to_obsolete(&format!("packs/{name}.pack"), &format!("{name}.pack"))?;
for kind in [
IndexKind::Revision,
IndexKind::Inventory,
IndexKind::Text,
IndexKind::Signature,
] {
let ext = index_extension(kind);
self.move_to_obsolete(&format!("indices/{name}{ext}"), &format!("{name}{ext}"))?;
}
}
Ok(())
}
fn move_to_obsolete(&self, from: &str, basename: &str) -> Result<(), RepositoryError> {
match self
.transport
.rename(from, &format!("obsolete_packs/{basename}"))
{
Ok(()) | Err(TransportError::NoSuchFile(_)) => Ok(()),
Err(e) => Err(e.into()),
}
}
pub fn all_revision_ids(&self) -> Result<Vec<Vec<u8>>, RepositoryError> {
let mut ids: Vec<Vec<u8>> = self
.revisions
.keys()?
.into_iter()
.filter_map(|k| k.into_iter().next())
.collect();
ids.sort();
Ok(ids)
}
pub fn get_parent_map(
&self,
revision_ids: &[Vec<u8>],
) -> Result<std::collections::HashMap<Vec<u8>, Vec<Vec<u8>>>, RepositoryError> {
let keys: Vec<KnitKey> = revision_ids.iter().map(|r| vec![r.clone()]).collect();
let raw = self
.revisions
.get_parent_map(&keys)
.map_err(RepositoryError::Knit)?;
Ok(unkey_knit_parent_map(raw))
}
pub fn get_revision(
&self,
revision_id: &[u8],
) -> Result<crate::revision::Revision, RepositoryError> {
use crate::serializer::RevisionSerializer;
let key: KnitKey = vec![revision_id.to_vec()];
let bytes = self.revisions.get_text(&key).map_err(|e| match e {
crate::knit::KnitError::RevisionNotPresent(_) => {
RepositoryError::NoSuchRevision(revision_id.to_vec())
}
other => RepositoryError::Corrupt(format!("revision {other}")),
})?;
crate::xml_serializer::XMLRevisionSerializer5
.read_revision_from_string(&bytes)
.map_err(|e| RepositoryError::Corrupt(format!("revision parse: {e:?}")))
}
pub fn get_file_text(
&self,
file_id: &[u8],
revision: &[u8],
) -> Result<Vec<u8>, RepositoryError> {
let key: KnitKey = vec![file_id.to_vec(), revision.to_vec()];
self.texts
.get_text(&key)
.map_err(|e| RepositoryError::Corrupt(format!("text {e}")))
}
pub fn get_inventory_xml(&self, revision_id: &[u8]) -> Result<Vec<u8>, RepositoryError> {
let key: KnitKey = vec![revision_id.to_vec()];
self.inventories
.get_text(&key)
.map_err(|e| RepositoryError::Corrupt(format!("inventory {e}")))
}
pub fn format(&self) -> &'static RepositoryFormat {
self.format
}
fn inventory_serializer(&self) -> &'static dyn crate::serializer::InventorySerializer {
self.format.inventory_serializer
}
pub fn get_inventory(
&self,
revision_id: &[u8],
) -> Result<crate::inventory::MutableInventory, RepositoryError> {
let xml = self.get_inventory_xml(revision_id)?;
let lines: Vec<Vec<u8>> = split_lines(&xml);
let line_refs: Vec<&[u8]> = lines.iter().map(|l| l.as_slice()).collect();
self.inventory_serializer()
.read_inventory_from_lines(&line_refs, Some(crate::RevisionId::from(revision_id)))
.map_err(|e| RepositoryError::Corrupt(format!("inventory parse: {e:?}")))
}
pub fn add_inventory_from_entries(
&mut self,
revision_id: &[u8],
parents: &[Vec<u8>],
_root_id: &[u8],
entries: &[crate::inventory::Entry],
) -> Result<Vec<u8>, RepositoryError> {
let mut inv = crate::inventory::MutableInventory::new();
inv.revision_id = Some(crate::RevisionId::from(revision_id));
for entry in entries {
inv.add(entry.clone())
.map_err(|e| RepositoryError::Corrupt(format!("build inventory: {e:?}")))?;
}
let lines = self
.inventory_serializer()
.write_inventory_to_lines(&inv, false)
.map_err(|e| RepositoryError::Corrupt(format!("serialise inventory: {e:?}")))?;
let line_refs: Vec<&[u8]> = lines.iter().map(|l| l.as_slice()).collect();
let sha1 = crate::weave::sha_strings(&line_refs);
let xml: Vec<u8> = lines.concat();
self.add_inventory_xml(revision_id, parents, &xml)?;
Ok(sha1)
}
pub fn add_inventory_by_delta(
&mut self,
basis_revision_id: &[u8],
delta: &crate::inventory_delta::InventoryDelta,
new_revision_id: &[u8],
parents: &[Vec<u8>],
) -> Result<Vec<u8>, RepositoryError> {
let basis = if basis_revision_id == crate::branch::NULL_REVISION {
crate::inventory::MutableInventory::new()
} else {
self.get_inventory(basis_revision_id)?
};
let new_inv = basis
.create_by_apply_delta(delta, crate::RevisionId::from(new_revision_id))
.map_err(|e| RepositoryError::Corrupt(format!("apply inventory delta: {e:?}")))?;
let lines = self
.inventory_serializer()
.write_inventory_to_lines(&new_inv, false)
.map_err(|e| RepositoryError::Corrupt(format!("serialise inventory: {e:?}")))?;
let line_refs: Vec<&[u8]> = lines.iter().map(|l| l.as_slice()).collect();
let sha1 = crate::weave::sha_strings(&line_refs);
let xml: Vec<u8> = lines.concat();
self.add_inventory_xml(new_revision_id, parents, &xml)?;
Ok(sha1)
}
}
impl super::Repository for KnitPackRepository {
fn format(&self) -> &'static RepositoryFormat {
KnitPackRepository::format(self)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn try_fetch_from(
&mut self,
source: &dyn super::Repository,
revision_ids: &[Vec<u8>],
) -> Result<bool, RepositoryError> {
match source.as_any().downcast_ref::<KnitPackRepository>() {
Some(src)
if src.format().inventory_serializer.format_num()
== self.format().inventory_serializer.format_num()
&& src.format().rich_root_data == self.format().rich_root_data =>
{
self.stream_fetch_from(src, revision_ids)?;
Ok(true)
}
_ => Ok(false),
}
}
fn all_revision_ids(&self) -> Result<Vec<Vec<u8>>, RepositoryError> {
KnitPackRepository::all_revision_ids(self)
}
fn get_parent_map(
&self,
revision_ids: &[Vec<u8>],
) -> Result<std::collections::HashMap<Vec<u8>, Vec<Vec<u8>>>, RepositoryError> {
KnitPackRepository::get_parent_map(self, revision_ids)
}
fn get_revision(
&self,
revision_id: &[u8],
) -> Result<crate::revision::Revision, RepositoryError> {
KnitPackRepository::get_revision(self, revision_id)
}
fn get_inventory(
&self,
revision_id: &[u8],
) -> Result<Box<dyn crate::inventory::Inventory>, RepositoryError> {
Ok(Box::new(KnitPackRepository::get_inventory(
self,
revision_id,
)?))
}
fn get_file_text(&self, file_id: &[u8], revision: &[u8]) -> Result<Vec<u8>, RepositoryError> {
KnitPackRepository::get_file_text(self, file_id, revision)
}
fn start_write_group(&mut self) -> Result<(), RepositoryError> {
KnitPackRepository::start_write_group(self)
}
fn add_revision(
&mut self,
revision: &crate::revision::Revision,
parents: &[Vec<u8>],
) -> Result<(), RepositoryError> {
KnitPackRepository::add_revision(self, revision, parents)
}
fn add_inventory_from_entries(
&mut self,
revision_id: &[u8],
parents: &[Vec<u8>],
root_id: &[u8],
entries: &[crate::inventory::Entry],
) -> Result<Vec<u8>, RepositoryError> {
KnitPackRepository::add_inventory_from_entries(self, revision_id, parents, root_id, entries)
}
fn add_inventory_by_delta(
&mut self,
basis_revision_id: &[u8],
delta: &crate::inventory_delta::InventoryDelta,
new_revision_id: &[u8],
parents: &[Vec<u8>],
) -> Result<Vec<u8>, RepositoryError> {
KnitPackRepository::add_inventory_by_delta(
self,
basis_revision_id,
delta,
new_revision_id,
parents,
)
}
fn add_text(
&mut self,
file_id: &[u8],
revision: &[u8],
parents: &[(Vec<u8>, Vec<u8>)],
bytes: &[u8],
) -> Result<(), RepositoryError> {
KnitPackRepository::add_text(self, file_id, revision, parents, bytes)
}
fn add_signature_text(
&mut self,
revision_id: &[u8],
signature: &[u8],
) -> Result<(), RepositoryError> {
KnitPackRepository::add_signature(self, revision_id, signature)
}
fn get_signature_text(&self, revision_id: &[u8]) -> Result<Option<Vec<u8>>, RepositoryError> {
KnitPackRepository::get_signature_text(self, revision_id)
}
fn commit_write_group(&mut self) -> Result<(), RepositoryError> {
KnitPackRepository::commit_write_group(self)
}
fn pack(&mut self) -> Result<(), RepositoryError> {
KnitPackRepository::pack(self)
}
fn autopack(&mut self) -> Result<bool, RepositoryError> {
KnitPackRepository::autopack(self)
}
fn reconcile(&mut self) -> Result<super::ReconcileResult, RepositoryError> {
KnitPackRepository::reconcile(self)
}
}
pub fn open_knit_pack(
transport: SharedTransport,
) -> Result<Box<dyn super::Repository>, RepositoryError> {
Ok(Box::new(KnitPackRepository::open(transport)?))
}
pub fn create_knit_pack(
format: &'static RepositoryFormat,
transport: SharedTransport,
) -> Result<Box<dyn super::Repository>, RepositoryError> {
Ok(Box::new(KnitPackRepository::create(transport, format)?))
}
fn check_format(transport: &dyn Transport) -> Result<&'static RepositoryFormat, RepositoryError> {
let marker = transport.get_bytes("format")?;
let format = super::format::find_format(&marker)
.ok_or_else(|| RepositoryError::UnknownFormat(marker.clone()))?;
if !std::ptr::fn_addr_eq(format.open, open_knit_pack as super::format::OpenFn) {
return Err(RepositoryError::UnsupportedFormat(
format.get_format_description(),
));
}
Ok(format)
}
fn new_pack_name() -> String {
crate::osutils::rand_chars(32)
.chars()
.map(|ch| char::from_digit((ch as u32) % 16, 16).unwrap())
.collect()
}
fn read_pack_names_with_values(
transport: &dyn Transport,
) -> Result<Vec<(String, Vec<u8>)>, RepositoryError> {
let index = super::pack_index::PackIndex::open(transport, "pack-names")?;
let mut out = Vec::new();
for (key, value, _refs) in index.iter_all_entries() {
if let Some(name) = key.first() {
out.push((String::from_utf8_lossy(name).into_owned(), value.clone()));
}
}
Ok(out)
}
fn read_pack_names(transport: &dyn Transport) -> Result<Vec<PackName>, RepositoryError> {
let index = super::pack_index::PackIndex::open(transport, "pack-names")?;
let mut names = Vec::new();
for (key, _value, _refs) in index.iter_all_entries() {
if let Some(name) = key.first() {
names.push(String::from_utf8_lossy(name).into_owned());
}
}
Ok(names)
}
fn split_lines(bytes: &[u8]) -> Vec<Vec<u8>> {
let mut lines = Vec::new();
let mut start = 0;
for (i, b) in bytes.iter().enumerate() {
if *b == b'\n' {
lines.push(bytes[start..=i].to_vec());
start = i + 1;
}
}
if start < bytes.len() {
lines.push(bytes[start..].to_vec());
}
lines
}
use std::sync::{Arc, Mutex};
use crate::knit::{encode_graph_index_record, KnitMethod as KM};
use crate::pack::ContainerWriter;
type KnitWriteRecord = (
KnitKey,
Vec<KnitMethod>,
KnitIndexMemo<PackName>,
Vec<KnitKey>,
);
struct KnitWriteIndex {
has_deltas: bool,
records: Mutex<Vec<KnitWriteRecord>>,
}
impl KnitWriteIndex {
fn new(has_deltas: bool) -> Self {
KnitWriteIndex {
has_deltas,
records: Mutex::new(Vec::new()),
}
}
fn take_records(&self) -> Vec<KnitWriteRecord> {
std::mem::take(&mut self.records.lock().unwrap())
}
}
impl KnitIndex for KnitWriteIndex {
type F = PackName;
fn get_build_details(
&self,
_keys: &[KnitKey],
) -> Result<HashMap<KnitKey, KnitRecordDetails<Self::F>>, KnitError> {
Ok(HashMap::new())
}
fn keys(&self) -> Result<Vec<KnitKey>, KnitError> {
Ok(self
.records
.lock()
.unwrap()
.iter()
.map(|(k, _, _, _)| k.clone())
.collect())
}
fn get_parent_map(
&self,
_keys: &[KnitKey],
) -> Result<HashMap<KnitKey, Vec<KnitKey>>, KnitError> {
Ok(HashMap::new())
}
fn get_method(&self, key: &KnitKey) -> Result<KnitMethod, KnitError> {
Err(KnitError::RevisionNotPresent(key.clone()))
}
fn get_total_build_size(
&self,
_keys: &[KnitKey],
_positions: &HashMap<KnitKey, KnitRecordDetails<Self::F>>,
) -> usize {
0
}
fn sort_keys_by_io(
&self,
_keys: &mut [KnitKey],
_positions: &HashMap<KnitKey, KnitRecordDetails<Self::F>>,
) {
}
fn has_graph(&self) -> bool {
true
}
fn contains(&self, _key: &KnitKey) -> Result<bool, KnitError> {
Ok(false)
}
fn get_missing_compression_parents(&self) -> Result<Vec<KnitKey>, KnitError> {
Ok(Vec::new())
}
fn check_write_ok(&self) -> Result<(), KnitError> {
Ok(())
}
fn add_records(
&self,
records: &[KnitWriteRecord],
_random_id: bool,
_missing_compression_parents: bool,
) -> Result<(), KnitError> {
self.records.lock().unwrap().extend_from_slice(records);
Ok(())
}
}
#[derive(Clone)]
struct KnitWriteAccess {
pack_name: PackName,
pack: Arc<Mutex<ContainerWriter<Vec<u8>>>>,
}
impl KnitAccess for KnitWriteAccess {
type F = PackName;
fn get_raw_record(&self, _memo: &KnitIndexMemo<Self::F>) -> Result<Vec<u8>, KnitError> {
Err(KnitError::Corrupt("write-only access".to_string()))
}
fn get_raw_records(
&self,
_memos: &[KnitIndexMemo<Self::F>],
) -> Result<Vec<Vec<u8>>, KnitError> {
Err(KnitError::Corrupt("write-only access".to_string()))
}
fn add_raw_record(
&self,
_key: &KnitKey,
size: usize,
data: Vec<Vec<u8>>,
) -> Result<KnitIndexMemo<Self::F>, KnitError> {
let refs: Vec<&[u8]> = data.iter().map(|c| c.as_slice()).collect();
let mut pack = self.pack.lock().unwrap();
let (start, length) = pack
.add_bytes_record(&refs, size, &[])
.map_err(|e| KnitError::Corrupt(format!("writing pack record: {e}")))?;
Ok(KnitIndexMemo {
file_ref: self.pack_name.clone(),
offset: start,
length: length as usize,
})
}
fn flush(&self) -> Result<(), KnitError> {
Ok(())
}
fn reload_or_raise(&self, err: KnitError) -> Result<(), KnitError> {
Err(err)
}
}
type WriteStore = KnitVersionedFiles<KnitWriteIndex, KnitWriteAccess, KnitPlainFactory>;
struct WriteGroup {
pack: Arc<Mutex<ContainerWriter<Vec<u8>>>>,
revisions: WriteStore,
inventories: WriteStore,
signatures: WriteStore,
texts: WriteStore,
uses_btree: bool,
}
impl WriteGroup {
fn new(pack_name: &str, uses_btree: bool) -> Result<Self, RepositoryError> {
let mut writer = ContainerWriter::new(Vec::new());
writer
.begin()
.map_err(|e| RepositoryError::Corrupt(format!("pack begin: {e}")))?;
let pack = Arc::new(Mutex::new(writer));
let make = |has_deltas: bool| -> WriteStore {
let access = KnitWriteAccess {
pack_name: pack_name.to_string(),
pack: pack.clone(),
};
KnitVersionedFiles::new(KnitWriteIndex::new(has_deltas), access, KnitPlainFactory, 0)
};
let revisions = make(false);
let inventories = make(true);
let signatures = make(false);
let texts = make(true);
Ok(WriteGroup {
pack,
revisions,
inventories,
signatures,
texts,
uses_btree,
})
}
fn copy_store(&self, source: &Store, target: RepackTarget) -> Result<(), RepositoryError> {
let mut keys = source.keys()?;
keys.sort();
self.copy_store_keys(source, target, &keys)
}
fn copy_store_keys(
&self,
source: &Store,
target: RepackTarget,
keys: &[KnitKey],
) -> Result<(), RepositoryError> {
use crate::versionedfile::ContentFactory;
let store = match target {
RepackTarget::Revisions => &self.revisions,
RepackTarget::Inventories => &self.inventories,
RepackTarget::Texts => &self.texts,
RepackTarget::Signatures => &self.signatures,
};
for record in source.get_record_stream(keys, "unordered", true)? {
if record.storage_kind() == "absent" {
continue;
}
let key = record.key.clone();
let parents = record.parents.clone().unwrap_or_default();
let lines: Vec<Vec<u8>> = record.to_lines().map(|l| l.into_owned()).collect();
store.add_lines(key, parents, lines, true)?;
}
Ok(())
}
fn finish(
self,
transport: &dyn Transport,
existing: &[(String, Vec<u8>)],
) -> Result<String, RepositoryError> {
let WriteGroup {
pack,
revisions,
inventories,
signatures,
texts,
uses_btree,
} = self;
let rix = serialise_index(revisions.index, 1, uses_btree)?;
let iix = serialise_index(inventories.index, 1, uses_btree)?;
let tix = serialise_index(texts.index, 2, uses_btree)?;
let six = serialise_index(signatures.index, 1, uses_btree)?;
let pack_bytes = {
let mut writer = pack.lock().unwrap();
writer
.end()
.map_err(|e| RepositoryError::Corrupt(format!("pack end: {e}")))?;
std::mem::take(writer.get_mut())
};
let pack_name = md5_hex(&pack_bytes);
transport.put_bytes(&format!("packs/{pack_name}.pack"), &pack_bytes, None)?;
let write_index = |ext: &str, bytes: &[u8]| -> Result<usize, RepositoryError> {
transport.put_bytes(&format!("indices/{pack_name}{ext}"), bytes, None)?;
Ok(bytes.len())
};
let sizes = [
write_index(index_extension(IndexKind::Revision), &rix)?,
write_index(index_extension(IndexKind::Inventory), &iix)?,
write_index(index_extension(IndexKind::Text), &tix)?,
write_index(index_extension(IndexKind::Signature), &six)?,
];
let new_value = sizes
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(" ")
.into_bytes();
let mut names = super::pack_index::IndexBuilder::new(uses_btree, 0, 1);
for (name, value) in existing {
names
.add_node(vec![name.clone().into_bytes()], value.clone(), vec![])
.map_err(|e| RepositoryError::Corrupt(format!("pack-names node: {e}")))?;
}
names
.add_node(vec![pack_name.clone().into_bytes()], new_value, vec![])
.map_err(|e| RepositoryError::Corrupt(format!("pack-names node: {e}")))?;
let names_bytes = names
.finish()
.map_err(|e| RepositoryError::Corrupt(format!("pack-names finish: {e}")))?;
transport.put_bytes("pack-names", &names_bytes, None)?;
Ok(pack_name)
}
}
fn md5_hex(bytes: &[u8]) -> String {
use md5::{Digest, Md5};
Md5::digest(bytes)
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
fn serialise_index(
index: KnitWriteIndex,
key_elements: usize,
uses_btree: bool,
) -> Result<Vec<u8>, RepositoryError> {
let has_deltas = index.has_deltas;
let ref_lists = if has_deltas { 2 } else { 1 };
let mut builder = super::pack_index::IndexBuilder::new(uses_btree, ref_lists, key_elements);
for (key, options, memo, parents) in index.take_records() {
let noeol = options.contains(&KM::NoEol);
let method = if options.contains(&KM::LineDelta) {
KM::LineDelta
} else {
KM::Fulltext
};
let (value, node_refs) = encode_graph_index_record(
noeol,
memo.offset,
memo.length as u64,
method,
true,
has_deltas,
&parents,
)
.map_err(|e| RepositoryError::Corrupt(format!("encode index: {e}")))?;
builder
.add_node(key, value, node_refs)
.map_err(|e| RepositoryError::Corrupt(format!("index node: {e}")))?;
}
builder
.finish()
.map_err(|e| RepositoryError::Corrupt(format!("index finish: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::LocalTransport;
use std::sync::Arc;
fn temp() -> (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)))
}
#[test]
fn create_rejects_non_knitpack_format() {
let (_d, t) = temp();
let fmt = super::super::format::find_format(
b"Bazaar repository format 2a (needs bzr 1.16 or later)\n",
)
.unwrap();
assert!(KnitPackRepository::create(t, fmt).is_err());
}
fn knitpack6() -> &'static RepositoryFormat {
super::super::format::find_format(b"Bazaar RepositoryFormatKnitPack6 (bzr 1.9)\n").unwrap()
}
fn make_revision(id: &[u8]) -> crate::revision::Revision {
crate::revision::Revision::new(
crate::RevisionId::from(id),
vec![],
Some("T <t@e>".to_string()),
"m".to_string(),
std::collections::HashMap::new(),
None,
1577880000.0,
Some(0),
)
}
fn commit_one(repo: &mut KnitPackRepository, rev: &[u8]) {
repo.start_write_group().unwrap();
let root = crate::inventory::ROOT_ID;
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(&[b"hello\n"])),
Some(6),
Some(false),
None,
),
];
let inv_sha = repo
.add_inventory_from_entries(rev, &[], root, &entries)
.unwrap();
let mut r = make_revision(rev);
r.inventory_sha1 = Some(inv_sha);
repo.add_revision(&r, &[]).unwrap();
repo.add_text(b"file-1", rev, &[], b"hello\n").unwrap();
repo.commit_write_group().unwrap();
}
#[test]
fn pack_combines_packs() {
let (_d, t) = temp();
let mut repo = KnitPackRepository::create(t.clone(), knitpack6()).unwrap();
commit_one(&mut repo, b"rev-1");
commit_one(&mut repo, b"rev-2");
commit_one(&mut repo, b"rev-3");
let before = read_pack_names(t.as_ref()).unwrap();
assert_eq!(before.len(), 3);
repo.pack().unwrap();
let after = read_pack_names(t.as_ref()).unwrap();
assert_eq!(after.len(), 1);
assert!(!before.contains(&after[0]));
for name in &before {
assert!(t.has(&format!("obsolete_packs/{name}.pack")).unwrap());
assert!(!t.has(&format!("packs/{name}.pack")).unwrap());
}
let repo = KnitPackRepository::open(t).unwrap();
let mut ids = repo.all_revision_ids().unwrap();
ids.sort();
assert_eq!(
ids,
vec![b"rev-1".to_vec(), b"rev-2".to_vec(), b"rev-3".to_vec()]
);
for rev in [&b"rev-1"[..], b"rev-2", b"rev-3"] {
assert_eq!(repo.get_revision(rev).unwrap().message, "m");
assert_eq!(repo.get_file_text(b"file-1", rev).unwrap(), b"hello\n");
}
}
#[test]
fn pack_name_is_content_md5() {
let (_d, t) = temp();
let mut repo = KnitPackRepository::create(t.clone(), knitpack6()).unwrap();
commit_one(&mut repo, b"rev-1");
let names = read_pack_names(t.as_ref()).unwrap();
assert_eq!(names.len(), 1);
let pack_bytes = t.get_bytes(&format!("packs/{}.pack", names[0])).unwrap();
assert_eq!(names[0], md5_hex(&pack_bytes));
}
#[test]
fn autopack_bounds_pack_count_on_commit() {
let (_d, t) = temp();
let mut repo = KnitPackRepository::create(t.clone(), knitpack6()).unwrap();
for i in 0..12u32 {
let rev = format!("rev-{i}");
commit_one(&mut repo, rev.as_bytes());
}
let names = read_pack_names(t.as_ref()).unwrap();
assert!(
names.len() < 12,
"autopack should consolidate, got {}",
names.len()
);
let repo = KnitPackRepository::open(t).unwrap();
assert_eq!(repo.all_revision_ids().unwrap().len(), 12);
for i in 0..12u32 {
let rev = format!("rev-{i}");
assert_eq!(
repo.get_file_text(b"file-1", rev.as_bytes()).unwrap(),
b"hello\n"
);
}
}
#[test]
fn reconcile_drops_garbage_inventory() {
let (_d, t) = temp();
let mut repo = KnitPackRepository::create(t.clone(), knitpack6()).unwrap();
commit_one(&mut repo, b"rev-good");
let root = crate::inventory::ROOT_ID;
repo.start_write_group().unwrap();
repo.add_inventory_from_entries(
b"rev-garbage",
&[],
root,
&[crate::inventory::Entry::root(
crate::FileId::from(root),
Some(crate::RevisionId::from(&b"rev-garbage"[..])),
)],
)
.unwrap();
repo.commit_write_group().unwrap();
let mut repo = KnitPackRepository::open(t.clone()).unwrap();
let result = repo.reconcile().unwrap();
assert_eq!(result.garbage_inventories, 1);
assert!(result.repacked);
let repo = KnitPackRepository::open(t).unwrap();
assert_eq!(repo.all_revision_ids().unwrap(), vec![b"rev-good".to_vec()]);
assert_eq!(
repo.get_file_text(b"file-1", b"rev-good").unwrap(),
b"hello\n"
);
}
}