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#[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(
31 "API request exceeded the model context window during {operation} ({model}, HTTP {status}): \
32 {body}"
33 )]
34 #[diagnostic(
35 code(lgit::api::context_length),
36 help(
37 "Reduce or split the diff, enable map-reduce, or use a model with a larger context \
38 window."
39 )
40 )]
41 ApiContextLengthExceeded {
42 operation: String,
43 model: String,
44 status: u16,
45 body: String,
46 },
47
48 #[error("API call failed after {retries} retries")]
49 #[diagnostic(
50 code(lgit::api::retry_exhausted),
51 help(
52 "Check that your LiteLLM server is running and reachable.\nYou can increase max_retries \
53 in ~/.config/llm-git/config.toml"
54 )
55 )]
56 ApiRetryExhausted {
57 retries: u32,
58 #[source]
59 source: Box<Self>,
60 },
61
62 #[error("Failed to generate compose commit message for {group_id} ({files}): {source}")]
63 #[diagnostic(code(lgit::compose::message))]
64 ComposeMessageError {
65 group_id: String,
66 files: String,
67 #[source]
68 source: Box<Self>,
69 },
70
71 #[error("Validation failed: {0}")]
72 #[diagnostic(code(lgit::validation))]
73 ValidationError(String),
74
75 #[error("No changes found in {mode} mode")]
76 #[diagnostic(
77 code(lgit::git::no_changes),
78 help("Stage changes with `git add` or use --mode=unstaged to analyze working tree changes")
79 )]
80 NoChanges { mode: String },
81
82 #[error("Diff parsing failed: {0}")]
83 #[diagnostic(code(lgit::diff))]
84 #[allow(dead_code, reason = "Reserved for future diff parsing error handling")]
85 DiffParseError(String),
86
87 #[error("Invalid commit type: {0}")]
88 #[diagnostic(
89 code(lgit::types::commit_type),
90 help("Valid types: feat, fix, refactor, docs, test, chore, style, perf, build, ci, revert")
91 )]
92 InvalidCommitType(String),
93
94 #[error("Invalid scope format: {0}")]
95 #[diagnostic(
96 code(lgit::types::scope),
97 help("Scopes must be lowercase alphanumeric with at most 2 segments (e.g. api/client)")
98 )]
99 InvalidScope(String),
100
101 #[error("Summary too long: {len} chars (max {max})")]
102 #[diagnostic(code(lgit::validation::length))]
103 SummaryTooLong { len: usize, max: usize },
104
105 #[error("IO error: {0}")]
106 #[diagnostic(code(lgit::io))]
107 IoError(#[from] std::io::Error),
108
109 #[error("JSON error: {0}")]
110 #[diagnostic(code(lgit::json))]
111 JsonError(#[from] serde_json::Error),
112
113 #[error("HTTP error: {0}")]
114 #[diagnostic(code(lgit::http))]
115 HttpError(#[from] reqwest::Error),
116
117 #[error("Clipboard error: {0}")]
118 #[diagnostic(code(lgit::clipboard))]
119 ClipboardError(#[from] arboard::Error),
120
121 #[error("{0}")]
122 Other(String),
123
124 #[error("Failed to parse changelog {path}: {reason}")]
125 #[diagnostic(code(lgit::changelog::parse))]
126 ChangelogParseError { path: String, reason: String },
127
128 #[error("No [Unreleased] section found in {path}")]
129 #[diagnostic(
130 code(lgit::changelog::no_unreleased),
131 help("Add an ## [Unreleased] section to the changelog file")
132 )]
133 NoUnreleasedSection { path: String },
134}
135
136impl CommitGenError {
137 pub fn git(msg: impl Into<String>) -> Self {
140 Self::GitError { message: msg.into() }
141 }
142}
143
144pub type Result<T, E = CommitGenError> = std::result::Result<T, E>;