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, WorktreeLinkStyle};
use crate::refs::{Backend, RefName, Target};
#[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(transparent)]
Reftable(#[from] crate::refs::reftable::Error),
#[error("cannot create {path}: {source}")]
Io {
path: PathBuf,
registration: Option<PathBuf>,
#[source]
source: io::Error,
},
#[error("created worktree metadata failed validation: {source}")]
Open {
registration: PathBuf,
#[source]
source: Box<super::OpenError>,
},
}
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> {
if max_names == 0 {
return Err(CreateWorktreeError::Limit);
}
if !branch.as_bytes().starts_with(b"refs/heads/") {
return Err(CreateWorktreeError::Branch);
}
if self.references()?.read(branch)?.is_some() {
return Err(CreateWorktreeError::ExistingBranch);
}
let destination = destination.as_ref();
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(self.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 index = crate::index::Index::empty(self.object_format())
.encode(crate::index::Limits::default())?;
let table = if self.reference_backend() == Backend::Reftable {
Some(
initial_head_table(self.object_format(), branch)
.encode(crate::refs::reftable::Limits::default())?,
)
} else {
None
};
let config_path = self.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 = link_style.uses_relative(configured_relative);
let registrations = self.common_dir().join("worktrees");
fs::create_dir_all(®istrations).map_err(|e| io_error(®istrations, None, e))?;
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,
)?;
if let Some(table) = table {
create_dir(®istration.join("reftable"), ®istration)?;
write_file(
®istration.join("reftable/initial.ref"),
&table,
®istration,
)?;
write_file(
®istration.join("reftable/tables.list"),
b"initial.ref\n",
®istration,
)?;
create_dir(®istration.join("refs"), ®istration)?;
write_file(
®istration.join("refs/heads"),
b"this repository uses the reftable format\n",
®istration,
)?;
write_file(
®istration.join("HEAD"),
b"ref: refs/heads/.invalid\n",
®istration,
)?;
} else {
let mut head = b"ref: ".to_vec();
head.extend_from_slice(branch.as_bytes());
head.push(b'\n');
write_file(®istration.join("HEAD"), &head, ®istration)?;
}
write_file(®istration.join("index"), &index, ®istration)?;
create_dir(&destination, ®istration)?;
write_file(
&destination.join(".git"),
&gitfile_line(&forward),
®istration,
)?;
Self::open(&destination).map_err(|source| CreateWorktreeError::Open {
registration: registration.clone(),
source: Box::new(source),
})
})()
}
}
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::new(b"HEAD").expect("fixed name").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"
);
}
}