1use anyhow::{Error, Result, anyhow};
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 OffsetDateTime, UtcOffset,
19 format_description::{
20 self,
21 well_known::{Iso8601, Rfc3339},
22 },
23};
24use vergen_lib::{
25 AddEntries, CargoRerunIfChanged, CargoRustcEnvMap, CargoWarning, DefaultConfig, VergenKey,
26 add_default_map_entry, add_map_entry,
27 constants::{
28 GIT_BRANCH_NAME, GIT_COMMIT_AUTHOR_EMAIL, GIT_COMMIT_AUTHOR_NAME, GIT_COMMIT_COUNT,
29 GIT_COMMIT_DATE_NAME, GIT_COMMIT_MESSAGE, GIT_COMMIT_TIMESTAMP_NAME, GIT_DESCRIBE_NAME,
30 GIT_DIRTY_NAME, GIT_SHA_NAME,
31 },
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 #[cfg(unix)]
1015 use test_util::TEST_MTIME;
1016 use test_util::TestRepos;
1017 use vergen_lib::{VergenKey, count_idempotent};
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!(
1055 Gitcl::add_git_cmd_entry(
1056 "such_a_terrible_cmd",
1057 None,
1058 VergenKey::GitCommitMessage,
1059 &mut map
1060 )
1061 .is_err()
1062 );
1063 Ok(())
1064 }
1065
1066 #[test]
1067 #[serial]
1068 fn non_working_tree_is_error() -> Result<()> {
1069 assert!(Gitcl::check_inside_git_worktree(Some(&temp_dir())).is_err());
1070 Ok(())
1071 }
1072
1073 #[test]
1074 #[serial]
1075 fn invalid_git_is_error() -> Result<()> {
1076 assert!(Gitcl::check_git("such_a_terrible_cmd -v").is_err());
1077 Ok(())
1078 }
1079
1080 #[cfg(not(target_family = "windows"))]
1081 #[test]
1082 #[serial]
1083 fn shell_env_works() -> Result<()> {
1084 temp_env::with_var("SHELL", Some("bash"), || {
1085 let mut map = BTreeMap::new();
1086 assert!(
1087 Gitcl::add_git_cmd_entry("git -v", None, VergenKey::GitCommitMessage, &mut map)
1088 .is_ok()
1089 );
1090 });
1091 Ok(())
1092 }
1093
1094 #[test]
1095 #[serial]
1096 fn git_all_idempotent() -> Result<()> {
1097 let gitcl = GitclBuilder::all_git()?;
1098 let emitter = Emitter::default()
1099 .idempotent()
1100 .add_instructions(&gitcl)?
1101 .test_emit();
1102 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1103 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1104 assert_eq!(2, emitter.cargo_warning().len());
1105 Ok(())
1106 }
1107
1108 #[test]
1109 #[serial]
1110 fn git_all_idempotent_no_warn() -> Result<()> {
1111 let gitcl = GitclBuilder::all_git()?;
1112 let emitter = Emitter::default()
1113 .idempotent()
1114 .quiet()
1115 .add_instructions(&gitcl)?
1116 .test_emit();
1117 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1118 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1119 assert_eq!(2, emitter.cargo_warning().len());
1120 Ok(())
1121 }
1122
1123 #[test]
1124 #[serial]
1125 fn git_all_at_path() -> Result<()> {
1126 let repo = TestRepos::new(false, false, false)?;
1127 let mut gitcl = GitclBuilder::all_git()?;
1128 let _ = gitcl.at_path(repo.path());
1129 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1130 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1131 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1132 assert_eq!(0, emitter.cargo_warning().len());
1133 Ok(())
1134 }
1135
1136 #[test]
1137 #[serial]
1138 fn git_all() -> Result<()> {
1139 let gitcl = GitclBuilder::all_git()?;
1140 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1141 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1142 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1143 assert_eq!(0, emitter.cargo_warning().len());
1144 Ok(())
1145 }
1146
1147 #[test]
1148 #[serial]
1149 fn git_all_shallow_clone() -> Result<()> {
1150 let repo = TestRepos::new(false, false, true)?;
1151 let mut gitcl = GitclBuilder::all_git()?;
1152 let _ = gitcl.at_path(repo.path());
1153 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1154 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1155 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1156 assert_eq!(0, emitter.cargo_warning().len());
1157 Ok(())
1158 }
1159
1160 #[test]
1161 #[serial]
1162 fn git_all_dirty_tags_short() -> Result<()> {
1163 let gitcl = GitclBuilder::default()
1164 .all()
1165 .describe(true, true, None)
1166 .sha(true)
1167 .build()?;
1168 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1169 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1170 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1171 assert_eq!(0, emitter.cargo_warning().len());
1172 Ok(())
1173 }
1174
1175 #[test]
1176 #[serial]
1177 fn fails_on_bad_git_command() -> Result<()> {
1178 let mut gitcl = GitclBuilder::all_git()?;
1179 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1180 assert!(
1181 Emitter::default()
1182 .fail_on_error()
1183 .add_instructions(&gitcl)
1184 .is_err()
1185 );
1186 Ok(())
1187 }
1188
1189 #[test]
1190 #[serial]
1191 fn defaults_on_bad_git_command() -> Result<()> {
1192 let mut gitcl = GitclBuilder::all_git()?;
1193 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1194 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1195 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1196 assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
1197 assert_eq!(11, emitter.cargo_warning().len());
1198 Ok(())
1199 }
1200
1201 #[test]
1202 #[serial]
1203 fn bad_timestamp_defaults() -> Result<()> {
1204 let mut map = BTreeMap::new();
1205 let mut warnings = vec![];
1206 let gitcl = GitclBuilder::all_git()?;
1207 assert!(
1208 gitcl
1209 .add_git_timestamp_entries(
1210 "this_is_not_a_git_cmd",
1211 None,
1212 false,
1213 &mut map,
1214 &mut warnings
1215 )
1216 .is_ok()
1217 );
1218 assert_eq!(2, map.len());
1219 assert_eq!(2, warnings.len());
1220 Ok(())
1221 }
1222
1223 #[test]
1224 #[serial]
1225 fn source_date_epoch_works() {
1226 temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), || {
1227 let result = || -> Result<()> {
1228 let mut stdout_buf = vec![];
1229 let gitcl = GitclBuilder::default()
1230 .commit_date(true)
1231 .commit_timestamp(true)
1232 .build()?;
1233 _ = Emitter::new()
1234 .idempotent()
1235 .add_instructions(&gitcl)?
1236 .emit_to(&mut stdout_buf)?;
1237 let output = String::from_utf8_lossy(&stdout_buf);
1238 for (idx, line) in output.lines().enumerate() {
1239 if idx == 0 {
1240 assert_eq!("cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2022-12-23", line);
1241 } else if idx == 1 {
1242 assert_eq!(
1243 "cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2022-12-23T15:29:20.000000000Z",
1244 line
1245 );
1246 }
1247 }
1248 Ok(())
1249 }();
1250 assert!(result.is_ok());
1251 });
1252 }
1253
1254 #[test]
1255 #[serial]
1256 #[cfg(unix)]
1257 fn bad_source_date_epoch_fails() {
1258 use std::ffi::OsStr;
1259 use std::os::unix::prelude::OsStrExt;
1260
1261 let source = [0x66, 0x6f, 0x80, 0x6f];
1262 let os_str = OsStr::from_bytes(&source[..]);
1263 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1264 let result = || -> Result<bool> {
1265 let mut stdout_buf = vec![];
1266 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1267 Emitter::new()
1268 .idempotent()
1269 .fail_on_error()
1270 .add_instructions(&gitcl)?
1271 .emit_to(&mut stdout_buf)
1272 }();
1273 assert!(result.is_err());
1274 });
1275 }
1276
1277 #[test]
1278 #[serial]
1279 #[cfg(unix)]
1280 fn bad_source_date_epoch_defaults() {
1281 use std::ffi::OsStr;
1282 use std::os::unix::prelude::OsStrExt;
1283
1284 let source = [0x66, 0x6f, 0x80, 0x6f];
1285 let os_str = OsStr::from_bytes(&source[..]);
1286 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1287 let result = || -> Result<bool> {
1288 let mut stdout_buf = vec![];
1289 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1290 Emitter::new()
1291 .idempotent()
1292 .add_instructions(&gitcl)?
1293 .emit_to(&mut stdout_buf)
1294 }();
1295 assert!(result.is_ok());
1296 });
1297 }
1298
1299 #[test]
1300 #[serial]
1301 #[cfg(windows)]
1302 fn bad_source_date_epoch_fails() {
1303 use std::ffi::OsString;
1304 use std::os::windows::prelude::OsStringExt;
1305
1306 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1307 let os_string = OsString::from_wide(&source[..]);
1308 let os_str = os_string.as_os_str();
1309 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1310 let result = || -> Result<bool> {
1311 let mut stdout_buf = vec![];
1312 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1313 Emitter::new()
1314 .fail_on_error()
1315 .idempotent()
1316 .add_instructions(&gitcl)?
1317 .emit_to(&mut stdout_buf)
1318 }();
1319 assert!(result.is_err());
1320 });
1321 }
1322
1323 #[test]
1324 #[serial]
1325 #[cfg(windows)]
1326 fn bad_source_date_epoch_defaults() {
1327 use std::ffi::OsString;
1328 use std::os::windows::prelude::OsStringExt;
1329
1330 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1331 let os_string = OsString::from_wide(&source[..]);
1332 let os_str = os_string.as_os_str();
1333 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1334 let result = || -> Result<bool> {
1335 let mut stdout_buf = vec![];
1336 let gitcl = GitclBuilder::default().commit_date(true).build()?;
1337 Emitter::new()
1338 .idempotent()
1339 .add_instructions(&gitcl)?
1340 .emit_to(&mut stdout_buf)
1341 }();
1342 assert!(result.is_ok());
1343 });
1344 }
1345
1346 #[test]
1347 #[serial]
1348 #[cfg(unix)]
1349 fn git_no_index_update() -> Result<()> {
1350 let repo = TestRepos::new(true, true, false)?;
1351 repo.set_index_magic_mtime()?;
1352
1353 let mut gitcl = GitclBuilder::default()
1355 .all()
1356 .describe(true, true, None)
1357 .build()?;
1358 let _ = gitcl.at_path(repo.path());
1359 let failed = Emitter::default()
1360 .add_instructions(&gitcl)?
1361 .emit_to(&mut stdout())?;
1362 assert!(!failed);
1363
1364 assert_eq!(*TEST_MTIME, repo.get_index_magic_mtime()?);
1365 Ok(())
1366 }
1367}