shadow_rs/
git.rs

1use crate::build::{ConstType, ConstVal, ShadowConst};
2use crate::ci::CiType;
3use crate::err::*;
4use crate::{DateTime, Format};
5use std::collections::BTreeMap;
6use std::io::{BufReader, Read};
7use std::path::Path;
8use std::process::{Command, Stdio};
9
10const BRANCH_DOC: &str = r#"
11The name of the Git branch that this project was built from.
12This constant will be empty if the branch cannot be determined."#;
13pub const BRANCH: ShadowConst = "BRANCH";
14
15const TAG_DOC: &str = r#"
16The name of the Git tag that this project was built from.
17Note that this will be empty if there is no tag for the HEAD at the time of build."#;
18pub const TAG: ShadowConst = "TAG";
19
20const LAST_TAG_DOC: &str = r#"
21The name of the last Git tag on the branch that this project was built from.
22As opposed to [`TAG`], this does not require the current commit to be tagged, just one of its parents.
23
24This constant will be empty if the last tag cannot be determined."#;
25pub const LAST_TAG: ShadowConst = "LAST_TAG";
26
27pub const COMMITS_SINCE_TAG_DOC: &str = r#"
28The number of commits since the last Git tag on the branch that this project was built from.
29This value indicates how many commits have been made after the last tag and before the current commit.
30
31If there are no additional commits after the last tag (i.e., the current commit is exactly at a tag),
32this value will be `0`.
33
34This constant will be empty or `0` if the last tag cannot be determined or if there are no commits after it.
35"#;
36
37pub const COMMITS_SINCE_TAG: &str = "COMMITS_SINCE_TAG";
38
39const SHORT_COMMIT_DOC: &str = r#"
40The short hash of the Git commit that this project was built from.
41Note that this will always truncate [`COMMIT_HASH`] to 8 characters if necessary.
42Depending on the amount of commits in your project, this may not yield a unique Git identifier
43([see here for more details on hash abbreviation](https://git-scm.com/docs/git-describe#_examples)).
44
45This constant will be empty if the last commit cannot be determined."#;
46pub const SHORT_COMMIT: ShadowConst = "SHORT_COMMIT";
47
48const COMMIT_HASH_DOC: &str = r#"
49The full commit hash of the Git commit that this project was built from.
50An abbreviated, but not necessarily unique, version of this is [`SHORT_COMMIT`].
51
52This constant will be empty if the last commit cannot be determined."#;
53pub const COMMIT_HASH: ShadowConst = "COMMIT_HASH";
54
55const COMMIT_DATE_DOC: &str = r#"The time of the Git commit that this project was built from.
56The time is formatted in modified ISO 8601 format (`YYYY-MM-DD HH-MM ±hh-mm` where hh-mm is the offset from UTC).
57
58This constant will be empty if the last commit cannot be determined."#;
59pub const COMMIT_DATE: ShadowConst = "COMMIT_DATE";
60
61const COMMIT_DATE_2822_DOC: &str = r#"
62The name of the Git branch that this project was built from.
63The time is formatted according to [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822#section-3.3) (e.g. HTTP Headers).
64
65This constant will be empty if the last commit cannot be determined."#;
66pub const COMMIT_DATE_2822: ShadowConst = "COMMIT_DATE_2822";
67
68const COMMIT_DATE_3339_DOC: &str = r#"
69The name of the Git branch that this project was built from.
70The time is formatted according to [RFC 3339 and ISO 8601](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6).
71
72This constant will be empty if the last commit cannot be determined."#;
73pub const COMMIT_DATE_3339: ShadowConst = "COMMIT_DATE_3339";
74
75const COMMIT_AUTHOR_DOC: &str = r#"
76The author of the Git commit that this project was built from.
77
78This constant will be empty if the last commit cannot be determined."#;
79pub const COMMIT_AUTHOR: ShadowConst = "COMMIT_AUTHOR";
80
81const COMMIT_EMAIL_DOC: &str = r#"
82The e-mail address of the author of the Git commit that this project was built from.
83
84This constant will be empty if the last commit cannot be determined."#;
85pub const COMMIT_EMAIL: ShadowConst = "COMMIT_EMAIL";
86
87const GIT_CLEAN_DOC: &str = r#"
88Whether the Git working tree was clean at the time of project build (`true`), or not (`false`).
89
90This constant will be `false` if the last commit cannot be determined."#;
91pub const GIT_CLEAN: ShadowConst = "GIT_CLEAN";
92
93const GIT_STATUS_FILE_DOC: &str = r#"
94The Git working tree status as a list of files with their status, similar to `git status`.
95Each line of the list is preceded with `  * `, followed by the file name.
96Files marked `(dirty)` have unstaged changes.
97Files marked `(staged)` have staged changes.
98
99This constant will be empty if the working tree status cannot be determined."#;
100pub const GIT_STATUS_FILE: ShadowConst = "GIT_STATUS_FILE";
101
102#[derive(Default, Debug)]
103pub struct Git {
104    map: BTreeMap<ShadowConst, ConstVal>,
105    ci_type: CiType,
106}
107
108impl Git {
109    fn update_str(&mut self, c: ShadowConst, v: String) {
110        if let Some(val) = self.map.get_mut(c) {
111            *val = ConstVal {
112                desc: val.desc.clone(),
113                v,
114                t: ConstType::Str,
115            }
116        }
117    }
118
119    fn update_bool(&mut self, c: ShadowConst, v: bool) {
120        if let Some(val) = self.map.get_mut(c) {
121            *val = ConstVal {
122                desc: val.desc.clone(),
123                v: v.to_string(),
124                t: ConstType::Bool,
125            }
126        }
127    }
128
129    fn update_usize(&mut self, c: ShadowConst, v: usize) {
130        if let Some(val) = self.map.get_mut(c) {
131            *val = ConstVal {
132                desc: val.desc.clone(),
133                v: v.to_string(),
134                t: ConstType::Usize,
135            }
136        }
137    }
138
139    fn init(&mut self, path: &Path, std_env: &BTreeMap<String, String>) -> SdResult<()> {
140        // First, try executing using the git command.
141        if let Err(err) = self.init_git() {
142            println!("{err}");
143        }
144
145        // If the git2 feature is enabled, then replace the corresponding values with git2.
146        self.init_git2(path)?;
147
148        // use command branch
149        if let Some(x) = find_branch_in(path) {
150            self.update_str(BRANCH, x)
151        };
152
153        // use command tag
154        if let Some(x) = command_current_tag() {
155            self.update_str(TAG, x)
156        }
157
158        // use command get last tag
159        let describe = command_git_describe();
160        if let Some(x) = describe.0 {
161            self.update_str(LAST_TAG, x)
162        }
163
164        if let Some(x) = describe.1 {
165            self.update_usize(COMMITS_SINCE_TAG, x)
166        }
167
168        // try use ci branch,tag
169        self.ci_branch_tag(std_env);
170        Ok(())
171    }
172
173    fn init_git(&mut self) -> SdResult<()> {
174        // check git status
175        let x = command_git_clean();
176        self.update_bool(GIT_CLEAN, x);
177
178        let x = command_git_status_file();
179        self.update_str(GIT_STATUS_FILE, x);
180
181        let git_info = command_git_head();
182
183        self.update_str(COMMIT_EMAIL, git_info.email);
184        self.update_str(COMMIT_AUTHOR, git_info.author);
185        self.update_str(SHORT_COMMIT, git_info.short_commit);
186        self.update_str(COMMIT_HASH, git_info.commit);
187
188        let time_stamp = git_info.date.parse::<i64>()?;
189        if let Ok(date_time) = DateTime::timestamp_2_utc(time_stamp) {
190            self.update_str(COMMIT_DATE, date_time.human_format());
191            self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
192            self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
193        }
194
195        Ok(())
196    }
197
198    #[allow(unused_variables)]
199    fn init_git2(&mut self, path: &Path) -> SdResult<()> {
200        #[cfg(feature = "git2")]
201        {
202            use crate::date_time::DateTime;
203            use crate::git::git2_mod::git_repo;
204            use crate::Format;
205
206            let repo = git_repo(path).map_err(ShadowError::new)?;
207            let reference = repo.head().map_err(ShadowError::new)?;
208
209            //get branch
210            let branch = reference
211                .shorthand()
212                .map(|x| x.trim().to_string())
213                .or_else(command_current_branch)
214                .unwrap_or_default();
215
216            //get HEAD branch
217            let tag = command_current_tag().unwrap_or_default();
218            self.update_str(BRANCH, branch);
219            self.update_str(TAG, tag);
220
221            // use command get last tag
222            let describe = command_git_describe();
223            if let Some(x) = describe.0 {
224                self.update_str(LAST_TAG, x)
225            }
226
227            if let Some(x) = describe.1 {
228                self.update_usize(COMMITS_SINCE_TAG, x)
229            }
230
231            if let Some(v) = reference.target() {
232                let commit = v.to_string();
233                self.update_str(COMMIT_HASH, commit.clone());
234                let mut short_commit = commit.as_str();
235
236                if commit.len() > 8 {
237                    short_commit = short_commit.get(0..8).unwrap();
238                }
239                self.update_str(SHORT_COMMIT, short_commit.to_string());
240            }
241
242            let commit = reference.peel_to_commit().map_err(ShadowError::new)?;
243
244            let author = commit.author();
245            if let Some(v) = author.email() {
246                self.update_str(COMMIT_EMAIL, v.to_string());
247            }
248
249            if let Some(v) = author.name() {
250                self.update_str(COMMIT_AUTHOR, v.to_string());
251            }
252            let status_file = Self::git2_dirty_stage(&repo);
253            if status_file.trim().is_empty() {
254                self.update_bool(GIT_CLEAN, true);
255            } else {
256                self.update_bool(GIT_CLEAN, false);
257            }
258            self.update_str(GIT_STATUS_FILE, status_file);
259
260            let time_stamp = commit.time().seconds().to_string().parse::<i64>()?;
261            if let Ok(date_time) = DateTime::timestamp_2_utc(time_stamp) {
262                self.update_str(COMMIT_DATE, date_time.human_format());
263
264                self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
265
266                self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
267            }
268        }
269        Ok(())
270    }
271
272    //use git2 crates git repository 'dirty or stage' status files.
273    #[cfg(feature = "git2")]
274    pub fn git2_dirty_stage(repo: &git2::Repository) -> String {
275        let mut repo_opts = git2::StatusOptions::new();
276        repo_opts.include_ignored(false);
277        if let Ok(statue) = repo.statuses(Some(&mut repo_opts)) {
278            let mut dirty_files = Vec::new();
279            let mut staged_files = Vec::new();
280
281            for status in statue.iter() {
282                if let Some(path) = status.path() {
283                    match status.status() {
284                        git2::Status::CURRENT => (),
285                        git2::Status::INDEX_NEW
286                        | git2::Status::INDEX_MODIFIED
287                        | git2::Status::INDEX_DELETED
288                        | git2::Status::INDEX_RENAMED
289                        | git2::Status::INDEX_TYPECHANGE => staged_files.push(path.to_string()),
290                        _ => dirty_files.push(path.to_string()),
291                    };
292                }
293            }
294            filter_git_dirty_stage(dirty_files, staged_files)
295        } else {
296            "".into()
297        }
298    }
299
300    #[allow(clippy::manual_strip)]
301    fn ci_branch_tag(&mut self, std_env: &BTreeMap<String, String>) {
302        let mut branch: Option<String> = None;
303        let mut tag: Option<String> = None;
304        match self.ci_type {
305            CiType::Gitlab => {
306                if let Some(v) = std_env.get("CI_COMMIT_TAG") {
307                    tag = Some(v.to_string());
308                } else if let Some(v) = std_env.get("CI_COMMIT_REF_NAME") {
309                    branch = Some(v.to_string());
310                }
311            }
312            CiType::Github => {
313                if let Some(v) = std_env.get("GITHUB_REF") {
314                    let ref_branch_prefix: &str = "refs/heads/";
315                    let ref_tag_prefix: &str = "refs/tags/";
316
317                    if v.starts_with(ref_branch_prefix) {
318                        branch = Some(
319                            v.get(ref_branch_prefix.len()..)
320                                .unwrap_or_default()
321                                .to_string(),
322                        )
323                    } else if v.starts_with(ref_tag_prefix) {
324                        tag = Some(
325                            v.get(ref_tag_prefix.len()..)
326                                .unwrap_or_default()
327                                .to_string(),
328                        )
329                    }
330                }
331            }
332            _ => {}
333        }
334        if let Some(x) = branch {
335            self.update_str(BRANCH, x);
336        }
337
338        if let Some(x) = tag {
339            self.update_str(TAG, x.clone());
340            self.update_str(LAST_TAG, x);
341        }
342    }
343}
344
345pub(crate) fn new_git(
346    path: &Path,
347    ci: CiType,
348    std_env: &BTreeMap<String, String>,
349) -> BTreeMap<ShadowConst, ConstVal> {
350    let mut git = Git {
351        map: Default::default(),
352        ci_type: ci,
353    };
354    git.map.insert(BRANCH, ConstVal::new(BRANCH_DOC));
355
356    git.map.insert(TAG, ConstVal::new(TAG_DOC));
357
358    git.map.insert(LAST_TAG, ConstVal::new(LAST_TAG_DOC));
359
360    git.map
361        .insert(COMMITS_SINCE_TAG, ConstVal::new(COMMITS_SINCE_TAG_DOC));
362
363    git.map.insert(COMMIT_HASH, ConstVal::new(COMMIT_HASH_DOC));
364
365    git.map
366        .insert(SHORT_COMMIT, ConstVal::new(SHORT_COMMIT_DOC));
367
368    git.map
369        .insert(COMMIT_AUTHOR, ConstVal::new(COMMIT_AUTHOR_DOC));
370    git.map
371        .insert(COMMIT_EMAIL, ConstVal::new(COMMIT_EMAIL_DOC));
372    git.map.insert(COMMIT_DATE, ConstVal::new(COMMIT_DATE_DOC));
373
374    git.map
375        .insert(COMMIT_DATE_2822, ConstVal::new(COMMIT_DATE_2822_DOC));
376
377    git.map
378        .insert(COMMIT_DATE_3339, ConstVal::new(COMMIT_DATE_3339_DOC));
379
380    git.map.insert(GIT_CLEAN, ConstVal::new_bool(GIT_CLEAN_DOC));
381
382    git.map
383        .insert(GIT_STATUS_FILE, ConstVal::new(GIT_STATUS_FILE_DOC));
384
385    if let Err(e) = git.init(path, std_env) {
386        println!("{e}");
387    }
388
389    git.map
390}
391
392#[cfg(feature = "git2")]
393pub mod git2_mod {
394    use git2::Error as git2Error;
395    use git2::Repository;
396    use std::path::Path;
397
398    pub fn git_repo<P: AsRef<Path>>(path: P) -> Result<Repository, git2Error> {
399        Repository::discover(path)
400    }
401
402    pub fn git2_current_branch(repo: &Repository) -> Option<String> {
403        repo.head()
404            .map(|x| x.shorthand().map(|x| x.to_string()))
405            .unwrap_or(None)
406    }
407}
408
409/// get current repository git branch.
410///
411/// When current repository exists git folder.
412///
413/// It's use default feature.This function try use [git2] crates get current branch.
414/// If not use git2 feature,then try use [Command] to get.
415pub fn branch() -> String {
416    #[cfg(feature = "git2")]
417    {
418        use crate::git::git2_mod::{git2_current_branch, git_repo};
419        git_repo(".")
420            .map(|x| git2_current_branch(&x))
421            .unwrap_or_else(|_| command_current_branch())
422            .unwrap_or_default()
423    }
424    #[cfg(not(feature = "git2"))]
425    {
426        command_current_branch().unwrap_or_default()
427    }
428}
429
430/// get current repository git tag.
431///
432/// When current repository exists git folder.
433/// I's use [Command] to get.
434pub fn tag() -> String {
435    command_current_tag().unwrap_or_default()
436}
437
438/// Check current git Repository status without nothing(dirty or stage)
439///
440/// if nothing,It means clean:true. On the contrary, it is 'dirty':false
441pub fn git_clean() -> bool {
442    #[cfg(feature = "git2")]
443    {
444        use crate::git::git2_mod::git_repo;
445        git_repo(".")
446            .map(|x| Git::git2_dirty_stage(&x))
447            .map(|x| x.trim().is_empty())
448            .unwrap_or(true)
449    }
450    #[cfg(not(feature = "git2"))]
451    {
452        command_git_clean()
453    }
454}
455
456/// List current git Repository statue(dirty or stage) contain file changed
457///
458/// Refer to the 'cargo fix' result output when git statue(dirty or stage) changed.
459///
460/// Example output:`   * examples/builtin_fn.rs (dirty)`
461pub fn git_status_file() -> String {
462    #[cfg(feature = "git2")]
463    {
464        use crate::git::git2_mod::git_repo;
465        git_repo(".")
466            .map(|x| Git::git2_dirty_stage(&x))
467            .unwrap_or_default()
468    }
469    #[cfg(not(feature = "git2"))]
470    {
471        command_git_status_file()
472    }
473}
474
475struct GitHeadInfo {
476    commit: String,
477    short_commit: String,
478    email: String,
479    author: String,
480    date: String,
481}
482
483struct GitCommandExecutor<'a> {
484    path: &'a Path,
485}
486
487impl Default for GitCommandExecutor<'_> {
488    fn default() -> Self {
489        Self::new(Path::new("."))
490    }
491}
492
493impl<'a> GitCommandExecutor<'a> {
494    fn new(path: &'a Path) -> Self {
495        GitCommandExecutor { path }
496    }
497
498    fn exec(&self, args: &[&str]) -> Option<String> {
499        Command::new("git")
500            .env("GIT_OPTIONAL_LOCKS", "0")
501            .current_dir(self.path)
502            .args(args)
503            .output()
504            .map(|x| {
505                String::from_utf8(x.stdout)
506                    .map(|x| x.trim().to_string())
507                    .ok()
508            })
509            .unwrap_or(None)
510    }
511}
512
513fn command_git_head() -> GitHeadInfo {
514    let cli = |args: &[&str]| GitCommandExecutor::default().exec(args).unwrap_or_default();
515    GitHeadInfo {
516        commit: cli(&["rev-parse", "HEAD"]),
517        short_commit: cli(&["rev-parse", "--short", "HEAD"]),
518        author: cli(&["log", "-1", "--pretty=format:%an"]),
519        email: cli(&["log", "-1", "--pretty=format:%ae"]),
520        date: cli(&["show", "--pretty=format:%ct", "--date=raw", "-s"]),
521    }
522}
523
524/// Command exec git current tag
525fn command_current_tag() -> Option<String> {
526    GitCommandExecutor::default().exec(&["tag", "-l", "--contains", "HEAD"])
527}
528
529/// git describe --tags HEAD
530/// Command exec git describe
531fn command_git_describe() -> (Option<String>, Option<usize>, Option<String>) {
532    let last_tag =
533        GitCommandExecutor::default().exec(&["describe", "--tags", "--abbrev=0", "HEAD"]);
534    if last_tag.is_none() {
535        return (None, None, None);
536    }
537
538    let tag = last_tag.unwrap();
539
540    let describe = GitCommandExecutor::default().exec(&["describe", "--tags", "HEAD"]);
541    if let Some(desc) = describe {
542        match parse_git_describe(&tag, &desc) {
543            Ok((tag, commits, hash)) => {
544                return (Some(tag), commits, hash);
545            }
546            Err(_) => {
547                return (Some(tag), None, None);
548            }
549        }
550    }
551    (Some(tag), None, None)
552}
553
554fn parse_git_describe(
555    last_tag: &str,
556    describe: &str,
557) -> SdResult<(String, Option<usize>, Option<String>)> {
558    if !describe.starts_with(last_tag) {
559        return Err(ShadowError::String("git describe result error".to_string()));
560    }
561
562    if last_tag == describe {
563        return Ok((describe.to_string(), None, None));
564    }
565
566    let parts: Vec<&str> = describe.rsplit('-').collect();
567
568    if parts.is_empty() || parts.len() == 2 {
569        return Err(ShadowError::String(
570            "git describe result error,expect:<tag>-<num_commits>-g<hash>".to_string(),
571        ));
572    }
573
574    if parts.len() > 2 {
575        let short_hash = parts[0]; // last part
576
577        if !short_hash.starts_with('g') {
578            return Err(ShadowError::String(
579                "git describe result error,expect commit hash end with:-g<hash>".to_string(),
580            ));
581        }
582        let short_hash = short_hash.trim_start_matches('g');
583
584        // Full example:v1.0.0-alpha0-5-ga1b2c3d
585        let num_commits_str = parts[1];
586        let num_commits = num_commits_str
587            .parse::<usize>()
588            .map_err(|e| ShadowError::String(e.to_string()))?;
589        let last_tag = parts[2..]
590            .iter()
591            .rev()
592            .copied()
593            .collect::<Vec<_>>()
594            .join("-");
595        return Ok((last_tag, Some(num_commits), Some(short_hash.to_string())));
596    }
597    Ok((describe.to_string(), None, None))
598}
599
600/// git clean:git status --porcelain
601/// check repository git status is clean
602fn command_git_clean() -> bool {
603    GitCommandExecutor::default()
604        .exec(&["status", "--porcelain"])
605        .map(|x| x.is_empty())
606        .unwrap_or(true)
607}
608
609/// check git repository 'dirty or stage' status files.
610/// git dirty:git status  --porcelain | grep '^\sM.' |awk '{print $2}'
611/// git stage:git status --porcelain --untracked-files=all | grep '^[A|M|D|R]'|awk '{print $2}'
612fn command_git_status_file() -> String {
613    let git_status_files =
614        move |args: &[&str], grep: &[&str], awk: &[&str]| -> SdResult<Vec<String>> {
615            let git_shell = Command::new("git")
616                .args(args)
617                .stdin(Stdio::piped())
618                .stdout(Stdio::piped())
619                .spawn()?;
620            let git_out = git_shell.stdout.ok_or("Failed to exec git stdout")?;
621
622            let grep_shell = Command::new("grep")
623                .args(grep)
624                .stdin(Stdio::from(git_out))
625                .stdout(Stdio::piped())
626                .spawn()?;
627            let grep_out = grep_shell.stdout.ok_or("Failed to exec grep stdout")?;
628
629            let mut awk_shell = Command::new("awk")
630                .args(awk)
631                .stdin(Stdio::from(grep_out))
632                .stdout(Stdio::piped())
633                .spawn()?;
634            let mut awk_out = BufReader::new(
635                awk_shell
636                    .stdout
637                    .as_mut()
638                    .ok_or("Failed to exec awk stdout")?,
639            );
640            let mut line = String::new();
641            awk_out.read_to_string(&mut line)?;
642            Ok(line.lines().map(|x| x.into()).collect())
643        };
644
645    let dirty = git_status_files(&["status", "--porcelain"], &[r"^\sM."], &["{print $2}"])
646        .unwrap_or_default();
647
648    let stage = git_status_files(
649        &["status", "--porcelain", "--untracked-files=all"],
650        &[r#"^[A|M|D|R]"#],
651        &["{print $2}"],
652    )
653    .unwrap_or_default();
654    filter_git_dirty_stage(dirty, stage)
655}
656
657/// Command exec git current branch
658fn command_current_branch() -> Option<String> {
659    find_branch_in(Path::new("."))
660}
661
662fn find_branch_in(path: &Path) -> Option<String> {
663    GitCommandExecutor::new(path).exec(&["symbolic-ref", "--short", "HEAD"])
664}
665
666fn filter_git_dirty_stage(dirty_files: Vec<String>, staged_files: Vec<String>) -> String {
667    let mut concat_file = String::new();
668    for file in dirty_files {
669        concat_file.push_str("  * ");
670        concat_file.push_str(&file);
671        concat_file.push_str(" (dirty)\n");
672    }
673    for file in staged_files {
674        concat_file.push_str("  * ");
675        concat_file.push_str(&file);
676        concat_file.push_str(" (staged)\n");
677    }
678    concat_file
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684    use crate::get_std_env;
685
686    #[test]
687    fn test_git() {
688        let env_map = get_std_env();
689        let map = new_git(Path::new("./"), CiType::Github, &env_map);
690        for (k, v) in map {
691            assert!(!v.desc.is_empty());
692            if !k.eq(TAG)
693                && !k.eq(LAST_TAG)
694                && !k.eq(COMMITS_SINCE_TAG)
695                && !k.eq(BRANCH)
696                && !k.eq(GIT_STATUS_FILE)
697            {
698                assert!(!v.v.is_empty());
699                continue;
700            }
701
702            //assert github tag always exist value
703            if let Some(github_ref) = env_map.get("GITHUB_REF") {
704                if github_ref.starts_with("refs/tags/") && k.eq(TAG) {
705                    assert!(!v.v.is_empty(), "not empty");
706                } else if github_ref.starts_with("refs/heads/") && k.eq(BRANCH) {
707                    assert!(!v.v.is_empty());
708                }
709            }
710        }
711    }
712
713    #[test]
714    fn test_current_branch() {
715        if get_std_env().contains_key("GITHUB_REF") {
716            return;
717        }
718        #[cfg(feature = "git2")]
719        {
720            use crate::git::git2_mod::{git2_current_branch, git_repo};
721            let git2_branch = git_repo(".")
722                .map(|x| git2_current_branch(&x))
723                .unwrap_or(None);
724            let command_branch = command_current_branch();
725            assert!(git2_branch.is_some());
726            assert!(command_branch.is_some());
727            assert_eq!(command_branch, git2_branch);
728        }
729
730        assert_eq!(Some(branch()), command_current_branch());
731    }
732
733    #[test]
734    fn test_parse_git_describe() {
735        let commit_hash = "24skp4489";
736        let describe = "v1.0.0";
737        assert_eq!(
738            parse_git_describe("v1.0.0", describe).unwrap(),
739            (describe.into(), None, None)
740        );
741
742        let describe = "v1.0.0-0-g24skp4489";
743        assert_eq!(
744            parse_git_describe("v1.0.0", describe).unwrap(),
745            ("v1.0.0".into(), Some(0), Some(commit_hash.into()))
746        );
747
748        let describe = "v1.0.0-1-g24skp4489";
749        assert_eq!(
750            parse_git_describe("v1.0.0", describe).unwrap(),
751            ("v1.0.0".into(), Some(1), Some(commit_hash.into()))
752        );
753
754        let describe = "v1.0.0-alpha-0-g24skp4489";
755        assert_eq!(
756            parse_git_describe("v1.0.0-alpha", describe).unwrap(),
757            ("v1.0.0-alpha".into(), Some(0), Some(commit_hash.into()))
758        );
759
760        let describe = "v1.0.0.alpha-0-g24skp4489";
761        assert_eq!(
762            parse_git_describe("v1.0.0.alpha", describe).unwrap(),
763            ("v1.0.0.alpha".into(), Some(0), Some(commit_hash.into()))
764        );
765
766        let describe = "v1.0.0-alpha";
767        assert_eq!(
768            parse_git_describe("v1.0.0-alpha", describe).unwrap(),
769            ("v1.0.0-alpha".into(), None, None)
770        );
771
772        let describe = "v1.0.0-alpha-99-0-g24skp4489";
773        assert_eq!(
774            parse_git_describe("v1.0.0-alpha-99", describe).unwrap(),
775            ("v1.0.0-alpha-99".into(), Some(0), Some(commit_hash.into()))
776        );
777
778        let describe = "v1.0.0-alpha-99-024skp4489";
779        assert!(parse_git_describe("v1.0.0-alpha-99", describe).is_err());
780
781        let describe = "v1.0.0-alpha-024skp4489";
782        assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
783
784        let describe = "v1.0.0-alpha-024skp4489";
785        assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
786
787        let describe = "v1.0.0-alpha-g024skp4489";
788        assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
789
790        let describe = "v1.0.0----alpha-g024skp4489";
791        assert!(parse_git_describe("v1.0.0----alpha", describe).is_err());
792    }
793}