pub mod format;
#[cfg(any(feature = "weave", feature = "knit"))]
mod wt3;
pub use format::{all_formats, find_format, WorkingTreeFormat};
#[cfg(any(feature = "weave", feature = "knit"))]
pub use wt3::WorkingTree3;
use crate::declare_workingtree_format;
use crate::dirstate::{DefaultSHA1Provider, DirState, Kind, LoadError};
use crate::transport::{SharedTransport, TransportError};
#[cfg(any(feature = "weave", feature = "knit"))]
declare_workingtree_format! {
FORMAT_3 {
format_string: b"Bazaar-NG Working Tree format 3",
description: "Working tree format 3 (pre-dirstate)",
deprecated: true,
supported: true,
}
}
declare_workingtree_format! {
FORMAT_4 {
format_string: b"Bazaar Working Tree Format 4 (bzr 0.15)\n",
description: "Working tree format 4 (dirstate)",
uses_dirstate: true,
supported: true,
}
}
declare_workingtree_format! {
FORMAT_5 {
format_string: b"Bazaar Working Tree Format 5 (bzr 1.11)\n",
description: "Working tree format 5 (dirstate, content filtering)",
uses_dirstate: true,
supports_content_filtering: true,
supported: true,
}
}
declare_workingtree_format! {
FORMAT_6 {
format_string: b"Bazaar Working Tree Format 6 (bzr 1.14)\n",
description: "Working tree format 6 (dirstate, views, content filtering)",
uses_dirstate: true,
supports_content_filtering: true,
supports_views: true,
supported: true,
}
}
const DIRSTATE_PATH: &str = ".bzr/checkout/dirstate";
#[derive(Debug)]
pub enum WorkingTreeError {
Dirstate(LoadError),
NotVersioned(String),
Add(crate::dirstate::AddError),
Remove(crate::dirstate::MakeAbsentError),
Commit(String),
PointlessCommit,
StrictCommitFailed(Vec<String>),
CannotCommitSelectedFileMerge,
Repository(crate::repository::RepositoryError),
Branch(crate::branch::BranchError),
Transport(TransportError),
UnsupportedFormat(Vec<u8>),
Corrupt(String),
Unsupported(String),
}
impl std::fmt::Display for WorkingTreeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WorkingTreeError::Dirstate(e) => write!(f, "dirstate: {e}"),
WorkingTreeError::NotVersioned(p) => write!(f, "path not versioned: {p}"),
WorkingTreeError::Add(e) => write!(f, "add: {e}"),
WorkingTreeError::Remove(e) => write!(f, "remove: {e}"),
WorkingTreeError::Commit(m) => write!(f, "commit: {m}"),
WorkingTreeError::PointlessCommit => {
write!(f, "no changes to commit (use allow_pointless to override)")
}
WorkingTreeError::StrictCommitFailed(unknowns) => {
write!(
f,
"strict commit failed: {} unversioned file(s) present",
unknowns.len()
)
}
WorkingTreeError::CannotCommitSelectedFileMerge => {
write!(f, "cannot commit selected files with pending merges")
}
WorkingTreeError::Repository(e) => write!(f, "repository: {e}"),
WorkingTreeError::Branch(e) => write!(f, "branch: {e}"),
WorkingTreeError::Transport(e) => write!(f, "transport error: {e}"),
WorkingTreeError::UnsupportedFormat(marker) => write!(
f,
"unsupported working-tree format: {}",
String::from_utf8_lossy(marker)
),
WorkingTreeError::Corrupt(m) => write!(f, "corrupt working-tree control file: {m}"),
WorkingTreeError::Unsupported(m) => write!(f, "unsupported operation: {m}"),
}
}
}
impl std::error::Error for WorkingTreeError {}
impl From<LoadError> for WorkingTreeError {
fn from(e: LoadError) -> Self {
WorkingTreeError::Dirstate(e)
}
}
impl From<TransportError> for WorkingTreeError {
fn from(e: TransportError) -> Self {
WorkingTreeError::Transport(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryKind {
File,
Directory,
Symlink,
TreeReference,
}
impl EntryKind {
fn from_minikind(k: Kind) -> Option<Self> {
match k {
Kind::File => Some(EntryKind::File),
Kind::Directory => Some(EntryKind::Directory),
Kind::Symlink => Some(EntryKind::Symlink),
Kind::TreeReference => Some(EntryKind::TreeReference),
Kind::Absent | Kind::Relocated => None,
}
}
fn to_osutils_kind(self) -> crate::osutils::Kind {
match self {
EntryKind::File => crate::osutils::Kind::File,
EntryKind::Directory => crate::osutils::Kind::Directory,
EntryKind::Symlink => crate::osutils::Kind::Symlink,
EntryKind::TreeReference => crate::osutils::Kind::TreeReference,
}
}
fn from_inventory_kind(k: crate::osutils::Kind) -> Option<Self> {
match k {
crate::osutils::Kind::File => Some(EntryKind::File),
crate::osutils::Kind::Directory => Some(EntryKind::Directory),
crate::osutils::Kind::Symlink => Some(EntryKind::Symlink),
crate::osutils::Kind::TreeReference => Some(EntryKind::TreeReference),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionedEntry {
pub path: String,
pub file_id: Vec<u8>,
pub kind: EntryKind,
}
#[derive(Debug, Clone)]
pub struct WorkingTreeChange {
pub file_id: Vec<u8>,
pub old_path: Option<String>,
pub new_path: Option<String>,
pub content_change: bool,
pub new_name: Option<String>,
pub new_parent_id: Option<Vec<u8>>,
pub new_kind: Option<EntryKind>,
pub new_executable: bool,
pub basis_revision: Option<Vec<u8>>,
pub text_parents: Vec<Vec<u8>>,
}
#[derive(Debug, Clone, Default)]
pub struct CommitOptions {
pub message: String,
pub committer: String,
pub authors: Vec<String>,
pub timestamp: u64,
pub timezone: i32,
pub revprops: std::collections::HashMap<String, Vec<u8>>,
pub revision_id: Option<Vec<u8>>,
pub branch_nick: Option<String>,
pub allow_pointless: bool,
pub strict: bool,
pub specific_files: Vec<String>,
pub exclude: Vec<String>,
pub signing_key: Option<Vec<u8>>,
}
impl CommitOptions {
pub fn new(committer: impl Into<String>, message: impl Into<String>) -> Self {
CommitOptions {
message: message.into(),
committer: committer.into(),
..Default::default()
}
}
pub fn timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
pub fn timezone(mut self, timezone: i32) -> Self {
self.timezone = timezone;
self
}
pub fn authors(mut self, authors: Vec<String>) -> Self {
self.authors = authors;
self
}
pub fn revprops(mut self, revprops: std::collections::HashMap<String, Vec<u8>>) -> Self {
self.revprops = revprops;
self
}
pub fn revision_id(mut self, revision_id: Vec<u8>) -> Self {
self.revision_id = Some(revision_id);
self
}
pub fn branch_nick(mut self, nick: impl Into<String>) -> Self {
self.branch_nick = Some(nick.into());
self
}
pub fn allow_pointless(mut self, allow: bool) -> Self {
self.allow_pointless = allow;
self
}
pub fn strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
pub fn specific_files(mut self, files: Vec<String>) -> Self {
self.specific_files = files;
self
}
pub fn exclude(mut self, exclude: Vec<String>) -> Self {
self.exclude = exclude;
self
}
pub fn signing_key(mut self, key: Vec<u8>) -> Self {
self.signing_key = Some(key);
self
}
fn build_properties(
&self,
) -> Result<std::collections::HashMap<String, Vec<u8>>, WorkingTreeError> {
let mut props = self.revprops.clone();
if !self.authors.is_empty() {
let key = if self.authors.len() == 1 {
"author"
} else {
"authors"
};
props.insert(key.to_string(), self.authors.join("\n").into_bytes());
}
if let Some(nick) = &self.branch_nick {
props
.entry("branch-nick".to_string())
.or_insert_with(|| nick.clone().into_bytes());
}
for (k, v) in &props {
if v.contains(&b'\r') {
return Err(WorkingTreeError::Commit(format!(
"revision property {k:?} contains a carriage return"
)));
}
}
Ok(props)
}
}
pub trait WorkingTree: Send + Sync {
fn basis_revision(&self) -> Option<Vec<u8>>;
fn parent_ids(&self) -> Vec<Vec<u8>>;
fn add_pending_merge(&mut self, revision_id: &[u8]) -> Result<(), WorkingTreeError>;
fn list_files(&self) -> Vec<VersionedEntry>;
fn path2id(&self, path: &str) -> Option<Vec<u8>>;
fn get_file_text(&self, path: &str) -> Result<Vec<u8>, WorkingTreeError>;
fn unknowns(&self) -> Result<Vec<String>, WorkingTreeError>;
fn iter_changes(
&self,
basis: &crate::repository::RevisionTree,
) -> Result<Vec<WorkingTreeChange>, WorkingTreeError>;
fn iter_changes_with_parents(
&self,
basis: &crate::repository::RevisionTree,
other_parents: &[crate::repository::RevisionTree],
) -> Result<Vec<WorkingTreeChange>, WorkingTreeError>;
fn add(
&mut self,
path: &str,
kind: EntryKind,
file_id: Option<&[u8]>,
) -> Result<Vec<u8>, WorkingTreeError>;
fn remove(&mut self, path: &str) -> Result<(), WorkingTreeError>;
fn rename(&mut self, from_path: &str, to_path: &str) -> Result<(), WorkingTreeError>;
fn commit(
&mut self,
repository: &mut dyn crate::repository::Repository,
branch: &crate::branch::Branch,
options: &CommitOptions,
) -> Result<Vec<u8>, WorkingTreeError>;
fn control_transport(&self) -> &SharedTransport;
fn supports_views(&self) -> bool {
false
}
fn views(&self) -> Result<ViewInfo, WorkingTreeError> {
let bytes = match self.control_transport().get_bytes(VIEWS_PATH) {
Ok(b) => b,
Err(TransportError::NoSuchFile(_)) => return Ok(ViewInfo::default()),
Err(e) => return Err(e.into()),
};
ViewInfo::deserialize(&bytes).map_err(|e| WorkingTreeError::Corrupt(e.to_string()))
}
fn set_views(&self, info: &ViewInfo) -> Result<(), WorkingTreeError> {
if !self.supports_views() {
return Err(WorkingTreeError::Unsupported(
"this working-tree format does not support views".to_string(),
));
}
if let Some(current) = &info.current {
if !info.views.contains_key(current) {
return Err(WorkingTreeError::Corrupt(format!(
"current view {current:?} is not a defined view"
)));
}
}
self.control_transport()
.put_bytes(VIEWS_PATH, &info.serialize(), None)?;
Ok(())
}
fn conflicts(&self) -> Result<Vec<Conflict>, WorkingTreeError> {
let bytes = match self.control_transport().get_bytes(CONFLICTS_PATH) {
Ok(b) => b,
Err(TransportError::NoSuchFile(_)) => return Ok(Vec::new()),
Err(e) => return Err(e.into()),
};
conflicts_io::deserialize(&bytes).map_err(WorkingTreeError::Corrupt)
}
fn set_conflicts(&self, conflicts: &[Conflict]) -> Result<(), WorkingTreeError> {
self.control_transport().put_bytes(
CONFLICTS_PATH,
&conflicts_io::serialize(conflicts),
None,
)?;
Ok(())
}
}
const VIEWS_PATH: &str = ".bzr/checkout/views";
const CONFLICTS_PATH: &str = ".bzr/checkout/conflicts";
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ViewInfo {
pub current: Option<String>,
pub views: crate::views::ViewDict,
}
const CURRENT_KEYWORD: &str = "current";
impl ViewInfo {
pub fn serialize(&self) -> Vec<u8> {
if self.current.is_none() && self.views.is_empty() {
return Vec::new();
}
let mut keywords = crate::views::Keywords::new();
if let Some(current) = &self.current {
keywords.insert(CURRENT_KEYWORD.to_string(), current.clone());
}
crate::views::serialize(&keywords, &self.views)
}
pub fn deserialize(content: &[u8]) -> Result<Self, crate::views::ViewsError> {
let (keywords, views) = crate::views::deserialize(content)?;
Ok(ViewInfo {
current: keywords.get(CURRENT_KEYWORD).cloned(),
views,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Conflict {
pub typestring: String,
pub path: String,
pub file_id: Option<Vec<u8>>,
}
pub fn open(transport: SharedTransport) -> Result<Box<dyn WorkingTree>, WorkingTreeError> {
match transport.get_bytes(".bzr/checkout/format") {
Ok(marker) => match find_format(&marker) {
Some(fmt) if fmt.uses_dirstate => Ok(Box::new(WorkingTree4::open(transport)?)),
#[cfg(any(feature = "weave", feature = "knit"))]
Some(fmt) if fmt.format_string == FORMAT_3_MARKER => {
Ok(Box::new(WorkingTree3::open(transport)?))
}
_ => Err(WorkingTreeError::UnsupportedFormat(marker)),
},
Err(TransportError::NoSuchFile(_)) => Ok(Box::new(WorkingTree4::open(transport)?)),
Err(e) => Err(WorkingTreeError::Transport(e)),
}
}
#[cfg(any(feature = "weave", feature = "knit"))]
const FORMAT_3_MARKER: &[u8] = b"Bazaar-NG Working Tree format 3";
pub struct WorkingTree4 {
transport: SharedTransport,
dirstate: DirState,
}
impl WorkingTree4 {
pub fn open(transport: SharedTransport) -> Result<Self, WorkingTreeError> {
let data = transport.get_bytes(DIRSTATE_PATH)?;
let mut dirstate =
DirState::new(DIRSTATE_PATH, Box::new(DefaultSHA1Provider), 0, true, false);
dirstate.load_bytes(&data)?;
Ok(WorkingTree4 {
transport,
dirstate,
})
}
pub fn basis_revision(&self) -> Option<Vec<u8>> {
self.dirstate.parents.first().cloned()
}
pub fn parent_ids(&self) -> Vec<Vec<u8>> {
self.dirstate
.parents
.iter()
.filter(|p| p.as_slice() != crate::branch::NULL_REVISION)
.cloned()
.collect()
}
pub fn add_pending_merge(&mut self, revision_id: &[u8]) -> Result<(), WorkingTreeError> {
let mut parents = self.parent_ids();
if parents.iter().any(|p| p == revision_id) {
return Ok(());
}
parents.push(revision_id.to_vec());
let basis_entries = self.basis_tree_entries();
let mut per_parent: Vec<Vec<(Vec<u8>, Vec<u8>, crate::dirstate::TreeData)>> =
vec![basis_entries];
for _ in 1..parents.len() {
per_parent.push(Vec::new());
}
self.dirstate
.set_parent_trees(parents, Vec::new(), per_parent)
.map_err(|e| WorkingTreeError::Commit(format!("set parents: {e:?}")))?;
self.save_dirstate()
}
fn basis_tree_entries(&self) -> Vec<(Vec<u8>, Vec<u8>, crate::dirstate::TreeData)> {
let mut out = Vec::new();
for entry in self.dirstate.iter_entries() {
let td = match entry.trees.get(1) {
Some(t) if !matches!(t.minikind, Kind::Absent | Kind::Relocated) => t.clone(),
_ => continue,
};
let path = join_path(&entry.key.dirname, &entry.key.basename);
out.push((path.into_bytes(), entry.key.file_id.clone(), td));
}
out
}
pub fn list_files(&self) -> Vec<VersionedEntry> {
let mut out = Vec::new();
for entry in self.dirstate.iter_entries() {
let kind = match entry
.trees
.first()
.and_then(|t| EntryKind::from_minikind(t.minikind))
{
Some(k) => k,
None => continue,
};
let path = join_path(&entry.key.dirname, &entry.key.basename);
if path.is_empty() {
continue;
}
out.push(VersionedEntry {
path,
file_id: entry.key.file_id.clone(),
kind,
});
}
out
}
pub fn path2id(&self, path: &str) -> Option<Vec<u8>> {
let (dirname, basename) = split_path(path);
for entry in self.dirstate.iter_entries() {
if entry.key.dirname == dirname.as_bytes() && entry.key.basename == basename.as_bytes()
{
if let Some(t) = entry.trees.first() {
if EntryKind::from_minikind(t.minikind).is_some() {
return Some(entry.key.file_id.clone());
}
}
}
}
None
}
pub fn get_file_text(&self, path: &str) -> Result<Vec<u8>, WorkingTreeError> {
if self.path2id(path).is_none() {
return Err(WorkingTreeError::NotVersioned(path.to_string()));
}
Ok(self.transport.get_bytes(path)?)
}
pub fn unknowns(&self) -> Result<Vec<String>, WorkingTreeError> {
let tracked: std::collections::HashSet<String> =
self.list_files().into_iter().map(|e| e.path).collect();
let mut unknowns = Vec::new();
let mut dirs = vec![String::new()];
while let Some(dir) = dirs.pop() {
let names = match self.transport.list_dir(&dir) {
Ok(n) => n,
Err(TransportError::NoSuchFile(_)) => continue,
Err(e) => return Err(e.into()),
};
for name in names {
if dir.is_empty() && name == ".bzr" {
continue;
}
let path = if dir.is_empty() {
name.clone()
} else {
format!("{dir}/{name}")
};
if tracked.contains(&path) {
if let Ok(st) = self.transport.stat(&path) {
if st.is_dir {
dirs.push(path);
}
}
} else {
unknowns.push(path);
}
}
}
unknowns.sort();
Ok(unknowns)
}
pub fn iter_changes(
&self,
basis: &crate::repository::RevisionTree,
) -> Result<Vec<WorkingTreeChange>, WorkingTreeError> {
self.iter_changes_with_parents(basis, &[])
}
pub fn iter_changes_with_parents(
&self,
basis: &crate::repository::RevisionTree,
other_parents: &[crate::repository::RevisionTree],
) -> Result<Vec<WorkingTreeChange>, WorkingTreeError> {
compute_changes(
&self.transport,
&read_and_hash(&self.transport),
&self.collect_live_entries(),
basis,
other_parents,
)
}
pub fn add(
&mut self,
path: &str,
kind: EntryKind,
file_id: Option<&[u8]>,
) -> Result<Vec<u8>, WorkingTreeError> {
let path = path.trim_matches('/');
if let Some(existing) = self.path2id(path) {
return Ok(existing);
}
let file_id = match file_id {
Some(id) => id.to_vec(),
None => crate::gen_ids::gen_file_id(path),
};
self.dirstate
.add_path(path, &file_id, kind.to_osutils_kind(), None, b"")
.map_err(WorkingTreeError::Add)?;
self.save_dirstate()?;
Ok(file_id)
}
pub fn remove(&mut self, path: &str) -> Result<(), WorkingTreeError> {
let path = path.trim_matches('/');
if self.path2id(path).is_none() {
return Err(WorkingTreeError::NotVersioned(path.to_string()));
}
let prefix = format!("{path}/");
let mut keys: Vec<crate::dirstate::EntryKey> = Vec::new();
for entry in self.dirstate.iter_entries() {
let tree0 = match entry.trees.first() {
Some(t) => t,
None => continue,
};
if EntryKind::from_minikind(tree0.minikind).is_none() {
continue; }
let entry_path = join_path(&entry.key.dirname, &entry.key.basename);
if entry_path == path || entry_path.starts_with(&prefix) {
keys.push(entry.key.clone());
}
}
keys.sort_by_key(|k| std::cmp::Reverse(k.dirname.len()));
for key in &keys {
self.dirstate
.make_absent(key)
.map_err(WorkingTreeError::Remove)?;
}
self.save_dirstate()
}
pub fn rename(&mut self, from_path: &str, to_path: &str) -> Result<(), WorkingTreeError> {
let from_path = from_path.trim_matches('/');
let to_path = to_path.trim_matches('/');
let file_id = self
.path2id(from_path)
.ok_or_else(|| WorkingTreeError::NotVersioned(from_path.to_string()))?;
if self.path2id(to_path).is_some() {
return Err(WorkingTreeError::Commit(format!(
"destination already versioned: {to_path}"
)));
}
let (kind, from_key) = self
.dirstate
.iter_entries()
.find_map(|e| {
let path = join_path(&e.key.dirname, &e.key.basename);
if path != from_path {
return None;
}
let k = EntryKind::from_minikind(e.trees.first()?.minikind)?;
Some((k, e.key.clone()))
})
.ok_or_else(|| WorkingTreeError::NotVersioned(from_path.to_string()))?;
if kind == EntryKind::Directory {
let child_prefix = format!("{from_path}/");
if self
.dirstate
.iter_entries()
.any(|e| join_path(&e.key.dirname, &e.key.basename).starts_with(&child_prefix))
{
return Err(WorkingTreeError::Commit(
"moving a directory with versioned children is not supported".to_string(),
));
}
}
self.dirstate
.make_absent(&from_key)
.map_err(WorkingTreeError::Remove)?;
self.dirstate
.add_path(to_path, &file_id, kind.to_osutils_kind(), None, b"")
.map_err(WorkingTreeError::Add)?;
self.transport
.rename(from_path, to_path)
.map_err(WorkingTreeError::Transport)?;
self.save_dirstate()
}
pub fn commit(
&mut self,
repository: &mut dyn crate::repository::Repository,
branch: &crate::branch::Branch,
options: &CommitOptions,
) -> Result<Vec<u8>, WorkingTreeError> {
if options.strict {
let unknowns = self.unknowns()?;
if !unknowns.is_empty() {
return Err(WorkingTreeError::StrictCommitFailed(unknowns));
}
}
let parents: Vec<Vec<u8>> = self
.dirstate
.parents
.iter()
.filter(|p| p.as_slice() != crate::branch::NULL_REVISION)
.cloned()
.collect();
let revid = match &options.revision_id {
Some(id) => id.clone(),
None => crate::RevisionId::generate(&options.committer, Some(options.timestamp))
.as_bytes()
.to_vec(),
};
let properties = options.build_properties()?;
let basis_revision_id = parents
.first()
.cloned()
.unwrap_or_else(|| crate::branch::NULL_REVISION.to_vec());
let selective = !options.specific_files.is_empty() || !options.exclude.is_empty();
if selective && parents.len() > 1 {
return Err(WorkingTreeError::CannotCommitSelectedFileMerge);
}
let basis = repository
.revision_tree(&basis_revision_id)
.map_err(WorkingTreeError::Repository)?;
let other_parents: Vec<crate::repository::RevisionTree> = parents
.iter()
.skip(1)
.map(|p| repository.revision_tree(p))
.collect::<Result<_, _>>()
.map_err(WorkingTreeError::Repository)?;
let mut changes = self.iter_changes_with_parents(&basis, &other_parents)?;
if selective {
changes.retain(|c| change_selected(c, &options.specific_files, &options.exclude));
}
if !options.allow_pointless && parents.len() <= 1 {
let basis_is_null = basis_revision_id == crate::branch::NULL_REVISION;
let pointless = if basis_is_null {
changes.len() <= 1
} else {
changes.is_empty()
};
if pointless {
return Err(WorkingTreeError::PointlessCommit);
}
}
repository
.start_write_group()
.map_err(WorkingTreeError::Repository)?;
{
let mut builder = repository
.get_commit_builder(
parents.clone(),
revid.clone(),
options.committer.clone(),
options.timestamp,
options.timezone,
)
.with_properties(properties.clone());
builder
.record_iter_changes(&changes, |path| {
self.transport
.get_bytes(path)
.map_err(crate::repository::RepositoryError::Transport)
})
.map_err(WorkingTreeError::Repository)?;
builder
.finish_inventory()
.map_err(WorkingTreeError::Repository)?;
builder
.commit(&options.message)
.map_err(WorkingTreeError::Repository)?;
}
if let Some(key) = &options.signing_key {
let (paths, inv_entries) = self.build_committed_entries(&revid, &basis, &changes)?;
let signature = sign_commit(
&parents,
&revid,
options,
&properties,
&paths,
&inv_entries,
key,
)?;
repository
.add_signature_text(&revid, &signature)
.map_err(WorkingTreeError::Repository)?;
}
repository
.commit_write_group()
.map_err(WorkingTreeError::Repository)?;
let deleted_paths: Vec<String> = changes
.iter()
.filter(|c| c.new_path.is_none())
.filter_map(|c| c.old_path.clone())
.filter(|p| self.path2id(p).is_some())
.collect();
for path in &deleted_paths {
self.remove(path)?;
}
let new_revno = self.dirstate_revno() + 1;
branch
.set_last_revision_info(new_revno, &revid)
.map_err(WorkingTreeError::Branch)?;
self.update_basis_from_changes(&revid, &basis, &changes)?;
Ok(revid)
}
fn update_basis_from_changes(
&mut self,
revid: &[u8],
basis: &crate::repository::RevisionTree,
changes: &[WorkingTreeChange],
) -> Result<(), WorkingTreeError> {
let (paths, inv_entries) = self.build_committed_entries(revid, basis, changes)?;
self.update_basis(revid, &paths, &inv_entries)
}
fn build_committed_entries(
&self,
revid: &[u8],
basis: &crate::repository::RevisionTree,
changes: &[WorkingTreeChange],
) -> Result<(Vec<String>, Vec<crate::inventory::Entry>), WorkingTreeError> {
build_committed_entries(
&self.transport,
&self.collect_live_entries(),
revid,
basis,
changes,
)
}
fn update_basis(
&mut self,
revid: &[u8],
paths: &[String],
inv_entries: &[crate::inventory::Entry],
) -> Result<(), WorkingTreeError> {
use crate::dirstate::{inv_entry_to_details, TreeData};
if self.transport.local_path(DIRSTATE_PATH).is_none() {
return Ok(()); }
let parent_entries: Vec<(Vec<u8>, Vec<u8>, TreeData)> = paths
.iter()
.zip(inv_entries)
.map(|(path, entry)| {
let (minikind, fingerprint, size, executable, _rev) = inv_entry_to_details(entry);
let td = TreeData {
minikind,
fingerprint,
size,
executable,
packed_stat: crate::dirstate::NULLSTAT.to_vec(),
};
(
path.clone().into_bytes(),
entry.file_id().as_bytes().to_vec(),
td,
)
})
.collect();
self.dirstate
.set_parent_trees(vec![revid.to_vec()], Vec::new(), vec![parent_entries])
.map_err(|e| WorkingTreeError::Commit(format!("set basis: {e:?}")))?;
self.save_dirstate()
}
fn save_dirstate(&mut self) -> Result<(), WorkingTreeError> {
use crate::dirstate::{FileTransport, Transport as DirstateTransport};
let dirstate_path = match self.transport.local_path(DIRSTATE_PATH) {
Some(p) => p,
None => return Ok(()),
};
let mut ft = FileTransport::new(&dirstate_path);
ft.lock_write()
.map_err(|e| WorkingTreeError::Commit(format!("lock dirstate: {e:?}")))?;
self.dirstate.mark_modified(&[], true);
self.dirstate
.save_to(&mut ft)
.map_err(|e| WorkingTreeError::Commit(format!("save dirstate: {e:?}")))?;
ft.unlock()
.map_err(|e| WorkingTreeError::Commit(format!("unlock dirstate: {e:?}")))?;
Ok(())
}
fn dirstate_revno(&self) -> u64 {
if self.dirstate.parents.is_empty()
|| self
.dirstate
.parents
.iter()
.all(|p| p.as_slice() == crate::branch::NULL_REVISION)
{
0
} else {
self.dirstate.parents.len() as u64
}
}
fn collect_live_entries(&self) -> LiveEntries {
let mut entries = Vec::new();
let mut root_id = crate::inventory::ROOT_ID.to_vec();
for entry in self.dirstate.iter_entries() {
let tree0 = match entry.trees.first() {
Some(t) => t,
None => continue,
};
let kind = match EntryKind::from_minikind(tree0.minikind) {
Some(k) => k,
None => continue,
};
let path = join_path(&entry.key.dirname, &entry.key.basename);
if path.is_empty() {
root_id = entry.key.file_id.clone();
continue;
}
entries.push(LiveEntry {
path,
file_id: entry.key.file_id.clone(),
kind,
executable: tree0.executable,
symlink_target: if kind == EntryKind::Symlink {
tree0.fingerprint.clone()
} else {
Vec::new()
},
});
}
LiveEntries { root_id, entries }
}
}
impl WorkingTree for WorkingTree4 {
fn basis_revision(&self) -> Option<Vec<u8>> {
WorkingTree4::basis_revision(self)
}
fn parent_ids(&self) -> Vec<Vec<u8>> {
WorkingTree4::parent_ids(self)
}
fn add_pending_merge(&mut self, revision_id: &[u8]) -> Result<(), WorkingTreeError> {
WorkingTree4::add_pending_merge(self, revision_id)
}
fn list_files(&self) -> Vec<VersionedEntry> {
WorkingTree4::list_files(self)
}
fn path2id(&self, path: &str) -> Option<Vec<u8>> {
WorkingTree4::path2id(self, path)
}
fn get_file_text(&self, path: &str) -> Result<Vec<u8>, WorkingTreeError> {
WorkingTree4::get_file_text(self, path)
}
fn unknowns(&self) -> Result<Vec<String>, WorkingTreeError> {
WorkingTree4::unknowns(self)
}
fn iter_changes(
&self,
basis: &crate::repository::RevisionTree,
) -> Result<Vec<WorkingTreeChange>, WorkingTreeError> {
WorkingTree4::iter_changes(self, basis)
}
fn iter_changes_with_parents(
&self,
basis: &crate::repository::RevisionTree,
other_parents: &[crate::repository::RevisionTree],
) -> Result<Vec<WorkingTreeChange>, WorkingTreeError> {
WorkingTree4::iter_changes_with_parents(self, basis, other_parents)
}
fn add(
&mut self,
path: &str,
kind: EntryKind,
file_id: Option<&[u8]>,
) -> Result<Vec<u8>, WorkingTreeError> {
WorkingTree4::add(self, path, kind, file_id)
}
fn remove(&mut self, path: &str) -> Result<(), WorkingTreeError> {
WorkingTree4::remove(self, path)
}
fn rename(&mut self, from_path: &str, to_path: &str) -> Result<(), WorkingTreeError> {
WorkingTree4::rename(self, from_path, to_path)
}
fn commit(
&mut self,
repository: &mut dyn crate::repository::Repository,
branch: &crate::branch::Branch,
options: &CommitOptions,
) -> Result<Vec<u8>, WorkingTreeError> {
WorkingTree4::commit(self, repository, branch, options)
}
fn control_transport(&self) -> &SharedTransport {
&self.transport
}
fn supports_views(&self) -> bool {
match self.transport.get_bytes(".bzr/checkout/format") {
Ok(marker) => find_format(&marker)
.map(|f| f.supports_views)
.unwrap_or(false),
Err(_) => false,
}
}
}
struct LiveEntry {
path: String,
file_id: Vec<u8>,
kind: EntryKind,
executable: bool,
symlink_target: Vec<u8>,
}
struct LiveEntries {
root_id: Vec<u8>,
entries: Vec<LiveEntry>,
}
fn join_path(dirname: &[u8], basename: &[u8]) -> String {
let dir = String::from_utf8_lossy(dirname);
let base = String::from_utf8_lossy(basename);
if dir.is_empty() {
base.into_owned()
} else {
format!("{dir}/{base}")
}
}
fn split_path(path: &str) -> (String, String) {
match path.rsplit_once('/') {
Some((dir, base)) => (dir.to_string(), base.to_string()),
None => (String::new(), path.to_string()),
}
}
fn path_is_within(path: &str, prefix: &str) -> bool {
if prefix.is_empty() {
return true;
}
path == prefix || path.starts_with(&format!("{prefix}/"))
}
fn change_selected(
change: &WorkingTreeChange,
specific_files: &[String],
exclude: &[String],
) -> bool {
let path = match change.new_path.as_deref().or(change.old_path.as_deref()) {
Some(p) => p,
None => return false,
};
if path.is_empty() {
return true; }
if exclude.iter().any(|e| path_is_within(path, e)) {
return false;
}
if specific_files.is_empty() {
return true;
}
specific_files.iter().any(|f| path_is_within(path, f))
}
fn compute_changes(
transport: &SharedTransport,
file_sha: &FileSha<'_>,
live: &LiveEntries,
basis: &crate::repository::RevisionTree,
other_parents: &[crate::repository::RevisionTree],
) -> Result<Vec<WorkingTreeChange>, WorkingTreeError> {
use crate::FileId;
let mut changes = Vec::new();
let mut seen: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
let root_fid = FileId::from(live.root_id.as_slice());
seen.insert(live.root_id.clone());
if basis
.get_entry(&root_fid)
.map_err(|e| WorkingTreeError::Commit(format!("reading basis inventory: {e:?}")))?
.is_none()
{
changes.push(WorkingTreeChange {
file_id: live.root_id.clone(),
old_path: None,
new_path: Some(String::new()),
content_change: false,
new_name: Some(String::new()),
new_parent_id: None,
new_kind: Some(EntryKind::Directory),
new_executable: false,
basis_revision: None,
text_parents: Vec::new(),
});
}
for e in &live.entries {
seen.insert(e.file_id.clone());
let fid = FileId::from(e.file_id.as_slice());
let basis_entry = basis
.get_entry(&fid)
.map_err(|e| WorkingTreeError::Commit(format!("reading basis inventory: {e:?}")))?;
let old_path = basis
.id2path(&fid)
.map_err(|e| WorkingTreeError::Commit(format!("reading basis inventory: {e:?}")))?;
let basis_revision = basis_entry
.as_ref()
.and_then(|be| be.revision().map(|r| r.as_bytes().to_vec()));
if matches!(e.kind, EntryKind::File | EntryKind::Symlink) && !transport.has(&e.path)? {
if old_path.is_some() {
changes.push(WorkingTreeChange {
file_id: e.file_id.clone(),
old_path,
new_path: None,
content_change: false,
new_name: None,
new_parent_id: None,
new_kind: None,
new_executable: false,
basis_revision,
text_parents: Vec::new(),
});
}
continue;
}
let content_change = content_changed(file_sha, e, basis_entry.as_ref())?;
let new_parent_id = parent_id_of(&e.path, live);
let new_name = basename(&e.path).to_string();
let meta_change = match &basis_entry {
None => true, Some(be) => {
let kind_changed = match EntryKind::from_inventory_kind(be.kind()) {
Some(k) => k != e.kind,
None => true,
};
be.name() != new_name
|| be.parent_id().map(|p| p.as_bytes()) != Some(new_parent_id.as_slice())
|| kind_changed
|| be.executable() != e.executable
}
};
let moved = old_path.as_deref() != Some(e.path.as_str());
let text_parents = text_parents_for(&fid, basis, other_parents)?;
let unchanged_merged = !other_parents.is_empty() && text_parents.len() > 1;
if content_change || meta_change || moved || unchanged_merged {
changes.push(WorkingTreeChange {
file_id: e.file_id.clone(),
old_path,
new_path: Some(e.path.clone()),
content_change,
new_name: Some(new_name),
new_parent_id: Some(new_parent_id),
new_kind: Some(e.kind),
new_executable: e.executable,
basis_revision,
text_parents,
});
}
}
for fid in basis
.inventory()
.all_file_ids()
.map_err(|e| WorkingTreeError::Commit(format!("reading basis inventory: {e:?}")))?
{
if seen.contains(fid.as_bytes()) {
continue;
}
let old_path = match basis
.id2path(&fid)
.map_err(|e| WorkingTreeError::Commit(format!("reading basis inventory: {e:?}")))?
{
Some(p) if !p.is_empty() => p, _ => continue,
};
let basis_revision = basis
.get_entry(&fid)
.map_err(|e| WorkingTreeError::Commit(format!("reading basis inventory: {e:?}")))?
.and_then(|be| be.revision().map(|r| r.as_bytes().to_vec()));
changes.push(WorkingTreeChange {
file_id: fid.as_bytes().to_vec(),
old_path: Some(old_path),
new_path: None,
content_change: false,
new_name: None,
new_parent_id: None,
new_kind: None,
new_executable: false,
basis_revision,
text_parents: Vec::new(),
});
}
Ok(changes)
}
fn text_parents_for(
file_id: &crate::FileId,
basis: &crate::repository::RevisionTree,
other_parents: &[crate::repository::RevisionTree],
) -> Result<Vec<Vec<u8>>, WorkingTreeError> {
let mut parents = Vec::new();
let mut seen = std::collections::HashSet::new();
for tree in std::iter::once(basis).chain(other_parents.iter()) {
if let Some(rev) = tree
.get_file_revision(file_id)
.map_err(|e| WorkingTreeError::Commit(format!("reading basis inventory: {e:?}")))?
{
if seen.insert(rev.clone()) {
parents.push(rev);
}
}
}
Ok(parents)
}
fn content_changed(
file_sha: &FileSha<'_>,
entry: &LiveEntry,
basis_entry: Option<&crate::inventory::Entry>,
) -> Result<bool, WorkingTreeError> {
let basis_entry = match basis_entry {
None => return Ok(true),
Some(be) => be,
};
match entry.kind {
EntryKind::File => {
let sha1 = file_sha(&entry.path)?;
Ok(basis_entry.text_sha1() != Some(sha1.as_slice()))
}
EntryKind::Symlink => Ok(basis_entry.symlink_target().map(|s| s.as_bytes())
!= Some(entry.symlink_target.as_slice())),
EntryKind::Directory | EntryKind::TreeReference => Ok(false),
}
}
type FileSha<'a> = dyn Fn(&str) -> Result<Vec<u8>, WorkingTreeError> + 'a;
fn read_and_hash<'a>(
transport: &'a SharedTransport,
) -> impl Fn(&str) -> Result<Vec<u8>, WorkingTreeError> + 'a {
move |path| {
let content = transport.get_bytes(path)?;
Ok(crate::weave::sha_strings(&[content.as_slice()]))
}
}
fn parent_id_of(path: &str, live: &LiveEntries) -> Vec<u8> {
match path.rsplit_once('/') {
None => live.root_id.clone(),
Some((parent, _)) => live
.entries
.iter()
.find(|e| e.path == parent)
.map(|e| e.file_id.clone())
.unwrap_or_else(|| live.root_id.clone()),
}
}
fn basename(path: &str) -> &str {
match path.rsplit_once('/') {
Some((_, base)) => base,
None => path,
}
}
fn build_committed_entries(
transport: &SharedTransport,
live: &LiveEntries,
revid: &[u8],
basis: &crate::repository::RevisionTree,
changes: &[WorkingTreeChange],
) -> Result<(Vec<String>, Vec<crate::inventory::Entry>), WorkingTreeError> {
use crate::FileId;
let mut new_rev_ids: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
for c in changes {
let merges_parents = c.text_parents.len() > 1;
if c.new_path.is_some()
&& (c.content_change || c.basis_revision.is_none() || merges_parents)
{
new_rev_ids.insert(c.file_id.clone());
}
}
let mut path_to_id: std::collections::HashMap<String, Vec<u8>> =
std::collections::HashMap::new();
path_to_id.insert(String::new(), live.root_id.clone());
for e in &live.entries {
path_to_id.insert(e.path.clone(), e.file_id.clone());
}
let root_fid = FileId::from(live.root_id.as_slice());
let root_rev = match basis
.get_file_revision(&root_fid)
.map_err(|e| WorkingTreeError::Commit(format!("reading basis inventory: {e:?}")))?
{
Some(r) => crate::RevisionId::from(r.as_slice()),
None => crate::RevisionId::from(revid),
};
let mut paths: Vec<String> = vec![String::new()];
let mut inv_entries = vec![crate::inventory::Entry::root(root_fid, Some(root_rev))];
for e in &live.entries {
let (parent_path, name) = split_path(&e.path);
let parent_id = path_to_id
.get(&parent_path)
.ok_or_else(|| WorkingTreeError::Commit(format!("no parent for {}", e.path)))?;
let parent_fid = FileId::from(parent_id.as_slice());
let fid = FileId::from(e.file_id.as_slice());
let entry_rev = if new_rev_ids.contains(&e.file_id) {
crate::RevisionId::from(revid)
} else {
match basis
.get_file_revision(&fid)
.map_err(|e| WorkingTreeError::Commit(format!("reading basis inventory: {e:?}")))?
{
Some(r) => crate::RevisionId::from(r.as_slice()),
None => crate::RevisionId::from(revid),
}
};
paths.push(e.path.clone());
inv_entries.push(match e.kind {
EntryKind::Directory => {
crate::inventory::Entry::directory(fid, name, parent_fid, Some(entry_rev))
}
EntryKind::File => {
let recorded_at_new = new_rev_ids.contains(&e.file_id);
let (sha1, size) = if recorded_at_new {
let content = transport.get_bytes(&e.path)?;
let sha1 = crate::weave::sha_strings(&[content.as_slice()]);
(sha1, content.len() as u64)
} else {
match basis.get_entry(&fid).map_err(|e| {
WorkingTreeError::Commit(format!("reading basis inventory: {e:?}"))
})? {
Some(be) => (
be.text_sha1().map(|s| s.to_vec()).unwrap_or_default(),
be.text_size().unwrap_or(0),
),
None => {
let content = transport.get_bytes(&e.path)?;
let sha1 = crate::weave::sha_strings(&[content.as_slice()]);
(sha1, content.len() as u64)
}
}
};
crate::inventory::Entry::file(
fid,
name,
parent_fid,
Some(entry_rev),
Some(sha1),
Some(size),
Some(e.executable),
None,
)
}
EntryKind::Symlink => {
let target = String::from_utf8_lossy(&e.symlink_target).into_owned();
crate::inventory::Entry::link(fid, name, parent_fid, Some(entry_rev), Some(target))
}
EntryKind::TreeReference => {
return Err(WorkingTreeError::Commit(
"tree references are not supported".to_string(),
))
}
});
}
Ok((paths, inv_entries))
}
#[allow(clippy::too_many_arguments)]
fn sign_commit(
parents: &[Vec<u8>],
revid: &[u8],
options: &CommitOptions,
properties: &std::collections::HashMap<String, Vec<u8>>,
paths: &[String],
inv_entries: &[crate::inventory::Entry],
signing_key: &[u8],
) -> Result<Vec<u8>, WorkingTreeError> {
#[cfg(not(feature = "gpg"))]
{
let _ = (
parents,
revid,
options,
properties,
paths,
inv_entries,
signing_key,
);
Err(WorkingTreeError::Commit(
"commit signing requires the crate's `gpg` feature".to_string(),
))
}
#[cfg(feature = "gpg")]
{
use crate::testament::{EntryKind as TKind, Testament, TestamentEntry, TestamentFormat};
let revprops: std::collections::BTreeMap<String, String> = properties
.iter()
.map(|(k, v)| (k.clone(), String::from_utf8_lossy(v).into_owned()))
.collect();
let mut entries = Vec::new();
for (path, entry) in paths.iter().zip(inv_entries) {
if path.is_empty() {
continue; }
let (kind, content) = match entry {
crate::inventory::Entry::File { text_sha1, .. } => {
(TKind::File, text_sha1.clone().unwrap_or_default())
}
crate::inventory::Entry::Directory { .. } => (TKind::Directory, Vec::new()),
crate::inventory::Entry::Link { symlink_target, .. } => (
TKind::Symlink,
symlink_target.clone().unwrap_or_default().into_bytes(),
),
crate::inventory::Entry::TreeReference { .. } => (TKind::TreeReference, Vec::new()),
crate::inventory::Entry::Root { .. } => continue,
};
entries.push(TestamentEntry {
path: path.clone(),
kind,
file_id: entry.file_id().as_bytes().to_vec(),
content,
revision: entry
.revision()
.map(|r| r.as_bytes().to_vec())
.unwrap_or_default(),
executable: entry.executable(),
});
}
let testament = Testament {
revision_id: revid.to_vec(),
committer: options.committer.clone(),
timestamp: options.timestamp as i64,
timezone: options.timezone,
message: options.message.clone(),
parent_ids: parents
.iter()
.filter(|p| p.as_slice() != crate::branch::NULL_REVISION)
.cloned()
.collect(),
revprops,
entries,
};
let short = testament
.as_short_text(TestamentFormat::Strict3)
.map_err(|e| WorkingTreeError::Commit(format!("testament: {e:?}")))?;
crate::gpg::clearsign(&short, signing_key)
.map_err(|e| WorkingTreeError::Commit(format!("sign: {e}")))
}
}
mod conflicts_io {
use super::Conflict;
use crate::rio::{read_stanzas, rio_iter, Stanza, StanzaValue};
const HEADER: &[u8] = b"BZR conflict list format 1";
const HEADER_LINE: &[u8] = b"BZR conflict list format 1\n";
pub(super) fn serialize(conflicts: &[Conflict]) -> Vec<u8> {
let stanzas = conflicts.iter().map(|c| {
let mut s = Stanza::new();
let _ = s.add(
"type".to_string(),
StanzaValue::String(c.typestring.clone()),
);
let _ = s.add("path".to_string(), StanzaValue::String(c.path.clone()));
if let Some(fid) = &c.file_id {
let _ = s.add(
"file_id".to_string(),
StanzaValue::String(String::from_utf8_lossy(fid).into_owned()),
);
}
s
});
rio_iter(stanzas, Some(HEADER.to_vec())).flatten().collect()
}
pub(super) fn deserialize(bytes: &[u8]) -> Result<Vec<Conflict>, String> {
let rest = bytes
.strip_prefix(HEADER_LINE)
.ok_or_else(|| "missing 'BZR conflict list format 1' header".to_string())?;
let mut reader = std::io::BufReader::new(rest);
let stanzas = read_stanzas(&mut reader).map_err(|e| format!("conflicts rio: {e:?}"))?;
let mut out = Vec::new();
for stanza in stanzas {
let get = |tag: &str| match stanza.get(tag) {
Some(StanzaValue::String(s)) => Some(s.clone()),
_ => None,
};
let typestring =
get("type").ok_or_else(|| "conflict stanza missing 'type'".to_string())?;
let path = get("path").ok_or_else(|| "conflict stanza missing 'path'".to_string())?;
let file_id = get("file_id").map(|s| s.into_bytes());
out.push(Conflict {
typestring,
path,
file_id,
});
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bzrdir::{BzrDirMeta, ControlDir};
use crate::transport::{LocalTransport, SharedTransport};
use std::sync::Arc;
#[test]
fn split_and_join_round_trip() {
for p in ["a.txt", "sub/b.txt", "a/b/c"] {
let (d, b) = split_path(p);
assert_eq!(join_path(d.as_bytes(), b.as_bytes()), p);
}
}
#[test]
fn create_and_commit_empty_tree() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let cd = BzrDirMeta::create(&parent).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let mut wt = cd.open_workingtree().unwrap();
let revid = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "empty commit")
.timestamp(1577880000)
.allow_pointless(true),
)
.unwrap();
assert_eq!(wt.basis_revision().as_deref(), Some(revid.as_slice()));
let reread = WorkingTree4::open(parent.clone()).unwrap();
assert_eq!(reread.basis_revision().as_deref(), Some(revid.as_slice()));
let reopened = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
let branch = reopened.open_branch().unwrap();
assert_eq!(branch.last_revision_info().unwrap(), (1, revid.clone()));
let repo = reopened.open_repository().unwrap();
let rev = repo.get_revision(&revid).unwrap();
assert_eq!(rev.message, "empty commit");
let inv = repo.get_inventory(&revid).unwrap();
assert!(inv.entries().unwrap().is_empty());
}
#[cfg(feature = "weave")]
#[test]
fn weave_all_in_one_create_commit_read() {
use crate::bzrdir::BzrDirAllInOne;
use crate::transport::Transport;
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let cd = BzrDirAllInOne::create(&parent).unwrap();
parent.put_bytes("a.txt", b"hi\n", None).unwrap();
let mut wt = cd.open_workingtree().unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let revid = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "one").timestamp(1577880000),
)
.unwrap();
let reopened = BzrDirAllInOne::open(parent.subtransport(".bzr").unwrap()).unwrap();
let branch = reopened.open_branch().unwrap();
assert_eq!(branch.last_revision_info().unwrap(), (1, revid.clone()));
let repo = reopened.open_repository().unwrap();
assert_eq!(repo.all_revision_ids().unwrap(), vec![revid.clone()]);
assert_eq!(repo.get_revision(&revid).unwrap().message, "one");
let inv = repo.get_inventory(&revid).unwrap();
let paths: Vec<String> = inv.entries().unwrap().into_iter().map(|(p, _)| p).collect();
assert_eq!(paths, vec!["a.txt".to_string()]);
let file_id = wt.path2id("a.txt").unwrap();
assert_eq!(repo.get_file_text(&file_id, &revid).unwrap(), b"hi\n");
}
#[cfg(feature = "weave")]
#[test]
fn weave_disk_layout_escapes_file_id() {
use crate::bzrdir::BzrDirAllInOne;
use crate::transport::Transport;
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let cd = BzrDirAllInOne::create(&parent).unwrap();
parent.put_bytes("foo", b"content\n", None).unwrap();
let mut wt = cd.open_workingtree().unwrap();
wt.add("foo", EntryKind::File, Some(b"Foo:Bar")).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
wt.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "first post")
.timestamp(1577880000)
.revision_id(b"first".to_vec()),
)
.unwrap();
let weave = parent
.get_bytes(".bzr/weaves/74/Foo:Bar.weave")
.expect("weave at the unescaped path");
assert_eq!(
weave,
b"# bzr weave file v5\n\
i\n\
1 7fe70820e08a1aac0ef224d9c66ab66831cc4ab1\n\
n first\n\
\n\
w\n\
{ 0\n\
. content\n\
}\n\
W\n"
);
assert_eq!(
repo.get_file_text(b"Foo:Bar", b"first").unwrap(),
b"content\n"
);
}
fn fresh_tree() -> (tempfile::TempDir, SharedTransport, WorkingTree4) {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let cd = BzrDirMeta::create(&parent).unwrap();
let wt = WorkingTree4::open(parent.clone()).unwrap();
let _ = cd;
(dir, parent, wt)
}
#[test]
fn add_versions_a_path_and_persists() {
let (_d, parent, mut wt) = fresh_tree();
parent.put_bytes("a.txt", b"hello\n", None).unwrap();
let file_id = wt.add("a.txt", EntryKind::File, None).unwrap();
assert_eq!(wt.path2id("a.txt"), Some(file_id.clone()));
assert_eq!(
wt.list_files(),
vec![VersionedEntry {
path: "a.txt".to_string(),
file_id: file_id.clone(),
kind: EntryKind::File,
}]
);
let reread = WorkingTree4::open(parent.clone()).unwrap();
assert_eq!(reread.path2id("a.txt"), Some(file_id));
}
#[test]
fn add_is_idempotent_and_honours_explicit_id() {
let (_d, parent, mut wt) = fresh_tree();
parent.put_bytes("a.txt", b"x\n", None).unwrap();
let id = wt.add("a.txt", EntryKind::File, Some(b"my-id")).unwrap();
assert_eq!(id, b"my-id".to_vec());
let id2 = wt.add("a.txt", EntryKind::File, Some(b"other")).unwrap();
assert_eq!(id2, b"my-id".to_vec());
}
#[test]
fn add_unversioned_parent_is_rejected() {
let (_d, parent, mut wt) = fresh_tree();
parent.mkdir("sub").unwrap();
parent.put_bytes("sub/a.txt", b"a\n", None).unwrap();
assert!(wt.add("sub/a.txt", EntryKind::File, None).is_err());
}
#[test]
fn add_pending_merge_appends_and_is_idempotent() {
let (_d, _parent, mut wt) = fresh_tree();
assert!(wt.parent_ids().is_empty());
wt.add_pending_merge(b"foo@a-1").unwrap();
assert_eq!(wt.parent_ids(), vec![b"foo@a-1".to_vec()]);
wt.add_pending_merge(b"foo@a-1").unwrap();
assert_eq!(wt.parent_ids(), vec![b"foo@a-1".to_vec()]);
wt.add_pending_merge(b"wibble@b-2").unwrap();
assert_eq!(
wt.parent_ids(),
vec![b"foo@a-1".to_vec(), b"wibble@b-2".to_vec()]
);
}
#[test]
fn remove_unversions_directory_and_children() {
let (_d, parent, mut wt) = fresh_tree();
parent.mkdir("sub").unwrap();
parent.put_bytes("sub/a.txt", b"a\n", None).unwrap();
parent.put_bytes("keep.txt", b"k\n", None).unwrap();
wt.add("sub", EntryKind::Directory, None).unwrap();
wt.add("sub/a.txt", EntryKind::File, None).unwrap();
wt.add("keep.txt", EntryKind::File, None).unwrap();
wt.remove("sub").unwrap();
assert_eq!(wt.path2id("sub"), None);
assert_eq!(wt.path2id("sub/a.txt"), None);
assert!(wt.path2id("keep.txt").is_some());
assert!(parent.has("sub/a.txt").unwrap());
assert!(matches!(
wt.remove("sub"),
Err(WorkingTreeError::NotVersioned(_))
));
}
#[test]
fn rename_moves_entry_and_keeps_file_id() {
let (_d, parent, mut wt) = fresh_tree();
parent.put_bytes("a.txt", b"hello\n", None).unwrap();
let id = wt.add("a.txt", EntryKind::File, None).unwrap();
wt.rename("a.txt", "b.txt").unwrap();
assert_eq!(wt.path2id("a.txt"), None);
assert_eq!(wt.path2id("b.txt"), Some(id));
assert!(!parent.has("a.txt").unwrap());
assert_eq!(wt.get_file_text("b.txt").unwrap(), b"hello\n");
}
#[test]
fn rename_unversioned_source_is_rejected() {
let (_d, parent, mut wt) = fresh_tree();
parent.put_bytes("a.txt", b"hello\n", None).unwrap();
assert!(matches!(
wt.rename("a.txt", "b.txt"),
Err(WorkingTreeError::NotVersioned(_))
));
}
#[test]
fn rename_onto_versioned_destination_is_rejected() {
let (_d, parent, mut wt) = fresh_tree();
parent.put_bytes("a.txt", b"a\n", None).unwrap();
parent.put_bytes("b.txt", b"b\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
wt.add("b.txt", EntryKind::File, None).unwrap();
assert!(wt.rename("a.txt", "b.txt").is_err());
assert!(wt.path2id("a.txt").is_some());
assert!(wt.path2id("b.txt").is_some());
}
#[test]
fn add_then_commit_records_the_files() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"hello\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let revid = wt
.commit(
repo.as_mut(),
&branch,
&crate::workingtree::CommitOptions::new("T <t@e>", "add a").timestamp(1577880000),
)
.unwrap();
let reopened = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
let repo = reopened.open_repository().unwrap();
let inv = repo.get_inventory(&revid).unwrap();
let paths: Vec<String> = inv.entries().unwrap().into_iter().map(|(p, _)| p).collect();
assert_eq!(paths, vec!["a.txt".to_string()]);
let file_id = wt.path2id("a.txt").unwrap();
assert_eq!(repo.get_file_text(&file_id, &revid).unwrap(), b"hello\n");
}
#[test]
fn iter_changes_reports_only_differences() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"hello\n", None).unwrap();
parent.put_bytes("b.txt", b"world\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
wt.add("b.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let revid = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "two files").timestamp(1577880000),
)
.unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
parent.put_bytes("a.txt", b"changed\n", None).unwrap();
parent.put_bytes("c.txt", b"new\n", None).unwrap();
wt.add("c.txt", EntryKind::File, None).unwrap();
let repo = cd.open_repository().unwrap();
let basis = repo.revision_tree(&revid).unwrap();
let mut changes = wt.iter_changes(&basis).unwrap();
changes.sort_by(|x, y| x.new_path.cmp(&y.new_path));
let summary: Vec<(Option<String>, Option<String>, bool)> = changes
.iter()
.map(|c| (c.old_path.clone(), c.new_path.clone(), c.content_change))
.collect();
assert_eq!(
summary,
vec![
(Some("a.txt".to_string()), Some("a.txt".to_string()), true),
(None, Some("c.txt".to_string()), true),
]
);
}
#[test]
fn iter_changes_reports_removals() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"hello\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let revid = wt
.commit(
repo.as_mut(),
&branch,
&crate::workingtree::CommitOptions::new("T <t@e>", "add a").timestamp(1577880000),
)
.unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
wt.remove("a.txt").unwrap();
let repo = cd.open_repository().unwrap();
let basis = repo.revision_tree(&revid).unwrap();
let changes = wt.iter_changes(&basis).unwrap();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].old_path.as_deref(), Some("a.txt"));
assert_eq!(changes[0].new_path, None);
}
#[test]
fn second_commit_is_incremental() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"a one\n", None).unwrap();
parent.put_bytes("b.txt", b"b one\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
wt.add("b.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev1 = wt
.commit(
repo.as_mut(),
&branch,
&crate::workingtree::CommitOptions::new("T <t@e>", "first").timestamp(1577880000),
)
.unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
parent.put_bytes("a.txt", b"a two\n", None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev2 = wt
.commit(
repo.as_mut(),
&branch,
&crate::workingtree::CommitOptions::new("T <t@e>", "second").timestamp(1577890000),
)
.unwrap();
assert_ne!(rev1, rev2);
let repo = cd.open_repository().unwrap();
let inv = repo.get_inventory(&rev2).unwrap();
let mut paths: Vec<String> = inv.entries().unwrap().into_iter().map(|(p, _)| p).collect();
paths.sort();
assert_eq!(paths, vec!["a.txt".to_string(), "b.txt".to_string()]);
let a_id = wt.path2id("a.txt").unwrap();
let b_id = wt.path2id("b.txt").unwrap();
let a_entry = inv
.get_entry(&crate::FileId::from(a_id.as_slice()))
.unwrap()
.unwrap();
let b_entry = inv
.get_entry(&crate::FileId::from(b_id.as_slice()))
.unwrap()
.unwrap();
assert_eq!(a_entry.revision().unwrap().as_bytes(), rev2.as_slice());
assert_eq!(b_entry.revision().unwrap().as_bytes(), rev1.as_slice());
assert_eq!(repo.get_file_text(&a_id, &rev2).unwrap(), b"a two\n");
assert!(repo.get_file_text(&b_id, &rev2).is_err());
assert_eq!(repo.get_file_text(&b_id, &rev1).unwrap(), b"b one\n");
}
#[test]
fn commit_records_revprops_authors_and_revid() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"hi\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let mut props = std::collections::HashMap::new();
props.insert("custom".to_string(), b"value".to_vec());
let options = CommitOptions::new("T <t@e>", "msg")
.timestamp(1577880000)
.revprops(props)
.authors(vec!["A <a@e>".to_string(), "B <b@e>".to_string()])
.branch_nick("trunk")
.revision_id(b"my-explicit-revid".to_vec());
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let revid = wt.commit(repo.as_mut(), &branch, &options).unwrap();
assert_eq!(revid, b"my-explicit-revid".to_vec());
let repo = cd.open_repository().unwrap();
let rev = repo.get_revision(&revid).unwrap();
assert_eq!(
rev.properties.get("custom").map(|v| v.as_slice()),
Some(&b"value"[..])
);
assert_eq!(
rev.properties.get("authors").map(|v| v.as_slice()),
Some(&b"A <a@e>\nB <b@e>"[..])
);
assert_eq!(
rev.properties.get("branch-nick").map(|v| v.as_slice()),
Some(&b"trunk"[..])
);
}
#[test]
fn commit_rejects_cr_in_revprops() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"hi\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let mut props = std::collections::HashMap::new();
props.insert("bad".to_string(), b"has\rcr".to_vec());
let options = CommitOptions::new("T <t@e>", "msg")
.timestamp(1577880000)
.revprops(props);
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
assert!(matches!(
wt.commit(repo.as_mut(), &branch, &options),
Err(WorkingTreeError::Commit(_))
));
}
#[test]
fn pointless_commit_is_refused_then_allowed() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"hi\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
wt.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "first").timestamp(1577880000),
)
.unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
assert!(matches!(
wt.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "empty").timestamp(1577890000)
),
Err(WorkingTreeError::PointlessCommit)
));
let revid = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "empty")
.timestamp(1577890000)
.allow_pointless(true),
)
.unwrap();
assert!(!revid.is_empty());
}
#[test]
fn commit_records_disk_deletion() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"a\n", None).unwrap();
parent.put_bytes("b.txt", b"b\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
wt.add("b.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
wt.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "two").timestamp(1577880000),
)
.unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
parent.delete("a.txt").unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev2 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "del a").timestamp(1577890000),
)
.unwrap();
let repo = cd.open_repository().unwrap();
let inv = repo.get_inventory(&rev2).unwrap();
let paths: Vec<String> = inv.entries().unwrap().into_iter().map(|(p, _)| p).collect();
assert_eq!(paths, vec!["b.txt".to_string()]);
assert_eq!(wt.path2id("a.txt"), None);
assert!(wt.path2id("b.txt").is_some());
}
#[test]
fn unknowns_lists_unversioned_files() {
let (_d, parent, mut wt) = fresh_tree();
parent.put_bytes("tracked.txt", b"t\n", None).unwrap();
parent.put_bytes("loose.txt", b"l\n", None).unwrap();
wt.add("tracked.txt", EntryKind::File, None).unwrap();
let wt = WorkingTree4::open(parent.clone()).unwrap();
assert_eq!(wt.unknowns().unwrap(), vec!["loose.txt".to_string()]);
}
#[test]
fn strict_commit_refuses_unknown_files() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"a\n", None).unwrap();
parent.put_bytes("loose.txt", b"l\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let strict = CommitOptions::new("T <t@e>", "c")
.timestamp(1577880000)
.strict(true);
assert!(matches!(
wt.commit(repo.as_mut(), &branch, &strict),
Err(WorkingTreeError::StrictCommitFailed(_))
));
parent.delete("loose.txt").unwrap();
let revid = wt.commit(repo.as_mut(), &branch, &strict).unwrap();
assert!(!revid.is_empty());
}
#[test]
fn selective_commit_records_only_named_files() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"a1\n", None).unwrap();
parent.put_bytes("b.txt", b"b1\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
wt.add("b.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev1 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "two").timestamp(1577880000),
)
.unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
parent.put_bytes("a.txt", b"a2\n", None).unwrap();
parent.put_bytes("b.txt", b"b2\n", None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev2 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "only a")
.timestamp(1577890000)
.specific_files(vec!["a.txt".to_string()]),
)
.unwrap();
let repo = cd.open_repository().unwrap();
let inv = repo.get_inventory(&rev2).unwrap();
let a_id = wt.path2id("a.txt").unwrap();
let b_id = wt.path2id("b.txt").unwrap();
let a_entry = inv
.get_entry(&crate::FileId::from(a_id.as_slice()))
.unwrap()
.unwrap();
let b_entry = inv
.get_entry(&crate::FileId::from(b_id.as_slice()))
.unwrap()
.unwrap();
assert_eq!(a_entry.revision().unwrap().as_bytes(), rev2.as_slice());
assert_eq!(b_entry.revision().unwrap().as_bytes(), rev1.as_slice());
assert_eq!(repo.get_file_text(&a_id, &rev2).unwrap(), b"a2\n");
assert_eq!(repo.get_file_text(&b_id, &rev1).unwrap(), b"b1\n");
}
#[cfg(feature = "gpg")]
#[test]
fn commit_with_signing_key_stores_signature() {
use sequoia_openpgp::cert::CertBuilder;
use sequoia_openpgp::serialize::Serialize;
let (cert, _) = CertBuilder::new().add_signing_subkey().generate().unwrap();
let mut tsk = Vec::new();
cert.as_tsk().serialize(&mut tsk).unwrap();
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"hi\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let revid = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "signed")
.timestamp(1577880000)
.signing_key(tsk),
)
.unwrap();
let repo = cd.open_repository().unwrap();
let sig = repo.get_signature_text(&revid).unwrap().unwrap();
let sig = String::from_utf8(sig).unwrap();
assert!(sig.starts_with("-----BEGIN PGP SIGNED MESSAGE-----"));
assert!(sig.contains("bazaar testament short form 3 strict"));
assert!(sig.contains("-----BEGIN PGP SIGNATURE-----"));
}
#[test]
fn merge_commit_records_multiple_parents() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"a1\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev1 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "c1").timestamp(1577880000),
)
.unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
parent.put_bytes("a.txt", b"a2\n", None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev2 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "c2").timestamp(1577890000),
)
.unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
wt.add_pending_merge(&rev1).unwrap();
assert_eq!(wt.parent_ids(), vec![rev2.clone(), rev1.clone()]);
parent.put_bytes("a.txt", b"a3\n", None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev3 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "merge").timestamp(1577900000),
)
.unwrap();
let repo = cd.open_repository().unwrap();
let rev = repo.get_revision(&rev3).unwrap();
let parent_ids: Vec<Vec<u8>> = rev
.parent_ids
.iter()
.map(|p| p.as_bytes().to_vec())
.collect();
assert_eq!(parent_ids, vec![rev2.clone(), rev1.clone()]);
assert_eq!(
repo.get_file_text(&wt.path2id("a.txt").unwrap(), &rev3)
.unwrap(),
b"a3\n"
);
assert_eq!(wt.parent_ids(), vec![rev3]);
}
#[test]
fn merge_commit_records_unchanged_merged_file() {
let (_d, parent, mut wt) = fresh_tree();
let cd = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
parent.put_bytes("a.txt", b"a1\n", None).unwrap();
let fid_bytes = wt.add("a.txt", EntryKind::File, None).unwrap();
let fid = crate::FileId::from(fid_bytes.as_slice());
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev1 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "c1").timestamp(1577880000),
)
.unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
parent.put_bytes("a.txt", b"a2\n", None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev2 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "c2").timestamp(1577890000),
)
.unwrap();
let repo = cd.open_repository().unwrap();
assert_ne!(
repo.revision_tree(&rev1)
.unwrap()
.get_file_revision(&fid)
.unwrap(),
repo.revision_tree(&rev2)
.unwrap()
.get_file_revision(&fid)
.unwrap()
);
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
wt.add_pending_merge(&rev1).unwrap();
parent.put_bytes("a.txt", b"a2\n", None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let rev3 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "merge").timestamp(1577900000),
)
.unwrap();
let repo = cd.open_repository().unwrap();
let tree3 = repo.revision_tree(&rev3).unwrap();
assert_eq!(
tree3.get_file_revision(&fid).unwrap().as_deref(),
Some(rev3.as_slice())
);
assert_eq!(repo.get_file_text(&fid_bytes, &rev3).unwrap(), b"a2\n");
}
fn create_commit_read(format_name: &str, rich_root: bool) {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let fmt = crate::bzrdir::find_control_dir_format(format_name).unwrap();
let cd = BzrDirMeta::create_with_format(&parent, fmt).unwrap();
parent.put_bytes("a.txt", b"hello\n", None).unwrap();
let mut wt = cd.open_workingtree().unwrap();
let file_id = wt.add("a.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let revid = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "a commit").timestamp(1577880000),
)
.unwrap();
let reopened = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
let repo = reopened.open_repository().unwrap();
assert_eq!(repo.get_revision(&revid).unwrap().message, "a commit");
let inv = repo.get_inventory(&revid).unwrap();
let paths: Vec<String> = inv.entries().unwrap().into_iter().map(|(p, _)| p).collect();
assert_eq!(paths, vec!["a.txt".to_string()]);
assert_eq!(repo.get_file_text(&file_id, &revid).unwrap(), b"hello\n");
let root_text = repo.get_file_text(crate::inventory::ROOT_ID, &revid);
assert_eq!(root_text.is_ok(), rich_root, "root text for {format_name}");
}
#[cfg(feature = "knitpack")]
#[test]
fn create_commit_read_btree_knit_pack() {
create_commit_read("1.9", false);
}
#[cfg(feature = "knitpack")]
#[test]
fn create_commit_read_graphindex_knit_pack() {
create_commit_read("pack-0.92", false);
}
#[cfg(feature = "knitpack")]
#[test]
fn create_commit_read_rich_root_pack() {
create_commit_read("rich-root-pack", true);
}
#[test]
fn create_commit_read_knit() {
create_commit_read("knit", false);
}
fn revision_attributes_round_trip(format_name: &str) {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let fmt = crate::bzrdir::find_control_dir_format(format_name).unwrap();
let cd = BzrDirMeta::create_with_format(&parent, fmt).unwrap();
let mut props = std::collections::HashMap::new();
props.insert("empty".to_string(), b"".to_vec());
props.insert("value".to_string(), b"one".to_vec());
props.insert("unicode".to_string(), "\u{b5}".as_bytes().to_vec());
props.insert("multiline".to_string(), b"foo\nbar\n\n".to_vec());
let mut wt = cd.open_workingtree().unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let revid = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("jaq", "quux")
.timestamp(1577880000)
.timezone(0)
.revprops(props.clone())
.revision_id(b"rev-attrs-1".to_vec())
.allow_pointless(true),
)
.unwrap();
assert_eq!(revid, b"rev-attrs-1".to_vec());
let reopened = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
let rev = reopened
.open_repository()
.unwrap()
.get_revision(&revid)
.unwrap();
assert_eq!(rev.message, "quux");
assert_eq!(rev.committer.as_deref(), Some("jaq"));
assert_eq!(rev.timestamp, 1577880000.0);
assert_eq!(rev.timezone, Some(0));
assert_eq!(rev.revision_id.as_bytes(), b"rev-attrs-1");
for (name, value) in &props {
assert_eq!(rev.properties.get(name), Some(value), "revprop {name}");
}
}
#[test]
fn revision_attributes_round_trip_2a() {
revision_attributes_round_trip("2a");
}
#[test]
fn revision_attributes_round_trip_knit_pack() {
revision_attributes_round_trip("1.9");
}
#[test]
fn revision_attributes_round_trip_knit() {
revision_attributes_round_trip("knit");
}
#[cfg(any(feature = "weave", feature = "knit"))]
fn fresh_wt3() -> (tempfile::TempDir, SharedTransport, WorkingTree3) {
let dir = tempfile::tempdir().unwrap();
let probe: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
probe.mkdir(".bzr").unwrap();
probe.mkdir(".bzr/checkout").unwrap();
probe
.put_bytes(
".bzr/checkout/format",
b"Bazaar-NG Working Tree format 3",
None,
)
.unwrap();
let shared: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let wt = WorkingTree3::open(shared).unwrap();
(dir, probe, wt)
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn wt3_open_dispatches_on_format_marker() {
let (_d, _probe, _wt) = fresh_wt3();
let dir = tempfile::tempdir().unwrap();
let t: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
t.mkdir(".bzr").unwrap();
t.mkdir(".bzr/checkout").unwrap();
t.put_bytes(
".bzr/checkout/format",
b"Bazaar-NG Working Tree format 3",
None,
)
.unwrap();
let opened = open(Arc::new(LocalTransport::new(dir.path()))).unwrap();
assert!(opened.list_files().is_empty());
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn wt3_add_versions_and_persists() {
let (_d, probe, mut wt) = fresh_wt3();
probe.put_bytes("a.txt", b"hi\n", None).unwrap();
let fid = wt.add("a.txt", EntryKind::File, None).unwrap();
assert_eq!(wt.path2id("a.txt"), Some(fid.clone()));
assert_eq!(wt.get_file_text("a.txt").unwrap(), b"hi\n");
let reopened = WorkingTree3::open(Arc::new(LocalTransport::new(_d.path()))).unwrap();
assert_eq!(reopened.path2id("a.txt"), Some(fid));
let files = reopened.list_files();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, "a.txt");
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn wt3_add_explicit_id_is_idempotent() {
let (_d, probe, mut wt) = fresh_wt3();
probe.put_bytes("a.txt", b"hi\n", None).unwrap();
let id = wt.add("a.txt", EntryKind::File, Some(b"my-id")).unwrap();
assert_eq!(id, b"my-id".to_vec());
let again = wt.add("a.txt", EntryKind::File, None).unwrap();
assert_eq!(again, b"my-id".to_vec());
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn wt3_remove_unversions_directory_and_children() {
let (_d, probe, mut wt) = fresh_wt3();
probe.mkdir("sub").unwrap();
probe.put_bytes("sub/a.txt", b"a\n", None).unwrap();
wt.add("sub", EntryKind::Directory, None).unwrap();
wt.add("sub/a.txt", EntryKind::File, None).unwrap();
assert_eq!(wt.list_files().len(), 2);
wt.remove("sub").unwrap();
assert_eq!(wt.path2id("sub"), None);
assert_eq!(wt.path2id("sub/a.txt"), None);
assert!(wt.list_files().is_empty());
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn wt3_rename_moves_entry_and_keeps_file_id() {
let (_d, probe, mut wt) = fresh_wt3();
probe.put_bytes("a.txt", b"a\n", None).unwrap();
let id = wt.add("a.txt", EntryKind::File, None).unwrap();
wt.rename("a.txt", "b.txt").unwrap();
assert_eq!(wt.path2id("a.txt"), None);
assert_eq!(wt.path2id("b.txt"), Some(id));
assert_eq!(probe.get_bytes("b.txt").unwrap(), b"a\n");
}
#[cfg(any(feature = "weave", feature = "knit"))]
#[test]
fn wt3_pending_merges_round_trip() {
let (_d, _probe, mut wt) = fresh_wt3();
wt.add_pending_merge(b"rev-merge-1").unwrap();
wt.add_pending_merge(b"rev-merge-1").unwrap(); wt.add_pending_merge(b"rev-merge-2").unwrap();
assert_eq!(
wt.parent_ids(),
vec![b"rev-merge-1".to_vec(), b"rev-merge-2".to_vec()]
);
}
#[cfg(feature = "knit")]
#[test]
fn knit_format_create_commit_read() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let format = crate::bzrdir::find_control_dir_format("knit").unwrap();
let cd = BzrDirMeta::create_with_format(&parent, format).unwrap();
parent.put_bytes("a.txt", b"hi\n", None).unwrap();
let mut wt = cd.open_workingtree().unwrap();
let file_id = wt.add("a.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let revid = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "first").timestamp(1577880000),
)
.unwrap();
assert_eq!(wt.basis_revision().as_deref(), Some(revid.as_slice()));
let reopened = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
let wt2 = reopened.open_workingtree().unwrap();
assert_eq!(wt2.basis_revision().as_deref(), Some(revid.as_slice()));
let branch = reopened.open_branch().unwrap();
assert_eq!(branch.last_revision_info().unwrap(), (1, revid.clone()));
let repo = reopened.open_repository().unwrap();
assert_eq!(repo.get_revision(&revid).unwrap().message, "first");
let inv = repo.get_inventory(&revid).unwrap();
let paths: Vec<String> = inv
.entries()
.unwrap()
.iter()
.map(|(p, _)| p.clone())
.collect();
assert_eq!(paths, vec!["a.txt".to_string()]);
assert_eq!(repo.get_file_text(&file_id, &revid).unwrap(), b"hi\n");
}
#[cfg(feature = "knit")]
#[test]
fn knit_format_writes_basis_and_stat_caches() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let format = crate::bzrdir::find_control_dir_format("knit").unwrap();
let cd = BzrDirMeta::create_with_format(&parent, format).unwrap();
parent.put_bytes("a.txt", b"hi\n", None).unwrap();
let mut wt = cd.open_workingtree().unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
wt.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "first").timestamp(1577880000),
)
.unwrap();
let cache = parent
.get_bytes(".bzr/checkout/basis-inventory-cache")
.unwrap();
assert!(cache.starts_with(b"<inventory format=\"7\""));
let reopened = BzrDirMeta::open(parent.subtransport(".bzr").unwrap()).unwrap();
let wt2 = reopened.open_workingtree().unwrap();
let repo2 = reopened.open_repository().unwrap();
let basis = repo2
.revision_tree(wt2.basis_revision().unwrap().as_slice())
.unwrap();
wt2.iter_changes(&basis).unwrap();
let stat_cache = parent.get_bytes(".bzr/checkout/stat-cache").unwrap();
assert!(stat_cache.starts_with(b"### bzr hashcache v5\n"));
}
#[test]
fn views_round_trip() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let cd = BzrDirMeta::create(&parent).unwrap();
let wt = cd.open_workingtree().unwrap();
assert!(wt.supports_views());
assert_eq!(wt.views().unwrap(), ViewInfo::default());
let mut info = ViewInfo::default();
info.views
.insert("my".to_string(), vec!["src".to_string(), "doc".to_string()]);
info.views
.insert("other".to_string(), vec!["lib".to_string()]);
info.current = Some("my".to_string());
wt.set_views(&info).unwrap();
let on_disk = parent.get_bytes(".bzr/checkout/views").unwrap();
assert_eq!(
on_disk,
b"Bazaar views format 1\ncurrent=my\nviews:\nmy\0src\0doc\nother\0lib\n".to_vec()
);
let reread = WorkingTree4::open(parent.clone()).unwrap();
assert_eq!(reread.views().unwrap(), info);
}
#[test]
fn set_views_rejects_unknown_current() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let cd = BzrDirMeta::create(&parent).unwrap();
let wt = cd.open_workingtree().unwrap();
let mut info = ViewInfo::default();
info.current = Some("nope".to_string());
assert!(matches!(
wt.set_views(&info),
Err(WorkingTreeError::Corrupt(_))
));
}
#[test]
fn conflicts_round_trip() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let cd = BzrDirMeta::create(&parent).unwrap();
let wt = cd.open_workingtree().unwrap();
assert!(wt.conflicts().unwrap().is_empty());
let conflicts = vec![
Conflict {
typestring: "text conflict".to_string(),
path: "a.txt".to_string(),
file_id: Some(b"a-id".to_vec()),
},
Conflict {
typestring: "path conflict".to_string(),
path: "dir/b".to_string(),
file_id: None,
},
];
wt.set_conflicts(&conflicts).unwrap();
let on_disk = parent.get_bytes(".bzr/checkout/conflicts").unwrap();
assert!(on_disk.starts_with(b"BZR conflict list format 1\n"));
let reread = WorkingTree4::open(parent).unwrap();
assert_eq!(reread.conflicts().unwrap(), conflicts);
}
}