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