use std::fmt;
use std::path::PathBuf;
use crate::git_path::user_display_path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorktreeOperation {
Remove,
Move,
Lock,
Unlock,
}
impl WorktreeOperation {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Remove => "remove",
Self::Move => "move",
Self::Lock => "lock",
Self::Unlock => "unlock",
}
}
}
impl fmt::Display for WorktreeOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum WorktreeError {
#[error("cannot add a worktree to a bare repository")]
BareRepository,
#[error("'{}' is not a registered worktree", user_display_path(.path))]
NotRegistered {
path: PathBuf,
},
#[error("cannot {operation} the main working tree")]
MainWorktree {
operation: WorktreeOperation,
},
#[error("{}", with_reason("worktree is locked", .reason))]
Locked {
reason: Option<String>,
},
#[error("{}", with_reason("worktree is already locked", .reason))]
AlreadyLocked {
reason: Option<String>,
},
#[error("worktree is not locked")]
NotLocked,
#[error("worktree contains modified or untracked files; use -f or --force to remove")]
HasLocalChanges,
#[error("working trees containing submodules cannot be moved or removed")]
HasSubmodules,
#[error("'{}' is already a registered worktree", user_display_path(.path))]
PathRegistered {
path: PathBuf,
},
#[error(
"'{}' is a missing but already registered worktree; use add with --force to override, or prune or remove to clear",
user_display_path(.path)
)]
PathRegisteredMissing {
path: PathBuf,
},
#[error("'{}' already exists", user_display_path(.path))]
PathExists {
path: PathBuf,
},
#[error("'{}' already exists and is not a directory", user_display_path(.path))]
PathExistsNotDirectory {
path: PathBuf,
},
#[error("'{}' is not a directory", user_display_path(.path))]
NotADirectory {
path: PathBuf,
},
#[error("'{}' is not a valid path", user_display_path(.path))]
PathNotFound {
path: PathBuf,
},
#[error("invalid worktree path '{}'", user_display_path(.path))]
NoFileName {
path: PathBuf,
},
#[error("invalid worktree admin directory '{}'", user_display_path(.path))]
InvalidAdminDir {
path: PathBuf,
},
#[error("branch '{branch}' does not match start commit")]
BranchTipMismatch {
branch: String,
},
#[error("'{}' is a directory, not a worktree gitfile", user_display_path(.path))]
DotGitIsDirectory {
path: PathBuf,
},
#[error("'{}' does not point at a registered worktree", user_display_path(.path))]
BrokenGitFile {
path: PathBuf,
},
#[error("could not acquire lock for '{}'", user_display_path(.path))]
LockHeld {
path: PathBuf,
},
#[error(
"submodule path in index is not valid UTF-8: {}",
String::from_utf8_lossy(.path)
)]
NonUtf8SubmodulePath {
path: Vec<u8>,
},
}
fn with_reason(prefix: &str, reason: &Option<String>) -> String {
match reason {
Some(r) => format!("{prefix}: {r}"),
None => prefix.to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locked_messages_include_reason_when_present() {
assert_eq!(
WorktreeError::Locked { reason: None }.to_string(),
"worktree is locked"
);
assert_eq!(
WorktreeError::AlreadyLocked {
reason: Some("hold".into())
}
.to_string(),
"worktree is already locked: hold"
);
}
#[test]
fn main_worktree_message_names_operation() {
let msgs: Vec<String> = [
WorktreeOperation::Remove,
WorktreeOperation::Move,
WorktreeOperation::Lock,
WorktreeOperation::Unlock,
]
.into_iter()
.map(|operation| WorktreeError::MainWorktree { operation }.to_string())
.collect();
assert_eq!(
msgs,
[
"cannot remove the main working tree",
"cannot move the main working tree",
"cannot lock the main working tree",
"cannot unlock the main working tree",
]
);
}
#[cfg(unix)]
#[test]
fn path_variants_display_the_path() {
let path = PathBuf::from("/tmp/wt");
let err = WorktreeError::NotRegistered { path: path.clone() };
assert_eq!(err.to_string(), "'/tmp/wt' is not a registered worktree");
let err = WorktreeError::PathExists { path };
assert_eq!(err.to_string(), "'/tmp/wt' already exists");
}
}