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(
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   /// Construct a [`GitError`](CommitGenError::GitError) from any displayable
138   /// message.
139   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>;