1use self::gitcl_builder::Empty;
10use anyhow::{Error, Result, anyhow};
11use bon::Builder;
12use std::{
13 env::{self, VarError},
14 path::PathBuf,
15 process::{Command, Output, Stdio},
16 str::FromStr,
17};
18use time::{
19 OffsetDateTime, UtcOffset,
20 format_description::{
21 self,
22 well_known::{Iso8601, Rfc3339},
23 },
24};
25use vergen_lib::{
26 AddEntries, CargoRerunIfChanged, CargoRustcEnvMap, CargoWarning, DefaultConfig, Describe,
27 Dirty, Sha, VergenKey, add_default_map_entry, add_map_entry,
28 constants::{
29 GIT_BRANCH_NAME, GIT_COMMIT_AUTHOR_EMAIL, GIT_COMMIT_AUTHOR_NAME, GIT_COMMIT_COUNT,
30 GIT_COMMIT_DATE_NAME, GIT_COMMIT_MESSAGE, GIT_COMMIT_TIMESTAMP_NAME, GIT_DESCRIBE_NAME,
31 GIT_DIRTY_NAME, GIT_SHA_NAME,
32 },
33};
34
35macro_rules! branch_cmd {
37 () => {
38 "git rev-parse --abbrev-ref --symbolic-full-name HEAD"
39 };
40}
41const BRANCH_CMD: &str = branch_cmd!();
42macro_rules! author_email {
43 () => {
44 "git log -1 --pretty=format:'%ae'"
45 };
46}
47const COMMIT_AUTHOR_EMAIL: &str = author_email!();
48macro_rules! author_name {
49 () => {
50 "git log -1 --pretty=format:'%an'"
51 };
52}
53const COMMIT_AUTHOR_NAME: &str = author_name!();
54macro_rules! commit_count {
55 () => {
56 "git rev-list --count HEAD"
57 };
58}
59const COMMIT_COUNT: &str = commit_count!();
60macro_rules! commit_date {
61 () => {
62 "git log -1 --pretty=format:'%cs'"
63 };
64}
65macro_rules! commit_message {
66 () => {
67 "git log -1 --format=%s"
68 };
69}
70const COMMIT_MESSAGE: &str = commit_message!();
71macro_rules! commit_timestamp {
72 () => {
73 "git log -1 --pretty=format:'%cI'"
74 };
75}
76const COMMIT_TIMESTAMP: &str = commit_timestamp!();
77macro_rules! describe {
78 () => {
79 "git describe --always"
80 };
81}
82const DESCRIBE: &str = describe!();
83macro_rules! sha {
84 () => {
85 "git rev-parse"
86 };
87}
88const SHA: &str = sha!();
89macro_rules! dirty {
90 () => {
91 "git status --porcelain"
92 };
93}
94const DIRTY: &str = dirty!();
95
96#[derive(Builder, Clone, Debug, PartialEq)]
236#[allow(clippy::struct_excessive_bools)]
237pub struct Gitcl {
238 #[builder(field)]
242 all: bool,
243 #[builder(into)]
245 repo_path: Option<PathBuf>,
246 #[builder(default = all)]
253 branch: bool,
254 #[builder(default = all)]
261 commit_author_name: bool,
262 #[builder(default = all)]
269 commit_author_email: bool,
270 #[builder(default = all)]
276 commit_count: bool,
277 #[builder(default = all)]
284 commit_message: bool,
285 #[doc = concat!(commit_date!())]
294 #[builder(default = all)]
296 commit_date: bool,
297 #[builder(default = all)]
304 commit_timestamp: bool,
305 #[builder(
323 required,
324 default = all.then(|| Describe::builder().build()),
325 with = |tags: bool, dirty: bool, match_pattern: Option<&'static str>| {
326 Some(Describe::builder().tags(tags).dirty(dirty).maybe_match_pattern(match_pattern).build())
327 }
328 )]
329 describe: Option<Describe>,
330 #[builder(
342 required,
343 default = all.then(|| Sha::builder().build()),
344 with = |short: bool| Some(Sha::builder().short(short).build())
345 )]
346 sha: Option<Sha>,
347 #[builder(
357 required,
358 default = all.then(|| Dirty::builder().build()),
359 with = |include_untracked: bool| Some(Dirty::builder().include_untracked(include_untracked).build())
360 )]
361 dirty: Option<Dirty>,
362 #[builder(default = false)]
364 use_local: bool,
365 git_cmd: Option<&'static str>,
367}
368
369impl<S: gitcl_builder::State> GitclBuilder<S> {
370 fn all(mut self) -> Self {
375 self.all = true;
376 self
377 }
378}
379
380impl Gitcl {
381 #[must_use]
383 pub fn all_git() -> Gitcl {
384 Self::builder().all().build()
385 }
386
387 pub fn all() -> GitclBuilder<Empty> {
389 Self::builder().all()
390 }
391
392 fn any(&self) -> bool {
393 self.branch
394 || self.commit_author_email
395 || self.commit_author_name
396 || self.commit_count
397 || self.commit_date
398 || self.commit_message
399 || self.commit_timestamp
400 || self.describe.is_some()
401 || self.sha.is_some()
402 || self.dirty.is_some()
403 }
404
405 pub fn at_path(&mut self, path: PathBuf) -> &mut Self {
407 self.repo_path = Some(path);
408 self
409 }
410
411 pub fn git_cmd(&mut self, cmd: Option<&'static str>) -> &mut Self {
420 self.git_cmd = cmd;
421 self
422 }
423
424 fn check_git(cmd: &str) -> Result<()> {
425 if Self::git_cmd_exists(cmd) {
426 Ok(())
427 } else {
428 Err(anyhow!("no suitable 'git' command found!"))
429 }
430 }
431
432 fn check_inside_git_worktree(path: Option<&PathBuf>) -> Result<()> {
433 if Self::inside_git_worktree(path) {
434 Ok(())
435 } else {
436 Err(anyhow!("not within a suitable 'git' worktree!"))
437 }
438 }
439
440 fn git_cmd_exists(cmd: &str) -> bool {
441 Self::run_cmd(cmd, None)
442 .map(|output| output.status.success())
443 .unwrap_or(false)
444 }
445
446 fn inside_git_worktree(path: Option<&PathBuf>) -> bool {
447 Self::run_cmd("git rev-parse --is-inside-work-tree", path)
448 .map(|output| {
449 let stdout = String::from_utf8_lossy(&output.stdout);
450 output.status.success() && stdout.contains("true")
451 })
452 .unwrap_or(false)
453 }
454
455 #[cfg(not(target_env = "msvc"))]
456 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
457 let shell = if let Some(shell_path) = env::var_os("SHELL") {
458 shell_path.to_string_lossy().into_owned()
459 } else {
460 "sh".to_string()
462 };
463 let mut cmd = Command::new(shell);
464 if let Some(path) = path_opt {
465 _ = cmd.current_dir(path);
466 }
467 _ = cmd.env("GIT_OPTIONAL_LOCKS", "0");
469 _ = cmd.arg("-c");
470 _ = cmd.arg(command);
471 _ = cmd.stdout(Stdio::piped());
472 _ = cmd.stderr(Stdio::piped());
473
474 let output = cmd.output()?;
475 if !output.status.success() {
476 eprintln!("Command failed: `{command}`");
477 eprintln!("--- stdout:\n{}\n", String::from_utf8_lossy(&output.stdout));
478 eprintln!("--- stderr:\n{}\n", String::from_utf8_lossy(&output.stderr));
479 }
480
481 Ok(output)
482 }
483
484 #[cfg(target_env = "msvc")]
485 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
486 let mut cmd = Command::new("cmd");
487 if let Some(path) = path_opt {
488 _ = cmd.current_dir(path);
489 }
490 _ = cmd.env("GIT_OPTIONAL_LOCKS", "0");
492 _ = cmd.arg("/c");
493 _ = cmd.arg(command);
494 _ = cmd.stdout(Stdio::piped());
495 _ = cmd.stderr(Stdio::piped());
496
497 let output = cmd.output()?;
498 if !output.status.success() {
499 eprintln!("Command failed: `{command}`");
500 eprintln!("--- stdout:\n{}\n", String::from_utf8_lossy(&output.stdout));
501 eprintln!("--- stderr:\n{}\n", String::from_utf8_lossy(&output.stderr));
502 }
503
504 Ok(output)
505 }
506
507 fn run_cmd_checked(command: &str, path_opt: Option<&PathBuf>) -> Result<Vec<u8>> {
508 let output = Self::run_cmd(command, path_opt)?;
509 if output.status.success() {
510 Ok(output.stdout)
511 } else {
512 let stderr = String::from_utf8_lossy(&output.stderr);
513 Err(anyhow!("Failed to run '{command}'! {stderr}"))
514 }
515 }
516
517 #[allow(clippy::too_many_lines)]
518 fn inner_add_git_map_entries(
519 &self,
520 idempotent: bool,
521 cargo_rustc_env: &mut CargoRustcEnvMap,
522 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
523 cargo_warning: &mut CargoWarning,
524 ) -> Result<()> {
525 if !idempotent && self.any() {
526 Self::add_rerun_if_changed(cargo_rerun_if_changed, self.repo_path.as_ref())?;
527 }
528
529 if self.branch {
530 if let Ok(_value) = env::var(GIT_BRANCH_NAME) {
531 add_default_map_entry(
532 idempotent,
533 VergenKey::GitBranch,
534 cargo_rustc_env,
535 cargo_warning,
536 );
537 } else {
538 Self::add_git_cmd_entry(
539 BRANCH_CMD,
540 self.repo_path.as_ref(),
541 VergenKey::GitBranch,
542 cargo_rustc_env,
543 )?;
544 }
545 }
546
547 if self.commit_author_email {
548 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_EMAIL) {
549 add_default_map_entry(
550 idempotent,
551 VergenKey::GitCommitAuthorEmail,
552 cargo_rustc_env,
553 cargo_warning,
554 );
555 } else {
556 Self::add_git_cmd_entry(
557 COMMIT_AUTHOR_EMAIL,
558 self.repo_path.as_ref(),
559 VergenKey::GitCommitAuthorEmail,
560 cargo_rustc_env,
561 )?;
562 }
563 }
564
565 if self.commit_author_name {
566 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_NAME) {
567 add_default_map_entry(
568 idempotent,
569 VergenKey::GitCommitAuthorName,
570 cargo_rustc_env,
571 cargo_warning,
572 );
573 } else {
574 Self::add_git_cmd_entry(
575 COMMIT_AUTHOR_NAME,
576 self.repo_path.as_ref(),
577 VergenKey::GitCommitAuthorName,
578 cargo_rustc_env,
579 )?;
580 }
581 }
582
583 if self.commit_count {
584 if let Ok(_value) = env::var(GIT_COMMIT_COUNT) {
585 add_default_map_entry(
586 idempotent,
587 VergenKey::GitCommitCount,
588 cargo_rustc_env,
589 cargo_warning,
590 );
591 } else {
592 Self::add_git_cmd_entry(
593 COMMIT_COUNT,
594 self.repo_path.as_ref(),
595 VergenKey::GitCommitCount,
596 cargo_rustc_env,
597 )?;
598 }
599 }
600
601 self.add_git_timestamp_entries(
602 COMMIT_TIMESTAMP,
603 self.repo_path.as_ref(),
604 idempotent,
605 cargo_rustc_env,
606 cargo_warning,
607 )?;
608
609 if self.commit_message {
610 if let Ok(_value) = env::var(GIT_COMMIT_MESSAGE) {
611 add_default_map_entry(
612 idempotent,
613 VergenKey::GitCommitMessage,
614 cargo_rustc_env,
615 cargo_warning,
616 );
617 } else {
618 Self::add_git_cmd_entry(
619 COMMIT_MESSAGE,
620 self.repo_path.as_ref(),
621 VergenKey::GitCommitMessage,
622 cargo_rustc_env,
623 )?;
624 }
625 }
626
627 let mut dirty_cache = None; if let Some(dirty) = self.dirty {
629 if let Ok(_value) = env::var(GIT_DIRTY_NAME) {
630 add_default_map_entry(
631 idempotent,
632 VergenKey::GitDirty,
633 cargo_rustc_env,
634 cargo_warning,
635 );
636 } else {
637 let use_dirty = self.compute_dirty(dirty.include_untracked())?;
638 if !dirty.include_untracked() {
639 dirty_cache = Some(use_dirty);
640 }
641 add_map_entry(
642 VergenKey::GitDirty,
643 bool::to_string(&use_dirty),
644 cargo_rustc_env,
645 );
646 }
647 }
648
649 if let Some(describe) = self.describe {
650 if let Ok(_value) = env::var(GIT_DESCRIBE_NAME) {
655 add_default_map_entry(
656 idempotent,
657 VergenKey::GitDescribe,
658 cargo_rustc_env,
659 cargo_warning,
660 );
661 } else {
662 let mut describe_cmd = String::from(DESCRIBE);
663 if describe.tags() {
664 describe_cmd.push_str(" --tags");
665 }
666 if let Some(pattern) = *describe.match_pattern() {
667 Self::match_pattern_cmd_str(&mut describe_cmd, pattern);
668 }
669 let stdout = Self::run_cmd_checked(&describe_cmd, self.repo_path.as_ref())?;
670 let mut describe_value = String::from_utf8_lossy(&stdout).trim().to_string();
671 if describe.dirty()
672 && (dirty_cache.is_some_and(|dirty| dirty) || self.compute_dirty(false)?)
673 {
674 describe_value.push_str("-dirty");
675 }
676 add_map_entry(VergenKey::GitDescribe, describe_value, cargo_rustc_env);
677 }
678 }
679
680 if let Some(sha) = self.sha {
681 if let Ok(_value) = env::var(GIT_SHA_NAME) {
682 add_default_map_entry(
683 idempotent,
684 VergenKey::GitSha,
685 cargo_rustc_env,
686 cargo_warning,
687 );
688 } else {
689 let mut sha_cmd = String::from(SHA);
690 if sha.short() {
691 sha_cmd.push_str(" --short");
692 }
693 sha_cmd.push_str(" HEAD");
694 Self::add_git_cmd_entry(
695 &sha_cmd,
696 self.repo_path.as_ref(),
697 VergenKey::GitSha,
698 cargo_rustc_env,
699 )?;
700 }
701 }
702
703 Ok(())
704 }
705
706 fn add_rerun_if_changed(
707 rerun_if_changed: &mut Vec<String>,
708 path: Option<&PathBuf>,
709 ) -> Result<()> {
710 let git_path = Self::run_cmd("git rev-parse --git-dir", path)?;
711 if git_path.status.success() {
712 let git_path_str = String::from_utf8_lossy(&git_path.stdout).trim().to_string();
713 let git_path = PathBuf::from(&git_path_str);
714
715 let mut head_path = git_path.clone();
717 head_path.push("HEAD");
718
719 if head_path.exists() {
720 rerun_if_changed.push(format!("{}", head_path.display()));
721 }
722
723 let refp = Self::setup_ref_path(path)?;
725 if refp.status.success() {
726 let ref_path_str = String::from_utf8_lossy(&refp.stdout).trim().to_string();
727 let mut ref_path = git_path;
728 ref_path.push(ref_path_str);
729 if ref_path.exists() {
730 rerun_if_changed.push(format!("{}", ref_path.display()));
731 }
732 }
733 }
734 Ok(())
735 }
736
737 #[cfg(not(target_os = "windows"))]
738 fn match_pattern_cmd_str(describe_cmd: &mut String, pattern: &str) {
739 describe_cmd.push_str(" --match \"");
740 describe_cmd.push_str(pattern);
741 describe_cmd.push('\"');
742 }
743
744 #[cfg(target_os = "windows")]
745 fn match_pattern_cmd_str(describe_cmd: &mut String, pattern: &str) {
746 describe_cmd.push_str(" --match ");
747 describe_cmd.push_str(pattern);
748 }
749
750 #[cfg(not(test))]
751 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
752 Self::run_cmd("git symbolic-ref HEAD", path)
753 }
754
755 #[cfg(all(test, not(target_os = "windows")))]
756 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
757 Self::run_cmd("pwd", path)
758 }
759
760 #[cfg(all(test, target_os = "windows"))]
761 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
762 Self::run_cmd("cd", path)
763 }
764
765 fn add_git_cmd_entry(
766 cmd: &str,
767 path: Option<&PathBuf>,
768 key: VergenKey,
769 cargo_rustc_env: &mut CargoRustcEnvMap,
770 ) -> Result<()> {
771 let stdout = Self::run_cmd_checked(cmd, path)?;
772 let stdout = String::from_utf8_lossy(&stdout)
773 .trim()
774 .trim_matches('\'')
775 .to_string();
776 add_map_entry(key, stdout, cargo_rustc_env);
777 Ok(())
778 }
779
780 fn add_git_timestamp_entries(
781 &self,
782 cmd: &str,
783 path: Option<&PathBuf>,
784 idempotent: bool,
785 cargo_rustc_env: &mut CargoRustcEnvMap,
786 cargo_warning: &mut CargoWarning,
787 ) -> Result<()> {
788 let mut date_override = false;
789 if let Ok(_value) = env::var(GIT_COMMIT_DATE_NAME) {
790 add_default_map_entry(
791 idempotent,
792 VergenKey::GitCommitDate,
793 cargo_rustc_env,
794 cargo_warning,
795 );
796 date_override = true;
797 }
798
799 let mut timestamp_override = false;
800 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
801 add_default_map_entry(
802 idempotent,
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(
832 idempotent,
833 VergenKey::GitCommitDate,
834 cargo_rustc_env,
835 cargo_warning,
836 );
837 }
838
839 if self.commit_timestamp && !timestamp_override {
840 add_default_map_entry(
841 idempotent,
842 VergenKey::GitCommitTimestamp,
843 cargo_rustc_env,
844 cargo_warning,
845 );
846 }
847 } else {
848 if self.commit_date && !date_override {
849 let format = format_description::parse("[year]-[month]-[day]")?;
850 add_map_entry(
851 VergenKey::GitCommitDate,
852 ts.format(&format)?,
853 cargo_rustc_env,
854 );
855 }
856
857 if self.commit_timestamp && !timestamp_override {
858 add_map_entry(
859 VergenKey::GitCommitTimestamp,
860 ts.format(&Iso8601::DEFAULT)?,
861 cargo_rustc_env,
862 );
863 }
864 }
865 } else {
866 if self.commit_date && !date_override {
867 add_default_map_entry(
868 idempotent,
869 VergenKey::GitCommitDate,
870 cargo_rustc_env,
871 cargo_warning,
872 );
873 }
874
875 if self.commit_timestamp && !timestamp_override {
876 add_default_map_entry(
877 idempotent,
878 VergenKey::GitCommitTimestamp,
879 cargo_rustc_env,
880 cargo_warning,
881 );
882 }
883 }
884
885 Ok(())
886 }
887
888 #[cfg_attr(coverage_nightly, coverage(off))]
889 fn compute_local_offset(&self, stdout: &str) -> Result<(bool, OffsetDateTime)> {
891 let no_offset = OffsetDateTime::parse(stdout, &Rfc3339)?;
892 if self.use_local {
893 let local = UtcOffset::local_offset_at(no_offset)?;
894 let local_offset = no_offset.checked_to_offset(local).unwrap_or(no_offset);
895 Ok((false, local_offset))
896 } else {
897 Ok((false, no_offset))
898 }
899 }
900
901 fn compute_dirty(&self, include_untracked: bool) -> Result<bool> {
902 let mut dirty_cmd = String::from(DIRTY);
903 if !include_untracked {
904 dirty_cmd.push_str(" --untracked-files=no");
905 }
906 let stdout = Self::run_cmd_checked(&dirty_cmd, self.repo_path.as_ref())?;
907 Ok(!stdout.is_empty())
908 }
909}
910
911impl AddEntries for Gitcl {
912 fn add_map_entries(
913 &self,
914 idempotent: bool,
915 cargo_rustc_env: &mut CargoRustcEnvMap,
916 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
917 cargo_warning: &mut CargoWarning,
918 ) -> Result<()> {
919 if self.any() {
920 let git_cmd = self.git_cmd.unwrap_or("git --version");
921 Self::check_git(git_cmd)
922 .and_then(|()| Self::check_inside_git_worktree(self.repo_path.as_ref()))?;
923 self.inner_add_git_map_entries(
924 idempotent,
925 cargo_rustc_env,
926 cargo_rerun_if_changed,
927 cargo_warning,
928 )?;
929 }
930 Ok(())
931 }
932
933 fn add_default_entries(
934 &self,
935 config: &DefaultConfig,
936 cargo_rustc_env_map: &mut CargoRustcEnvMap,
937 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
938 cargo_warning: &mut CargoWarning,
939 ) -> Result<()> {
940 if *config.fail_on_error() {
941 let error = Error::msg(format!("{}", config.error()));
942 Err(error)
943 } else {
944 cargo_warning.clear();
947 cargo_rerun_if_changed.clear();
948
949 cargo_warning.push(format!("{}", config.error()));
950
951 if self.branch {
952 add_default_map_entry(
953 *config.idempotent(),
954 VergenKey::GitBranch,
955 cargo_rustc_env_map,
956 cargo_warning,
957 );
958 }
959 if self.commit_author_email {
960 add_default_map_entry(
961 *config.idempotent(),
962 VergenKey::GitCommitAuthorEmail,
963 cargo_rustc_env_map,
964 cargo_warning,
965 );
966 }
967 if self.commit_author_name {
968 add_default_map_entry(
969 *config.idempotent(),
970 VergenKey::GitCommitAuthorName,
971 cargo_rustc_env_map,
972 cargo_warning,
973 );
974 }
975 if self.commit_count {
976 add_default_map_entry(
977 *config.idempotent(),
978 VergenKey::GitCommitCount,
979 cargo_rustc_env_map,
980 cargo_warning,
981 );
982 }
983 if self.commit_date {
984 add_default_map_entry(
985 *config.idempotent(),
986 VergenKey::GitCommitDate,
987 cargo_rustc_env_map,
988 cargo_warning,
989 );
990 }
991 if self.commit_message {
992 add_default_map_entry(
993 *config.idempotent(),
994 VergenKey::GitCommitMessage,
995 cargo_rustc_env_map,
996 cargo_warning,
997 );
998 }
999 if self.commit_timestamp {
1000 add_default_map_entry(
1001 *config.idempotent(),
1002 VergenKey::GitCommitTimestamp,
1003 cargo_rustc_env_map,
1004 cargo_warning,
1005 );
1006 }
1007 if self.describe.is_some() {
1008 add_default_map_entry(
1009 *config.idempotent(),
1010 VergenKey::GitDescribe,
1011 cargo_rustc_env_map,
1012 cargo_warning,
1013 );
1014 }
1015 if self.sha.is_some() {
1016 add_default_map_entry(
1017 *config.idempotent(),
1018 VergenKey::GitSha,
1019 cargo_rustc_env_map,
1020 cargo_warning,
1021 );
1022 }
1023 if self.dirty.is_some() {
1024 add_default_map_entry(
1025 *config.idempotent(),
1026 VergenKey::GitDirty,
1027 cargo_rustc_env_map,
1028 cargo_warning,
1029 );
1030 }
1031 Ok(())
1032 }
1033 }
1034}
1035
1036#[cfg(test)]
1037mod test {
1038 use super::Gitcl;
1039 use crate::Emitter;
1040 use anyhow::Result;
1041 use serial_test::serial;
1042 #[cfg(unix)]
1043 use std::io::stdout;
1044 use std::{collections::BTreeMap, env::temp_dir, io::Write};
1045 #[cfg(unix)]
1046 use test_util::TEST_MTIME;
1047 use test_util::TestRepos;
1048 use vergen_lib::{VergenKey, count_idempotent};
1049
1050 #[test]
1051 #[serial]
1052 #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
1053 fn gitcl_clone_works() {
1054 let gitcl = Gitcl::all_git();
1055 let another = gitcl.clone();
1056 assert_eq!(another, gitcl);
1057 }
1058
1059 #[test]
1060 #[serial]
1061 fn gitcl_debug_works() -> Result<()> {
1062 let gitcl = Gitcl::all_git();
1063 let mut buf = vec![];
1064 write!(buf, "{gitcl:?}")?;
1065 assert!(!buf.is_empty());
1066 Ok(())
1067 }
1068
1069 #[test]
1070 #[serial]
1071 fn gix_default() -> Result<()> {
1072 let gitcl = Gitcl::builder().build();
1073 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1074 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1075 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1076 assert_eq!(0, emitter.cargo_warning().len());
1077 Ok(())
1078 }
1079
1080 #[test]
1081 #[serial]
1082 fn bad_command_is_error() -> Result<()> {
1083 let mut map = BTreeMap::new();
1084 assert!(
1085 Gitcl::add_git_cmd_entry(
1086 "such_a_terrible_cmd",
1087 None,
1088 VergenKey::GitCommitMessage,
1089 &mut map
1090 )
1091 .is_err()
1092 );
1093 Ok(())
1094 }
1095
1096 #[test]
1097 #[serial]
1098 fn non_working_tree_is_error() -> Result<()> {
1099 assert!(Gitcl::check_inside_git_worktree(Some(&temp_dir())).is_err());
1100 Ok(())
1101 }
1102
1103 #[test]
1104 #[serial]
1105 fn invalid_git_is_error() -> Result<()> {
1106 assert!(Gitcl::check_git("such_a_terrible_cmd -v").is_err());
1107 Ok(())
1108 }
1109
1110 #[cfg(not(target_family = "windows"))]
1111 #[test]
1112 #[serial]
1113 fn shell_env_works() -> Result<()> {
1114 temp_env::with_var("SHELL", Some("bash"), || {
1115 let mut map = BTreeMap::new();
1116 assert!(
1117 Gitcl::add_git_cmd_entry("git -v", None, VergenKey::GitCommitMessage, &mut map)
1118 .is_ok()
1119 );
1120 });
1121 Ok(())
1122 }
1123
1124 #[test]
1125 #[serial]
1126 fn git_all_idempotent() -> Result<()> {
1127 let gitcl = Gitcl::all_git();
1128 let emitter = Emitter::default()
1129 .idempotent()
1130 .add_instructions(&gitcl)?
1131 .test_emit();
1132 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1133 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1134 assert_eq!(2, emitter.cargo_warning().len());
1135 Ok(())
1136 }
1137
1138 #[test]
1139 #[serial]
1140 fn git_all_idempotent_no_warn() -> Result<()> {
1141 let gitcl = Gitcl::all_git();
1142 let emitter = Emitter::default()
1143 .idempotent()
1144 .quiet()
1145 .add_instructions(&gitcl)?
1146 .test_emit();
1147 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1148 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1149 assert_eq!(2, emitter.cargo_warning().len());
1150 Ok(())
1151 }
1152
1153 #[test]
1154 #[serial]
1155 fn git_all_at_path() -> Result<()> {
1156 let repo = TestRepos::new(false, false, false)?;
1157 let mut gitcl = Gitcl::all_git();
1158 let _ = gitcl.at_path(repo.path());
1159 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1160 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1161 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1162 assert_eq!(0, emitter.cargo_warning().len());
1163 Ok(())
1164 }
1165
1166 #[test]
1167 #[serial]
1168 fn git_all() -> Result<()> {
1169 let gitcl = Gitcl::all_git();
1170 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1171 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1172 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1173 assert_eq!(0, emitter.cargo_warning().len());
1174 Ok(())
1175 }
1176
1177 #[test]
1178 #[serial]
1179 fn git_all_shallow_clone() -> Result<()> {
1180 let repo = TestRepos::new(false, false, true)?;
1181 let mut gitcl = Gitcl::all_git();
1182 let _ = gitcl.at_path(repo.path());
1183 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1184 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1185 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1186 assert_eq!(0, emitter.cargo_warning().len());
1187 Ok(())
1188 }
1189
1190 #[test]
1191 #[serial]
1192 fn git_all_dirty_tags_short() -> Result<()> {
1193 let gitcl = Gitcl::builder()
1194 .all()
1195 .describe(true, true, None)
1196 .sha(true)
1197 .build();
1198 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1199 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1200 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1201 assert_eq!(0, emitter.cargo_warning().len());
1202 Ok(())
1203 }
1204
1205 #[test]
1206 #[serial]
1207 fn fails_on_bad_git_command() -> Result<()> {
1208 let mut gitcl = Gitcl::all_git();
1209 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1210 assert!(
1211 Emitter::default()
1212 .fail_on_error()
1213 .add_instructions(&gitcl)
1214 .is_err()
1215 );
1216 Ok(())
1217 }
1218
1219 #[test]
1220 #[serial]
1221 fn defaults_on_bad_git_command() -> Result<()> {
1222 let mut gitcl = Gitcl::all_git();
1223 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1224 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1225 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1226 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1227 assert_eq!(11, emitter.cargo_warning().len());
1228 Ok(())
1229 }
1230
1231 #[test]
1232 #[serial]
1233 fn idempotent_on_bad_git_command() -> Result<()> {
1234 let mut gitcl = Gitcl::all_git();
1235 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1236 let emitter = Emitter::default()
1237 .idempotent()
1238 .add_instructions(&gitcl)?
1239 .test_emit();
1240 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1241 assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
1242 assert_eq!(11, emitter.cargo_warning().len());
1243 Ok(())
1244 }
1245
1246 #[test]
1247 #[serial]
1248 fn bad_timestamp_defaults() -> Result<()> {
1249 let mut map = BTreeMap::new();
1250 let mut warnings = vec![];
1251 let gitcl = Gitcl::all_git();
1252 assert!(
1253 gitcl
1254 .add_git_timestamp_entries(
1255 "this_is_not_a_git_cmd",
1256 None,
1257 false,
1258 &mut map,
1259 &mut warnings
1260 )
1261 .is_ok()
1262 );
1263 assert_eq!(0, map.len());
1264 assert_eq!(2, warnings.len());
1265 Ok(())
1266 }
1267
1268 #[test]
1269 #[serial]
1270 fn bad_timestamp_idempotent() -> Result<()> {
1271 let mut map = BTreeMap::new();
1272 let mut warnings = vec![];
1273 let gitcl = Gitcl::all_git();
1274 assert!(
1275 gitcl
1276 .add_git_timestamp_entries(
1277 "this_is_not_a_git_cmd",
1278 None,
1279 true,
1280 &mut map,
1281 &mut warnings
1282 )
1283 .is_ok()
1284 );
1285 assert_eq!(2, map.len());
1286 assert_eq!(2, warnings.len());
1287 Ok(())
1288 }
1289
1290 #[test]
1291 #[serial]
1292 fn source_date_epoch_works() {
1293 temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), || {
1294 let result = || -> Result<()> {
1295 let mut stdout_buf = vec![];
1296 let gitcl = Gitcl::builder()
1297 .commit_date(true)
1298 .commit_timestamp(true)
1299 .build();
1300 _ = Emitter::new()
1301 .idempotent()
1302 .add_instructions(&gitcl)?
1303 .emit_to(&mut stdout_buf)?;
1304 let output = String::from_utf8_lossy(&stdout_buf);
1305 for (idx, line) in output.lines().enumerate() {
1306 if idx == 0 {
1307 assert_eq!("cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2022-12-23", line);
1308 } else if idx == 1 {
1309 assert_eq!(
1310 "cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2022-12-23T15:29:20.000000000Z",
1311 line
1312 );
1313 }
1314 }
1315 Ok(())
1316 }();
1317 assert!(result.is_ok());
1318 });
1319 }
1320
1321 #[test]
1322 #[serial]
1323 #[cfg(unix)]
1324 fn bad_source_date_epoch_fails() {
1325 use std::ffi::OsStr;
1326 use std::os::unix::prelude::OsStrExt;
1327
1328 let source = [0x66, 0x6f, 0x80, 0x6f];
1329 let os_str = OsStr::from_bytes(&source[..]);
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 = Gitcl::builder().commit_date(true).build();
1334 Emitter::new()
1335 .idempotent()
1336 .fail_on_error()
1337 .add_instructions(&gitcl)?
1338 .emit_to(&mut stdout_buf)
1339 }();
1340 assert!(result.is_err());
1341 });
1342 }
1343
1344 #[test]
1345 #[serial]
1346 #[cfg(unix)]
1347 fn bad_source_date_epoch_defaults() {
1348 use std::ffi::OsStr;
1349 use std::os::unix::prelude::OsStrExt;
1350
1351 let source = [0x66, 0x6f, 0x80, 0x6f];
1352 let os_str = OsStr::from_bytes(&source[..]);
1353 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1354 let result = || -> Result<bool> {
1355 let mut stdout_buf = vec![];
1356 let gitcl = Gitcl::builder().commit_date(true).build();
1357 Emitter::new()
1358 .idempotent()
1359 .add_instructions(&gitcl)?
1360 .emit_to(&mut stdout_buf)
1361 }();
1362 assert!(result.is_ok());
1363 });
1364 }
1365
1366 #[test]
1367 #[serial]
1368 #[cfg(windows)]
1369 fn bad_source_date_epoch_fails() {
1370 use std::ffi::OsString;
1371 use std::os::windows::prelude::OsStringExt;
1372
1373 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1374 let os_string = OsString::from_wide(&source[..]);
1375 let os_str = os_string.as_os_str();
1376 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1377 let result = || -> Result<bool> {
1378 let mut stdout_buf = vec![];
1379 let gitcl = Gitcl::builder().commit_date(true).build();
1380 Emitter::new()
1381 .fail_on_error()
1382 .idempotent()
1383 .add_instructions(&gitcl)?
1384 .emit_to(&mut stdout_buf)
1385 }();
1386 assert!(result.is_err());
1387 });
1388 }
1389
1390 #[test]
1391 #[serial]
1392 #[cfg(windows)]
1393 fn bad_source_date_epoch_defaults() {
1394 use std::ffi::OsString;
1395 use std::os::windows::prelude::OsStringExt;
1396
1397 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1398 let os_string = OsString::from_wide(&source[..]);
1399 let os_str = os_string.as_os_str();
1400 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1401 let result = || -> Result<bool> {
1402 let mut stdout_buf = vec![];
1403 let gitcl = Gitcl::builder().commit_date(true).build();
1404 Emitter::new()
1405 .idempotent()
1406 .add_instructions(&gitcl)?
1407 .emit_to(&mut stdout_buf)
1408 }();
1409 assert!(result.is_ok());
1410 });
1411 }
1412
1413 #[test]
1414 #[serial]
1415 #[cfg(unix)]
1416 fn git_no_index_update() -> Result<()> {
1417 let repo = TestRepos::new(true, true, false)?;
1418 repo.set_index_magic_mtime()?;
1419
1420 let mut gitcl = Gitcl::builder().all().describe(true, true, None).build();
1422 let _ = gitcl.at_path(repo.path());
1423 let failed = Emitter::default()
1424 .add_instructions(&gitcl)?
1425 .emit_to(&mut stdout())?;
1426 assert!(!failed);
1427
1428 assert_eq!(*TEST_MTIME, repo.get_index_magic_mtime()?);
1429 Ok(())
1430 }
1431}