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).
57The timezone information from the original commit is preserved.
58
59This constant will be empty if the last commit cannot be determined."#;
60pub const COMMIT_DATE: ShadowConst = "COMMIT_DATE";
61
62const COMMIT_DATE_2822_DOC: &str = r#"
63The time of the Git commit that this project was built from.
64The time is formatted according to [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822#section-3.3) (e.g. HTTP Headers).
65The timezone information from the original commit is preserved.
66
67This constant will be empty if the last commit cannot be determined."#;
68pub const COMMIT_DATE_2822: ShadowConst = "COMMIT_DATE_2822";
69
70const COMMIT_DATE_3339_DOC: &str = r#"
71The time of the Git commit that this project was built from.
72The time is formatted according to [RFC 3339 and ISO 8601](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6).
73The timezone information from the original commit is preserved.
74
75This constant will be empty if the last commit cannot be determined."#;
76pub const COMMIT_DATE_3339: ShadowConst = "COMMIT_DATE_3339";
77
78const COMMIT_TIMESTAMP_DOC: &str = r#"
79The time of the Git commit as a Unix timestamp (seconds since Unix epoch).
80
81This constant will be empty if the last commit cannot be determined."#;
82pub const COMMIT_TIMESTAMP: ShadowConst = "COMMIT_TIMESTAMP";
83
84const COMMIT_AUTHOR_DOC: &str = r#"
85The author of the Git commit that this project was built from.
86
87This constant will be empty if the last commit cannot be determined."#;
88pub const COMMIT_AUTHOR: ShadowConst = "COMMIT_AUTHOR";
89
90const COMMIT_EMAIL_DOC: &str = r#"
91The e-mail address of the author of the Git commit that this project was built from.
92
93This constant will be empty if the last commit cannot be determined."#;
94pub const COMMIT_EMAIL: ShadowConst = "COMMIT_EMAIL";
95
96const GIT_CLEAN_DOC: &str = r#"
97Whether the Git working tree was clean at the time of project build (`true`), or not (`false`).
98
99This constant will be `false` if the last commit cannot be determined."#;
100pub const GIT_CLEAN: ShadowConst = "GIT_CLEAN";
101
102const GIT_STATUS_FILE_DOC: &str = r#"
103The Git working tree status as a list of files with their status, similar to `git status`.
104Each line of the list is preceded with ` * `, followed by the file name.
105Files marked `(dirty)` have unstaged changes.
106Files marked `(staged)` have staged changes.
107
108This constant will be empty if the working tree status cannot be determined."#;
109pub const GIT_STATUS_FILE: ShadowConst = "GIT_STATUS_FILE";
110
111#[derive(Default, Debug)]
112pub struct Git {
113 map: BTreeMap<ShadowConst, ConstVal>,
114 ci_type: CiType,
115}
116
117impl Git {
118 fn update_str(&mut self, c: ShadowConst, v: String) {
119 if let Some(val) = self.map.get_mut(c) {
120 *val = ConstVal {
121 desc: val.desc.clone(),
122 v,
123 t: ConstType::Str,
124 }
125 }
126 }
127
128 fn update_bool(&mut self, c: ShadowConst, v: bool) {
129 if let Some(val) = self.map.get_mut(c) {
130 *val = ConstVal {
131 desc: val.desc.clone(),
132 v: v.to_string(),
133 t: ConstType::Bool,
134 }
135 }
136 }
137
138 fn update_usize(&mut self, c: ShadowConst, v: usize) {
139 if let Some(val) = self.map.get_mut(c) {
140 *val = ConstVal {
141 desc: val.desc.clone(),
142 v: v.to_string(),
143 t: ConstType::Usize,
144 }
145 }
146 }
147
148 fn update_int(&mut self, c: ShadowConst, v: i64) {
149 if let Some(val) = self.map.get_mut(c) {
150 *val = ConstVal {
151 desc: val.desc.clone(),
152 v: v.to_string(),
153 t: ConstType::Int,
154 }
155 }
156 }
157
158 fn init(&mut self, path: &Path, std_env: &BTreeMap<String, String>) -> SdResult<()> {
159 if let Err(err) = self.init_git() {
161 println!("{err}");
162 }
163
164 self.init_git2(path)?;
166
167 if let Some(x) = find_branch_in(path) {
169 self.update_str(BRANCH, x)
170 };
171
172 if let Some(x) = command_current_tag() {
174 self.update_str(TAG, x)
175 }
176
177 let describe = command_git_describe();
179 if let Some(x) = describe.0 {
180 self.update_str(LAST_TAG, x)
181 }
182
183 if let Some(x) = describe.1 {
184 self.update_usize(COMMITS_SINCE_TAG, x)
185 }
186
187 self.ci_branch_tag(std_env);
189 Ok(())
190 }
191
192 fn init_git(&mut self) -> SdResult<()> {
193 let x = command_git_clean();
195 self.update_bool(GIT_CLEAN, x);
196
197 let x = command_git_status_file();
198 self.update_str(GIT_STATUS_FILE, x);
199
200 let git_info = command_git_head();
201
202 self.update_str(COMMIT_EMAIL, git_info.email);
203 self.update_str(COMMIT_AUTHOR, git_info.author);
204 self.update_str(SHORT_COMMIT, git_info.short_commit);
205 self.update_str(COMMIT_HASH, git_info.commit);
206
207 if !git_info.date_iso.is_empty() {
209 if let Ok(date_time) = DateTime::from_iso8601_string(&git_info.date_iso) {
210 self.update_str(COMMIT_DATE, date_time.human_format());
211 self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
212 self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
213 self.update_int(COMMIT_TIMESTAMP, date_time.timestamp());
214 } else if let Ok(time_stamp) = git_info.date.parse::<i64>() {
215 if let Ok(date_time) = DateTime::timestamp_2_utc(time_stamp) {
216 self.update_str(COMMIT_DATE, date_time.human_format());
217 self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
218 self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
219 self.update_int(COMMIT_TIMESTAMP, date_time.timestamp());
220 }
221 }
222 } else if let Ok(time_stamp) = git_info.date.parse::<i64>() {
223 if let Ok(date_time) = DateTime::timestamp_2_utc(time_stamp) {
224 self.update_str(COMMIT_DATE, date_time.human_format());
225 self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
226 self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
227 self.update_int(COMMIT_TIMESTAMP, date_time.timestamp());
228 }
229 }
230
231 Ok(())
232 }
233
234 #[allow(unused_variables)]
235 fn init_git2(&mut self, path: &Path) -> SdResult<()> {
236 #[cfg(feature = "git2")]
237 {
238 use crate::date_time::DateTime;
239 use crate::git::git2_mod::git_repo;
240 use crate::Format;
241
242 let repo = git_repo(path).map_err(ShadowError::new)?;
243 let reference = repo.head().map_err(ShadowError::new)?;
244
245 let branch = reference
247 .shorthand()
248 .map(|x| x.trim().to_string())
249 .or_else(command_current_branch)
250 .unwrap_or_default();
251
252 let tag = command_current_tag().unwrap_or_default();
254 self.update_str(BRANCH, branch);
255 self.update_str(TAG, tag);
256
257 let describe = command_git_describe();
259 if let Some(x) = describe.0 {
260 self.update_str(LAST_TAG, x)
261 }
262
263 if let Some(x) = describe.1 {
264 self.update_usize(COMMITS_SINCE_TAG, x)
265 }
266
267 if let Some(v) = reference.target() {
268 let commit = v.to_string();
269 self.update_str(COMMIT_HASH, commit.clone());
270 let mut short_commit = commit.as_str();
271
272 if commit.len() > 8 {
273 short_commit = short_commit.get(0..8).unwrap();
274 }
275 self.update_str(SHORT_COMMIT, short_commit.to_string());
276 }
277
278 let commit = reference.peel_to_commit().map_err(ShadowError::new)?;
279
280 let author = commit.author();
281 if let Some(v) = author.email() {
282 self.update_str(COMMIT_EMAIL, v.to_string());
283 }
284
285 if let Some(v) = author.name() {
286 self.update_str(COMMIT_AUTHOR, v.to_string());
287 }
288 let status_file = Self::git2_dirty_stage(&repo);
289 if status_file.trim().is_empty() {
290 self.update_bool(GIT_CLEAN, true);
291 } else {
292 self.update_bool(GIT_CLEAN, false);
293 }
294 self.update_str(GIT_STATUS_FILE, status_file);
295
296 let commit_time = commit.time();
297 let time_stamp = commit_time.seconds();
298 let offset_minutes = commit_time.offset_minutes();
299
300 if let Ok(utc_time) = jiff::Timestamp::from_second(time_stamp) {
302 let date_time =
303 if let Ok(offset) = jiff::tz::Offset::from_seconds(offset_minutes * 60) {
304 let tz = jiff::tz::TimeZone::fixed(offset);
305 DateTime::new(utc_time.to_zoned(tz))
306 } else {
307 let tz = jiff::tz::TimeZone::UTC;
309 DateTime::new(utc_time.to_zoned(tz))
310 };
311
312 self.update_str(COMMIT_DATE, date_time.human_format());
313 self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
314 self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
315 }
316 }
317 Ok(())
318 }
319
320 #[cfg(feature = "git2")]
322 pub fn git2_dirty_stage(repo: &git2::Repository) -> String {
323 let mut repo_opts = git2::StatusOptions::new();
324 repo_opts.include_ignored(false);
325 if let Ok(statue) = repo.statuses(Some(&mut repo_opts)) {
326 let mut dirty_files = Vec::new();
327 let mut staged_files = Vec::new();
328
329 for status in statue.iter() {
330 if let Some(path) = status.path() {
331 match status.status() {
332 git2::Status::CURRENT => (),
333 git2::Status::INDEX_NEW
334 | git2::Status::INDEX_MODIFIED
335 | git2::Status::INDEX_DELETED
336 | git2::Status::INDEX_RENAMED
337 | git2::Status::INDEX_TYPECHANGE => staged_files.push(path.to_string()),
338 _ => dirty_files.push(path.to_string()),
339 };
340 }
341 }
342 filter_git_dirty_stage(dirty_files, staged_files)
343 } else {
344 "".into()
345 }
346 }
347
348 #[allow(clippy::manual_strip)]
349 fn ci_branch_tag(&mut self, std_env: &BTreeMap<String, String>) {
350 let mut branch: Option<String> = None;
351 let mut tag: Option<String> = None;
352 match self.ci_type {
353 CiType::Gitlab => {
354 if let Some(v) = std_env.get("CI_COMMIT_TAG") {
355 tag = Some(v.to_string());
356 } else if let Some(v) = std_env.get("CI_COMMIT_REF_NAME") {
357 branch = Some(v.to_string());
358 }
359 }
360 CiType::Github => {
361 if let Some(v) = std_env.get("GITHUB_REF") {
362 let ref_branch_prefix: &str = "refs/heads/";
363 let ref_tag_prefix: &str = "refs/tags/";
364
365 if v.starts_with(ref_branch_prefix) {
366 branch = Some(
367 v.get(ref_branch_prefix.len()..)
368 .unwrap_or_default()
369 .to_string(),
370 )
371 } else if v.starts_with(ref_tag_prefix) {
372 tag = Some(
373 v.get(ref_tag_prefix.len()..)
374 .unwrap_or_default()
375 .to_string(),
376 )
377 }
378 }
379 }
380 _ => {}
381 }
382 if let Some(x) = branch {
383 self.update_str(BRANCH, x);
384 }
385
386 if let Some(x) = tag {
387 self.update_str(TAG, x.clone());
388 self.update_str(LAST_TAG, x);
389 }
390 }
391}
392
393pub(crate) fn new_git(
394 path: &Path,
395 ci: CiType,
396 std_env: &BTreeMap<String, String>,
397) -> BTreeMap<ShadowConst, ConstVal> {
398 let mut git = Git {
399 map: Default::default(),
400 ci_type: ci,
401 };
402 git.map.insert(BRANCH, ConstVal::new(BRANCH_DOC));
403
404 git.map.insert(TAG, ConstVal::new(TAG_DOC));
405
406 git.map.insert(LAST_TAG, ConstVal::new(LAST_TAG_DOC));
407
408 git.map.insert(
409 COMMITS_SINCE_TAG,
410 ConstVal::new_usize(COMMITS_SINCE_TAG_DOC),
411 );
412
413 git.map.insert(COMMIT_HASH, ConstVal::new(COMMIT_HASH_DOC));
414
415 git.map
416 .insert(SHORT_COMMIT, ConstVal::new(SHORT_COMMIT_DOC));
417
418 git.map
419 .insert(COMMIT_AUTHOR, ConstVal::new(COMMIT_AUTHOR_DOC));
420 git.map
421 .insert(COMMIT_EMAIL, ConstVal::new(COMMIT_EMAIL_DOC));
422 git.map.insert(COMMIT_DATE, ConstVal::new(COMMIT_DATE_DOC));
423
424 git.map
425 .insert(COMMIT_DATE_2822, ConstVal::new(COMMIT_DATE_2822_DOC));
426
427 git.map
428 .insert(COMMIT_DATE_3339, ConstVal::new(COMMIT_DATE_3339_DOC));
429
430 git.map
431 .insert(COMMIT_TIMESTAMP, ConstVal::new(COMMIT_TIMESTAMP_DOC));
432
433 git.map.insert(GIT_CLEAN, ConstVal::new_bool(GIT_CLEAN_DOC));
434
435 git.map
436 .insert(GIT_STATUS_FILE, ConstVal::new(GIT_STATUS_FILE_DOC));
437
438 if let Err(e) = git.init(path, std_env) {
439 println!("{e}");
440 }
441
442 git.map
443}
444
445#[cfg(feature = "git2")]
446pub mod git2_mod {
447 use git2::Error as git2Error;
448 use git2::Repository;
449 use std::path::Path;
450
451 pub fn git_repo<P: AsRef<Path>>(path: P) -> Result<Repository, git2Error> {
452 Repository::discover(path)
453 }
454
455 pub fn git2_current_branch(repo: &Repository) -> Option<String> {
456 repo.head()
457 .map(|x| x.shorthand().map(|x| x.to_string()))
458 .unwrap_or(None)
459 }
460}
461
462pub fn branch() -> String {
469 #[cfg(feature = "git2")]
470 {
471 use crate::git::git2_mod::{git2_current_branch, git_repo};
472 git_repo(".")
473 .map(|x| git2_current_branch(&x))
474 .unwrap_or_else(|_| command_current_branch())
475 .unwrap_or_default()
476 }
477 #[cfg(not(feature = "git2"))]
478 {
479 command_current_branch().unwrap_or_default()
480 }
481}
482
483pub fn tag() -> String {
488 command_current_tag().unwrap_or_default()
489}
490
491pub fn git_clean() -> bool {
495 #[cfg(feature = "git2")]
496 {
497 use crate::git::git2_mod::git_repo;
498 git_repo(".")
499 .map(|x| Git::git2_dirty_stage(&x))
500 .map(|x| x.trim().is_empty())
501 .unwrap_or(true)
502 }
503 #[cfg(not(feature = "git2"))]
504 {
505 command_git_clean()
506 }
507}
508
509pub fn git_status_file() -> String {
515 #[cfg(feature = "git2")]
516 {
517 use crate::git::git2_mod::git_repo;
518 git_repo(".")
519 .map(|x| Git::git2_dirty_stage(&x))
520 .unwrap_or_default()
521 }
522 #[cfg(not(feature = "git2"))]
523 {
524 command_git_status_file()
525 }
526}
527
528struct GitHeadInfo {
529 commit: String,
530 short_commit: String,
531 email: String,
532 author: String,
533 date: String,
534 date_iso: String,
535}
536
537struct GitCommandExecutor<'a> {
538 path: &'a Path,
539}
540
541impl Default for GitCommandExecutor<'_> {
542 fn default() -> Self {
543 Self::new(Path::new("."))
544 }
545}
546
547impl<'a> GitCommandExecutor<'a> {
548 fn new(path: &'a Path) -> Self {
549 GitCommandExecutor { path }
550 }
551
552 fn exec(&self, args: &[&str]) -> Option<String> {
553 Command::new("git")
554 .env("GIT_OPTIONAL_LOCKS", "0")
555 .current_dir(self.path)
556 .args(args)
557 .output()
558 .map(|x| {
559 String::from_utf8(x.stdout)
560 .map(|x| x.trim().to_string())
561 .ok()
562 })
563 .unwrap_or(None)
564 }
565}
566
567fn command_git_head() -> GitHeadInfo {
568 let cli = |args: &[&str]| GitCommandExecutor::default().exec(args).unwrap_or_default();
569 GitHeadInfo {
570 commit: cli(&["rev-parse", "HEAD"]),
571 short_commit: cli(&["rev-parse", "--short", "HEAD"]),
572 author: cli(&["log", "-1", "--pretty=format:%an"]),
573 email: cli(&["log", "-1", "--pretty=format:%ae"]),
574 date: cli(&["show", "--pretty=format:%ct", "--date=raw", "-s"]),
575 date_iso: cli(&["log", "-1", "--pretty=format:%cI"]),
576 }
577}
578
579fn command_current_tag() -> Option<String> {
581 GitCommandExecutor::default().exec(&["tag", "-l", "--contains", "HEAD"])
582}
583
584fn command_git_describe() -> (Option<String>, Option<usize>, Option<String>) {
587 let last_tag =
588 GitCommandExecutor::default().exec(&["describe", "--tags", "--abbrev=0", "HEAD"]);
589 if last_tag.is_none() {
590 return (None, None, None);
591 }
592
593 let tag = last_tag.unwrap();
594
595 let describe = GitCommandExecutor::default().exec(&["describe", "--tags", "HEAD"]);
596 if let Some(desc) = describe {
597 match parse_git_describe(&tag, &desc) {
598 Ok((tag, commits, hash)) => {
599 return (Some(tag), commits, hash);
600 }
601 Err(_) => {
602 return (Some(tag), None, None);
603 }
604 }
605 }
606 (Some(tag), None, None)
607}
608
609fn parse_git_describe(
610 last_tag: &str,
611 describe: &str,
612) -> SdResult<(String, Option<usize>, Option<String>)> {
613 if !describe.starts_with(last_tag) {
614 return Err(ShadowError::String("git describe result error".to_string()));
615 }
616
617 if last_tag == describe {
618 return Ok((describe.to_string(), None, None));
619 }
620
621 let parts: Vec<&str> = describe.rsplit('-').collect();
622
623 if parts.is_empty() || parts.len() == 2 {
624 return Err(ShadowError::String(
625 "git describe result error,expect:<tag>-<num_commits>-g<hash>".to_string(),
626 ));
627 }
628
629 if parts.len() > 2 {
630 let short_hash = parts[0]; if !short_hash.starts_with('g') {
633 return Err(ShadowError::String(
634 "git describe result error,expect commit hash end with:-g<hash>".to_string(),
635 ));
636 }
637 let short_hash = short_hash.trim_start_matches('g');
638
639 let num_commits_str = parts[1];
641 let num_commits = num_commits_str
642 .parse::<usize>()
643 .map_err(|e| ShadowError::String(e.to_string()))?;
644 let last_tag = parts[2..]
645 .iter()
646 .rev()
647 .copied()
648 .collect::<Vec<_>>()
649 .join("-");
650 return Ok((last_tag, Some(num_commits), Some(short_hash.to_string())));
651 }
652 Ok((describe.to_string(), None, None))
653}
654
655fn command_git_clean() -> bool {
658 GitCommandExecutor::default()
659 .exec(&["status", "--porcelain"])
660 .map(|x| x.is_empty())
661 .unwrap_or(true)
662}
663
664fn command_git_status_file() -> String {
668 let git_status_files =
669 move |args: &[&str], grep: &[&str], awk: &[&str]| -> SdResult<Vec<String>> {
670 let git_shell = Command::new("git")
671 .env("GIT_OPTIONAL_LOCKS", "0")
672 .args(args)
673 .stdin(Stdio::piped())
674 .stdout(Stdio::piped())
675 .spawn()?;
676 let git_out = git_shell.stdout.ok_or("Failed to exec git stdout")?;
677
678 let grep_shell = Command::new("grep")
679 .args(grep)
680 .stdin(Stdio::from(git_out))
681 .stdout(Stdio::piped())
682 .spawn()?;
683 let grep_out = grep_shell.stdout.ok_or("Failed to exec grep stdout")?;
684
685 let mut awk_shell = Command::new("awk")
686 .args(awk)
687 .stdin(Stdio::from(grep_out))
688 .stdout(Stdio::piped())
689 .spawn()?;
690 let mut awk_out = BufReader::new(
691 awk_shell
692 .stdout
693 .as_mut()
694 .ok_or("Failed to exec awk stdout")?,
695 );
696 let mut line = String::new();
697 awk_out.read_to_string(&mut line)?;
698 Ok(line.lines().map(|x| x.into()).collect())
699 };
700
701 let dirty = git_status_files(&["status", "--porcelain"], &[r"^\sM."], &["{print $2}"])
702 .unwrap_or_default();
703
704 let stage = git_status_files(
705 &["status", "--porcelain", "--untracked-files=all"],
706 &[r#"^[A|M|D|R]"#],
707 &["{print $2}"],
708 )
709 .unwrap_or_default();
710 filter_git_dirty_stage(dirty, stage)
711}
712
713fn command_current_branch() -> Option<String> {
715 find_branch_in(Path::new("."))
716}
717
718fn find_branch_in(path: &Path) -> Option<String> {
719 GitCommandExecutor::new(path).exec(&["symbolic-ref", "--short", "HEAD"])
720}
721
722fn filter_git_dirty_stage(dirty_files: Vec<String>, staged_files: Vec<String>) -> String {
723 let mut concat_file = String::new();
724 for file in dirty_files {
725 concat_file.push_str(" * ");
726 concat_file.push_str(&file);
727 concat_file.push_str(" (dirty)\n");
728 }
729 for file in staged_files {
730 concat_file.push_str(" * ");
731 concat_file.push_str(&file);
732 concat_file.push_str(" (staged)\n");
733 }
734 concat_file
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740 use crate::get_std_env;
741
742 #[test]
743 fn test_git() {
744 let env_map = get_std_env();
745 let map = new_git(Path::new("./"), CiType::Github, &env_map);
746 for (k, v) in map {
747 assert!(!v.desc.is_empty());
748 if !k.eq(TAG)
749 && !k.eq(LAST_TAG)
750 && !k.eq(COMMITS_SINCE_TAG)
751 && !k.eq(BRANCH)
752 && !k.eq(GIT_STATUS_FILE)
753 {
754 assert!(!v.v.is_empty());
755 continue;
756 }
757
758 if let Some(github_ref) = env_map.get("GITHUB_REF") {
760 if github_ref.starts_with("refs/tags/") && k.eq(TAG) {
761 assert!(!v.v.is_empty(), "not empty");
762 } else if github_ref.starts_with("refs/heads/") && k.eq(BRANCH) {
763 assert!(!v.v.is_empty());
764 }
765 }
766 }
767 }
768
769 #[test]
770 fn test_current_branch() {
771 if get_std_env().contains_key("GITHUB_REF") {
772 return;
773 }
774 #[cfg(feature = "git2")]
775 {
776 use crate::git::git2_mod::{git2_current_branch, git_repo};
777 let git2_branch = git_repo(".")
778 .map(|x| git2_current_branch(&x))
779 .unwrap_or(None);
780 let command_branch = command_current_branch();
781 assert!(git2_branch.is_some());
782 assert!(command_branch.is_some());
783 assert_eq!(command_branch, git2_branch);
784 }
785
786 assert_eq!(Some(branch()), command_current_branch());
787 }
788
789 #[test]
790 fn test_parse_git_describe() {
791 let commit_hash = "24skp4489";
792 let describe = "v1.0.0";
793 assert_eq!(
794 parse_git_describe("v1.0.0", describe).unwrap(),
795 (describe.into(), None, None)
796 );
797
798 let describe = "v1.0.0-0-g24skp4489";
799 assert_eq!(
800 parse_git_describe("v1.0.0", describe).unwrap(),
801 ("v1.0.0".into(), Some(0), Some(commit_hash.into()))
802 );
803
804 let describe = "v1.0.0-1-g24skp4489";
805 assert_eq!(
806 parse_git_describe("v1.0.0", describe).unwrap(),
807 ("v1.0.0".into(), Some(1), Some(commit_hash.into()))
808 );
809
810 let describe = "v1.0.0-alpha-0-g24skp4489";
811 assert_eq!(
812 parse_git_describe("v1.0.0-alpha", describe).unwrap(),
813 ("v1.0.0-alpha".into(), Some(0), Some(commit_hash.into()))
814 );
815
816 let describe = "v1.0.0.alpha-0-g24skp4489";
817 assert_eq!(
818 parse_git_describe("v1.0.0.alpha", describe).unwrap(),
819 ("v1.0.0.alpha".into(), Some(0), Some(commit_hash.into()))
820 );
821
822 let describe = "v1.0.0-alpha";
823 assert_eq!(
824 parse_git_describe("v1.0.0-alpha", describe).unwrap(),
825 ("v1.0.0-alpha".into(), None, None)
826 );
827
828 let describe = "v1.0.0-alpha-99-0-g24skp4489";
829 assert_eq!(
830 parse_git_describe("v1.0.0-alpha-99", describe).unwrap(),
831 ("v1.0.0-alpha-99".into(), Some(0), Some(commit_hash.into()))
832 );
833
834 let describe = "v1.0.0-alpha-99-024skp4489";
835 assert!(parse_git_describe("v1.0.0-alpha-99", describe).is_err());
836
837 let describe = "v1.0.0-alpha-024skp4489";
838 assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
839
840 let describe = "v1.0.0-alpha-024skp4489";
841 assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
842
843 let describe = "v1.0.0-alpha-g024skp4489";
844 assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
845
846 let describe = "v1.0.0----alpha-g024skp4489";
847 assert!(parse_git_describe("v1.0.0----alpha", describe).is_err());
848 }
849
850 #[test]
851 fn test_commit_date_timezone_preservation() {
852 use crate::DateTime;
853
854 let iso_date = "2021-08-04T12:34:03+08:00";
856 let date_time = DateTime::from_iso8601_string(iso_date).unwrap();
857 assert_eq!(date_time.human_format(), "2021-08-04 12:34:03 +08:00");
858 assert!(date_time.to_rfc3339().contains("+08:00"));
859
860 let iso_date_utc = "2021-08-04T12:34:03Z";
862 let date_time_utc = DateTime::from_iso8601_string(iso_date_utc).unwrap();
863 assert_eq!(date_time_utc.human_format(), "2021-08-04 12:34:03 +00:00");
864
865 let iso_date_neg = "2021-08-04T12:34:03-05:00";
867 let date_time_neg = DateTime::from_iso8601_string(iso_date_neg).unwrap();
868 assert_eq!(date_time_neg.human_format(), "2021-08-04 12:34:03 -05:00");
869 }
870}