pub const WEAVE_V5_FORMAT: &[u8] = b"# bzr weave file v5\n";
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct WeaveFile {
pub parents: Vec<Vec<usize>>,
pub sha1s: Vec<Vec<u8>>,
pub names: Vec<Vec<u8>>,
pub weave: Vec<WeaveEntry>,
}
pub fn sha_strings<L: AsRef<[u8]>>(lines: &[L]) -> Vec<u8> {
use sha1::{Digest, Sha1};
let mut hasher = Sha1::new();
for line in lines {
hasher.update(line.as_ref());
}
let digest = hasher.finalize();
let mut hex = vec![0u8; digest.len() * 2];
for (i, byte) in digest.iter().enumerate() {
let high = byte >> 4;
let low = byte & 0x0f;
hex[i * 2] = if high < 10 {
b'0' + high
} else {
b'a' + high - 10
};
hex[i * 2 + 1] = if low < 10 {
b'0' + low
} else {
b'a' + low - 10
};
}
hex
}
impl WeaveFile {
pub fn lookup(&self, name: &[u8]) -> Option<usize> {
self.names.iter().position(|n| n == name)
}
pub fn num_versions(&self) -> usize {
self.parents.len()
}
pub fn versions(&self) -> Vec<Vec<u8>> {
self.names.clone()
}
pub fn has_version(&self, name: &[u8]) -> bool {
self.lookup(name).is_some()
}
pub fn get_sha1s<'a, I>(&self, version_ids: I) -> Result<Vec<(Vec<u8>, Vec<u8>)>, WeaveError>
where
I: IntoIterator<Item = &'a [u8]>,
{
let mut out = Vec::new();
for v in version_ids {
let idx = self
.lookup(v)
.ok_or_else(|| WeaveError::RevisionNotPresentByName(v.to_vec()))?;
out.push((v.to_vec(), self.sha1s[idx].clone()));
}
Ok(out)
}
pub fn get_parent_map<'a, I>(&self, version_ids: I) -> Vec<(Vec<u8>, Vec<Vec<u8>>)>
where
I: IntoIterator<Item = &'a [u8]>,
{
let mut out = Vec::new();
for v in version_ids {
if let Some(idx) = self.lookup(v) {
let parents = self.parents[idx]
.iter()
.map(|&p| self.names[p].clone())
.collect();
out.push((v.to_vec(), parents));
}
}
out
}
pub fn get_ancestry<'a, I>(&self, version_ids: I) -> Result<Vec<Vec<u8>>, WeaveError>
where
I: IntoIterator<Item = &'a [u8]>,
{
let mut indices = Vec::new();
for v in version_ids {
indices.push(
self.lookup(v)
.ok_or_else(|| WeaveError::RevisionNotPresentByName(v.to_vec()))?,
);
}
let included = inclusions(&self.parents, &indices);
let mut names: Vec<Vec<u8>> = included.iter().map(|&i| self.names[i].clone()).collect();
names.sort();
Ok(names)
}
pub fn compatible_parents(
my_parents: &std::collections::HashSet<usize>,
other_parents: &std::collections::HashSet<usize>,
) -> bool {
other_parents.is_subset(my_parents)
}
pub fn get_lines(&self, version: usize) -> Result<Vec<Vec<u8>>, WeaveError> {
if version >= self.parents.len() {
return Err(WeaveError::RevisionNotPresent(version));
}
let included = inclusions(&self.parents, &[version]);
let extracted = extract(&self.weave, &included)?;
let result: Vec<Vec<u8>> = extracted.iter().map(|e| e.text.to_vec()).collect();
let measured = sha_strings(&result);
let expected = &self.sha1s[version];
if &measured != expected {
return Err(WeaveError::InvalidChecksum {
version,
expected: expected.clone(),
measured,
});
}
Ok(result)
}
pub fn annotate(&self, version: usize) -> Result<Vec<(Vec<u8>, Vec<u8>)>, WeaveError> {
if version >= self.parents.len() {
return Err(WeaveError::RevisionNotPresent(version));
}
let included = inclusions(&self.parents, &[version]);
let extracted = extract(&self.weave, &included)?;
Ok(extracted
.into_iter()
.map(|e| (self.names[e.origin].clone(), e.text.to_vec()))
.collect())
}
pub fn add(
&mut self,
version_id: Option<&[u8]>,
lines: &[Vec<u8>],
parents: &[usize],
sha1: Option<Vec<u8>>,
nostore_sha: Option<&[u8]>,
) -> Result<usize, WeaveError> {
let sha1 = sha1.unwrap_or_else(|| sha_strings(lines));
if let Some(no) = nostore_sha {
if no == sha1.as_slice() {
return Err(WeaveError::ExistingContent);
}
}
let owned_name: Vec<u8>;
let version_id: &[u8] = match version_id {
Some(v) => v,
None => {
owned_name = {
let mut s = b"sha1:".to_vec();
s.extend_from_slice(&sha1);
s
};
&owned_name
}
};
if let Some(idx) = self.lookup(version_id) {
return self.check_repeated_add(version_id, parents, &sha1, idx);
}
for &p in parents {
if p >= self.parents.len() {
return Err(WeaveError::RevisionNotPresent(p));
}
}
let new_version = self.parents.len();
self.parents.push(parents.to_vec());
self.sha1s.push(sha1.clone());
self.names.push(version_id.to_vec());
if parents.is_empty() {
if !lines.is_empty() {
self.weave.push(WeaveEntry::Control {
op: Instruction::InsertOpen,
version: new_version,
});
for line in lines {
self.weave.push(WeaveEntry::Line(line.clone()));
}
self.weave.push(WeaveEntry::Control {
op: Instruction::InsertClose,
version: 0,
});
}
return Ok(new_version);
}
if parents.len() == 1 && self.sha1s[parents[0]] == sha1 {
return Ok(new_version);
}
let ancestors = inclusions(&self.parents, parents);
let extracted = extract(&self.weave, &ancestors)?;
let mut basis_lineno: Vec<usize> = extracted.iter().map(|e| e.lineno).collect();
let basis_lines: Vec<&[u8]> = extracted.iter().map(|e| e.text).collect();
if basis_lines.len() == lines.len()
&& basis_lines
.iter()
.zip(lines.iter())
.all(|(a, b)| *a == b.as_slice())
{
return Ok(new_version);
}
basis_lineno.push(self.weave.len());
let basis_owned: Vec<Vec<u8>> = basis_lines.iter().map(|s| s.to_vec()).collect();
let mut sm = patiencediff::SequenceMatcher::new(&basis_owned, lines);
let opcodes = sm.get_opcodes();
let mut offset: isize = 0;
for op in opcodes {
if matches!(op, patiencediff::Opcode::Equal(_, _, _, _)) {
continue;
}
let i1_basis = op.a_start();
let i2_basis = op.a_end();
let j1 = op.b_start();
let j2 = op.b_end();
let i1 = basis_lineno[i1_basis];
let i2 = basis_lineno[i2_basis];
if i1 != i2 {
let pos1 = (i1 as isize + offset) as usize;
self.weave.insert(
pos1,
WeaveEntry::Control {
op: Instruction::DeleteOpen,
version: new_version,
},
);
let pos2 = (i2 as isize + offset + 1) as usize;
self.weave.insert(
pos2,
WeaveEntry::Control {
op: Instruction::DeleteClose,
version: new_version,
},
);
offset += 2;
}
if j1 != j2 {
let i = (i2 as isize + offset) as usize;
let mut splice: Vec<WeaveEntry> = Vec::with_capacity(j2 - j1 + 2);
splice.push(WeaveEntry::Control {
op: Instruction::InsertOpen,
version: new_version,
});
for line in &lines[j1..j2] {
splice.push(WeaveEntry::Line(line.clone()));
}
splice.push(WeaveEntry::Control {
op: Instruction::InsertClose,
version: 0,
});
let added = splice.len();
let tail = self.weave.split_off(i);
self.weave.extend(splice);
self.weave.extend(tail);
offset += added as isize;
}
}
Ok(new_version)
}
fn check_repeated_add(
&self,
name: &[u8],
parents: &[usize],
sha1: &[u8],
idx: usize,
) -> Result<usize, WeaveError> {
let mut existing = self.parents[idx].clone();
existing.sort_unstable();
let mut requested = parents.to_vec();
requested.sort_unstable();
if existing != requested || self.sha1s[idx] != sha1 {
return Err(WeaveError::RevisionAlreadyPresent(name.to_vec()));
}
Ok(idx)
}
pub fn add_lines(
&mut self,
version_id: &[u8],
parent_names: &[&[u8]],
lines: &[Vec<u8>],
sha1: Option<Vec<u8>>,
nostore_sha: Option<&[u8]>,
) -> Result<usize, WeaveError> {
let mut parent_idxs = Vec::with_capacity(parent_names.len());
for name in parent_names {
parent_idxs.push(
self.lookup(name)
.ok_or_else(|| WeaveError::RevisionNotPresentByName(name.to_vec()))?,
);
}
self.add(Some(version_id), lines, &parent_idxs, sha1, nostore_sha)
}
pub fn iter_lines_added_or_present_in_versions<'a, I>(
&self,
version_names: Option<I>,
) -> Result<impl Iterator<Item = (Vec<u8>, Vec<u8>)> + '_, WeaveError>
where
I: IntoIterator<Item = &'a [u8]>,
{
let included: std::collections::HashSet<usize> = match version_names {
None => (0..self.parents.len()).collect(),
Some(iter) => {
let mut s = std::collections::HashSet::new();
for name in iter {
if let Some(idx) = self.lookup(name) {
s.insert(idx);
}
}
s
}
};
let walked = walk_internal(&self.weave)?;
Ok(walked.into_iter().filter_map(move |w| {
if !included.contains(&w.insert) {
return None;
}
let mut line = w.text.to_vec();
if !line.ends_with(b"\n") {
line.push(b'\n');
}
Some((line, self.names[w.insert].clone()))
}))
}
pub fn plan_merge(
&self,
ver_a: &[u8],
ver_b: &[u8],
) -> Result<Vec<(PlanMergeState, Vec<u8>)>, WeaveError> {
let idx_a = self
.lookup(ver_a)
.ok_or_else(|| WeaveError::RevisionNotPresentByName(ver_a.to_vec()))?;
let idx_b = self
.lookup(ver_b)
.ok_or_else(|| WeaveError::RevisionNotPresentByName(ver_b.to_vec()))?;
let inc_a = inclusions(&self.parents, &[idx_a]);
let inc_b = inclusions(&self.parents, &[idx_b]);
let inc_c: std::collections::HashSet<usize> = inc_a.intersection(&inc_b).copied().collect();
let walked = walk_internal(&self.weave)?;
let mut out = Vec::new();
for w in walked {
let deletes_hit_c = w.deletes.iter().any(|d| inc_c.contains(d));
let state = if deletes_hit_c {
PlanMergeState::KilledBase
} else if inc_c.contains(&w.insert) {
let killed_a = w.deletes.iter().any(|d| inc_a.contains(d));
let killed_b = w.deletes.iter().any(|d| inc_b.contains(d));
match (killed_a, killed_b) {
(true, true) => PlanMergeState::KilledBoth,
(true, false) => PlanMergeState::KilledA,
(false, true) => PlanMergeState::KilledB,
(false, false) => PlanMergeState::Unchanged,
}
} else if inc_a.contains(&w.insert) {
if w.deletes.iter().any(|d| inc_a.contains(d)) {
PlanMergeState::GhostA
} else {
PlanMergeState::NewA
}
} else if inc_b.contains(&w.insert) {
if w.deletes.iter().any(|d| inc_b.contains(d)) {
PlanMergeState::GhostB
} else {
PlanMergeState::NewB
}
} else {
PlanMergeState::Irrelevant
};
out.push((state, w.text.to_vec()));
}
Ok(out)
}
pub fn check(&self) -> Result<(), WeaveError> {
use sha1::{Digest, Sha1};
let nv = self.num_versions();
for v in 0..nv {
if let Some(&max) = self.parents[v].iter().max() {
if max >= v {
return Err(WeaveError::Acyclic { version: v, max });
}
}
}
let mut inclusions_by_name: std::collections::HashMap<
Vec<u8>,
std::collections::HashSet<Vec<u8>>,
> = std::collections::HashMap::with_capacity(nv);
let mut hashers: std::collections::HashMap<Vec<u8>, Sha1> =
std::collections::HashMap::with_capacity(nv);
for v in 0..nv {
let name = &self.names[v];
let mut new_inc: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
new_inc.insert(name.clone());
for &p in &self.parents[v] {
let parent_name = &self.names[p];
if let Some(parent_inc) = inclusions_by_name.get(parent_name) {
new_inc.extend(parent_inc.iter().cloned());
}
}
let computed = self
.get_ancestry(std::iter::once(name.as_slice()))?
.into_iter()
.collect::<std::collections::HashSet<_>>();
if computed != new_inc {
return Err(WeaveError::AncestryMismatch(name.clone()));
}
inclusions_by_name.insert(name.clone(), new_inc);
hashers.insert(name.clone(), Sha1::new());
}
for w in walk_internal(&self.weave)? {
let insert_name = &self.names[w.insert];
let delete_names: Vec<&[u8]> = w
.deletes
.iter()
.map(|&d| self.names[d].as_slice())
.collect();
for v in 0..nv {
let name = &self.names[v];
let inc = &inclusions_by_name[name];
if !inc.contains(insert_name) {
continue;
}
if delete_names.iter().any(|d| inc.contains(*d)) {
continue;
}
hashers.get_mut(name).expect("seeded above").update(w.text);
}
}
for v in 0..nv {
let name = &self.names[v];
let digest = hashers.remove(name).expect("seeded above").finalize();
let mut hex = vec![0u8; digest.len() * 2];
for (i, byte) in digest.iter().enumerate() {
let high = byte >> 4;
let low = byte & 0x0f;
hex[i * 2] = if high < 10 {
b'0' + high
} else {
b'a' + high - 10
};
hex[i * 2 + 1] = if low < 10 {
b'0' + low
} else {
b'a' + low - 10
};
}
if hex != self.sha1s[v] {
return Err(WeaveError::InvalidChecksum {
version: v,
expected: self.sha1s[v].clone(),
measured: hex,
});
}
}
Ok(())
}
pub fn imported_parents(
&self,
other: &WeaveFile,
other_idx: usize,
) -> Result<Vec<usize>, WeaveError> {
let mut new_parents = Vec::with_capacity(other.parents[other_idx].len());
for &parent_idx in &other.parents[other_idx] {
let parent_name = &other.names[parent_idx];
let mapped = self
.lookup(parent_name)
.ok_or_else(|| WeaveError::MissingParent {
parent: parent_name.clone(),
child: other.names[other_idx].clone(),
})?;
new_parents.push(mapped);
}
Ok(new_parents)
}
pub fn check_version_consistent(
&self,
other: &WeaveFile,
other_idx: usize,
name: &[u8],
) -> Result<bool, WeaveError> {
let this_idx = match self.lookup(name) {
None => return Ok(false),
Some(i) => i,
};
if self.sha1s[this_idx] != other.sha1s[other_idx] {
return Err(WeaveError::TextDiffers(name.to_vec()));
}
let self_parents: std::collections::HashSet<Vec<u8>> = self.parents[this_idx]
.iter()
.map(|&p| self.names[p].clone())
.collect();
let other_parents: std::collections::HashSet<Vec<u8>> = other.parents[other_idx]
.iter()
.map(|&p| other.names[p].clone())
.collect();
if !other_parents.is_subset(&self_parents) {
return Err(WeaveError::ParentMismatch {
version: name.to_vec(),
a: self_parents.into_iter().collect(),
b: other_parents.into_iter().collect(),
});
}
Ok(true)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlanMergeState {
KilledBase,
KilledBoth,
KilledA,
KilledB,
Unchanged,
NewA,
NewB,
GhostA,
GhostB,
Irrelevant,
}
impl PlanMergeState {
pub fn tag(self) -> &'static [u8] {
match self {
PlanMergeState::KilledBase => b"killed-base",
PlanMergeState::KilledBoth => b"killed-both",
PlanMergeState::KilledA => b"killed-a",
PlanMergeState::KilledB => b"killed-b",
PlanMergeState::Unchanged => b"unchanged",
PlanMergeState::NewA => b"new-a",
PlanMergeState::NewB => b"new-b",
PlanMergeState::GhostA => b"ghost-a",
PlanMergeState::GhostB => b"ghost-b",
PlanMergeState::Irrelevant => b"irrelevant",
}
}
}
pub fn order_record_stream(
wf: &WeaveFile,
names: &[Vec<u8>],
ordering: &str,
) -> Option<Vec<Vec<u8>>> {
use vcs_graph::tsort::TopoSorter;
let mut known = Vec::new();
let mut unknown = Vec::new();
for name in names {
if wf.lookup(name).is_some() {
known.push(name.clone());
} else {
unknown.push(name.clone());
}
}
match ordering {
"unordered" => Some(names.to_vec()),
"topological" => {
let pairs: Vec<(Vec<u8>, Vec<Vec<u8>>)> = known
.iter()
.map(|name| {
let idx = wf.lookup(name).expect("known by construction");
let parents = wf.parents[idx]
.iter()
.map(|&p| wf.names[p].clone())
.collect();
(name.clone(), parents)
})
.collect();
let mut sorter = TopoSorter::new(pairs.into_iter());
let mut sorted = sorter.sorted().expect("acyclic by invariant");
sorted.extend(unknown);
Some(sorted)
}
"groupcompress" => {
let parent_map: Vec<(Vec<Vec<u8>>, Vec<Vec<Vec<u8>>>)> = known
.iter()
.map(|name| {
let idx = wf.lookup(name).expect("known by construction");
let key = vec![name.clone()];
let parents: Vec<Vec<Vec<u8>>> = wf.parents[idx]
.iter()
.map(|&p| vec![wf.names[p].clone()])
.collect();
(key, parents)
})
.collect();
let mut out: Vec<Vec<u8>> = crate::groupcompress::sort::sort_gc_optimal(parent_map)
.into_iter()
.map(|key| key.into_iter().next().expect("single-segment key"))
.collect();
out.extend(unknown);
Some(out)
}
_ => None,
}
}
pub fn reweave(wa: &WeaveFile, wb: &WeaveFile) -> Result<WeaveFile, WeaveError> {
use vcs_graph::tsort::TopoSorter;
let mut combined: std::collections::BTreeMap<Vec<u8>, std::collections::BTreeSet<Vec<u8>>> =
std::collections::BTreeMap::new();
for w in [wa, wb] {
for (idx, name) in w.names.iter().enumerate() {
let entry = combined.entry(name.clone()).or_default();
for &p in &w.parents[idx] {
entry.insert(w.names[p].clone());
}
}
}
let pairs: Vec<(Vec<u8>, Vec<Vec<u8>>)> = combined
.iter()
.map(|(k, v)| (k.clone(), v.iter().cloned().collect()))
.collect();
let mut sorter = TopoSorter::new(pairs.into_iter());
let order = sorter.sorted().map_err(|_| WeaveError::ReweaveCycle)?;
let mut wr = WeaveFile::default();
for name in order {
let lines = if let Some(idx_a) = wa.lookup(&name) {
let lines_a = wa.get_lines(idx_a)?;
if let Some(idx_b) = wb.lookup(&name) {
let lines_b = wb.get_lines(idx_b)?;
if lines_a != lines_b {
return Err(WeaveError::TextDiffers(name.clone()));
}
}
lines_a
} else {
let idx_b = wb.lookup(&name).expect("name from combined map");
wb.get_lines(idx_b)?
};
let parent_names: Vec<Vec<u8>> = combined
.get(&name)
.expect("name from combined map")
.iter()
.cloned()
.collect();
let parent_refs: Vec<&[u8]> = parent_names.iter().map(|p| p.as_slice()).collect();
wr.add_lines(&name, &parent_refs, &lines, None, None)?;
}
Ok(wr)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WeaveFileError {
BadHeader(Vec<u8>),
UnexpectedEof,
UnexpectedLine(Vec<u8>),
InvalidInteger(Vec<u8>),
}
impl std::fmt::Display for WeaveFileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WeaveFileError::BadHeader(l) => write!(f, "invalid weave file header: {:?}", l),
WeaveFileError::UnexpectedEof => write!(f, "unexpected end of weave file"),
WeaveFileError::UnexpectedLine(l) => write!(f, "unexpected line {:?}", l),
WeaveFileError::InvalidInteger(s) => write!(f, "not a valid integer: {:?}", s),
}
}
}
impl std::error::Error for WeaveFileError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Instruction {
InsertOpen,
InsertClose,
DeleteOpen,
DeleteClose,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WeaveEntry {
Line(Vec<u8>),
Control { op: Instruction, version: usize },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WeaveError {
UnmatchedInsertClose,
UnmatchedDeleteClose(usize),
UnclosedInsertions(Vec<usize>),
UnclosedDeletions(Vec<usize>),
RevisionAlreadyPresent(Vec<u8>),
RevisionNotPresent(usize),
RevisionNotPresentByName(Vec<u8>),
ExistingContent,
InvalidChecksum {
version: usize,
expected: Vec<u8>,
measured: Vec<u8>,
},
Acyclic { version: usize, max: usize },
AncestryMismatch(Vec<u8>),
MissingParent { parent: Vec<u8>, child: Vec<u8> },
TextDiffers(Vec<u8>),
ParentMismatch {
version: Vec<u8>,
a: Vec<Vec<u8>>,
b: Vec<Vec<u8>>,
},
ReweaveCycle,
}
impl std::fmt::Display for WeaveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WeaveError::UnmatchedInsertClose => write!(f, "unmatched '}}' in weave"),
WeaveError::UnmatchedDeleteClose(v) => {
write!(f, "unmatched ']' for version {} in weave", v)
}
WeaveError::UnclosedInsertions(v) => {
write!(f, "unclosed insertion blocks at end of weave: {:?}", v)
}
WeaveError::UnclosedDeletions(v) => {
write!(f, "unclosed deletion blocks at end of weave: {:?}", v)
}
WeaveError::RevisionAlreadyPresent(name) => {
write!(f, "revision {:?} already present", name)
}
WeaveError::RevisionNotPresent(v) => {
write!(f, "revision index {} not present", v)
}
WeaveError::RevisionNotPresentByName(name) => {
write!(f, "revision {:?} not present", name)
}
WeaveError::ExistingContent => write!(f, "content already stored under nostore_sha"),
WeaveError::InvalidChecksum {
version,
expected,
measured,
} => write!(
f,
"invalid checksum for version {}: expected {:?}, measured {:?}",
version, expected, measured
),
WeaveError::Acyclic { version, max } => {
write!(f, "invalid included version {} for index {}", max, version)
}
WeaveError::AncestryMismatch(name) => {
write!(f, "ancestry mismatch for version {:?}", name)
}
WeaveError::MissingParent { parent, child } => {
write!(f, "missing parent {{{:?}}} of {{{:?}}}", parent, child)
}
WeaveError::TextDiffers(name) => {
write!(f, "weaves differ on text content for version {:?}", name)
}
WeaveError::ParentMismatch { version, a, b } => write!(
f,
"inconsistent parents for version {{{:?}}}: {:?} vs {:?}",
version, a, b
),
WeaveError::ReweaveCycle => write!(f, "cycle in combined parent graph during reweave"),
}
}
}
impl std::error::Error for WeaveError {}
#[derive(Debug, PartialEq, Eq)]
pub struct ExtractLine<'a> {
pub origin: usize,
pub lineno: usize,
pub text: &'a [u8],
}
pub fn extract<'a>(
weave: &'a [WeaveEntry],
included: &std::collections::HashSet<usize>,
) -> Result<Vec<ExtractLine<'a>>, WeaveError> {
let mut istack: Vec<usize> = Vec::new();
let mut dset: std::collections::HashSet<usize> = std::collections::HashSet::new();
let mut isactive: Option<bool> = None;
let mut result = Vec::new();
for (lineno, entry) in weave.iter().enumerate() {
match entry {
WeaveEntry::Control { op, version } => {
isactive = None;
match op {
Instruction::InsertOpen => istack.push(*version),
Instruction::InsertClose => {
istack.pop().ok_or(WeaveError::UnmatchedInsertClose)?;
}
Instruction::DeleteOpen => {
if included.contains(version) {
dset.insert(*version);
}
}
Instruction::DeleteClose => {
if included.contains(version) && !dset.remove(version) {
return Err(WeaveError::UnmatchedDeleteClose(*version));
}
}
}
}
WeaveEntry::Line(text) => {
let active = match isactive {
Some(a) => a,
None => {
let a = dset.is_empty()
&& istack.last().is_some_and(|top| included.contains(top));
isactive = Some(a);
a
}
};
if active {
result.push(ExtractLine {
origin: *istack.last().expect("active implies non-empty istack"),
lineno,
text,
});
}
}
}
}
if !istack.is_empty() {
return Err(WeaveError::UnclosedInsertions(istack));
}
if !dset.is_empty() {
let mut v: Vec<usize> = dset.iter().copied().collect();
v.sort_unstable();
return Err(WeaveError::UnclosedDeletions(v));
}
Ok(result)
}
pub fn inclusions(
parents_by_version: &[Vec<usize>],
versions: &[usize],
) -> std::collections::HashSet<usize> {
let mut out = std::collections::HashSet::new();
if versions.is_empty() {
return out;
}
out.extend(versions.iter().copied());
let max_v = *versions.iter().max().expect("non-empty");
for v in (1..=max_v).rev() {
if out.contains(&v) {
if let Some(ps) = parents_by_version.get(v) {
out.extend(ps.iter().copied());
}
}
}
out
}
#[derive(Debug, PartialEq, Eq)]
pub struct WalkLine<'a> {
pub lineno: usize,
pub insert: usize,
pub deletes: Vec<usize>,
pub text: &'a [u8],
}
pub fn walk_internal(weave: &[WeaveEntry]) -> Result<Vec<WalkLine<'_>>, WeaveError> {
let mut istack: Vec<usize> = Vec::new();
let mut dset: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
let mut result = Vec::new();
for (lineno, entry) in weave.iter().enumerate() {
match entry {
WeaveEntry::Control { op, version } => match op {
Instruction::InsertOpen => istack.push(*version),
Instruction::InsertClose => {
istack.pop().ok_or(WeaveError::UnmatchedInsertClose)?;
}
Instruction::DeleteOpen => {
dset.insert(*version);
}
Instruction::DeleteClose => {
if !dset.remove(version) {
return Err(WeaveError::UnmatchedDeleteClose(*version));
}
}
},
WeaveEntry::Line(text) => {
let insert = *istack.last().expect("line outside any insertion block");
result.push(WalkLine {
lineno,
insert,
deletes: dset.iter().copied().collect(),
text,
});
}
}
}
if !istack.is_empty() {
return Err(WeaveError::UnclosedInsertions(istack));
}
if !dset.is_empty() {
return Err(WeaveError::UnclosedDeletions(
dset.iter().copied().collect(),
));
}
Ok(result)
}
pub fn read_weave_v5(data: &[u8]) -> Result<WeaveFile, WeaveFileError> {
let lines = split_with_newlines(data);
let mut iter = lines.into_iter();
let first = iter.next().ok_or(WeaveFileError::UnexpectedEof)?;
if first != WEAVE_V5_FORMAT {
return Err(WeaveFileError::BadHeader(first.to_vec()));
}
let mut out = WeaveFile::default();
loop {
let line = iter.next().ok_or(WeaveFileError::UnexpectedEof)?;
if line == b"w\n" {
break;
}
if line.first() == Some(&b'i') {
let ps = if line.len() > 2 {
let trimmed = trim_trailing_newline(&line[2..]);
let mut result = Vec::new();
for part in trimmed.split(|&b| b == b' ') {
result.push(parse_usize(part)?);
}
result
} else {
Vec::new()
};
out.parents.push(ps);
let sha1_line = iter.next().ok_or(WeaveFileError::UnexpectedEof)?;
out.sha1s
.push(trim_trailing_newline(&sha1_line[2..]).to_vec());
let name_line = iter.next().ok_or(WeaveFileError::UnexpectedEof)?;
out.names
.push(trim_trailing_newline(&name_line[2..]).to_vec());
iter.next().ok_or(WeaveFileError::UnexpectedEof)?;
} else {
return Err(WeaveFileError::UnexpectedLine(line.to_vec()));
}
}
loop {
let line = iter.next().ok_or(WeaveFileError::UnexpectedEof)?;
if line == b"W\n" {
break;
}
if line.starts_with(b". ") {
out.weave.push(WeaveEntry::Line(line[2..].to_vec()));
} else if line.starts_with(b", ") {
out.weave
.push(WeaveEntry::Line(trim_trailing_newline(&line[2..]).to_vec()));
} else if line == b"}\n" {
out.weave.push(WeaveEntry::Control {
op: Instruction::InsertClose,
version: 0,
});
} else {
let tag = *line
.first()
.ok_or_else(|| WeaveFileError::UnexpectedLine(line.to_vec()))?;
let op = match tag {
b'{' => Instruction::InsertOpen,
b'[' => Instruction::DeleteOpen,
b']' => Instruction::DeleteClose,
_ => return Err(WeaveFileError::UnexpectedLine(line.to_vec())),
};
if line.len() < 3 || line[1] != b' ' {
return Err(WeaveFileError::UnexpectedLine(line.to_vec()));
}
let version = parse_usize(trim_trailing_newline(&line[2..]))?;
out.weave.push(WeaveEntry::Control { op, version });
}
}
Ok(out)
}
pub fn write_weave_v5(wf: &WeaveFile) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(WEAVE_V5_FORMAT);
for version in 0..wf.parents.len() {
let parents = &wf.parents[version];
if parents.is_empty() {
out.extend_from_slice(b"i\n");
} else {
out.extend_from_slice(b"i ");
for (i, &p) in parents.iter().enumerate() {
if i > 0 {
out.push(b' ');
}
out.extend_from_slice(p.to_string().as_bytes());
}
out.push(b'\n');
}
out.extend_from_slice(b"1 ");
out.extend_from_slice(&wf.sha1s[version]);
out.push(b'\n');
out.extend_from_slice(b"n ");
out.extend_from_slice(&wf.names[version]);
out.push(b'\n');
out.push(b'\n');
}
out.extend_from_slice(b"w\n");
for entry in &wf.weave {
match entry {
WeaveEntry::Control { op, version } => match op {
Instruction::InsertClose => out.extend_from_slice(b"}\n"),
Instruction::InsertOpen => {
out.extend_from_slice(b"{ ");
out.extend_from_slice(version.to_string().as_bytes());
out.push(b'\n');
}
Instruction::DeleteOpen => {
out.extend_from_slice(b"[ ");
out.extend_from_slice(version.to_string().as_bytes());
out.push(b'\n');
}
Instruction::DeleteClose => {
out.extend_from_slice(b"] ");
out.extend_from_slice(version.to_string().as_bytes());
out.push(b'\n');
}
},
WeaveEntry::Line(line) => {
if line.is_empty() {
out.extend_from_slice(b", \n");
} else if line.last() == Some(&b'\n') {
out.extend_from_slice(b". ");
out.extend_from_slice(line);
} else {
out.extend_from_slice(b", ");
out.extend_from_slice(line);
out.push(b'\n');
}
}
}
}
out.extend_from_slice(b"W\n");
out
}
fn split_with_newlines(data: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut start = 0;
for (i, &b) in data.iter().enumerate() {
if b == b'\n' {
out.push(&data[start..=i]);
start = i + 1;
}
}
if start < data.len() {
out.push(&data[start..]);
}
out
}
fn trim_trailing_newline(line: &[u8]) -> &[u8] {
if line.last() == Some(&b'\n') {
&line[..line.len() - 1]
} else {
line
}
}
fn parse_usize(bytes: &[u8]) -> Result<usize, WeaveFileError> {
std::str::from_utf8(bytes)
.ok()
.and_then(|s| s.trim().parse::<usize>().ok())
.ok_or_else(|| WeaveFileError::InvalidInteger(bytes.to_vec()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn line(s: &[u8]) -> WeaveEntry {
WeaveEntry::Line(s.to_vec())
}
fn ctl(op: Instruction, v: usize) -> WeaveEntry {
WeaveEntry::Control { op, version: v }
}
fn set(xs: &[usize]) -> HashSet<usize> {
xs.iter().copied().collect()
}
#[test]
fn single_version_extract() {
let weave = vec![
ctl(Instruction::InsertOpen, 0),
line(b"a\n"),
line(b"b\n"),
line(b"c\n"),
ctl(Instruction::InsertClose, 0),
];
let got = extract(&weave, &set(&[0])).unwrap();
let lines: Vec<&[u8]> = got.iter().map(|e| e.text).collect();
assert_eq!(lines, vec![b"a\n".as_slice(), b"b\n", b"c\n"]);
assert!(got.iter().all(|e| e.origin == 0));
}
#[test]
fn excluded_version_filtered() {
let weave = vec![
ctl(Instruction::InsertOpen, 0),
line(b"base\n"),
ctl(Instruction::InsertClose, 0),
ctl(Instruction::InsertOpen, 1),
line(b"only-in-1\n"),
ctl(Instruction::InsertClose, 1),
];
let got = extract(&weave, &set(&[0])).unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].text, b"base\n");
assert_eq!(got[0].origin, 0);
}
#[test]
fn nested_insertion_origin() {
let weave = vec![
ctl(Instruction::InsertOpen, 0),
line(b"top\n"),
ctl(Instruction::InsertOpen, 1),
line(b"nested\n"),
ctl(Instruction::InsertClose, 1),
line(b"bottom\n"),
ctl(Instruction::InsertClose, 0),
];
let got = extract(&weave, &set(&[0, 1])).unwrap();
let pairs: Vec<(usize, &[u8])> = got.iter().map(|e| (e.origin, e.text)).collect();
assert_eq!(
pairs,
vec![(0, b"top\n".as_slice()), (1, b"nested\n"), (0, b"bottom\n"),]
);
}
#[test]
fn deletion_suppresses_line() {
let weave = vec![
ctl(Instruction::InsertOpen, 0),
line(b"keep\n"),
ctl(Instruction::DeleteOpen, 1),
line(b"gone\n"),
ctl(Instruction::DeleteClose, 1),
line(b"also\n"),
ctl(Instruction::InsertClose, 0),
];
let got_v0 = extract(&weave, &set(&[0])).unwrap();
assert_eq!(got_v0.len(), 3, "without version 1, delete is inert");
let got_v01 = extract(&weave, &set(&[0, 1])).unwrap();
let lines: Vec<&[u8]> = got_v01.iter().map(|e| e.text).collect();
assert_eq!(lines, vec![b"keep\n".as_slice(), b"also\n"]);
}
#[test]
fn unclosed_insertion_errors() {
let weave = vec![ctl(Instruction::InsertOpen, 0), line(b"x\n")];
assert_eq!(
extract(&weave, &set(&[0])),
Err(WeaveError::UnclosedInsertions(vec![0]))
);
}
#[test]
fn unmatched_close_errors() {
let weave = vec![ctl(Instruction::InsertClose, 0)];
assert_eq!(
extract(&weave, &set(&[0])),
Err(WeaveError::UnmatchedInsertClose)
);
}
#[test]
fn inclusions_empty_input() {
assert!(inclusions(&[vec![]], &[]).is_empty());
}
#[test]
fn inclusions_linear_chain() {
let parents = vec![vec![], vec![0], vec![1], vec![2]];
let got = inclusions(&parents, &[3]);
assert_eq!(got, set(&[0, 1, 2, 3]));
}
#[test]
fn inclusions_version_zero_root_is_not_expanded() {
let parents = vec![vec![999], vec![0]];
let got = inclusions(&parents, &[1]);
assert_eq!(got, set(&[0, 1]));
}
#[test]
fn inclusions_merges_converge() {
let parents = vec![vec![], vec![0], vec![0], vec![1, 2]];
let got = inclusions(&parents, &[3]);
assert_eq!(got, set(&[0, 1, 2, 3]));
}
#[test]
fn walk_internal_reports_deletes() {
let weave = vec![
ctl(Instruction::InsertOpen, 0),
line(b"a\n"),
ctl(Instruction::DeleteOpen, 1),
line(b"b\n"),
ctl(Instruction::DeleteClose, 1),
ctl(Instruction::InsertClose, 0),
];
let got = walk_internal(&weave).unwrap();
assert_eq!(got.len(), 2);
assert_eq!(got[0].text, b"a\n");
assert_eq!(got[0].insert, 0);
assert!(got[0].deletes.is_empty());
assert_eq!(got[1].text, b"b\n");
assert_eq!(got[1].insert, 0);
assert_eq!(got[1].deletes, vec![1]);
}
#[test]
fn three_way_merge_extract() {
let weave = vec![
ctl(Instruction::InsertOpen, 0),
line(b"header"),
ctl(Instruction::InsertClose, 0),
ctl(Instruction::InsertOpen, 1),
line(b""),
line(b"line from 1"),
ctl(Instruction::InsertClose, 1),
ctl(Instruction::InsertOpen, 2),
ctl(Instruction::DeleteOpen, 3),
line(b""),
ctl(Instruction::DeleteClose, 3),
ctl(Instruction::InsertOpen, 3),
line(b"fixup line"),
ctl(Instruction::InsertClose, 3),
line(b"line from 2"),
ctl(Instruction::DeleteOpen, 3),
line(b"more from 2"),
ctl(Instruction::InsertClose, 2),
ctl(Instruction::DeleteClose, 3),
];
let got = extract(&weave, &set(&[0, 1, 2, 3])).unwrap();
let pairs: Vec<(usize, &[u8])> = got.iter().map(|e| (e.origin, e.text)).collect();
assert_eq!(
pairs,
vec![
(0, b"header".as_slice()),
(1, b""),
(1, b"line from 1"),
(3, b"fixup line"),
(2, b"line from 2"),
]
);
}
#[test]
fn read_weave_v5_minimal() {
let mut data = WEAVE_V5_FORMAT.to_vec();
data.extend_from_slice(b"i\n1 0000000000000000000000000000000000000000\nn text0\n\n");
data.extend_from_slice(b"w\n");
data.extend_from_slice(b"{ 0\n. hello\n}\n");
data.extend_from_slice(b"W\n");
let wf = read_weave_v5(&data).unwrap();
assert_eq!(wf.parents, vec![Vec::<usize>::new()]);
assert_eq!(
wf.sha1s,
vec![b"0000000000000000000000000000000000000000".to_vec()]
);
assert_eq!(wf.names, vec![b"text0".to_vec()]);
assert_eq!(
wf.weave,
vec![
WeaveEntry::Control {
op: Instruction::InsertOpen,
version: 0,
},
WeaveEntry::Line(b"hello\n".to_vec()),
WeaveEntry::Control {
op: Instruction::InsertClose,
version: 0,
},
]
);
}
#[test]
fn read_weave_v5_with_parents_and_no_eol_line() {
let mut data = WEAVE_V5_FORMAT.to_vec();
data.extend_from_slice(b"i\n1 aaa\nn text0\n\n");
data.extend_from_slice(b"i 0\n1 bbb\nn text1\n\n");
data.extend_from_slice(b"w\n");
data.extend_from_slice(b"{ 0\n. line\n, noeol\n}\n");
data.extend_from_slice(b"[ 1\n, gone\n] 1\n");
data.extend_from_slice(b"W\n");
let wf = read_weave_v5(&data).unwrap();
assert_eq!(wf.parents, vec![Vec::<usize>::new(), vec![0]]);
assert_eq!(wf.sha1s, vec![b"aaa".to_vec(), b"bbb".to_vec()]);
assert_eq!(wf.names, vec![b"text0".to_vec(), b"text1".to_vec()]);
assert_eq!(
wf.weave,
vec![
WeaveEntry::Control {
op: Instruction::InsertOpen,
version: 0,
},
WeaveEntry::Line(b"line\n".to_vec()),
WeaveEntry::Line(b"noeol".to_vec()),
WeaveEntry::Control {
op: Instruction::InsertClose,
version: 0,
},
WeaveEntry::Control {
op: Instruction::DeleteOpen,
version: 1,
},
WeaveEntry::Line(b"gone".to_vec()),
WeaveEntry::Control {
op: Instruction::DeleteClose,
version: 1,
},
]
);
}
#[test]
fn read_weave_v5_multiple_parents_on_one_version() {
let mut data = WEAVE_V5_FORMAT.to_vec();
data.extend_from_slice(b"i\n1 a\nn v0\n\n");
data.extend_from_slice(b"i 0\n1 b\nn v1\n\n");
data.extend_from_slice(b"i 0 1\n1 c\nn v2\n\n");
data.extend_from_slice(b"w\nW\n");
let wf = read_weave_v5(&data).unwrap();
assert_eq!(wf.parents, vec![vec![], vec![0], vec![0, 1]]);
assert_eq!(wf.weave, Vec::<WeaveEntry>::new());
}
#[test]
fn read_weave_v5_empty_line_roundtrips_to_empty_bytes() {
let mut data = WEAVE_V5_FORMAT.to_vec();
data.extend_from_slice(b"i\n1 a\nn v0\n\n");
data.extend_from_slice(b"w\n{ 0\n, \n}\nW\n");
let wf = read_weave_v5(&data).unwrap();
assert_eq!(
wf.weave,
vec![
WeaveEntry::Control {
op: Instruction::InsertOpen,
version: 0,
},
WeaveEntry::Line(b"".to_vec()),
WeaveEntry::Control {
op: Instruction::InsertClose,
version: 0,
},
]
);
}
#[test]
fn read_weave_v5_rejects_bad_header() {
let err = read_weave_v5(b"not-a-weave\n").unwrap_err();
assert!(matches!(err, WeaveFileError::BadHeader(_)));
}
#[test]
fn read_weave_v5_rejects_empty_input() {
assert_eq!(read_weave_v5(b""), Err(WeaveFileError::UnexpectedEof));
}
#[test]
fn read_weave_v5_rejects_truncated_after_header() {
let err = read_weave_v5(WEAVE_V5_FORMAT).unwrap_err();
assert_eq!(err, WeaveFileError::UnexpectedEof);
}
fn sample_weave_file() -> WeaveFile {
WeaveFile {
parents: vec![vec![], vec![0], vec![0, 1]],
sha1s: vec![
b"1111111111111111111111111111111111111111".to_vec(),
b"2222222222222222222222222222222222222222".to_vec(),
b"3333333333333333333333333333333333333333".to_vec(),
],
names: vec![b"text0".to_vec(), b"text1".to_vec(), b"merge".to_vec()],
weave: vec![
WeaveEntry::Control {
op: Instruction::InsertOpen,
version: 0,
},
WeaveEntry::Line(b"hello\n".to_vec()),
WeaveEntry::Line(b"no-eol".to_vec()),
WeaveEntry::Control {
op: Instruction::InsertClose,
version: 0,
},
WeaveEntry::Control {
op: Instruction::DeleteOpen,
version: 1,
},
WeaveEntry::Line(b"".to_vec()),
WeaveEntry::Control {
op: Instruction::DeleteClose,
version: 1,
},
],
}
}
#[test]
fn write_weave_v5_shape() {
let expected: Vec<u8> = [
b"# bzr weave file v5\n".as_slice(),
b"i\n1 1111111111111111111111111111111111111111\nn text0\n\n",
b"i 0\n1 2222222222222222222222222222222222222222\nn text1\n\n",
b"i 0 1\n1 3333333333333333333333333333333333333333\nn merge\n\n",
b"w\n",
b"{ 0\n. hello\n, no-eol\n}\n",
b"[ 1\n, \n] 1\n",
b"W\n",
]
.concat();
assert_eq!(write_weave_v5(&sample_weave_file()), expected);
}
#[test]
fn weave_file_round_trip() {
let wf = sample_weave_file();
let bytes = write_weave_v5(&wf);
let parsed = read_weave_v5(&bytes).unwrap();
assert_eq!(parsed, wf);
}
#[test]
fn weave_file_round_trip_minimal() {
let wf = WeaveFile {
parents: vec![vec![]],
sha1s: vec![b"a".to_vec()],
names: vec![b"v0".to_vec()],
weave: vec![],
};
let bytes = write_weave_v5(&wf);
assert_eq!(read_weave_v5(&bytes).unwrap(), wf);
}
#[test]
fn weave_file_round_trip_empty_weave_body() {
let wf = WeaveFile {
parents: vec![vec![], vec![0]],
sha1s: vec![b"x".to_vec(), b"y".to_vec()],
names: vec![b"a".to_vec(), b"b".to_vec()],
weave: vec![],
};
let bytes = write_weave_v5(&wf);
assert_eq!(read_weave_v5(&bytes).unwrap(), wf);
}
#[test]
fn walk_internal_unclosed_insertion_errors() {
let weave = vec![ctl(Instruction::InsertOpen, 0), line(b"x\n")];
assert_eq!(
walk_internal(&weave),
Err(WeaveError::UnclosedInsertions(vec![0]))
);
}
#[test]
fn inactive_insertion_blocks_lines() {
let weave = vec![
ctl(Instruction::InsertOpen, 1),
line(b"only-in-1\n"),
ctl(Instruction::InsertClose, 1),
];
let got = extract(&weave, &set(&[0])).unwrap();
assert!(got.is_empty());
}
fn ls(strs: &[&[u8]]) -> Vec<Vec<u8>> {
strs.iter().map(|s| s.to_vec()).collect()
}
#[test]
fn duplicate_add_returns_existing_index() {
let mut wf = WeaveFile::default();
let text = ls(&[b"line 1\n", b"line 2\n"]);
let idx1 = wf.add(Some(b"text0"), &text, &[], None, None).unwrap();
let idx2 = wf.add(Some(b"text0"), &text, &[], None, None).unwrap();
assert_eq!(idx1, idx2);
assert_eq!(wf.parents.len(), 1);
}
#[test]
fn invalid_repeated_add_errors() {
let mut wf = WeaveFile::default();
let text = ls(&[b"line 1\n"]);
wf.add(Some(b"basis"), &text, &[], None, None).unwrap();
wf.add(Some(b"text0"), &text, &[], None, None).unwrap();
let other = ls(&[b"different\n"]);
let err = wf.add(Some(b"text0"), &other, &[], None, None).unwrap_err();
assert_eq!(err, WeaveError::RevisionAlreadyPresent(b"text0".to_vec()));
let err = wf.add(Some(b"text0"), &text, &[0], None, None).unwrap_err();
assert_eq!(err, WeaveError::RevisionAlreadyPresent(b"text0".to_vec()));
}
#[test]
fn invalid_add_missing_parent_errors() {
let mut wf = WeaveFile::default();
let err = wf
.add(Some(b"text0"), &ls(&[b"new text\n"]), &[69], None, None)
.unwrap_err();
assert_eq!(err, WeaveError::RevisionNotPresent(69));
}
#[test]
fn annotate_one_version() {
let mut wf = WeaveFile::default();
let text = ls(&[b"hello\n", b"world\n"]);
let idx = wf.add(Some(b"text0"), &text, &[], None, None).unwrap();
let annotated = wf.annotate(idx).unwrap();
assert_eq!(
annotated,
vec![
(b"text0".to_vec(), b"hello\n".to_vec()),
(b"text0".to_vec(), b"world\n".to_vec()),
]
);
}
#[test]
fn insert_one_line_attribution() {
let mut wf = WeaveFile::default();
wf.add(Some(b"text0"), &ls(&[b"line 1\n"]), &[], None, None)
.unwrap();
wf.add(
Some(b"text1"),
&ls(&[b"line 1\n", b"line 2\n"]),
&[0],
None,
None,
)
.unwrap();
assert_eq!(
wf.annotate(0).unwrap(),
vec![(b"text0".to_vec(), b"line 1\n".to_vec())]
);
assert_eq!(
wf.get_lines(1).unwrap(),
vec![b"line 1\n".to_vec(), b"line 2\n".to_vec()]
);
assert_eq!(
wf.annotate(1).unwrap(),
vec![
(b"text0".to_vec(), b"line 1\n".to_vec()),
(b"text1".to_vec(), b"line 2\n".to_vec()),
]
);
}
#[test]
fn insert_lines_merge_attribution() {
let mut wf = WeaveFile::default();
wf.add(Some(b"text0"), &ls(&[b"line 1\n"]), &[], None, None)
.unwrap();
wf.add(
Some(b"text1"),
&ls(&[b"line 1\n", b"line 2\n"]),
&[0],
None,
None,
)
.unwrap();
wf.add(
Some(b"text3"),
&ls(&[b"line 1\n", b"middle line\n", b"line 2\n"]),
&[0, 1],
None,
None,
)
.unwrap();
assert_eq!(
wf.annotate(2).unwrap(),
vec![
(b"text0".to_vec(), b"line 1\n".to_vec()),
(b"text3".to_vec(), b"middle line\n".to_vec()),
(b"text1".to_vec(), b"line 2\n".to_vec()),
]
);
}
#[test]
fn delete_lines_round_trip() {
let mut wf = WeaveFile::default();
let base = ls(&[b"one\n", b"two\n", b"three\n", b"four\n"]);
wf.add(Some(b"text0"), &base, &[], None, None).unwrap();
let texts: Vec<Vec<Vec<u8>>> = vec![
ls(&[b"one\n", b"two\n", b"three\n"]),
ls(&[b"two\n", b"three\n", b"four\n"]),
ls(&[b"one\n", b"four\n"]),
ls(&[b"one\n", b"two\n", b"three\n", b"four\n"]),
];
for (i, t) in texts.iter().enumerate() {
wf.add(
Some(format!("text{}", i + 1).as_bytes()),
t,
&[0],
None,
None,
)
.unwrap();
}
for (i, t) in texts.iter().enumerate() {
assert_eq!(&wf.get_lines(i + 1).unwrap(), t);
}
}
#[test]
fn add_nostore_sha_blocks_storage() {
let mut wf = WeaveFile::default();
let text = ls(&[b"line\n"]);
let sha = sha_strings(&text);
let err = wf
.add(Some(b"text0"), &text, &[], None, Some(&sha))
.unwrap_err();
assert_eq!(err, WeaveError::ExistingContent);
assert!(wf.parents.is_empty());
}
#[test]
fn has_version_reports_membership() {
let mut wf = WeaveFile::default();
assert!(!wf.has_version(b"foo"));
wf.add(Some(b"foo"), &ls(&[b"x\n"]), &[], None, None)
.unwrap();
assert!(wf.has_version(b"foo"));
assert!(!wf.has_version(b"bar"));
}
#[test]
fn versions_returns_names_in_order() {
let mut wf = WeaveFile::default();
wf.add(Some(b"a"), &ls(&[b"a\n"]), &[], None, None).unwrap();
wf.add(Some(b"b"), &ls(&[b"a\n", b"b\n"]), &[0], None, None)
.unwrap();
assert_eq!(wf.versions(), vec![b"a".to_vec(), b"b".to_vec()]);
assert_eq!(wf.num_versions(), 2);
}
#[test]
fn get_sha1s_for_known_and_unknown() {
let mut wf = WeaveFile::default();
let text = ls(&[b"hello\n"]);
let sha = sha_strings(&text);
wf.add(Some(b"v0"), &text, &[], None, None).unwrap();
let known: &[&[u8]] = &[b"v0"];
assert_eq!(
wf.get_sha1s(known.iter().copied()).unwrap(),
vec![(b"v0".to_vec(), sha)]
);
let unknown: &[&[u8]] = &[b"nope"];
let err = wf.get_sha1s(unknown.iter().copied()).unwrap_err();
assert_eq!(err, WeaveError::RevisionNotPresentByName(b"nope".to_vec()));
}
#[test]
fn get_parent_map_drops_unknown() {
let mut wf = WeaveFile::default();
wf.add(Some(b"a"), &ls(&[b"x\n"]), &[], None, None).unwrap();
wf.add(Some(b"b"), &ls(&[b"x\n", b"y\n"]), &[0], None, None)
.unwrap();
let lookups: &[&[u8]] = &[b"a", b"missing", b"b"];
let mp = wf.get_parent_map(lookups.iter().copied());
assert_eq!(
mp,
vec![
(b"a".to_vec(), vec![]),
(b"b".to_vec(), vec![b"a".to_vec()]),
]
);
}
#[test]
fn get_ancestry_diverged() {
let mut wf = WeaveFile::default();
wf.add(Some(b"0"), &ls(&[b"first line"]), &[], None, None)
.unwrap();
wf.add(
Some(b"1"),
&ls(&[b"first line", b"second line"]),
&[0],
None,
None,
)
.unwrap();
wf.add(
Some(b"2"),
&ls(&[b"first line", b"alternative second line"]),
&[0],
None,
None,
)
.unwrap();
let asked: &[&[u8]] = &[b"2"];
let anc = wf.get_ancestry(asked.iter().copied()).unwrap();
assert_eq!(anc, vec![b"0".to_vec(), b"2".to_vec()]);
}
#[test]
fn compatible_parents_subset_check() {
let my: HashSet<usize> = [1, 2, 3].iter().copied().collect();
assert!(WeaveFile::compatible_parents(
&my,
&[3].iter().copied().collect()
));
assert!(WeaveFile::compatible_parents(&my, &my));
assert!(WeaveFile::compatible_parents(
&HashSet::new(),
&HashSet::new()
));
assert!(!WeaveFile::compatible_parents(
&HashSet::new(),
&[1].iter().copied().collect()
));
assert!(!WeaveFile::compatible_parents(
&my,
&[1, 2, 3, 4].iter().copied().collect()
));
assert!(!WeaveFile::compatible_parents(
&my,
&[4].iter().copied().collect()
));
}
#[test]
fn add_anonymous_uses_sha_name() {
let mut wf = WeaveFile::default();
let text = ls(&[b"hi\n"]);
let sha = sha_strings(&text);
let idx = wf.add(None, &text, &[], None, None).unwrap();
assert_eq!(wf.names[idx], {
let mut n = b"sha1:".to_vec();
n.extend_from_slice(&sha);
n
});
}
#[test]
fn add_lines_by_name_unknown_parent_errors() {
let mut wf = WeaveFile::default();
let lines = ls(&[b"x\n"]);
let parents: &[&[u8]] = &[b"69"];
let err = wf
.add_lines(b"text0", parents, &lines, None, None)
.unwrap_err();
assert_eq!(err, WeaveError::RevisionNotPresentByName(b"69".to_vec()));
}
#[test]
fn add_lines_by_name_resolves_parent() {
let mut wf = WeaveFile::default();
wf.add_lines(b"v0", &[], &ls(&[b"a\n"]), None, None)
.unwrap();
let p: &[&[u8]] = &[b"v0"];
let idx = wf
.add_lines(b"v1", p, &ls(&[b"a\n", b"b\n"]), None, None)
.unwrap();
assert_eq!(idx, 1);
assert_eq!(wf.get_lines(1).unwrap(), ls(&[b"a\n", b"b\n"]));
}
#[test]
fn iter_lines_appends_missing_newline() {
let mut wf = WeaveFile::default();
wf.add(Some(b"v0"), &[b"no-eol".to_vec()], &[], None, None)
.unwrap();
let names: Vec<&[u8]> = vec![b"v0"];
let got = wf
.iter_lines_added_or_present_in_versions(Some(names))
.unwrap()
.collect::<Vec<_>>();
assert_eq!(got, vec![(b"no-eol\n".to_vec(), b"v0".to_vec())]);
}
#[test]
fn iter_lines_filters_by_version() {
let mut wf = WeaveFile::default();
wf.add(Some(b"v0"), &ls(&[b"a\n", b"b\n"]), &[], None, None)
.unwrap();
wf.add(
Some(b"v1"),
&ls(&[b"a\n", b"b\n", b"c\n"]),
&[0],
None,
None,
)
.unwrap();
let names: Vec<&[u8]> = vec![b"v1"];
let got = wf
.iter_lines_added_or_present_in_versions(Some(names))
.unwrap()
.collect::<Vec<_>>();
assert_eq!(got, vec![(b"c\n".to_vec(), b"v1".to_vec())]);
}
#[test]
fn iter_lines_all_versions() {
let mut wf = WeaveFile::default();
wf.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
wf.add(Some(b"v1"), &ls(&[b"a\n", b"b\n"]), &[0], None, None)
.unwrap();
let got = wf
.iter_lines_added_or_present_in_versions::<std::iter::Empty<&[u8]>>(None)
.unwrap()
.collect::<Vec<_>>();
assert_eq!(
got,
vec![
(b"a\n".to_vec(), b"v0".to_vec()),
(b"b\n".to_vec(), b"v1".to_vec()),
]
);
}
#[test]
fn plan_merge_disjoint_additions() {
let mut wf = WeaveFile::default();
wf.add(Some(b"base"), &ls(&[b"shared\n"]), &[], None, None)
.unwrap();
wf.add(
Some(b"a"),
&ls(&[b"shared\n", b"from-a\n"]),
&[0],
None,
None,
)
.unwrap();
wf.add(
Some(b"b"),
&ls(&[b"shared\n", b"from-b\n"]),
&[0],
None,
None,
)
.unwrap();
let plan = wf.plan_merge(b"a", b"b").unwrap();
let by_state: Vec<(&'static [u8], &[u8])> =
plan.iter().map(|(s, l)| (s.tag(), l.as_slice())).collect();
assert!(by_state.contains(&(b"unchanged", b"shared\n".as_slice())));
assert!(by_state.contains(&(b"new-a", b"from-a\n".as_slice())));
assert!(by_state.contains(&(b"new-b", b"from-b\n".as_slice())));
}
#[test]
fn plan_merge_killed_both() {
let mut wf = WeaveFile::default();
wf.add(Some(b"base"), &ls(&[b"keep\n", b"drop\n"]), &[], None, None)
.unwrap();
wf.add(Some(b"a"), &ls(&[b"keep\n"]), &[0], None, None)
.unwrap();
wf.add(Some(b"b"), &ls(&[b"keep\n"]), &[0], None, None)
.unwrap();
let plan = wf.plan_merge(b"a", b"b").unwrap();
let drops: Vec<&'static [u8]> = plan
.iter()
.filter(|(_, l)| l == b"drop\n")
.map(|(s, _)| s.tag())
.collect();
assert_eq!(drops, vec![b"killed-both"]);
}
#[test]
fn check_passes_on_clean_weave() {
let mut wf = WeaveFile::default();
wf.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
wf.add(Some(b"v1"), &ls(&[b"a\n", b"b\n"]), &[0], None, None)
.unwrap();
wf.check().unwrap();
}
#[test]
fn check_rejects_corrupt_sha1() {
let mut wf = WeaveFile::default();
wf.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
wf.sha1s[0] = b"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_vec();
let err = wf.check().unwrap_err();
assert!(matches!(
err,
WeaveError::InvalidChecksum { version: 0, .. }
));
}
#[test]
fn check_rejects_acyclic_violation() {
let mut wf = WeaveFile::default();
wf.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
wf.add(Some(b"v1"), &ls(&[b"a\n"]), &[0], None, None)
.unwrap();
wf.parents[1] = vec![1];
let err = wf.check().unwrap_err();
assert_eq!(err, WeaveError::Acyclic { version: 1, max: 1 });
}
#[test]
fn imported_parents_translates_indices() {
let mut wa = WeaveFile::default();
wa.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
wa.add(Some(b"v1"), &ls(&[b"a\n", b"b\n"]), &[0], None, None)
.unwrap();
let mut wb = WeaveFile::default();
wb.add(Some(b"v1"), &ls(&[b"x\n"]), &[], None, None)
.unwrap();
wb.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
let got = wa.imported_parents(&wb, 0).unwrap();
assert!(got.is_empty());
}
#[test]
fn imported_parents_missing_parent_errors() {
let mut wa = WeaveFile::default();
wa.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
let mut wb = WeaveFile::default();
wb.add(Some(b"orphan"), &ls(&[b"x\n"]), &[], None, None)
.unwrap();
wb.add(Some(b"v0"), &ls(&[b"a\n"]), &[0], None, None)
.unwrap();
let err = wa.imported_parents(&wb, 1).unwrap_err();
assert!(matches!(err, WeaveError::MissingParent { .. }));
}
#[test]
fn check_version_consistent_states() {
let mut wa = WeaveFile::default();
wa.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
let mut wb = WeaveFile::default();
wb.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
wb.add(Some(b"v1"), &ls(&[b"a\n", b"b\n"]), &[0], None, None)
.unwrap();
assert_eq!(wa.check_version_consistent(&wb, 1, b"v1").unwrap(), false);
assert_eq!(wa.check_version_consistent(&wb, 0, b"v0").unwrap(), true);
wb.sha1s[0] = b"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_vec();
let err = wa.check_version_consistent(&wb, 0, b"v0").unwrap_err();
assert_eq!(err, WeaveError::TextDiffers(b"v0".to_vec()));
}
#[test]
fn reweave_merges_disjoint_branches() {
let mut wa = WeaveFile::default();
wa.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
wa.add(
Some(b"a-only"),
&ls(&[b"a\n", b"alpha\n"]),
&[0],
None,
None,
)
.unwrap();
let mut wb = WeaveFile::default();
wb.add(Some(b"v0"), &ls(&[b"a\n"]), &[], None, None)
.unwrap();
wb.add(Some(b"b-only"), &ls(&[b"a\n", b"beta\n"]), &[0], None, None)
.unwrap();
let result = reweave(&wa, &wb).unwrap();
let mut names = result.versions();
names.sort();
assert_eq!(
names,
vec![b"a-only".to_vec(), b"b-only".to_vec(), b"v0".to_vec()]
);
let i = result.lookup(b"a-only").unwrap();
assert_eq!(result.get_lines(i).unwrap(), ls(&[b"a\n", b"alpha\n"]));
let j = result.lookup(b"b-only").unwrap();
assert_eq!(result.get_lines(j).unwrap(), ls(&[b"a\n", b"beta\n"]));
}
#[test]
fn order_record_stream_topological() {
let mut wf = WeaveFile::default();
wf.add(Some(b"a"), &ls(&[b"a\n"]), &[], None, None).unwrap();
wf.add(Some(b"b"), &ls(&[b"a\n", b"b\n"]), &[0], None, None)
.unwrap();
wf.add(Some(b"c"), &ls(&[b"a\n", b"b\n", b"c\n"]), &[1], None, None)
.unwrap();
let names: Vec<Vec<u8>> = vec![
b"c".to_vec(),
b"a".to_vec(),
b"missing".to_vec(),
b"b".to_vec(),
];
let got = order_record_stream(&wf, &names, "topological").unwrap();
assert_eq!(
got,
vec![
b"a".to_vec(),
b"b".to_vec(),
b"c".to_vec(),
b"missing".to_vec(),
]
);
}
#[test]
fn order_record_stream_unordered() {
let mut wf = WeaveFile::default();
wf.add(Some(b"a"), &ls(&[b"a\n"]), &[], None, None).unwrap();
wf.add(Some(b"b"), &ls(&[b"a\n", b"b\n"]), &[0], None, None)
.unwrap();
let names: Vec<Vec<u8>> = vec![b"b".to_vec(), b"a".to_vec()];
assert_eq!(
order_record_stream(&wf, &names, "unordered").unwrap(),
vec![b"b".to_vec(), b"a".to_vec()]
);
}
#[test]
fn order_record_stream_rejects_unknown_ordering() {
let wf = WeaveFile::default();
assert!(order_record_stream(&wf, &[], "bogus").is_none());
}
#[test]
fn reweave_text_diffs_error() {
let mut wa = WeaveFile::default();
wa.add(Some(b"v0"), &ls(&[b"hello\n"]), &[], None, None)
.unwrap();
let mut wb = WeaveFile::default();
wb.add(Some(b"v0"), &ls(&[b"goodbye\n"]), &[], None, None)
.unwrap();
let err = reweave(&wa, &wb).unwrap_err();
assert_eq!(err, WeaveError::TextDiffers(b"v0".to_vec()));
}
#[test]
fn round_trip_equality() {
let mut w1 = WeaveFile::default();
w1.add(Some(b"text0"), &[b"header".to_vec()], &[], None, None)
.unwrap();
w1.add(
Some(b"text1"),
&[b"header".to_vec(), b"".to_vec(), b"line from 1".to_vec()],
&[0],
None,
None,
)
.unwrap();
let bytes = write_weave_v5(&w1);
let w2 = read_weave_v5(&bytes).unwrap();
assert_eq!(w1.parents, w2.parents);
assert_eq!(w1.sha1s, w2.sha1s);
assert_eq!(w1.names, w2.names);
assert_eq!(w1.weave, w2.weave);
assert_eq!(w1, w2);
}
#[test]
fn canned_delete_round_trip() {
let lines_v0 = ls(&[b"first line", b"line to be deleted", b"last line"]);
let lines_v1 = ls(&[b"first line", b"last line"]);
let weave_body = vec![
ctl(Instruction::InsertOpen, 0),
line(b"first line"),
ctl(Instruction::DeleteOpen, 1),
line(b"line to be deleted"),
ctl(Instruction::DeleteClose, 1),
line(b"last line"),
ctl(Instruction::InsertClose, 0),
];
let wf = WeaveFile {
parents: vec![vec![], vec![0]],
sha1s: vec![sha_strings(&lines_v0), sha_strings(&lines_v1)],
names: vec![b"v0".to_vec(), b"v1".to_vec()],
weave: weave_body,
};
assert_eq!(wf.get_lines(0).unwrap(), lines_v0);
assert_eq!(wf.get_lines(1).unwrap(), lines_v1);
}
#[test]
fn canned_replacement_round_trip() {
let lines_v0 = ls(&[b"first line", b"line to be deleted", b"last line"]);
let lines_v1 = ls(&[b"first line", b"replacement line", b"last line"]);
let weave_body = vec![
ctl(Instruction::InsertOpen, 0),
line(b"first line"),
ctl(Instruction::DeleteOpen, 1),
line(b"line to be deleted"),
ctl(Instruction::DeleteClose, 1),
ctl(Instruction::InsertOpen, 1),
line(b"replacement line"),
ctl(Instruction::InsertClose, 1),
line(b"last line"),
ctl(Instruction::InsertClose, 0),
];
let wf = WeaveFile {
parents: vec![vec![], vec![0]],
sha1s: vec![sha_strings(&lines_v0), sha_strings(&lines_v1)],
names: vec![b"v0".to_vec(), b"v1".to_vec()],
weave: weave_body,
};
assert_eq!(wf.get_lines(0).unwrap(), lines_v0);
assert_eq!(wf.get_lines(1).unwrap(), lines_v1);
}
#[test]
fn insert_nested_round_trip() {
let v0 = ls(&[b"foo {", b"}"]);
let v1 = ls(&[b"foo {", b" added in version 1", b" also from v1", b"}"]);
let v2 = ls(&[b"foo {", b" added in v2", b"}"]);
let v3 = ls(&[
b"foo {",
b" added in version 1",
b" added in v2",
b" also from v1",
b"}",
]);
let weave_body = vec![
ctl(Instruction::InsertOpen, 0),
line(b"foo {"),
ctl(Instruction::InsertOpen, 1),
line(b" added in version 1"),
ctl(Instruction::InsertOpen, 2),
line(b" added in v2"),
ctl(Instruction::InsertClose, 2),
line(b" also from v1"),
ctl(Instruction::InsertClose, 1),
line(b"}"),
ctl(Instruction::InsertClose, 0),
];
let wf = WeaveFile {
parents: vec![vec![], vec![0], vec![0], vec![0, 1, 2]],
sha1s: vec![
sha_strings(&v0),
sha_strings(&v1),
sha_strings(&v2),
sha_strings(&v3),
],
names: vec![
b"v0".to_vec(),
b"v1".to_vec(),
b"v2".to_vec(),
b"v3".to_vec(),
],
weave: weave_body,
};
assert_eq!(wf.get_lines(0).unwrap(), v0);
assert_eq!(wf.get_lines(1).unwrap(), v1);
assert_eq!(wf.get_lines(2).unwrap(), v2);
assert_eq!(wf.get_lines(3).unwrap(), v3);
}
#[test]
fn include_versions_round_trip() {
let v0 = ls(&[b"first line"]);
let v01 = ls(&[b"first line", b"second line"]);
let weave_body = vec![
ctl(Instruction::InsertOpen, 0),
line(b"first line"),
ctl(Instruction::InsertClose, 0),
ctl(Instruction::InsertOpen, 1),
line(b"second line"),
ctl(Instruction::InsertClose, 1),
];
let wf = WeaveFile {
parents: vec![vec![], vec![0]],
sha1s: vec![sha_strings(&v0), sha_strings(&v01)],
names: vec![b"v0".to_vec(), b"v1".to_vec()],
weave: weave_body,
};
assert_eq!(wf.get_lines(0).unwrap(), v0);
assert_eq!(wf.get_lines(1).unwrap(), v01);
}
#[test]
fn diverged_includes_round_trip() {
let v0 = ls(&[b"first line"]);
let v01 = ls(&[b"first line", b"second line"]);
let v02 = ls(&[b"first line", b"alternative second line"]);
let weave_body = vec![
ctl(Instruction::InsertOpen, 0),
line(b"first line"),
ctl(Instruction::InsertClose, 0),
ctl(Instruction::InsertOpen, 1),
line(b"second line"),
ctl(Instruction::InsertClose, 1),
ctl(Instruction::InsertOpen, 2),
line(b"alternative second line"),
ctl(Instruction::InsertClose, 2),
];
let wf = WeaveFile {
parents: vec![vec![], vec![0], vec![0]],
sha1s: vec![sha_strings(&v0), sha_strings(&v01), sha_strings(&v02)],
names: vec![b"0".to_vec(), b"1".to_vec(), b"2".to_vec()],
weave: weave_body,
};
assert_eq!(wf.get_lines(0).unwrap(), v0);
assert_eq!(wf.get_lines(1).unwrap(), v01);
assert_eq!(wf.get_lines(2).unwrap(), v02);
let ancestry: Vec<&[u8]> = vec![b"2"];
let mut got = wf.get_ancestry(ancestry).unwrap();
got.sort();
assert_eq!(got, vec![b"0".to_vec(), b"2".to_vec()]);
}
}