1use std::path::PathBuf;
8
9pub const EXIT_REFUSED: u8 = 10;
12pub const EXIT_MISSING: u8 = 12;
14pub const EXIT_INTERNAL: u8 = 13;
16
17#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
19pub enum WorktreeError {
20 #[error("not a git repository: {start} is not inside a git work tree; the worktree topology needs one")]
22 NotInGitRepository {
23 start: PathBuf,
25 },
26
27 #[error(
29 "the worktree topology requires a non-bare primary checkout; {git_dir} is a bare repository, \
30 which has no primary work tree to derive a sibling container from"
31 )]
32 BareRepositoryUnsupported {
33 git_dir: PathBuf,
35 },
36
37 #[error(
39 "cannot determine the primary worktree of {git_common_dir}: `git worktree list --porcelain` \
40 named no non-bare main work tree; the repository metadata is incomplete"
41 )]
42 CannotDeterminePrimaryWorktree {
43 git_common_dir: PathBuf,
45 },
46
47 #[error(
49 "cannot derive a sibling worktree container for {primary}: it has no parent directory"
50 )]
51 NoParentDirectory {
52 primary: PathBuf,
54 },
55
56 #[error("invalid branch name '{given}': {reason}")]
58 InvalidBranchName {
59 given: String,
61 reason: String,
63 },
64
65 #[error(
68 "the worktree path derived for branch '{branch}' ({path}) is not inside the container {root}; \
69 nothing was created"
70 )]
71 PathEscape {
72 branch: String,
74 path: PathBuf,
76 root: PathBuf,
78 },
79
80 #[error(
82 "the worktree container {path} exists and is not a directory; it is where every linked \
83 worktree of this repository belongs, so it cannot be a file or a symlink to one"
84 )]
85 ContainerNotADirectory {
86 path: PathBuf,
88 },
89
90 #[error(
92 "branch '{branch}' already has its canonical worktree at {path}; nothing was created. \
93 Work there: cd \"$(majordomus worktree path {branch})\""
94 )]
95 WorktreeAlreadyExists {
96 path: PathBuf,
98 branch: String,
100 },
101
102 #[error(
104 "branch '{branch}' is checked out at {path}, which is not its canonical worktree \
105 {expected}; git allows one checkout of a branch at a time. Bring it home with \
106 `majordomus worktree migrate` and work there"
107 )]
108 BranchAlreadyCheckedOut {
109 branch: String,
111 path: PathBuf,
113 expected: PathBuf,
115 },
116
117 #[error(
119 "{path} already exists and is {what}; nothing was created, nothing was moved and nothing \
120 was deleted. Move it aside, or choose another branch name"
121 )]
122 DestinationConflict {
123 path: PathBuf,
125 what: String,
127 },
128
129 #[error("base ref '{base}' does not resolve in this repository; nothing was created and nothing was fetched")]
131 BaseDoesNotExist {
132 base: String,
134 },
135
136 #[error(
138 "worktree {path} holds branch '{branch}' but that branch belongs at {expected}; \
139 feature work happens in the canonical worktree. Run `majordomus worktree migrate --plan` \
140 to see the move, `majordomus worktree migrate` to make it, then continue there"
141 )]
142 Misplaced {
143 path: PathBuf,
145 branch: String,
147 expected: PathBuf,
149 },
150
151 #[error(
153 "the primary checkout {path} is on branch '{branch}', and the primary checkout hosts the trunk \
154 ('{trunk}'); that branch belongs at {expected}. Nothing was changed: switch the primary \
155 checkout back to the trunk when it is clean, and work on '{branch}' in its canonical worktree \
156 (`majordomus worktree create {branch}`)"
157 )]
158 PrimaryOnNonTrunk {
159 path: PathBuf,
161 branch: String,
163 trunk: String,
165 expected: PathBuf,
167 },
168
169 #[error(
171 "worktree {path} is dirty ({summary}); {operation} would lose that work. Commit it there \
172 first, or pass --force to say that losing it is intended"
173 )]
174 DirtyWorktree {
175 path: PathBuf,
177 summary: String,
179 operation: String,
181 },
182
183 #[error(
185 "worktree {path} is locked{}; unlock it first (git worktree unlock {path})",
186 reason.as_ref().filter(|r| !r.is_empty()).map(|r| format!(" ({r})")).unwrap_or_default()
187 )]
188 LockedWorktree {
189 path: PathBuf,
191 reason: Option<String>,
193 },
194
195 #[error(
197 "{path} is this repository's primary checkout; `worktree {operation}` operates on linked \
198 worktrees only and never moves or deletes the primary checkout"
199 )]
200 PrimaryWorktreeProtected {
201 path: PathBuf,
203 operation: String,
205 },
206
207 #[error(
209 "no worktree of this repository matches '{selector}'; a selector is an exact branch name \
210 or an exact path. `majordomus worktree list` shows them"
211 )]
212 NoSuchWorktree {
213 selector: String,
215 },
216
217 #[error(
219 "{path} is not inside a worktree of this repository (git directory {git_common_dir}); \
220 the topology answers only about its own repository"
221 )]
222 NotThisRepository {
223 path: PathBuf,
225 git_common_dir: PathBuf,
227 },
228
229 #[error("cannot migrate {path} safely: {reason}; nothing was moved")]
231 MigrationUnsafe {
232 path: PathBuf,
234 reason: String,
236 },
237
238 #[error(
240 "migration of {path} could not be verified: {}. The worktree is registered at {path}; \
241 inspect it before touching anything else (majordomus worktree status --repo {path})",
242 differences.join("; ")
243 )]
244 VerificationFailed {
245 path: PathBuf,
247 differences: Vec<String>,
249 },
250
251 #[error(
253 "another process is changing this repository's worktrees (lock {path}{}); \
254 nothing was changed. Retry when it finishes",
255 holder.as_ref().map(|h| format!(", held by {h}")).unwrap_or_default()
256 )]
257 LockUnavailable {
258 path: PathBuf,
260 holder: Option<String>,
262 },
263
264 #[error("the worktree topology has {count} error(s); `majordomus worktree doctor` lists them")]
266 TopologyInvalid {
267 count: usize,
269 },
270
271 #[error("git {command} failed ({status}): {stderr}")]
273 GitCommandFailed {
274 command: String,
276 status: String,
278 stderr: String,
280 },
281
282 #[error("cannot run git: {reason}")]
284 GitUnavailable {
285 reason: String,
287 },
288
289 #[error("{path}: {reason}")]
291 Io {
292 path: PathBuf,
294 reason: String,
296 },
297}
298
299impl WorktreeError {
300 pub fn exit_code(&self) -> u8 {
313 use WorktreeError::*;
314 match self {
315 NoSuchWorktree { .. } | BaseDoesNotExist { .. } => EXIT_MISSING,
316 GitCommandFailed { .. } | GitUnavailable { .. } | Io { .. } => EXIT_INTERNAL,
317 _ => EXIT_REFUSED,
318 }
319 }
320
321 pub fn code(&self) -> &'static str {
333 use WorktreeError::*;
334 match self {
335 NotInGitRepository { .. } => "NotInGitRepository",
336 BareRepositoryUnsupported { .. } => "BareRepositoryUnsupported",
337 CannotDeterminePrimaryWorktree { .. } => "CannotDeterminePrimaryWorktree",
338 NoParentDirectory { .. } => "NoParentDirectory",
339 InvalidBranchName { .. } => "InvalidBranchName",
340 PathEscape { .. } => "PathEscape",
341 ContainerNotADirectory { .. } => "ContainerNotADirectory",
342 WorktreeAlreadyExists { .. } => "WorktreeAlreadyExists",
343 BranchAlreadyCheckedOut { .. } => "BranchAlreadyCheckedOut",
344 DestinationConflict { .. } => "DestinationConflict",
345 BaseDoesNotExist { .. } => "BaseDoesNotExist",
346 Misplaced { .. } => "Misplaced",
347 PrimaryOnNonTrunk { .. } => "PrimaryOnNonTrunk",
348 DirtyWorktree { .. } => "DirtyWorktree",
349 LockedWorktree { .. } => "LockedWorktree",
350 PrimaryWorktreeProtected { .. } => "PrimaryWorktreeProtected",
351 NoSuchWorktree { .. } => "NoSuchWorktree",
352 NotThisRepository { .. } => "NotThisRepository",
353 MigrationUnsafe { .. } => "MigrationUnsafe",
354 VerificationFailed { .. } => "VerificationFailed",
355 LockUnavailable { .. } => "LockUnavailable",
356 TopologyInvalid { .. } => "TopologyInvalid",
357 GitCommandFailed { .. } => "GitCommandFailed",
358 GitUnavailable { .. } => "GitUnavailable",
359 Io { .. } => "Io",
360 }
361 }
362
363 pub fn io(path: impl Into<PathBuf>, e: &std::io::Error) -> Self {
365 WorktreeError::Io {
366 path: path.into(),
367 reason: e.to_string(),
368 }
369 }
370}
371
372pub type Result<T> = std::result::Result<T, WorktreeError>;