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 #[error(transparent)]
64 #[diagnostic(forward(0))]
65 Prune(#[from] PruneError),
66
67 #[error(transparent)]
69 #[diagnostic(forward(0))]
70 Changeset(#[from] ChangesetError),
71}
72
73#[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#[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#[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#[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#[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#[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#[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#[derive(Error, Diagnostic, Debug)]
438pub enum CheckoutError {
439 #[error(transparent)]
441 #[diagnostic(code(workon::checkout::git_error))]
442 Git(#[from] git2::Error),
443
444 #[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 #[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 #[error("Checkout aborted")]
462 #[diagnostic(code(workon::checkout::aborted))]
463 Aborted,
464}
465
466#[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#[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}