1use self::gitcl_builder::Empty;
10use anyhow::{Error, Result, anyhow};
11use bon::Builder;
12#[cfg(feature = "allow_remote")]
13use std::{env::temp_dir, fs::create_dir_all};
14use std::{
15 env::{self, VarError},
16 path::PathBuf,
17 process::{Command, Output, Stdio},
18 str::FromStr,
19};
20use time::{
21 OffsetDateTime, UtcOffset,
22 format_description::{
23 self,
24 well_known::{Iso8601, Rfc3339},
25 },
26};
27use vergen_lib::{
28 AddEntries, CargoRerunIfChanged, CargoRustcEnvMap, CargoWarning, DefaultConfig, Describe,
29 Dirty, Sha, VergenKey, add_default_map_entry, add_map_entry,
30 constants::{
31 GIT_BRANCH_NAME, GIT_COMMIT_AUTHOR_EMAIL, GIT_COMMIT_AUTHOR_NAME, GIT_COMMIT_COUNT,
32 GIT_COMMIT_DATE_NAME, GIT_COMMIT_MESSAGE, GIT_COMMIT_TIMESTAMP_NAME, GIT_DESCRIBE_NAME,
33 GIT_DIRTY_NAME, GIT_SHA_NAME,
34 },
35};
36
37macro_rules! branch_cmd {
39 () => {
40 "git rev-parse --abbrev-ref --symbolic-full-name HEAD"
41 };
42}
43const BRANCH_CMD: &str = branch_cmd!();
44macro_rules! author_email {
45 () => {
46 "git log -1 --pretty=format:'%ae'"
47 };
48}
49const COMMIT_AUTHOR_EMAIL: &str = author_email!();
50macro_rules! author_name {
51 () => {
52 "git log -1 --pretty=format:'%an'"
53 };
54}
55const COMMIT_AUTHOR_NAME: &str = author_name!();
56macro_rules! commit_count {
57 () => {
58 "git rev-list --count HEAD"
59 };
60}
61const COMMIT_COUNT: &str = commit_count!();
62macro_rules! commit_date {
63 () => {
64 "git log -1 --pretty=format:'%cs'"
65 };
66}
67macro_rules! commit_message {
68 () => {
69 "git log -1 --format=%s"
70 };
71}
72const COMMIT_MESSAGE: &str = commit_message!();
73macro_rules! commit_timestamp {
74 () => {
75 "git log -1 --pretty=format:'%cI'"
76 };
77}
78const COMMIT_TIMESTAMP: &str = commit_timestamp!();
79macro_rules! describe {
80 () => {
81 "git describe --always"
82 };
83}
84const DESCRIBE: &str = describe!();
85macro_rules! sha {
86 () => {
87 "git rev-parse"
88 };
89}
90const SHA: &str = sha!();
91macro_rules! dirty {
92 () => {
93 "git status --porcelain"
94 };
95}
96const DIRTY: &str = dirty!();
97
98#[derive(Builder, Clone, Debug, PartialEq)]
238#[allow(clippy::struct_excessive_bools)]
239pub struct Gitcl {
240 #[builder(field)]
244 all: bool,
245 #[builder(into)]
247 local_repo_path: Option<PathBuf>,
248 #[builder(default = false)]
250 force_local: bool,
251 #[cfg(test)]
253 #[builder(default = false)]
254 force_remote: bool,
255 #[builder(into)]
257 remote_url: Option<String>,
258 #[builder(into)]
261 remote_repo_path: Option<PathBuf>,
262 #[builder(default = all)]
269 branch: bool,
270 #[builder(default = all)]
277 commit_author_name: bool,
278 #[builder(default = all)]
285 commit_author_email: bool,
286 #[builder(default = all)]
292 commit_count: bool,
293 #[builder(default = all)]
300 commit_message: bool,
301 #[doc = concat!(commit_date!())]
310 #[builder(default = all)]
312 commit_date: bool,
313 #[builder(default = all)]
320 commit_timestamp: bool,
321 #[builder(
339 required,
340 default = all.then(|| Describe::builder().build()),
341 with = |tags: bool, dirty: bool, match_pattern: Option<&'static str>| {
342 Some(Describe::builder().tags(tags).dirty(dirty).maybe_match_pattern(match_pattern).build())
343 }
344 )]
345 describe: Option<Describe>,
346 #[builder(
358 required,
359 default = all.then(|| Sha::builder().build()),
360 with = |short: bool| Some(Sha::builder().short(short).build())
361 )]
362 sha: Option<Sha>,
363 #[builder(
373 required,
374 default = all.then(|| Dirty::builder().build()),
375 with = |include_untracked: bool| Some(Dirty::builder().include_untracked(include_untracked).build())
376 )]
377 dirty: Option<Dirty>,
378 #[builder(default = false)]
380 use_local: bool,
381 git_cmd: Option<&'static str>,
383}
384
385impl<S: gitcl_builder::State> GitclBuilder<S> {
386 fn all(mut self) -> Self {
391 self.all = true;
392 self
393 }
394}
395
396impl Gitcl {
397 #[must_use]
399 pub fn all_git() -> Gitcl {
400 Self::builder().all().build()
401 }
402
403 pub fn all() -> GitclBuilder<Empty> {
405 Self::builder().all()
406 }
407
408 fn any(&self) -> bool {
409 self.branch
410 || self.commit_author_email
411 || self.commit_author_name
412 || self.commit_count
413 || self.commit_date
414 || self.commit_message
415 || self.commit_timestamp
416 || self.describe.is_some()
417 || self.sha.is_some()
418 || self.dirty.is_some()
419 }
420
421 pub fn at_path(&mut self, path: PathBuf) -> &mut Self {
423 self.local_repo_path = Some(path);
424 self
425 }
426
427 pub fn git_cmd(&mut self, cmd: Option<&'static str>) -> &mut Self {
436 self.git_cmd = cmd;
437 self
438 }
439
440 fn check_git(cmd: &str) -> Result<()> {
441 if Self::git_cmd_exists(cmd) {
442 Ok(())
443 } else {
444 Err(anyhow!("no suitable 'git' command found!"))
445 }
446 }
447
448 fn check_inside_git_worktree(path: Option<&PathBuf>) -> Result<()> {
449 if Self::inside_git_worktree(path) {
450 Ok(())
451 } else {
452 Err(anyhow!("not within a suitable 'git' worktree!"))
453 }
454 }
455
456 fn git_cmd_exists(cmd: &str) -> bool {
457 Self::run_cmd(cmd, None)
458 .map(|output| output.status.success())
459 .unwrap_or(false)
460 }
461
462 #[cfg(feature = "allow_remote")]
463 fn clone(remote_url: &str, path: Option<&PathBuf>) -> bool {
464 Self::run_cmd(&format!("git clone --depth 5 {remote_url} ."), path)
465 .map(|output| output.status.success())
466 .unwrap_or(false)
467 }
468
469 fn inside_git_worktree(path: Option<&PathBuf>) -> bool {
470 Self::run_cmd("git rev-parse --is-inside-work-tree", path)
471 .map(|output| {
472 let stdout = String::from_utf8_lossy(&output.stdout);
473 output.status.success() && stdout.contains("true")
474 })
475 .unwrap_or(false)
476 }
477
478 #[cfg(not(target_env = "msvc"))]
479 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
480 let shell = if let Some(shell_path) = env::var_os("SHELL") {
481 shell_path.to_string_lossy().into_owned()
482 } else {
483 "sh".to_string()
485 };
486 let mut cmd = Command::new(shell);
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 #[cfg(target_env = "msvc")]
508 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
509 let mut cmd = Command::new("cmd");
510 if let Some(path) = path_opt {
511 _ = cmd.current_dir(path);
512 }
513 _ = cmd.env("GIT_OPTIONAL_LOCKS", "0");
515 _ = cmd.arg("/c");
516 _ = cmd.arg(command);
517 _ = cmd.stdout(Stdio::piped());
518 _ = cmd.stderr(Stdio::piped());
519
520 let output = cmd.output()?;
521 if !output.status.success() {
522 eprintln!("Command failed: `{command}`");
523 eprintln!("--- stdout:\n{}\n", String::from_utf8_lossy(&output.stdout));
524 eprintln!("--- stderr:\n{}\n", String::from_utf8_lossy(&output.stderr));
525 }
526
527 Ok(output)
528 }
529
530 fn run_cmd_checked(command: &str, path_opt: Option<&PathBuf>) -> Result<Vec<u8>> {
531 let output = Self::run_cmd(command, path_opt)?;
532 if output.status.success() {
533 Ok(output.stdout)
534 } else {
535 let stderr = String::from_utf8_lossy(&output.stderr);
536 Err(anyhow!("Failed to run '{command}'! {stderr}"))
537 }
538 }
539
540 #[allow(clippy::too_many_lines)]
541 fn inner_add_git_map_entries(
542 &self,
543 repo_path: Option<&PathBuf>,
544 idempotent: bool,
545 cargo_rustc_env: &mut CargoRustcEnvMap,
546 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
547 cargo_warning: &mut CargoWarning,
548 ) -> Result<()> {
549 if !idempotent && self.any() {
550 Self::add_rerun_if_changed(cargo_rerun_if_changed, repo_path)?;
551 }
552
553 if self.branch {
554 if let Ok(_value) = env::var(GIT_BRANCH_NAME) {
555 add_default_map_entry(
556 idempotent,
557 VergenKey::GitBranch,
558 cargo_rustc_env,
559 cargo_warning,
560 );
561 } else {
562 Self::add_git_cmd_entry(
563 BRANCH_CMD,
564 repo_path,
565 VergenKey::GitBranch,
566 cargo_rustc_env,
567 )?;
568 }
569 }
570
571 if self.commit_author_email {
572 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_EMAIL) {
573 add_default_map_entry(
574 idempotent,
575 VergenKey::GitCommitAuthorEmail,
576 cargo_rustc_env,
577 cargo_warning,
578 );
579 } else {
580 Self::add_git_cmd_entry(
581 COMMIT_AUTHOR_EMAIL,
582 repo_path,
583 VergenKey::GitCommitAuthorEmail,
584 cargo_rustc_env,
585 )?;
586 }
587 }
588
589 if self.commit_author_name {
590 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_NAME) {
591 add_default_map_entry(
592 idempotent,
593 VergenKey::GitCommitAuthorName,
594 cargo_rustc_env,
595 cargo_warning,
596 );
597 } else {
598 Self::add_git_cmd_entry(
599 COMMIT_AUTHOR_NAME,
600 repo_path,
601 VergenKey::GitCommitAuthorName,
602 cargo_rustc_env,
603 )?;
604 }
605 }
606
607 if self.commit_count {
608 if let Ok(_value) = env::var(GIT_COMMIT_COUNT) {
609 add_default_map_entry(
610 idempotent,
611 VergenKey::GitCommitCount,
612 cargo_rustc_env,
613 cargo_warning,
614 );
615 } else {
616 Self::add_git_cmd_entry(
617 COMMIT_COUNT,
618 repo_path,
619 VergenKey::GitCommitCount,
620 cargo_rustc_env,
621 )?;
622 }
623 }
624
625 self.add_git_timestamp_entries(
626 COMMIT_TIMESTAMP,
627 repo_path,
628 idempotent,
629 cargo_rustc_env,
630 cargo_warning,
631 )?;
632
633 if self.commit_message {
634 if let Ok(_value) = env::var(GIT_COMMIT_MESSAGE) {
635 add_default_map_entry(
636 idempotent,
637 VergenKey::GitCommitMessage,
638 cargo_rustc_env,
639 cargo_warning,
640 );
641 } else {
642 Self::add_git_cmd_entry(
643 COMMIT_MESSAGE,
644 repo_path,
645 VergenKey::GitCommitMessage,
646 cargo_rustc_env,
647 )?;
648 }
649 }
650
651 let mut dirty_cache = None; if let Some(dirty) = self.dirty {
653 if let Ok(_value) = env::var(GIT_DIRTY_NAME) {
654 add_default_map_entry(
655 idempotent,
656 VergenKey::GitDirty,
657 cargo_rustc_env,
658 cargo_warning,
659 );
660 } else {
661 let use_dirty = Self::compute_dirty(repo_path, dirty.include_untracked())?;
662 if !dirty.include_untracked() {
663 dirty_cache = Some(use_dirty);
664 }
665 add_map_entry(
666 VergenKey::GitDirty,
667 bool::to_string(&use_dirty),
668 cargo_rustc_env,
669 );
670 }
671 }
672
673 if let Some(describe) = self.describe {
674 if let Ok(_value) = env::var(GIT_DESCRIBE_NAME) {
679 add_default_map_entry(
680 idempotent,
681 VergenKey::GitDescribe,
682 cargo_rustc_env,
683 cargo_warning,
684 );
685 } else {
686 let mut describe_cmd = String::from(DESCRIBE);
687 if describe.tags() {
688 describe_cmd.push_str(" --tags");
689 }
690 if let Some(pattern) = *describe.match_pattern() {
691 Self::match_pattern_cmd_str(&mut describe_cmd, pattern);
692 }
693 let stdout = Self::run_cmd_checked(&describe_cmd, repo_path)?;
694 let mut describe_value = String::from_utf8_lossy(&stdout).trim().to_string();
695 if describe.dirty()
696 && (dirty_cache.is_some_and(|dirty| dirty)
697 || Self::compute_dirty(repo_path, false)?)
698 {
699 describe_value.push_str("-dirty");
700 }
701 add_map_entry(VergenKey::GitDescribe, describe_value, cargo_rustc_env);
702 }
703 }
704
705 if let Some(sha) = self.sha {
706 if let Ok(_value) = env::var(GIT_SHA_NAME) {
707 add_default_map_entry(
708 idempotent,
709 VergenKey::GitSha,
710 cargo_rustc_env,
711 cargo_warning,
712 );
713 } else {
714 let mut sha_cmd = String::from(SHA);
715 if sha.short() {
716 sha_cmd.push_str(" --short");
717 }
718 sha_cmd.push_str(" HEAD");
719 Self::add_git_cmd_entry(&sha_cmd, repo_path, VergenKey::GitSha, cargo_rustc_env)?;
720 }
721 }
722
723 Ok(())
724 }
725
726 fn add_rerun_if_changed(
727 rerun_if_changed: &mut Vec<String>,
728 path: Option<&PathBuf>,
729 ) -> Result<()> {
730 let git_path = Self::run_cmd("git rev-parse --git-dir", path)?;
731 if git_path.status.success() {
732 let git_path_str = String::from_utf8_lossy(&git_path.stdout).trim().to_string();
733 let git_path = PathBuf::from(&git_path_str);
734
735 let mut head_path = git_path.clone();
737 head_path.push("HEAD");
738
739 if head_path.exists() {
740 rerun_if_changed.push(format!("{}", head_path.display()));
741 }
742
743 let refp = Self::setup_ref_path(path)?;
745 if refp.status.success() {
746 let ref_path_str = String::from_utf8_lossy(&refp.stdout).trim().to_string();
747 let mut ref_path = git_path;
748 ref_path.push(ref_path_str);
749 if ref_path.exists() {
750 rerun_if_changed.push(format!("{}", ref_path.display()));
751 }
752 }
753 }
754 Ok(())
755 }
756
757 #[cfg(not(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 describe_cmd.push('\"');
762 }
763
764 #[cfg(target_os = "windows")]
765 fn match_pattern_cmd_str(describe_cmd: &mut String, pattern: &str) {
766 describe_cmd.push_str(" --match ");
767 describe_cmd.push_str(pattern);
768 }
769
770 #[cfg(not(test))]
771 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
772 Self::run_cmd("git symbolic-ref HEAD", path)
773 }
774
775 #[cfg(all(test, not(target_os = "windows")))]
776 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
777 Self::run_cmd("pwd", path)
778 }
779
780 #[cfg(all(test, target_os = "windows"))]
781 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
782 Self::run_cmd("cd", path)
783 }
784
785 fn add_git_cmd_entry(
786 cmd: &str,
787 path: Option<&PathBuf>,
788 key: VergenKey,
789 cargo_rustc_env: &mut CargoRustcEnvMap,
790 ) -> Result<()> {
791 let stdout = Self::run_cmd_checked(cmd, path)?;
792 let stdout = String::from_utf8_lossy(&stdout)
793 .trim()
794 .trim_matches('\'')
795 .to_string();
796 add_map_entry(key, stdout, cargo_rustc_env);
797 Ok(())
798 }
799
800 fn add_git_timestamp_entries(
801 &self,
802 cmd: &str,
803 path: Option<&PathBuf>,
804 idempotent: bool,
805 cargo_rustc_env: &mut CargoRustcEnvMap,
806 cargo_warning: &mut CargoWarning,
807 ) -> Result<()> {
808 let mut date_override = false;
809 if let Ok(_value) = env::var(GIT_COMMIT_DATE_NAME) {
810 add_default_map_entry(
811 idempotent,
812 VergenKey::GitCommitDate,
813 cargo_rustc_env,
814 cargo_warning,
815 );
816 date_override = true;
817 }
818
819 let mut timestamp_override = false;
820 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
821 add_default_map_entry(
822 idempotent,
823 VergenKey::GitCommitTimestamp,
824 cargo_rustc_env,
825 cargo_warning,
826 );
827 timestamp_override = true;
828 }
829
830 let output = Self::run_cmd(cmd, path)?;
831 if output.status.success() {
832 let stdout = String::from_utf8_lossy(&output.stdout)
833 .lines()
834 .last()
835 .ok_or_else(|| anyhow!("invalid 'git log' output"))?
836 .trim()
837 .trim_matches('\'')
838 .to_string();
839
840 let (sde, ts) = match env::var("SOURCE_DATE_EPOCH") {
841 Ok(v) => (
842 true,
843 OffsetDateTime::from_unix_timestamp(i64::from_str(&v)?)?,
844 ),
845 Err(VarError::NotPresent) => self.compute_local_offset(&stdout)?,
846 Err(e) => return Err(e.into()),
847 };
848
849 if idempotent && !sde {
850 if self.commit_date && !date_override {
851 add_default_map_entry(
852 idempotent,
853 VergenKey::GitCommitDate,
854 cargo_rustc_env,
855 cargo_warning,
856 );
857 }
858
859 if self.commit_timestamp && !timestamp_override {
860 add_default_map_entry(
861 idempotent,
862 VergenKey::GitCommitTimestamp,
863 cargo_rustc_env,
864 cargo_warning,
865 );
866 }
867 } else {
868 if self.commit_date && !date_override {
869 let format = format_description::parse("[year]-[month]-[day]")?;
870 add_map_entry(
871 VergenKey::GitCommitDate,
872 ts.format(&format)?,
873 cargo_rustc_env,
874 );
875 }
876
877 if self.commit_timestamp && !timestamp_override {
878 add_map_entry(
879 VergenKey::GitCommitTimestamp,
880 ts.format(&Iso8601::DEFAULT)?,
881 cargo_rustc_env,
882 );
883 }
884 }
885 } else {
886 if self.commit_date && !date_override {
887 add_default_map_entry(
888 idempotent,
889 VergenKey::GitCommitDate,
890 cargo_rustc_env,
891 cargo_warning,
892 );
893 }
894
895 if self.commit_timestamp && !timestamp_override {
896 add_default_map_entry(
897 idempotent,
898 VergenKey::GitCommitTimestamp,
899 cargo_rustc_env,
900 cargo_warning,
901 );
902 }
903 }
904
905 Ok(())
906 }
907
908 #[cfg_attr(coverage_nightly, coverage(off))]
909 fn compute_local_offset(&self, stdout: &str) -> Result<(bool, OffsetDateTime)> {
911 let no_offset = OffsetDateTime::parse(stdout, &Rfc3339)?;
912 if self.use_local {
913 let local = UtcOffset::local_offset_at(no_offset)?;
914 let local_offset = no_offset.checked_to_offset(local).unwrap_or(no_offset);
915 Ok((false, local_offset))
916 } else {
917 Ok((false, no_offset))
918 }
919 }
920
921 fn compute_dirty(repo_path: Option<&PathBuf>, include_untracked: bool) -> Result<bool> {
922 let mut dirty_cmd = String::from(DIRTY);
923 if !include_untracked {
924 dirty_cmd.push_str(" --untracked-files=no");
925 }
926 let stdout = Self::run_cmd_checked(&dirty_cmd, repo_path)?;
927 Ok(!stdout.is_empty())
928 }
929
930 #[cfg(not(feature = "allow_remote"))]
931 #[allow(clippy::unused_self)]
932 fn cleanup(&self) {}
933
934 #[cfg(feature = "allow_remote")]
935 fn cleanup(&self) {
936 if let Some(_remote_url) = self.remote_url.as_ref() {
937 let temp_dir = temp_dir().join("vergen-gitcl");
938 if let Some(path) = &self.remote_repo_path {
940 if path.exists() {
941 let _ = std::fs::remove_dir_all(path).ok();
942 }
943 } else if temp_dir.exists() {
944 let _ = std::fs::remove_dir_all(temp_dir).ok();
945 }
946 }
947 }
948
949 #[cfg(all(not(test), feature = "allow_remote"))]
950 #[allow(clippy::unused_self)]
951 fn try_local(&self) -> bool {
952 true
953 }
954
955 #[cfg(all(test, feature = "allow_remote"))]
956 fn try_local(&self) -> bool {
957 self.force_local || !self.force_remote
958 }
959
960 #[cfg(all(not(test), feature = "allow_remote"))]
961 fn try_remote(&self) -> bool {
962 !self.force_local
963 }
964
965 #[cfg(all(test, feature = "allow_remote"))]
966 fn try_remote(&self) -> bool {
967 self.force_remote || !self.force_local
968 }
969
970 #[cfg(feature = "allow_remote")]
971 fn setup_repo_path(
972 &self,
973 repo_path: Option<&PathBuf>,
974 cargo_warning: &mut CargoWarning,
975 ) -> Result<Option<PathBuf>> {
976 if self.try_local() && Self::check_inside_git_worktree(repo_path).is_ok() {
977 Ok(repo_path.cloned())
978 } else if self.try_remote()
979 && let Some(remote_url) = &self.remote_url
980 {
981 let remote_path = if let Some(remote_path) = &self.remote_repo_path {
982 remote_path.clone()
983 } else {
984 temp_dir().join("vergen-gitcl")
985 };
986 create_dir_all(&remote_path)?;
987 if !Self::clone(remote_url, Some(&remote_path)) {
988 return Err(anyhow!("Failed to clone git repository"));
989 }
990 cargo_warning.push(format!(
991 "Using remote repository from '{remote_url}' at '{}'",
992 remote_path.display()
993 ));
994 Ok(Some(remote_path))
995 } else {
996 Ok(repo_path.cloned())
997 }
998 }
999
1000 #[cfg(not(feature = "allow_remote"))]
1001 #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
1002 fn setup_repo_path(
1003 &self,
1004 repo_path: Option<&PathBuf>,
1005 _cargo_warning: &mut CargoWarning,
1006 ) -> Result<Option<PathBuf>> {
1007 Ok(repo_path.cloned())
1008 }
1009}
1010
1011impl AddEntries for Gitcl {
1012 fn add_map_entries(
1013 &self,
1014 idempotent: bool,
1015 cargo_rustc_env: &mut CargoRustcEnvMap,
1016 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
1017 cargo_warning: &mut CargoWarning,
1018 ) -> Result<()> {
1019 if self.any() {
1020 let git_cmd = self.git_cmd.unwrap_or("git --version");
1021 Self::check_git(git_cmd)?;
1022
1023 let repo_path = self.setup_repo_path(self.local_repo_path.as_ref(), cargo_warning)?;
1024 let repo_path = repo_path.as_ref();
1025 Self::check_inside_git_worktree(repo_path)?;
1026
1027 self.inner_add_git_map_entries(
1028 repo_path,
1029 idempotent,
1030 cargo_rustc_env,
1031 cargo_rerun_if_changed,
1032 cargo_warning,
1033 )?;
1034
1035 self.cleanup();
1036 }
1037 Ok(())
1038 }
1039
1040 fn add_default_entries(
1041 &self,
1042 config: &DefaultConfig,
1043 cargo_rustc_env_map: &mut CargoRustcEnvMap,
1044 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
1045 cargo_warning: &mut CargoWarning,
1046 ) -> Result<()> {
1047 if *config.fail_on_error() {
1048 let error = Error::msg(format!("{}", config.error()));
1049 Err(error)
1050 } else {
1051 cargo_warning.clear();
1054 cargo_rerun_if_changed.clear();
1055
1056 cargo_warning.push(format!("{}", config.error()));
1057
1058 if self.branch {
1059 add_default_map_entry(
1060 *config.idempotent(),
1061 VergenKey::GitBranch,
1062 cargo_rustc_env_map,
1063 cargo_warning,
1064 );
1065 }
1066 if self.commit_author_email {
1067 add_default_map_entry(
1068 *config.idempotent(),
1069 VergenKey::GitCommitAuthorEmail,
1070 cargo_rustc_env_map,
1071 cargo_warning,
1072 );
1073 }
1074 if self.commit_author_name {
1075 add_default_map_entry(
1076 *config.idempotent(),
1077 VergenKey::GitCommitAuthorName,
1078 cargo_rustc_env_map,
1079 cargo_warning,
1080 );
1081 }
1082 if self.commit_count {
1083 add_default_map_entry(
1084 *config.idempotent(),
1085 VergenKey::GitCommitCount,
1086 cargo_rustc_env_map,
1087 cargo_warning,
1088 );
1089 }
1090 if self.commit_date {
1091 add_default_map_entry(
1092 *config.idempotent(),
1093 VergenKey::GitCommitDate,
1094 cargo_rustc_env_map,
1095 cargo_warning,
1096 );
1097 }
1098 if self.commit_message {
1099 add_default_map_entry(
1100 *config.idempotent(),
1101 VergenKey::GitCommitMessage,
1102 cargo_rustc_env_map,
1103 cargo_warning,
1104 );
1105 }
1106 if self.commit_timestamp {
1107 add_default_map_entry(
1108 *config.idempotent(),
1109 VergenKey::GitCommitTimestamp,
1110 cargo_rustc_env_map,
1111 cargo_warning,
1112 );
1113 }
1114 if self.describe.is_some() {
1115 add_default_map_entry(
1116 *config.idempotent(),
1117 VergenKey::GitDescribe,
1118 cargo_rustc_env_map,
1119 cargo_warning,
1120 );
1121 }
1122 if self.sha.is_some() {
1123 add_default_map_entry(
1124 *config.idempotent(),
1125 VergenKey::GitSha,
1126 cargo_rustc_env_map,
1127 cargo_warning,
1128 );
1129 }
1130 if self.dirty.is_some() {
1131 add_default_map_entry(
1132 *config.idempotent(),
1133 VergenKey::GitDirty,
1134 cargo_rustc_env_map,
1135 cargo_warning,
1136 );
1137 }
1138 Ok(())
1139 }
1140 }
1141}
1142
1143#[cfg(test)]
1144mod test {
1145 use super::Gitcl;
1146 use crate::Emitter;
1147 use anyhow::Result;
1148 use serial_test::serial;
1149 #[cfg(unix)]
1150 use std::io::stdout;
1151 use std::{collections::BTreeMap, env::temp_dir, io::Write};
1152 #[cfg(unix)]
1153 use test_util::TEST_MTIME;
1154 use test_util::TestRepos;
1155 use vergen_lib::{VergenKey, count_idempotent};
1156
1157 #[test]
1158 #[serial]
1159 #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
1160 fn gitcl_clone_works() {
1161 let gitcl = Gitcl::all_git();
1162 let another = gitcl.clone();
1163 assert_eq!(another, gitcl);
1164 }
1165
1166 #[test]
1167 #[serial]
1168 fn gitcl_debug_works() -> Result<()> {
1169 let gitcl = Gitcl::all_git();
1170 let mut buf = vec![];
1171 write!(buf, "{gitcl:?}")?;
1172 assert!(!buf.is_empty());
1173 Ok(())
1174 }
1175
1176 #[test]
1177 #[serial]
1178 fn gitcl_default() -> Result<()> {
1179 let gitcl = Gitcl::builder().build();
1180 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1181 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1182 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1183 assert_eq!(0, emitter.cargo_warning().len());
1184 Ok(())
1185 }
1186
1187 #[test]
1188 #[serial]
1189 fn bad_command_is_error() -> Result<()> {
1190 let mut map = BTreeMap::new();
1191 assert!(
1192 Gitcl::add_git_cmd_entry(
1193 "such_a_terrible_cmd",
1194 None,
1195 VergenKey::GitCommitMessage,
1196 &mut map
1197 )
1198 .is_err()
1199 );
1200 Ok(())
1201 }
1202
1203 #[test]
1204 #[serial]
1205 fn non_working_tree_is_error() -> Result<()> {
1206 assert!(Gitcl::check_inside_git_worktree(Some(&temp_dir())).is_err());
1207 Ok(())
1208 }
1209
1210 #[test]
1211 #[serial]
1212 fn invalid_git_is_error() -> Result<()> {
1213 assert!(Gitcl::check_git("such_a_terrible_cmd -v").is_err());
1214 Ok(())
1215 }
1216
1217 #[cfg(not(target_family = "windows"))]
1218 #[test]
1219 #[serial]
1220 fn shell_env_works() -> Result<()> {
1221 temp_env::with_var("SHELL", Some("bash"), || {
1222 let mut map = BTreeMap::new();
1223 assert!(
1224 Gitcl::add_git_cmd_entry("git -v", None, VergenKey::GitCommitMessage, &mut map)
1225 .is_ok()
1226 );
1227 });
1228 Ok(())
1229 }
1230
1231 #[test]
1232 #[serial]
1233 fn git_all_idempotent() -> Result<()> {
1234 let gitcl = Gitcl::all_git();
1235 let emitter = Emitter::default()
1236 .idempotent()
1237 .add_instructions(&gitcl)?
1238 .test_emit();
1239 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1240 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1241 assert_eq!(2, emitter.cargo_warning().len());
1242 Ok(())
1243 }
1244
1245 #[test]
1246 #[serial]
1247 fn git_all_idempotent_no_warn() -> Result<()> {
1248 let gitcl = Gitcl::all_git();
1249 let emitter = Emitter::default()
1250 .idempotent()
1251 .quiet()
1252 .add_instructions(&gitcl)?
1253 .test_emit();
1254 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1255 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1256 assert_eq!(2, emitter.cargo_warning().len());
1257 Ok(())
1258 }
1259
1260 #[test]
1261 #[serial]
1262 fn git_all_at_path() -> Result<()> {
1263 let repo = TestRepos::new(false, false, false)?;
1264 let mut gitcl = Gitcl::all_git();
1265 let _ = gitcl.at_path(repo.path());
1266 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1267 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1268 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1269 assert_eq!(0, emitter.cargo_warning().len());
1270 Ok(())
1271 }
1272
1273 #[test]
1274 #[serial]
1275 fn git_all() -> Result<()> {
1276 let gitcl = Gitcl::all_git();
1277 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1278 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1279 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1280 assert_eq!(0, emitter.cargo_warning().len());
1281 Ok(())
1282 }
1283
1284 #[test]
1285 #[serial]
1286 fn git_all_shallow_clone() -> Result<()> {
1287 let repo = TestRepos::new(false, false, true)?;
1288 let mut gitcl = Gitcl::all_git();
1289 let _ = gitcl.at_path(repo.path());
1290 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1291 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1292 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1293 assert_eq!(0, emitter.cargo_warning().len());
1294 Ok(())
1295 }
1296
1297 #[test]
1298 #[serial]
1299 fn git_all_dirty_tags_short() -> Result<()> {
1300 let gitcl = Gitcl::builder()
1301 .all()
1302 .describe(true, true, None)
1303 .sha(true)
1304 .build();
1305 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1306 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1307 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1308 assert_eq!(0, emitter.cargo_warning().len());
1309 Ok(())
1310 }
1311
1312 #[test]
1313 #[serial]
1314 fn fails_on_bad_git_command() -> Result<()> {
1315 let mut gitcl = Gitcl::all_git();
1316 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1317 assert!(
1318 Emitter::default()
1319 .fail_on_error()
1320 .add_instructions(&gitcl)
1321 .is_err()
1322 );
1323 Ok(())
1324 }
1325
1326 #[test]
1327 #[serial]
1328 fn defaults_on_bad_git_command() -> Result<()> {
1329 let mut gitcl = Gitcl::all_git();
1330 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1331 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1332 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1333 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1334 assert_eq!(11, emitter.cargo_warning().len());
1335 Ok(())
1336 }
1337
1338 #[test]
1339 #[serial]
1340 fn idempotent_on_bad_git_command() -> Result<()> {
1341 let mut gitcl = Gitcl::all_git();
1342 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1343 let emitter = Emitter::default()
1344 .idempotent()
1345 .add_instructions(&gitcl)?
1346 .test_emit();
1347 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1348 assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
1349 assert_eq!(11, emitter.cargo_warning().len());
1350 Ok(())
1351 }
1352
1353 #[test]
1354 #[serial]
1355 fn bad_timestamp_defaults() -> Result<()> {
1356 let mut map = BTreeMap::new();
1357 let mut warnings = vec![];
1358 let gitcl = Gitcl::all_git();
1359 assert!(
1360 gitcl
1361 .add_git_timestamp_entries(
1362 "this_is_not_a_git_cmd",
1363 None,
1364 false,
1365 &mut map,
1366 &mut warnings
1367 )
1368 .is_ok()
1369 );
1370 assert_eq!(0, map.len());
1371 assert_eq!(2, warnings.len());
1372 Ok(())
1373 }
1374
1375 #[test]
1376 #[serial]
1377 fn bad_timestamp_idempotent() -> Result<()> {
1378 let mut map = BTreeMap::new();
1379 let mut warnings = vec![];
1380 let gitcl = Gitcl::all_git();
1381 assert!(
1382 gitcl
1383 .add_git_timestamp_entries(
1384 "this_is_not_a_git_cmd",
1385 None,
1386 true,
1387 &mut map,
1388 &mut warnings
1389 )
1390 .is_ok()
1391 );
1392 assert_eq!(2, map.len());
1393 assert_eq!(2, warnings.len());
1394 Ok(())
1395 }
1396
1397 #[test]
1398 #[serial]
1399 fn source_date_epoch_works() {
1400 temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), || {
1401 let result = || -> Result<()> {
1402 let mut stdout_buf = vec![];
1403 let gitcl = Gitcl::builder()
1404 .commit_date(true)
1405 .commit_timestamp(true)
1406 .build();
1407 _ = Emitter::new()
1408 .idempotent()
1409 .add_instructions(&gitcl)?
1410 .emit_to(&mut stdout_buf)?;
1411 let output = String::from_utf8_lossy(&stdout_buf);
1412 for (idx, line) in output.lines().enumerate() {
1413 if idx == 0 {
1414 assert_eq!("cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2022-12-23", line);
1415 } else if idx == 1 {
1416 assert_eq!(
1417 "cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2022-12-23T15:29:20.000000000Z",
1418 line
1419 );
1420 }
1421 }
1422 Ok(())
1423 }();
1424 assert!(result.is_ok());
1425 });
1426 }
1427
1428 #[test]
1429 #[serial]
1430 #[cfg(unix)]
1431 fn bad_source_date_epoch_fails() {
1432 use std::ffi::OsStr;
1433 use std::os::unix::prelude::OsStrExt;
1434
1435 let source = [0x66, 0x6f, 0x80, 0x6f];
1436 let os_str = OsStr::from_bytes(&source[..]);
1437 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1438 let result = || -> Result<bool> {
1439 let mut stdout_buf = vec![];
1440 let gitcl = Gitcl::builder().commit_date(true).build();
1441 Emitter::new()
1442 .idempotent()
1443 .fail_on_error()
1444 .add_instructions(&gitcl)?
1445 .emit_to(&mut stdout_buf)
1446 }();
1447 assert!(result.is_err());
1448 });
1449 }
1450
1451 #[test]
1452 #[serial]
1453 #[cfg(unix)]
1454 fn bad_source_date_epoch_defaults() {
1455 use std::ffi::OsStr;
1456 use std::os::unix::prelude::OsStrExt;
1457
1458 let source = [0x66, 0x6f, 0x80, 0x6f];
1459 let os_str = OsStr::from_bytes(&source[..]);
1460 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1461 let result = || -> Result<bool> {
1462 let mut stdout_buf = vec![];
1463 let gitcl = Gitcl::builder().commit_date(true).build();
1464 Emitter::new()
1465 .idempotent()
1466 .add_instructions(&gitcl)?
1467 .emit_to(&mut stdout_buf)
1468 }();
1469 assert!(result.is_ok());
1470 });
1471 }
1472
1473 #[test]
1474 #[serial]
1475 #[cfg(windows)]
1476 fn bad_source_date_epoch_fails() {
1477 use std::ffi::OsString;
1478 use std::os::windows::prelude::OsStringExt;
1479
1480 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1481 let os_string = OsString::from_wide(&source[..]);
1482 let os_str = os_string.as_os_str();
1483 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1484 let result = || -> Result<bool> {
1485 let mut stdout_buf = vec![];
1486 let gitcl = Gitcl::builder().commit_date(true).build();
1487 Emitter::new()
1488 .fail_on_error()
1489 .idempotent()
1490 .add_instructions(&gitcl)?
1491 .emit_to(&mut stdout_buf)
1492 }();
1493 assert!(result.is_err());
1494 });
1495 }
1496
1497 #[test]
1498 #[serial]
1499 #[cfg(windows)]
1500 fn bad_source_date_epoch_defaults() {
1501 use std::ffi::OsString;
1502 use std::os::windows::prelude::OsStringExt;
1503
1504 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1505 let os_string = OsString::from_wide(&source[..]);
1506 let os_str = os_string.as_os_str();
1507 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1508 let result = || -> Result<bool> {
1509 let mut stdout_buf = vec![];
1510 let gitcl = Gitcl::builder().commit_date(true).build();
1511 Emitter::new()
1512 .idempotent()
1513 .add_instructions(&gitcl)?
1514 .emit_to(&mut stdout_buf)
1515 }();
1516 assert!(result.is_ok());
1517 });
1518 }
1519
1520 #[test]
1521 #[serial]
1522 #[cfg(unix)]
1523 fn git_no_index_update() -> Result<()> {
1524 let repo = TestRepos::new(true, true, false)?;
1525 repo.set_index_magic_mtime()?;
1526
1527 let mut gitcl = Gitcl::builder().all().describe(true, true, None).build();
1529 let _ = gitcl.at_path(repo.path());
1530 let failed = Emitter::default()
1531 .add_instructions(&gitcl)?
1532 .emit_to(&mut stdout())?;
1533 assert!(!failed);
1534
1535 assert_eq!(*TEST_MTIME, repo.get_index_magic_mtime()?);
1536 Ok(())
1537 }
1538
1539 #[test]
1540 #[serial]
1541 #[cfg(feature = "allow_remote")]
1542 fn remote_clone_works() -> Result<()> {
1543 let gitcl = Gitcl::all()
1544 .force_remote(true)
1546 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1547 .describe(true, true, None)
1548 .build();
1549 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1550 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1551 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1552 assert_eq!(1, emitter.cargo_warning().len());
1553 Ok(())
1554 }
1555
1556 #[test]
1557 #[serial]
1558 #[cfg(feature = "allow_remote")]
1559 fn remote_clone_with_path_works() -> Result<()> {
1560 let remote_path = temp_dir().join("blah");
1561 let gitcl = Gitcl::all()
1562 .force_remote(true)
1564 .remote_repo_path(&remote_path)
1565 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1566 .describe(true, true, None)
1567 .build();
1568 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1569 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1570 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1571 assert_eq!(1, emitter.cargo_warning().len());
1572 Ok(())
1573 }
1574
1575 #[test]
1576 #[serial]
1577 #[cfg(feature = "allow_remote")]
1578 fn remote_clone_with_force_local_works() -> Result<()> {
1579 let gitcl = Gitcl::all()
1580 .force_local(true)
1581 .force_remote(true)
1583 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1584 .describe(true, true, None)
1585 .build();
1586 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1587 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1588 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1589 assert_eq!(0, emitter.cargo_warning().len());
1590 Ok(())
1591 }
1592}