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) = time::OffsetDateTime::from_unix_timestamp(time_stamp) {
302 if let Ok(offset) = time::UtcOffset::from_whole_seconds(offset_minutes * 60) {
303 let local_time = utc_time.to_offset(offset);
304 let date_time = DateTime::Local(local_time);
305
306 self.update_str(COMMIT_DATE, date_time.human_format());
307 self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
308 self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
309 } else {
310 let date_time = DateTime::Utc(utc_time);
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 }
318 Ok(())
319 }
320
321 #[cfg(feature = "git2")]
323 pub fn git2_dirty_stage(repo: &git2::Repository) -> String {
324 let mut repo_opts = git2::StatusOptions::new();
325 repo_opts.include_ignored(false);
326 if let Ok(statue) = repo.statuses(Some(&mut repo_opts)) {
327 let mut dirty_files = Vec::new();
328 let mut staged_files = Vec::new();
329
330 for status in statue.iter() {
331 if let Some(path) = status.path() {
332 match status.status() {
333 git2::Status::CURRENT => (),
334 git2::Status::INDEX_NEW
335 | git2::Status::INDEX_MODIFIED
336 | git2::Status::INDEX_DELETED
337 | git2::Status::INDEX_RENAMED
338 | git2::Status::INDEX_TYPECHANGE => staged_files.push(path.to_string()),
339 _ => dirty_files.push(path.to_string()),
340 };
341 }
342 }
343 filter_git_dirty_stage(dirty_files, staged_files)
344 } else {
345 "".into()
346 }
347 }
348
349 #[allow(clippy::manual_strip)]
350 fn ci_branch_tag(&mut self, std_env: &BTreeMap<String, String>) {
351 let mut branch: Option<String> = None;
352 let mut tag: Option<String> = None;
353 match self.ci_type {
354 CiType::Gitlab => {
355 if let Some(v) = std_env.get("CI_COMMIT_TAG") {
356 tag = Some(v.to_string());
357 } else if let Some(v) = std_env.get("CI_COMMIT_REF_NAME") {
358 branch = Some(v.to_string());
359 }
360 }
361 CiType::Github => {
362 if let Some(v) = std_env.get("GITHUB_REF") {
363 let ref_branch_prefix: &str = "refs/heads/";
364 let ref_tag_prefix: &str = "refs/tags/";
365
366 if v.starts_with(ref_branch_prefix) {
367 branch = Some(
368 v.get(ref_branch_prefix.len()..)
369 .unwrap_or_default()
370 .to_string(),
371 )
372 } else if v.starts_with(ref_tag_prefix) {
373 tag = Some(
374 v.get(ref_tag_prefix.len()..)
375 .unwrap_or_default()
376 .to_string(),
377 )
378 }
379 }
380 }
381 _ => {}
382 }
383 if let Some(x) = branch {
384 self.update_str(BRANCH, x);
385 }
386
387 if let Some(x) = tag {
388 self.update_str(TAG, x.clone());
389 self.update_str(LAST_TAG, x);
390 }
391 }
392}
393
394pub(crate) fn new_git(
395 path: &Path,
396 ci: CiType,
397 std_env: &BTreeMap<String, String>,
398) -> BTreeMap<ShadowConst, ConstVal> {
399 let mut git = Git {
400 map: Default::default(),
401 ci_type: ci,
402 };
403 git.map.insert(BRANCH, ConstVal::new(BRANCH_DOC));
404
405 git.map.insert(TAG, ConstVal::new(TAG_DOC));
406
407 git.map.insert(LAST_TAG, ConstVal::new(LAST_TAG_DOC));
408
409 git.map.insert(
410 COMMITS_SINCE_TAG,
411 ConstVal::new_usize(COMMITS_SINCE_TAG_DOC),
412 );
413
414 git.map.insert(COMMIT_HASH, ConstVal::new(COMMIT_HASH_DOC));
415
416 git.map
417 .insert(SHORT_COMMIT, ConstVal::new(SHORT_COMMIT_DOC));
418
419 git.map
420 .insert(COMMIT_AUTHOR, ConstVal::new(COMMIT_AUTHOR_DOC));
421 git.map
422 .insert(COMMIT_EMAIL, ConstVal::new(COMMIT_EMAIL_DOC));
423 git.map.insert(COMMIT_DATE, ConstVal::new(COMMIT_DATE_DOC));
424
425 git.map
426 .insert(COMMIT_DATE_2822, ConstVal::new(COMMIT_DATE_2822_DOC));
427
428 git.map
429 .insert(COMMIT_DATE_3339, ConstVal::new(COMMIT_DATE_3339_DOC));
430
431 git.map
432 .insert(COMMIT_TIMESTAMP, ConstVal::new(COMMIT_TIMESTAMP_DOC));
433
434 git.map.insert(GIT_CLEAN, ConstVal::new_bool(GIT_CLEAN_DOC));
435
436 git.map
437 .insert(GIT_STATUS_FILE, ConstVal::new(GIT_STATUS_FILE_DOC));
438
439 if let Err(e) = git.init(path, std_env) {
440 println!("{e}");
441 }
442
443 git.map
444}
445
446#[cfg(feature = "git2")]
447pub mod git2_mod {
448 use git2::Error as git2Error;
449 use git2::Repository;
450 use std::path::Path;
451
452 pub fn git_repo<P: AsRef<Path>>(path: P) -> Result<Repository, git2Error> {
453 Repository::discover(path)
454 }
455
456 pub fn git2_current_branch(repo: &Repository) -> Option<String> {
457 repo.head()
458 .map(|x| x.shorthand().map(|x| x.to_string()))
459 .unwrap_or(None)
460 }
461}
462
463pub fn branch() -> String {
470 #[cfg(feature = "git2")]
471 {
472 use crate::git::git2_mod::{git2_current_branch, git_repo};
473 git_repo(".")
474 .map(|x| git2_current_branch(&x))
475 .unwrap_or_else(|_| command_current_branch())
476 .unwrap_or_default()
477 }
478 #[cfg(not(feature = "git2"))]
479 {
480 command_current_branch().unwrap_or_default()
481 }
482}
483
484pub fn tag() -> String {
489 command_current_tag().unwrap_or_default()
490}
491
492pub fn git_clean() -> bool {
496 #[cfg(feature = "git2")]
497 {
498 use crate::git::git2_mod::git_repo;
499 git_repo(".")
500 .map(|x| Git::git2_dirty_stage(&x))
501 .map(|x| x.trim().is_empty())
502 .unwrap_or(true)
503 }
504 #[cfg(not(feature = "git2"))]
505 {
506 command_git_clean()
507 }
508}
509
510pub fn git_status_file() -> String {
516 #[cfg(feature = "git2")]
517 {
518 use crate::git::git2_mod::git_repo;
519 git_repo(".")
520 .map(|x| Git::git2_dirty_stage(&x))
521 .unwrap_or_default()
522 }
523 #[cfg(not(feature = "git2"))]
524 {
525 command_git_status_file()
526 }
527}
528
529struct GitHeadInfo {
530 commit: String,
531 short_commit: String,
532 email: String,
533 author: String,
534 date: String,
535 date_iso: String,
536}
537
538struct GitCommandExecutor<'a> {
539 path: &'a Path,
540}
541
542impl Default for GitCommandExecutor<'_> {
543 fn default() -> Self {
544 Self::new(Path::new("."))
545 }
546}
547
548impl<'a> GitCommandExecutor<'a> {
549 fn new(path: &'a Path) -> Self {
550 GitCommandExecutor { path }
551 }
552
553 fn exec(&self, args: &[&str]) -> Option<String> {
554 Command::new("git")
555 .env("GIT_OPTIONAL_LOCKS", "0")
556 .current_dir(self.path)
557 .args(args)
558 .output()
559 .map(|x| {
560 String::from_utf8(x.stdout)
561 .map(|x| x.trim().to_string())
562 .ok()
563 })
564 .unwrap_or(None)
565 }
566}
567
568fn command_git_head() -> GitHeadInfo {
569 let cli = |args: &[&str]| GitCommandExecutor::default().exec(args).unwrap_or_default();
570 GitHeadInfo {
571 commit: cli(&["rev-parse", "HEAD"]),
572 short_commit: cli(&["rev-parse", "--short", "HEAD"]),
573 author: cli(&["log", "-1", "--pretty=format:%an"]),
574 email: cli(&["log", "-1", "--pretty=format:%ae"]),
575 date: cli(&["show", "--pretty=format:%ct", "--date=raw", "-s"]),
576 date_iso: cli(&["log", "-1", "--pretty=format:%cI"]),
577 }
578}
579
580fn command_current_tag() -> Option<String> {
582 GitCommandExecutor::default().exec(&["tag", "-l", "--contains", "HEAD"])
583}
584
585fn command_git_describe() -> (Option<String>, Option<usize>, Option<String>) {
588 let last_tag =
589 GitCommandExecutor::default().exec(&["describe", "--tags", "--abbrev=0", "HEAD"]);
590 if last_tag.is_none() {
591 return (None, None, None);
592 }
593
594 let tag = last_tag.unwrap();
595
596 let describe = GitCommandExecutor::default().exec(&["describe", "--tags", "HEAD"]);
597 if let Some(desc) = describe {
598 match parse_git_describe(&tag, &desc) {
599 Ok((tag, commits, hash)) => {
600 return (Some(tag), commits, hash);
601 }
602 Err(_) => {
603 return (Some(tag), None, None);
604 }
605 }
606 }
607 (Some(tag), None, None)
608}
609
610fn parse_git_describe(
611 last_tag: &str,
612 describe: &str,
613) -> SdResult<(String, Option<usize>, Option<String>)> {
614 if !describe.starts_with(last_tag) {
615 return Err(ShadowError::String("git describe result error".to_string()));
616 }
617
618 if last_tag == describe {
619 return Ok((describe.to_string(), None, None));
620 }
621
622 let parts: Vec<&str> = describe.rsplit('-').collect();
623
624 if parts.is_empty() || parts.len() == 2 {
625 return Err(ShadowError::String(
626 "git describe result error,expect:<tag>-<num_commits>-g<hash>".to_string(),
627 ));
628 }
629
630 if parts.len() > 2 {
631 let short_hash = parts[0]; if !short_hash.starts_with('g') {
634 return Err(ShadowError::String(
635 "git describe result error,expect commit hash end with:-g<hash>".to_string(),
636 ));
637 }
638 let short_hash = short_hash.trim_start_matches('g');
639
640 let num_commits_str = parts[1];
642 let num_commits = num_commits_str
643 .parse::<usize>()
644 .map_err(|e| ShadowError::String(e.to_string()))?;
645 let last_tag = parts[2..]
646 .iter()
647 .rev()
648 .copied()
649 .collect::<Vec<_>>()
650 .join("-");
651 return Ok((last_tag, Some(num_commits), Some(short_hash.to_string())));
652 }
653 Ok((describe.to_string(), None, None))
654}
655
656fn command_git_clean() -> bool {
659 GitCommandExecutor::default()
660 .exec(&["status", "--porcelain"])
661 .map(|x| x.is_empty())
662 .unwrap_or(true)
663}
664
665fn command_git_status_file() -> String {
669 let git_status_files =
670 move |args: &[&str], grep: &[&str], awk: &[&str]| -> SdResult<Vec<String>> {
671 let git_shell = Command::new("git")
672 .env("GIT_OPTIONAL_LOCKS", "0")
673 .args(args)
674 .stdin(Stdio::piped())
675 .stdout(Stdio::piped())
676 .spawn()?;
677 let git_out = git_shell.stdout.ok_or("Failed to exec git stdout")?;
678
679 let grep_shell = Command::new("grep")
680 .args(grep)
681 .stdin(Stdio::from(git_out))
682 .stdout(Stdio::piped())
683 .spawn()?;
684 let grep_out = grep_shell.stdout.ok_or("Failed to exec grep stdout")?;
685
686 let mut awk_shell = Command::new("awk")
687 .args(awk)
688 .stdin(Stdio::from(grep_out))
689 .stdout(Stdio::piped())
690 .spawn()?;
691 let mut awk_out = BufReader::new(
692 awk_shell
693 .stdout
694 .as_mut()
695 .ok_or("Failed to exec awk stdout")?,
696 );
697 let mut line = String::new();
698 awk_out.read_to_string(&mut line)?;
699 Ok(line.lines().map(|x| x.into()).collect())
700 };
701
702 let dirty = git_status_files(&["status", "--porcelain"], &[r"^\sM."], &["{print $2}"])
703 .unwrap_or_default();
704
705 let stage = git_status_files(
706 &["status", "--porcelain", "--untracked-files=all"],
707 &[r#"^[A|M|D|R]"#],
708 &["{print $2}"],
709 )
710 .unwrap_or_default();
711 filter_git_dirty_stage(dirty, stage)
712}
713
714fn command_current_branch() -> Option<String> {
716 find_branch_in(Path::new("."))
717}
718
719fn find_branch_in(path: &Path) -> Option<String> {
720 GitCommandExecutor::new(path).exec(&["symbolic-ref", "--short", "HEAD"])
721}
722
723fn filter_git_dirty_stage(dirty_files: Vec<String>, staged_files: Vec<String>) -> String {
724 let mut concat_file = String::new();
725 for file in dirty_files {
726 concat_file.push_str(" * ");
727 concat_file.push_str(&file);
728 concat_file.push_str(" (dirty)\n");
729 }
730 for file in staged_files {
731 concat_file.push_str(" * ");
732 concat_file.push_str(&file);
733 concat_file.push_str(" (staged)\n");
734 }
735 concat_file
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741 use crate::get_std_env;
742
743 #[test]
744 fn test_git() {
745 let env_map = get_std_env();
746 let map = new_git(Path::new("./"), CiType::Github, &env_map);
747 for (k, v) in map {
748 assert!(!v.desc.is_empty());
749 if !k.eq(TAG)
750 && !k.eq(LAST_TAG)
751 && !k.eq(COMMITS_SINCE_TAG)
752 && !k.eq(BRANCH)
753 && !k.eq(GIT_STATUS_FILE)
754 {
755 assert!(!v.v.is_empty());
756 continue;
757 }
758
759 if let Some(github_ref) = env_map.get("GITHUB_REF") {
761 if github_ref.starts_with("refs/tags/") && k.eq(TAG) {
762 assert!(!v.v.is_empty(), "not empty");
763 } else if github_ref.starts_with("refs/heads/") && k.eq(BRANCH) {
764 assert!(!v.v.is_empty());
765 }
766 }
767 }
768 }
769
770 #[test]
771 fn test_current_branch() {
772 if get_std_env().contains_key("GITHUB_REF") {
773 return;
774 }
775 #[cfg(feature = "git2")]
776 {
777 use crate::git::git2_mod::{git2_current_branch, git_repo};
778 let git2_branch = git_repo(".")
779 .map(|x| git2_current_branch(&x))
780 .unwrap_or(None);
781 let command_branch = command_current_branch();
782 assert!(git2_branch.is_some());
783 assert!(command_branch.is_some());
784 assert_eq!(command_branch, git2_branch);
785 }
786
787 assert_eq!(Some(branch()), command_current_branch());
788 }
789
790 #[test]
791 fn test_parse_git_describe() {
792 let commit_hash = "24skp4489";
793 let describe = "v1.0.0";
794 assert_eq!(
795 parse_git_describe("v1.0.0", describe).unwrap(),
796 (describe.into(), None, None)
797 );
798
799 let describe = "v1.0.0-0-g24skp4489";
800 assert_eq!(
801 parse_git_describe("v1.0.0", describe).unwrap(),
802 ("v1.0.0".into(), Some(0), Some(commit_hash.into()))
803 );
804
805 let describe = "v1.0.0-1-g24skp4489";
806 assert_eq!(
807 parse_git_describe("v1.0.0", describe).unwrap(),
808 ("v1.0.0".into(), Some(1), Some(commit_hash.into()))
809 );
810
811 let describe = "v1.0.0-alpha-0-g24skp4489";
812 assert_eq!(
813 parse_git_describe("v1.0.0-alpha", describe).unwrap(),
814 ("v1.0.0-alpha".into(), Some(0), Some(commit_hash.into()))
815 );
816
817 let describe = "v1.0.0.alpha-0-g24skp4489";
818 assert_eq!(
819 parse_git_describe("v1.0.0.alpha", describe).unwrap(),
820 ("v1.0.0.alpha".into(), Some(0), Some(commit_hash.into()))
821 );
822
823 let describe = "v1.0.0-alpha";
824 assert_eq!(
825 parse_git_describe("v1.0.0-alpha", describe).unwrap(),
826 ("v1.0.0-alpha".into(), None, None)
827 );
828
829 let describe = "v1.0.0-alpha-99-0-g24skp4489";
830 assert_eq!(
831 parse_git_describe("v1.0.0-alpha-99", describe).unwrap(),
832 ("v1.0.0-alpha-99".into(), Some(0), Some(commit_hash.into()))
833 );
834
835 let describe = "v1.0.0-alpha-99-024skp4489";
836 assert!(parse_git_describe("v1.0.0-alpha-99", describe).is_err());
837
838 let describe = "v1.0.0-alpha-024skp4489";
839 assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
840
841 let describe = "v1.0.0-alpha-024skp4489";
842 assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
843
844 let describe = "v1.0.0-alpha-g024skp4489";
845 assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
846
847 let describe = "v1.0.0----alpha-g024skp4489";
848 assert!(parse_git_describe("v1.0.0----alpha", describe).is_err());
849 }
850
851 #[test]
852 fn test_commit_date_timezone_preservation() {
853 use crate::DateTime;
854
855 let iso_date = "2021-08-04T12:34:03+08:00";
857 let date_time = DateTime::from_iso8601_string(iso_date).unwrap();
858 assert_eq!(date_time.human_format(), "2021-08-04 12:34:03 +08:00");
859 assert!(date_time.to_rfc3339().contains("+08:00"));
860
861 let iso_date_utc = "2021-08-04T12:34:03Z";
863 let date_time_utc = DateTime::from_iso8601_string(iso_date_utc).unwrap();
864 assert_eq!(date_time_utc.human_format(), "2021-08-04 12:34:03 +00:00");
865
866 let iso_date_neg = "2021-08-04T12:34:03-05:00";
868 let date_time_neg = DateTime::from_iso8601_string(iso_date_neg).unwrap();
869 assert_eq!(date_time_neg.human_format(), "2021-08-04 12:34:03 -05:00");
870 }
871}