use { crate::{ error::Error, extended::Extended, rapid::Rapid, semver::SemVer, simple::Simple, version::Kind, version::Version, }, chrono::{offset::Utc, DateTime}, serde::{Deserialize, Serialize}, std::{cmp::Ordering, fmt, str::FromStr}, }; /// Represents a Git revisionrather than a regular release. /// A `GitRev` release contains the revision's hash and the Date/Time of the release. /// Git revisions are not the preferred method of distribution for obvious reasons, as /// they generally do not represent a stable release of the code. Another drawback is /// that a Git revision can only be compared against another Git revision, making it /// impossible to properly order updates between a proper `SemVer` type release and a\ /// Git revision. /// ### Notes on display formatting /// The hash as stored in this struct should be the short form revision hash (the first /// 7 characters of the full hash). The full date and time information is saved minus /// any fractional seconds. When displaying, the date and time should be converted to a /// Unix timestamp. /// ### Parsing from a string /// In order to parse this struct from a string, two fields `hash` and `datetime` should /// be separated by a period character '.' and the date/time take the form of a Unix /// timestamp. This information can be retrieved from git for a given commit using the /// following git commandline: /// ```Sh /// git show --pretty=format:"%h.%at" | head -n 1 /// ``` /// The resulting string can then be parsed using the `FromStr` trait, which provides /// both `from_str` and `parse`. /// ### Comparison - ordering /// Git revisions are ordered by date/time #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct GitRev { /// the short revision hash pub hash: String, /// the time of the revision commit pub datetime: DateTime, } impl fmt::Display for GitRev { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "git_{}.{}", self.hash, self.datetime.timestamp()) } } impl PartialOrd for GitRev { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for GitRev { fn cmp(&self, other: &Self) -> Ordering { self.datetime.cmp(&other.datetime) } } impl FromStr for GitRev { type Err = Error; fn from_str(s: &str) -> Result { if let Some(gitrev) = s.strip_prefix("git_") { if let Some((hash, datetime)) = gitrev.split_once('.') { if hash.len() == 7 { let secs: i64 = datetime.parse()?; let Some(datetime) = DateTime::::from_timestamp(secs, 0) else { return Err(Error::ParseInt); }; return Ok(Self { hash: hash.to_string(), datetime, }); } } } Err(Error::ParseGitRev) } } impl PartialEq for GitRev { fn eq(&self, _other: &Extended) -> bool { false } } impl PartialOrd for GitRev { fn partial_cmp(&self, _other: &Extended) -> Option { None } } impl PartialEq for GitRev { fn eq(&self, _other: &SemVer) -> bool { false } } impl PartialOrd for GitRev { fn partial_cmp(&self, _other: &SemVer) -> Option { None } } impl PartialEq for GitRev { fn eq(&self, _other: &Rapid) -> bool { false } } impl PartialOrd for GitRev { fn partial_cmp(&self, _other: &Rapid) -> Option { None } } impl PartialEq for GitRev { fn eq(&self, _other: &Simple) -> bool { false } } impl PartialOrd for GitRev { fn partial_cmp(&self, _other: &Simple) -> Option { None } } impl TryFrom for GitRev { type Error = Error; fn try_from(value: Version) -> Result { match value.0 { Kind::GitRev(g) => Ok(g), _ => Err(Error::ParseGitRev), } } } #[cfg(test)] mod test { use std::{thread, time::Duration}; use super::*; #[test] fn ord() { let a = GitRev { hash: "aaabxxx".to_string(), datetime: Utc::now(), }; thread::sleep(Duration::from_millis(10)); let b = GitRev { hash: "aaaaxxx".to_string(), datetime: Utc::now(), }; assert!(a < b); } #[test] fn from_str() { let now = DateTime::::from_timestamp(Utc::now().timestamp(), 0).unwrap(); let rev: GitRev = format!("git_r2d2xxx.{}", now.timestamp()).parse().unwrap(); println!("Version = {rev}"); assert_eq!( rev, GitRev { hash: "r2d2xxx".to_string(), datetime: now, } ); } }