use anyhow::{Context, Result};
use chrono::Utc;
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct GitVersion {
pub version_with_suffix: String,
pub publish_suffix: String,
pub branch_name: String,
pub revision: String,
pub is_dirty: bool,
pub commits_since_tag: u32,
}
impl GitVersion {
pub fn veridentity(&self, base_version: &str) -> String {
let timestamp = Utc::now().format("%Y%m%d%H%M%S").to_string();
let short = self.revision.chars().take(7).collect::<String>();
let dirty = if self.is_dirty { "-dirty" } else { "" };
format!("{}-{}-{}{}", base_version, timestamp, short, dirty)
}
pub fn from_project_root(project_root: &Path, base_version: &str) -> Result<Self> {
let branch_name = get_git_branch(project_root)?;
let revision = get_git_revision(project_root)?;
let is_dirty = is_git_dirty(project_root)?;
let commits_since_tag = get_commits_since_tag(project_root)?;
let publish_suffix = String::new();
let version_with_suffix = base_version.to_string();
Ok(GitVersion {
version_with_suffix,
publish_suffix,
branch_name,
revision,
is_dirty,
commits_since_tag,
})
}
}
fn get_git_branch(project_root: &Path) -> Result<String> {
let output = Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(project_root)
.output()
.context("Failed to get git branch")?;
if !output.status.success() {
return Ok("unknown".to_string());
}
let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(if branch.is_empty() { "unknown".to_string() } else { branch })
}
fn get_git_revision(project_root: &Path) -> Result<String> {
let output = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.current_dir(project_root)
.output()
.context("Failed to get git revision")?;
if !output.status.success() {
return Ok("unknown".to_string());
}
let revision = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(if revision.is_empty() { "unknown".to_string() } else { revision })
}
fn is_git_dirty(project_root: &Path) -> Result<bool> {
let output = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(project_root)
.output()
.context("Failed to check git status")?;
if !output.status.success() {
return Ok(false);
}
let status = String::from_utf8_lossy(&output.stdout);
Ok(!status.trim().is_empty())
}
fn get_commits_since_tag(project_root: &Path) -> Result<u32> {
let tag_output = Command::new("git")
.args(["describe", "--tags", "--abbrev=0"])
.current_dir(project_root)
.output()
.context("Failed to get git tags")?;
if !tag_output.status.success() {
return count_all_commits(project_root);
}
let latest_tag = String::from_utf8_lossy(&tag_output.stdout).trim().to_string();
if latest_tag.is_empty() {
return count_all_commits(project_root);
}
let count_output = Command::new("git")
.args(["rev-list", &format!("{latest_tag}..HEAD"), "--count"])
.current_dir(project_root)
.output()
.context("Failed to count commits since tag")?;
if !count_output.status.success() {
return Ok(0);
}
let count_str = String::from_utf8_lossy(&count_output.stdout).trim().to_string();
Ok(count_str.parse::<u32>().unwrap_or(0))
}
fn count_all_commits(project_root: &Path) -> Result<u32> {
let output = Command::new("git")
.args(["rev-list", "HEAD", "--count"])
.current_dir(project_root)
.output()
.context("Failed to count all commits")?;
if !output.status.success() {
return Ok(0);
}
let count_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(count_str.parse::<u32>().unwrap_or(0))
}
#[allow(dead_code)] fn calculate_publish_suffix(commits_since_tag: u32, is_dirty: bool) -> String {
if commits_since_tag == 0 && !is_dirty {
"release".to_string()
} else {
let base = if commits_since_tag > 0 {
format!("beta.{}", commits_since_tag)
} else {
"beta.0".to_string()
};
if is_dirty {
format!("{}-dirty", base)
} else {
base
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_publish_suffix_release() {
assert_eq!(calculate_publish_suffix(0, false), "release");
}
#[test]
fn test_publish_suffix_beta() {
assert_eq!(calculate_publish_suffix(5, false), "beta.5");
assert_eq!(calculate_publish_suffix(18, false), "beta.18");
}
#[test]
fn test_publish_suffix_dirty() {
assert_eq!(calculate_publish_suffix(0, true), "beta.0-dirty");
assert_eq!(calculate_publish_suffix(18, true), "beta.18-dirty");
}
}