1use std::path::PathBuf;
2
3use miette::Diagnostic;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, WorkonError>;
8
9#[derive(Error, Diagnostic, Debug)]
11pub enum WorkonError {
12 #[error(transparent)]
14 #[diagnostic(code(workon::git_error))]
15 Git(#[from] git2::Error),
16
17 #[error(transparent)]
19 #[diagnostic(code(workon::io_error))]
20 Io(#[from] std::io::Error),
21
22 #[error(transparent)]
24 #[diagnostic(forward(0))]
25 Repo(#[from] RepoError),
26
27 #[error(transparent)]
29 #[diagnostic(forward(0))]
30 Worktree(#[from] WorktreeError),
31
32 #[error(transparent)]
34 #[diagnostic(forward(0))]
35 Config(#[from] ConfigError),
36
37 #[error(transparent)]
39 #[diagnostic(forward(0))]
40 DefaultBranch(#[from] DefaultBranchError),
41
42 #[error(transparent)]
44 #[diagnostic(forward(0))]
45 Pr(#[from] PrError),
46
47 #[error(transparent)]
49 #[diagnostic(forward(0))]
50 Copy(#[from] CopyError),
51
52 #[error(transparent)]
54 #[diagnostic(forward(0))]
55 Stack(#[from] StackError),
56
57 #[error(transparent)]
59 #[diagnostic(forward(0))]
60 Checkout(#[from] CheckoutError),
61}
62
63#[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#[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#[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#[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#[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#[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#[derive(Error, Diagnostic, Debug)]
340pub enum CheckoutError {
341 #[error(transparent)]
343 #[diagnostic(code(workon::checkout::git_error))]
344 Git(#[from] git2::Error),
345
346 #[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 #[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 #[error("Checkout aborted")]
364 #[diagnostic(code(workon::checkout::aborted))]
365 Aborted,
366}
367
368#[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}