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.insert(
361        COMMITS_SINCE_TAG,
362        ConstVal::new_usize(COMMITS_SINCE_TAG_DOC),
363    );
364
365    git.map.insert(COMMIT_HASH, ConstVal::new(COMMIT_HASH_DOC));
366
367    git.map
368        .insert(SHORT_COMMIT, ConstVal::new(SHORT_COMMIT_DOC));
369
370    git.map
371        .insert(COMMIT_AUTHOR, ConstVal::new(COMMIT_AUTHOR_DOC));
372    git.map
373        .insert(COMMIT_EMAIL, ConstVal::new(COMMIT_EMAIL_DOC));
374    git.map.insert(COMMIT_DATE, ConstVal::new(COMMIT_DATE_DOC));
375
376    git.map
377        .insert(COMMIT_DATE_2822, ConstVal::new(COMMIT_DATE_2822_DOC));
378
379    git.map
380        .insert(COMMIT_DATE_3339, ConstVal::new(COMMIT_DATE_3339_DOC));
381
382    git.map.insert(GIT_CLEAN, ConstVal::new_bool(GIT_CLEAN_DOC));
383
384    git.map
385        .insert(GIT_STATUS_FILE, ConstVal::new(GIT_STATUS_FILE_DOC));
386
387    if let Err(e) = git.init(path, std_env) {
388        println!("{e}");
389    }
390
391    git.map
392}
393
394#[cfg(feature = "git2")]
395pub mod git2_mod {
396    use git2::Error as git2Error;
397    use git2::Repository;
398    use std::path::Path;
399
400    pub fn git_repo<P: AsRef<Path>>(path: P) -> Result<Repository, git2Error> {
401        Repository::discover(path)
402    }
403
404    pub fn git2_current_branch(repo: &Repository) -> Option<String> {
405        repo.head()
406            .map(|x| x.shorthand().map(|x| x.to_string()))
407            .unwrap_or(None)
408    }
409}
410
411/// get current repository git branch.
412///
413/// When current repository exists git folder.
414///
415/// It's use default feature.This function try use [git2] crates get current branch.
416/// If not use git2 feature,then try use [Command] to get.
417pub fn branch() -> String {
418    #[cfg(feature = "git2")]
419    {
420        use crate::git::git2_mod::{git2_current_branch, git_repo};
421        git_repo(".")
422            .map(|x| git2_current_branch(&x))
423            .unwrap_or_else(|_| command_current_branch())
424            .unwrap_or_default()
425    }
426    #[cfg(not(feature = "git2"))]
427    {
428        command_current_branch().unwrap_or_default()
429    }
430}
431
432/// get current repository git tag.
433///
434/// When current repository exists git folder.
435/// I's use [Command] to get.
436pub fn tag() -> String {
437    command_current_tag().unwrap_or_default()
438}
439
440/// Check current git Repository status without nothing(dirty or stage)
441///
442/// if nothing,It means clean:true. On the contrary, it is 'dirty':false
443pub fn git_clean() -> bool {
444    #[cfg(feature = "git2")]
445    {
446        use crate::git::git2_mod::git_repo;
447        git_repo(".")
448            .map(|x| Git::git2_dirty_stage(&x))
449            .map(|x| x.trim().is_empty())
450            .unwrap_or(true)
451    }
452    #[cfg(not(feature = "git2"))]
453    {
454        command_git_clean()
455    }
456}
457
458/// List current git Repository statue(dirty or stage) contain file changed
459///
460/// Refer to the 'cargo fix' result output when git statue(dirty or stage) changed.
461///
462/// Example output:`   * examples/builtin_fn.rs (dirty)`
463pub fn git_status_file() -> String {
464    #[cfg(feature = "git2")]
465    {
466        use crate::git::git2_mod::git_repo;
467        git_repo(".")
468            .map(|x| Git::git2_dirty_stage(&x))
469            .unwrap_or_default()
470    }
471    #[cfg(not(feature = "git2"))]
472    {
473        command_git_status_file()
474    }
475}
476
477struct GitHeadInfo {
478    commit: String,
479    short_commit: String,
480    email: String,
481    author: String,
482    date: String,
483}
484
485struct GitCommandExecutor<'a> {
486    path: &'a Path,
487}
488
489impl Default for GitCommandExecutor<'_> {
490    fn default() -> Self {
491        Self::new(Path::new("."))
492    }
493}
494
495impl<'a> GitCommandExecutor<'a> {
496    fn new(path: &'a Path) -> Self {
497        GitCommandExecutor { path }
498    }
499
500    fn exec(&self, args: &[&str]) -> Option<String> {
501        Command::new("git")
502            .env("GIT_OPTIONAL_LOCKS", "0")
503            .current_dir(self.path)
504            .args(args)
505            .output()
506            .map(|x| {
507                String::from_utf8(x.stdout)
508                    .map(|x| x.trim().to_string())
509                    .ok()
510            })
511            .unwrap_or(None)
512    }
513}
514
515fn command_git_head() -> GitHeadInfo {
516    let cli = |args: &[&str]| GitCommandExecutor::default().exec(args).unwrap_or_default();
517    GitHeadInfo {
518        commit: cli(&["rev-parse", "HEAD"]),
519        short_commit: cli(&["rev-parse", "--short", "HEAD"]),
520        author: cli(&["log", "-1", "--pretty=format:%an"]),
521        email: cli(&["log", "-1", "--pretty=format:%ae"]),
522        date: cli(&["show", "--pretty=format:%ct", "--date=raw", "-s"]),
523    }
524}
525
526/// Command exec git current tag
527fn command_current_tag() -> Option<String> {
528    GitCommandExecutor::default().exec(&["tag", "-l", "--contains", "HEAD"])
529}
530
531/// git describe --tags HEAD
532/// Command exec git describe
533fn command_git_describe() -> (Option<String>, Option<usize>, Option<String>) {
534    let last_tag =
535        GitCommandExecutor::default().exec(&["describe", "--tags", "--abbrev=0", "HEAD"]);
536    if last_tag.is_none() {
537        return (None, None, None);
538    }
539
540    let tag = last_tag.unwrap();
541
542    let describe = GitCommandExecutor::default().exec(&["describe", "--tags", "HEAD"]);
543    if let Some(desc) = describe {
544        match parse_git_describe(&tag, &desc) {
545            Ok((tag, commits, hash)) => {
546                return (Some(tag), commits, hash);
547            }
548            Err(_) => {
549                return (Some(tag), None, None);
550            }
551        }
552    }
553    (Some(tag), None, None)
554}
555
556fn parse_git_describe(
557    last_tag: &str,
558    describe: &str,
559) -> SdResult<(String, Option<usize>, Option<String>)> {
560    if !describe.starts_with(last_tag) {
561        return Err(ShadowError::String("git describe result error".to_string()));
562    }
563
564    if last_tag == describe {
565        return Ok((describe.to_string(), None, None));
566    }
567
568    let parts: Vec<&str> = describe.rsplit('-').collect();
569
570    if parts.is_empty() || parts.len() == 2 {
571        return Err(ShadowError::String(
572            "git describe result error,expect:<tag>-<num_commits>-g<hash>".to_string(),
573        ));
574    }
575
576    if parts.len() > 2 {
577        let short_hash = parts[0]; // last part
578
579        if !short_hash.starts_with('g') {
580            return Err(ShadowError::String(
581                "git describe result error,expect commit hash end with:-g<hash>".to_string(),
582            ));
583        }
584        let short_hash = short_hash.trim_start_matches('g');
585
586        // Full example:v1.0.0-alpha0-5-ga1b2c3d
587        let num_commits_str = parts[1];
588        let num_commits = num_commits_str
589            .parse::<usize>()
590            .map_err(|e| ShadowError::String(e.to_string()))?;
591        let last_tag = parts[2..]
592            .iter()
593            .rev()
594            .copied()
595            .collect::<Vec<_>>()
596            .join("-");
597        return Ok((last_tag, Some(num_commits), Some(short_hash.to_string())));
598    }
599    Ok((describe.to_string(), None, None))
600}
601
602/// git clean:git status --porcelain
603/// check repository git status is clean
604fn command_git_clean() -> bool {
605    GitCommandExecutor::default()
606        .exec(&["status", "--porcelain"])
607        .map(|x| x.is_empty())
608        .unwrap_or(true)
609}
610
611/// check git repository 'dirty or stage' status files.
612/// git dirty:git status  --porcelain | grep '^\sM.' |awk '{print $2}'
613/// git stage:git status --porcelain --untracked-files=all | grep '^[A|M|D|R]'|awk '{print $2}'
614fn command_git_status_file() -> String {
615    let git_status_files =
616        move |args: &[&str], grep: &[&str], awk: &[&str]| -> SdResult<Vec<String>> {
617            let git_shell = Command::new("git")
618                .env("GIT_OPTIONAL_LOCKS", "0")
619                .args(args)
620                .stdin(Stdio::piped())
621                .stdout(Stdio::piped())
622                .spawn()?;
623            let git_out = git_shell.stdout.ok_or("Failed to exec git stdout")?;
624
625            let grep_shell = Command::new("grep")
626                .args(grep)
627                .stdin(Stdio::from(git_out))
628                .stdout(Stdio::piped())
629                .spawn()?;
630            let grep_out = grep_shell.stdout.ok_or("Failed to exec grep stdout")?;
631
632            let mut awk_shell = Command::new("awk")
633                .args(awk)
634                .stdin(Stdio::from(grep_out))
635                .stdout(Stdio::piped())
636                .spawn()?;
637            let mut awk_out = BufReader::new(
638                awk_shell
639                    .stdout
640                    .as_mut()
641                    .ok_or("Failed to exec awk stdout")?,
642            );
643            let mut line = String::new();
644            awk_out.read_to_string(&mut line)?;
645            Ok(line.lines().map(|x| x.into()).collect())
646        };
647
648    let dirty = git_status_files(&["status", "--porcelain"], &[r"^\sM."], &["{print $2}"])
649        .unwrap_or_default();
650
651    let stage = git_status_files(
652        &["status", "--porcelain", "--untracked-files=all"],
653        &[r#"^[A|M|D|R]"#],
654        &["{print $2}"],
655    )
656    .unwrap_or_default();
657    filter_git_dirty_stage(dirty, stage)
658}
659
660/// Command exec git current branch
661fn command_current_branch() -> Option<String> {
662    find_branch_in(Path::new("."))
663}
664
665fn find_branch_in(path: &Path) -> Option<String> {
666    GitCommandExecutor::new(path).exec(&["symbolic-ref", "--short", "HEAD"])
667}
668
669fn filter_git_dirty_stage(dirty_files: Vec<String>, staged_files: Vec<String>) -> String {
670    let mut concat_file = String::new();
671    for file in dirty_files {
672        concat_file.push_str("  * ");
673        concat_file.push_str(&file);
674        concat_file.push_str(" (dirty)\n");
675    }
676    for file in staged_files {
677        concat_file.push_str("  * ");
678        concat_file.push_str(&file);
679        concat_file.push_str(" (staged)\n");
680    }
681    concat_file
682}
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687    use crate::get_std_env;
688
689    #[test]
690    fn test_git() {
691        let env_map = get_std_env();
692        let map = new_git(Path::new("./"), CiType::Github, &env_map);
693        for (k, v) in map {
694            assert!(!v.desc.is_empty());
695            if !k.eq(TAG)
696                && !k.eq(LAST_TAG)
697                && !k.eq(COMMITS_SINCE_TAG)
698                && !k.eq(BRANCH)
699                && !k.eq(GIT_STATUS_FILE)
700            {
701                assert!(!v.v.is_empty());
702                continue;
703            }
704
705            //assert github tag always exist value
706            if let Some(github_ref) = env_map.get("GITHUB_REF") {
707                if github_ref.starts_with("refs/tags/") && k.eq(TAG) {
708                    assert!(!v.v.is_empty(), "not empty");
709                } else if github_ref.starts_with("refs/heads/") && k.eq(BRANCH) {
710                    assert!(!v.v.is_empty());
711                }
712            }
713        }
714    }
715
716    #[test]
717    fn test_current_branch() {
718        if get_std_env().contains_key("GITHUB_REF") {
719            return;
720        }
721        #[cfg(feature = "git2")]
722        {
723            use crate::git::git2_mod::{git2_current_branch, git_repo};
724            let git2_branch = git_repo(".")
725                .map(|x| git2_current_branch(&x))
726                .unwrap_or(None);
727            let command_branch = command_current_branch();
728            assert!(git2_branch.is_some());
729            assert!(command_branch.is_some());
730            assert_eq!(command_branch, git2_branch);
731        }
732
733        assert_eq!(Some(branch()), command_current_branch());
734    }
735
736    #[test]
737    fn test_parse_git_describe() {
738        let commit_hash = "24skp4489";
739        let describe = "v1.0.0";
740        assert_eq!(
741            parse_git_describe("v1.0.0", describe).unwrap(),
742            (describe.into(), None, None)
743        );
744
745        let describe = "v1.0.0-0-g24skp4489";
746        assert_eq!(
747            parse_git_describe("v1.0.0", describe).unwrap(),
748            ("v1.0.0".into(), Some(0), Some(commit_hash.into()))
749        );
750
751        let describe = "v1.0.0-1-g24skp4489";
752        assert_eq!(
753            parse_git_describe("v1.0.0", describe).unwrap(),
754            ("v1.0.0".into(), Some(1), Some(commit_hash.into()))
755        );
756
757        let describe = "v1.0.0-alpha-0-g24skp4489";
758        assert_eq!(
759            parse_git_describe("v1.0.0-alpha", describe).unwrap(),
760            ("v1.0.0-alpha".into(), Some(0), Some(commit_hash.into()))
761        );
762
763        let describe = "v1.0.0.alpha-0-g24skp4489";
764        assert_eq!(
765            parse_git_describe("v1.0.0.alpha", describe).unwrap(),
766            ("v1.0.0.alpha".into(), Some(0), Some(commit_hash.into()))
767        );
768
769        let describe = "v1.0.0-alpha";
770        assert_eq!(
771            parse_git_describe("v1.0.0-alpha", describe).unwrap(),
772            ("v1.0.0-alpha".into(), None, None)
773        );
774
775        let describe = "v1.0.0-alpha-99-0-g24skp4489";
776        assert_eq!(
777            parse_git_describe("v1.0.0-alpha-99", describe).unwrap(),
778            ("v1.0.0-alpha-99".into(), Some(0), Some(commit_hash.into()))
779        );
780
781        let describe = "v1.0.0-alpha-99-024skp4489";
782        assert!(parse_git_describe("v1.0.0-alpha-99", 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-024skp4489";
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        let describe = "v1.0.0----alpha-g024skp4489";
794        assert!(parse_git_describe("v1.0.0----alpha", describe).is_err());
795    }
796}