LCOV - code coverage report
Current view: top level - dashql-pack/src - git_info.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 86.2 % 87 75
Test Date: 2026-11-06 17:46:50

            Line data    Source code
       1              : use anyhow::anyhow;
       2              : use anyhow::Result;
       3              : use git2::Repository;
       4              : use lazy_static::lazy_static;
       5              : use regex::Regex;
       6              : use semver::BuildMetadata;
       7              : use semver::Prerelease;
       8              : use semver::Version;
       9              : use std::fmt;
      10              : use std::path::PathBuf;
      11              : 
      12              : lazy_static! {
      13              :     static ref REGEX_DESCRIBE_LONG: Regex =
      14              :         Regex::new("v([0-9]+).([0-9]+).([0-9]+)-([0-9]+)-g([a-f0-9]+)").unwrap();
      15              : }
      16              : 
      17              : /// A version derived from the long `git describe` output of a repository
      18              : #[derive(Clone)]
      19              : pub struct GitVersion {
      20              :     /// The raw `git describe` output, e.g. `v1.2.3-4-gabcdef0`
      21              :     pub description: String,
      22              :     /// The major version of the latest tag
      23              :     pub major: u32,
      24              :     /// The minor version of the latest tag
      25              :     pub minor: u32,
      26              :     /// The patch version of the latest tag
      27              :     pub patch: u32,
      28              :     /// The number of commits since the latest tag, 0 for a tagged commit
      29              :     pub iteration: u32,
      30              :     /// The abbreviated hash of the described commit
      31              :     pub short_hash: String,
      32              : }
      33              : 
      34              : impl GitVersion {
      35           16 :     pub fn as_semver(&self) -> Version {
      36           16 :         if self.iteration == 0 {
      37           13 :             Version {
      38           13 :                 major: self.major as u64,
      39           13 :                 minor: self.minor as u64,
      40           13 :                 patch: self.patch as u64,
      41           13 :                 pre: Prerelease::EMPTY,
      42           13 :                 build: BuildMetadata::EMPTY,
      43           13 :             }
      44              :         } else {
      45            3 :             Version {
      46            3 :                 major: self.major as u64,
      47            3 :                 minor: self.minor as u64,
      48            3 :                 patch: (self.patch + 1) as u64,
      49            3 :                 pre: Prerelease::new(&format!("dev.{}", self.iteration)).unwrap(),
      50            3 :                 // build: BuildMetadata::new(&self.short_hash).unwrap(),
      51            3 :                 build: BuildMetadata::EMPTY,
      52            3 :             }
      53              :         }
      54           16 :     }
      55              : }
      56              : 
      57              : impl fmt::Display for GitVersion {
      58            1 :     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
      59            1 :         self.as_semver().fmt(f)
      60            1 :     }
      61              : }
      62              : 
      63              : /// The git information of the repository a release is built from
      64              : #[derive(Clone)]
      65              : pub struct GitInfo {
      66              :     /// The version described by git
      67              :     pub version: GitVersion,
      68              : }
      69              : 
      70           12 : pub fn describe_git_version(source_dir: &PathBuf) -> Result<GitVersion> {
      71              :     // Open the current git repository
      72           12 :     let repo = match Repository::open(source_dir) {
      73            9 :         Ok(repo) => repo,
      74            3 :         Err(e) => return Err(anyhow!("failed to open git repository at: {}", e)),
      75              :     };
      76              :     // Resolve the git repo description
      77            9 :     let mut describe_opts = git2::DescribeOptions::new();
      78            9 :     describe_opts.describe_tags();
      79            9 :     let mut format_opts = git2::DescribeFormatOptions::new();
      80            9 :     format_opts.always_use_long_format(true);
      81              : 
      82            9 :     let description = repo.describe(&describe_opts)?;
      83            9 :     let description_text = description.format(Some(&format_opts))?;
      84              : 
      85            9 :     let description_parts = match REGEX_DESCRIBE_LONG.captures(&description_text) {
      86            8 :         Some(parts) => parts,
      87              :         None => {
      88            1 :             return Err(anyhow!(
      89            1 :                 "git describe output is not matching version regex: {}",
      90            1 :                 &description_text
      91            1 :             ))
      92              :         }
      93              :     };
      94              : 
      95              :     Ok(GitVersion {
      96            8 :         description: description_text.clone(),
      97            8 :         major: description_parts[1].parse().map_err(|e| {
      98            0 :             anyhow!(
      99              :                 "failed to parse major version {}, {}",
     100            0 :                 &description_parts[1],
     101              :                 e
     102              :             )
     103            0 :         })?,
     104            8 :         minor: description_parts[2].parse().map_err(|e| {
     105            0 :             anyhow!(
     106              :                 "failed to parse minor version {}, {}",
     107            0 :                 &description_parts[2],
     108              :                 e
     109              :             )
     110            0 :         })?,
     111            8 :         patch: description_parts[3].parse().map_err(|e| {
     112            0 :             anyhow!(
     113              :                 "failed to parse patch version {}, {}",
     114            0 :                 &description_parts[3],
     115              :                 e
     116              :             )
     117            0 :         })?,
     118            8 :         iteration: description_parts[4].parse().map_err(|e| {
     119            0 :             anyhow!(
     120              :                 "failed to parse iteration version {}, {}",
     121            0 :                 &description_parts[4],
     122              :                 e
     123              :             )
     124            0 :         })?,
     125            8 :         short_hash: description_parts[5].to_string(),
     126              :     })
     127           12 : }
     128              : 
     129            9 : pub fn collect_git_info(source_dir: &PathBuf) -> Result<GitInfo> {
     130            9 :     let version = describe_git_version(&source_dir)?;
     131            7 :     Ok(GitInfo { version })
     132            9 : }
     133              : 
     134              : #[cfg(test)]
     135              : mod tests {
     136              :     use super::*;
     137              :     use crate::test_s3::{git_repository, TempDir};
     138              : 
     139              :     #[test]
     140            1 :     fn describes_a_tagged_commit() {
     141            1 :         let dir = TempDir::new();
     142            1 :         git_repository(dir.path(), "v1.2.3", 0);
     143            1 :         let info = collect_git_info(&dir.path().to_path_buf()).unwrap();
     144            1 :         assert_eq!((info.version.major, info.version.minor, info.version.patch, info.version.iteration), (1, 2, 3, 0));
     145            1 :         assert_eq!(info.version.short_hash.len(), 7);
     146            1 :         assert!(info.version.description.starts_with("v1.2.3-0-g"), "{}", info.version.description);
     147            1 :         assert_eq!(info.version.to_string(), "1.2.3");
     148            1 :     }
     149              : 
     150              :     #[test]
     151            1 :     fn counts_the_commits_after_the_tag() {
     152            1 :         let dir = TempDir::new();
     153            1 :         git_repository(dir.path(), "v1.2.3", 4);
     154            1 :         let version = describe_git_version(&dir.path().to_path_buf()).unwrap();
     155            1 :         assert_eq!(version.iteration, 4);
     156            1 :         assert_eq!(version.as_semver().to_string(), "1.2.4-dev.4");
     157            1 :     }
     158              : 
     159              :     #[test]
     160            1 :     fn fails_outside_of_a_repository() {
     161            1 :         let dir = TempDir::new();
     162            1 :         let error = describe_git_version(&dir.path().to_path_buf()).err().unwrap();
     163            1 :         assert!(error.to_string().contains("failed to open git repository"), "{error}");
     164            1 :     }
     165              : 
     166              :     #[test]
     167            1 :     fn fails_for_a_tag_that_is_no_version() {
     168            1 :         let dir = TempDir::new();
     169            1 :         git_repository(dir.path(), "nightly", 1);
     170            1 :         let error = describe_git_version(&dir.path().to_path_buf()).err().unwrap();
     171            1 :         assert!(error.to_string().contains("not matching version regex"), "{error}");
     172            1 :     }
     173              : }
        

Generated by: LCOV version 2.0-1