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    /// Changeset assembly errors
68    #[error(transparent)]
69    #[diagnostic(forward(0))]
70    Changeset(#[from] ChangesetError),
71}
72
73/// Repository-specific errors
74#[derive(Error, Diagnostic, Debug)]
75pub enum RepoError {
76    #[error("Not a bare repository at {0}")]
77    #[diagnostic(
78        code(workon::repo::not_bare),
79        help("Workon commands must be run in bare repositories")
80    )]
81    NotBare(String),
82}
83
84/// Worktree-specific errors
85#[derive(Error, Diagnostic, Debug)]
86pub enum WorktreeError {
87    #[error("Could not find worktree '{0}'")]
88    #[diagnostic(
89        code(workon::worktree::not_found),
90        help("Use 'git workon list' to see available worktrees")
91    )]
92    NotFound(String),
93
94    #[error("Not in a worktree directory")]
95    #[diagnostic(
96        code(workon::worktree::not_in_worktree),
97        help("Run this command from within a worktree directory")
98    )]
99    NotInWorktree,
100
101    #[error("Could not determine branch target")]
102    #[diagnostic(
103        code(workon::worktree::no_branch_target),
104        help("The branch may be in an invalid state")
105    )]
106    NoBranchTarget,
107
108    #[error("Could not get current branch target")]
109    #[diagnostic(code(workon::worktree::no_current_branch_target))]
110    NoCurrentBranchTarget,
111
112    #[error("Could not get local branch target")]
113    #[diagnostic(code(workon::worktree::no_local_branch_target))]
114    NoLocalBranchTarget,
115
116    #[error("Worktree path has no parent directory")]
117    #[diagnostic(
118        code(workon::worktree::no_parent),
119        help("Cannot create parent directories for worktree path")
120    )]
121    NoParent,
122
123    #[error("Invalid worktree name: contains invalid UTF-8")]
124    #[diagnostic(
125        code(workon::worktree::invalid_name),
126        help("Worktree names must be valid UTF-8 strings")
127    )]
128    InvalidName,
129
130    #[error("Expected an empty index!")]
131    #[diagnostic(code(workon::worktree::non_empty_index))]
132    NonEmptyIndex,
133
134    #[error("Worktree '{to}' already exists")]
135    #[diagnostic(
136        code(workon::worktree::target_exists),
137        help("Choose a different name or remove the existing worktree first")
138    )]
139    TargetExists { to: String },
140
141    #[error("Cannot move detached HEAD worktree")]
142    #[diagnostic(
143        code(workon::worktree::move_detached),
144        help("Detached HEAD worktrees have no branch to rename")
145    )]
146    CannotMoveDetached,
147
148    #[error("Branch '{0}' is protected and cannot be renamed")]
149    #[diagnostic(
150        code(workon::worktree::protected_branch_move),
151        help("Protected branches are configured in workon.pruneProtectedBranches. Use --force to override.")
152    )]
153    ProtectedBranchMove(String),
154
155    #[error("Worktree is dirty (uncommitted changes)")]
156    #[diagnostic(
157        code(workon::worktree::dirty_worktree),
158        help("Commit or stash changes, or use --force to override")
159    )]
160    DirtyWorktree,
161
162    #[error("Worktree has unpushed commits")]
163    #[diagnostic(
164        code(workon::worktree::unpushed_commits),
165        help("Push commits first, or use --force to override")
166    )]
167    UnpushedCommits,
168
169    #[error("Worktree admin name '{name}' is already in use by {path}")]
170    #[diagnostic(
171        code(workon::worktree::name_conflict),
172        help("Remove or rename the existing worktree, or run 'git workon doctor' to find a stale admin name")
173    )]
174    WorktreeNameConflict { name: String, path: String },
175}
176
177/// Configuration-related errors
178#[derive(Error, Diagnostic, Debug)]
179pub enum ConfigError {
180    #[error("Invalid PR format: '{format}' - {reason}")]
181    #[diagnostic(
182        code(workon::config::invalid_pr_format),
183        help("Valid placeholders: {{number}}, {{title}}, {{author}}, {{branch}}")
184    )]
185    InvalidPrFormat { format: String, reason: String },
186
187    #[error("Config entry has no value")]
188    #[diagnostic(code(workon::config::no_value))]
189    NoValue,
190}
191
192/// Default branch detection errors
193#[derive(Error, Diagnostic, Debug)]
194pub enum DefaultBranchError {
195    #[error("Could not determine default branch for remote {remote:?}")]
196    #[diagnostic(
197        code(workon::default_branch::no_remote_default),
198        help("The remote may not have a default branch configured")
199    )]
200    NoRemoteDefault { remote: Option<String> },
201
202    #[error("Remote is not connected")]
203    #[diagnostic(
204        code(workon::default_branch::not_connected),
205        help("Failed to establish connection to remote repository")
206    )]
207    NotConnected,
208
209    #[error("Could not determine default branch: neither 'main' nor 'master' exist, and init.defaultBranch is not configured")]
210    #[diagnostic(
211        code(workon::default_branch::no_default_branch),
212        help("Set init.defaultBranch in your git config, or create a 'main' or 'master' branch")
213    )]
214    NoDefaultBranch,
215}
216
217/// Stacked diff workflow errors
218#[derive(Error, Diagnostic, Debug)]
219pub enum StackError {
220    #[error("Stack model '{model}' is not yet supported")]
221    #[diagnostic(
222        code(workon::stack::unsupported_model),
223        help(
224            "'graphite' and 'gh-stack' are implemented in this version. \
225             Support for branchless and sapling is planned. 'ghstack' (Meta's \
226             Phabricator-style stacker) is a different tool and is not planned."
227        )
228    )]
229    UnsupportedModel { model: String },
230
231    #[error("Unknown stack model '{value}'")]
232    #[diagnostic(
233        code(workon::stack::unknown_model),
234        help("Valid values: graphite, gh-stack, git, none, auto")
235    )]
236    UnknownModel { value: String },
237
238    #[error("Worktree granularity 'diff' is not yet implemented")]
239    #[diagnostic(
240        code(workon::stack::unsupported_granularity),
241        help(
242            "Only 'stack' (one worktree per stack) is supported in this version. \
243             'diff' (one worktree per branch) is planned."
244        )
245    )]
246    UnsupportedGranularity,
247
248    #[error("Unknown worktree granularity '{value}'")]
249    #[diagnostic(code(workon::stack::unknown_granularity), help("Valid values: stack"))]
250    UnknownGranularity { value: String },
251
252    #[error("Graphite CLI ('gt') is not installed or not in PATH")]
253    #[diagnostic(
254        code(workon::stack::gt_not_installed),
255        help(
256            "Install Graphite: https://graphite.dev/cli \
257             Or set workon.stackModel = none to disable stack support."
258        )
259    )]
260    GtNotInstalled,
261
262    #[error("Graphite command failed: {stderr}")]
263    #[diagnostic(code(workon::stack::gt_command_failed))]
264    GtCommandFailed { stderr: String },
265
266    #[error("Failed to parse Graphite metadata: {message}")]
267    #[diagnostic(code(workon::stack::gt_parse_failed))]
268    GtParseFailed { message: String },
269
270    #[error("Repository is not Graphite-managed (no .graphite_repo_config)")]
271    #[diagnostic(
272        code(workon::stack::not_a_graphite_repo),
273        help("Run 'gt init' in this repository, or unset workon.stackModel.")
274    )]
275    NotAGraphiteRepo,
276
277    #[error("Branch '{branch}' exists in stack metadata but its local ref was deleted")]
278    #[diagnostic(
279        code(workon::stack::deleted_branch_node),
280        help(
281            "The branch was tracked by Graphite but its local ref no longer exists. \
282             Run 'gt branch checkout {branch}' to restore it, or \
283             'gt branch delete {branch}' to remove it from the stack."
284        )
285    )]
286    DeletedBranchNode { branch: String },
287
288    #[error("gh-stack file '{}' uses schema version {version}, which this version of git-workon does not support", path.display())]
289    #[diagnostic(
290        code(workon::stack::gh_stack_schema_unsupported),
291        help("Upgrade git-workon, or downgrade the gh-stack extension that wrote this file.")
292    )]
293    GhStackSchemaUnsupported { path: PathBuf, version: u64 },
294
295    #[error("Failed to parse gh-stack file '{}': {message}", path.display())]
296    #[diagnostic(code(workon::stack::gh_stack_parse_failed))]
297    GhStackParseFailed { path: PathBuf, message: String },
298
299    #[error("Timed out waiting for gh-stack lock '{}'", path.display())]
300    #[diagnostic(
301        code(workon::stack::gh_stack_locked),
302        help("Another `gh stack` or `git-workon` process may be mid-write; try again shortly.")
303    )]
304    GhStackLocked { path: PathBuf },
305
306    #[error("gh-stack migration is pending ('{}' exists)", path.display())]
307    #[diagnostic(
308        code(workon::stack::gh_stack_migration_pending),
309        help("Run any `gh stack` command to finish migration, then retry.")
310    )]
311    GhStackMigrationPending { path: PathBuf },
312
313    #[error("Failed to write gh-stack file '{}': {message}", path.display())]
314    #[diagnostic(code(workon::stack::gh_stack_write_failed))]
315    GhStackWriteFailed { path: PathBuf, message: String },
316
317    #[error("No gh-stack stack ends at branch '{base}'")]
318    #[diagnostic(
319        code(workon::stack::gh_stack_no_stack_for_base),
320        help(
321            "Run `gh stack init` or `gh stack add` for '{base}' first, or check workon.stackModel."
322        )
323    )]
324    GhStackNoStackForBase { base: String },
325
326    #[error("No gh-stack stack ends at branch '{base}' in the canonical store")]
327    #[diagnostic(
328        code(workon::stack::gh_stack_stack_in_unlinked_worktree),
329        help(
330            "'{base}' may be tracked in an unlinked worktree's gh-stack file. \
331             Run any `gh stack` command (gh-stack >=0.2 migrates it automatically), then retry."
332        )
333    )]
334    GhStackStackInUnlinkedWorktree { base: String },
335
336    #[error("Branch '{branch}' exists in stack metadata but its local ref was deleted")]
337    #[diagnostic(
338        code(workon::stack::deleted_stack_node),
339        help(
340            "The branch was tracked by your stack tool but its local ref no longer exists. \
341             Recreate the branch, or remove it from the stack metadata."
342        )
343    )]
344    DeletedStackNode { branch: String },
345}
346
347/// Changeset assembly errors
348#[derive(Error, Diagnostic, Debug)]
349pub enum ChangesetError {
350    #[error("Branch '{branch}' has no resolvable local ref")]
351    #[diagnostic(
352        code(workon::changeset::unresolvable_branch),
353        help("The branch may have been deleted while stack metadata lingered; sync your stack tool (e.g. `gt sync`) or re-create the branch")
354    )]
355    UnresolvableBranch { branch: String },
356
357    #[error(
358        "Recorded parent revision '{revision}' for branch '{branch}' does not resolve to a commit"
359    )]
360    #[diagnostic(
361        code(workon::changeset::invalid_parent_revision),
362        help("Stack metadata may be corrupt or copied from another clone; resync your stack tool (e.g. `gt restack`) to rewrite it")
363    )]
364    InvalidParentRevision { branch: String, revision: String },
365
366    #[error("Branch '{branch}' has no upstream to infer changesets from")]
367    #[diagnostic(
368        code(workon::changeset::no_upstream),
369        help(
370            "Set an upstream (git branch --set-upstream-to=<remote>/<branch>) or use a stack tool"
371        )
372    )]
373    NoUpstream { branch: String },
374}
375
376/// Pull request-related errors
377#[derive(Error, Diagnostic, Debug)]
378pub enum PrError {
379    #[error("Invalid PR reference: {input}")]
380    #[diagnostic(
381        code(workon::pr::invalid_reference),
382        help("Use formats like #123, pr-123, or https://github.com/owner/repo/pull/123")
383    )]
384    InvalidReference { input: String },
385
386    #[error("PR #{number} not found on remote {remote}")]
387    #[diagnostic(
388        code(workon::pr::not_found),
389        help("Verify the PR number exists and you have access to the repository")
390    )]
391    PrNotFound { number: u32, remote: String },
392
393    #[error("No git remote configured")]
394    #[diagnostic(
395        code(workon::pr::no_remote),
396        help("Add a remote with: git remote add origin <url>")
397    )]
398    NoRemoteConfigured,
399
400    #[error("Failed to fetch PR refs from {remote}: {message}")]
401    #[diagnostic(
402        code(workon::pr::fetch_failed),
403        help("Check your network connection and repository access")
404    )]
405    FetchFailed { remote: String, message: String },
406
407    #[error("gh CLI is not installed or not in PATH")]
408    #[diagnostic(
409        code(workon::pr::gh_not_installed),
410        help("Install gh CLI: https://cli.github.com/")
411    )]
412    GhNotInstalled,
413
414    #[error("Failed to fetch PR metadata from gh: {message}")]
415    #[diagnostic(
416        code(workon::pr::gh_fetch_failed),
417        help("Check your network connection and GitHub authentication (gh auth status)")
418    )]
419    GhFetchFailed { message: String },
420
421    #[error("Invalid JSON output from gh CLI: {message}")]
422    #[diagnostic(
423        code(workon::pr::gh_json_parse_failed),
424        help("This may indicate a gh CLI version incompatibility")
425    )]
426    GhJsonParseFailed { message: String },
427
428    #[error("Fork repository missing owner information")]
429    #[diagnostic(
430        code(workon::pr::missing_fork_owner),
431        help("This PR may be from a deleted fork")
432    )]
433    MissingForkOwner,
434}
435
436/// In-place checkout errors
437#[derive(Error, Diagnostic, Debug)]
438pub enum CheckoutError {
439    /// A git2 error during checkout
440    #[error(transparent)]
441    #[diagnostic(code(workon::checkout::git_error))]
442    Git(#[from] git2::Error),
443
444    /// Branch not found in the host worktree
445    #[error("Branch '{branch}' not found in the worktree")]
446    #[diagnostic(
447        code(workon::checkout::branch_not_found),
448        help("Ensure the branch exists locally before checking it out in place")
449    )]
450    BranchNotFound { branch: String },
451
452    /// Checkout conflicts with uncommitted changes in the working tree
453    #[error("Checkout of '{branch}' conflicts with uncommitted changes in {path}")]
454    #[diagnostic(
455        code(workon::checkout::conflict),
456        help("Stash or commit changes first, or use the interactive prompt to shelve them")
457    )]
458    Conflict { branch: String, path: String },
459
460    /// User aborted an interactive checkout
461    #[error("Checkout aborted")]
462    #[diagnostic(code(workon::checkout::aborted))]
463    Aborted,
464}
465
466/// Prune-related errors
467#[derive(Error, Diagnostic, Debug)]
468pub enum PruneError {
469    #[error("worktree(s) not found: {}", .names.join(", "))]
470    #[diagnostic(
471        code(workon::prune::names_not_found),
472        help("Use 'git workon list' to see available worktrees")
473    )]
474    NamesNotFound { names: Vec<String> },
475}
476
477/// File copy errors
478#[derive(Error, Diagnostic, Debug)]
479pub enum CopyError {
480    #[error("Invalid glob pattern '{pattern}'")]
481    #[diagnostic(
482        code(workon::copy::invalid_glob_pattern),
483        help("Check glob pattern syntax: *, **, ?, [...]")
484    )]
485    InvalidGlobPattern {
486        pattern: String,
487        #[source]
488        source: glob::PatternError,
489    },
490
491    #[error("Path is not valid UTF-8: {}", path.display())]
492    #[diagnostic(code(workon::copy::invalid_path))]
493    InvalidPath { path: PathBuf },
494
495    #[error("Failed to read glob entry")]
496    #[diagnostic(code(workon::copy::glob_error))]
497    GlobEntry(#[from] glob::GlobError),
498
499    #[error("Failed to copy '{}' to '{}'", src.display(), dest.display())]
500    #[diagnostic(code(workon::copy::copy_failed))]
501    CopyFailed {
502        src: PathBuf,
503        dest: PathBuf,
504        #[source]
505        source: std::io::Error,
506    },
507
508    #[error("Failed to open repository at '{}'", path.display())]
509    #[diagnostic(
510        code(workon::copy::repo_open_error),
511        help("Ensure the path is a valid git repository")
512    )]
513    RepoOpen {
514        path: PathBuf,
515        #[source]
516        source: git2::Error,
517    },
518}