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 if let Err(err) = self.init_git() {
142 println!("{err}");
143 }
144
145 self.init_git2(path)?;
147
148 if let Some(x) = find_branch_in(path) {
150 self.update_str(BRANCH, x)
151 };
152
153 if let Some(x) = command_current_tag() {
155 self.update_str(TAG, x)
156 }
157
158 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 self.ci_branch_tag(std_env);
170 Ok(())
171 }
172
173 fn init_git(&mut self) -> SdResult<()> {
174 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 let branch = reference
211 .shorthand()
212 .map(|x| x.trim().to_string())
213 .or_else(command_current_branch)
214 .unwrap_or_default();
215
216 let tag = command_current_tag().unwrap_or_default();
218 self.update_str(BRANCH, branch);
219 self.update_str(TAG, tag);
220
221 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 #[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
411pub 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
432pub fn tag() -> String {
437 command_current_tag().unwrap_or_default()
438}
439
440pub 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
458pub 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
526fn command_current_tag() -> Option<String> {
528 GitCommandExecutor::default().exec(&["tag", "-l", "--contains", "HEAD"])
529}
530
531fn 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]; 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 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
602fn command_git_clean() -> bool {
605 GitCommandExecutor::default()
606 .exec(&["status", "--porcelain"])
607 .map(|x| x.is_empty())
608 .unwrap_or(true)
609}
610
611fn 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
660fn 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 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}