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>),
ReservedId(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::ReservedId(r) => {
write!(f, "reserved revision id: {}", String::from_utf8_lossy(r))
}
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 set_parent_ids(
&mut self,
_repository: &dyn crate::repository::Repository,
_revision_ids: &[Vec<u8>],
_allow_leftmost_as_ghost: bool,
) -> Result<(), WorkingTreeError> {
Err(WorkingTreeError::Unsupported("set_parent_ids".to_string()))
}
fn apply_inventory_delta(
&mut self,
_delta: &crate::inventory_delta::InventoryDelta,
) -> Result<(), WorkingTreeError> {
Err(WorkingTreeError::Unsupported(
"apply_inventory_delta".to_string(),
))
}
fn list_files(&self) -> Vec<VersionedEntry>;
fn live_inventory_entries(
&self,
) -> Result<Vec<(String, crate::inventory::Entry)>, WorkingTreeError> {
Err(WorkingTreeError::Unsupported(
"live_inventory_entries".to_string(),
))
}
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(())
}
fn merge_modified_hashes(&self) -> Result<Vec<MergeModifiedHash>, WorkingTreeError> {
let bytes = match self.control_transport().get_bytes(MERGE_MODIFIED_PATH) {
Ok(b) => b,
Err(TransportError::NoSuchFile(_)) => return Ok(Vec::new()),
Err(e) => return Err(e.into()),
};
merge_modified_io::deserialize(&bytes).map_err(WorkingTreeError::Corrupt)
}
fn set_merge_modified_hashes(
&self,
hashes: &[MergeModifiedHash],
) -> Result<(), WorkingTreeError> {
self.control_transport().put_bytes(
MERGE_MODIFIED_PATH,
&merge_modified_io::serialize(hashes),
None,
)?;
Ok(())
}
}
const VIEWS_PATH: &str = ".bzr/checkout/views";
const CONFLICTS_PATH: &str = ".bzr/checkout/conflicts";
const MERGE_MODIFIED_PATH: &str = ".bzr/checkout/merge-hashes";
pub type MergeModifiedHash = (Vec<u8>, Vec<u8>);
#[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 action: Option<String>,
pub conflict_path: Option<String>,
pub conflict_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()
}
pub fn set_parent_ids(
&mut self,
repository: &dyn crate::repository::Repository,
revision_ids: &[Vec<u8>],
allow_leftmost_as_ghost: bool,
) -> Result<(), WorkingTreeError> {
let present: Vec<bool> = revision_ids
.iter()
.map(|r| {
if r.as_slice() == crate::branch::NULL_REVISION {
return Ok(true);
}
repository
.has_revision(r)
.map_err(WorkingTreeError::Repository)
})
.collect::<Result<_, _>>()?;
if let Some(false) = present.first() {
if !allow_leftmost_as_ghost {
return Err(WorkingTreeError::Commit(format!(
"left-hand parent {} is a ghost",
String::from_utf8_lossy(&revision_ids[0])
)));
}
}
let heads = self.parent_heads(repository, revision_ids)?;
let mut accepted: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
let mut kept: Vec<(Vec<u8>, bool)> = Vec::new();
for (rev_id, is_present) in revision_ids.iter().zip(&present) {
if !accepted.is_empty() && (accepted.contains(rev_id) || !heads.contains(rev_id)) {
continue;
}
if rev_id.ends_with(b":") {
return Err(WorkingTreeError::ReservedId(rev_id.clone()));
}
accepted.insert(rev_id.clone());
kept.push((rev_id.clone(), *is_present));
}
let mut parents = Vec::new();
let mut ghosts = Vec::new();
let mut per_parent = Vec::new();
for (rev_id, is_present) in kept {
parents.push(rev_id.clone());
if is_present {
per_parent.push(self.revision_tree_entries(repository, &rev_id)?);
} else {
ghosts.push(rev_id.clone());
}
}
self.dirstate
.set_parent_trees(parents, ghosts, per_parent)
.map_err(|e| WorkingTreeError::Commit(format!("set parents: {e:?}")))?;
self.save_dirstate()
}
pub fn apply_inventory_delta(
&mut self,
delta: &crate::inventory_delta::InventoryDelta,
) -> Result<(), WorkingTreeError> {
self.dirstate
.update_by_delta_from_inventory_delta(delta)
.map_err(|e| WorkingTreeError::Commit(format!("apply inventory delta: {e:?}")))?;
self.save_dirstate()
}
fn parent_heads(
&self,
repository: &dyn crate::repository::Repository,
revision_ids: &[Vec<u8>],
) -> Result<std::collections::HashSet<Vec<u8>>, WorkingTreeError> {
let mut parent_map: std::collections::HashMap<Vec<u8>, Vec<Vec<u8>>> =
std::collections::HashMap::new();
let mut pending: Vec<Vec<u8>> = revision_ids
.iter()
.filter(|r| r.as_slice() != crate::branch::NULL_REVISION)
.cloned()
.collect();
while !pending.is_empty() {
let batch = std::mem::take(&mut pending);
let map = repository
.get_parent_map(&batch)
.map_err(|e| WorkingTreeError::Commit(format!("parent map: {e:?}")))?;
for (rev, parents) in map {
for p in &parents {
if !parent_map.contains_key(p) && p.as_slice() != crate::branch::NULL_REVISION {
pending.push(p.clone());
}
}
parent_map.insert(rev, parents);
}
}
for rev_id in revision_ids {
if rev_id.as_slice() != crate::branch::NULL_REVISION {
parent_map.entry(rev_id.clone()).or_default();
}
}
let mut graph = vcs_graph::KnownGraph::new(parent_map, false);
Ok(graph
.heads(revision_ids.iter().cloned())
.into_iter()
.collect())
}
fn revision_tree_entries(
&self,
repository: &dyn crate::repository::Repository,
revision_id: &[u8],
) -> Result<Vec<(Vec<u8>, Vec<u8>, crate::dirstate::TreeData)>, WorkingTreeError> {
use crate::dirstate::{inv_entry_to_details, TreeData, NULLSTAT};
let tree = repository
.revision_tree(revision_id)
.map_err(|e| WorkingTreeError::Commit(format!("revision tree: {e:?}")))?;
let mut out = Vec::new();
for (path, entry) in tree.iter_entries() {
let (minikind, fingerprint, size, executable, _rev) = inv_entry_to_details(&entry);
out.push((
path.into_bytes(),
entry.file_id().as_bytes().to_vec(),
TreeData {
minikind,
fingerprint,
size,
executable,
packed_stat: NULLSTAT.to_vec(),
},
));
}
Ok(out)
}
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 live_inventory_entries(
&self,
) -> Result<Vec<(String, crate::inventory::Entry)>, WorkingTreeError> {
use crate::inventory::Entry as InvEntry;
use crate::{FileId, RevisionId};
let mut path_to_id: std::collections::HashMap<String, Vec<u8>> =
std::collections::HashMap::new();
let mut root_id: Option<Vec<u8>> = None;
for entry in self.dirstate.iter_entries() {
let Some(td) = entry.trees.first() else {
continue;
};
if EntryKind::from_minikind(td.minikind).is_none() {
continue;
}
let path = join_path(&entry.key.dirname, &entry.key.basename);
if path.is_empty() {
root_id = Some(entry.key.file_id.clone());
} else if td.minikind == Kind::Directory {
path_to_id.insert(path, entry.key.file_id.clone());
}
}
let root_id =
root_id.ok_or_else(|| WorkingTreeError::Commit("dirstate has no root".to_string()))?;
let mut out: Vec<(String, InvEntry)> = Vec::new();
for entry in self.dirstate.iter_entries() {
let Some(td) = entry.trees.first() else {
continue;
};
let minikind = td.minikind;
if EntryKind::from_minikind(minikind).is_none() {
continue;
}
let path = join_path(&entry.key.dirname, &entry.key.basename);
let file_id = FileId::from(entry.key.file_id.clone());
if path.is_empty() {
out.push((String::new(), InvEntry::root(file_id, None)));
continue;
}
let name = String::from_utf8_lossy(&entry.key.basename).into_owned();
let dirname = String::from_utf8_lossy(&entry.key.dirname).into_owned();
let parent_id = if dirname.is_empty() {
FileId::from(root_id.clone())
} else {
match path_to_id.get(&dirname) {
Some(id) => FileId::from(id.clone()),
None => continue,
}
};
let inv_entry = match minikind {
Kind::Directory => InvEntry::directory(file_id, name, parent_id, None),
Kind::File => InvEntry::file(
file_id,
name,
parent_id,
None,
None,
None,
Some(td.executable),
None,
),
Kind::Symlink => InvEntry::link(file_id, name, parent_id, None, None),
Kind::TreeReference => {
let reference = (!td.fingerprint.is_empty())
.then(|| RevisionId::from(td.fingerprint.clone()));
InvEntry::tree_reference(file_id, name, parent_id, None, reference)
}
Kind::Absent | Kind::Relocated => continue,
};
out.push((path, inv_entry));
}
Ok(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;
}
let executable = if kind == EntryKind::File && self.dirstate.use_filesystem_for_exec {
disk_executable(&self.transport, &path).unwrap_or(tree0.executable)
} else {
tree0.executable
};
entries.push(LiveEntry {
path,
file_id: entry.key.file_id.clone(),
kind,
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 set_parent_ids(
&mut self,
repository: &dyn crate::repository::Repository,
revision_ids: &[Vec<u8>],
allow_leftmost_as_ghost: bool,
) -> Result<(), WorkingTreeError> {
WorkingTree4::set_parent_ids(self, repository, revision_ids, allow_leftmost_as_ghost)
}
fn apply_inventory_delta(
&mut self,
delta: &crate::inventory_delta::InventoryDelta,
) -> Result<(), WorkingTreeError> {
WorkingTree4::apply_inventory_delta(self, delta)
}
fn list_files(&self) -> Vec<VersionedEntry> {
WorkingTree4::list_files(self)
}
fn live_inventory_entries(
&self,
) -> Result<Vec<(String, crate::inventory::Entry)>, WorkingTreeError> {
WorkingTree4::live_inventory_entries(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 disk_executable(transport: &SharedTransport, path: &str) -> Option<bool> {
let local = transport.local_path(path)?;
let metadata = std::fs::symlink_metadata(local).ok()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
Some(metadata.is_file() && metadata.permissions().mode() & 0o100 != 0)
}
#[cfg(not(unix))]
{
let _ = metadata;
Some(false)
}
}
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 mut add = |tag: &str, value: String| {
let _ = s.add(tag.to_string(), StanzaValue::String(value));
};
let file_id = |id: &Vec<u8>| String::from_utf8_lossy(id).into_owned();
add("path", c.path.clone());
add("type", c.typestring.clone());
if let Some(id) = &c.file_id {
add("file_id", file_id(id));
}
if let Some(action) = &c.action {
add("action", action.clone());
}
if let Some(conflict_path) = &c.conflict_path {
add("conflict_path", conflict_path.clone());
}
if let Some(id) = &c.conflict_file_id {
add("conflict_file_id", file_id(id));
}
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())?;
out.push(Conflict {
typestring,
path,
file_id: get("file_id").map(|s| s.into_bytes()),
action: get("action"),
conflict_path: get("conflict_path"),
conflict_file_id: get("conflict_file_id").map(|s| s.into_bytes()),
});
}
Ok(out)
}
}
mod merge_modified_io {
use crate::rio::{read_stanzas, rio_iter, Stanza, StanzaValue};
const HEADER: &[u8] = b"BZR merge-modified list format 1";
const HEADER_LINE: &[u8] = b"BZR merge-modified list format 1\n";
pub(super) fn serialize(hashes: &[super::MergeModifiedHash]) -> Vec<u8> {
let stanzas = hashes.iter().map(|(file_id, sha1)| {
let mut s = Stanza::new();
let _ = s.add(
"file_id".to_string(),
StanzaValue::String(String::from_utf8_lossy(file_id).into_owned()),
);
let _ = s.add(
"hash".to_string(),
StanzaValue::String(String::from_utf8_lossy(sha1).into_owned()),
);
s
});
rio_iter(stanzas, Some(HEADER.to_vec())).flatten().collect()
}
pub(super) fn deserialize(bytes: &[u8]) -> Result<Vec<super::MergeModifiedHash>, String> {
let rest = bytes
.strip_prefix(HEADER_LINE)
.ok_or_else(|| "missing 'BZR merge-modified list format 1' header".to_string())?;
let mut reader = std::io::BufReader::new(rest);
let stanzas =
read_stanzas(&mut reader).map_err(|e| format!("merge-modified 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 file_id = get("file_id")
.ok_or_else(|| "merge-modified stanza missing 'file_id'".to_string())?;
let hash =
get("hash").ok_or_else(|| "merge-modified stanza missing 'hash'".to_string())?;
out.push((file_id.into_bytes(), hash.into_bytes()));
}
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());
}
fn tree_with_two_commits() -> (
tempfile::TempDir,
SharedTransport,
BzrDirMeta,
Vec<u8>,
Vec<u8>,
) {
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();
parent.put_bytes("a.txt", b"one\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
let r1 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "one").timestamp(1577880000),
)
.unwrap();
let mut repo = cd.open_repository().unwrap();
let branch = cd.open_branch().unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
parent.put_bytes("a.txt", b"two\n", None).unwrap();
let r2 = wt
.commit(
repo.as_mut(),
&branch,
&CommitOptions::new("T <t@e>", "two").timestamp(1577880001),
)
.unwrap();
(dir, parent, cd, r1, r2)
}
#[test]
fn set_parent_ids_rewrites_the_dirstate_basis() {
let (_dir, parent, cd, r1, r2) = tree_with_two_commits();
let wt = WorkingTree4::open(parent.clone()).unwrap();
assert_eq!(wt.parent_ids(), vec![r2.clone()]);
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
let repo = cd.open_repository().unwrap();
wt.set_parent_ids(repo.as_ref(), std::slice::from_ref(&r1), false)
.unwrap();
assert_eq!(wt.parent_ids(), vec![r1.clone()]);
let reread = WorkingTree4::open(parent.clone()).unwrap();
assert_eq!(reread.basis_revision().as_deref(), Some(r1.as_slice()));
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
wt.set_parent_ids(repo.as_ref(), &[], false).unwrap();
assert_eq!(wt.parent_ids(), Vec::<Vec<u8>>::new());
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
wt.set_parent_ids(repo.as_ref(), &[r1.clone(), b"ghost-rev".to_vec()], false)
.unwrap();
assert_eq!(wt.parent_ids(), vec![r1.clone(), b"ghost-rev".to_vec()]);
let reread = WorkingTree4::open(parent).unwrap();
assert_eq!(reread.parent_ids(), vec![r1, b"ghost-rev".to_vec()]);
assert_eq!(reread.path2id("a.txt"), wt.path2id("a.txt"));
}
#[test]
fn set_parent_ids_drops_ancestor_parents() {
let (_dir, parent, cd, r1, r2) = tree_with_two_commits();
let repo = cd.open_repository().unwrap();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
wt.set_parent_ids(repo.as_ref(), &[r2.clone(), r1], false)
.unwrap();
assert_eq!(wt.parent_ids(), vec![r2.clone()]);
let reread = WorkingTree4::open(parent).unwrap();
assert_eq!(reread.parent_ids(), vec![r2]);
}
#[test]
fn set_parent_ids_leftmost_ghost() {
let (_dir, parent, cd, _r1, r2) = tree_with_two_commits();
let repo = cd.open_repository().unwrap();
let ghost = b"ghost-rev".to_vec();
let mut wt = WorkingTree4::open(parent.clone()).unwrap();
match wt.set_parent_ids(repo.as_ref(), std::slice::from_ref(&ghost), false) {
Err(WorkingTreeError::Commit(message)) => {
assert_eq!(message, "left-hand parent ghost-rev is a ghost")
}
other => panic!("expected a ghost error, got {:?}", other),
}
assert_eq!(wt.parent_ids(), vec![r2]);
wt.set_parent_ids(repo.as_ref(), std::slice::from_ref(&ghost), true)
.unwrap();
assert_eq!(wt.parent_ids(), vec![ghost.clone()]);
let reread = WorkingTree4::open(parent).unwrap();
assert_eq!(reread.parent_ids(), vec![ghost]);
}
#[test]
fn set_parent_ids_rejects_reserved_ids() {
let (_dir, parent, cd, r1, r2) = tree_with_two_commits();
let repo = cd.open_repository().unwrap();
let mut wt = WorkingTree4::open(parent).unwrap();
for parents in [
vec![crate::branch::NULL_REVISION.to_vec()],
vec![r1, b"current:".to_vec()],
] {
let reserved = parents.last().unwrap().clone();
match wt.set_parent_ids(repo.as_ref(), &parents, false) {
Err(WorkingTreeError::ReservedId(revision_id)) => {
assert_eq!(revision_id, reserved)
}
other => panic!("expected a reserved id error, got {:?}", other),
}
assert_eq!(wt.parent_ids(), vec![r2.clone()]);
}
}
use crate::repository::RepositoryError;
struct UnreadableRepository;
impl crate::repository::Repository for UnreadableRepository {
fn format(&self) -> &'static crate::repository::RepositoryFormat {
unimplemented!()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn all_revision_ids(&self) -> Result<Vec<Vec<u8>>, RepositoryError> {
unimplemented!()
}
fn get_parent_map(
&self,
_revision_ids: &[Vec<u8>],
) -> Result<std::collections::HashMap<Vec<u8>, Vec<Vec<u8>>>, RepositoryError> {
Err(RepositoryError::Corrupt("unreadable index".to_string()))
}
fn get_revision(
&self,
_revision_id: &[u8],
) -> Result<crate::revision::Revision, RepositoryError> {
unimplemented!()
}
fn get_inventory(
&self,
_revision_id: &[u8],
) -> Result<Box<dyn crate::inventory::Inventory>, RepositoryError> {
unimplemented!()
}
fn get_file_text(
&self,
_file_id: &[u8],
_revision: &[u8],
) -> Result<Vec<u8>, RepositoryError> {
unimplemented!()
}
fn start_write_group(&mut self) -> Result<(), RepositoryError> {
unimplemented!()
}
fn add_revision(
&mut self,
_revision: &crate::revision::Revision,
_parents: &[Vec<u8>],
) -> Result<(), RepositoryError> {
unimplemented!()
}
fn add_inventory_from_entries(
&mut self,
_revision_id: &[u8],
_parents: &[Vec<u8>],
_root_id: &[u8],
_entries: &[crate::inventory::Entry],
) -> Result<Vec<u8>, RepositoryError> {
unimplemented!()
}
fn add_inventory_by_delta(
&mut self,
_basis_revision_id: &[u8],
_delta: &crate::inventory_delta::InventoryDelta,
_new_revision_id: &[u8],
_parents: &[Vec<u8>],
) -> Result<Vec<u8>, RepositoryError> {
unimplemented!()
}
fn add_text(
&mut self,
_file_id: &[u8],
_revision: &[u8],
_parents: &[(Vec<u8>, Vec<u8>)],
_bytes: &[u8],
) -> Result<(), RepositoryError> {
unimplemented!()
}
fn add_signature_text(
&mut self,
_revision_id: &[u8],
_signature: &[u8],
) -> Result<(), RepositoryError> {
unimplemented!()
}
fn get_signature_text(
&self,
_revision_id: &[u8],
) -> Result<Option<Vec<u8>>, RepositoryError> {
unimplemented!()
}
fn commit_write_group(&mut self) -> Result<(), RepositoryError> {
unimplemented!()
}
}
#[test]
fn set_parent_ids_propagates_repository_errors() {
let (_dir, parent, _cd, r1, r2) = tree_with_two_commits();
let mut wt = WorkingTree4::open(parent).unwrap();
match wt.set_parent_ids(&UnreadableRepository, std::slice::from_ref(&r1), true) {
Err(WorkingTreeError::Repository(RepositoryError::Corrupt(message))) => {
assert_eq!(message, "unreadable index")
}
other => panic!("expected a repository error, got {:?}", other),
}
assert_eq!(wt.parent_ids(), vec![r2]);
}
#[test]
fn apply_inventory_delta_versions_a_new_file() {
use crate::inventory::Entry;
use crate::inventory_delta::{InventoryDelta, InventoryDeltaEntry};
use crate::FileId;
let (_d, parent, mut wt) = fresh_tree();
let root_id = wt.path2id("").unwrap();
parent.put_bytes("new.txt", b"hi\n", None).unwrap();
let entry = Entry::file(
FileId::from(b"new-id".to_vec()),
"new.txt".to_string(),
FileId::from(root_id),
None,
None,
Some(3),
Some(false),
None,
);
let delta = InventoryDelta(vec![InventoryDeltaEntry {
old_path: None,
new_path: Some("new.txt".to_string()),
file_id: FileId::from(b"new-id".to_vec()),
new_entry: Some(entry),
}]);
wt.apply_inventory_delta(&delta).unwrap();
assert_eq!(wt.path2id("new.txt"), Some(b"new-id".to_vec()));
let reread = WorkingTree4::open(parent).unwrap();
assert_eq!(reread.path2id("new.txt"), Some(b"new-id".to_vec()));
}
#[test]
fn apply_inventory_delta_removes_and_renames() {
use crate::inventory::Entry;
use crate::inventory_delta::{InventoryDelta, InventoryDeltaEntry};
use crate::FileId;
let (_d, parent, mut wt) = fresh_tree();
let root_id = wt.path2id("").unwrap();
parent.put_bytes("gone.txt", b"gone\n", None).unwrap();
parent.put_bytes("old.txt", b"moved\n", None).unwrap();
wt.add("gone.txt", EntryKind::File, None).unwrap();
wt.add("old.txt", EntryKind::File, None).unwrap();
let gone_id = wt.path2id("gone.txt").unwrap();
let moved_id = wt.path2id("old.txt").unwrap();
let renamed = Entry::file(
FileId::from(moved_id.clone()),
"new.txt".to_string(),
FileId::from(root_id),
None,
None,
Some(6),
Some(false),
None,
);
let delta = InventoryDelta(vec![
InventoryDeltaEntry {
old_path: Some("gone.txt".to_string()),
new_path: None,
file_id: FileId::from(gone_id),
new_entry: None,
},
InventoryDeltaEntry {
old_path: Some("old.txt".to_string()),
new_path: Some("new.txt".to_string()),
file_id: FileId::from(moved_id.clone()),
new_entry: Some(renamed),
},
]);
wt.apply_inventory_delta(&delta).unwrap();
let reread = WorkingTree4::open(parent).unwrap();
assert_eq!(reread.path2id("gone.txt"), None);
assert_eq!(reread.path2id("old.txt"), None);
assert_eq!(reread.path2id("new.txt"), Some(moved_id));
}
#[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);
}
#[cfg(unix)]
#[test]
fn iter_changes_ignores_other_execute_bits() {
use std::os::unix::fs::PermissionsExt;
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 path = parent.local_path("a.txt").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o645)).unwrap();
let wt = WorkingTree4::open(parent.clone()).unwrap();
let repo = cd.open_repository().unwrap();
let basis = repo.revision_tree(&revid).unwrap();
assert_eq!(wt.iter_changes(&basis).unwrap().len(), 0);
}
#[cfg(unix)]
#[test]
fn iter_changes_reports_executable_bit_change() {
use std::os::unix::fs::PermissionsExt;
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 path = parent.local_path("a.txt").unwrap();
let mut perms = std::fs::metadata(&path).unwrap().permissions();
perms.set_mode(perms.mode() | 0o111);
std::fs::set_permissions(&path, perms).unwrap();
let wt = WorkingTree4::open(parent.clone()).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].new_path.as_deref(), Some("a.txt"));
assert!(changes[0].new_executable);
assert!(!changes[0].content_change);
}
#[test]
fn live_inventory_entries_lists_versioned_entries() {
let (_d, parent, mut wt) = fresh_tree();
parent.mkdir("sub").unwrap();
parent.put_bytes("a.txt", b"a\n", None).unwrap();
parent.put_bytes("sub/b.txt", b"b\n", None).unwrap();
wt.add("sub", EntryKind::Directory, None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
wt.add("sub/b.txt", EntryKind::File, None).unwrap();
let entries = wt.live_inventory_entries().unwrap();
let by_path: std::collections::HashMap<&str, &crate::inventory::Entry> =
entries.iter().map(|(p, e)| (p.as_str(), e)).collect();
let root = by_path[""];
assert!(matches!(root, crate::inventory::Entry::Root { .. }));
let root_id = root.file_id().clone();
let a = by_path["a.txt"];
assert_eq!(a.kind(), crate::osutils::Kind::File);
assert_eq!(a.name(), "a.txt");
assert_eq!(a.parent_id(), Some(&root_id));
let sub = by_path["sub"];
assert_eq!(sub.kind(), crate::osutils::Kind::Directory);
assert_eq!(sub.parent_id(), Some(&root_id));
let b = by_path["sub/b.txt"];
assert_eq!(b.name(), "b.txt");
assert_eq!(b.parent_id(), Some(sub.file_id()));
}
#[test]
fn live_inventory_entries_omit_cached_details() {
use crate::inventory::Entry;
let (_d, parent, mut wt) = fresh_tree();
parent.put_bytes("a.txt", b"contents\n", None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
wt.add("link", EntryKind::Symlink, None).unwrap();
for block in wt.dirstate.dirblocks.iter_mut() {
for entry in block.entries.iter_mut() {
if entry.key.basename == b"a.txt" {
entry.trees[0].fingerprint =
b"0f0e0d0c0b0a09080706050403020100deadbeef".to_vec();
entry.trees[0].size = 9;
} else if entry.key.basename == b"link" {
entry.trees[0].fingerprint = b"a.txt".to_vec();
}
}
}
let entries = wt.live_inventory_entries().unwrap();
match &entries[1].1 {
Entry::File {
text_sha1,
text_size,
executable,
..
} => {
assert_eq!(text_sha1, &None);
assert_eq!(text_size, &None);
assert!(!executable);
}
other => panic!("expected a file entry, got {:?}", other),
}
match &entries[2].1 {
Entry::Link { symlink_target, .. } => assert_eq!(symlink_target, &None),
other => panic!("expected a link entry, got {:?}", other),
}
}
#[test]
fn live_inventory_entries_skips_entries_without_parent() {
let (_d, parent, mut wt) = fresh_tree();
parent.mkdir("sub").unwrap();
parent.put_bytes("sub/b.txt", b"b\n", None).unwrap();
parent.put_bytes("a.txt", b"a\n", None).unwrap();
wt.add("sub", EntryKind::Directory, None).unwrap();
wt.add("sub/b.txt", EntryKind::File, None).unwrap();
wt.add("a.txt", EntryKind::File, None).unwrap();
for block in wt.dirstate.dirblocks.iter_mut() {
for entry in block.entries.iter_mut() {
if entry.key.dirname.is_empty() && entry.key.basename == b"sub" {
entry.trees[0].minikind = Kind::Absent;
}
}
}
let entries = wt.live_inventory_entries().unwrap();
let paths: Vec<&str> = entries.iter().map(|(p, _)| p.as_str()).collect();
assert_eq!(paths, vec!["", "a.txt"]);
}
#[cfg(unix)]
#[test]
fn live_inventory_entries_lists_a_symlink() {
let (_d, parent, mut wt) = fresh_tree();
parent.put_bytes("target.txt", b"contents\n", None).unwrap();
std::os::unix::fs::symlink("target.txt", parent.local_path("link").unwrap()).unwrap();
wt.add("target.txt", EntryKind::File, None).unwrap();
let link_id = wt.add("link", EntryKind::Symlink, None).unwrap();
let entries = wt.live_inventory_entries().unwrap();
let paths: Vec<&str> = entries.iter().map(|(p, _)| p.as_str()).collect();
assert_eq!(paths, vec!["", "link", "target.txt"]);
let root_id = entries[0].1.file_id().clone();
let link = &entries[1].1;
assert_eq!(link.kind(), crate::osutils::Kind::Symlink);
assert_eq!(link.name(), "link");
assert_eq!(link.file_id().as_bytes(), link_id.as_slice());
assert_eq!(link.parent_id(), Some(&root_id));
}
#[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(_))
));
}
const BREEZY_CONFLICTS: &[u8] = b"BZR conflict list format 1\n\
path: dir/b\n\
type: path conflict\n\
file_id: b-id\n\
conflict_path: dir/c\n\
\n\
path: a.txt\n\
type: text conflict\n\
file_id: a-id\n\
\n\
path: x.moved\n\
type: duplicate\n\
file_id: x-id\n\
action: Moved existing file to\n\
conflict_path: x\n\
conflict_file_id: y-id\n\
\n\
path: d\n\
type: missing parent\n\
action: Created directory\n";
fn breezy_conflicts() -> Vec<Conflict> {
let conflict = |typestring: &str, path: &str, file_id: Option<&[u8]>| Conflict {
typestring: typestring.to_string(),
path: path.to_string(),
file_id: file_id.map(|id| id.to_vec()),
action: None,
conflict_path: None,
conflict_file_id: None,
};
vec![
Conflict {
conflict_path: Some("dir/c".to_string()),
..conflict("path conflict", "dir/b", Some(b"b-id"))
},
conflict("text conflict", "a.txt", Some(b"a-id")),
Conflict {
action: Some("Moved existing file to".to_string()),
conflict_path: Some("x".to_string()),
conflict_file_id: Some(b"y-id".to_vec()),
..conflict("duplicate", "x.moved", Some(b"x-id"))
},
Conflict {
action: Some("Created directory".to_string()),
..conflict("missing parent", "d", None)
},
]
}
#[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());
wt.set_conflicts(&breezy_conflicts()).unwrap();
let reread = WorkingTree4::open(parent).unwrap();
assert_eq!(reread.conflicts().unwrap(), breezy_conflicts());
}
#[test]
fn conflicts_are_written_as_breezy_does() {
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();
wt.set_conflicts(&breezy_conflicts()).unwrap();
assert_eq!(
String::from_utf8(parent.get_bytes(".bzr/checkout/conflicts").unwrap()).unwrap(),
String::from_utf8(BREEZY_CONFLICTS.to_vec()).unwrap()
);
}
#[test]
fn conflicts_written_by_breezy_are_read() {
let dir = tempfile::tempdir().unwrap();
let parent: SharedTransport = Arc::new(LocalTransport::new(dir.path()));
let cd = BzrDirMeta::create(&parent).unwrap();
parent
.put_bytes(".bzr/checkout/conflicts", BREEZY_CONFLICTS, None)
.unwrap();
assert_eq!(
cd.open_workingtree().unwrap().conflicts().unwrap(),
breezy_conflicts()
);
}
#[test]
fn merge_modified_hashes_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.merge_modified_hashes().unwrap().is_empty());
let hashes = vec![
(b"a-id".to_vec(), b"0123456789abcdef".to_vec()),
(b"b-id".to_vec(), b"fedcba9876543210".to_vec()),
];
wt.set_merge_modified_hashes(&hashes).unwrap();
let on_disk = parent.get_bytes(".bzr/checkout/merge-hashes").unwrap();
assert!(on_disk.starts_with(b"BZR merge-modified list format 1\n"));
let reread = WorkingTree4::open(parent).unwrap();
assert_eq!(reread.merge_modified_hashes().unwrap(), hashes);
}
}