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