Skip to main content

workon/
error.rs

1use std::path::PathBuf;
2
3use miette::Diagnostic;
4use thiserror::Error;
5
6/// Result type alias using WorkonError
7pub type Result<T> = std::result::Result<T, WorkonError>;
8
9/// Main error type for the workon library
10#[derive(Error, Diagnostic, Debug)]
11pub enum WorkonError {
12    /// Git operation failed
13    #[error(transparent)]
14    #[diagnostic(code(workon::git_error))]
15    Git(#[from] git2::Error),
16
17    /// I/O operation failed
18    #[error(transparent)]
19    #[diagnostic(code(workon::io_error))]
20    Io(#[from] std::io::Error),
21
22    /// Repository-related errors
23    #[error(transparent)]
24    #[diagnostic(forward(0))]
25    Repo(#[from] RepoError),
26
27    /// Worktree-related errors
28    #[error(transparent)]
29    #[diagnostic(forward(0))]
30    Worktree(#[from] WorktreeError),
31
32    /// Configuration-related errors
33    #[error(transparent)]
34    #[diagnostic(forward(0))]
35    Config(#[from] ConfigError),
36
37    /// Default branch detection errors
38    #[error(transparent)]
39    #[diagnostic(forward(0))]
40    DefaultBranch(#[from] DefaultBranchError),
41
42    /// Pull request-related errors
43    #[error(transparent)]
44    #[diagnostic(forward(0))]
45    Pr(#[from] PrError),
46
47    /// File copy errors
48    #[error(transparent)]
49    #[diagnostic(forward(0))]
50    Copy(#[from] CopyError),
51
52    /// Stacked diff workflow errors
53    #[error(transparent)]
54    #[diagnostic(forward(0))]
55    Stack(#[from] StackError),
56
57    /// In-place checkout errors
58    #[error(transparent)]
59    #[diagnostic(forward(0))]
60    Checkout(#[from] CheckoutError),
61}
62
63/// Repository-specific errors
64#[derive(Error, Diagnostic, Debug)]
65pub enum RepoError {
66    #[error("Not a bare repository at {0}")]
67    #[diagnostic(
68        code(workon::repo::not_bare),
69        help("Workon commands must be run in bare repositories")
70    )]
71    NotBare(String),
72}
73
74/// Worktree-specific errors
75#[derive(Error, Diagnostic, Debug)]
76pub enum WorktreeError {
77    #[error("Invalid .git file format")]
78    #[diagnostic(
79        code(workon::worktree::invalid_git_file),
80        help("The .git file should contain 'gitdir: <path>' pointing to the git directory")
81    )]
82    InvalidGitFile,
83
84    #[error("Could not find worktree '{0}'")]
85    #[diagnostic(
86        code(workon::worktree::not_found),
87        help("Use 'git workon list' to see available worktrees")
88    )]
89    NotFound(String),
90
91    #[error("Not in a worktree directory")]
92    #[diagnostic(
93        code(workon::worktree::not_in_worktree),
94        help("Run this command from within a worktree directory")
95    )]
96    NotInWorktree,
97
98    #[error("Could not determine branch target")]
99    #[diagnostic(
100        code(workon::worktree::no_branch_target),
101        help("The branch may be in an invalid state")
102    )]
103    NoBranchTarget,
104
105    #[error("Could not get current branch target")]
106    #[diagnostic(code(workon::worktree::no_current_branch_target))]
107    NoCurrentBranchTarget,
108
109    #[error("Could not get local branch target")]
110    #[diagnostic(code(workon::worktree::no_local_branch_target))]
111    NoLocalBranchTarget,
112
113    #[error("Worktree path has no parent directory")]
114    #[diagnostic(
115        code(workon::worktree::no_parent),
116        help("Cannot create parent directories for worktree path")
117    )]
118    NoParent,
119
120    #[error("Invalid worktree name: contains invalid UTF-8")]
121    #[diagnostic(
122        code(workon::worktree::invalid_name),
123        help("Worktree names must be valid UTF-8 strings")
124    )]
125    InvalidName,
126
127    #[error("Expected an empty index!")]
128    #[diagnostic(code(workon::worktree::non_empty_index))]
129    NonEmptyIndex,
130
131    #[error("Worktree '{to}' already exists")]
132    #[diagnostic(
133        code(workon::worktree::target_exists),
134        help("Choose a different name or remove the existing worktree first")
135    )]
136    TargetExists { to: String },
137
138    #[error("Cannot move detached HEAD worktree")]
139    #[diagnostic(
140        code(workon::worktree::move_detached),
141        help("Detached HEAD worktrees have no branch to rename")
142    )]
143    CannotMoveDetached,
144
145    #[error("Branch '{0}' is protected and cannot be renamed")]
146    #[diagnostic(
147        code(workon::worktree::protected_branch_move),
148        help("Protected branches are configured in workon.pruneProtectedBranches. Use --force to override.")
149    )]
150    ProtectedBranchMove(String),
151
152    #[error("Worktree is dirty (uncommitted changes)")]
153    #[diagnostic(
154        code(workon::worktree::dirty_worktree),
155        help("Commit or stash changes, or use --force to override")
156    )]
157    DirtyWorktree,
158
159    #[error("Worktree has unpushed commits")]
160    #[diagnostic(
161        code(workon::worktree::unpushed_commits),
162        help("Push commits first, or use --force to override")
163    )]
164    UnpushedCommits,
165}
166
167/// Configuration-related errors
168#[derive(Error, Diagnostic, Debug)]
169pub enum ConfigError {
170    #[error("Invalid PR format: '{format}' - {reason}")]
171    #[diagnostic(
172        code(workon::config::invalid_pr_format),
173        help("Valid placeholders: {{number}}, {{title}}, {{author}}, {{branch}}")
174    )]
175    InvalidPrFormat { format: String, reason: String },
176
177    #[error("Config entry has no value")]
178    #[diagnostic(code(workon::config::no_value))]
179    NoValue,
180}
181
182/// Default branch detection errors
183#[derive(Error, Diagnostic, Debug)]
184pub enum DefaultBranchError {
185    #[error("Could not determine default branch for remote {remote:?}")]
186    #[diagnostic(
187        code(workon::default_branch::no_remote_default),
188        help("The remote may not have a default branch configured")
189    )]
190    NoRemoteDefault { remote: Option<String> },
191
192    #[error("Remote is not connected")]
193    #[diagnostic(
194        code(workon::default_branch::not_connected),
195        help("Failed to establish connection to remote repository")
196    )]
197    NotConnected,
198
199    #[error("Could not determine default branch: neither 'main' nor 'master' exist, and init.defaultBranch is not configured")]
200    #[diagnostic(
201        code(workon::default_branch::no_default_branch),
202        help("Set init.defaultBranch in your git config, or create a 'main' or 'master' branch")
203    )]
204    NoDefaultBranch,
205}
206
207/// Stacked diff workflow errors
208#[derive(Error, Diagnostic, Debug)]
209pub enum StackError {
210    #[error("Stack model '{model}' is not yet supported")]
211    #[diagnostic(
212        code(workon::stack::unsupported_model),
213        help(
214            "Only 'graphite' is implemented in this version. \
215             Support for branchless, sapling, and spr is planned."
216        )
217    )]
218    UnsupportedModel { model: String },
219
220    #[error("Unknown stack model '{value}'")]
221    #[diagnostic(
222        code(workon::stack::unknown_model),
223        help("Valid values: graphite, none, auto")
224    )]
225    UnknownModel { value: String },
226
227    #[error("Worktree granularity 'diff' is not yet implemented")]
228    #[diagnostic(
229        code(workon::stack::unsupported_granularity),
230        help(
231            "Only 'stack' (one worktree per stack) is supported in this version. \
232             'diff' (one worktree per branch) is planned."
233        )
234    )]
235    UnsupportedGranularity,
236
237    #[error("Unknown worktree granularity '{value}'")]
238    #[diagnostic(code(workon::stack::unknown_granularity), help("Valid values: stack"))]
239    UnknownGranularity { value: String },
240
241    #[error("Graphite CLI ('gt') is not installed or not in PATH")]
242    #[diagnostic(
243        code(workon::stack::gt_not_installed),
244        help(
245            "Install Graphite: https://graphite.dev/cli \
246             Or set workon.stackModel = none to disable stack support."
247        )
248    )]
249    GtNotInstalled,
250
251    #[error("Graphite command failed: {stderr}")]
252    #[diagnostic(code(workon::stack::gt_command_failed))]
253    GtCommandFailed { stderr: String },
254
255    #[error("Failed to parse Graphite metadata: {message}")]
256    #[diagnostic(code(workon::stack::gt_parse_failed))]
257    GtParseFailed { message: String },
258
259    #[error("Repository is not Graphite-managed (no .graphite_repo_config)")]
260    #[diagnostic(
261        code(workon::stack::not_a_graphite_repo),
262        help("Run 'gt init' in this repository, or unset workon.stackModel.")
263    )]
264    NotAGraphiteRepo,
265
266    #[error("Branch '{branch}' exists in stack metadata but its local ref was deleted")]
267    #[diagnostic(
268        code(workon::stack::deleted_branch_node),
269        help(
270            "The branch was tracked by Graphite but its local ref no longer exists. \
271             Run 'gt branch checkout {branch}' to restore it, or \
272             'gt branch delete {branch}' to remove it from the stack."
273        )
274    )]
275    DeletedBranchNode { branch: String },
276}
277
278/// Pull request-related errors
279#[derive(Error, Diagnostic, Debug)]
280pub enum PrError {
281    #[error("Invalid PR reference: {input}")]
282    #[diagnostic(
283        code(workon::pr::invalid_reference),
284        help("Use formats like #123, pr-123, or https://github.com/owner/repo/pull/123")
285    )]
286    InvalidReference { input: String },
287
288    #[error("PR #{number} not found on remote {remote}")]
289    #[diagnostic(
290        code(workon::pr::not_found),
291        help("Verify the PR number exists and you have access to the repository")
292    )]
293    PrNotFound { number: u32, remote: String },
294
295    #[error("No git remote configured")]
296    #[diagnostic(
297        code(workon::pr::no_remote),
298        help("Add a remote with: git remote add origin <url>")
299    )]
300    NoRemoteConfigured,
301
302    #[error("Failed to fetch PR refs from {remote}: {message}")]
303    #[diagnostic(
304        code(workon::pr::fetch_failed),
305        help("Check your network connection and repository access")
306    )]
307    FetchFailed { remote: String, message: String },
308
309    #[error("gh CLI is not installed or not in PATH")]
310    #[diagnostic(
311        code(workon::pr::gh_not_installed),
312        help("Install gh CLI: https://cli.github.com/")
313    )]
314    GhNotInstalled,
315
316    #[error("Failed to fetch PR metadata from gh: {message}")]
317    #[diagnostic(
318        code(workon::pr::gh_fetch_failed),
319        help("Check your network connection and GitHub authentication (gh auth status)")
320    )]
321    GhFetchFailed { message: String },
322
323    #[error("Invalid JSON output from gh CLI: {message}")]
324    #[diagnostic(
325        code(workon::pr::gh_json_parse_failed),
326        help("This may indicate a gh CLI version incompatibility")
327    )]
328    GhJsonParseFailed { message: String },
329
330    #[error("Fork repository missing owner information")]
331    #[diagnostic(
332        code(workon::pr::missing_fork_owner),
333        help("This PR may be from a deleted fork")
334    )]
335    MissingForkOwner,
336}
337
338/// In-place checkout errors
339#[derive(Error, Diagnostic, Debug)]
340pub enum CheckoutError {
341    /// A git2 error during checkout
342    #[error(transparent)]
343    #[diagnostic(code(workon::checkout::git_error))]
344    Git(#[from] git2::Error),
345
346    /// Branch not found in the host worktree
347    #[error("Branch '{branch}' not found in the worktree")]
348    #[diagnostic(
349        code(workon::checkout::branch_not_found),
350        help("Ensure the branch exists locally before checking it out in place")
351    )]
352    BranchNotFound { branch: String },
353
354    /// Checkout conflicts with uncommitted changes in the working tree
355    #[error("Checkout of '{branch}' conflicts with uncommitted changes in {path}")]
356    #[diagnostic(
357        code(workon::checkout::conflict),
358        help("Stash or commit changes first, or use the interactive prompt to shelve them")
359    )]
360    Conflict { branch: String, path: String },
361
362    /// User aborted an interactive checkout
363    #[error("Checkout aborted")]
364    #[diagnostic(code(workon::checkout::aborted))]
365    Aborted,
366}
367
368/// File copy errors
369#[derive(Error, Diagnostic, Debug)]
370pub enum CopyError {
371    #[error("Invalid glob pattern '{pattern}'")]
372    #[diagnostic(
373        code(workon::copy::invalid_glob_pattern),
374        help("Check glob pattern syntax: *, **, ?, [...]")
375    )]
376    InvalidGlobPattern {
377        pattern: String,
378        #[source]
379        source: glob::PatternError,
380    },
381
382    #[error("Path is not valid UTF-8: {}", path.display())]
383    #[diagnostic(code(workon::copy::invalid_path))]
384    InvalidPath { path: PathBuf },
385
386    #[error("Failed to read glob entry")]
387    #[diagnostic(code(workon::copy::glob_error))]
388    GlobEntry(#[from] glob::GlobError),
389
390    #[error("Failed to copy '{}' to '{}'", src.display(), dest.display())]
391    #[diagnostic(code(workon::copy::copy_failed))]
392    CopyFailed {
393        src: PathBuf,
394        dest: PathBuf,
395        #[source]
396        source: std::io::Error,
397    },
398
399    #[error("Failed to open repository at '{}'", path.display())]
400    #[diagnostic(
401        code(workon::copy::repo_open_error),
402        help("Ensure the path is a valid git repository")
403    )]
404    RepoOpen {
405        path: PathBuf,
406        #[source]
407        source: git2::Error,
408    },
409}