Skip to main content

grit_lib/worktree/
error.rs

1//! Typed failures for linked worktree add, remove, move, lock and repair.
2
3use std::fmt;
4use std::path::PathBuf;
5
6use crate::git_path::user_display_path;
7
8/// Linked worktree mutation that was pointed at the main working tree.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum WorktreeOperation {
11    /// [`remove_linked_worktree`](crate::worktree::remove_linked_worktree).
12    Remove,
13    /// [`move_linked_worktree`](crate::worktree::move_linked_worktree).
14    Move,
15    /// [`RepositoryWorktrees::lock`](crate::repo_handles::RepositoryWorktrees::lock).
16    Lock,
17    /// [`RepositoryWorktrees::unlock`](crate::repo_handles::RepositoryWorktrees::unlock).
18    Unlock,
19}
20
21impl WorktreeOperation {
22    /// Lowercase verb for the operation (`remove`, `move`, `lock`, `unlock`).
23    #[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/// Linked worktree failures, wrapped by [`Error::Worktree`](crate::error::Error::Worktree).
41///
42/// Branch problems met while adding a worktree (invalid, missing or existing branch names)
43/// are reported as [`BranchError`](crate::porcelain::branch_error::BranchError) instead, and a
44/// branch held by another worktree as
45/// [`Error::BranchCheckedOutInWorktree`](crate::error::Error::BranchCheckedOutInWorktree).
46#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
47#[non_exhaustive]
48pub enum WorktreeError {
49    /// Linked worktrees cannot be added to a bare repository.
50    #[error("cannot add a worktree to a bare repository")]
51    BareRepository,
52
53    /// No registered worktree has `path` as its working tree root.
54    #[error("'{}' is not a registered worktree", user_display_path(.path))]
55    NotRegistered {
56        /// Normalized path that matched no registry entry.
57        path: PathBuf,
58    },
59
60    /// The operation only applies to linked worktrees.
61    #[error("cannot {operation} the main working tree")]
62    MainWorktree {
63        /// Operation that was refused.
64        operation: WorktreeOperation,
65    },
66
67    /// The worktree is locked and the caller did not force the operation.
68    #[error("{}", with_reason("worktree is locked", .reason))]
69    Locked {
70        /// Contents of the `locked` file, when non-empty.
71        reason: Option<String>,
72    },
73
74    /// Lock was requested for a worktree that is already locked.
75    #[error("{}", with_reason("worktree is already locked", .reason))]
76    AlreadyLocked {
77        /// Contents of the existing `locked` file, when non-empty.
78        reason: Option<String>,
79    },
80
81    /// Unlock was requested for a worktree that is not locked.
82    #[error("worktree is not locked")]
83    NotLocked,
84
85    /// Remove without force found staged, unstaged, conflicted or untracked changes.
86    #[error("worktree contains modified or untracked files; use -f or --force to remove")]
87    HasLocalChanges,
88
89    /// Move found populated submodules inside the worktree.
90    #[error("working trees containing submodules cannot be moved or removed")]
91    HasSubmodules,
92
93    /// The path is registered to a worktree whose directory still exists.
94    #[error("'{}' is already a registered worktree", user_display_path(.path))]
95    PathRegistered {
96        /// Registered working tree path.
97        path: PathBuf,
98    },
99
100    /// The path is registered to a worktree whose directory is missing, and add was not forced.
101    #[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        /// Registered working tree path that is missing on disk.
107        path: PathBuf,
108    },
109
110    /// The target directory exists and is not empty (add) or exists at all (move).
111    #[error("'{}' already exists", user_display_path(.path))]
112    PathExists {
113        /// Occupied path.
114        path: PathBuf,
115    },
116
117    /// The add target exists and is a file or other non-directory.
118    #[error("'{}' already exists and is not a directory", user_display_path(.path))]
119    PathExistsNotDirectory {
120        /// Occupied path.
121        path: PathBuf,
122    },
123
124    /// A path that must be a directory (repair target, move destination parent) is not one.
125    #[error("'{}' is not a directory", user_display_path(.path))]
126    NotADirectory {
127        /// Offending path.
128        path: PathBuf,
129    },
130
131    /// A repair target does not exist.
132    #[error("'{}' is not a valid path", user_display_path(.path))]
133    PathNotFound {
134        /// Missing path.
135        path: PathBuf,
136    },
137
138    /// The path has no final component to name the worktree after (for example `/`).
139    #[error("invalid worktree path '{}'", user_display_path(.path))]
140    NoFileName {
141        /// Path without a final component.
142        path: PathBuf,
143    },
144
145    /// A worktree admin directory has no parent directory.
146    #[error("invalid worktree admin directory '{}'", user_display_path(.path))]
147    InvalidAdminDir {
148        /// Admin directory path.
149        path: PathBuf,
150    },
151
152    /// An existing branch used for the worktree `HEAD` does not point at the start commit.
153    #[error("branch '{branch}' does not match start commit")]
154    BranchTipMismatch {
155        /// Short branch name.
156        branch: String,
157    },
158
159    /// Repair expected a `.git` gitfile but found a directory.
160    #[error("'{}' is a directory, not a worktree gitfile", user_display_path(.path))]
161    DotGitIsDirectory {
162        /// The worktree's `.git` path.
163        path: PathBuf,
164    },
165
166    /// Repair could not map a worktree's `.git` gitfile to a registered admin directory.
167    #[error("'{}' does not point at a registered worktree", user_display_path(.path))]
168    BrokenGitFile {
169        /// The worktree's `.git` path.
170        path: PathBuf,
171    },
172
173    /// Another process holds the lock file guarding a worktree link file.
174    #[error("could not acquire lock for '{}'", user_display_path(.path))]
175    LockHeld {
176        /// Lock file path.
177        path: PathBuf,
178    },
179
180    /// A gitlink entry in the worktree index has a path that is not valid UTF-8.
181    #[error(
182        "submodule path in index is not valid UTF-8: {}",
183        String::from_utf8_lossy(.path)
184    )]
185    NonUtf8SubmodulePath {
186        /// Raw index path bytes.
187        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}