Skip to main content

llm_git/
error.rs

1#![allow(unused_assignments, reason = "miette::Diagnostic derive generates field assignments")]
2
3use std::path::PathBuf;
4
5use miette::Diagnostic;
6use thiserror::Error;
7
8/// Top-level error type for the commit message generator.
9///
10/// Each variant carries context appropriate to the failure mode and, where
11/// applicable, [`miette::Diagnostic`] metadata (help text, error codes) so
12/// that the CLI renders human-friendly reports.
13#[derive(Debug, Error, Diagnostic)]
14pub enum CommitGenError {
15   #[error("git: {message}")]
16   #[diagnostic(code(lgit::git))]
17   GitError { message: String },
18
19   #[error("git index is locked")]
20   #[diagnostic(
21      code(lgit::git::index_locked),
22      help("Another git process may be running in this repository.\nRemove the lock file to continue:\n  rm {}", lock_path.display()),
23   )]
24   GitIndexLocked { lock_path: PathBuf },
25
26   #[error("API request failed (HTTP {status}): {body}")]
27   #[diagnostic(code(lgit::api))]
28   ApiError { status: u16, body: String },
29
30   #[error("API call failed after {retries} retries")]
31   #[diagnostic(
32      code(lgit::api::retry_exhausted),
33      help(
34         "Check that your LiteLLM server is running and reachable.\nYou can increase max_retries \
35          in ~/.config/llm-git/config.toml"
36      )
37   )]
38   ApiRetryExhausted {
39      retries: u32,
40      #[source]
41      source:  Box<Self>,
42   },
43
44   #[error("Validation failed: {0}")]
45   #[diagnostic(code(lgit::validation))]
46   ValidationError(String),
47
48   #[error("No changes found in {mode} mode")]
49   #[diagnostic(
50      code(lgit::git::no_changes),
51      help("Stage changes with `git add` or use --mode=unstaged to analyze working tree changes")
52   )]
53   NoChanges { mode: String },
54
55   #[error("Diff parsing failed: {0}")]
56   #[diagnostic(code(lgit::diff))]
57   #[allow(dead_code, reason = "Reserved for future diff parsing error handling")]
58   DiffParseError(String),
59
60   #[error("Invalid commit type: {0}")]
61   #[diagnostic(
62      code(lgit::types::commit_type),
63      help("Valid types: feat, fix, refactor, docs, test, chore, style, perf, build, ci, revert")
64   )]
65   InvalidCommitType(String),
66
67   #[error("Invalid scope format: {0}")]
68   #[diagnostic(
69      code(lgit::types::scope),
70      help("Scopes must be lowercase alphanumeric with at most 2 segments (e.g. api/client)")
71   )]
72   InvalidScope(String),
73
74   #[error("Summary too long: {len} chars (max {max})")]
75   #[diagnostic(code(lgit::validation::length))]
76   SummaryTooLong { len: usize, max: usize },
77
78   #[error("IO error: {0}")]
79   #[diagnostic(code(lgit::io))]
80   IoError(#[from] std::io::Error),
81
82   #[error("JSON error: {0}")]
83   #[diagnostic(code(lgit::json))]
84   JsonError(#[from] serde_json::Error),
85
86   #[error("HTTP error: {0}")]
87   #[diagnostic(code(lgit::http))]
88   HttpError(#[from] reqwest::Error),
89
90   #[error("Clipboard error: {0}")]
91   #[diagnostic(code(lgit::clipboard))]
92   ClipboardError(#[from] arboard::Error),
93
94   #[error("{0}")]
95   Other(String),
96
97   #[error("Failed to parse changelog {path}: {reason}")]
98   #[diagnostic(code(lgit::changelog::parse))]
99   ChangelogParseError { path: String, reason: String },
100
101   #[error("No [Unreleased] section found in {path}")]
102   #[diagnostic(
103      code(lgit::changelog::no_unreleased),
104      help("Add an ## [Unreleased] section to the changelog file")
105   )]
106   NoUnreleasedSection { path: String },
107}
108
109impl CommitGenError {
110   /// Construct a [`GitError`](CommitGenError::GitError) from any displayable
111   /// message.
112   pub fn git(msg: impl Into<String>) -> Self {
113      Self::GitError { message: msg.into() }
114   }
115}
116
117pub type Result<T, E = CommitGenError> = std::result::Result<T, E>;