1use std::fmt;
4use std::path::PathBuf;
5
6use crate::git_path::user_display_path;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum WorktreeOperation {
11 Remove,
13 Move,
15 Lock,
17 Unlock,
19}
20
21impl WorktreeOperation {
22 #[must_use]
24 pub fn as_str(self) -> &'static str {
25 match self {
26 Self::Remove => "remove",
27 Self::Move => "move",
28 Self::Lock => "lock",
29 Self::Unlock => "unlock",
30 }
31 }
32}
33
34impl fmt::Display for WorktreeOperation {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.write_str(self.as_str())
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
47#[non_exhaustive]
48pub enum WorktreeError {
49 #[error("cannot add a worktree to a bare repository")]
51 BareRepository,
52
53 #[error("'{}' is not a registered worktree", user_display_path(.path))]
55 NotRegistered {
56 path: PathBuf,
58 },
59
60 #[error("cannot {operation} the main working tree")]
62 MainWorktree {
63 operation: WorktreeOperation,
65 },
66
67 #[error("{}", with_reason("worktree is locked", .reason))]
69 Locked {
70 reason: Option<String>,
72 },
73
74 #[error("{}", with_reason("worktree is already locked", .reason))]
76 AlreadyLocked {
77 reason: Option<String>,
79 },
80
81 #[error("worktree is not locked")]
83 NotLocked,
84
85 #[error("worktree contains modified or untracked files; use -f or --force to remove")]
87 HasLocalChanges,
88
89 #[error("working trees containing submodules cannot be moved or removed")]
91 HasSubmodules,
92
93 #[error("'{}' is already a registered worktree", user_display_path(.path))]
95 PathRegistered {
96 path: PathBuf,
98 },
99
100 #[error(
102 "'{}' is a missing but already registered worktree; use add with --force to override, or prune or remove to clear",
103 user_display_path(.path)
104 )]
105 PathRegisteredMissing {
106 path: PathBuf,
108 },
109
110 #[error("'{}' already exists", user_display_path(.path))]
112 PathExists {
113 path: PathBuf,
115 },
116
117 #[error("'{}' already exists and is not a directory", user_display_path(.path))]
119 PathExistsNotDirectory {
120 path: PathBuf,
122 },
123
124 #[error("'{}' is not a directory", user_display_path(.path))]
126 NotADirectory {
127 path: PathBuf,
129 },
130
131 #[error("'{}' is not a valid path", user_display_path(.path))]
133 PathNotFound {
134 path: PathBuf,
136 },
137
138 #[error("invalid worktree path '{}'", user_display_path(.path))]
140 NoFileName {
141 path: PathBuf,
143 },
144
145 #[error("invalid worktree admin directory '{}'", user_display_path(.path))]
147 InvalidAdminDir {
148 path: PathBuf,
150 },
151
152 #[error("branch '{branch}' does not match start commit")]
154 BranchTipMismatch {
155 branch: String,
157 },
158
159 #[error("'{}' is a directory, not a worktree gitfile", user_display_path(.path))]
161 DotGitIsDirectory {
162 path: PathBuf,
164 },
165
166 #[error("'{}' does not point at a registered worktree", user_display_path(.path))]
168 BrokenGitFile {
169 path: PathBuf,
171 },
172
173 #[error("could not acquire lock for '{}'", user_display_path(.path))]
175 LockHeld {
176 path: PathBuf,
178 },
179
180 #[error(
182 "submodule path in index is not valid UTF-8: {}",
183 String::from_utf8_lossy(.path)
184 )]
185 NonUtf8SubmodulePath {
186 path: Vec<u8>,
188 },
189}
190
191fn with_reason(prefix: &str, reason: &Option<String>) -> String {
192 match reason {
193 Some(r) => format!("{prefix}: {r}"),
194 None => prefix.to_owned(),
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn locked_messages_include_reason_when_present() {
204 assert_eq!(
205 WorktreeError::Locked { reason: None }.to_string(),
206 "worktree is locked"
207 );
208 assert_eq!(
209 WorktreeError::AlreadyLocked {
210 reason: Some("hold".into())
211 }
212 .to_string(),
213 "worktree is already locked: hold"
214 );
215 }
216
217 #[test]
218 fn main_worktree_message_names_operation() {
219 let msgs: Vec<String> = [
220 WorktreeOperation::Remove,
221 WorktreeOperation::Move,
222 WorktreeOperation::Lock,
223 WorktreeOperation::Unlock,
224 ]
225 .into_iter()
226 .map(|operation| WorktreeError::MainWorktree { operation }.to_string())
227 .collect();
228 assert_eq!(
229 msgs,
230 [
231 "cannot remove the main working tree",
232 "cannot move the main working tree",
233 "cannot lock the main working tree",
234 "cannot unlock the main working tree",
235 ]
236 );
237 }
238
239 #[cfg(unix)]
240 #[test]
241 fn path_variants_display_the_path() {
242 let path = PathBuf::from("/tmp/wt");
243 let err = WorktreeError::NotRegistered { path: path.clone() };
244 assert_eq!(err.to_string(), "'/tmp/wt' is not a registered worktree");
245 let err = WorktreeError::PathExists { path };
246 assert_eq!(err.to_string(), "'/tmp/wt' already exists");
247 }
248}