Skip to main content

gitstack/
event.rs

1use chrono::{DateTime, Local};
2
3/// Gitイベントの種類
4#[derive(Debug, Clone, PartialEq)]
5pub enum GitEventKind {
6    /// コミット
7    Commit,
8    /// マージコミット
9    Merge,
10    /// ブランチ切り替え(reflogから検出)
11    BranchSwitch,
12}
13
14/// Gitイベント
15#[derive(Debug, Clone)]
16pub struct GitEvent {
17    /// イベントの種類
18    pub kind: GitEventKind,
19    /// コミットハッシュ(短縮形)
20    pub short_hash: String,
21    /// コミットメッセージ(1行目)
22    pub message: String,
23    /// 作者名
24    pub author: String,
25    /// タイムスタンプ
26    pub timestamp: DateTime<Local>,
27    /// 変更ファイル数(追加)
28    pub files_added: usize,
29    /// 変更ファイル数(削除)
30    pub files_deleted: usize,
31    /// 親コミットのハッシュ一覧(グラフ表示用)
32    pub parent_hashes: Vec<String>,
33    /// ブランチラベル(このコミットを指しているブランチ名)
34    pub branch_labels: Vec<String>,
35}
36
37impl GitEvent {
38    /// 新しいコミットイベントを作成
39    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    /// 新しいマージイベントを作成
61    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    /// 親コミットを設定
81    pub fn with_parents(mut self, parents: Vec<String>) -> Self {
82        self.parent_hashes = parents;
83        self
84    }
85
86    /// ブランチラベルを設定
87    pub fn with_labels(mut self, labels: Vec<String>) -> Self {
88        self.branch_labels = labels;
89        self
90    }
91
92    /// ラベルがあるかどうか
93    pub fn has_labels(&self) -> bool {
94        !self.branch_labels.is_empty()
95    }
96
97    /// 相対時間を取得(例: "14m ago", "2h ago", "3d ago")
98    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}