1use anyhow::{anyhow, Error, Result};
10use derive_builder::Builder as DeriveBuilder;
11use std::{
12 env::{self, VarError},
13 path::PathBuf,
14 process::{Command, Output, Stdio},
15 str::FromStr,
16};
17use time::{
18 format_description::{
19 self,
20 well_known::{Iso8601, Rfc3339},
21 },
22 OffsetDateTime, UtcOffset,
23};
24use vergen_lib::{
25 add_default_map_entry, add_map_entry,
26 constants::{
27 GIT_BRANCH_NAME, GIT_COMMIT_AUTHOR_EMAIL, GIT_COMMIT_AUTHOR_NAME, GIT_COMMIT_COUNT,
28 GIT_COMMIT_DATE_NAME, GIT_COMMIT_MESSAGE, GIT_COMMIT_TIMESTAMP_NAME, GIT_DESCRIBE_NAME,
29 GIT_DIRTY_NAME, GIT_SHA_NAME,
30 },
31 AddEntries, CargoRerunIfChanged, CargoRustcEnvMap, CargoWarning, DefaultConfig, VergenKey,
32};
33
34macro_rules! branch_cmd {
36 () => {
37 "git rev-parse --abbrev-ref --symbolic-full-name HEAD"
38 };
39}
40const BRANCH_CMD: &str = branch_cmd!();
41macro_rules! author_email {
42 () => {
43 "git log -1 --pretty=format:'%ae'"
44 };
45}
46const COMMIT_AUTHOR_EMAIL: &str = author_email!();
47macro_rules! author_name {
48 () => {
49 "git log -1 --pretty=format:'%an'"
50 };
51}
52const COMMIT_AUTHOR_NAME: &str = author_name!();
53macro_rules! commit_count {
54 () => {
55 "git rev-list --count HEAD"
56 };
57}
58const COMMIT_COUNT: &str = commit_count!();
59macro_rules! commit_date {
60 () => {
61 "git log -1 --pretty=format:'%cs'"
62 };
63}
64macro_rules! commit_message {
65 () => {
66 "git log -1 --format=%s"
67 };
68}
69const COMMIT_MESSAGE: &str = commit_message!();
70macro_rules! commit_timestamp {
71 () => {
72 "git log -1 --pretty=format:'%cI'"
73 };
74}
75const COMMIT_TIMESTAMP: &str = commit_timestamp!();
76macro_rules! describe {
77 () => {
78 "git describe --always"
79 };
80}
81const DESCRIBE: &str = describe!();
82macro_rules! sha {
83 () => {
84 "git rev-parse"
85 };
86}
87const SHA: &str = sha!();
88macro_rules! dirty {
89 () => {
90 "git status --porcelain"
91 };
92}
93const DIRTY: &str = dirty!();
94
95#[derive(Clone, Debug, DeriveBuilder, PartialEq)]
235#[allow(clippy::struct_excessive_bools)]
236pub struct Gitcl {
237 #[builder(default = "None")]
239 repo_path: Option<PathBuf>,
240 #[builder(default = "false")]
247 branch: bool,
248 #[builder(default = "false")]
255 commit_author_name: bool,
256 #[builder(default = "false")]
263 commit_author_email: bool,
264 #[builder(default = "false")]
270 commit_count: bool,
271 #[builder(default = "false")]
278 commit_message: bool,
279 #[doc = concat!(commit_date!())]
288 #[builder(default = "false")]
290 commit_date: bool,
291 #[builder(default = "false")]
298 commit_timestamp: bool,
299 #[builder(default = "false", setter(custom))]
309 describe: bool,
310 #[builder(default = "false", private)]
312 describe_tags: bool,
313 #[builder(default = "false", private)]
315 describe_dirty: bool,
316 #[builder(default = "None", private)]
318 describe_match_pattern: Option<&'static str>,
319 #[builder(default = "false", setter(custom))]
329 sha: bool,
330 #[builder(default = "false", private)]
332 sha_short: bool,
333 #[builder(default = "false", setter(custom))]
341 dirty: bool,
342 #[builder(default = "false", private)]
344 dirty_include_untracked: bool,
345 #[builder(default = "false")]
347 use_local: bool,
348 #[builder(default = "None")]
350 git_cmd: Option<&'static str>,
351}
352
353impl GitclBuilder {
354 pub fn all_git() -> Result<Gitcl> {
360 Self::default()
361 .branch(true)
362 .commit_author_email(true)
363 .commit_author_name(true)
364 .commit_count(true)
365 .commit_date(true)
366 .commit_message(true)
367 .commit_timestamp(true)
368 .describe(false, false, None)
369 .sha(false)
370 .dirty(false)
371 .build()
372 .map_err(Into::into)
373 }
374
375 pub fn all(&mut self) -> &mut Self {
377 self.branch(true)
378 .commit_author_email(true)
379 .commit_author_name(true)
380 .commit_count(true)
381 .commit_date(true)
382 .commit_message(true)
383 .commit_timestamp(true)
384 .describe(false, false, None)
385 .sha(false)
386 .dirty(false)
387 }
388
389 pub fn describe(
399 &mut self,
400 tags: bool,
401 dirty: bool,
402 matches: Option<&'static str>,
403 ) -> &mut Self {
404 self.describe = Some(true);
405 let _ = self.describe_tags(tags);
406 let _ = self.describe_dirty(dirty);
407 let _ = self.describe_match_pattern(matches);
408 self
409 }
410
411 pub fn dirty(&mut self, include_untracked: bool) -> &mut Self {
419 self.dirty = Some(true);
420 let _ = self.dirty_include_untracked(include_untracked);
421 self
422 }
423
424 pub fn sha(&mut self, short: bool) -> &mut Self {
434 self.sha = Some(true);
435 let _ = self.sha_short(short);
436 self
437 }
438}
439
440impl Gitcl {
441 fn any(&self) -> bool {
442 self.branch
443 || self.commit_author_email
444 || self.commit_author_name
445 || self.commit_count
446 || self.commit_date
447 || self.commit_message
448 || self.commit_timestamp
449 || self.describe
450 || self.sha
451 || self.dirty
452 }
453
454 pub fn at_path(&mut self, path: PathBuf) -> &mut Self {
456 self.repo_path = Some(path);
457 self
458 }
459
460 pub fn git_cmd(&mut self, cmd: Option<&'static str>) -> &mut Self {
469 self.git_cmd = cmd;
470 self
471 }
472
473 fn check_git(cmd: &str) -> Result<()> {
474 if Self::git_cmd_exists(cmd) {
475 Ok(())
476 } else {
477 Err(anyhow!("no suitable 'git' command found!"))
478 }
479 }
480
481 fn check_inside_git_worktree(path: Option<&PathBuf>) -> Result<()> {
482 if Self::inside_git_worktree(path) {
483 Ok(())
484 } else {
485 Err(anyhow!("not within a suitable 'git' worktree!"))
486 }
487 }
488
489 fn git_cmd_exists(cmd: &str) -> bool {
490 Self::run_cmd(cmd, None)
491 .map(|output| output.status.success())
492 .unwrap_or(false)
493 }
494
495 fn inside_git_worktree(path: Option<&PathBuf>) -> bool {
496 Self::run_cmd("git rev-parse --is-inside-work-tree", path)
497 .map(|output| {
498 let stdout = String::from_utf8_lossy(&output.stdout);
499 output.status.success() && stdout.trim() == "true"
500 })
501 .unwrap_or(false)
502 }
503
504 #[cfg(not(target_env = "msvc"))]
505 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
506 let shell = if let Some(shell_path) = env::var_os("SHELL") {
507 shell_path.to_string_lossy().into_owned()
508 } else {
509 "sh".to_string()
511 };
512 let mut cmd = Command::new(shell);
513 if let Some(path) = path_opt {
514 _ = cmd.current_dir(path);
515 }
516 _ = cmd.arg("-c");
517 _ = cmd.arg(command);
518 _ = cmd.stdout(Stdio::piped());
519 _ = cmd.stderr(Stdio::piped());
520 Ok(cmd.output()?)
521 }
522
523 #[cfg(target_env = "msvc")]
524 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
525 let mut cmd = Command::new("cmd");
526 if let Some(path) = path_opt {
527 _ = cmd.current_dir(path);
528 }
529 _ = cmd.arg("/c");
530 _ = cmd.arg(command);
531 _ = cmd.stdout(Stdio::piped());
532 _ = cmd.stderr(Stdio::piped());
533 Ok(cmd.output()?)
534 }
535
536 #[allow(clippy::too_many_lines)]
537 fn inner_add_git_map_entries(
538 &self,
539 idempotent: bool,
540 cargo_rustc_env: &mut CargoRustcEnvMap,
541 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
542 cargo_warning: &mut CargoWarning,
543 ) -> Result<()> {
544 if !idempotent && self.any() {
545 Self::add_rerun_if_changed(cargo_rerun_if_changed, self.repo_path.as_ref())?;
546 }
547
548 if self.branch {
549 if let Ok(_value) = env::var(GIT_BRANCH_NAME) {
550 add_default_map_entry(VergenKey::GitBranch, cargo_rustc_env, cargo_warning);
551 } else {
552 Self::add_git_cmd_entry(
553 BRANCH_CMD,
554 self.repo_path.as_ref(),
555 VergenKey::GitBranch,
556 cargo_rustc_env,
557 )?;
558 }
559 }
560
561 if self.commit_author_email {
562 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_EMAIL) {
563 add_default_map_entry(
564 VergenKey::GitCommitAuthorEmail,
565 cargo_rustc_env,
566 cargo_warning,
567 );
568 } else {
569 Self::add_git_cmd_entry(
570 COMMIT_AUTHOR_EMAIL,
571 self.repo_path.as_ref(),
572 VergenKey::GitCommitAuthorEmail,
573 cargo_rustc_env,
574 )?;
575 }
576 }
577
578 if self.commit_author_name {
579 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_NAME) {
580 add_default_map_entry(
581 VergenKey::GitCommitAuthorName,
582 cargo_rustc_env,
583 cargo_warning,
584 );
585 } else {
586 Self::add_git_cmd_entry(
587 COMMIT_AUTHOR_NAME,
588 self.repo_path.as_ref(),
589 VergenKey::GitCommitAuthorName,
590 cargo_rustc_env,
591 )?;
592 }
593 }
594
595 if self.commit_count {
596 if let Ok(_value) = env::var(GIT_COMMIT_COUNT) {
597 add_default_map_entry(VergenKey::GitCommitCount, cargo_rustc_env, cargo_warning);
598 } else {
599 Self::add_git_cmd_entry(
600 COMMIT_COUNT,
601 self.repo_path.as_ref(),
602 VergenKey::GitCommitCount,
603 cargo_rustc_env,
604 )?;
605 }
606 }
607
608 self.add_git_timestamp_entries(
609 COMMIT_TIMESTAMP,
610 self.repo_path.as_ref(),
611 idempotent,
612 cargo_rustc_env,
613 cargo_warning,
614 )?;
615
616 if self.commit_message {
617 if let Ok(_value) = env::var(GIT_COMMIT_MESSAGE) {
618 add_default_map_entry(VergenKey::GitCommitMessage, cargo_rustc_env, cargo_warning);
619 } else {
620 Self::add_git_cmd_entry(
621 COMMIT_MESSAGE,
622 self.repo_path.as_ref(),
623 VergenKey::GitCommitMessage,
624 cargo_rustc_env,
625 )?;
626 }
627 }
628
629 if self.describe {
630 if let Ok(_value) = env::var(GIT_DESCRIBE_NAME) {
631 add_default_map_entry(VergenKey::GitDescribe, cargo_rustc_env, cargo_warning);
632 } else {
633 let mut describe_cmd = String::from(DESCRIBE);
634 if self.describe_dirty {
635 describe_cmd.push_str(" --dirty");
636 }
637 if self.describe_tags {
638 describe_cmd.push_str(" --tags");
639 }
640 if let Some(pattern) = self.describe_match_pattern {
641 describe_cmd.push_str(" --match \"");
642 describe_cmd.push_str(pattern);
643 describe_cmd.push('\"');
644 }
645 Self::add_git_cmd_entry(
646 &describe_cmd,
647 self.repo_path.as_ref(),
648 VergenKey::GitDescribe,
649 cargo_rustc_env,
650 )?;
651 }
652 }
653
654 if self.sha {
655 if let Ok(_value) = env::var(GIT_SHA_NAME) {
656 add_default_map_entry(VergenKey::GitSha, cargo_rustc_env, cargo_warning);
657 } else {
658 let mut sha_cmd = String::from(SHA);
659 if self.sha_short {
660 sha_cmd.push_str(" --short");
661 }
662 sha_cmd.push_str(" HEAD");
663 Self::add_git_cmd_entry(
664 &sha_cmd,
665 self.repo_path.as_ref(),
666 VergenKey::GitSha,
667 cargo_rustc_env,
668 )?;
669 }
670 }
671
672 if self.dirty {
673 if let Ok(_value) = env::var(GIT_DIRTY_NAME) {
674 add_default_map_entry(VergenKey::GitDirty, cargo_rustc_env, cargo_warning);
675 } else {
676 let mut dirty_cmd = String::from(DIRTY);
677 if !self.dirty_include_untracked {
678 dirty_cmd.push_str(" --untracked-files=no");
679 }
680 let output = Self::run_cmd(&dirty_cmd, self.repo_path.as_ref())?;
681 if output.stdout.is_empty() {
682 add_map_entry(VergenKey::GitDirty, "false", cargo_rustc_env);
683 } else {
684 add_map_entry(VergenKey::GitDirty, "true", cargo_rustc_env);
685 }
686 }
687 }
688
689 Ok(())
690 }
691
692 fn add_rerun_if_changed(
693 rerun_if_changed: &mut Vec<String>,
694 path: Option<&PathBuf>,
695 ) -> Result<()> {
696 let git_path = Self::run_cmd("git rev-parse --git-dir", path)?;
697 if git_path.status.success() {
698 let git_path_str = String::from_utf8_lossy(&git_path.stdout).trim().to_string();
699 let git_path = PathBuf::from(&git_path_str);
700
701 let mut head_path = git_path.clone();
703 head_path.push("HEAD");
704
705 if head_path.exists() {
706 rerun_if_changed.push(format!("{}", head_path.display()));
707 }
708
709 let refp = Self::setup_ref_path(path)?;
711 if refp.status.success() {
712 let ref_path_str = String::from_utf8_lossy(&refp.stdout).trim().to_string();
713 let mut ref_path = git_path;
714 ref_path.push(ref_path_str);
715 if ref_path.exists() {
716 rerun_if_changed.push(format!("{}", ref_path.display()));
717 }
718 }
719 }
720 Ok(())
721 }
722
723 #[cfg(not(test))]
724 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
725 Self::run_cmd("git symbolic-ref HEAD", path)
726 }
727
728 #[cfg(all(test, not(target_os = "windows")))]
729 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
730 Self::run_cmd("pwd", path)
731 }
732
733 #[cfg(all(test, target_os = "windows"))]
734 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
735 Self::run_cmd("cd", path)
736 }
737
738 fn add_git_cmd_entry(
739 cmd: &str,
740 path: Option<&PathBuf>,
741 key: VergenKey,
742 cargo_rustc_env: &mut CargoRustcEnvMap,
743 ) -> Result<()> {
744 let output = Self::run_cmd(cmd, path)?;
745 if output.status.success() {
746 let stdout = String::from_utf8_lossy(&output.stdout)
747 .trim()
748 .trim_matches('\'')
749 .to_string();
750 add_map_entry(key, stdout, cargo_rustc_env);
751 } else {
752 let stderr = String::from_utf8_lossy(&output.stderr);
753 return Err(anyhow!("Failed to run '{cmd}'! {stderr}"));
754 }
755 Ok(())
756 }
757
758 fn add_git_timestamp_entries(
759 &self,
760 cmd: &str,
761 path: Option<&PathBuf>,
762 idempotent: bool,
763 cargo_rustc_env: &mut CargoRustcEnvMap,
764 cargo_warning: &mut CargoWarning,
765 ) -> Result<()> {
766 let mut date_override = false;
767 if let Ok(_value) = env::var(GIT_COMMIT_DATE_NAME) {
768 add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env, cargo_warning);
769 date_override = true;
770 }
771
772 let mut timestamp_override = false;
773 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
774 add_default_map_entry(
775 VergenKey::GitCommitTimestamp,
776 cargo_rustc_env,
777 cargo_warning,
778 );
779 timestamp_override = true;
780 }
781
782 let output = Self::run_cmd(cmd, path)?;
783 if output.status.success() {
784 let stdout = String::from_utf8_lossy(&output.stdout)
785 .lines()
786 .last()
787 .ok_or_else(|| anyhow!("invalid 'git log' output"))?
788 .trim()
789 .trim_matches('\'')
790 .to_string();
791
792 let (sde, ts) = match env::var("SOURCE_DATE_EPOCH") {
793 Ok(v) => (
794 true,
795 OffsetDateTime::from_unix_timestamp(i64::from_str(&v)?)?,
796 ),
797 Err(VarError::NotPresent) => self.compute_local_offset(&stdout)?,
798 Err(e) => return Err(e.into()),
799 };
800
801 if idempotent && !sde {
802 if self.commit_date && !date_override {
803 add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env, cargo_warning);
804 }
805
806 if self.commit_timestamp && !timestamp_override {
807 add_default_map_entry(
808 VergenKey::GitCommitTimestamp,
809 cargo_rustc_env,
810 cargo_warning,
811 );
812 }
813 } else {
814 if self.commit_date && !date_override {
815 let format = format_description::parse("[year]-[month]-[day]")?;
816 add_map_entry(
817 VergenKey::GitCommitDate,
818 ts.format(&format)?,
819 cargo_rustc_env,
820 );
821 }
822
823 if self.commit_timestamp && !timestamp_override {
824 add_map_entry(
825 VergenKey::GitCommitTimestamp,
826 ts.format(&Iso8601::DEFAULT)?,
827 cargo_rustc_env,
828 );
829 }
830 }
831 } else {
832 if self.commit_date && !date_override {
833 add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env, cargo_warning);
834 }
835
836 if self.commit_timestamp && !timestamp_override {
837 add_default_map_entry(
838 VergenKey::GitCommitTimestamp,
839 cargo_rustc_env,
840 cargo_warning,
841 );
842 }
843 }
844
845 Ok(())
846 }
847
848 #[cfg_attr(coverage_nightly, coverage(off))]
849 fn compute_local_offset(&self, stdout: &str) -> Result<(bool, OffsetDateTime)> {
851 let no_offset = OffsetDateTime::parse(stdout, &Rfc3339)?;
852 if self.use_local {
853 let local = UtcOffset::local_offset_at(no_offset)?;
854 let local_offset = no_offset.checked_to_offset(local).unwrap_or(no_offset);
855 Ok((false, local_offset))
856 } else {
857 Ok((false, no_offset))
858 }
859 }
860}
861
862impl AddEntries for Gitcl {
863 fn add_map_entries(
864 &self,
865 idempotent: bool,
866 cargo_rustc_env: &mut CargoRustcEnvMap,
867 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
868 cargo_warning: &mut CargoWarning,
869 ) -> Result<()> {
870 if self.any() {
871 let git_cmd = self.git_cmd.unwrap_or("git --version");
872 Self::check_git(git_cmd)
873 .and_then(|()| Self::check_inside_git_worktree(self.repo_path.as_ref()))?;
874 self.inner_add_git_map_entries(
875 idempotent,
876 cargo_rustc_env,
877 cargo_rerun_if_changed,
878 cargo_warning,
879 )?;
880 }
881 Ok(())
882 }
883
884 fn add_default_entries(
885 &self,
886 config: &DefaultConfig,
887 cargo_rustc_env_map: &mut CargoRustcEnvMap,
888 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
889 cargo_warning: &mut CargoWarning,
890 ) -> Result<()> {
891 if *config.fail_on_error() {
892 let error = Error::msg(format!("{}", config.error()));
893 Err(error)
894 } else {
895 cargo_warning.clear();
898 cargo_rerun_if_changed.clear();
899
900 cargo_warning.push(format!("{}", config.error()));
901
902 if self.branch {
903 add_default_map_entry(VergenKey::GitBranch, cargo_rustc_env_map, cargo_warning);
904 }
905 if self.commit_author_email {
906 add_default_map_entry(
907 VergenKey::GitCommitAuthorEmail,
908 cargo_rustc_env_map,
909 cargo_warning,
910 );
911 }
912 if self.commit_author_name {
913 add_default_map_entry(
914 VergenKey::GitCommitAuthorName,
915 cargo_rustc_env_map,
916 cargo_warning,
917 );
918 }
919 if self.commit_count {
920 add_default_map_entry(
921 VergenKey::GitCommitCount,
922 cargo_rustc_env_map,
923 cargo_warning,
924 );
925 }
926 if self.commit_date {
927 add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env_map, cargo_warning);
928 }
929 if self.commit_message {
930 add_default_map_entry(
931 VergenKey::GitCommitMessage,
932 cargo_rustc_env_map,
933 cargo_warning,
934 );
935 }
936 if self.commit_timestamp {
937 add_default_map_entry(
938 VergenKey::GitCommitTimestamp,
939 cargo_rustc_env_map,
940 cargo_warning,
941 );
942 }
943 if self.describe {
944 add_default_map_entry(VergenKey::GitDescribe, cargo_rustc_env_map, cargo_warning);
945 }
946 if self.sha {
947 add_default_map_entry(VergenKey::GitSha, cargo_rustc_env_map, cargo_warning);
948 }
949 if self.dirty {
950 add_default_map_entry(VergenKey::GitDirty, cargo_rustc_env_map, cargo_warning);
951 }
952 Ok(())
953 }
954 }
955}
956
957#[cfg(test)]
958mod test {
959 use super::{Gitcl, GitclBuilder};
960 use crate::Emitter;
961 use anyhow::Result;
962 use serial_test::serial;
963 use std::{collections::BTreeMap, env::temp_dir, io::Write};
964 use test_util::TestRepos;
965 use vergen_lib::{count_idempotent, VergenKey};
966
967 #[test]
968 #[serial]
969 #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
970 fn gitcl_clone_works() -> Result<()> {
971 let gitcl = GitclBuilder::all_git()?;
972 let another = gitcl.clone();
973 assert_eq!(another, gitcl);
974 Ok(())
975 }
976
977 #[test]
978 #[serial]
979 fn gitcl_debug_works() -> Result<()> {
980 let gitcl = GitclBuilder::all_git()?;
981 let mut buf = vec![];
982 write!(buf, "{gitcl:?}")?;
983 assert!(!buf.is_empty());
984 Ok(())
985 }
986
987 #[test]
988 #[serial]
989 fn gix_default() -> Result<()> {
990 let gitcl = GitclBuilder::default().build()?;
991 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
992 assert_eq!(0, emitter.cargo_rustc_env_map().len());
993 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
994 assert_eq!(0, emitter.cargo_warning().len());
995 Ok(())
996 }
997
998 #[test]
999 #[serial]
1000 fn bad_command_is_error() -> Result<()> {
1001 let mut map = BTreeMap::new();
1002 assert!(Gitcl::add_git_cmd_entry(
1003 "such_a_terrible_cmd",
1004 None,
1005 VergenKey::GitCommitMessage,
1006 &mut map
1007 )
1008 .is_err());
1009 Ok(())
1010 }
1011
1012 #[test]
1013 #[serial]
1014 fn non_working_tree_is_error() -> Result<()> {
1015 assert!(Gitcl::check_inside_git_worktree(Some(&temp_dir())).is_err());
1016 Ok(())
1017 }
1018
1019 #[test]
1020 #[serial]
1021 fn invalid_git_is_error() -> Result<()> {
1022 assert!(Gitcl::check_git("such_a_terrible_cmd -v").is_err());
1023 Ok(())
1024 }
1025
1026 #[cfg(not(target_family = "windows"))]
1027 #[test]
1028 #[serial]
1029 fn shell_env_works() -> Result<()> {
1030 temp_env::with_var("SHELL", Some("bash"), || {
1031 let mut map = BTreeMap::new();
1032 assert!(Gitcl::add_git_cmd_entry(
1033 "git -v",
1034 None,
1035 VergenKey::GitCommitMessage,
1036 &mut map
1037 )
1038 .is_ok());
1039 });
1040 Ok(())
1041 }
1042
1043 #[test]
1044 #[serial]
1045 fn git_all_idempotent() -> Result<()> {
1046 let gitcl = GitclBuilder::all_git()?;
1047 let emitter = Emitter::default()
1048 .idempotent()
1049 .add_instructions(&gitcl)?
1050 .test_emit();
1051 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1052 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1053 assert_eq!(2, emitter.cargo_warning().len());
1054 Ok(())
1055 }
1056
1057 #[test]
1058 #[serial]
1059 fn git_all_idempotent_no_warn() -> Result<()> {
1060 let gitcl = GitclBuilder::all_git()?;
1061 let emitter = Emitter::default()
1062 .idempotent()
1063 .quiet()
1064 .add_instructions(&gitcl)?
1065 .test_emit();
1066 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1067 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1068 assert_eq!(2, emitter.cargo_warning().len());
1069 Ok(())
1070 }
1071
1072 #[test]
1073 #[serial]
1074 fn git_all_at_path() -> Result<()> {
1075 let repo = TestRepos::new(false, false, false)?;
1076 let mut gitcl = GitclBuilder::all_git()?;
1077 let _ = gitcl.at_path(repo.path());
1078 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1079 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1080 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1081 assert_eq!(0, emitter.cargo_warning().len());
1082 Ok(())
1083 }
1084
1085 #[test]
1086 #[serial]
1087 fn git_all() -> Result<()> {
1088 let gitcl = GitclBuilder::all_git()?;
1089 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1090 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1091 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1092 assert_eq!(0, emitter.cargo_warning().len());
1093 Ok(())
1094 }
1095
1096 #[test]
1097 #[serial]
1098 fn git_all_shallow_clone() -> Result<()> {
1099 let repo = TestRepos::new(false, false, true)?;
1100 let mut gitcl = GitclBuilder::all_git()?;
1101 let _ = gitcl.at_path(repo.path());
1102 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1103 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1104 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1105 assert_eq!(0, emitter.cargo_warning().len());
1106 Ok(())
1107 }
1108
1109 #[test]
1110 #[serial]
1111 fn git_all_dirty_tags_short() -> Result<()> {
1112 let gitcl = GitclBuilder::default()
1113 .all()
1114 .describe(true, true, None)
1115 .sha(true)
1116 .build()?;
1117 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1118 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1119 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1120 assert_eq!(0, emitter.cargo_warning().len());
1121 Ok(())
1122 }
1123
1124 #[test]
1125 #[serial]
1126 fn fails_on_bad_git_command() -> Result<()> {
1127 let mut gitcl = GitclBuilder::all_git()?;
1128 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1129 assert!(Emitter::default()
1130 .fail_on_error()
1131 .add_instructions(&gitcl)
1132 .is_err());
1133 Ok(())
1134 }
1135
1136 #[test]
1137 #[serial]
1138 fn defaults_on_bad_git_command() -> Result<()> {
1139 let mut gitcl = GitclBuilder::all_git()?;
1140 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1141 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1142 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1143 assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
1144 assert_eq!(11, emitter.cargo_warning().len());
1145 Ok(())
1146 }
1147
1148 #[test]
1149 #[serial]
1150 fn bad_timestamp_defaults() -> Result<()> {
1151 let mut map = BTreeMap::new();
1152 let mut warnings = vec![];
1153 let gitcl = GitclBuilder::all_git()?;
1154 assert!(gitcl
1155 .add_git_timestamp_entries(
1156 "this_is_not_a_git_cmd",
1157 None,
1158 false,
1159 &mut map,
1160 &mut warnings
1161 )
1162 .is_ok());
1163 assert_eq!(2, map.len());
1164 assert_eq!(2, warnings.len());
1165 Ok(())
1166 }
1167
1168 #[test]
1169 #[serial]
1170 fn source_date_epoch_works() {
1171 temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), || {
1172 let result = || -> Result<()> {
1173 let mut stdout_buf = vec![];
1174 let gitcl = GitclBuilder::default()
1175 .commit_date(true)
1176 .commit_timestamp(true)
1177 .build()?;
1178 _ = Emitter::new()
1179 .idempotent()
1180 .add_instructions(&gitcl)?
1181 .emit_to(&mut stdout_buf)?;
1182 let output = String::from_utf8_lossy(&stdout_buf);
1183 for (idx, line) in output.lines().enumerate() {
1184 if idx == 0 {
1185 assert_eq!("cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2022-12-23", line);
1186 } else if idx == 1 {
1187 assert_eq!(
1188 "cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2022-12-23T15:29:20.000000000Z",
1189 line
1190 );
1191 }
1192 }
1193 Ok(())
1194 }();
1195 assert!(result.is_ok());
1196 });
1197 }
1198
1199 #[test]
1200 #[serial]
1201 #[cfg(unix)]
1202 fn bad_source_date_epoch_fails() {
1203 use std::ffi::OsStr;
1204 use std::os::unix::prelude::OsStrExt;
1205
1206 let source = [0x66, 0x6f, 0x80, 0x6f];
1207 let os_str = OsStr::from_bytes(&source[..]);
1208 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1209 let result = || -> Result<bool> {
1210 let mut stdout_buf = vec![];
1211 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1212 Emitter::new()
1213 .idempotent()
1214 .fail_on_error()
1215 .add_instructions(&gitcl)?
1216 .emit_to(&mut stdout_buf)
1217 }();
1218 assert!(result.is_err());
1219 });
1220 }
1221
1222 #[test]
1223 #[serial]
1224 #[cfg(unix)]
1225 fn bad_source_date_epoch_defaults() {
1226 use std::ffi::OsStr;
1227 use std::os::unix::prelude::OsStrExt;
1228
1229 let source = [0x66, 0x6f, 0x80, 0x6f];
1230 let os_str = OsStr::from_bytes(&source[..]);
1231 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1232 let result = || -> Result<bool> {
1233 let mut stdout_buf = vec![];
1234 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1235 Emitter::new()
1236 .idempotent()
1237 .add_instructions(&gitcl)?
1238 .emit_to(&mut stdout_buf)
1239 }();
1240 assert!(result.is_ok());
1241 });
1242 }
1243
1244 #[test]
1245 #[serial]
1246 #[cfg(windows)]
1247 fn bad_source_date_epoch_fails() {
1248 use std::ffi::OsString;
1249 use std::os::windows::prelude::OsStringExt;
1250
1251 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1252 let os_string = OsString::from_wide(&source[..]);
1253 let os_str = os_string.as_os_str();
1254 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1255 let result = || -> Result<bool> {
1256 let mut stdout_buf = vec![];
1257 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1258 Emitter::new()
1259 .fail_on_error()
1260 .idempotent()
1261 .add_instructions(&gitcl)?
1262 .emit_to(&mut stdout_buf)
1263 }();
1264 assert!(result.is_err());
1265 });
1266 }
1267
1268 #[test]
1269 #[serial]
1270 #[cfg(windows)]
1271 fn bad_source_date_epoch_defaults() {
1272 use std::ffi::OsString;
1273 use std::os::windows::prelude::OsStringExt;
1274
1275 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1276 let os_string = OsString::from_wide(&source[..]);
1277 let os_str = os_string.as_os_str();
1278 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1279 let result = || -> Result<bool> {
1280 let mut stdout_buf = vec![];
1281 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1282 Emitter::new()
1283 .idempotent()
1284 .add_instructions(&gitcl)?
1285 .emit_to(&mut stdout_buf)
1286 }();
1287 assert!(result.is_ok());
1288 });
1289 }
1290}