git_cliff_core/
summary.rs1use std::fmt::Display;
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5
6use crate::error::Error as AppError;
7
8#[non_exhaustive]
10#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub enum CommitProcessingErrorKind {
13 Io,
15 Parse,
17 Json,
19 Field,
21 Group,
23 Skipped,
25 Other,
27}
28
29impl From<AppError> for CommitProcessingErrorKind {
30 fn from(err: AppError) -> Self {
31 CommitProcessingErrorKind::from(&err)
32 }
33}
34
35impl From<&AppError> for CommitProcessingErrorKind {
36 fn from(err: &AppError) -> Self {
37 match err {
38 AppError::IoError(_) => CommitProcessingErrorKind::Io,
39 AppError::ParseError(_) => CommitProcessingErrorKind::Parse,
40 AppError::JsonError(_) => CommitProcessingErrorKind::Json,
41 AppError::FieldError(_) => CommitProcessingErrorKind::Field,
42 AppError::GroupError(msg) if msg.contains("Skipping commit") => {
43 CommitProcessingErrorKind::Skipped
44 }
45 AppError::GroupError(_) => CommitProcessingErrorKind::Group,
46 _ => CommitProcessingErrorKind::Other,
47 }
48 }
49}
50
51impl Display for CommitProcessingErrorKind {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 let s = match self {
54 CommitProcessingErrorKind::Io => "I/O error",
55 CommitProcessingErrorKind::Parse => "parse error",
56 CommitProcessingErrorKind::Json => "JSON error",
57 CommitProcessingErrorKind::Field => "field error",
58 CommitProcessingErrorKind::Group => "grouping error",
59 CommitProcessingErrorKind::Skipped => "intentionally skipped commit",
60 CommitProcessingErrorKind::Other => "other error",
61 };
62 f.write_str(s)
63 }
64}
65
66impl CommitProcessingErrorKind {
67 #[must_use]
69 pub fn should_warn(self) -> bool {
70 matches!(
71 self,
72 CommitProcessingErrorKind::Io |
73 CommitProcessingErrorKind::Parse |
74 CommitProcessingErrorKind::Json |
75 CommitProcessingErrorKind::Field |
76 CommitProcessingErrorKind::Group |
77 CommitProcessingErrorKind::Other
78 )
79 }
80}
81
82#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct Summary {
86 pub processed: usize,
88 pub by_kind: IndexMap<CommitProcessingErrorKind, usize>,
93}
94
95impl Summary {
96 pub fn record_ok(&mut self) {
98 self.processed += 1;
99 }
100
101 pub fn record_err(&mut self, err: &AppError) {
103 self.processed += 1;
104 let kind = CommitProcessingErrorKind::from(err);
105 *self.by_kind.entry(kind).or_insert(0) += 1;
106 }
107}
108
109#[cfg(test)]
110mod test {
111 use std::io::Error as StdIoError;
112
113 use git_conventional::Commit;
114
115 use super::*;
116 use crate::error::Error as AppError;
117
118 #[test]
119 fn commit_processing_error_kind_from_app_error() {
120 let err = AppError::IoError(StdIoError::other("something went wrong".to_string()));
121 let kind = CommitProcessingErrorKind::from(&err);
122 assert_eq!(kind, CommitProcessingErrorKind::Io);
123
124 let err = Commit::parse("")
125 .map_err(AppError::ParseError)
126 .expect_err("expected parse error");
127 let kind = CommitProcessingErrorKind::from(&err);
128 assert_eq!(kind, CommitProcessingErrorKind::Parse);
129
130 let err = serde_json::from_str::<serde_json::Value>("{ invalid json }")
131 .map_err(AppError::from)
132 .expect_err("expected JSON parse error");
133 let kind = CommitProcessingErrorKind::from(&err);
134 assert_eq!(kind, CommitProcessingErrorKind::Json);
135
136 let err = AppError::FieldError("missing field".into());
137 let kind = CommitProcessingErrorKind::from(&err);
138 assert_eq!(kind, CommitProcessingErrorKind::Field);
139
140 let err = AppError::GroupError("no matching group".into());
141 let kind = CommitProcessingErrorKind::from(&err);
142 assert_eq!(kind, CommitProcessingErrorKind::Group);
143
144 let err = AppError::GroupError("Skipping commit due to config".into());
145 let kind = CommitProcessingErrorKind::from(&err);
146 assert_eq!(kind, CommitProcessingErrorKind::Skipped);
147
148 let err = AppError::UnmatchedCommitsError(1);
149 let kind = CommitProcessingErrorKind::from(&err);
150 assert_eq!(kind, CommitProcessingErrorKind::Other);
151 }
152
153 #[test]
154 fn commit_processing_error_kind_from_app_error_owned() {
155 let err = AppError::IoError(StdIoError::other("something went wrong".to_string()));
156 let kind: CommitProcessingErrorKind = err.into();
157 assert_eq!(kind, CommitProcessingErrorKind::Io);
158
159 let err = Commit::parse("")
160 .map_err(AppError::ParseError)
161 .expect_err("expected parse error");
162 let kind: CommitProcessingErrorKind = err.into();
163 assert_eq!(kind, CommitProcessingErrorKind::Parse);
164
165 let err = serde_json::from_str::<serde_json::Value>("{ invalid json }")
166 .map_err(AppError::from)
167 .expect_err("expected JSON parse error");
168 let kind: CommitProcessingErrorKind = err.into();
169 assert_eq!(kind, CommitProcessingErrorKind::Json);
170
171 let err = AppError::FieldError("missing field".into());
172 let kind: CommitProcessingErrorKind = err.into();
173 assert_eq!(kind, CommitProcessingErrorKind::Field);
174
175 let err = AppError::GroupError("no matching group".into());
176 let kind: CommitProcessingErrorKind = err.into();
177 assert_eq!(kind, CommitProcessingErrorKind::Group);
178
179 let err = AppError::GroupError("Skipping commit due to config".into());
180 let kind: CommitProcessingErrorKind = err.into();
181 assert_eq!(kind, CommitProcessingErrorKind::Skipped);
182
183 let err = AppError::UnmatchedCommitsError(1);
184 let kind: CommitProcessingErrorKind = err.into();
185 assert_eq!(kind, CommitProcessingErrorKind::Other);
186 }
187
188 #[test]
189 fn commit_processing_error_kind_should_warn_or_not() {
190 assert!(CommitProcessingErrorKind::Io.should_warn());
191 assert!(CommitProcessingErrorKind::Parse.should_warn());
192 assert!(CommitProcessingErrorKind::Json.should_warn());
193 assert!(CommitProcessingErrorKind::Field.should_warn());
194 assert!(CommitProcessingErrorKind::Group.should_warn());
195 assert!(!CommitProcessingErrorKind::Skipped.should_warn());
196 assert!(CommitProcessingErrorKind::Other.should_warn());
197 }
198
199 #[test]
200 fn commit_processing_error_kind_display_is_human_readable() {
201 let kind = CommitProcessingErrorKind::Io;
202 let s = kind.to_string();
203 assert!(!s.is_empty());
204
205 let kind = CommitProcessingErrorKind::Parse;
206 let s = kind.to_string();
207 assert!(!s.is_empty());
208
209 let kind = CommitProcessingErrorKind::Json;
210 let s = kind.to_string();
211 assert!(!s.is_empty());
212
213 let kind = CommitProcessingErrorKind::Field;
214 let s = kind.to_string();
215 assert!(!s.is_empty());
216
217 let kind = CommitProcessingErrorKind::Group;
218 let s = kind.to_string();
219 assert!(!s.is_empty());
220
221 let kind = CommitProcessingErrorKind::Skipped;
222 let s = kind.to_string();
223 assert!(!s.is_empty());
224
225 let kind = CommitProcessingErrorKind::Other;
226 let s = kind.to_string();
227 assert!(!s.is_empty());
228 }
229
230 #[test]
231 fn summary_record_ok_increments_processed_only() {
232 let mut summary = Summary::default();
233 summary.record_ok();
234 assert_eq!(summary.processed, 1);
235 assert!(summary.by_kind.is_empty());
236 }
237
238 #[test]
239 fn summary_record_err_increments_processed_and_error_kind() {
240 let mut summary = Summary::default();
241 let err = AppError::FieldError("missing field".into());
242 summary.record_err(&err);
243 assert_eq!(summary.processed, 1);
244 assert_eq!(
245 summary.by_kind.get(&CommitProcessingErrorKind::Field),
246 Some(&1)
247 );
248 }
249
250 #[test]
251 fn summary_record_err_accumulates_same_kind() {
252 let mut summary = Summary::default();
253 let err = AppError::FieldError("missing field".into());
254 summary.record_err(&err);
255 summary.record_err(&err);
256 assert_eq!(summary.processed, 2);
257 assert_eq!(
258 summary.by_kind.get(&CommitProcessingErrorKind::Field),
259 Some(&2)
260 );
261 }
262}