1use chrono::{DateTime, Local};
2
3#[derive(Debug, Clone, PartialEq)]
5#[allow(dead_code)]
6pub enum GitEventKind {
7 Commit,
9 Merge,
11 BranchSwitch,
13}
14
15#[derive(Debug, Clone)]
17#[allow(dead_code)]
18pub struct GitEvent {
19 pub kind: GitEventKind,
21 pub short_hash: String,
23 pub message: String,
25 pub author: String,
27 pub timestamp: DateTime<Local>,
29 pub files_added: usize,
31 pub files_deleted: usize,
33 pub parent_hashes: Vec<String>,
35 pub branch_labels: Vec<String>,
37}
38
39impl GitEvent {
40 pub fn commit(
42 short_hash: String,
43 message: String,
44 author: String,
45 timestamp: DateTime<Local>,
46 files_added: usize,
47 files_deleted: usize,
48 ) -> Self {
49 Self {
50 kind: GitEventKind::Commit,
51 short_hash,
52 message,
53 author,
54 timestamp,
55 files_added,
56 files_deleted,
57 parent_hashes: Vec::new(),
58 branch_labels: Vec::new(),
59 }
60 }
61
62 pub fn merge(
64 short_hash: String,
65 message: String,
66 author: String,
67 timestamp: DateTime<Local>,
68 ) -> Self {
69 Self {
70 kind: GitEventKind::Merge,
71 short_hash,
72 message,
73 author,
74 timestamp,
75 files_added: 0,
76 files_deleted: 0,
77 parent_hashes: Vec::new(),
78 branch_labels: Vec::new(),
79 }
80 }
81
82 pub fn with_parents(mut self, parents: Vec<String>) -> Self {
84 self.parent_hashes = parents;
85 self
86 }
87
88 pub fn with_labels(mut self, labels: Vec<String>) -> Self {
90 self.branch_labels = labels;
91 self
92 }
93
94 pub fn has_labels(&self) -> bool {
96 !self.branch_labels.is_empty()
97 }
98
99 #[allow(dead_code)]
101 pub fn relative_time(&self) -> String {
102 let now = Local::now();
103 let duration = now.signed_duration_since(self.timestamp);
104
105 if duration.num_minutes() < 1 {
106 "just now".to_string()
107 } else if duration.num_minutes() < 60 {
108 format!("{}m ago", duration.num_minutes())
109 } else if duration.num_hours() < 24 {
110 format!("{}h ago", duration.num_hours())
111 } else if duration.num_days() < 30 {
112 format!("{}d ago", duration.num_days())
113 } else {
114 format!("{}mo ago", duration.num_days() / 30)
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use chrono::Duration;
123
124 fn create_test_timestamp() -> DateTime<Local> {
125 Local::now()
126 }
127
128 #[test]
129 fn test_git_event_kind_commit_is_distinct() {
130 assert_ne!(GitEventKind::Commit, GitEventKind::Merge);
131 assert_ne!(GitEventKind::Commit, GitEventKind::BranchSwitch);
132 }
133
134 #[test]
135 fn test_git_event_commit_creates_commit_kind() {
136 let event = GitEvent::commit(
137 "abc1234".to_string(),
138 "feat: add feature".to_string(),
139 "author".to_string(),
140 create_test_timestamp(),
141 2,
142 1,
143 );
144 assert_eq!(event.kind, GitEventKind::Commit);
145 }
146
147 #[test]
148 fn test_git_event_commit_stores_hash() {
149 let event = GitEvent::commit(
150 "abc1234".to_string(),
151 "message".to_string(),
152 "author".to_string(),
153 create_test_timestamp(),
154 0,
155 0,
156 );
157 assert_eq!(event.short_hash, "abc1234");
158 }
159
160 #[test]
161 fn test_git_event_commit_stores_message() {
162 let event = GitEvent::commit(
163 "abc1234".to_string(),
164 "feat: new feature".to_string(),
165 "author".to_string(),
166 create_test_timestamp(),
167 0,
168 0,
169 );
170 assert_eq!(event.message, "feat: new feature");
171 }
172
173 #[test]
174 fn test_git_event_commit_stores_author() {
175 let event = GitEvent::commit(
176 "abc1234".to_string(),
177 "message".to_string(),
178 "John Doe".to_string(),
179 create_test_timestamp(),
180 0,
181 0,
182 );
183 assert_eq!(event.author, "John Doe");
184 }
185
186 #[test]
187 fn test_git_event_commit_stores_file_counts() {
188 let event = GitEvent::commit(
189 "abc1234".to_string(),
190 "message".to_string(),
191 "author".to_string(),
192 create_test_timestamp(),
193 5,
194 3,
195 );
196 assert_eq!(event.files_added, 5);
197 assert_eq!(event.files_deleted, 3);
198 }
199
200 #[test]
201 fn test_git_event_merge_creates_merge_kind() {
202 let event = GitEvent::merge(
203 "abc1234".to_string(),
204 "Merge PR #1".to_string(),
205 "author".to_string(),
206 create_test_timestamp(),
207 );
208 assert_eq!(event.kind, GitEventKind::Merge);
209 }
210
211 #[test]
212 fn test_git_event_merge_has_zero_file_counts() {
213 let event = GitEvent::merge(
214 "abc1234".to_string(),
215 "Merge PR #1".to_string(),
216 "author".to_string(),
217 create_test_timestamp(),
218 );
219 assert_eq!(event.files_added, 0);
220 assert_eq!(event.files_deleted, 0);
221 }
222
223 #[test]
224 fn test_relative_time_just_now() {
225 let event = GitEvent::commit(
226 "abc1234".to_string(),
227 "message".to_string(),
228 "author".to_string(),
229 Local::now(),
230 0,
231 0,
232 );
233 assert_eq!(event.relative_time(), "just now");
234 }
235
236 #[test]
237 fn test_relative_time_minutes() {
238 let event = GitEvent::commit(
239 "abc1234".to_string(),
240 "message".to_string(),
241 "author".to_string(),
242 Local::now() - Duration::minutes(14),
243 0,
244 0,
245 );
246 assert_eq!(event.relative_time(), "14m ago");
247 }
248
249 #[test]
250 fn test_relative_time_hours() {
251 let event = GitEvent::commit(
252 "abc1234".to_string(),
253 "message".to_string(),
254 "author".to_string(),
255 Local::now() - Duration::hours(2),
256 0,
257 0,
258 );
259 assert_eq!(event.relative_time(), "2h ago");
260 }
261
262 #[test]
263 fn test_relative_time_days() {
264 let event = GitEvent::commit(
265 "abc1234".to_string(),
266 "message".to_string(),
267 "author".to_string(),
268 Local::now() - Duration::days(3),
269 0,
270 0,
271 );
272 assert_eq!(event.relative_time(), "3d ago");
273 }
274}