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