pub mod format;
pub use format::{control_dir_formats, find_control_dir_format, ControlDirFormat};
use crate::declare_bzrdir_format;
use crate::transport::{SharedTransport, Transport, TransportError};
pub const METADIR_MARKER: &[u8] = b"Bazaar-NG meta directory, format 1\n";
pub const REPOSITORY_FORMAT_2A: &[u8] = b"Bazaar repository format 2a (needs bzr 1.16 or later)\n";
pub const BRANCH_FORMAT_7: &[u8] = b"Bazaar Branch Format 7 (needs bzr 1.6)\n";
pub const WORKINGTREE_FORMAT_6: &[u8] = b"Bazaar Working Tree Format 6 (bzr 1.14)\n";
const B5: &[u8] = b"Bazaar-NG branch format 5\n";
const B6: &[u8] = b"Bazaar Branch Format 6 (bzr 0.15)\n";
const B7: &[u8] = BRANCH_FORMAT_7;
const WT3: &[u8] = b"Bazaar-NG Working Tree format 3";
const WT4: &[u8] = b"Bazaar Working Tree Format 4 (bzr 0.15)\n";
const WT5: &[u8] = b"Bazaar Working Tree Format 5 (bzr 1.11)\n";
const WT6: &[u8] = WORKINGTREE_FORMAT_6;
declare_bzrdir_format! {
FORMAT_2A {
name: "2a",
repo_marker: REPOSITORY_FORMAT_2A,
branch_marker: B7,
wt_marker: WT6,
wt_has_views: true,
}
}
#[cfg(feature = "knitpack")]
declare_bzrdir_format! {
FORMAT_PACK_0_92 {
name: "pack-0.92",
repo_marker: b"Bazaar pack repository format 1 (needs bzr 0.92)\n",
branch_marker: B6,
wt_marker: WT4,
}
}
#[cfg(feature = "knitpack")]
declare_bzrdir_format! {
FORMAT_PACK_0_92_SUBTREE {
name: "pack-0.92-subtree",
repo_marker: b"Bazaar pack repository format 1 with subtree support (needs bzr 0.92)\n",
branch_marker: B6,
wt_marker: WT4,
}
}
#[cfg(feature = "knitpack")]
declare_bzrdir_format! {
FORMAT_RICH_ROOT_PACK {
name: "rich-root-pack",
repo_marker: b"Bazaar pack repository format 1 with rich root (needs bzr 1.0)\n",
branch_marker: B6,
wt_marker: WT4,
}
}
#[cfg(feature = "knitpack")]
declare_bzrdir_format! {
FORMAT_1_6 {
name: "1.6",
repo_marker: b"Bazaar RepositoryFormatKnitPack5 (bzr 1.6)\n",
branch_marker: B7,
wt_marker: WT4,
}
}
#[cfg(feature = "knitpack")]
declare_bzrdir_format! {
FORMAT_1_6_1_RICH_ROOT {
name: "1.6.1-rich-root",
repo_marker: b"Bazaar RepositoryFormatKnitPack5RichRoot (bzr 1.6.1)\n",
branch_marker: B7,
wt_marker: WT4,
}
}
#[cfg(feature = "knitpack")]
declare_bzrdir_format! {
FORMAT_1_9 {
name: "1.9",
repo_marker: b"Bazaar RepositoryFormatKnitPack6 (bzr 1.9)\n",
branch_marker: B7,
wt_marker: WT4,
}
}
#[cfg(feature = "knitpack")]
declare_bzrdir_format! {
FORMAT_1_9_RICH_ROOT {
name: "1.9-rich-root",
repo_marker: b"Bazaar RepositoryFormatKnitPack6RichRoot (bzr 1.9)\n",
branch_marker: B7,
wt_marker: WT4,
}
}
#[cfg(feature = "knitpack")]
declare_bzrdir_format! {
FORMAT_1_14 {
name: "1.14",
repo_marker: b"Bazaar RepositoryFormatKnitPack6 (bzr 1.9)\n",
branch_marker: B7,
wt_marker: WT5,
}
}
#[cfg(feature = "knitpack")]
declare_bzrdir_format! {
FORMAT_1_14_RICH_ROOT {
name: "1.14-rich-root",
repo_marker: b"Bazaar RepositoryFormatKnitPack6RichRoot (bzr 1.9)\n",
branch_marker: B7,
wt_marker: WT5,
}
}
#[cfg(feature = "knit")]
declare_bzrdir_format! {
FORMAT_KNIT {
name: "knit",
repo_marker: b"Bazaar-NG Knit Repository Format 1",
branch_marker: B5,
wt_marker: WT3,
}
}
#[derive(Debug)]
pub enum BzrDirError {
NotABzrDir,
NotMetaDir(Vec<u8>),
UnsupportedFormat {
component: Component,
found: Vec<u8>,
},
Component(String),
NoRepositoryPresent,
Transport(TransportError),
}
impl std::fmt::Display for BzrDirError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BzrDirError::NotABzrDir => write!(f, "not a .bzr control directory"),
BzrDirError::NotMetaDir(m) => write!(
f,
"not a meta-directory .bzr (found marker {:?})",
String::from_utf8_lossy(m)
),
BzrDirError::UnsupportedFormat { component, found } => write!(
f,
"unsupported {} format: {:?}",
component.as_str(),
String::from_utf8_lossy(found)
),
BzrDirError::Component(m) => write!(f, "{m}"),
BzrDirError::NoRepositoryPresent => {
write!(f, "no repository present and no shared repository found")
}
BzrDirError::Transport(e) => write!(f, "transport error: {e}"),
}
}
}
impl std::error::Error for BzrDirError {}
impl From<TransportError> for BzrDirError {
fn from(e: TransportError) -> Self {
BzrDirError::Transport(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Component {
Repository,
Branch,
WorkingTree,
}
impl Component {
fn as_str(self) -> &'static str {
match self {
Component::Repository => "repository",
Component::Branch => "branch",
Component::WorkingTree => "working tree",
}
}
fn subdir(self) -> &'static str {
match self {
Component::Repository => "repository",
Component::Branch => "branch",
Component::WorkingTree => "checkout",
}
}
fn format_is_supported(self, marker: &[u8]) -> bool {
match self {
Component::Repository => crate::repository::find_format(marker)
.map(|f| f.is_supported())
.unwrap_or(false),
Component::Branch => crate::branch::find_format(marker)
.map(|f| f.is_supported() || f.is_reference)
.unwrap_or(false),
Component::WorkingTree => crate::workingtree::find_format(marker)
.map(|f| f.is_supported())
.unwrap_or(false),
}
}
}
pub trait ControlDir: Send + Sync {
fn transport(&self) -> &SharedTransport;
fn has_repository(&self) -> bool;
fn has_branch(&self) -> bool;
fn has_workingtree(&self) -> bool;
fn open_repository(&self) -> Result<Box<dyn crate::repository::Repository>, BzrDirError>;
fn open_repository_stacked(
&self,
) -> Result<Box<dyn crate::repository::Repository>, BzrDirError> {
self.open_repository()
}
fn open_branch(&self) -> Result<crate::branch::Branch, BzrDirError>;
fn open_workingtree(&self) -> Result<Box<dyn crate::workingtree::WorkingTree>, BzrDirError>;
fn is_shared(&self) -> Result<bool, BzrDirError> {
Ok(false)
}
fn make_working_trees(&self) -> Result<bool, BzrDirError> {
Ok(true)
}
fn set_make_working_trees(&self, _value: bool) -> Result<(), BzrDirError> {
Err(BzrDirError::Component(
"format does not support setting make_working_trees".to_string(),
))
}
fn find_repository(&self) -> Result<Box<dyn crate::repository::Repository>, BzrDirError> {
self.open_repository()
}
}
pub struct BzrDirMeta {
transport: SharedTransport,
has_repository: bool,
has_branch: bool,
has_workingtree: bool,
}
impl BzrDirMeta {
pub fn open(transport: SharedTransport) -> Result<Self, BzrDirError> {
let marker = match transport.get_bytes("branch-format") {
Ok(b) => b,
Err(TransportError::NoSuchFile(_)) => return Err(BzrDirError::NotABzrDir),
Err(e) => return Err(e.into()),
};
if marker != METADIR_MARKER {
return Err(BzrDirError::NotMetaDir(marker));
}
let has_repository = Self::verify_component(transport.as_ref(), Component::Repository)?;
let has_branch = Self::verify_component(transport.as_ref(), Component::Branch)?;
let has_workingtree = Self::verify_component(transport.as_ref(), Component::WorkingTree)?;
Ok(BzrDirMeta {
transport,
has_repository,
has_branch,
has_workingtree,
})
}
pub fn create(parent: &SharedTransport) -> Result<Self, BzrDirError> {
Self::create_with_format(
parent,
find_control_dir_format("2a").expect("2a format is registered"),
)
}
pub fn create_with_format(
parent: &SharedTransport,
format: &ControlDirFormat,
) -> Result<Self, BzrDirError> {
let bzr = parent.subtransport(".bzr")?;
bzr.mkdir("")?;
bzr.put_bytes("branch-format", METADIR_MARKER, None)?;
bzr.put_bytes(
"README",
b"This is a Bazaar control directory.\n\
Do not change any files in this directory.\n\
See http://bazaar.canonical.com/ for more information about Bazaar.\n",
None,
)?;
let repo_t = bzr.subtransport("repository")?;
let repo_format = crate::repository::find_format(format.repo_marker).ok_or_else(|| {
BzrDirError::Component(format!(
"repository format not registered: {:?}",
String::from_utf8_lossy(format.repo_marker)
))
})?;
(repo_format.create)(repo_format, repo_t)
.map_err(|e| BzrDirError::Component(format!("creating repository: {e}")))?;
let branch = bzr.subtransport("branch")?;
branch.mkdir("")?;
branch.put_bytes("format", format.branch_marker, None)?;
let branch_is_format5 = crate::branch::find_format(format.branch_marker)
.map(|f| f.full_history)
.unwrap_or(false);
if branch_is_format5 {
branch.put_bytes("revision-history", b"", None)?;
branch.put_bytes("branch-name", b"", None)?;
} else {
branch.put_bytes("last-revision", b"0 null:\n", None)?;
}
branch.put_bytes("branch.conf", b"", None)?;
branch.put_bytes("tags", b"", None)?;
let checkout = bzr.subtransport("checkout")?;
checkout.mkdir("")?;
checkout.put_bytes("format", format.wt_marker, None)?;
checkout.put_bytes("conflicts", b"BZR conflict list format 1\n", None)?;
let wt_uses_dirstate = crate::workingtree::find_format(format.wt_marker)
.map(|f| f.uses_dirstate)
.unwrap_or(true);
if wt_uses_dirstate {
if format.wt_has_views {
checkout.put_bytes("views", b"", None)?;
}
checkout.put_bytes("dirstate", &empty_dirstate_bytes(), None)?;
} else {
checkout.put_bytes(
"inventory",
b"<inventory format=\"5\">\n</inventory>\n",
None,
)?;
checkout.put_bytes("pending-merges", b"", None)?;
}
Self::open(bzr)
}
pub fn create_shared_repository(parent: &SharedTransport) -> Result<Self, BzrDirError> {
Self::create_shared_repository_with_format(
parent,
find_control_dir_format("2a").expect("2a format is registered"),
)
}
pub fn create_shared_repository_with_format(
parent: &SharedTransport,
format: &ControlDirFormat,
) -> Result<Self, BzrDirError> {
let repo_format = crate::repository::find_format(format.repo_marker).ok_or_else(|| {
BzrDirError::Component(format!(
"repository format not registered: {:?}",
String::from_utf8_lossy(format.repo_marker)
))
})?;
if repo_format.is_all_in_one() {
return Err(BzrDirError::Component(
"all-in-one formats cannot be shared repositories".to_string(),
));
}
let bzr = parent.subtransport(".bzr")?;
bzr.mkdir("")?;
bzr.put_bytes("branch-format", METADIR_MARKER, None)?;
let repo_t = bzr.subtransport("repository")?;
(repo_format.create)(repo_format, repo_t)
.map_err(|e| BzrDirError::Component(format!("creating repository: {e}")))?;
bzr.subtransport("repository")?
.put_bytes("shared-storage", b"", None)?;
Self::open(bzr)
}
fn open_referenced_branch(&self, location: &str) -> Result<crate::branch::Branch, BzrDirError> {
let path = location.strip_prefix("file://").unwrap_or(location);
let containing = self.transport.subtransport(path)?;
let target_bzr = containing.subtransport(".bzr")?;
let target = open(target_bzr)?;
target.open_branch()
}
fn open_stacked_on_repository(
&self,
location: &str,
) -> Result<Box<dyn crate::repository::Repository>, BzrDirError> {
let path = location.strip_prefix("file://").unwrap_or(location);
let containing = self.transport.subtransport(path)?;
let base_bzr = containing.subtransport(".bzr")?;
let base = BzrDirMeta::open(base_bzr)?;
base.open_repository_stacked()
}
fn verify_component(
transport: &dyn Transport,
component: Component,
) -> Result<bool, BzrDirError> {
let format_path = format!("{}/format", component.subdir());
match transport.get_bytes(&format_path) {
Ok(found) => {
if component.format_is_supported(&found) {
Ok(true)
} else {
Err(BzrDirError::UnsupportedFormat { component, found })
}
}
Err(TransportError::NoSuchFile(_)) => Ok(false),
Err(e) => Err(e.into()),
}
}
fn repository_transport(&self) -> Result<SharedTransport, BzrDirError> {
Ok(self
.transport
.subtransport(Component::Repository.subdir())?)
}
}
fn same_location(a: &SharedTransport, b: &SharedTransport) -> bool {
match (a.local_path(""), b.local_path("")) {
(Some(pa), Some(pb)) => {
let ca = std::fs::canonicalize(&pa).unwrap_or(pa);
let cb = std::fs::canonicalize(&pb).unwrap_or(pb);
ca == cb
}
_ => match (a.abspath(""), b.abspath("")) {
(Ok(pa), Ok(pb)) => pa == pb,
_ => false,
},
}
}
impl ControlDir for BzrDirMeta {
fn transport(&self) -> &SharedTransport {
&self.transport
}
fn has_repository(&self) -> bool {
self.has_repository
}
fn has_branch(&self) -> bool {
self.has_branch
}
fn has_workingtree(&self) -> bool {
self.has_workingtree
}
fn open_repository(&self) -> Result<Box<dyn crate::repository::Repository>, BzrDirError> {
if !self.has_repository {
return Err(BzrDirError::NotABzrDir);
}
let sub = self
.transport
.subtransport(Component::Repository.subdir())?;
crate::repository::open(sub)
.map_err(|e| BzrDirError::Component(format!("opening repository: {e}")))
}
fn open_repository_stacked(
&self,
) -> Result<Box<dyn crate::repository::Repository>, BzrDirError> {
let repo = self.open_repository()?;
if !self.has_branch {
return Ok(repo);
}
let branch = self.open_branch()?;
let stacked_on = match branch.get_stacked_on_url() {
Ok(url) => url,
Err(crate::branch::BranchError::NotStacked)
| Err(crate::branch::BranchError::Unstackable) => return Ok(repo),
Err(e) => {
return Err(BzrDirError::Component(format!(
"reading stacked-on location: {e}"
)))
}
};
use crate::repository::Repository as _;
let base = self.open_stacked_on_repository(&stacked_on)?;
let mut stacked = crate::repository::StackedRepository::new(repo);
stacked
.add_fallback_repository(base)
.map_err(|e| BzrDirError::Component(format!("wiring fallback repository: {e}")))?;
Ok(Box::new(stacked))
}
fn open_branch(&self) -> Result<crate::branch::Branch, BzrDirError> {
if !self.has_branch {
return Err(BzrDirError::NotABzrDir);
}
let sub = self.transport.subtransport(Component::Branch.subdir())?;
let branch = crate::branch::Branch::new(sub);
match branch
.get_reference()
.map_err(|e| BzrDirError::Component(format!("reading branch reference: {e}")))?
{
Some(location) => self.open_referenced_branch(&location),
None => Ok(branch),
}
}
fn open_workingtree(&self) -> Result<Box<dyn crate::workingtree::WorkingTree>, BzrDirError> {
if !self.has_workingtree {
return Err(BzrDirError::NotABzrDir);
}
let root = self.transport.subtransport("..")?;
crate::workingtree::open(root)
.map_err(|e| BzrDirError::Component(format!("opening working tree: {e}")))
}
fn is_shared(&self) -> Result<bool, BzrDirError> {
if !self.has_repository {
return Err(BzrDirError::NotABzrDir);
}
Ok(self.repository_transport()?.has("shared-storage")?)
}
fn make_working_trees(&self) -> Result<bool, BzrDirError> {
if !self.has_repository {
return Err(BzrDirError::NotABzrDir);
}
Ok(!self.repository_transport()?.has("no-working-trees")?)
}
fn set_make_working_trees(&self, value: bool) -> Result<(), BzrDirError> {
if !self.has_repository {
return Err(BzrDirError::NotABzrDir);
}
let repo = self.repository_transport()?;
if value {
match repo.delete("no-working-trees") {
Ok(()) | Err(TransportError::NoSuchFile(_)) => Ok(()),
Err(e) => Err(e.into()),
}
} else {
repo.put_bytes("no-working-trees", b"", None)?;
Ok(())
}
}
fn find_repository(&self) -> Result<Box<dyn crate::repository::Repository>, BzrDirError> {
if self.has_repository {
return self.open_repository();
}
let mut dir = self.transport.subtransport("..")?;
loop {
let parent = dir.subtransport("..")?;
if same_location(&dir, &parent) {
return Err(BzrDirError::NoRepositoryPresent);
}
let next_bzr = parent.subtransport(".bzr")?;
match BzrDirMeta::open(next_bzr) {
Ok(found) if found.has_repository => {
if found.is_shared()? {
return found.open_repository();
}
return Err(BzrDirError::NoRepositoryPresent);
}
Ok(_) | Err(BzrDirError::NotABzrDir) | Err(BzrDirError::NotMetaDir(_)) => {
dir = parent;
}
Err(e) => return Err(e),
}
}
}
}
#[cfg(feature = "weave")]
pub struct BzrDirAllInOne {
transport: SharedTransport,
format: &'static crate::repository::RepositoryFormat,
}
#[cfg(feature = "weave")]
impl BzrDirAllInOne {
pub fn open(transport: SharedTransport) -> Result<Self, BzrDirError> {
let marker = match transport.get_bytes("branch-format") {
Ok(b) => b,
Err(TransportError::NoSuchFile(_)) => return Err(BzrDirError::NotABzrDir),
Err(e) => return Err(e.into()),
};
let format = crate::repository::find_format(&marker)
.filter(|f| f.is_all_in_one() && f.is_supported());
match format {
Some(format) => Ok(BzrDirAllInOne { transport, format }),
None => Err(BzrDirError::NotMetaDir(marker)),
}
}
pub fn create(parent: &SharedTransport) -> Result<Self, BzrDirError> {
let marker: &[u8] = b"Bazaar-NG branch, format 6\n";
let format = crate::repository::find_format(marker)
.filter(|f| f.is_all_in_one())
.ok_or_else(|| BzrDirError::Component("weave format 6 not registered".to_string()))?;
let bzr = parent.subtransport(".bzr")?;
bzr.mkdir("")?;
bzr.put_bytes("branch-format", marker, None)?;
bzr.put_bytes("revision-history", b"", None)?;
bzr.put_bytes("pending-merges", b"", None)?;
bzr.put_bytes(
"inventory",
b"<inventory format=\"5\">\n</inventory>\n",
None,
)?;
crate::repository::WeaveRepository::create(bzr.clone(), format)
.map_err(|e| BzrDirError::Component(format!("creating repository: {e}")))?;
Self::open(bzr)
}
}
#[cfg(feature = "weave")]
impl ControlDir for BzrDirAllInOne {
fn transport(&self) -> &SharedTransport {
&self.transport
}
fn has_repository(&self) -> bool {
true
}
fn has_branch(&self) -> bool {
true
}
fn has_workingtree(&self) -> bool {
true
}
fn open_repository(&self) -> Result<Box<dyn crate::repository::Repository>, BzrDirError> {
let repo = crate::repository::WeaveRepository::open(self.transport.clone(), self.format)
.map_err(|e| BzrDirError::Component(format!("opening repository: {e}")))?;
Ok(Box::new(repo))
}
fn open_branch(&self) -> Result<crate::branch::Branch, BzrDirError> {
let format = crate::branch::find_format(b"Bazaar-NG branch format 5\n")
.ok_or_else(|| BzrDirError::Component("branch format 5 not registered".to_string()))?;
Ok(crate::branch::Branch::with_format(
self.transport.clone(),
format,
))
}
fn open_workingtree(&self) -> Result<Box<dyn crate::workingtree::WorkingTree>, BzrDirError> {
let root = self.transport.subtransport("..")?;
let wt = crate::workingtree::WorkingTree3::open_all_in_one(root)
.map_err(|e| BzrDirError::Component(format!("opening working tree: {e}")))?;
Ok(Box::new(wt))
}
}
pub fn open(transport: SharedTransport) -> Result<Box<dyn ControlDir>, BzrDirError> {
let marker = match transport.get_bytes("branch-format") {
Ok(b) => b,
Err(TransportError::NoSuchFile(_)) => return Err(BzrDirError::NotABzrDir),
Err(e) => return Err(e.into()),
};
if marker == METADIR_MARKER {
return Ok(Box::new(BzrDirMeta::open(transport)?));
}
#[cfg(feature = "weave")]
{
Ok(Box::new(BzrDirAllInOne::open(transport)?))
}
#[cfg(not(feature = "weave"))]
{
Err(BzrDirError::NotMetaDir(marker))
}
}
pub fn upgrade(
parent: &SharedTransport,
target_format: &ControlDirFormat,
) -> Result<(), BzrDirError> {
let source_bzr = parent.subtransport(".bzr")?;
let source = BzrDirMeta::open(source_bzr)?;
if !source.has_repository || !source.has_branch {
return Err(BzrDirError::Component(
"can only upgrade a control directory with a branch and repository".to_string(),
));
}
let source_repo = source.open_repository()?;
if source_repo.format().format_string == target_format.repo_marker {
return Err(BzrDirError::Component(
"already in the target format".to_string(),
));
}
let source_branch = source.open_branch()?;
let (revno, tip) = source_branch
.last_revision_info()
.map_err(|e| BzrDirError::Component(format!("reading branch tip: {e}")))?;
let tags = source_branch
.tags()
.map_err(|e| BzrDirError::Component(format!("reading tags: {e}")))?;
let tmp_name = "upgrade.tmp";
let _ = parent.subtransport(tmp_name).and_then(|t| {
let _ = t.subtransport(".bzr").map(|b| delete_tree(b.as_ref()));
Ok(())
});
parent.mkdir(tmp_name)?;
let tmp = parent.subtransport(tmp_name)?;
let new = BzrDirMeta::create_with_format(&tmp, target_format)?;
{
let mut new_repo = new.open_repository()?;
crate::repository::fetch(source_repo.as_ref(), new_repo.as_mut(), None)
.map_err(|e| BzrDirError::Component(format!("fetching revisions: {e}")))?;
}
let new_branch = new.open_branch()?;
new_branch
.set_last_revision_info(revno, &tip)
.map_err(|e| BzrDirError::Component(format!("setting branch tip: {e}")))?;
if !tags.is_empty() {
new_branch
.set_tags(&tags)
.map_err(|e| BzrDirError::Component(format!("setting tags: {e}")))?;
}
if parent.has("backup.bzr")? {
return Err(BzrDirError::Component(
"backup.bzr already exists; remove it before upgrading".to_string(),
));
}
parent.rename(".bzr", "backup.bzr")?;
parent.rename(&format!("{tmp_name}/.bzr"), ".bzr")?;
parent.rmdir(tmp_name)?;
Ok(())
}
fn delete_tree(transport: &dyn Transport) -> Result<(), BzrDirError> {
let entries = match transport.list_dir("") {
Ok(e) => e,
Err(TransportError::NoSuchFile(_)) => return Ok(()),
Err(e) => return Err(e.into()),
};
for entry in entries {
match transport.delete(&entry) {
Ok(()) => {}
Err(_) => {
let sub = transport.subtransport(&entry)?;
delete_tree(sub.as_ref())?;
let _ = transport.rmdir(&entry);
}
}
}
Ok(())
}
fn empty_dirstate_bytes() -> Vec<u8> {
use crate::dirstate::{DefaultSHA1Provider, DirState};
let mut state = DirState::new("dirstate", Box::new(DefaultSHA1Provider), 0, true, false);
state.set_data(Vec::new(), DirState::empty_tree_dirblocks());
state.get_lines().concat()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::LocalTransport;
fn supported_marker(c: Component) -> &'static [u8] {
match c {
Component::Repository => REPOSITORY_FORMAT_2A,
Component::Branch => BRANCH_FORMAT_7,
Component::WorkingTree => WORKINGTREE_FORMAT_6,
}
}
fn make_bzrdir(root: &std::path::Path, with: &[Component]) {
let bzr = root.join(".bzr");
std::fs::create_dir_all(&bzr).unwrap();
std::fs::write(bzr.join("branch-format"), METADIR_MARKER).unwrap();
for &c in with {
let dir = bzr.join(c.subdir());
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("format"), supported_marker(c)).unwrap();
}
}
fn bzr_transport(root: &std::path::Path) -> SharedTransport {
std::sync::Arc::new(LocalTransport::new(root.join(".bzr")))
}
#[test]
fn format_registries_are_separate() {
let repo = REPOSITORY_FORMAT_2A;
let branch = BRANCH_FORMAT_7;
let wt = WORKINGTREE_FORMAT_6;
assert!(crate::repository::find_format(repo).is_some());
assert!(crate::repository::find_format(branch).is_none());
assert!(crate::repository::find_format(wt).is_none());
assert!(crate::branch::find_format(branch).is_some());
assert!(crate::branch::find_format(repo).is_none());
assert!(crate::branch::find_format(wt).is_none());
assert!(crate::workingtree::find_format(wt).is_some());
assert!(crate::workingtree::find_format(repo).is_none());
assert!(crate::workingtree::find_format(branch).is_none());
}
#[test]
fn opens_full_metadir() {
let dir = tempfile::tempdir().unwrap();
make_bzrdir(
dir.path(),
&[
Component::Repository,
Component::Branch,
Component::WorkingTree,
],
);
let t = bzr_transport(dir.path());
let bd = BzrDirMeta::open(t).unwrap();
assert!(bd.has_repository());
assert!(bd.has_branch());
assert!(bd.has_workingtree());
}
#[test]
fn opens_repository_only() {
let dir = tempfile::tempdir().unwrap();
make_bzrdir(dir.path(), &[Component::Repository]);
let t = bzr_transport(dir.path());
let bd = BzrDirMeta::open(t).unwrap();
assert!(bd.has_repository());
assert!(!bd.has_branch());
assert!(!bd.has_workingtree());
}
#[test]
fn missing_dir_is_not_a_bzrdir() {
let dir = tempfile::tempdir().unwrap();
let t = bzr_transport(dir.path());
match BzrDirMeta::open(t) {
Err(BzrDirError::NotABzrDir) => {}
other => panic!("expected NotABzrDir, got {:?}", other.err()),
}
}
#[test]
fn non_metadir_marker_rejected() {
let dir = tempfile::tempdir().unwrap();
let bzr = dir.path().join(".bzr");
std::fs::create_dir_all(&bzr).unwrap();
std::fs::write(bzr.join("branch-format"), b"Bazaar-NG branch, format 6\n").unwrap();
let t = bzr_transport(dir.path());
match BzrDirMeta::open(t) {
Err(BzrDirError::NotMetaDir(_)) => {}
other => panic!("expected NotMetaDir, got {:?}", other.err()),
}
}
#[test]
fn unsupported_repository_format_rejected() {
let dir = tempfile::tempdir().unwrap();
let bzr = dir.path().join(".bzr");
std::fs::create_dir_all(bzr.join("repository")).unwrap();
std::fs::write(bzr.join("branch-format"), METADIR_MARKER).unwrap();
std::fs::write(
bzr.join("repository/format"),
b"Bazaar pack repository format 1 (needs bzr 1.6)\n",
)
.unwrap();
let t = bzr_transport(dir.path());
match BzrDirMeta::open(t) {
Err(BzrDirError::UnsupportedFormat {
component: Component::Repository,
..
}) => {}
other => panic!(
"expected UnsupportedFormat(Repository), got {:?}",
other.err()
),
}
}
#[cfg(feature = "weave")]
const WEAVE_REVID: &[u8] = b"jelmer@jelmer.uk-20200101120000-jebv9gxg8ubhzbj8";
#[cfg(feature = "weave")]
const WEAVE_FILE_ID: &[u8] = b"a.txt-20260604015637-2c5ba92i40zw1mvp-1";
#[cfg(feature = "weave")]
const WEAVE_INVENTORY: &[u8] = b"# bzr weave file v5\ni\n1 8a002a6377d9177f17c988d81dda2e0175a18398\nn jelmer@jelmer.uk-20200101120000-jebv9gxg8ubhzbj8\n\nw\n{ 0\n. <inventory format=\"5\" revision_id=\"jelmer@jelmer.uk-20200101120000-jebv9gxg8ubhzbj8\">\n. <file file_id=\"a.txt-20260604015637-2c5ba92i40zw1mvp-1\" name=\"a.txt\" revision=\"jelmer@jelmer.uk-20200101120000-jebv9gxg8ubhzbj8\" text_sha1=\"55ca6286e3e4f4fba5d0448333fa99fc5a404a73\" text_size=\"3\" />\n. </inventory>\n}\nW\n";
#[cfg(feature = "weave")]
const WEAVE_REVISION: &[u8] = b"<revision committer=\"Jelmer Vernooij <jelmer@jelmer.uk>\" format=\"5\" inventory_sha1=\"8a002a6377d9177f17c988d81dda2e0175a18398\" revision_id=\"jelmer@jelmer.uk-20200101120000-jebv9gxg8ubhzbj8\" timestamp=\"1577880000.000\" timezone=\"0\">\n<message>one</message>\n<properties><property name=\"branch-nick\">wv</property>\n</properties>\n</revision>\n";
#[cfg(feature = "weave")]
const WEAVE_FILE_WEAVE: &[u8] = b"# bzr weave file v5\ni\n1 55ca6286e3e4f4fba5d0448333fa99fc5a404a73\nn jelmer@jelmer.uk-20200101120000-jebv9gxg8ubhzbj8\n\nw\n{ 0\n. hi\n}\nW\n";
#[cfg(feature = "weave")]
fn make_weave_bzrdir(root: &std::path::Path) {
use crate::key_mapper::{hash_prefix_map, url_unquote};
let bzr = root.join(".bzr");
std::fs::create_dir_all(&bzr).unwrap();
std::fs::write(bzr.join("branch-format"), b"Bazaar-NG branch, format 6\n").unwrap();
std::fs::write(bzr.join("inventory.weave"), WEAVE_INVENTORY).unwrap();
let mut history = WEAVE_REVID.to_vec();
history.push(b'\n');
std::fs::write(bzr.join("revision-history"), &history).unwrap();
std::fs::write(
bzr.join("inventory"),
format!(
"<inventory format=\"5\">\n<file file_id=\"{}\" name=\"a.txt\" />\n</inventory>\n",
std::str::from_utf8(WEAVE_FILE_ID).unwrap()
),
)
.unwrap();
std::fs::write(bzr.join("pending-merges"), b"").unwrap();
std::fs::write(root.join("a.txt"), b"hi\n").unwrap();
let rev_name = url_unquote(&hash_prefix_map(WEAVE_REVID));
let rev_path = bzr.join(format!("revision-store/{rev_name}"));
std::fs::create_dir_all(rev_path.parent().unwrap()).unwrap();
std::fs::write(rev_path, WEAVE_REVISION).unwrap();
let weave_name = url_unquote(&hash_prefix_map(WEAVE_FILE_ID));
let weave_path = bzr.join(format!("weaves/{weave_name}.weave"));
std::fs::create_dir_all(weave_path.parent().unwrap()).unwrap();
std::fs::write(weave_path, WEAVE_FILE_WEAVE).unwrap();
}
#[cfg(feature = "weave")]
#[test]
fn opens_all_in_one_weave() {
let dir = tempfile::tempdir().unwrap();
make_weave_bzrdir(dir.path());
let cd = open(bzr_transport(dir.path())).unwrap();
assert!(cd.has_repository());
assert!(cd.has_branch());
assert!(cd.has_workingtree());
let repo = cd.open_repository().unwrap();
assert_eq!(repo.all_revision_ids().unwrap(), vec![WEAVE_REVID.to_vec()]);
let rev = repo.get_revision(WEAVE_REVID).unwrap();
assert_eq!(rev.message, "one");
let inv = repo.get_inventory(WEAVE_REVID).unwrap();
let a_txt = inv
.entries()
.unwrap()
.into_iter()
.find(|(path, _)| path == "a.txt")
.expect("a.txt in inventory");
assert_eq!(a_txt.1.file_id().as_bytes(), WEAVE_FILE_ID);
let text = repo.get_file_text(WEAVE_FILE_ID, WEAVE_REVID).unwrap();
assert_eq!(text, b"hi\n".to_vec());
let branch = cd.open_branch().unwrap();
assert_eq!(
branch.last_revision_info().unwrap(),
(1, WEAVE_REVID.to_vec())
);
let wt = cd.open_workingtree().unwrap();
assert_eq!(wt.basis_revision().as_deref(), Some(WEAVE_REVID));
let files = wt.list_files();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, "a.txt");
assert_eq!(files[0].file_id, WEAVE_FILE_ID.to_vec());
assert_eq!(wt.path2id("a.txt").as_deref(), Some(WEAVE_FILE_ID));
}
#[test]
fn open_branch_follows_reference() {
let dir = tempfile::tempdir().unwrap();
let target_root = dir.path().join("target");
std::fs::create_dir_all(&target_root).unwrap();
let target_parent: SharedTransport = std::sync::Arc::new(LocalTransport::new(&target_root));
let target = BzrDirMeta::create(&target_parent).unwrap();
target
.open_branch()
.unwrap()
.set_last_revision_info(3, b"rev-real")
.unwrap();
let ref_root = dir.path().join("ref");
let ref_bzr = ref_root.join(".bzr");
std::fs::create_dir_all(ref_bzr.join("branch")).unwrap();
std::fs::write(ref_bzr.join("branch-format"), METADIR_MARKER).unwrap();
std::fs::write(
ref_bzr.join("branch/format"),
b"Bazaar-NG Branch Reference Format 1\n",
)
.unwrap();
std::fs::write(
ref_bzr.join("branch/location"),
target_root.to_str().unwrap().as_bytes(),
)
.unwrap();
let ref_bzr_transport: SharedTransport = std::sync::Arc::new(LocalTransport::new(&ref_bzr));
let cd = BzrDirMeta::open(ref_bzr_transport).unwrap();
let branch = cd.open_branch().unwrap();
assert_eq!(
branch.last_revision_info().unwrap(),
(3, b"rev-real".to_vec())
);
}
#[test]
fn open_repository_stacked_resolves_from_base() {
use crate::inventory::ROOT_ID;
let dir = tempfile::tempdir().unwrap();
let base_root = dir.path().join("base");
std::fs::create_dir_all(&base_root).unwrap();
let base_parent: SharedTransport = std::sync::Arc::new(LocalTransport::new(&base_root));
let base = BzrDirMeta::create(&base_parent).unwrap();
{
let mut repo = base.open_repository().unwrap();
repo.start_write_group().unwrap();
let rev = crate::revision::Revision::new(
crate::RevisionId::from(&b"rev-base"[..]),
vec![],
Some("T <t@e>".to_string()),
"base commit".to_string(),
std::collections::HashMap::new(),
None,
1577880000.0,
Some(0),
);
repo.add_revision(&rev, &[]).unwrap();
let entries = vec![crate::inventory::Entry::root(
crate::FileId::from(ROOT_ID),
Some(crate::RevisionId::from(&b"rev-base"[..])),
)];
repo.add_inventory_from_entries(b"rev-base", &[], ROOT_ID, &entries)
.unwrap();
repo.commit_write_group().unwrap();
}
let top_root = dir.path().join("top");
std::fs::create_dir_all(&top_root).unwrap();
let top_parent: SharedTransport = std::sync::Arc::new(LocalTransport::new(&top_root));
let top = BzrDirMeta::create(&top_parent).unwrap();
top.open_branch()
.unwrap()
.set_stacked_on_url(Some(base_root.to_str().unwrap()))
.unwrap();
assert!(!top
.open_repository()
.unwrap()
.has_revision(b"rev-base")
.unwrap());
let stacked = top.open_repository_stacked().unwrap();
assert!(stacked.has_revision(b"rev-base").unwrap());
assert_eq!(
stacked.get_revision(b"rev-base").unwrap().message,
"base commit"
);
}
#[test]
fn create_shared_repository_is_shared() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = std::sync::Arc::new(LocalTransport::new(dir.path()));
let cd = BzrDirMeta::create_shared_repository(&parent).unwrap();
assert!(cd.has_repository());
assert!(!cd.has_branch());
assert!(cd.is_shared().unwrap());
let other = tempfile::tempdir().unwrap();
let op: SharedTransport = std::sync::Arc::new(LocalTransport::new(other.path()));
assert!(!BzrDirMeta::create(&op).unwrap().is_shared().unwrap());
}
#[cfg(feature = "knitpack")]
#[test]
fn create_shared_repository_knitpack_format() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = std::sync::Arc::new(LocalTransport::new(dir.path()));
let fmt = find_control_dir_format("1.9").expect("1.9 format registered");
let cd = BzrDirMeta::create_shared_repository_with_format(&parent, fmt).unwrap();
assert!(cd.has_repository());
assert!(!cd.has_branch());
assert!(cd.is_shared().unwrap());
assert_eq!(
cd.open_repository().unwrap().format().format_string,
fmt.repo_marker
);
}
#[test]
fn make_working_trees_toggles_marker() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = std::sync::Arc::new(LocalTransport::new(dir.path()));
let cd = BzrDirMeta::create_shared_repository(&parent).unwrap();
assert!(cd.make_working_trees().unwrap());
cd.set_make_working_trees(false).unwrap();
assert!(!cd.make_working_trees().unwrap());
cd.set_make_working_trees(true).unwrap();
assert!(cd.make_working_trees().unwrap());
}
#[test]
fn find_repository_walks_up_to_shared() {
let dir = tempfile::tempdir().unwrap();
let shared_root = dir.path().join("shared");
std::fs::create_dir_all(&shared_root).unwrap();
let shared_parent: SharedTransport = std::sync::Arc::new(LocalTransport::new(&shared_root));
let shared = BzrDirMeta::create_shared_repository(&shared_parent).unwrap();
{
let mut repo = shared.open_repository().unwrap();
repo.start_write_group().unwrap();
let rev = crate::revision::Revision::new(
crate::RevisionId::from(&b"rev-shared"[..]),
vec![],
Some("T <t@e>".to_string()),
"shared".to_string(),
std::collections::HashMap::new(),
None,
1577880000.0,
Some(0),
);
repo.add_revision(&rev, &[]).unwrap();
repo.add_inventory_from_entries(
b"rev-shared",
&[],
crate::inventory::ROOT_ID,
&[crate::inventory::Entry::root(
crate::FileId::from(crate::inventory::ROOT_ID),
Some(crate::RevisionId::from(&b"rev-shared"[..])),
)],
)
.unwrap();
repo.commit_write_group().unwrap();
}
let branch_root = shared_root.join("branch1");
let branch_bzr = branch_root.join(".bzr");
std::fs::create_dir_all(branch_bzr.join("branch")).unwrap();
std::fs::write(branch_bzr.join("branch-format"), METADIR_MARKER).unwrap();
std::fs::write(branch_bzr.join("branch/format"), BRANCH_FORMAT_7).unwrap();
std::fs::write(branch_bzr.join("branch/last-revision"), b"0 null:\n").unwrap();
let branch_cd =
BzrDirMeta::open(std::sync::Arc::new(LocalTransport::new(&branch_bzr))).unwrap();
assert!(!branch_cd.has_repository());
let repo = branch_cd.find_repository().unwrap();
assert!(repo.has_revision(b"rev-shared").unwrap());
}
#[test]
fn find_repository_errors_when_none() {
let dir = tempfile::tempdir().unwrap();
let branch_root = dir.path().join("lonely");
let branch_bzr = branch_root.join(".bzr");
std::fs::create_dir_all(branch_bzr.join("branch")).unwrap();
std::fs::write(branch_bzr.join("branch-format"), METADIR_MARKER).unwrap();
std::fs::write(branch_bzr.join("branch/format"), BRANCH_FORMAT_7).unwrap();
std::fs::write(branch_bzr.join("branch/last-revision"), b"0 null:\n").unwrap();
let cd = BzrDirMeta::open(std::sync::Arc::new(LocalTransport::new(&branch_bzr))).unwrap();
assert!(matches!(
cd.find_repository(),
Err(BzrDirError::NoRepositoryPresent)
));
}
#[cfg(feature = "knitpack")]
#[test]
fn upgrade_knitpack_to_2a() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = std::sync::Arc::new(LocalTransport::new(dir.path()));
let knitpack = find_control_dir_format("1.9").expect("1.9 format registered");
let cd = BzrDirMeta::create_with_format(&parent, knitpack).unwrap();
let revid;
{
let mut repo = cd.open_repository().unwrap();
let root = crate::inventory::ROOT_ID;
revid = b"rev-1".to_vec();
repo.start_write_group().unwrap();
repo.add_text(b"file-1", &revid, &[], b"hi\n").unwrap();
let entries = vec![
crate::inventory::Entry::root(
crate::FileId::from(root),
Some(crate::RevisionId::from(revid.as_slice())),
),
crate::inventory::Entry::file(
crate::FileId::from(&b"file-1"[..]),
"a.txt".into(),
crate::FileId::from(root),
Some(crate::RevisionId::from(revid.as_slice())),
Some(crate::weave::sha_strings(&[b"hi\n"])),
Some(3),
Some(false),
None,
),
];
repo.add_inventory_from_entries(&revid, &[], root, &entries)
.unwrap();
let rev = crate::revision::Revision::new(
crate::RevisionId::from(revid.as_slice()),
vec![],
Some("T <t@e>".to_string()),
"msg".to_string(),
std::collections::HashMap::new(),
None,
1577880000.0,
Some(0),
);
repo.add_revision(&rev, &[]).unwrap();
repo.commit_write_group().unwrap();
}
cd.open_branch()
.unwrap()
.set_last_revision_info(1, &revid)
.unwrap();
let mut tags = std::collections::BTreeMap::new();
tags.insert("v1".to_string(), revid.clone());
cd.open_branch().unwrap().set_tags(&tags).unwrap();
let target = find_control_dir_format("2a").unwrap();
upgrade(&parent, target).unwrap();
assert!(parent.has("backup.bzr").unwrap());
let upgraded = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
assert_eq!(
upgraded.open_repository().unwrap().format().format_string,
target.repo_marker
);
let repo = upgraded.open_repository().unwrap();
assert!(repo.has_revision(&revid).unwrap());
assert_eq!(repo.get_file_text(b"file-1", &revid).unwrap(), b"hi\n");
let branch = upgraded.open_branch().unwrap();
assert_eq!(branch.last_revision_info().unwrap(), (1, revid.clone()));
assert_eq!(branch.tags().unwrap().get("v1"), Some(&revid));
}
}