abbaye/version_extractors/
git.rs1use chrono::{DateTime, Utc};
2use miette::{IntoDiagnostic, Result, miette};
3use serde::{Deserialize, Serialize};
4
5use super::{VersionExtractor, VersionInfo};
6
7fn default_dirty_suffix() -> String {
8 "-dirty".to_owned()
9}
10
11#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct GitVersionConfig {
14 pub tag_prefix: Option<String>,
17
18 #[serde(default = "default_dirty_suffix")]
22 pub dirty_suffix: String,
23}
24
25impl Default for GitVersionConfig {
26 fn default() -> Self {
27 Self {
28 tag_prefix: None,
29 dirty_suffix: default_dirty_suffix(),
30 }
31 }
32}
33
34pub struct GitVersion;
36
37async fn git_commit_date(refspec: &str) -> Option<DateTime<Utc>> {
41 let output = tokio::process::Command::new("git")
42 .args(["log", "-1", "--format=%cI", refspec])
43 .output()
44 .await
45 .ok()?;
46
47 if !output.status.success() {
48 return None;
49 }
50
51 let raw = String::from_utf8(output.stdout).ok()?;
52 DateTime::parse_from_rfc3339(raw.trim())
53 .ok()
54 .map(|dt| dt.with_timezone(&Utc))
55}
56
57impl VersionExtractor for GitVersion {
58 type ConfigType = GitVersionConfig;
59
60 async fn get_last_version(&self, config: Self::ConfigType) -> Result<VersionInfo> {
61 let dirty_arg = format!("--dirty={}", config.dirty_suffix);
62
63 let output = tokio::process::Command::new("git")
64 .args(["describe", "--tags", "--always", &dirty_arg])
65 .output()
66 .await
67 .into_diagnostic()?;
68
69 if !output.status.success() {
70 let stderr = String::from_utf8_lossy(&output.stderr);
71 return Err(miette!("git describe failed:\n{stderr}"));
72 }
73
74 let raw = String::from_utf8(output.stdout).into_diagnostic()?;
75 let version_raw = raw.trim();
76
77 let version = match &config.tag_prefix {
78 Some(prefix) => version_raw
79 .strip_prefix(prefix.as_str())
80 .unwrap_or(version_raw),
81 None => version_raw,
82 }
83 .to_owned();
84
85 let date = git_commit_date("HEAD").await;
87
88 Ok(VersionInfo { version, date })
89 }
90
91 async fn get_all_versions(&self, config: Self::ConfigType) -> Result<Vec<VersionInfo>> {
95 let output = tokio::process::Command::new("git")
97 .args([
98 "for-each-ref",
99 "--sort=version:refname",
100 "--format=%(refname:short)\t%(creatordate:iso-strict)",
101 "refs/tags",
102 ])
103 .output()
104 .await
105 .into_diagnostic()?;
106
107 if !output.status.success() {
108 let stderr = String::from_utf8_lossy(&output.stderr);
109 return Err(miette!("git for-each-ref failed:\n{stderr}"));
110 }
111
112 let raw = String::from_utf8(output.stdout).into_diagnostic()?;
113
114 let versions = raw
115 .lines()
116 .filter(|s| !s.is_empty())
117 .map(|line| {
118 let (tag, date_str) = line.split_once('\t').unwrap_or((line, ""));
120
121 let version = match &config.tag_prefix {
122 Some(prefix) => tag.strip_prefix(prefix.as_str()).unwrap_or(tag),
123 None => tag,
124 }
125 .to_owned();
126
127 let date = DateTime::parse_from_rfc3339(date_str.trim())
128 .ok()
129 .map(|dt| dt.with_timezone(&Utc));
130
131 VersionInfo { version, date }
132 })
133 .collect();
134
135 Ok(versions)
136 }
137}