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