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 .env("GIT_OPTIONAL_LOCKS", "0")
617 .args(args)
618 .stdin(Stdio::piped())
619 .stdout(Stdio::piped())
620 .spawn()?;
621 let git_out = git_shell.stdout.ok_or("Failed to exec git stdout")?;
622
623 let grep_shell = Command::new("grep")
624 .args(grep)
625 .stdin(Stdio::from(git_out))
626 .stdout(Stdio::piped())
627 .spawn()?;
628 let grep_out = grep_shell.stdout.ok_or("Failed to exec grep stdout")?;
629
630 let mut awk_shell = Command::new("awk")
631 .args(awk)
632 .stdin(Stdio::from(grep_out))
633 .stdout(Stdio::piped())
634 .spawn()?;
635 let mut awk_out = BufReader::new(
636 awk_shell
637 .stdout
638 .as_mut()
639 .ok_or("Failed to exec awk stdout")?,
640 );
641 let mut line = String::new();
642 awk_out.read_to_string(&mut line)?;
643 Ok(line.lines().map(|x| x.into()).collect())
644 };
645
646 let dirty = git_status_files(&["status", "--porcelain"], &[r"^\sM."], &["{print $2}"])
647 .unwrap_or_default();
648
649 let stage = git_status_files(
650 &["status", "--porcelain", "--untracked-files=all"],
651 &[r#"^[A|M|D|R]"#],
652 &["{print $2}"],
653 )
654 .unwrap_or_default();
655 filter_git_dirty_stage(dirty, stage)
656}
657
658fn command_current_branch() -> Option<String> {
660 find_branch_in(Path::new("."))
661}
662
663fn find_branch_in(path: &Path) -> Option<String> {
664 GitCommandExecutor::new(path).exec(&["symbolic-ref", "--short", "HEAD"])
665}
666
667fn filter_git_dirty_stage(dirty_files: Vec<String>, staged_files: Vec<String>) -> String {
668 let mut concat_file = String::new();
669 for file in dirty_files {
670 concat_file.push_str(" * ");
671 concat_file.push_str(&file);
672 concat_file.push_str(" (dirty)\n");
673 }
674 for file in staged_files {
675 concat_file.push_str(" * ");
676 concat_file.push_str(&file);
677 concat_file.push_str(" (staged)\n");
678 }
679 concat_file
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685 use crate::get_std_env;
686
687 #[test]
688 fn test_git() {
689 let env_map = get_std_env();
690 let map = new_git(Path::new("./"), CiType::Github, &env_map);
691 for (k, v) in map {
692 assert!(!v.desc.is_empty());
693 if !k.eq(TAG)
694 && !k.eq(LAST_TAG)
695 && !k.eq(COMMITS_SINCE_TAG)
696 && !k.eq(BRANCH)
697 && !k.eq(GIT_STATUS_FILE)
698 {
699 assert!(!v.v.is_empty());
700 continue;
701 }
702
703 if let Some(github_ref) = env_map.get("GITHUB_REF") {
705 if github_ref.starts_with("refs/tags/") && k.eq(TAG) {
706 assert!(!v.v.is_empty(), "not empty");
707 } else if github_ref.starts_with("refs/heads/") && k.eq(BRANCH) {
708 assert!(!v.v.is_empty());
709 }
710 }
711 }
712 }
713
714 #[test]
715 fn test_current_branch() {
716 if get_std_env().contains_key("GITHUB_REF") {
717 return;
718 }
719 #[cfg(feature = "git2")]
720 {
721 use crate::git::git2_mod::{git2_current_branch, git_repo};
722 let git2_branch = git_repo(".")
723 .map(|x| git2_current_branch(&x))
724 .unwrap_or(None);
725 let command_branch = command_current_branch();
726 assert!(git2_branch.is_some());
727 assert!(command_branch.is_some());
728 assert_eq!(command_branch, git2_branch);
729 }
730
731 assert_eq!(Some(branch()), command_current_branch());
732 }
733
734 #[test]
735 fn test_parse_git_describe() {
736 let commit_hash = "24skp4489";
737 let describe = "v1.0.0";
738 assert_eq!(
739 parse_git_describe("v1.0.0", describe).unwrap(),
740 (describe.into(), None, None)
741 );
742
743 let describe = "v1.0.0-0-g24skp4489";
744 assert_eq!(
745 parse_git_describe("v1.0.0", describe).unwrap(),
746 ("v1.0.0".into(), Some(0), Some(commit_hash.into()))
747 );
748
749 let describe = "v1.0.0-1-g24skp4489";
750 assert_eq!(
751 parse_git_describe("v1.0.0", describe).unwrap(),
752 ("v1.0.0".into(), Some(1), Some(commit_hash.into()))
753 );
754
755 let describe = "v1.0.0-alpha-0-g24skp4489";
756 assert_eq!(
757 parse_git_describe("v1.0.0-alpha", describe).unwrap(),
758 ("v1.0.0-alpha".into(), Some(0), Some(commit_hash.into()))
759 );
760
761 let describe = "v1.0.0.alpha-0-g24skp4489";
762 assert_eq!(
763 parse_git_describe("v1.0.0.alpha", describe).unwrap(),
764 ("v1.0.0.alpha".into(), Some(0), Some(commit_hash.into()))
765 );
766
767 let describe = "v1.0.0-alpha";
768 assert_eq!(
769 parse_git_describe("v1.0.0-alpha", describe).unwrap(),
770 ("v1.0.0-alpha".into(), None, None)
771 );
772
773 let describe = "v1.0.0-alpha-99-0-g24skp4489";
774 assert_eq!(
775 parse_git_describe("v1.0.0-alpha-99", describe).unwrap(),
776 ("v1.0.0-alpha-99".into(), Some(0), Some(commit_hash.into()))
777 );
778
779 let describe = "v1.0.0-alpha-99-024skp4489";
780 assert!(parse_git_describe("v1.0.0-alpha-99", describe).is_err());
781
782 let describe = "v1.0.0-alpha-024skp4489";
783 assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
784
785 let describe = "v1.0.0-alpha-024skp4489";
786 assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
787
788 let describe = "v1.0.0-alpha-g024skp4489";
789 assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
790
791 let describe = "v1.0.0----alpha-g024skp4489";
792 assert!(parse_git_describe("v1.0.0----alpha", describe).is_err());
793 }
794}