use std::ffi::{OsStr, OsString};
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Component, Path, PathBuf};
use thiserror::Error;
use super::{Repository, RepositoryLocation, RepositoryMetadata, WorktreeLinkStyle};
use crate::SharedPermissions;
use crate::refs::{Backend, RefName, References, Target};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct WorktreeDurability {
pub index: bool,
pub references: bool,
}
impl WorktreeDurability {
#[must_use]
pub fn with_index(mut self, index: bool) -> Self {
self.index = index;
self
}
#[must_use]
pub fn with_references(mut self, references: bool) -> Self {
self.references = references;
self
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct OrphanWorktreeOptions {
pub link_style: WorktreeLinkStyle,
pub index_path: Option<PathBuf>,
pub index_limits: crate::index::Limits,
pub private_config: Option<Vec<u8>>,
pub shared_permissions: SharedPermissions,
pub durability: WorktreeDurability,
}
impl OrphanWorktreeOptions {
#[must_use]
pub fn with_link_style(mut self, link_style: WorktreeLinkStyle) -> Self {
self.link_style = link_style;
self
}
#[must_use]
pub fn with_index_path(mut self, index_path: Option<PathBuf>) -> Self {
self.index_path = index_path;
self
}
#[must_use]
pub fn with_index_limits(mut self, index_limits: crate::index::Limits) -> Self {
self.index_limits = index_limits;
self
}
#[must_use]
pub fn with_private_config(mut self, private_config: Option<Vec<u8>>) -> Self {
self.private_config = private_config;
self
}
#[must_use]
pub fn with_shared_permissions(mut self, shared_permissions: SharedPermissions) -> Self {
self.shared_permissions = shared_permissions;
self
}
#[must_use]
pub fn with_durability(mut self, durability: WorktreeDurability) -> Self {
self.durability = durability;
self
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CreateWorktreeError {
#[error("invalid orphan branch name")]
Branch,
#[error("branch already exists")]
ExistingBranch,
#[error("worktree destination already exists or has no filename: {0}")]
Destination(PathBuf),
#[error("unsupported worktree metadata path: {0}")]
Path(PathBuf),
#[error("worktree registration name limit exceeded")]
Limit,
#[error(transparent)]
References(#[from] crate::refs::ReferenceError),
#[error(transparent)]
Configuration(#[from] super::OpenError),
#[error(transparent)]
Index(#[from] crate::index::Error),
#[error("cannot initialize worktree index")]
IndexStorage {
registration: Option<PathBuf>,
#[source]
source: crate::index::StorageError,
},
#[error("worktree creation failed and index lock cleanup also failed: {cleanup}")]
Cleanup {
#[source]
operation: Box<CreateWorktreeError>,
cleanup: crate::index::StorageError,
},
#[error(transparent)]
Reftable(#[from] crate::refs::reftable::Error),
#[error("cannot create {path}")]
Io {
path: PathBuf,
registration: Option<PathBuf>,
#[source]
source: io::Error,
},
#[error("created worktree file {path} could not be synchronized")]
Synchronize {
path: PathBuf,
registration: PathBuf,
#[source]
source: io::Error,
},
#[error("created worktree metadata failed validation")]
Open {
registration: PathBuf,
#[source]
source: Box<super::OpenError>,
},
}
struct SelectedIndexAlias {
path: PathBuf,
target: PathBuf,
#[cfg(unix)]
spelling: PathBuf,
#[cfg(unix)]
link_metadata: fs::Metadata,
#[cfg(unix)]
target_metadata: Option<fs::Metadata>,
}
impl SelectedIndexAlias {
fn capture(path: &Path) -> Result<Option<Self>, CreateWorktreeError> {
let path = if path.is_absolute() {
path.to_owned()
} else {
std::env::current_dir()
.map_err(|source| io_error(path, None, source))?
.join(path)
};
let link_metadata = match fs::symlink_metadata(&path) {
Ok(metadata) if metadata.file_type().is_symlink() => metadata,
Ok(_) => return Ok(None),
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(io_error(&path, None, error)),
};
#[cfg(not(unix))]
{
let _ = link_metadata;
Err(CreateWorktreeError::IndexStorage {
registration: None,
source: crate::index::StorageError::NotRegular(path),
})
}
#[cfg(unix)]
{
let spelling = fs::read_link(&path).map_err(|source| io_error(&path, None, source))?;
let linked = if spelling.is_absolute() {
spelling.clone()
} else {
path.parent().expect("absolute path").join(&spelling)
};
let name = linked
.file_name()
.ok_or_else(|| CreateWorktreeError::Path(linked.clone()))?;
let parent = fs::canonicalize(linked.parent().expect("path with filename"))
.map_err(|source| io_error(&linked, None, source))?;
let target = parent.join(name);
let target_metadata = match fs::symlink_metadata(&target) {
Ok(metadata) if metadata.is_file() => Some(metadata),
Ok(_) => {
return Err(CreateWorktreeError::IndexStorage {
registration: None,
source: crate::index::StorageError::NotRegular(target),
});
}
Err(error) if error.kind() == io::ErrorKind::NotFound => None,
Err(error) => return Err(io_error(&target, None, error)),
};
Ok(Some(Self {
path,
target,
spelling,
link_metadata,
target_metadata,
}))
}
}
fn check(&self, registration: Option<&Path>) -> Result<(), CreateWorktreeError> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let current_link = fs::symlink_metadata(&self.path).ok();
let current_target = fs::symlink_metadata(&self.target).ok();
let same_identity = |left: &fs::Metadata, right: &fs::Metadata| {
left.file_type() == right.file_type()
&& left.dev() == right.dev()
&& left.ino() == right.ino()
};
let link_unchanged = current_link
.as_ref()
.is_some_and(|current| same_identity(&self.link_metadata, current))
&& fs::read_link(&self.path).ok().as_ref() == Some(&self.spelling);
let target_unchanged = match (&self.target_metadata, current_target.as_ref()) {
(None, None) => true,
(Some(before), Some(after)) => same_identity(before, after),
_ => false,
};
if link_unchanged && target_unchanged {
return Ok(());
}
}
Err(CreateWorktreeError::IndexStorage {
registration: registration.map(Path::to_owned),
source: crate::index::StorageError::Changed(self.path.clone()),
})
}
}
impl Repository {
pub fn create_orphan_worktree(
&self,
destination: impl AsRef<Path>,
branch: &RefName,
max_names: usize,
) -> Result<Self, CreateWorktreeError> {
self.create_orphan_worktree_with_link_style(
destination,
branch,
max_names,
WorktreeLinkStyle::Configured,
)
}
pub fn create_orphan_worktree_with_link_style(
&self,
destination: impl AsRef<Path>,
branch: &RefName,
max_names: usize,
link_style: WorktreeLinkStyle,
) -> Result<Self, CreateWorktreeError> {
self.create_orphan_worktree_with_options(
destination,
branch,
max_names,
OrphanWorktreeOptions {
link_style,
..Default::default()
},
)
}
pub fn create_orphan_worktree_with_options(
&self,
destination: impl AsRef<Path>,
branch: &RefName,
max_names: usize,
options: OrphanWorktreeOptions,
) -> Result<Self, CreateWorktreeError> {
let _lock_operation = crate::locks::operation();
let location = create_orphan_worktree(
self.references()?,
destination.as_ref(),
branch,
max_names,
options,
)?;
let registration = location.git_dir().to_path_buf();
location
.open_with_config(&crate::config::ConfigInputs::default())
.map_err(|source| CreateWorktreeError::Open {
registration,
source: Box::new(source),
})
}
}
impl RepositoryMetadata {
pub fn create_orphan_worktree_with_options(
&self,
destination: impl AsRef<Path>,
branch: &RefName,
max_names: usize,
options: OrphanWorktreeOptions,
) -> Result<RepositoryLocation, CreateWorktreeError> {
let _lock_operation = crate::locks::operation();
let references = References::from_layout(
self.git_dir(),
self.common_dir(),
self.object_format(),
self.reference_backend(),
)?;
create_orphan_worktree(references, destination.as_ref(), branch, max_names, options)
}
}
fn create_orphan_worktree(
references: References<'_>,
destination: &Path,
branch: &RefName,
max_names: usize,
options: OrphanWorktreeOptions,
) -> Result<RepositoryLocation, CreateWorktreeError> {
options
.shared_permissions
.validate()
.map_err(|source| io_error(destination, None, source))?;
if max_names == 0 {
return Err(CreateWorktreeError::Limit);
}
if !branch.as_bytes().starts_with(b"refs/heads/") {
return Err(CreateWorktreeError::Branch);
}
if references.read(branch)?.is_some() {
return Err(CreateWorktreeError::ExistingBranch);
}
let name = destination
.file_name()
.filter(|name| !name.is_empty())
.ok_or_else(|| CreateWorktreeError::Destination(destination.into()))?;
let parent = destination
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(Path::new("."));
let parent = fs::canonicalize(parent).map_err(|e| io_error(parent, None, e))?;
let destination = parent.join(name);
validate_path(&destination)?;
validate_path(references.common_dir)?;
match fs::symlink_metadata(&destination) {
Ok(_) => return Err(CreateWorktreeError::Destination(destination)),
Err(e) if e.kind() == io::ErrorKind::NotFound => (),
Err(e) => return Err(io_error(&destination, None, e)),
}
let table = if references.reference_backend == Backend::Reftable {
Some(
initial_head_table(references.object_format, branch)
.encode(crate::refs::reftable::Limits::default())?,
)
} else {
None
};
let config_path = references.common_dir.join("config");
let bytes = super::read_config(&config_path, crate::config::ResolveLimits::default().bytes)?;
let config = crate::Config::parse(&bytes).map_err(|source| super::OpenError::Config {
path: config_path.clone(),
source,
})?;
let configured_relative = super::extension_boolean(&config, &config_path, "relativeworktrees")?;
let relative = options.link_style.uses_relative(configured_relative);
let selected_alias = options
.index_path
.as_deref()
.map(SelectedIndexAlias::capture)
.transpose()?
.flatten();
let selected_index = selected_alias
.as_ref()
.map(|alias| &alias.target)
.or(options.index_path.as_ref());
let selected_missing = selected_index
.map(|path| match fs::symlink_metadata(path) {
Ok(_) => Ok(false),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(true),
Err(error) => Err(io_error(path, None, error)),
})
.transpose()?;
let mut index_edit = selected_index
.map(|path| {
crate::index::IndexEdit::acquire(
references.object_format,
path.clone(),
options.index_limits,
crate::index::EditOptions {
resolve_split: true,
follow_symlink: false,
},
)
.map_err(|source| CreateWorktreeError::IndexStorage {
registration: None,
source,
})
})
.transpose()?;
if let (Some(missing), Some(edit)) = (selected_missing, index_edit.as_ref())
&& missing != edit.original_missing()
{
let path = selected_index.expect("selected index path").clone();
let error = CreateWorktreeError::IndexStorage {
registration: None,
source: crate::index::StorageError::Changed(path),
};
return finish_creation(Err(error), index_edit);
}
if let Some(alias) = &selected_alias
&& let Err(error) = alias.check(None)
{
return finish_creation(Err(error), index_edit);
}
let version = match index_edit.as_ref().map(|edit| edit.index().version()) {
Some(crate::index::Version::V4) => crate::index::Version::V4,
_ => crate::index::Version::V2,
};
let empty_index = crate::index::Index::empty_for_orphan(references.object_format, version);
if let Err(error) = empty_index.encode(options.index_limits) {
return finish_creation(Err(CreateWorktreeError::Index(error)), index_edit);
}
let result = (|| {
let registrations = references.common_dir.join("worktrees");
fs::create_dir_all(®istrations).map_err(|e| io_error(®istrations, None, e))?;
options
.shared_permissions
.apply_directory(®istrations)
.map_err(|source| io_error(®istrations, None, source))?;
let registration = reserve_name(®istrations, name, max_names)?;
let forward = if relative {
relative_path(&destination, ®istration)
} else {
registration.clone()
};
let backlink = if relative {
relative_path(®istration, &destination.join(".git"))
} else {
destination.join(".git")
};
write_file(®istration.join("commondir"), b"../..\n", ®istration)?;
write_file(
®istration.join("gitdir"),
&path_line(&backlink),
®istration,
)?;
let refs = registration.join("refs");
create_dir(&refs, ®istration)?;
options
.shared_permissions
.apply_directory(&refs)
.map_err(|source| io_error(&refs, Some(®istration), source))?;
let reference_sync = table.is_some() && options.durability.references;
if let Some(table) = table {
let tables = registration.join("reftable");
create_dir(&tables, ®istration)?;
options
.shared_permissions
.apply_directory(&tables)
.map_err(|source| io_error(&tables, Some(®istration), source))?;
let name = crate::refs::reftable::table_name(1, 1);
write_shared_file(
&tables.join(&name),
&table,
®istration,
options.shared_permissions,
reference_sync,
)?;
write_shared_file(
®istration.join("reftable/tables.list"),
format!("{name}\n").as_bytes(),
®istration,
options.shared_permissions,
reference_sync,
)?;
write_shared_file(
®istration.join("refs/heads"),
b"this repository uses the reftable format\n",
®istration,
options.shared_permissions,
reference_sync,
)?;
write_shared_file(
®istration.join("HEAD"),
b"ref: refs/heads/.invalid\n",
®istration,
options.shared_permissions,
reference_sync,
)?;
} else {
let mut head = b"ref: ".to_vec();
head.extend_from_slice(branch.as_bytes());
head.push(b'\n');
write_shared_file(
®istration.join("HEAD"),
&head,
®istration,
options.shared_permissions,
false,
)?;
}
if let Some(bytes) = &options.private_config {
write_file(®istration.join("config.worktree"), bytes, ®istration)?;
}
if let Some(alias) = &selected_alias {
alias.check(Some(®istration))?;
}
if index_edit.is_none() {
index_edit = Some(
crate::index::IndexEdit::acquire(
references.object_format,
registration.join("index"),
options.index_limits,
crate::index::EditOptions {
resolve_split: true,
follow_symlink: true,
},
)
.map_err(|source| CreateWorktreeError::IndexStorage {
registration: Some(registration.clone()),
source,
})?,
);
}
index_edit
.as_mut()
.expect("selected or private index acquired")
.replace_index(empty_index)?;
let edit = index_edit
.take()
.expect("selected or private index acquired");
let commit_options = crate::index::IndexCommitOptions {
shared_permissions: options.shared_permissions,
sync: options.durability.index,
};
let publication = if selected_missing == Some(true) {
edit.commit_new_with_options(commit_options)
} else {
edit.commit_with_options(commit_options)
};
publication.map_err(|source| CreateWorktreeError::IndexStorage {
registration: Some(registration.clone()),
source,
})?;
create_dir(&destination, ®istration)?;
write_file(
&destination.join(".git"),
&gitfile_line(&forward),
®istration,
)?;
RepositoryLocation::at_git_dir(destination.join(".git")).map_err(|source| {
CreateWorktreeError::Open {
registration: registration.clone(),
source: Box::new(source),
}
})
})();
finish_creation(result, index_edit)
}
fn finish_creation(
result: Result<RepositoryLocation, CreateWorktreeError>,
index_edit: Option<crate::index::IndexEdit>,
) -> Result<RepositoryLocation, CreateWorktreeError> {
match (result, index_edit) {
(Err(operation), Some(edit)) => match edit.abort() {
Ok(()) => Err(operation),
Err(cleanup) => Err(CreateWorktreeError::Cleanup {
operation: Box::new(operation),
cleanup,
}),
},
(result, None) => result,
(Ok(_), Some(_)) => unreachable!("successful creation publishes the selected index"),
}
}
fn reserve_name(
root: &Path,
name: &OsStr,
max_names: usize,
) -> Result<PathBuf, CreateWorktreeError> {
for suffix in 0..max_names {
let mut candidate = OsString::from(name);
if suffix != 0 {
candidate.push(suffix.to_string());
}
let path = root.join(candidate);
match fs::create_dir(&path) {
Ok(()) => return Ok(path),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => (),
Err(e) => return Err(io_error(&path, None, e)),
}
}
Err(CreateWorktreeError::Limit)
}
fn initial_head_table(
format: crate::ObjectFormat,
branch: &RefName,
) -> crate::refs::reftable::Table {
use crate::refs::reftable::{RefRecord, Table};
Table {
format,
min_update_index: 1,
max_update_index: 1,
references: vec![RefRecord {
name: RefName::HEAD.into(),
update_index: 1,
target: Some(Target::Symbolic(branch.clone())),
peeled: None,
}],
logs: Vec::new(),
}
}
pub(super) fn relative_path(from: &Path, to: &Path) -> PathBuf {
let absolute = to.to_path_buf();
let from: Vec<Component<'_>> = from.components().collect();
let to: Vec<Component<'_>> = to.components().collect();
if from.first() != to.first() {
return absolute;
}
let shared = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
let mut result = PathBuf::new();
for _ in shared..from.len() {
result.push("..");
}
for component in &to[shared..] {
result.push(component.as_os_str());
}
result
}
pub(super) fn validate_path(path: &Path) -> Result<(), CreateWorktreeError> {
#[cfg(unix)]
let invalid = {
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().contains(&b'\n') || path.as_os_str().as_bytes().contains(&b'\r')
};
#[cfg(not(unix))]
let invalid = path.to_str().is_none()
|| path.to_string_lossy().as_bytes().contains(&b'\n')
|| path.to_string_lossy().as_bytes().contains(&b'\r');
if invalid {
Err(CreateWorktreeError::Path(path.into()))
} else {
Ok(())
}
}
pub(super) fn path_line(path: &Path) -> Vec<u8> {
#[cfg(unix)]
let mut bytes = {
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().to_vec()
};
#[cfg(windows)]
let mut bytes = {
let spelling = path.to_str().expect("validated metadata path");
let spelling = if let Some(unc) = spelling.strip_prefix(r"\\?\UNC\") {
format!(r"\\{unc}")
} else {
spelling
.strip_prefix(r"\\?\")
.unwrap_or(spelling)
.to_owned()
};
spelling.into_bytes()
};
#[cfg(not(any(unix, windows)))]
let mut bytes = path
.to_str()
.expect("validated metadata path")
.as_bytes()
.to_vec();
bytes.push(b'\n');
bytes
}
pub(super) fn gitfile_line(path: &Path) -> Vec<u8> {
let mut bytes = b"gitdir: ".to_vec();
bytes.extend_from_slice(&path_line(path));
bytes
}
fn create_dir(path: &Path, registration: &Path) -> Result<(), CreateWorktreeError> {
fs::create_dir(path).map_err(|e| io_error(path, Some(registration), e))
}
fn write_file(path: &Path, bytes: &[u8], registration: &Path) -> Result<(), CreateWorktreeError> {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|e| io_error(path, Some(registration), e))?;
file.write_all(bytes)
.map_err(|e| io_error(path, Some(registration), e))
}
fn io_error(path: &Path, registration: Option<&Path>, source: io::Error) -> CreateWorktreeError {
CreateWorktreeError::Io {
path: path.into(),
registration: registration.map(Path::to_path_buf),
source,
}
}
#[cfg(all(test, windows))]
mod tests {
use super::*;
#[test]
fn gitfile_paths_drop_windows_verbatim_prefixes() {
assert_eq!(
path_line(Path::new(r"\\?\C:\repo\worktrees\one")),
b"C:\\repo\\worktrees\\one\n"
);
assert_eq!(
path_line(Path::new(r"\\?\UNC\server\share\repo")),
b"\\\\server\\share\\repo\n"
);
}
}
fn write_shared_file(
path: &Path,
bytes: &[u8],
registration: &Path,
permissions: SharedPermissions,
sync: bool,
) -> Result<(), CreateWorktreeError> {
write_shared_file_with_sync(
path,
bytes,
registration,
permissions,
sync,
crate::file_policy::sync_file,
)
}
fn write_shared_file_with_sync(
path: &Path,
bytes: &[u8],
registration: &Path,
permissions: SharedPermissions,
sync: bool,
synchronize: impl FnOnce(&std::fs::File) -> io::Result<()>,
) -> Result<(), CreateWorktreeError> {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|source| io_error(path, Some(registration), source))?;
file.write_all(bytes)
.map_err(|source| io_error(path, Some(registration), source))?;
permissions
.apply_file(&file)
.map_err(|source| io_error(path, Some(registration), source))?;
if sync {
synchronize(&file).map_err(|source| CreateWorktreeError::Synchronize {
path: path.into(),
registration: registration.into(),
source,
})?;
}
Ok(())
}
#[cfg(test)]
mod policy_tests {
use rstest::rstest;
use super::*;
#[cfg(unix)]
#[test]
fn selected_alias_rejects_retarget_and_replaced_referent() {
let root = tempfile::tempdir().unwrap();
let selected = root.path().join("selected");
let first = root.path().join("first");
let second = root.path().join("second");
fs::write(&first, b"first").unwrap();
fs::write(&second, b"second").unwrap();
std::os::unix::fs::symlink("first", &selected).unwrap();
let alias = SelectedIndexAlias::capture(&selected).unwrap().unwrap();
assert!(alias.check(None).is_ok());
fs::remove_file(&selected).unwrap();
std::os::unix::fs::symlink("second", &selected).unwrap();
assert!(matches!(
alias.check(None),
Err(CreateWorktreeError::IndexStorage {
registration: None,
source: crate::index::StorageError::Changed(_),
})
));
fs::remove_file(&selected).unwrap();
std::os::unix::fs::symlink("first", &selected).unwrap();
let alias = SelectedIndexAlias::capture(&selected).unwrap().unwrap();
fs::write(root.path().join("replacement"), b"replacement").unwrap();
fs::rename(root.path().join("replacement"), &first).unwrap();
assert!(matches!(
alias.check(None),
Err(CreateWorktreeError::IndexStorage {
registration: None,
source: crate::index::StorageError::Changed(_),
})
));
}
#[cfg(unix)]
#[test]
fn dangling_selected_alias_rejects_new_referent_before_creation() {
let root = tempfile::tempdir().unwrap();
let selected = root.path().join("selected");
std::os::unix::fs::symlink("referent", &selected).unwrap();
let alias = SelectedIndexAlias::capture(&selected).unwrap().unwrap();
fs::write(root.path().join("referent"), b"foreign").unwrap();
assert!(matches!(
alias.check(None),
Err(CreateWorktreeError::IndexStorage {
registration: None,
source: crate::index::StorageError::Changed(_),
})
));
}
#[cfg(unix)]
#[test]
fn failed_creation_reports_index_cleanup_failure() {
let root = tempfile::tempdir().unwrap();
let edit = crate::index::IndexEdit::acquire(
crate::ObjectFormat::Sha1,
root.path().join("selected"),
crate::index::Limits::default(),
crate::index::EditOptions::default(),
)
.unwrap();
{
fs::rename(
root.path().join("selected.lock"),
root.path().join("owned.lock"),
)
.unwrap();
fs::write(root.path().join("selected.lock"), b"replacement owner").unwrap();
let error = finish_creation(Err(CreateWorktreeError::Limit), Some(edit)).unwrap_err();
assert!(
matches!(error, CreateWorktreeError::Cleanup { operation, .. } if matches!(*operation, CreateWorktreeError::Limit))
);
assert_eq!(
fs::read(root.path().join("selected.lock")).unwrap(),
b"replacement owner"
);
}
}
#[rstest]
#[case::disabled(false)]
#[case::requested(true)]
fn reference_file_sync_failure_retains_registration(#[case] requested: bool) {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("table.ref");
let called = std::cell::Cell::new(false);
let result = write_shared_file_with_sync(
&path,
b"retained table",
root.path(),
SharedPermissions::Umask,
requested,
|_| {
called.set(true);
Err(io::Error::other("injected synchronization failure"))
},
);
assert_eq!(called.get(), requested);
if requested {
assert!(
matches!(result, Err(CreateWorktreeError::Synchronize { registration, .. }) if registration == root.path())
);
} else {
result.unwrap();
}
assert_eq!(fs::read(path).unwrap(), b"retained table");
}
}