pub mod format;
pub use format::{all_formats, find_format, BranchFormat};
use std::collections::BTreeMap;
use crate::declare_branch_format;
use crate::lockdir::{Lock, LockDir, LockError};
use crate::transport::{SharedTransport, TransportError};
#[cfg(any(feature = "weave", feature = "knit"))]
declare_branch_format! {
FORMAT_5 {
format_string: b"Bazaar-NG branch format 5\n",
description: "Branch format 5 (full history)",
supports_tags: false,
full_history: true,
supported: true,
deprecated: true,
}
}
declare_branch_format! {
FORMAT_6 {
format_string: b"Bazaar Branch Format 6 (bzr 0.15)\n",
description: "Branch format 6",
supports_tags: true,
supports_reference_locations: true,
supported: true,
}
}
declare_branch_format! {
FORMAT_7 {
format_string: b"Bazaar Branch Format 7 (needs bzr 1.6)\n",
description: "Branch format 7 (stackable)",
supports_tags: true,
supports_stacking: true,
supports_reference_locations: true,
supported: true,
}
}
declare_branch_format! {
FORMAT_8 {
format_string: b"Bazaar Branch Format 8 (needs bzr 1.15)\n",
description: "Branch format 8 (reference locations)",
supports_tags: true,
supports_stacking: true,
supports_reference_locations: true,
supported: true,
}
}
declare_branch_format! {
REFERENCE_FORMAT_1 {
format_string: b"Bazaar-NG Branch Reference Format 1\n",
description: "Branch reference format 1",
is_reference: true,
}
}
const DEFAULT_FORMAT: &BranchFormat = &FORMAT_7;
pub const NULL_REVISION: &[u8] = b"null:";
#[derive(Debug)]
pub enum BranchError {
Corrupt(String),
Lock(LockError),
Transport(TransportError),
Config(crate::config::ConfigError),
NotStacked,
Unstackable,
Unsupported(String),
}
impl std::fmt::Display for BranchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BranchError::Corrupt(m) => write!(f, "corrupt branch data: {m}"),
BranchError::Lock(e) => write!(f, "branch lock error: {e}"),
BranchError::Transport(e) => write!(f, "transport error: {e}"),
BranchError::Config(e) => write!(f, "config error: {e}"),
BranchError::NotStacked => write!(f, "branch is not stacked"),
BranchError::Unstackable => write!(f, "branch format does not support stacking"),
BranchError::Unsupported(op) => write!(f, "unsupported branch operation: {op}"),
}
}
}
impl std::error::Error for BranchError {}
impl From<TransportError> for BranchError {
fn from(e: TransportError) -> Self {
BranchError::Transport(e)
}
}
impl From<LockError> for BranchError {
fn from(e: LockError) -> Self {
BranchError::Lock(e)
}
}
impl From<crate::config::ConfigError> for BranchError {
fn from(e: crate::config::ConfigError) -> Self {
BranchError::Config(e)
}
}
pub type RevisionInfo = (u64, Vec<u8>);
pub struct Branch {
transport: SharedTransport,
format: &'static BranchFormat,
}
impl Branch {
pub fn new(transport: SharedTransport) -> Self {
let format = match transport.get_bytes("format") {
Ok(marker) => find_format(&marker).unwrap_or(DEFAULT_FORMAT),
Err(_) => DEFAULT_FORMAT,
};
Branch { transport, format }
}
pub fn with_format(transport: SharedTransport, format: &'static BranchFormat) -> Self {
Branch { transport, format }
}
pub fn format(&self) -> &'static BranchFormat {
self.format
}
pub fn last_revision_info(&self) -> Result<RevisionInfo, BranchError> {
if self.format.full_history {
return self.last_revision_info_full_history();
}
let bytes = match self.transport.get_bytes("last-revision") {
Ok(b) => b,
Err(TransportError::NoSuchFile(_)) => return Ok((0, NULL_REVISION.to_vec())),
Err(e) => return Err(e.into()),
};
let line = bytes.strip_suffix(b"\n").unwrap_or(&bytes);
let space = line
.iter()
.position(|&b| b == b' ')
.ok_or_else(|| BranchError::Corrupt("last-revision missing space".to_string()))?;
let revno: u64 = std::str::from_utf8(&line[..space])
.ok()
.and_then(|s| s.parse().ok())
.ok_or_else(|| {
BranchError::Corrupt("last-revision revno not an integer".to_string())
})?;
let revision_id = line[space + 1..].to_vec();
Ok((revno, revision_id))
}
fn last_revision_info_full_history(&self) -> Result<RevisionInfo, BranchError> {
let history = self.revision_history()?;
match history.last() {
Some(tip) => Ok((history.len() as u64, tip.clone())),
None => Ok((0, NULL_REVISION.to_vec())),
}
}
pub fn revision_history(&self) -> Result<Vec<Vec<u8>>, BranchError> {
if !self.format.full_history {
let (revno, tip) = self.last_revision_info()?;
return Ok(if revno == 0 { vec![] } else { vec![tip] });
}
let bytes = match self.transport.get_bytes("revision-history") {
Ok(b) => b,
Err(TransportError::NoSuchFile(_)) => return Ok(vec![]),
Err(e) => return Err(e.into()),
};
Ok(bytes
.split(|&b| b == b'\n')
.filter(|l| !l.is_empty())
.map(|l| l.to_vec())
.collect())
}
pub fn last_revision(&self) -> Result<Vec<u8>, BranchError> {
Ok(self.last_revision_info()?.1)
}
pub fn tags(&self) -> Result<BTreeMap<String, Vec<u8>>, BranchError> {
let bytes = match self.transport.get_bytes("tags") {
Ok(b) => b,
Err(TransportError::NoSuchFile(_)) => return Ok(BTreeMap::new()),
Err(e) => return Err(e.into()),
};
decode_tags(&bytes)
}
pub fn get_config_bytes(&self) -> Result<Vec<u8>, BranchError> {
match self.transport.get_bytes("branch.conf") {
Ok(b) => Ok(b),
Err(TransportError::NoSuchFile(_)) => Ok(Vec::new()),
Err(e) => Err(e.into()),
}
}
fn with_write_lock<R>(
&self,
f: impl FnOnce() -> Result<R, BranchError>,
) -> Result<R, BranchError> {
let mut lock = LockDir::new(self.transport.as_ref(), "lock");
lock.create()?;
lock.attempt_lock()?;
let result = f();
let unlock = lock.unlock();
match (result, unlock) {
(Ok(r), Ok(())) => Ok(r),
(Err(e), _) => Err(e),
(Ok(_), Err(e)) => Err(e.into()),
}
}
pub fn set_last_revision_info(
&self,
revno: u64,
revision_id: &[u8],
) -> Result<(), BranchError> {
if self.format.full_history {
return self.set_last_revision_info_full_history(revno, revision_id);
}
self.with_write_lock(|| {
let mut content = format!("{revno} ").into_bytes();
content.extend_from_slice(revision_id);
content.push(b'\n');
self.transport.put_bytes("last-revision", &content, None)?;
Ok(())
})
}
fn set_last_revision_info_full_history(
&self,
revno: u64,
revision_id: &[u8],
) -> Result<(), BranchError> {
self.with_write_lock(|| {
let mut history = self.revision_history()?;
if revision_id == NULL_REVISION {
history.clear();
} else {
let target_len = revno as usize;
if target_len <= history.len() {
history.truncate(target_len.saturating_sub(1));
}
history.push(revision_id.to_vec());
}
if history.len() as u64 != revno {
return Err(BranchError::Corrupt(format!(
"revno {revno} does not match history length {}",
history.len()
)));
}
self.write_revision_history(&history)
})
}
pub fn set_revision_history(&self, history: &[Vec<u8>]) -> Result<(), BranchError> {
self.with_write_lock(|| self.write_revision_history(history))
}
fn write_revision_history(&self, history: &[Vec<u8>]) -> Result<(), BranchError> {
let content = history.join(&b'\n');
self.transport
.put_bytes("revision-history", &content, None)?;
Ok(())
}
pub fn set_tags(&self, tags: &BTreeMap<String, Vec<u8>>) -> Result<(), BranchError> {
self.with_write_lock(|| {
self.transport.put_bytes("tags", &encode_tags(tags), None)?;
Ok(())
})
}
fn get_config_location(&self, name: &str) -> Result<Option<String>, BranchError> {
let bytes = self.get_config_bytes()?;
let mut store = crate::config::IniFileStore::new();
store.load_from_bytes(&bytes)?;
let value = store
.get_sections()
.into_iter()
.find(|s| s.id().is_none())
.and_then(|s| s.get(name).map(|v| store.unquote(v)));
Ok(value.filter(|v| !v.is_empty()))
}
fn set_config_location(&self, name: &str, value: Option<&str>) -> Result<(), BranchError> {
self.with_write_lock(|| {
let bytes = self.get_config_bytes()?;
let mut store = crate::config::IniFileStore::new();
store.load_from_bytes(&bytes)?;
let mut section = store.get_mutable_section(None);
let raw = value.unwrap_or("");
let quoted = store.quote(raw).ok_or_else(|| {
BranchError::Unsupported(format!("config value cannot be quoted: {raw:?}"))
})?;
section.set(name, "ed);
store.apply_changes(§ion);
self.transport
.put_bytes("branch.conf", &store.to_bytes(), None)?;
Ok(())
})
}
fn get_config_bool(&self, name: &str) -> Result<Option<bool>, BranchError> {
match self.get_config_location(name)? {
Some(v) => Ok(crate::config::bool_from_store(&v)),
None => Ok(None),
}
}
pub fn get_stacked_on_url(&self) -> Result<String, BranchError> {
if !self.format.supports_stacking {
return Err(BranchError::Unstackable);
}
self.get_config_location("stacked_on_location")?
.ok_or(BranchError::NotStacked)
}
pub fn set_stacked_on_url(&self, url: Option<&str>) -> Result<(), BranchError> {
if !self.format.supports_stacking {
return Err(BranchError::Unstackable);
}
self.set_config_location("stacked_on_location", url)
}
pub fn get_bound_location(&self) -> Result<Option<String>, BranchError> {
if self.format.full_history {
return self.read_bound_file();
}
match self.get_config_bool("bound")? {
Some(true) => self.get_config_location("bound_location"),
_ => Ok(None),
}
}
pub fn get_old_bound_location(&self) -> Result<Option<String>, BranchError> {
if self.format.full_history {
return Ok(None);
}
match self.get_config_bool("bound")? {
Some(false) | None => self.get_config_location("bound_location"),
Some(true) => Ok(None),
}
}
pub fn set_bound_location(&self, location: Option<&str>) -> Result<(), BranchError> {
if self.format.full_history {
return self.write_bound_file(location);
}
match location {
None => self.set_config_bool("bound", false),
Some(loc) => {
self.set_config_location("bound_location", Some(loc))?;
self.set_config_bool("bound", true)
}
}
}
pub fn bind(&self, other_url: &str) -> Result<(), BranchError> {
self.set_bound_location(Some(other_url))
}
pub fn unbind(&self) -> Result<(), BranchError> {
self.set_bound_location(None)
}
fn read_bound_file(&self) -> Result<Option<String>, BranchError> {
match self.transport.get_bytes("bound") {
Ok(b) => {
let s = b.strip_suffix(b"\n").unwrap_or(&b);
let url = String::from_utf8(s.to_vec())
.map_err(|_| BranchError::Corrupt("bound file not utf-8".to_string()))?;
Ok(Some(url))
}
Err(TransportError::NoSuchFile(_)) => Ok(None),
Err(e) => Err(e.into()),
}
}
fn write_bound_file(&self, location: Option<&str>) -> Result<(), BranchError> {
self.with_write_lock(|| match location {
Some(loc) => {
let mut content = loc.as_bytes().to_vec();
content.push(b'\n');
self.transport.put_bytes("bound", &content, None)?;
Ok(())
}
None => match self.transport.delete("bound") {
Ok(()) | Err(TransportError::NoSuchFile(_)) => Ok(()),
Err(e) => Err(e.into()),
},
})
}
fn set_config_bool(&self, name: &str, value: bool) -> Result<(), BranchError> {
self.set_config_location(name, Some(if value { "True" } else { "False" }))
}
pub fn get_reference_info(
&self,
file_id: &[u8],
) -> Result<(Option<String>, Option<String>), BranchError> {
if !self.format.supports_reference_locations {
return Err(BranchError::Unsupported("reference locations".to_string()));
}
let info = self.read_all_reference_info()?;
Ok(info
.get(file_id)
.cloned()
.map(|(b, t)| (Some(b), t))
.unwrap_or((None, None)))
}
pub fn set_reference_info(
&self,
file_id: &[u8],
branch_location: Option<&str>,
tree_path: Option<&str>,
) -> Result<(), BranchError> {
if !self.format.supports_reference_locations {
return Err(BranchError::Unsupported("reference locations".to_string()));
}
self.with_write_lock(|| {
let mut info = self.read_all_reference_info()?;
match branch_location {
None => {
info.remove(file_id);
}
Some(loc) => {
info.insert(
file_id.to_vec(),
(loc.to_string(), tree_path.map(|s| s.to_string())),
);
}
}
self.write_all_reference_info(&info)?;
if self.format.format_string == FORMAT_7.format_string {
self.transport
.put_bytes("format", FORMAT_8.format_string, None)?;
}
Ok(())
})
}
fn read_all_reference_info(
&self,
) -> Result<BTreeMap<Vec<u8>, (String, Option<String>)>, BranchError> {
let bytes = match self.transport.get_bytes("references") {
Ok(b) => b,
Err(TransportError::NoSuchFile(_)) => return Ok(BTreeMap::new()),
Err(e) => return Err(e.into()),
};
let mut reader = std::io::BufReader::new(&bytes[..]);
let stanzas = crate::rio::read_stanzas(&mut reader)
.map_err(|e| BranchError::Corrupt(format!("references rio: {e:?}")))?;
let mut out = BTreeMap::new();
for stanza in stanzas {
let file_id = stanza_string(&stanza, "file_id")
.ok_or_else(|| BranchError::Corrupt("references stanza has no file_id".into()))?;
let branch_location = stanza_string(&stanza, "branch_location").ok_or_else(|| {
BranchError::Corrupt("references stanza has no branch_location".into())
})?;
let tree_path = stanza_string(&stanza, "tree_path");
out.insert(file_id.into_bytes(), (branch_location, tree_path));
}
Ok(out)
}
fn write_all_reference_info(
&self,
info: &BTreeMap<Vec<u8>, (String, Option<String>)>,
) -> Result<(), BranchError> {
use crate::rio::{Stanza, StanzaValue};
let mut out = Vec::new();
for (file_id, (branch_location, tree_path)) in info {
let mut stanza = Stanza::new();
let file_id = String::from_utf8(file_id.clone())
.map_err(|_| BranchError::Corrupt("reference file_id not utf-8".to_string()))?;
stanza
.add("file_id".to_string(), StanzaValue::String(file_id))
.and_then(|_| {
stanza.add(
"branch_location".to_string(),
StanzaValue::String(branch_location.clone()),
)
})
.map_err(|e| BranchError::Corrupt(format!("references rio: {e:?}")))?;
if let Some(tree_path) = tree_path {
stanza
.add(
"tree_path".to_string(),
StanzaValue::String(tree_path.clone()),
)
.map_err(|e| BranchError::Corrupt(format!("references rio: {e:?}")))?;
}
out.extend(stanza.to_bytes());
}
self.transport.put_bytes("references", &out, None)?;
Ok(())
}
pub fn get_reference(&self) -> Result<Option<String>, BranchError> {
if !self.format.is_reference {
return Ok(None);
}
match self.transport.get_bytes("location") {
Ok(b) => {
let url = String::from_utf8(b)
.map_err(|_| BranchError::Corrupt("location file not utf-8".to_string()))?;
Ok(Some(url))
}
Err(TransportError::NoSuchFile(_)) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn set_reference(&self, to_url: &str) -> Result<(), BranchError> {
if !self.format.is_reference {
return Err(BranchError::Unsupported("branch reference".to_string()));
}
self.transport
.put_bytes("location", to_url.as_bytes(), None)?;
Ok(())
}
}
fn stanza_string(stanza: &crate::rio::Stanza, tag: &str) -> Option<String> {
match stanza.get(tag) {
Some(crate::rio::StanzaValue::String(s)) => Some(s.clone()),
_ => None,
}
}
fn encode_tags(tags: &BTreeMap<String, Vec<u8>>) -> Vec<u8> {
use bendy::encoding::Encoder;
let mut e = Encoder::new();
e.emit_dict(|mut d| {
for (name, target) in tags {
d.emit_pair(name.as_bytes(), Bytes(target))?;
}
Ok(())
})
.expect("tag dict encoding cannot fail");
e.get_output().expect("tag dict encoding cannot fail")
}
struct Bytes<'a>(&'a [u8]);
impl bendy::encoding::ToBencode for Bytes<'_> {
const MAX_DEPTH: usize = 0;
fn encode(
&self,
encoder: bendy::encoding::SingleItemEncoder<'_>,
) -> Result<(), bendy::encoding::Error> {
encoder.emit_bytes(self.0)
}
}
fn decode_tags(bytes: &[u8]) -> Result<BTreeMap<String, Vec<u8>>, BranchError> {
use bendy::decoding::{Decoder, Object};
if bytes.is_empty() {
return Ok(BTreeMap::new());
}
let mut decoder = Decoder::new(bytes);
let obj = decoder
.next_object()
.map_err(|e| BranchError::Corrupt(format!("tags decode: {e}")))?;
let mut dict = match obj {
Some(Object::Dict(d)) => d,
_ => {
return Err(BranchError::Corrupt(
"tags is not a bencode dict".to_string(),
))
}
};
let mut out = BTreeMap::new();
while let Some((key, value)) = dict
.next_pair()
.map_err(|e| BranchError::Corrupt(format!("tags decode: {e}")))?
{
let name = String::from_utf8(key.to_vec())
.map_err(|_| BranchError::Corrupt("tag name not utf-8".to_string()))?;
let target = value
.try_into_bytes()
.map_err(|e| BranchError::Corrupt(format!("tag value not bytes: {e}")))?
.to_vec();
out.insert(name, target);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::{LocalTransport, Transport};
use std::sync::Arc;
fn branch_transport() -> (tempfile::TempDir, Branch, Arc<LocalTransport>) {
let dir = tempfile::tempdir().unwrap();
let probe = Arc::new(LocalTransport::new(dir.path()));
let shared: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
(dir, Branch::new(shared), probe)
}
#[cfg(any(feature = "weave", feature = "knit"))]
fn branch_transport_format5() -> (tempfile::TempDir, Branch, Arc<LocalTransport>) {
let dir = tempfile::tempdir().unwrap();
let probe = Arc::new(LocalTransport::new(dir.path()));
probe
.put_bytes("format", b"Bazaar-NG branch format 5\n", None)
.unwrap();
let shared: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
(dir, Branch::new(shared), probe)
}
#[test]
fn empty_branch_is_null_revision() {
let (_d, branch, _probe) = branch_transport();
assert_eq!(
branch.last_revision_info().unwrap(),
(0, NULL_REVISION.to_vec())
);
assert!(branch.tags().unwrap().is_empty());
}
#[test]
fn last_revision_round_trips() {
let (_d, branch, _probe) = branch_transport();
branch.set_last_revision_info(5, b"rev-abc").unwrap();
assert_eq!(
branch.last_revision_info().unwrap(),
(5, b"rev-abc".to_vec())
);
assert_eq!(branch.last_revision().unwrap(), b"rev-abc".to_vec());
}
#[test]
fn last_revision_on_disk_format() {
let (_d, branch, probe) = branch_transport();
branch.set_last_revision_info(2, b"x").unwrap();
assert_eq!(probe.get_bytes("last-revision").unwrap(), b"2 x\n");
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn format5_empty_branch_is_null_revision() {
let (_d, branch, _probe) = branch_transport_format5();
assert!(branch.format().full_history);
assert_eq!(
branch.last_revision_info().unwrap(),
(0, NULL_REVISION.to_vec())
);
assert!(branch.revision_history().unwrap().is_empty());
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn format5_appends_to_revision_history() {
let (_d, branch, probe) = branch_transport_format5();
branch.set_last_revision_info(1, b"rev-1").unwrap();
branch.set_last_revision_info(2, b"rev-2").unwrap();
assert_eq!(branch.last_revision_info().unwrap(), (2, b"rev-2".to_vec()));
assert_eq!(
branch.revision_history().unwrap(),
vec![b"rev-1".to_vec(), b"rev-2".to_vec()]
);
assert_eq!(
probe.get_bytes("revision-history").unwrap(),
b"rev-1\nrev-2".to_vec()
);
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn format5_set_revision_history_replaces() {
let (_d, branch, _probe) = branch_transport_format5();
branch
.set_revision_history(&[b"a".to_vec(), b"b".to_vec(), b"c".to_vec()])
.unwrap();
assert_eq!(branch.last_revision_info().unwrap(), (3, b"c".to_vec()));
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn format5_set_last_revision_info_truncates() {
let (_d, branch, _probe) = branch_transport_format5();
branch
.set_revision_history(&[b"a".to_vec(), b"b".to_vec(), b"c".to_vec()])
.unwrap();
branch.set_last_revision_info(2, b"b").unwrap();
assert_eq!(branch.last_revision_info().unwrap(), (2, b"b".to_vec()));
assert_eq!(
branch.revision_history().unwrap(),
vec![b"a".to_vec(), b"b".to_vec()]
);
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn format5_set_last_revision_info_null_empties_history() {
let (_d, branch, _probe) = branch_transport_format5();
branch.set_last_revision_info(1, b"rev-1").unwrap();
branch.set_last_revision_info(0, NULL_REVISION).unwrap();
assert_eq!(
branch.last_revision_info().unwrap(),
(0, NULL_REVISION.to_vec())
);
assert!(branch.revision_history().unwrap().is_empty());
}
#[test]
fn tags_round_trip() {
let (_d, branch, _probe) = branch_transport();
let mut tags = BTreeMap::new();
tags.insert("v1.0".to_string(), b"rev-1".to_vec());
tags.insert("v2.0".to_string(), b"rev-2".to_vec());
branch.set_tags(&tags).unwrap();
assert_eq!(branch.tags().unwrap(), tags);
}
#[test]
fn tags_on_disk_matches_breezy_bencode() {
let (_d, branch, probe) = branch_transport();
let mut tags = BTreeMap::new();
tags.insert(
"v1.0".to_string(),
b"test@example.com-20200101120000-x".to_vec(),
);
branch.set_tags(&tags).unwrap();
assert_eq!(
probe.get_bytes("tags").unwrap(),
b"d4:v1.033:test@example.com-20200101120000-xe".to_vec()
);
}
#[test]
fn tags_unicode_name_round_trips() {
let (_d, branch, _probe) = branch_transport();
let mut tags = BTreeMap::new();
tags.insert("\u{3b1}".to_string(), b"rev-1".to_vec());
branch.set_tags(&tags).unwrap();
let reopened = Branch::new(branch.transport.clone());
assert_eq!(reopened.tags().unwrap(), tags);
}
#[test]
fn tags_delete_removes_from_map() {
let (_d, branch, _probe) = branch_transport();
let mut tags = BTreeMap::new();
tags.insert("keep".to_string(), b"rev-1".to_vec());
tags.insert("drop".to_string(), b"rev-2".to_vec());
branch.set_tags(&tags).unwrap();
tags.remove("drop");
branch.set_tags(&tags).unwrap();
assert_eq!(branch.tags().unwrap(), tags);
assert!(!branch.tags().unwrap().contains_key("drop"));
}
#[test]
fn tags_ghost_target_is_stored() {
let (_d, branch, _probe) = branch_transport();
let mut tags = BTreeMap::new();
tags.insert("ghost".to_string(), b"idontexist".to_vec());
branch.set_tags(&tags).unwrap();
assert_eq!(
branch.tags().unwrap().get("ghost").map(|v| v.as_slice()),
Some(&b"idontexist"[..])
);
}
#[test]
fn get_config_bytes_reads_branch_conf() {
let (_d, branch, probe) = branch_transport();
assert!(branch.get_config_bytes().unwrap().is_empty());
let body = b"[DEFAULT]\nnickname = trunk\n";
probe.put_bytes("branch.conf", body, None).unwrap();
assert_eq!(branch.get_config_bytes().unwrap(), body);
}
fn branch_transport_format(
format: &'static BranchFormat,
) -> (tempfile::TempDir, Branch, Arc<LocalTransport>) {
let dir = tempfile::tempdir().unwrap();
let probe = Arc::new(LocalTransport::new(dir.path()));
probe
.put_bytes("format", format.format_string, None)
.unwrap();
let shared: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
(dir, Branch::new(shared), probe)
}
#[test]
fn not_stacked_by_default_on_format_7() {
let (_d, branch, _p) = branch_transport();
assert!(matches!(
branch.get_stacked_on_url(),
Err(BranchError::NotStacked)
));
}
#[test]
fn stacked_on_url_round_trips() {
let (_d, branch, probe) = branch_transport();
branch.set_stacked_on_url(Some("../parent")).unwrap();
assert_eq!(branch.get_stacked_on_url().unwrap(), "../parent");
let conf = String::from_utf8(probe.get_bytes("branch.conf").unwrap()).unwrap();
assert!(
conf.contains("stacked_on_location = ../parent"),
"{:?}",
conf
);
}
#[test]
fn clearing_stacked_on_url_makes_it_not_stacked() {
let (_d, branch, _p) = branch_transport();
branch.set_stacked_on_url(Some("../parent")).unwrap();
branch.set_stacked_on_url(None).unwrap();
assert!(matches!(
branch.get_stacked_on_url(),
Err(BranchError::NotStacked)
));
}
#[test]
fn format_6_is_unstackable() {
let (_d, branch, _p) = branch_transport_format(&FORMAT_6);
assert!(matches!(
branch.get_stacked_on_url(),
Err(BranchError::Unstackable)
));
assert!(matches!(
branch.set_stacked_on_url(Some("x")),
Err(BranchError::Unstackable)
));
}
#[test]
fn unbound_by_default() {
let (_d, branch, _p) = branch_transport();
assert_eq!(branch.get_bound_location().unwrap(), None);
}
#[test]
fn bind_then_get_bound_location() {
let (_d, branch, _p) = branch_transport();
branch.bind("http://example.com/master").unwrap();
assert_eq!(
branch.get_bound_location().unwrap().as_deref(),
Some("http://example.com/master")
);
}
#[test]
fn unbind_clears_bound_but_keeps_old_location() {
let (_d, branch, _p) = branch_transport();
branch.bind("http://example.com/master").unwrap();
branch.unbind().unwrap();
assert_eq!(branch.get_bound_location().unwrap(), None);
assert_eq!(
branch.get_old_bound_location().unwrap().as_deref(),
Some("http://example.com/master")
);
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn format_5_bound_uses_bound_file() {
let (_d, branch, probe) = branch_transport_format5();
assert_eq!(branch.get_bound_location().unwrap(), None);
branch.bind("/srv/master").unwrap();
assert_eq!(probe.get_bytes("bound").unwrap(), b"/srv/master\n");
assert_eq!(
branch.get_bound_location().unwrap().as_deref(),
Some("/srv/master")
);
branch.unbind().unwrap();
assert_eq!(branch.get_bound_location().unwrap(), None);
assert!(matches!(
probe.get_bytes("bound"),
Err(TransportError::NoSuchFile(_))
));
}
#[test]
fn reference_info_round_trips_on_format_8() {
let (_d, branch, _p) = branch_transport_format(&FORMAT_8);
assert_eq!(branch.get_reference_info(b"file-1").unwrap(), (None, None));
branch
.set_reference_info(b"file-1", Some("../subtree"), Some("sub/dir"))
.unwrap();
assert_eq!(
branch.get_reference_info(b"file-1").unwrap(),
(Some("../subtree".to_string()), Some("sub/dir".to_string()))
);
}
#[test]
fn setting_reference_upgrades_format_7_to_8() {
let (_d, branch, probe) = branch_transport_format(&FORMAT_7);
branch
.set_reference_info(b"file-1", Some("../subtree"), None)
.unwrap();
assert_eq!(probe.get_bytes("format").unwrap(), FORMAT_8.format_string);
}
#[test]
fn deleting_reference_info() {
let (_d, branch, _p) = branch_transport_format(&FORMAT_8);
branch
.set_reference_info(b"file-1", Some("../subtree"), None)
.unwrap();
branch.set_reference_info(b"file-1", None, None).unwrap();
assert_eq!(branch.get_reference_info(b"file-1").unwrap(), (None, None));
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn reference_info_unsupported_on_format_5() {
let (_d, branch, _p) = branch_transport_format5();
assert!(matches!(
branch.get_reference_info(b"file-1"),
Err(BranchError::Unsupported(_))
));
}
#[test]
fn get_reference_is_none_on_normal_branch() {
let (_d, branch, _p) = branch_transport();
assert_eq!(branch.get_reference().unwrap(), None);
assert!(matches!(
branch.set_reference("x"),
Err(BranchError::Unsupported(_))
));
}
#[test]
fn reference_round_trips() {
let (_d, branch, probe) = branch_transport_format(&REFERENCE_FORMAT_1);
assert_eq!(branch.get_reference().unwrap(), None);
branch.set_reference("file:///srv/real").unwrap();
assert_eq!(
branch.get_reference().unwrap().as_deref(),
Some("file:///srv/real")
);
assert_eq!(probe.get_bytes("location").unwrap(), b"file:///srv/real");
}
}