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