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