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.env("GIT_OPTIONAL_LOCKS", "0");
518 _ = cmd.arg("-c");
519 _ = cmd.arg(command);
520 _ = cmd.stdout(Stdio::piped());
521 _ = cmd.stderr(Stdio::piped());
522
523 let output = cmd.output()?;
524 if !output.status.success() {
525 eprintln!("Command failed: `{command}`");
526 eprintln!("--- stdout:\n{}\n", String::from_utf8_lossy(&output.stdout));
527 eprintln!("--- stderr:\n{}\n", String::from_utf8_lossy(&output.stderr));
528 }
529
530 Ok(output)
531 }
532
533 #[cfg(target_env = "msvc")]
534 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
535 let mut cmd = Command::new("cmd");
536 if let Some(path) = path_opt {
537 _ = cmd.current_dir(path);
538 }
539 _ = cmd.env("GIT_OPTIONAL_LOCKS", "0");
541 _ = cmd.arg("/c");
542 _ = cmd.arg(command);
543 _ = cmd.stdout(Stdio::piped());
544 _ = cmd.stderr(Stdio::piped());
545
546 let output = cmd.output()?;
547 if !output.status.success() {
548 eprintln!("Command failed: `{command}`");
549 eprintln!("--- stdout:\n{}\n", String::from_utf8_lossy(&output.stdout));
550 eprintln!("--- stderr:\n{}\n", String::from_utf8_lossy(&output.stderr));
551 }
552
553 Ok(output)
554 }
555
556 fn run_cmd_checked(command: &str, path_opt: Option<&PathBuf>) -> Result<Vec<u8>> {
557 let output = Self::run_cmd(command, path_opt)?;
558 if output.status.success() {
559 Ok(output.stdout)
560 } else {
561 let stderr = String::from_utf8_lossy(&output.stderr);
562 Err(anyhow!("Failed to run '{command}'! {stderr}"))
563 }
564 }
565
566 #[allow(clippy::too_many_lines)]
567 fn inner_add_git_map_entries(
568 &self,
569 idempotent: bool,
570 cargo_rustc_env: &mut CargoRustcEnvMap,
571 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
572 cargo_warning: &mut CargoWarning,
573 ) -> Result<()> {
574 if !idempotent && self.any() {
575 Self::add_rerun_if_changed(cargo_rerun_if_changed, self.repo_path.as_ref())?;
576 }
577
578 if self.branch {
579 if let Ok(_value) = env::var(GIT_BRANCH_NAME) {
580 add_default_map_entry(VergenKey::GitBranch, cargo_rustc_env, cargo_warning);
581 } else {
582 Self::add_git_cmd_entry(
583 BRANCH_CMD,
584 self.repo_path.as_ref(),
585 VergenKey::GitBranch,
586 cargo_rustc_env,
587 )?;
588 }
589 }
590
591 if self.commit_author_email {
592 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_EMAIL) {
593 add_default_map_entry(
594 VergenKey::GitCommitAuthorEmail,
595 cargo_rustc_env,
596 cargo_warning,
597 );
598 } else {
599 Self::add_git_cmd_entry(
600 COMMIT_AUTHOR_EMAIL,
601 self.repo_path.as_ref(),
602 VergenKey::GitCommitAuthorEmail,
603 cargo_rustc_env,
604 )?;
605 }
606 }
607
608 if self.commit_author_name {
609 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_NAME) {
610 add_default_map_entry(
611 VergenKey::GitCommitAuthorName,
612 cargo_rustc_env,
613 cargo_warning,
614 );
615 } else {
616 Self::add_git_cmd_entry(
617 COMMIT_AUTHOR_NAME,
618 self.repo_path.as_ref(),
619 VergenKey::GitCommitAuthorName,
620 cargo_rustc_env,
621 )?;
622 }
623 }
624
625 if self.commit_count {
626 if let Ok(_value) = env::var(GIT_COMMIT_COUNT) {
627 add_default_map_entry(VergenKey::GitCommitCount, cargo_rustc_env, cargo_warning);
628 } else {
629 Self::add_git_cmd_entry(
630 COMMIT_COUNT,
631 self.repo_path.as_ref(),
632 VergenKey::GitCommitCount,
633 cargo_rustc_env,
634 )?;
635 }
636 }
637
638 self.add_git_timestamp_entries(
639 COMMIT_TIMESTAMP,
640 self.repo_path.as_ref(),
641 idempotent,
642 cargo_rustc_env,
643 cargo_warning,
644 )?;
645
646 if self.commit_message {
647 if let Ok(_value) = env::var(GIT_COMMIT_MESSAGE) {
648 add_default_map_entry(VergenKey::GitCommitMessage, cargo_rustc_env, cargo_warning);
649 } else {
650 Self::add_git_cmd_entry(
651 COMMIT_MESSAGE,
652 self.repo_path.as_ref(),
653 VergenKey::GitCommitMessage,
654 cargo_rustc_env,
655 )?;
656 }
657 }
658
659 let mut dirty_cache = None; if self.dirty {
661 if let Ok(_value) = env::var(GIT_DIRTY_NAME) {
662 add_default_map_entry(VergenKey::GitDirty, cargo_rustc_env, cargo_warning);
663 } else {
664 let dirty = self.compute_dirty(self.dirty_include_untracked)?;
665 if !self.dirty_include_untracked {
666 dirty_cache = Some(dirty);
667 }
668 add_map_entry(
669 VergenKey::GitDirty,
670 bool::to_string(&dirty),
671 cargo_rustc_env,
672 );
673 }
674 }
675
676 if self.describe {
677 if let Ok(_value) = env::var(GIT_DESCRIBE_NAME) {
682 add_default_map_entry(VergenKey::GitDescribe, cargo_rustc_env, cargo_warning);
683 } else {
684 let mut describe_cmd = String::from(DESCRIBE);
685 if self.describe_tags {
686 describe_cmd.push_str(" --tags");
687 }
688 if let Some(pattern) = self.describe_match_pattern {
689 describe_cmd.push_str(" --match \"");
690 describe_cmd.push_str(pattern);
691 describe_cmd.push('\"');
692 }
693 let stdout = Self::run_cmd_checked(&describe_cmd, self.repo_path.as_ref())?;
694 let mut describe_value = String::from_utf8_lossy(&stdout).trim().to_string();
695 if self.describe_dirty
696 && (dirty_cache.is_some_and(|dirty| dirty) || self.compute_dirty(false)?)
697 {
698 describe_value.push_str("-dirty");
699 }
700 add_map_entry(VergenKey::GitDescribe, describe_value, cargo_rustc_env);
701 }
702 }
703
704 if self.sha {
705 if let Ok(_value) = env::var(GIT_SHA_NAME) {
706 add_default_map_entry(VergenKey::GitSha, cargo_rustc_env, cargo_warning);
707 } else {
708 let mut sha_cmd = String::from(SHA);
709 if self.sha_short {
710 sha_cmd.push_str(" --short");
711 }
712 sha_cmd.push_str(" HEAD");
713 Self::add_git_cmd_entry(
714 &sha_cmd,
715 self.repo_path.as_ref(),
716 VergenKey::GitSha,
717 cargo_rustc_env,
718 )?;
719 }
720 }
721
722 Ok(())
723 }
724
725 fn add_rerun_if_changed(
726 rerun_if_changed: &mut Vec<String>,
727 path: Option<&PathBuf>,
728 ) -> Result<()> {
729 let git_path = Self::run_cmd("git rev-parse --git-dir", path)?;
730 if git_path.status.success() {
731 let git_path_str = String::from_utf8_lossy(&git_path.stdout).trim().to_string();
732 let git_path = PathBuf::from(&git_path_str);
733
734 let mut head_path = git_path.clone();
736 head_path.push("HEAD");
737
738 if head_path.exists() {
739 rerun_if_changed.push(format!("{}", head_path.display()));
740 }
741
742 let refp = Self::setup_ref_path(path)?;
744 if refp.status.success() {
745 let ref_path_str = String::from_utf8_lossy(&refp.stdout).trim().to_string();
746 let mut ref_path = git_path;
747 ref_path.push(ref_path_str);
748 if ref_path.exists() {
749 rerun_if_changed.push(format!("{}", ref_path.display()));
750 }
751 }
752 }
753 Ok(())
754 }
755
756 #[cfg(not(test))]
757 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
758 Self::run_cmd("git symbolic-ref HEAD", path)
759 }
760
761 #[cfg(all(test, not(target_os = "windows")))]
762 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
763 Self::run_cmd("pwd", path)
764 }
765
766 #[cfg(all(test, target_os = "windows"))]
767 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
768 Self::run_cmd("cd", path)
769 }
770
771 fn add_git_cmd_entry(
772 cmd: &str,
773 path: Option<&PathBuf>,
774 key: VergenKey,
775 cargo_rustc_env: &mut CargoRustcEnvMap,
776 ) -> Result<()> {
777 let stdout = Self::run_cmd_checked(cmd, path)?;
778 let stdout = String::from_utf8_lossy(&stdout)
779 .trim()
780 .trim_matches('\'')
781 .to_string();
782 add_map_entry(key, stdout, cargo_rustc_env);
783 Ok(())
784 }
785
786 fn add_git_timestamp_entries(
787 &self,
788 cmd: &str,
789 path: Option<&PathBuf>,
790 idempotent: bool,
791 cargo_rustc_env: &mut CargoRustcEnvMap,
792 cargo_warning: &mut CargoWarning,
793 ) -> Result<()> {
794 let mut date_override = false;
795 if let Ok(_value) = env::var(GIT_COMMIT_DATE_NAME) {
796 add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env, cargo_warning);
797 date_override = true;
798 }
799
800 let mut timestamp_override = false;
801 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
802 add_default_map_entry(
803 VergenKey::GitCommitTimestamp,
804 cargo_rustc_env,
805 cargo_warning,
806 );
807 timestamp_override = true;
808 }
809
810 let output = Self::run_cmd(cmd, path)?;
811 if output.status.success() {
812 let stdout = String::from_utf8_lossy(&output.stdout)
813 .lines()
814 .last()
815 .ok_or_else(|| anyhow!("invalid 'git log' output"))?
816 .trim()
817 .trim_matches('\'')
818 .to_string();
819
820 let (sde, ts) = match env::var("SOURCE_DATE_EPOCH") {
821 Ok(v) => (
822 true,
823 OffsetDateTime::from_unix_timestamp(i64::from_str(&v)?)?,
824 ),
825 Err(VarError::NotPresent) => self.compute_local_offset(&stdout)?,
826 Err(e) => return Err(e.into()),
827 };
828
829 if idempotent && !sde {
830 if self.commit_date && !date_override {
831 add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env, cargo_warning);
832 }
833
834 if self.commit_timestamp && !timestamp_override {
835 add_default_map_entry(
836 VergenKey::GitCommitTimestamp,
837 cargo_rustc_env,
838 cargo_warning,
839 );
840 }
841 } else {
842 if self.commit_date && !date_override {
843 let format = format_description::parse("[year]-[month]-[day]")?;
844 add_map_entry(
845 VergenKey::GitCommitDate,
846 ts.format(&format)?,
847 cargo_rustc_env,
848 );
849 }
850
851 if self.commit_timestamp && !timestamp_override {
852 add_map_entry(
853 VergenKey::GitCommitTimestamp,
854 ts.format(&Iso8601::DEFAULT)?,
855 cargo_rustc_env,
856 );
857 }
858 }
859 } else {
860 if self.commit_date && !date_override {
861 add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env, cargo_warning);
862 }
863
864 if self.commit_timestamp && !timestamp_override {
865 add_default_map_entry(
866 VergenKey::GitCommitTimestamp,
867 cargo_rustc_env,
868 cargo_warning,
869 );
870 }
871 }
872
873 Ok(())
874 }
875
876 #[cfg_attr(coverage_nightly, coverage(off))]
877 fn compute_local_offset(&self, stdout: &str) -> Result<(bool, OffsetDateTime)> {
879 let no_offset = OffsetDateTime::parse(stdout, &Rfc3339)?;
880 if self.use_local {
881 let local = UtcOffset::local_offset_at(no_offset)?;
882 let local_offset = no_offset.checked_to_offset(local).unwrap_or(no_offset);
883 Ok((false, local_offset))
884 } else {
885 Ok((false, no_offset))
886 }
887 }
888
889 fn compute_dirty(&self, include_untracked: bool) -> Result<bool> {
890 let mut dirty_cmd = String::from(DIRTY);
891 if !include_untracked {
892 dirty_cmd.push_str(" --untracked-files=no");
893 }
894 let stdout = Self::run_cmd_checked(&dirty_cmd, self.repo_path.as_ref())?;
895 Ok(!stdout.is_empty())
896 }
897}
898
899impl AddEntries for Gitcl {
900 fn add_map_entries(
901 &self,
902 idempotent: bool,
903 cargo_rustc_env: &mut CargoRustcEnvMap,
904 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
905 cargo_warning: &mut CargoWarning,
906 ) -> Result<()> {
907 if self.any() {
908 let git_cmd = self.git_cmd.unwrap_or("git --version");
909 Self::check_git(git_cmd)
910 .and_then(|()| Self::check_inside_git_worktree(self.repo_path.as_ref()))?;
911 self.inner_add_git_map_entries(
912 idempotent,
913 cargo_rustc_env,
914 cargo_rerun_if_changed,
915 cargo_warning,
916 )?;
917 }
918 Ok(())
919 }
920
921 fn add_default_entries(
922 &self,
923 config: &DefaultConfig,
924 cargo_rustc_env_map: &mut CargoRustcEnvMap,
925 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
926 cargo_warning: &mut CargoWarning,
927 ) -> Result<()> {
928 if *config.fail_on_error() {
929 let error = Error::msg(format!("{}", config.error()));
930 Err(error)
931 } else {
932 cargo_warning.clear();
935 cargo_rerun_if_changed.clear();
936
937 cargo_warning.push(format!("{}", config.error()));
938
939 if self.branch {
940 add_default_map_entry(VergenKey::GitBranch, cargo_rustc_env_map, cargo_warning);
941 }
942 if self.commit_author_email {
943 add_default_map_entry(
944 VergenKey::GitCommitAuthorEmail,
945 cargo_rustc_env_map,
946 cargo_warning,
947 );
948 }
949 if self.commit_author_name {
950 add_default_map_entry(
951 VergenKey::GitCommitAuthorName,
952 cargo_rustc_env_map,
953 cargo_warning,
954 );
955 }
956 if self.commit_count {
957 add_default_map_entry(
958 VergenKey::GitCommitCount,
959 cargo_rustc_env_map,
960 cargo_warning,
961 );
962 }
963 if self.commit_date {
964 add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env_map, cargo_warning);
965 }
966 if self.commit_message {
967 add_default_map_entry(
968 VergenKey::GitCommitMessage,
969 cargo_rustc_env_map,
970 cargo_warning,
971 );
972 }
973 if self.commit_timestamp {
974 add_default_map_entry(
975 VergenKey::GitCommitTimestamp,
976 cargo_rustc_env_map,
977 cargo_warning,
978 );
979 }
980 if self.describe {
981 add_default_map_entry(VergenKey::GitDescribe, cargo_rustc_env_map, cargo_warning);
982 }
983 if self.sha {
984 add_default_map_entry(VergenKey::GitSha, cargo_rustc_env_map, cargo_warning);
985 }
986 if self.dirty {
987 add_default_map_entry(VergenKey::GitDirty, cargo_rustc_env_map, cargo_warning);
988 }
989 Ok(())
990 }
991 }
992}
993
994#[cfg(test)]
995mod test {
996 use super::{Gitcl, GitclBuilder};
997 use crate::Emitter;
998 use anyhow::Result;
999 use serial_test::serial;
1000 #[cfg(unix)]
1001 use std::io::stdout;
1002 use std::{collections::BTreeMap, env::temp_dir, io::Write};
1003 use test_util::TestRepos;
1004 #[cfg(unix)]
1005 use test_util::TEST_MTIME;
1006 use vergen_lib::{count_idempotent, VergenKey};
1007
1008 #[test]
1009 #[serial]
1010 #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
1011 fn gitcl_clone_works() -> Result<()> {
1012 let gitcl = GitclBuilder::all_git()?;
1013 let another = gitcl.clone();
1014 assert_eq!(another, gitcl);
1015 Ok(())
1016 }
1017
1018 #[test]
1019 #[serial]
1020 fn gitcl_debug_works() -> Result<()> {
1021 let gitcl = GitclBuilder::all_git()?;
1022 let mut buf = vec![];
1023 write!(buf, "{gitcl:?}")?;
1024 assert!(!buf.is_empty());
1025 Ok(())
1026 }
1027
1028 #[test]
1029 #[serial]
1030 fn gix_default() -> Result<()> {
1031 let gitcl = GitclBuilder::default().build()?;
1032 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1033 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1034 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1035 assert_eq!(0, emitter.cargo_warning().len());
1036 Ok(())
1037 }
1038
1039 #[test]
1040 #[serial]
1041 fn bad_command_is_error() -> Result<()> {
1042 let mut map = BTreeMap::new();
1043 assert!(Gitcl::add_git_cmd_entry(
1044 "such_a_terrible_cmd",
1045 None,
1046 VergenKey::GitCommitMessage,
1047 &mut map
1048 )
1049 .is_err());
1050 Ok(())
1051 }
1052
1053 #[test]
1054 #[serial]
1055 fn non_working_tree_is_error() -> Result<()> {
1056 assert!(Gitcl::check_inside_git_worktree(Some(&temp_dir())).is_err());
1057 Ok(())
1058 }
1059
1060 #[test]
1061 #[serial]
1062 fn invalid_git_is_error() -> Result<()> {
1063 assert!(Gitcl::check_git("such_a_terrible_cmd -v").is_err());
1064 Ok(())
1065 }
1066
1067 #[cfg(not(target_family = "windows"))]
1068 #[test]
1069 #[serial]
1070 fn shell_env_works() -> Result<()> {
1071 temp_env::with_var("SHELL", Some("bash"), || {
1072 let mut map = BTreeMap::new();
1073 assert!(Gitcl::add_git_cmd_entry(
1074 "git -v",
1075 None,
1076 VergenKey::GitCommitMessage,
1077 &mut map
1078 )
1079 .is_ok());
1080 });
1081 Ok(())
1082 }
1083
1084 #[test]
1085 #[serial]
1086 fn git_all_idempotent() -> Result<()> {
1087 let gitcl = GitclBuilder::all_git()?;
1088 let emitter = Emitter::default()
1089 .idempotent()
1090 .add_instructions(&gitcl)?
1091 .test_emit();
1092 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1093 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1094 assert_eq!(2, emitter.cargo_warning().len());
1095 Ok(())
1096 }
1097
1098 #[test]
1099 #[serial]
1100 fn git_all_idempotent_no_warn() -> Result<()> {
1101 let gitcl = GitclBuilder::all_git()?;
1102 let emitter = Emitter::default()
1103 .idempotent()
1104 .quiet()
1105 .add_instructions(&gitcl)?
1106 .test_emit();
1107 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1108 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1109 assert_eq!(2, emitter.cargo_warning().len());
1110 Ok(())
1111 }
1112
1113 #[test]
1114 #[serial]
1115 fn git_all_at_path() -> Result<()> {
1116 let repo = TestRepos::new(false, false, false)?;
1117 let mut gitcl = GitclBuilder::all_git()?;
1118 let _ = gitcl.at_path(repo.path());
1119 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1120 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1121 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1122 assert_eq!(0, emitter.cargo_warning().len());
1123 Ok(())
1124 }
1125
1126 #[test]
1127 #[serial]
1128 fn git_all() -> Result<()> {
1129 let gitcl = GitclBuilder::all_git()?;
1130 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1131 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1132 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1133 assert_eq!(0, emitter.cargo_warning().len());
1134 Ok(())
1135 }
1136
1137 #[test]
1138 #[serial]
1139 fn git_all_shallow_clone() -> Result<()> {
1140 let repo = TestRepos::new(false, false, true)?;
1141 let mut gitcl = GitclBuilder::all_git()?;
1142 let _ = gitcl.at_path(repo.path());
1143 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1144 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1145 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1146 assert_eq!(0, emitter.cargo_warning().len());
1147 Ok(())
1148 }
1149
1150 #[test]
1151 #[serial]
1152 fn git_all_dirty_tags_short() -> Result<()> {
1153 let gitcl = GitclBuilder::default()
1154 .all()
1155 .describe(true, true, None)
1156 .sha(true)
1157 .build()?;
1158 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1159 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1160 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1161 assert_eq!(0, emitter.cargo_warning().len());
1162 Ok(())
1163 }
1164
1165 #[test]
1166 #[serial]
1167 fn fails_on_bad_git_command() -> Result<()> {
1168 let mut gitcl = GitclBuilder::all_git()?;
1169 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1170 assert!(Emitter::default()
1171 .fail_on_error()
1172 .add_instructions(&gitcl)
1173 .is_err());
1174 Ok(())
1175 }
1176
1177 #[test]
1178 #[serial]
1179 fn defaults_on_bad_git_command() -> Result<()> {
1180 let mut gitcl = GitclBuilder::all_git()?;
1181 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1182 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1183 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1184 assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
1185 assert_eq!(11, emitter.cargo_warning().len());
1186 Ok(())
1187 }
1188
1189 #[test]
1190 #[serial]
1191 fn bad_timestamp_defaults() -> Result<()> {
1192 let mut map = BTreeMap::new();
1193 let mut warnings = vec![];
1194 let gitcl = GitclBuilder::all_git()?;
1195 assert!(gitcl
1196 .add_git_timestamp_entries(
1197 "this_is_not_a_git_cmd",
1198 None,
1199 false,
1200 &mut map,
1201 &mut warnings
1202 )
1203 .is_ok());
1204 assert_eq!(2, map.len());
1205 assert_eq!(2, warnings.len());
1206 Ok(())
1207 }
1208
1209 #[test]
1210 #[serial]
1211 fn source_date_epoch_works() {
1212 temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), || {
1213 let result = || -> Result<()> {
1214 let mut stdout_buf = vec![];
1215 let gitcl = GitclBuilder::default()
1216 .commit_date(true)
1217 .commit_timestamp(true)
1218 .build()?;
1219 _ = Emitter::new()
1220 .idempotent()
1221 .add_instructions(&gitcl)?
1222 .emit_to(&mut stdout_buf)?;
1223 let output = String::from_utf8_lossy(&stdout_buf);
1224 for (idx, line) in output.lines().enumerate() {
1225 if idx == 0 {
1226 assert_eq!("cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2022-12-23", line);
1227 } else if idx == 1 {
1228 assert_eq!(
1229 "cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2022-12-23T15:29:20.000000000Z",
1230 line
1231 );
1232 }
1233 }
1234 Ok(())
1235 }();
1236 assert!(result.is_ok());
1237 });
1238 }
1239
1240 #[test]
1241 #[serial]
1242 #[cfg(unix)]
1243 fn bad_source_date_epoch_fails() {
1244 use std::ffi::OsStr;
1245 use std::os::unix::prelude::OsStrExt;
1246
1247 let source = [0x66, 0x6f, 0x80, 0x6f];
1248 let os_str = OsStr::from_bytes(&source[..]);
1249 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1250 let result = || -> Result<bool> {
1251 let mut stdout_buf = vec![];
1252 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1253 Emitter::new()
1254 .idempotent()
1255 .fail_on_error()
1256 .add_instructions(&gitcl)?
1257 .emit_to(&mut stdout_buf)
1258 }();
1259 assert!(result.is_err());
1260 });
1261 }
1262
1263 #[test]
1264 #[serial]
1265 #[cfg(unix)]
1266 fn bad_source_date_epoch_defaults() {
1267 use std::ffi::OsStr;
1268 use std::os::unix::prelude::OsStrExt;
1269
1270 let source = [0x66, 0x6f, 0x80, 0x6f];
1271 let os_str = OsStr::from_bytes(&source[..]);
1272 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1273 let result = || -> Result<bool> {
1274 let mut stdout_buf = vec![];
1275 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1276 Emitter::new()
1277 .idempotent()
1278 .add_instructions(&gitcl)?
1279 .emit_to(&mut stdout_buf)
1280 }();
1281 assert!(result.is_ok());
1282 });
1283 }
1284
1285 #[test]
1286 #[serial]
1287 #[cfg(windows)]
1288 fn bad_source_date_epoch_fails() {
1289 use std::ffi::OsString;
1290 use std::os::windows::prelude::OsStringExt;
1291
1292 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1293 let os_string = OsString::from_wide(&source[..]);
1294 let os_str = os_string.as_os_str();
1295 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1296 let result = || -> Result<bool> {
1297 let mut stdout_buf = vec![];
1298 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1299 Emitter::new()
1300 .fail_on_error()
1301 .idempotent()
1302 .add_instructions(&gitcl)?
1303 .emit_to(&mut stdout_buf)
1304 }();
1305 assert!(result.is_err());
1306 });
1307 }
1308
1309 #[test]
1310 #[serial]
1311 #[cfg(windows)]
1312 fn bad_source_date_epoch_defaults() {
1313 use std::ffi::OsString;
1314 use std::os::windows::prelude::OsStringExt;
1315
1316 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1317 let os_string = OsString::from_wide(&source[..]);
1318 let os_str = os_string.as_os_str();
1319 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1320 let result = || -> Result<bool> {
1321 let mut stdout_buf = vec![];
1322 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1323 Emitter::new()
1324 .idempotent()
1325 .add_instructions(&gitcl)?
1326 .emit_to(&mut stdout_buf)
1327 }();
1328 assert!(result.is_ok());
1329 });
1330 }
1331
1332 #[test]
1333 #[serial]
1334 #[cfg(unix)]
1335 fn git_no_index_update() -> Result<()> {
1336 let repo = TestRepos::new(true, true, false)?;
1337 repo.set_index_magic_mtime()?;
1338
1339 let mut gitcl = GitclBuilder::default()
1341 .all()
1342 .describe(true, true, None)
1343 .build()?;
1344 let _ = gitcl.at_path(repo.path());
1345 let failed = Emitter::default()
1346 .add_instructions(&gitcl)?
1347 .emit_to(&mut stdout())?;
1348 assert!(!failed);
1349
1350 assert_eq!(*TEST_MTIME, repo.get_index_magic_mtime()?);
1351 Ok(())
1352 }
1353}