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