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("Could not find worktree '{0}'")]
83 #[diagnostic(
84 code(workon::worktree::not_found),
85 help("Use 'git workon list' to see available worktrees")
86 )]
87 NotFound(String),
88
89 #[error("Not in a worktree directory")]
90 #[diagnostic(
91 code(workon::worktree::not_in_worktree),
92 help("Run this command from within a worktree directory")
93 )]
94 NotInWorktree,
95
96 #[error("Could not determine branch target")]
97 #[diagnostic(
98 code(workon::worktree::no_branch_target),
99 help("The branch may be in an invalid state")
100 )]
101 NoBranchTarget,
102
103 #[error("Could not get current branch target")]
104 #[diagnostic(code(workon::worktree::no_current_branch_target))]
105 NoCurrentBranchTarget,
106
107 #[error("Could not get local branch target")]
108 #[diagnostic(code(workon::worktree::no_local_branch_target))]
109 NoLocalBranchTarget,
110
111 #[error("Worktree path has no parent directory")]
112 #[diagnostic(
113 code(workon::worktree::no_parent),
114 help("Cannot create parent directories for worktree path")
115 )]
116 NoParent,
117
118 #[error("Invalid worktree name: contains invalid UTF-8")]
119 #[diagnostic(
120 code(workon::worktree::invalid_name),
121 help("Worktree names must be valid UTF-8 strings")
122 )]
123 InvalidName,
124
125 #[error("Expected an empty index!")]
126 #[diagnostic(code(workon::worktree::non_empty_index))]
127 NonEmptyIndex,
128
129 #[error("Worktree '{to}' already exists")]
130 #[diagnostic(
131 code(workon::worktree::target_exists),
132 help("Choose a different name or remove the existing worktree first")
133 )]
134 TargetExists { to: String },
135
136 #[error("Cannot move detached HEAD worktree")]
137 #[diagnostic(
138 code(workon::worktree::move_detached),
139 help("Detached HEAD worktrees have no branch to rename")
140 )]
141 CannotMoveDetached,
142
143 #[error("Branch '{0}' is protected and cannot be renamed")]
144 #[diagnostic(
145 code(workon::worktree::protected_branch_move),
146 help("Protected branches are configured in workon.pruneProtectedBranches. Use --force to override.")
147 )]
148 ProtectedBranchMove(String),
149
150 #[error("Worktree is dirty (uncommitted changes)")]
151 #[diagnostic(
152 code(workon::worktree::dirty_worktree),
153 help("Commit or stash changes, or use --force to override")
154 )]
155 DirtyWorktree,
156
157 #[error("Worktree has unpushed commits")]
158 #[diagnostic(
159 code(workon::worktree::unpushed_commits),
160 help("Push commits first, or use --force to override")
161 )]
162 UnpushedCommits,
163}
164
165#[derive(Error, Diagnostic, Debug)]
167pub enum ConfigError {
168 #[error("Invalid PR format: '{format}' - {reason}")]
169 #[diagnostic(
170 code(workon::config::invalid_pr_format),
171 help("Valid placeholders: {{number}}, {{title}}, {{author}}, {{branch}}")
172 )]
173 InvalidPrFormat { format: String, reason: String },
174
175 #[error("Config entry has no value")]
176 #[diagnostic(code(workon::config::no_value))]
177 NoValue,
178}
179
180#[derive(Error, Diagnostic, Debug)]
182pub enum DefaultBranchError {
183 #[error("Could not determine default branch for remote {remote:?}")]
184 #[diagnostic(
185 code(workon::default_branch::no_remote_default),
186 help("The remote may not have a default branch configured")
187 )]
188 NoRemoteDefault { remote: Option<String> },
189
190 #[error("Remote is not connected")]
191 #[diagnostic(
192 code(workon::default_branch::not_connected),
193 help("Failed to establish connection to remote repository")
194 )]
195 NotConnected,
196
197 #[error("Could not determine default branch: neither 'main' nor 'master' exist, and init.defaultBranch is not configured")]
198 #[diagnostic(
199 code(workon::default_branch::no_default_branch),
200 help("Set init.defaultBranch in your git config, or create a 'main' or 'master' branch")
201 )]
202 NoDefaultBranch,
203}
204
205#[derive(Error, Diagnostic, Debug)]
207pub enum StackError {
208 #[error("Stack model '{model}' is not yet supported")]
209 #[diagnostic(
210 code(workon::stack::unsupported_model),
211 help(
212 "Only 'graphite' is implemented in this version. \
213 Support for branchless, sapling, and spr is planned."
214 )
215 )]
216 UnsupportedModel { model: String },
217
218 #[error("Unknown stack model '{value}'")]
219 #[diagnostic(
220 code(workon::stack::unknown_model),
221 help("Valid values: graphite, none, auto")
222 )]
223 UnknownModel { value: String },
224
225 #[error("Worktree granularity 'diff' is not yet implemented")]
226 #[diagnostic(
227 code(workon::stack::unsupported_granularity),
228 help(
229 "Only 'stack' (one worktree per stack) is supported in this version. \
230 'diff' (one worktree per branch) is planned."
231 )
232 )]
233 UnsupportedGranularity,
234
235 #[error("Unknown worktree granularity '{value}'")]
236 #[diagnostic(code(workon::stack::unknown_granularity), help("Valid values: stack"))]
237 UnknownGranularity { value: String },
238
239 #[error("Graphite CLI ('gt') is not installed or not in PATH")]
240 #[diagnostic(
241 code(workon::stack::gt_not_installed),
242 help(
243 "Install Graphite: https://graphite.dev/cli \
244 Or set workon.stackModel = none to disable stack support."
245 )
246 )]
247 GtNotInstalled,
248
249 #[error("Graphite command failed: {stderr}")]
250 #[diagnostic(code(workon::stack::gt_command_failed))]
251 GtCommandFailed { stderr: String },
252
253 #[error("Failed to parse Graphite metadata: {message}")]
254 #[diagnostic(code(workon::stack::gt_parse_failed))]
255 GtParseFailed { message: String },
256
257 #[error("Repository is not Graphite-managed (no .graphite_repo_config)")]
258 #[diagnostic(
259 code(workon::stack::not_a_graphite_repo),
260 help("Run 'gt init' in this repository, or unset workon.stackModel.")
261 )]
262 NotAGraphiteRepo,
263
264 #[error("Branch '{branch}' exists in stack metadata but its local ref was deleted")]
265 #[diagnostic(
266 code(workon::stack::deleted_branch_node),
267 help(
268 "The branch was tracked by Graphite but its local ref no longer exists. \
269 Run 'gt branch checkout {branch}' to restore it, or \
270 'gt branch delete {branch}' to remove it from the stack."
271 )
272 )]
273 DeletedBranchNode { branch: String },
274}
275
276#[derive(Error, Diagnostic, Debug)]
278pub enum PrError {
279 #[error("Invalid PR reference: {input}")]
280 #[diagnostic(
281 code(workon::pr::invalid_reference),
282 help("Use formats like #123, pr-123, or https://github.com/owner/repo/pull/123")
283 )]
284 InvalidReference { input: String },
285
286 #[error("PR #{number} not found on remote {remote}")]
287 #[diagnostic(
288 code(workon::pr::not_found),
289 help("Verify the PR number exists and you have access to the repository")
290 )]
291 PrNotFound { number: u32, remote: String },
292
293 #[error("No git remote configured")]
294 #[diagnostic(
295 code(workon::pr::no_remote),
296 help("Add a remote with: git remote add origin <url>")
297 )]
298 NoRemoteConfigured,
299
300 #[error("Failed to fetch PR refs from {remote}: {message}")]
301 #[diagnostic(
302 code(workon::pr::fetch_failed),
303 help("Check your network connection and repository access")
304 )]
305 FetchFailed { remote: String, message: String },
306
307 #[error("gh CLI is not installed or not in PATH")]
308 #[diagnostic(
309 code(workon::pr::gh_not_installed),
310 help("Install gh CLI: https://cli.github.com/")
311 )]
312 GhNotInstalled,
313
314 #[error("Failed to fetch PR metadata from gh: {message}")]
315 #[diagnostic(
316 code(workon::pr::gh_fetch_failed),
317 help("Check your network connection and GitHub authentication (gh auth status)")
318 )]
319 GhFetchFailed { message: String },
320
321 #[error("Invalid JSON output from gh CLI: {message}")]
322 #[diagnostic(
323 code(workon::pr::gh_json_parse_failed),
324 help("This may indicate a gh CLI version incompatibility")
325 )]
326 GhJsonParseFailed { message: String },
327
328 #[error("Fork repository missing owner information")]
329 #[diagnostic(
330 code(workon::pr::missing_fork_owner),
331 help("This PR may be from a deleted fork")
332 )]
333 MissingForkOwner,
334}
335
336#[derive(Error, Diagnostic, Debug)]
338pub enum CheckoutError {
339 #[error(transparent)]
341 #[diagnostic(code(workon::checkout::git_error))]
342 Git(#[from] git2::Error),
343
344 #[error("Branch '{branch}' not found in the worktree")]
346 #[diagnostic(
347 code(workon::checkout::branch_not_found),
348 help("Ensure the branch exists locally before checking it out in place")
349 )]
350 BranchNotFound { branch: String },
351
352 #[error("Checkout of '{branch}' conflicts with uncommitted changes in {path}")]
354 #[diagnostic(
355 code(workon::checkout::conflict),
356 help("Stash or commit changes first, or use the interactive prompt to shelve them")
357 )]
358 Conflict { branch: String, path: String },
359
360 #[error("Checkout aborted")]
362 #[diagnostic(code(workon::checkout::aborted))]
363 Aborted,
364}
365
366#[derive(Error, Diagnostic, Debug)]
368pub enum PruneError {
369 #[error("worktree(s) not found: {}", .names.join(", "))]
370 #[diagnostic(
371 code(workon::prune::names_not_found),
372 help("Use 'git workon list' to see available worktrees")
373 )]
374 NamesNotFound { names: Vec<String> },
375}
376
377#[derive(Error, Diagnostic, Debug)]
379pub enum CopyError {
380 #[error("Invalid glob pattern '{pattern}'")]
381 #[diagnostic(
382 code(workon::copy::invalid_glob_pattern),
383 help("Check glob pattern syntax: *, **, ?, [...]")
384 )]
385 InvalidGlobPattern {
386 pattern: String,
387 #[source]
388 source: glob::PatternError,
389 },
390
391 #[error("Path is not valid UTF-8: {}", path.display())]
392 #[diagnostic(code(workon::copy::invalid_path))]
393 InvalidPath { path: PathBuf },
394
395 #[error("Failed to read glob entry")]
396 #[diagnostic(code(workon::copy::glob_error))]
397 GlobEntry(#[from] glob::GlobError),
398
399 #[error("Failed to copy '{}' to '{}'", src.display(), dest.display())]
400 #[diagnostic(code(workon::copy::copy_failed))]
401 CopyFailed {
402 src: PathBuf,
403 dest: PathBuf,
404 #[source]
405 source: std::io::Error,
406 },
407
408 #[error("Failed to open repository at '{}'", path.display())]
409 #[diagnostic(
410 code(workon::copy::repo_open_error),
411 help("Ensure the path is a valid git repository")
412 )]
413 RepoOpen {
414 path: PathBuf,
415 #[source]
416 source: git2::Error,
417 },
418}