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
68#[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#[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#[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#[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#[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#[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#[derive(Error, Diagnostic, Debug)]
345pub enum CheckoutError {
346 #[error(transparent)]
348 #[diagnostic(code(workon::checkout::git_error))]
349 Git(#[from] git2::Error),
350
351 #[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 #[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 #[error("Checkout aborted")]
369 #[diagnostic(code(workon::checkout::aborted))]
370 Aborted,
371}
372
373#[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#[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}