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