git_plumber/
version.rs

1use std::fmt;
2
3// Include the generated build information
4pub mod built_info {
5    include!(concat!(env!("OUT_DIR"), "/built.rs"));
6}
7
8pub struct VersionInfo {
9    pub version: &'static str,
10    pub commit_hash: Option<&'static str>,
11    pub commit_hash_short: Option<&'static str>,
12    pub git_version: Option<&'static str>,
13    pub is_dirty: bool,
14    pub target: &'static str,
15    pub profile: &'static str,
16    pub rustc_version: &'static str,
17}
18
19impl Default for VersionInfo {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl VersionInfo {
26    pub fn new() -> Self {
27        Self {
28            version: built_info::PKG_VERSION,
29            commit_hash: built_info::GIT_COMMIT_HASH,
30            commit_hash_short: built_info::GIT_COMMIT_HASH_SHORT,
31            git_version: built_info::GIT_VERSION,
32            is_dirty: built_info::GIT_DIRTY.unwrap_or(false),
33            target: built_info::TARGET,
34            profile: built_info::PROFILE,
35            rustc_version: built_info::RUSTC_VERSION,
36        }
37    }
38
39    pub fn is_development_build(&self) -> bool {
40        // Consider it a dev build if:
41        // 1. The working directory is dirty, OR
42        // 2. We have commits after the last release tag, OR
43        // 3. Building in debug mode
44        self.is_dirty
45            || self.profile == "debug"
46            || self.git_version.is_some_and(|git_ver| {
47                // Parse git version like "v0.1.0-15-ge1c9641" to detect commits after tag
48                git_ver.contains('-') && !git_ver.ends_with("-0-g")
49            })
50    }
51
52    pub fn short_version(&self) -> String {
53        if self.is_development_build() {
54            if let Some(git_version) = self.git_version {
55                // Parse the git version string like "v0.1.0-15-ge1c9641"
56                if git_version.contains('-') {
57                    let parts: Vec<&str> = git_version.split('-').collect();
58                    if parts.len() >= 3 {
59                        let commits_after = parts[parts.len() - 2];
60                        let commit_hash = parts[parts.len() - 1]
61                            .strip_prefix('g')
62                            .unwrap_or(parts[parts.len() - 1]);
63                        if self.is_dirty {
64                            format!(
65                                "v{}-dev+{}.{}.dirty",
66                                self.version, commits_after, commit_hash
67                            )
68                        } else {
69                            format!("v{}-dev+{}.{}", self.version, commits_after, commit_hash)
70                        }
71                    } else if self.is_dirty {
72                        format!("v{}-dev+dirty", self.version)
73                    } else {
74                        format!("v{}-dev", self.version)
75                    }
76                } else if let Some(hash) = self.commit_hash_short {
77                    if self.is_dirty {
78                        format!("v{}-dev+dirty.{}", self.version, hash)
79                    } else {
80                        format!("v{}-dev+{}", self.version, hash)
81                    }
82                } else {
83                    format!("v{}-dev", self.version)
84                }
85            } else if let Some(hash) = self.commit_hash_short {
86                if self.is_dirty {
87                    format!("v{}-dev+dirty.{}", self.version, hash)
88                } else {
89                    format!("v{}-dev+{}", self.version, hash)
90                }
91            } else {
92                format!("v{}-dev", self.version)
93            }
94        } else {
95            format!("v{}", self.version)
96        }
97    }
98}
99
100impl fmt::Display for VersionInfo {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        writeln!(f, "git-plumber\n\nVersion: {}", self.short_version())?;
103
104        if self.is_development_build() {
105            if let Some(git_version) = self.git_version {
106                writeln!(f, "Git version notation: {git_version}")?;
107            }
108            if let Some(commit_hash) = self.commit_hash {
109                writeln!(f, "Commit hash: {commit_hash}")?;
110            }
111            if let Some(commits_ahead) = self
112                .git_version
113                .and_then(|v| v.split('-').nth(1).and_then(|n| n.parse::<u32>().ok()))
114            {
115                writeln!(f, "Commits since last tag: {commits_ahead}")?;
116            }
117
118            if self.is_dirty {
119                writeln!(f, "Working directory: dirty")?;
120            }
121
122            writeln!(f, "Profile: {}", self.profile)?;
123            writeln!(f, "Target: {}", self.target)?;
124            writeln!(f, "Rust: {}", self.rustc_version)?;
125        }
126
127        Ok(())
128    }
129}
130
131pub fn get_version_info() -> VersionInfo {
132    VersionInfo::new()
133}