1use self::gitcl_builder::Empty;
10use anyhow::{Error, Result, anyhow};
11use bon::Builder;
12use std::{
13 env::{self, VarError, temp_dir},
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 local_repo_path: Option<PathBuf>,
246 #[builder(default = false)]
248 force_local: bool,
249 #[builder(into)]
251 remote_url: Option<String>,
252 #[builder(into)]
255 remote_repo_path: Option<PathBuf>,
256 #[builder(default = all)]
263 branch: bool,
264 #[builder(default = all)]
271 commit_author_name: bool,
272 #[builder(default = all)]
279 commit_author_email: bool,
280 #[builder(default = all)]
286 commit_count: bool,
287 #[builder(default = all)]
294 commit_message: bool,
295 #[doc = concat!(commit_date!())]
304 #[builder(default = all)]
306 commit_date: bool,
307 #[builder(default = all)]
314 commit_timestamp: bool,
315 #[builder(
333 required,
334 default = all.then(|| Describe::builder().build()),
335 with = |tags: bool, dirty: bool, match_pattern: Option<&'static str>| {
336 Some(Describe::builder().tags(tags).dirty(dirty).maybe_match_pattern(match_pattern).build())
337 }
338 )]
339 describe: Option<Describe>,
340 #[builder(
352 required,
353 default = all.then(|| Sha::builder().build()),
354 with = |short: bool| Some(Sha::builder().short(short).build())
355 )]
356 sha: Option<Sha>,
357 #[builder(
367 required,
368 default = all.then(|| Dirty::builder().build()),
369 with = |include_untracked: bool| Some(Dirty::builder().include_untracked(include_untracked).build())
370 )]
371 dirty: Option<Dirty>,
372 #[builder(default = false)]
374 use_local: bool,
375 git_cmd: Option<&'static str>,
377}
378
379impl<S: gitcl_builder::State> GitclBuilder<S> {
380 fn all(mut self) -> Self {
385 self.all = true;
386 self
387 }
388}
389
390impl Gitcl {
391 #[must_use]
393 pub fn all_git() -> Gitcl {
394 Self::builder().all().build()
395 }
396
397 pub fn all() -> GitclBuilder<Empty> {
399 Self::builder().all()
400 }
401
402 fn any(&self) -> bool {
403 self.branch
404 || self.commit_author_email
405 || self.commit_author_name
406 || self.commit_count
407 || self.commit_date
408 || self.commit_message
409 || self.commit_timestamp
410 || self.describe.is_some()
411 || self.sha.is_some()
412 || self.dirty.is_some()
413 }
414
415 pub fn at_path(&mut self, path: PathBuf) -> &mut Self {
417 self.local_repo_path = Some(path);
418 self
419 }
420
421 pub fn git_cmd(&mut self, cmd: Option<&'static str>) -> &mut Self {
430 self.git_cmd = cmd;
431 self
432 }
433
434 fn check_git(cmd: &str) -> Result<()> {
435 if Self::git_cmd_exists(cmd) {
436 Ok(())
437 } else {
438 Err(anyhow!("no suitable 'git' command found!"))
439 }
440 }
441
442 fn check_inside_git_worktree(path: Option<&PathBuf>) -> Result<()> {
443 if Self::inside_git_worktree(path) {
444 Ok(())
445 } else {
446 Err(anyhow!("not within a suitable 'git' worktree!"))
447 }
448 }
449
450 fn git_cmd_exists(cmd: &str) -> bool {
451 Self::run_cmd(cmd, None)
452 .map(|output| output.status.success())
453 .unwrap_or(false)
454 }
455
456 fn clone(remote_url: &str, path: Option<&PathBuf>) -> bool {
457 Self::run_cmd(&format!("git clone --depth 5 {remote_url} ."), path)
458 .map(|output| output.status.success())
459 .unwrap_or(false)
460 }
461
462 fn inside_git_worktree(path: Option<&PathBuf>) -> bool {
463 Self::run_cmd("git rev-parse --is-inside-work-tree", path)
464 .map(|output| {
465 let stdout = String::from_utf8_lossy(&output.stdout);
466 output.status.success() && stdout.contains("true")
467 })
468 .unwrap_or(false)
469 }
470
471 #[cfg(not(target_env = "msvc"))]
472 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
473 let shell = if let Some(shell_path) = env::var_os("SHELL") {
474 shell_path.to_string_lossy().into_owned()
475 } else {
476 "sh".to_string()
478 };
479 let mut cmd = Command::new(shell);
480 if let Some(path) = path_opt {
481 _ = cmd.current_dir(path);
482 }
483 _ = cmd.env("GIT_OPTIONAL_LOCKS", "0");
485 _ = cmd.arg("-c");
486 _ = cmd.arg(command);
487 _ = cmd.stdout(Stdio::piped());
488 _ = cmd.stderr(Stdio::piped());
489
490 let output = cmd.output()?;
491 if !output.status.success() {
492 eprintln!("Command failed: `{command}`");
493 eprintln!("--- stdout:\n{}\n", String::from_utf8_lossy(&output.stdout));
494 eprintln!("--- stderr:\n{}\n", String::from_utf8_lossy(&output.stderr));
495 }
496
497 Ok(output)
498 }
499
500 #[cfg(target_env = "msvc")]
501 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
502 let mut cmd = Command::new("cmd");
503 if let Some(path) = path_opt {
504 _ = cmd.current_dir(path);
505 }
506 _ = cmd.env("GIT_OPTIONAL_LOCKS", "0");
508 _ = cmd.arg("/c");
509 _ = cmd.arg(command);
510 _ = cmd.stdout(Stdio::piped());
511 _ = cmd.stderr(Stdio::piped());
512
513 let output = cmd.output()?;
514 if !output.status.success() {
515 eprintln!("Command failed: `{command}`");
516 eprintln!("--- stdout:\n{}\n", String::from_utf8_lossy(&output.stdout));
517 eprintln!("--- stderr:\n{}\n", String::from_utf8_lossy(&output.stderr));
518 }
519
520 Ok(output)
521 }
522
523 fn run_cmd_checked(command: &str, path_opt: Option<&PathBuf>) -> Result<Vec<u8>> {
524 let output = Self::run_cmd(command, path_opt)?;
525 if output.status.success() {
526 Ok(output.stdout)
527 } else {
528 let stderr = String::from_utf8_lossy(&output.stderr);
529 Err(anyhow!("Failed to run '{command}'! {stderr}"))
530 }
531 }
532
533 #[allow(clippy::too_many_lines)]
534 fn inner_add_git_map_entries(
535 &self,
536 repo_path: Option<&PathBuf>,
537 idempotent: bool,
538 cargo_rustc_env: &mut CargoRustcEnvMap,
539 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
540 cargo_warning: &mut CargoWarning,
541 ) -> Result<()> {
542 if !idempotent && self.any() {
543 Self::add_rerun_if_changed(cargo_rerun_if_changed, repo_path)?;
544 }
545
546 if self.branch {
547 if let Ok(_value) = env::var(GIT_BRANCH_NAME) {
548 add_default_map_entry(
549 idempotent,
550 VergenKey::GitBranch,
551 cargo_rustc_env,
552 cargo_warning,
553 );
554 } else {
555 Self::add_git_cmd_entry(
556 BRANCH_CMD,
557 repo_path,
558 VergenKey::GitBranch,
559 cargo_rustc_env,
560 )?;
561 }
562 }
563
564 if self.commit_author_email {
565 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_EMAIL) {
566 add_default_map_entry(
567 idempotent,
568 VergenKey::GitCommitAuthorEmail,
569 cargo_rustc_env,
570 cargo_warning,
571 );
572 } else {
573 Self::add_git_cmd_entry(
574 COMMIT_AUTHOR_EMAIL,
575 repo_path,
576 VergenKey::GitCommitAuthorEmail,
577 cargo_rustc_env,
578 )?;
579 }
580 }
581
582 if self.commit_author_name {
583 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_NAME) {
584 add_default_map_entry(
585 idempotent,
586 VergenKey::GitCommitAuthorName,
587 cargo_rustc_env,
588 cargo_warning,
589 );
590 } else {
591 Self::add_git_cmd_entry(
592 COMMIT_AUTHOR_NAME,
593 repo_path,
594 VergenKey::GitCommitAuthorName,
595 cargo_rustc_env,
596 )?;
597 }
598 }
599
600 if self.commit_count {
601 if let Ok(_value) = env::var(GIT_COMMIT_COUNT) {
602 add_default_map_entry(
603 idempotent,
604 VergenKey::GitCommitCount,
605 cargo_rustc_env,
606 cargo_warning,
607 );
608 } else {
609 Self::add_git_cmd_entry(
610 COMMIT_COUNT,
611 repo_path,
612 VergenKey::GitCommitCount,
613 cargo_rustc_env,
614 )?;
615 }
616 }
617
618 self.add_git_timestamp_entries(
619 COMMIT_TIMESTAMP,
620 repo_path,
621 idempotent,
622 cargo_rustc_env,
623 cargo_warning,
624 )?;
625
626 if self.commit_message {
627 if let Ok(_value) = env::var(GIT_COMMIT_MESSAGE) {
628 add_default_map_entry(
629 idempotent,
630 VergenKey::GitCommitMessage,
631 cargo_rustc_env,
632 cargo_warning,
633 );
634 } else {
635 Self::add_git_cmd_entry(
636 COMMIT_MESSAGE,
637 repo_path,
638 VergenKey::GitCommitMessage,
639 cargo_rustc_env,
640 )?;
641 }
642 }
643
644 let mut dirty_cache = None; if let Some(dirty) = self.dirty {
646 if let Ok(_value) = env::var(GIT_DIRTY_NAME) {
647 add_default_map_entry(
648 idempotent,
649 VergenKey::GitDirty,
650 cargo_rustc_env,
651 cargo_warning,
652 );
653 } else {
654 let use_dirty = self.compute_dirty(repo_path, dirty.include_untracked())?;
655 if !dirty.include_untracked() {
656 dirty_cache = Some(use_dirty);
657 }
658 add_map_entry(
659 VergenKey::GitDirty,
660 bool::to_string(&use_dirty),
661 cargo_rustc_env,
662 );
663 }
664 }
665
666 if let Some(describe) = self.describe {
667 if let Ok(_value) = env::var(GIT_DESCRIBE_NAME) {
672 add_default_map_entry(
673 idempotent,
674 VergenKey::GitDescribe,
675 cargo_rustc_env,
676 cargo_warning,
677 );
678 } else {
679 let mut describe_cmd = String::from(DESCRIBE);
680 if describe.tags() {
681 describe_cmd.push_str(" --tags");
682 }
683 if let Some(pattern) = *describe.match_pattern() {
684 Self::match_pattern_cmd_str(&mut describe_cmd, pattern);
685 }
686 let stdout = Self::run_cmd_checked(&describe_cmd, repo_path)?;
687 let mut describe_value = String::from_utf8_lossy(&stdout).trim().to_string();
688 if describe.dirty()
689 && (dirty_cache.is_some_and(|dirty| dirty)
690 || self.compute_dirty(repo_path, false)?)
691 {
692 describe_value.push_str("-dirty");
693 }
694 add_map_entry(VergenKey::GitDescribe, describe_value, cargo_rustc_env);
695 }
696 }
697
698 if let Some(sha) = self.sha {
699 if let Ok(_value) = env::var(GIT_SHA_NAME) {
700 add_default_map_entry(
701 idempotent,
702 VergenKey::GitSha,
703 cargo_rustc_env,
704 cargo_warning,
705 );
706 } else {
707 let mut sha_cmd = String::from(SHA);
708 if sha.short() {
709 sha_cmd.push_str(" --short");
710 }
711 sha_cmd.push_str(" HEAD");
712 Self::add_git_cmd_entry(&sha_cmd, repo_path, VergenKey::GitSha, cargo_rustc_env)?;
713 }
714 }
715
716 Ok(())
717 }
718
719 fn add_rerun_if_changed(
720 rerun_if_changed: &mut Vec<String>,
721 path: Option<&PathBuf>,
722 ) -> Result<()> {
723 let git_path = Self::run_cmd("git rev-parse --git-dir", path)?;
724 if git_path.status.success() {
725 let git_path_str = String::from_utf8_lossy(&git_path.stdout).trim().to_string();
726 let git_path = PathBuf::from(&git_path_str);
727
728 let mut head_path = git_path.clone();
730 head_path.push("HEAD");
731
732 if head_path.exists() {
733 rerun_if_changed.push(format!("{}", head_path.display()));
734 }
735
736 let refp = Self::setup_ref_path(path)?;
738 if refp.status.success() {
739 let ref_path_str = String::from_utf8_lossy(&refp.stdout).trim().to_string();
740 let mut ref_path = git_path;
741 ref_path.push(ref_path_str);
742 if ref_path.exists() {
743 rerun_if_changed.push(format!("{}", ref_path.display()));
744 }
745 }
746 }
747 Ok(())
748 }
749
750 #[cfg(not(target_os = "windows"))]
751 fn match_pattern_cmd_str(describe_cmd: &mut String, pattern: &str) {
752 describe_cmd.push_str(" --match \"");
753 describe_cmd.push_str(pattern);
754 describe_cmd.push('\"');
755 }
756
757 #[cfg(target_os = "windows")]
758 fn match_pattern_cmd_str(describe_cmd: &mut String, pattern: &str) {
759 describe_cmd.push_str(" --match ");
760 describe_cmd.push_str(pattern);
761 }
762
763 #[cfg(not(test))]
764 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
765 Self::run_cmd("git symbolic-ref HEAD", path)
766 }
767
768 #[cfg(all(test, not(target_os = "windows")))]
769 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
770 Self::run_cmd("pwd", path)
771 }
772
773 #[cfg(all(test, target_os = "windows"))]
774 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
775 Self::run_cmd("cd", path)
776 }
777
778 fn add_git_cmd_entry(
779 cmd: &str,
780 path: Option<&PathBuf>,
781 key: VergenKey,
782 cargo_rustc_env: &mut CargoRustcEnvMap,
783 ) -> Result<()> {
784 let stdout = Self::run_cmd_checked(cmd, path)?;
785 let stdout = String::from_utf8_lossy(&stdout)
786 .trim()
787 .trim_matches('\'')
788 .to_string();
789 add_map_entry(key, stdout, cargo_rustc_env);
790 Ok(())
791 }
792
793 fn add_git_timestamp_entries(
794 &self,
795 cmd: &str,
796 path: Option<&PathBuf>,
797 idempotent: bool,
798 cargo_rustc_env: &mut CargoRustcEnvMap,
799 cargo_warning: &mut CargoWarning,
800 ) -> Result<()> {
801 let mut date_override = false;
802 if let Ok(_value) = env::var(GIT_COMMIT_DATE_NAME) {
803 add_default_map_entry(
804 idempotent,
805 VergenKey::GitCommitDate,
806 cargo_rustc_env,
807 cargo_warning,
808 );
809 date_override = true;
810 }
811
812 let mut timestamp_override = false;
813 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
814 add_default_map_entry(
815 idempotent,
816 VergenKey::GitCommitTimestamp,
817 cargo_rustc_env,
818 cargo_warning,
819 );
820 timestamp_override = true;
821 }
822
823 let output = Self::run_cmd(cmd, path)?;
824 if output.status.success() {
825 let stdout = String::from_utf8_lossy(&output.stdout)
826 .lines()
827 .last()
828 .ok_or_else(|| anyhow!("invalid 'git log' output"))?
829 .trim()
830 .trim_matches('\'')
831 .to_string();
832
833 let (sde, ts) = match env::var("SOURCE_DATE_EPOCH") {
834 Ok(v) => (
835 true,
836 OffsetDateTime::from_unix_timestamp(i64::from_str(&v)?)?,
837 ),
838 Err(VarError::NotPresent) => self.compute_local_offset(&stdout)?,
839 Err(e) => return Err(e.into()),
840 };
841
842 if idempotent && !sde {
843 if self.commit_date && !date_override {
844 add_default_map_entry(
845 idempotent,
846 VergenKey::GitCommitDate,
847 cargo_rustc_env,
848 cargo_warning,
849 );
850 }
851
852 if self.commit_timestamp && !timestamp_override {
853 add_default_map_entry(
854 idempotent,
855 VergenKey::GitCommitTimestamp,
856 cargo_rustc_env,
857 cargo_warning,
858 );
859 }
860 } else {
861 if self.commit_date && !date_override {
862 let format = format_description::parse("[year]-[month]-[day]")?;
863 add_map_entry(
864 VergenKey::GitCommitDate,
865 ts.format(&format)?,
866 cargo_rustc_env,
867 );
868 }
869
870 if self.commit_timestamp && !timestamp_override {
871 add_map_entry(
872 VergenKey::GitCommitTimestamp,
873 ts.format(&Iso8601::DEFAULT)?,
874 cargo_rustc_env,
875 );
876 }
877 }
878 } else {
879 if self.commit_date && !date_override {
880 add_default_map_entry(
881 idempotent,
882 VergenKey::GitCommitDate,
883 cargo_rustc_env,
884 cargo_warning,
885 );
886 }
887
888 if self.commit_timestamp && !timestamp_override {
889 add_default_map_entry(
890 idempotent,
891 VergenKey::GitCommitTimestamp,
892 cargo_rustc_env,
893 cargo_warning,
894 );
895 }
896 }
897
898 Ok(())
899 }
900
901 #[cfg_attr(coverage_nightly, coverage(off))]
902 fn compute_local_offset(&self, stdout: &str) -> Result<(bool, OffsetDateTime)> {
904 let no_offset = OffsetDateTime::parse(stdout, &Rfc3339)?;
905 if self.use_local {
906 let local = UtcOffset::local_offset_at(no_offset)?;
907 let local_offset = no_offset.checked_to_offset(local).unwrap_or(no_offset);
908 Ok((false, local_offset))
909 } else {
910 Ok((false, no_offset))
911 }
912 }
913
914 fn compute_dirty(&self, repo_path: Option<&PathBuf>, include_untracked: bool) -> Result<bool> {
915 let mut dirty_cmd = String::from(DIRTY);
916 if !include_untracked {
917 dirty_cmd.push_str(" --untracked-files=no");
918 }
919 let stdout = Self::run_cmd_checked(&dirty_cmd, repo_path)?;
920 Ok(!stdout.is_empty())
921 }
922
923 #[cfg(not(feature = "allow_remote"))]
924 fn cleanup(&self) {}
925
926 #[cfg(feature = "allow_remote")]
927 fn cleanup(&self) {
928 if let Some(_remote_url) = self.remote_url.as_ref() {
929 let temp_dir = temp_dir().join("vergen-gitcl");
930 if let Some(path) = &self.remote_repo_path {
932 if path.exists() {
933 let _ = std::fs::remove_dir_all(path).ok();
934 }
935 } else if temp_dir.exists() {
936 let _ = std::fs::remove_dir_all(temp_dir).ok();
937 }
938 }
939 }
940}
941
942impl AddEntries for Gitcl {
943 fn add_map_entries(
944 &self,
945 idempotent: bool,
946 cargo_rustc_env: &mut CargoRustcEnvMap,
947 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
948 cargo_warning: &mut CargoWarning,
949 ) -> Result<()> {
950 if self.any() {
951 let mut repo_path = if let Some(repo_path) = &self.local_repo_path {
952 repo_path.clone()
953 } else {
954 env::current_dir()?
955 };
956 let git_cmd = self.git_cmd.unwrap_or("git --version");
957 Self::check_git(git_cmd)?;
958
959 if Self::check_inside_git_worktree(Some(&repo_path)).is_err() {
960 if !self.force_local
961 && let Some(remote_url) = &self.remote_url
962 {
963 let remote_path = if let Some(remote_path) = &self.remote_repo_path {
964 remote_path.clone()
965 } else {
966 temp_dir().join("vergen-gitcl")
967 };
968 if !Self::clone(remote_url, Some(&remote_path)) {
969 return Err(anyhow!("Failed to clone git repository"));
970 }
971 cargo_warning.push(format!(
972 "Using remote repository from '{remote_url}' at '{}'",
973 remote_path.display()
974 ));
975 repo_path = remote_path;
976 } else {
977 Self::check_inside_git_worktree(Some(&repo_path))?;
978 cargo_warning.push(format!(
979 "Using local repository at '{}'",
980 repo_path.display()
981 ));
982 }
983 } else {
984 cargo_warning.push(format!(
985 "Using local repository at '{}'",
986 repo_path.display()
987 ));
988 }
989
990 self.inner_add_git_map_entries(
991 Some(&repo_path),
992 idempotent,
993 cargo_rustc_env,
994 cargo_rerun_if_changed,
995 cargo_warning,
996 )?;
997
998 self.cleanup();
999 }
1000 Ok(())
1001 }
1002
1003 fn add_default_entries(
1004 &self,
1005 config: &DefaultConfig,
1006 cargo_rustc_env_map: &mut CargoRustcEnvMap,
1007 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
1008 cargo_warning: &mut CargoWarning,
1009 ) -> Result<()> {
1010 if *config.fail_on_error() {
1011 let error = Error::msg(format!("{}", config.error()));
1012 Err(error)
1013 } else {
1014 cargo_warning.clear();
1017 cargo_rerun_if_changed.clear();
1018
1019 cargo_warning.push(format!("{}", config.error()));
1020
1021 if self.branch {
1022 add_default_map_entry(
1023 *config.idempotent(),
1024 VergenKey::GitBranch,
1025 cargo_rustc_env_map,
1026 cargo_warning,
1027 );
1028 }
1029 if self.commit_author_email {
1030 add_default_map_entry(
1031 *config.idempotent(),
1032 VergenKey::GitCommitAuthorEmail,
1033 cargo_rustc_env_map,
1034 cargo_warning,
1035 );
1036 }
1037 if self.commit_author_name {
1038 add_default_map_entry(
1039 *config.idempotent(),
1040 VergenKey::GitCommitAuthorName,
1041 cargo_rustc_env_map,
1042 cargo_warning,
1043 );
1044 }
1045 if self.commit_count {
1046 add_default_map_entry(
1047 *config.idempotent(),
1048 VergenKey::GitCommitCount,
1049 cargo_rustc_env_map,
1050 cargo_warning,
1051 );
1052 }
1053 if self.commit_date {
1054 add_default_map_entry(
1055 *config.idempotent(),
1056 VergenKey::GitCommitDate,
1057 cargo_rustc_env_map,
1058 cargo_warning,
1059 );
1060 }
1061 if self.commit_message {
1062 add_default_map_entry(
1063 *config.idempotent(),
1064 VergenKey::GitCommitMessage,
1065 cargo_rustc_env_map,
1066 cargo_warning,
1067 );
1068 }
1069 if self.commit_timestamp {
1070 add_default_map_entry(
1071 *config.idempotent(),
1072 VergenKey::GitCommitTimestamp,
1073 cargo_rustc_env_map,
1074 cargo_warning,
1075 );
1076 }
1077 if self.describe.is_some() {
1078 add_default_map_entry(
1079 *config.idempotent(),
1080 VergenKey::GitDescribe,
1081 cargo_rustc_env_map,
1082 cargo_warning,
1083 );
1084 }
1085 if self.sha.is_some() {
1086 add_default_map_entry(
1087 *config.idempotent(),
1088 VergenKey::GitSha,
1089 cargo_rustc_env_map,
1090 cargo_warning,
1091 );
1092 }
1093 if self.dirty.is_some() {
1094 add_default_map_entry(
1095 *config.idempotent(),
1096 VergenKey::GitDirty,
1097 cargo_rustc_env_map,
1098 cargo_warning,
1099 );
1100 }
1101 Ok(())
1102 }
1103 }
1104}
1105
1106#[cfg(test)]
1107mod test {
1108 use super::Gitcl;
1109 use crate::Emitter;
1110 use anyhow::Result;
1111 use serial_test::serial;
1112 #[cfg(unix)]
1113 use std::io::stdout;
1114 use std::{collections::BTreeMap, env::temp_dir, io::Write};
1115 #[cfg(unix)]
1116 use test_util::TEST_MTIME;
1117 use test_util::TestRepos;
1118 use vergen_lib::{VergenKey, count_idempotent};
1119
1120 #[test]
1121 #[serial]
1122 #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
1123 fn gitcl_clone_works() {
1124 let gitcl = Gitcl::all_git();
1125 let another = gitcl.clone();
1126 assert_eq!(another, gitcl);
1127 }
1128
1129 #[test]
1130 #[serial]
1131 fn gitcl_debug_works() -> Result<()> {
1132 let gitcl = Gitcl::all_git();
1133 let mut buf = vec![];
1134 write!(buf, "{gitcl:?}")?;
1135 assert!(!buf.is_empty());
1136 Ok(())
1137 }
1138
1139 #[test]
1140 #[serial]
1141 fn gix_default() -> Result<()> {
1142 let gitcl = Gitcl::builder().build();
1143 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1144 assert_eq!(0, 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 bad_command_is_error() -> Result<()> {
1153 let mut map = BTreeMap::new();
1154 assert!(
1155 Gitcl::add_git_cmd_entry(
1156 "such_a_terrible_cmd",
1157 None,
1158 VergenKey::GitCommitMessage,
1159 &mut map
1160 )
1161 .is_err()
1162 );
1163 Ok(())
1164 }
1165
1166 #[test]
1167 #[serial]
1168 fn non_working_tree_is_error() -> Result<()> {
1169 assert!(Gitcl::check_inside_git_worktree(Some(&temp_dir())).is_err());
1170 Ok(())
1171 }
1172
1173 #[test]
1174 #[serial]
1175 fn invalid_git_is_error() -> Result<()> {
1176 assert!(Gitcl::check_git("such_a_terrible_cmd -v").is_err());
1177 Ok(())
1178 }
1179
1180 #[cfg(not(target_family = "windows"))]
1181 #[test]
1182 #[serial]
1183 fn shell_env_works() -> Result<()> {
1184 temp_env::with_var("SHELL", Some("bash"), || {
1185 let mut map = BTreeMap::new();
1186 assert!(
1187 Gitcl::add_git_cmd_entry("git -v", None, VergenKey::GitCommitMessage, &mut map)
1188 .is_ok()
1189 );
1190 });
1191 Ok(())
1192 }
1193
1194 #[test]
1195 #[serial]
1196 fn git_all_idempotent() -> Result<()> {
1197 let gitcl = Gitcl::all_git();
1198 let emitter = Emitter::default()
1199 .idempotent()
1200 .add_instructions(&gitcl)?
1201 .test_emit();
1202 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1203 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1204 assert_eq!(2, emitter.cargo_warning().len());
1205 Ok(())
1206 }
1207
1208 #[test]
1209 #[serial]
1210 fn git_all_idempotent_no_warn() -> Result<()> {
1211 let gitcl = Gitcl::all_git();
1212 let emitter = Emitter::default()
1213 .idempotent()
1214 .quiet()
1215 .add_instructions(&gitcl)?
1216 .test_emit();
1217 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1218 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1219 assert_eq!(2, emitter.cargo_warning().len());
1220 Ok(())
1221 }
1222
1223 #[test]
1224 #[serial]
1225 fn git_all_at_path() -> Result<()> {
1226 let repo = TestRepos::new(false, false, false)?;
1227 let mut gitcl = Gitcl::all_git();
1228 let _ = gitcl.at_path(repo.path());
1229 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1230 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1231 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1232 assert_eq!(0, emitter.cargo_warning().len());
1233 Ok(())
1234 }
1235
1236 #[test]
1237 #[serial]
1238 fn git_all() -> Result<()> {
1239 let gitcl = Gitcl::all_git();
1240 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1241 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1242 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1243 assert_eq!(0, emitter.cargo_warning().len());
1244 Ok(())
1245 }
1246
1247 #[test]
1248 #[serial]
1249 fn git_all_shallow_clone() -> Result<()> {
1250 let repo = TestRepos::new(false, false, true)?;
1251 let mut gitcl = Gitcl::all_git();
1252 let _ = gitcl.at_path(repo.path());
1253 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1254 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1255 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1256 assert_eq!(0, emitter.cargo_warning().len());
1257 Ok(())
1258 }
1259
1260 #[test]
1261 #[serial]
1262 fn git_all_dirty_tags_short() -> Result<()> {
1263 let gitcl = Gitcl::builder()
1264 .all()
1265 .describe(true, true, None)
1266 .sha(true)
1267 .build();
1268 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1269 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1270 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1271 assert_eq!(0, emitter.cargo_warning().len());
1272 Ok(())
1273 }
1274
1275 #[test]
1276 #[serial]
1277 fn fails_on_bad_git_command() -> Result<()> {
1278 let mut gitcl = Gitcl::all_git();
1279 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1280 assert!(
1281 Emitter::default()
1282 .fail_on_error()
1283 .add_instructions(&gitcl)
1284 .is_err()
1285 );
1286 Ok(())
1287 }
1288
1289 #[test]
1290 #[serial]
1291 fn defaults_on_bad_git_command() -> Result<()> {
1292 let mut gitcl = Gitcl::all_git();
1293 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1294 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1295 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1296 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1297 assert_eq!(11, emitter.cargo_warning().len());
1298 Ok(())
1299 }
1300
1301 #[test]
1302 #[serial]
1303 fn idempotent_on_bad_git_command() -> Result<()> {
1304 let mut gitcl = Gitcl::all_git();
1305 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1306 let emitter = Emitter::default()
1307 .idempotent()
1308 .add_instructions(&gitcl)?
1309 .test_emit();
1310 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1311 assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
1312 assert_eq!(11, emitter.cargo_warning().len());
1313 Ok(())
1314 }
1315
1316 #[test]
1317 #[serial]
1318 fn bad_timestamp_defaults() -> Result<()> {
1319 let mut map = BTreeMap::new();
1320 let mut warnings = vec![];
1321 let gitcl = Gitcl::all_git();
1322 assert!(
1323 gitcl
1324 .add_git_timestamp_entries(
1325 "this_is_not_a_git_cmd",
1326 None,
1327 false,
1328 &mut map,
1329 &mut warnings
1330 )
1331 .is_ok()
1332 );
1333 assert_eq!(0, map.len());
1334 assert_eq!(2, warnings.len());
1335 Ok(())
1336 }
1337
1338 #[test]
1339 #[serial]
1340 fn bad_timestamp_idempotent() -> Result<()> {
1341 let mut map = BTreeMap::new();
1342 let mut warnings = vec![];
1343 let gitcl = Gitcl::all_git();
1344 assert!(
1345 gitcl
1346 .add_git_timestamp_entries(
1347 "this_is_not_a_git_cmd",
1348 None,
1349 true,
1350 &mut map,
1351 &mut warnings
1352 )
1353 .is_ok()
1354 );
1355 assert_eq!(2, map.len());
1356 assert_eq!(2, warnings.len());
1357 Ok(())
1358 }
1359
1360 #[test]
1361 #[serial]
1362 fn source_date_epoch_works() {
1363 temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), || {
1364 let result = || -> Result<()> {
1365 let mut stdout_buf = vec![];
1366 let gitcl = Gitcl::builder()
1367 .commit_date(true)
1368 .commit_timestamp(true)
1369 .build();
1370 _ = Emitter::new()
1371 .idempotent()
1372 .add_instructions(&gitcl)?
1373 .emit_to(&mut stdout_buf)?;
1374 let output = String::from_utf8_lossy(&stdout_buf);
1375 for (idx, line) in output.lines().enumerate() {
1376 if idx == 0 {
1377 assert_eq!("cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2022-12-23", line);
1378 } else if idx == 1 {
1379 assert_eq!(
1380 "cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2022-12-23T15:29:20.000000000Z",
1381 line
1382 );
1383 }
1384 }
1385 Ok(())
1386 }();
1387 assert!(result.is_ok());
1388 });
1389 }
1390
1391 #[test]
1392 #[serial]
1393 #[cfg(unix)]
1394 fn bad_source_date_epoch_fails() {
1395 use std::ffi::OsStr;
1396 use std::os::unix::prelude::OsStrExt;
1397
1398 let source = [0x66, 0x6f, 0x80, 0x6f];
1399 let os_str = OsStr::from_bytes(&source[..]);
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 .fail_on_error()
1407 .add_instructions(&gitcl)?
1408 .emit_to(&mut stdout_buf)
1409 }();
1410 assert!(result.is_err());
1411 });
1412 }
1413
1414 #[test]
1415 #[serial]
1416 #[cfg(unix)]
1417 fn bad_source_date_epoch_defaults() {
1418 use std::ffi::OsStr;
1419 use std::os::unix::prelude::OsStrExt;
1420
1421 let source = [0x66, 0x6f, 0x80, 0x6f];
1422 let os_str = OsStr::from_bytes(&source[..]);
1423 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1424 let result = || -> Result<bool> {
1425 let mut stdout_buf = vec![];
1426 let gitcl = Gitcl::builder().commit_date(true).build();
1427 Emitter::new()
1428 .idempotent()
1429 .add_instructions(&gitcl)?
1430 .emit_to(&mut stdout_buf)
1431 }();
1432 assert!(result.is_ok());
1433 });
1434 }
1435
1436 #[test]
1437 #[serial]
1438 #[cfg(windows)]
1439 fn bad_source_date_epoch_fails() {
1440 use std::ffi::OsString;
1441 use std::os::windows::prelude::OsStringExt;
1442
1443 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1444 let os_string = OsString::from_wide(&source[..]);
1445 let os_str = os_string.as_os_str();
1446 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1447 let result = || -> Result<bool> {
1448 let mut stdout_buf = vec![];
1449 let gitcl = Gitcl::builder().commit_date(true).build();
1450 Emitter::new()
1451 .fail_on_error()
1452 .idempotent()
1453 .add_instructions(&gitcl)?
1454 .emit_to(&mut stdout_buf)
1455 }();
1456 assert!(result.is_err());
1457 });
1458 }
1459
1460 #[test]
1461 #[serial]
1462 #[cfg(windows)]
1463 fn bad_source_date_epoch_defaults() {
1464 use std::ffi::OsString;
1465 use std::os::windows::prelude::OsStringExt;
1466
1467 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1468 let os_string = OsString::from_wide(&source[..]);
1469 let os_str = os_string.as_os_str();
1470 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1471 let result = || -> Result<bool> {
1472 let mut stdout_buf = vec![];
1473 let gitcl = Gitcl::builder().commit_date(true).build();
1474 Emitter::new()
1475 .idempotent()
1476 .add_instructions(&gitcl)?
1477 .emit_to(&mut stdout_buf)
1478 }();
1479 assert!(result.is_ok());
1480 });
1481 }
1482
1483 #[test]
1484 #[serial]
1485 #[cfg(unix)]
1486 fn git_no_index_update() -> Result<()> {
1487 let repo = TestRepos::new(true, true, false)?;
1488 repo.set_index_magic_mtime()?;
1489
1490 let mut gitcl = Gitcl::builder().all().describe(true, true, None).build();
1492 let _ = gitcl.at_path(repo.path());
1493 let failed = Emitter::default()
1494 .add_instructions(&gitcl)?
1495 .emit_to(&mut stdout())?;
1496 assert!(!failed);
1497
1498 assert_eq!(*TEST_MTIME, repo.get_index_magic_mtime()?);
1499 Ok(())
1500 }
1501}