Skip to main content

semver_common/models/
commit.rs

1use crate::models::Alert;
2use chrono::{DateTime, FixedOffset};
3use derive_getters::Getters;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::fmt::{self, Display, Formatter};
6
7const COMMIT_TIME_FORMAT: &str = "%a %b %d %H:%M:%S %Y %z";
8
9mod datetime_ser {
10    use serde::de;
11
12    use super::*;
13
14    pub fn serialize<S>(ext: &DateTime<FixedOffset>, serializer: S) -> Result<S::Ok, S::Error>
15    where
16        S: Serializer,
17    {
18        serializer.serialize_str(&ext.to_string())
19    }
20
21    pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<FixedOffset>, D::Error>
22    where
23        D: Deserializer<'de>,
24    {
25        println!("Begin deserialize");
26        let s = String::deserialize(deserializer)?;
27        println!("{}", s);
28        let dt = match DateTime::parse_from_str(&s, COMMIT_TIME_FORMAT) {
29            Ok(v) => v,
30            Err(e) => return Err(de::Error::custom(e.to_string())),
31        };
32        println!("After datetime");
33        Ok(dt)
34    }
35}
36
37#[derive(Serialize, Deserialize, Clone, Debug, Getters)]
38pub struct Commit {
39    id: String,
40    author: String,
41
42    #[serde(with = "datetime_ser")]
43    timestamp: DateTime<FixedOffset>,
44
45    message: String,
46}
47
48impl Ord for Commit {
49    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
50        if self.message > other.message {
51            return std::cmp::Ordering::Greater;
52        } else if self.message < other.message {
53            return std::cmp::Ordering::Less;
54        }
55        std::cmp::Ordering::Equal
56    }
57}
58
59impl Eq for Commit {}
60
61impl PartialOrd for Commit {
62    fn ge(&self, other: &Self) -> bool {
63        self.message >= other.message
64    }
65
66    fn gt(&self, other: &Self) -> bool {
67        self.message > other.message
68    }
69
70    fn le(&self, other: &Self) -> bool {
71        self.message <= other.message
72    }
73
74    fn lt(&self, other: &Self) -> bool {
75        self.message < other.message
76    }
77
78    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
79        Some(self.cmp(other))
80    }
81}
82
83impl PartialEq for Commit {
84    fn eq(&self, other: &Self) -> bool {
85        self.message == other.message
86    }
87}
88
89impl Display for Commit {
90    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
91        writeln!(f, "{}", self.message)
92    }
93}
94
95impl Commit {
96    pub fn new(id: &str, author: &str, timestamp: DateTime<FixedOffset>, message: &str) -> Self {
97        Commit {
98            id: id.to_string(),
99            author: author.to_string(),
100            timestamp,
101            message: message.to_string(),
102        }
103    }
104
105    /// Creates a new Commit object after converting string for timestamp to a DateTime.
106    pub fn new_from_str(
107        id: &str,
108        author: &str,
109        timestamp: &str,
110        message: &str,
111    ) -> Result<Self, Alert> {
112        let parsed_timestamp = DateTime::parse_from_str(timestamp, COMMIT_TIME_FORMAT)?;
113        Ok(Commit::new(id, author, parsed_timestamp, message))
114    }
115
116    /// Creates a new Commit object from a standard commit in text format from "git log" output.
117    ///
118    /// # Example:
119    ///
120    /// ```
121    /// use semver_common::Commit;
122    /// use chrono::DateTime;
123    ///
124    /// let c = String::from(
125    ///             "490049bf36b19b30d23b4be5a4u94f71b5c6475c
126    /// Author: Some Author <myemail@email.com>
127    /// Date:   Tue Apr 14 17:35:15 2026 -0400
128    ///
129    ///     feat: added feature to get commit list
130    /// ",
131    /// );
132    /// let commit =
133    ///     Commit::new_from_commit(c).expect("Commit could not be instantiated during test.");
134    /// assert_eq!(commit.id(), "490049bf36b19b30d23b4be5a4u94f71b5c6475c");
135    /// assert_eq!(commit.author(), "Some Author <myemail@email.com>");
136    /// assert_eq!(
137    ///     commit.timestamp(),
138    ///     &DateTime::parse_from_str("Tue Apr 14 17:35:15 2026 -0400", "%a %b %d %H:%M:%S %Y %z").unwrap()
139    /// );
140    /// assert_eq!(commit.message(), "feat: added feature to get commit list");
141    /// ```
142    pub fn new_from_commit(commit: String) -> Result<Self, Alert> {
143        let lines: Vec<&str> = commit.split("\n").collect();
144        if lines.len() > 3 {
145            let id_line: (&str, &str) = lines[0].split_once(" ").unwrap_or((lines[0], ""));
146            let commit_id = id_line.0.trim();
147            let author_line: (&str, &str) = lines[1]
148                .split_once(":")
149                .ok_or("Could not parse author line of commit.")?;
150            let author = author_line.1.trim();
151            let date_line: (&str, &str) = lines[2]
152                .split_once(":")
153                .ok_or("Could not parse date line of commit.")?;
154            let date = date_line.1.trim();
155            let commit_end_line: usize = lines.len() - 1;
156            let commit_message_untrimmed = lines[4..commit_end_line].join("\n");
157            let commit_message = commit_message_untrimmed.trim();
158            let object = Commit::new_from_str(commit_id, author, date, commit_message)?;
159            return Ok(object);
160        }
161        Err(Alert::from("Commit is not valid"))
162    }
163
164    pub fn msg(&self) -> &str {
165        &self.message
166    }
167}
168#[cfg(test)]
169mod test {
170    use super::*;
171
172    #[test]
173    fn test_commit_new_top_commit() {
174        let c = String::from(
175            "490049bf36b19b30d23b4be5a4u94f71b5c6475c (HEAD -> master)
176Author: Some Author <myemail@email.com>
177Date:   Tue Apr 14 17:35:15 2026 -0400
178
179    feat: added feature to get commit list
180",
181        );
182        let commit =
183            Commit::new_from_commit(c).expect("Commit could not be instantiated during test.");
184        assert_eq!(commit.id, "490049bf36b19b30d23b4be5a4u94f71b5c6475c");
185        assert_eq!(commit.author, "Some Author <myemail@email.com>");
186        assert_eq!(
187            commit.timestamp,
188            DateTime::parse_from_str("Tue Apr 14 17:35:15 2026 -0400", COMMIT_TIME_FORMAT).unwrap()
189        );
190        assert_eq!(commit.message, "feat: added feature to get commit list");
191    }
192
193    #[test]
194    fn test_commit_new_commit() {
195        let c = String::from(
196            "490049bf36b19b30d23b4be5a4u94f71b5c6475c
197Author: Some Author <myemail@email.com>
198Date:   Tue Apr 14 17:35:15 2026 -0400
199
200    feat: added feature to get commit list
201",
202        );
203        let commit =
204            Commit::new_from_commit(c).expect("Commit could not be instantiated during test.");
205        assert_eq!(commit.id, "490049bf36b19b30d23b4be5a4u94f71b5c6475c");
206        assert_eq!(commit.author, "Some Author <myemail@email.com>");
207        assert_eq!(
208            commit.timestamp,
209            DateTime::parse_from_str("Tue Apr 14 17:35:15 2026 -0400", COMMIT_TIME_FORMAT).unwrap()
210        );
211        assert_eq!(commit.message, "feat: added feature to get commit list");
212    }
213}