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