use std::{
path::Path,
sync::atomic::{AtomicBool, Ordering},
};
use gix_error::{ClassificationMarker, ErrorExt, ResultExt, bail, ensure, message};
use gix_utils::progress::{NestedProgress, Progress};
use crate::{Result, repository::FormatVersion};
use gix_ref::transaction::{LogChange, PreviousValue, RefEdit, RefLog};
use crate::config::{
cache::util::ApplyLeniency,
tree::{Core, Extensions, Worktree},
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Head {
Attached(gix_ref::FullName),
Detached(gix_hash::ObjectId),
}
#[derive(Debug)]
#[expect(missing_docs)]
pub enum Error {
NotLocalBranch {
name: gix_ref::FullName,
},
CheckedOut {
name: gix_ref::FullName,
worktree_dirs: Vec<std::path::PathBuf>,
},
DestinationRegistered {
destination: std::path::PathBuf,
},
Interrupted,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::NotLocalBranch { name } => write!(f, "{name:?} is not a local branch"),
Error::CheckedOut { name, worktree_dirs } => {
write!(f, "The local branch {name:?} is checked out in {worktree_dirs:?}")
}
Error::DestinationRegistered { destination } => {
write!(
f,
"The worktree destination {} is already registered",
destination.display()
)
}
Error::Interrupted => f.write_str("Adding the worktree was interrupted"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(match self {
Error::NotLocalBranch { .. } => const { &ClassificationMarker::VALIDATION },
Error::CheckedOut { .. } | Error::DestinationRegistered { .. } => const { &ClassificationMarker::CONFLICT },
Error::Interrupted => const { &ClassificationMarker::CANCELLED },
})
}
}
impl crate::Repository {
pub fn add_worktree<P>(
&self,
destination: impl AsRef<Path>,
head: Head,
mut progress: P,
should_interrupt: &AtomicBool,
) -> Result<(crate::Repository, gix_worktree_state::checkout::Outcome)>
where
P: NestedProgress,
P::SubProgress: NestedProgress + 'static,
{
let destination = destination.as_ref();
let relative_paths = Worktree::USE_RELATIVE_PATHS
.enrich_error(self.config.resolved.boolean(Worktree::USE_RELATIVE_PATHS))
.with_leniency(self.config.lenient_config)?
.unwrap_or_default();
let (head_target, commit_id, root_tree_id) = match head {
Head::Attached(name) => {
if name.category() != Some(gix_ref::Category::LocalBranch) {
bail!(Error::NotLocalBranch { name });
}
let checked_out = self.checked_out_branches_without_namespace()?;
if let Some(worktree_dirs) = checked_out.get(&name) {
bail!(Error::CheckedOut {
name,
worktree_dirs: worktree_dirs.clone(),
});
}
let mut source = self.clone();
source.clear_namespace();
let mut reference = source
.find_reference(name.as_ref())
.or_raise(|| message("The local branch could not be found"))?;
let commit = reference.peel_to_commit()?;
let root_tree_id = commit.tree_id()?.detach();
(gix_ref::Target::Symbolic(name), commit.id, root_tree_id)
}
Head::Detached(commit_id) => {
let root_tree_id = self
.find_commit(commit_id)
.or_raise(|| message("The detached target is not an existing commit"))?
.tree_id()?
.detach();
(gix_ref::Target::Object(commit_id), commit_id, root_tree_id)
}
};
ensure!(!should_interrupt.load(Ordering::Relaxed), Error::Interrupted);
let main_repo = self
.main_repo()
.or_raise(|| message("Could not open a worktree repository"))?;
let mut registered_destinations = main_repo.workdir().map(Path::to_owned).into_iter().collect::<Vec<_>>();
for worktree in self.worktrees()? {
registered_destinations.push(
worktree
.base()
.or_raise(|| message("Failed to read or iterate worktree directories"))?,
);
}
let prepared = gix_worktree::add::prepare(
self.common_dir(),
destination,
gix_worktree::add::Options { relative_paths },
)
.or_raise(|| message("Could not prepare the linked worktree"))?;
let canonical_destination =
std::fs::canonicalize(prepared.work_dir()).or_raise(|| message("Could not prepare the linked worktree"))?;
for registered_destination in registered_destinations {
let registered_destination = gix_path::realpath(registered_destination)
.or_raise(|| message("Failed to resolve a registered worktree directory"))?;
let same_destination = if registered_destination == prepared.work_dir() {
true
} else {
match std::fs::canonicalize(®istered_destination) {
Ok(path) => path == canonical_destination,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
Err(err) => {
bail!(err.and_raise(message("Failed to resolve a registered worktree directory")));
}
}
};
if same_destination {
bail!(Error::DestinationRegistered {
destination: destination.to_owned(),
});
}
}
if relative_paths {
let mut config = self
.config_file_mut(self.common_dir().join("config"))
.or_raise(|| message("Could not enable relative worktrees in the shared configuration"))?;
let version = Core::REPOSITORY_FORMAT_VERSION
.try_into_repository_format_version(config.integer(Core::REPOSITORY_FORMAT_VERSION))
.or_raise(|| message("Could not upgrade the repository format for relative worktrees"))?
.unwrap_or_default();
if version == FormatVersion::V0 {
version.validate_upgrade_to_v1(&config)?;
config
.set_raw_value(Core::REPOSITORY_FORMAT_VERSION, "1")
.or_raise(|| message("Could not set the repository format for relative worktrees"))?;
}
let enabled = Extensions::RELATIVE_WORKTREES
.enrich_error(config.boolean(Extensions::RELATIVE_WORKTREES))?
.unwrap_or_default();
if version == FormatVersion::V0 || !enabled {
config
.set_raw_value(Extensions::RELATIVE_WORKTREES, "true")
.or_raise(|| message("Could not set the repository format for relative worktrees"))?;
config
.commit()
.or_raise(|| message("Could not enable relative worktrees in the shared configuration"))?;
}
}
let mut head_contents = Vec::new();
let reflog_mode = match &head_target {
gix_ref::Target::Object(_) => {
self.object_hash()
.null()
.write_hex_to(&mut head_contents)
.or_raise(|| message("Could not write the linked worktree HEAD"))?;
RefLog::AndReference
}
gix_ref::Target::Symbolic(name) => {
head_contents.extend_from_slice(b"ref: ");
head_contents.extend_from_slice(name.as_bstr());
RefLog::Only
}
};
head_contents.push(b'\n');
std::fs::write(prepared.git_dir().join("HEAD"), head_contents)
.or_raise(|| message("Could not write the linked worktree HEAD"))?;
if Extensions::WORKTREE_CONFIG
.enrich_error(self.config.resolved.boolean(Extensions::WORKTREE_CONFIG))
.with_leniency(self.config.lenient_config)?
.unwrap_or_default()
{
copy_worktree_config(
&self.git_dir().join("config.worktree"),
&prepared.git_dir().join("config.worktree"),
)?;
}
let options = self
.options
.clone()
.without_repository_environment_overrides()
.open_path_as_is(true);
let mut repo = crate::ThreadSafeRepository::open_opts(prepared.git_dir(), options)
.or_raise(|| message("Could not open a worktree repository"))?
.to_thread_local();
repo.clear_namespace();
repo.edit_reference(RefEdit::update_with_log(
"HEAD".try_into().expect("valid reference name"),
commit_id,
PreviousValue::Any,
LogChange {
mode: reflog_mode,
..Default::default()
},
))
.or_raise(|| message("Could not initialize the linked worktree HEAD and its reflog"))?;
let mut index = repo
.index_from_tree(&root_tree_id)
.or_raise(|| message("Could not create an index from the target tree"))?;
let mut checkout_options = repo.checkout_options(gix_worktree::stack::state::attributes::Source::IdMapping)?;
checkout_options.destination_is_initially_empty = true;
let mut files = progress.add_child("checkout");
let mut bytes = progress.add_child("writing");
files.init(Some(index.entries().len()), crate::progress::count("files"));
bytes.init(None, crate::progress::bytes());
let started = std::time::Instant::now();
let outcome = gix_worktree_state::checkout(
&mut index,
prepared.work_dir(),
repo.objects
.clone()
.into_arc()
.or_raise(|| message("Failed to reopen the object database for checkout"))?,
&files,
&bytes,
should_interrupt,
checkout_options,
)?;
files.show_throughput(started);
bytes.show_throughput(started);
ensure!(!should_interrupt.load(Ordering::Relaxed), Error::Interrupted);
index
.write(Default::default())
.or_raise(|| message("Could not write the linked worktree index"))?;
prepared
.persist()
.or_raise(|| message("Could not finish adding the linked worktree"))?;
Ok((repo, outcome))
}
}
fn copy_worktree_config(source: &Path, destination: &Path) -> Result<()> {
let mut config = match gix_config::File::from_path_no_includes(source.to_owned(), gix_config::Source::Worktree) {
Ok(config) => config,
Err(err) if err.is_not_found() => return Ok(()),
Err(err) => {
bail!(err.and_raise(message("Could not read the source worktree configuration")));
}
};
if Core::BARE.enrich_error(config.boolean(Core::BARE))?.unwrap_or_default()
&& let Ok(mut values) = config.raw_values_mut(Core::BARE)
{
values.delete_all();
}
if let Ok(mut values) = config.raw_values_mut(Core::WORKTREE) {
values.delete_all();
}
let mut destination =
std::fs::File::create(destination).or_raise(|| message("Could not write the new worktree configuration"))?;
config
.write_to(&mut destination)
.or_raise(|| message("Could not write the new worktree configuration"))?;
Ok(())
}