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