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
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
409pub 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
430pub fn tag() -> String {
435 command_current_tag().unwrap_or_default()
436}
437
438pub 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
456pub 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
524fn command_current_tag() -> Option<String> {
526 GitCommandExecutor::default().exec(&["tag", "-l", "--contains", "HEAD"])
527}
528
529fn 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]; 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 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
600fn command_git_clean() -> bool {
603 GitCommandExecutor::default()
604 .exec(&["status", "--porcelain"])
605 .map(|x| x.is_empty())
606 .unwrap_or(true)
607}
608
609fn 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
657fn 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 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}