1use self::gitcl_builder::Empty;
10use anyhow::{Error, Result, anyhow};
11use bon::Builder;
12use std::{
13 env,
14 path::PathBuf,
15 process::{Command, Output, Stdio},
16};
17#[cfg(feature = "allow_remote")]
18use std::{env::temp_dir, fs::create_dir_all};
19use time::{
20 OffsetDateTime, UtcOffset,
21 format_description::{
22 self,
23 well_known::{Iso8601, Rfc3339},
24 },
25};
26use vergen_lib::{
27 AddEntries, CargoRerunIfChanged, CargoRustcEnvMap, CargoWarning, DefaultConfig, Describe,
28 Dirty, Sha, VergenKey, add_default_map_entry, add_map_entry,
29 constants::{
30 GIT_BRANCH_NAME, GIT_COMMIT_AUTHOR_EMAIL, GIT_COMMIT_AUTHOR_NAME, GIT_COMMIT_COUNT,
31 GIT_COMMIT_DATE_NAME, GIT_COMMIT_MESSAGE, GIT_COMMIT_TIMESTAMP_NAME,
32 GIT_COMMIT_TIMESTAMP_UNIX_NAME, GIT_DESCRIBE_NAME, GIT_DIRTY_NAME, GIT_SHA_NAME,
33 },
34};
35
36macro_rules! branch_cmd {
38 () => {
39 "git rev-parse --abbrev-ref --symbolic-full-name HEAD"
40 };
41}
42const BRANCH_CMD: &str = branch_cmd!();
43macro_rules! author_email {
44 () => {
45 "git log -1 --pretty=format:'%ae'"
46 };
47}
48const COMMIT_AUTHOR_EMAIL: &str = author_email!();
49macro_rules! author_name {
50 () => {
51 "git log -1 --pretty=format:'%an'"
52 };
53}
54const COMMIT_AUTHOR_NAME: &str = author_name!();
55macro_rules! commit_count {
56 () => {
57 "git rev-list --count HEAD"
58 };
59}
60const COMMIT_COUNT: &str = commit_count!();
61macro_rules! commit_date {
62 () => {
63 "git log -1 --pretty=format:'%cs'"
64 };
65}
66macro_rules! commit_message {
67 () => {
68 "git log -1 --format=%s"
69 };
70}
71const COMMIT_MESSAGE: &str = commit_message!();
72macro_rules! commit_timestamp {
73 () => {
74 "git log -1 --pretty=format:'%cI'"
75 };
76}
77const COMMIT_TIMESTAMP: &str = commit_timestamp!();
78macro_rules! describe {
79 () => {
80 "git describe --always"
81 };
82}
83const DESCRIBE: &str = describe!();
84macro_rules! sha {
85 () => {
86 "git rev-parse"
87 };
88}
89const SHA: &str = sha!();
90macro_rules! dirty {
91 () => {
92 "git status --porcelain"
93 };
94}
95const DIRTY: &str = dirty!();
96
97#[derive(Builder, Clone, Debug, PartialEq)]
206#[allow(clippy::struct_excessive_bools)]
207pub struct Gitcl {
208 #[builder(field)]
212 all: bool,
213 #[builder(into)]
215 local_repo_path: Option<PathBuf>,
216 #[builder(default = false)]
218 force_local: bool,
219 #[cfg(test)]
221 #[builder(default = false)]
222 force_remote: bool,
223 #[builder(into)]
225 remote_url: Option<String>,
226 #[builder(into)]
229 remote_repo_path: Option<PathBuf>,
230 #[builder(into)]
232 remote_tag: Option<String>,
233 #[builder(default = 100)]
235 fetch_depth: usize,
236 #[cfg(feature = "vcs_info")]
240 #[builder(default = false)]
241 vcs_info_fallback: bool,
242 #[builder(default = all)]
249 branch: bool,
250 #[builder(default = all)]
257 commit_author_name: bool,
258 #[builder(default = all)]
265 commit_author_email: bool,
266 #[builder(default = all)]
272 commit_count: bool,
273 #[builder(default = all)]
280 commit_message: bool,
281 #[doc = concat!(commit_date!())]
290 #[builder(default = all)]
292 commit_date: bool,
293 #[builder(default = all)]
300 commit_timestamp: bool,
301 #[builder(default = false)]
309 commit_timestamp_unix: bool,
310 #[builder(
328 required,
329 default = all.then(|| Describe::builder().build()),
330 with = |tags: bool, dirty: bool, match_pattern: Option<&'static str>| {
331 Some(Describe::builder().tags(tags).dirty(dirty).maybe_match_pattern(match_pattern).build())
332 }
333 )]
334 describe: Option<Describe>,
335 #[builder(
347 required,
348 default = all.then(|| Sha::builder().build()),
349 with = |short: bool| Some(Sha::builder().short(short).build())
350 )]
351 sha: Option<Sha>,
352 #[builder(
362 required,
363 default = all.then(|| Dirty::builder().build()),
364 with = |include_untracked: bool| Some(Dirty::builder().include_untracked(include_untracked).build())
365 )]
366 dirty: Option<Dirty>,
367 #[builder(default = false)]
369 use_local: bool,
370 git_cmd: Option<&'static str>,
372}
373
374impl<S: gitcl_builder::State> GitclBuilder<S> {
375 fn all(mut self) -> Self {
380 self.all = true;
381 self
382 }
383}
384
385impl Gitcl {
386 #[must_use]
388 pub fn all_git() -> Gitcl {
389 Self::builder().all().build()
390 }
391
392 pub fn all() -> GitclBuilder<Empty> {
394 Self::builder().all()
395 }
396
397 fn any(&self) -> bool {
398 self.branch
399 || self.commit_author_email
400 || self.commit_author_name
401 || self.commit_count
402 || self.commit_date
403 || self.commit_message
404 || self.commit_timestamp
405 || self.commit_timestamp_unix
406 || self.describe.is_some()
407 || self.sha.is_some()
408 || self.dirty.is_some()
409 }
410
411 pub fn at_path(&mut self, path: PathBuf) -> &mut Self {
413 self.local_repo_path = Some(path);
414 self
415 }
416
417 pub fn git_cmd(&mut self, cmd: Option<&'static str>) -> &mut Self {
426 self.git_cmd = cmd;
427 self
428 }
429
430 fn check_git(cmd: &str) -> Result<()> {
431 if Self::git_cmd_exists(cmd) {
432 Ok(())
433 } else {
434 Err(anyhow!("no suitable 'git' command found!"))
435 }
436 }
437
438 fn check_inside_git_worktree(path: Option<&PathBuf>) -> Result<()> {
439 if Self::inside_git_worktree(path) {
440 Ok(())
441 } else {
442 Err(anyhow!("not within a suitable 'git' worktree!"))
443 }
444 }
445
446 fn git_cmd_exists(cmd: &str) -> bool {
447 Self::run_cmd(cmd, None).is_ok_and(|output| output.status.success())
448 }
449
450 #[cfg(feature = "allow_remote")]
451 fn clone_repo(&self, remote_url: &str, path: Option<&PathBuf>) -> bool {
452 let cmd = if let Some(remote_tag) = self.remote_tag.as_deref() {
453 format!("git clone --branch {remote_tag} --single-branch {remote_url} .")
454 } else {
455 format!("git clone --depth {} {remote_url} .", self.fetch_depth)
456 };
457
458 Self::run_cmd(&cmd, path).is_ok_and(|output| output.status.success())
459 }
460
461 fn inside_git_worktree(path: Option<&PathBuf>) -> bool {
462 Self::run_cmd("git rev-parse --is-inside-work-tree", path).is_ok_and(|output| {
463 let stdout = String::from_utf8_lossy(&output.stdout);
464 output.status.success() && stdout.contains("true")
465 })
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 #[allow(clippy::too_many_lines)]
791 fn add_git_timestamp_entries(
792 &self,
793 cmd: &str,
794 path: Option<&PathBuf>,
795 idempotent: bool,
796 cargo_rustc_env: &mut CargoRustcEnvMap,
797 cargo_warning: &mut CargoWarning,
798 ) -> Result<()> {
799 let mut date_override = false;
800 if let Ok(_value) = env::var(GIT_COMMIT_DATE_NAME) {
801 add_default_map_entry(
802 idempotent,
803 VergenKey::GitCommitDate,
804 cargo_rustc_env,
805 cargo_warning,
806 );
807 date_override = true;
808 }
809
810 let mut timestamp_override = false;
811 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
812 add_default_map_entry(
813 idempotent,
814 VergenKey::GitCommitTimestamp,
815 cargo_rustc_env,
816 cargo_warning,
817 );
818 timestamp_override = true;
819 }
820
821 let mut timestamp_unix_override = false;
822 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_UNIX_NAME) {
823 add_default_map_entry(
824 idempotent,
825 VergenKey::GitCommitTimestampUnix,
826 cargo_rustc_env,
827 cargo_warning,
828 );
829 timestamp_unix_override = true;
830 }
831
832 let output = Self::run_cmd(cmd, path)?;
833 if output.status.success() {
834 let stdout = String::from_utf8_lossy(&output.stdout)
835 .lines()
836 .last()
837 .ok_or_else(|| anyhow!("invalid 'git log' output"))?
838 .trim()
839 .trim_matches('\'')
840 .to_string();
841
842 let ts = self.compute_local_offset(&stdout)?;
846
847 if idempotent {
848 if self.commit_date && !date_override {
849 add_default_map_entry(
850 idempotent,
851 VergenKey::GitCommitDate,
852 cargo_rustc_env,
853 cargo_warning,
854 );
855 }
856
857 if self.commit_timestamp && !timestamp_override {
858 add_default_map_entry(
859 idempotent,
860 VergenKey::GitCommitTimestamp,
861 cargo_rustc_env,
862 cargo_warning,
863 );
864 }
865
866 if self.commit_timestamp_unix && !timestamp_unix_override {
867 add_default_map_entry(
868 idempotent,
869 VergenKey::GitCommitTimestampUnix,
870 cargo_rustc_env,
871 cargo_warning,
872 );
873 }
874 } else {
875 if self.commit_date && !date_override {
876 let format = format_description::parse_borrowed::<1>("[year]-[month]-[day]")?;
877 add_map_entry(
878 VergenKey::GitCommitDate,
879 ts.format(&format)?,
880 cargo_rustc_env,
881 );
882 }
883
884 if self.commit_timestamp && !timestamp_override {
885 add_map_entry(
886 VergenKey::GitCommitTimestamp,
887 ts.format(&Iso8601::DEFAULT)?,
888 cargo_rustc_env,
889 );
890 }
891
892 if self.commit_timestamp_unix && !timestamp_unix_override {
893 add_map_entry(
894 VergenKey::GitCommitTimestampUnix,
895 ts.unix_timestamp().to_string(),
896 cargo_rustc_env,
897 );
898 }
899 }
900 } else {
901 if self.commit_date && !date_override {
902 add_default_map_entry(
903 idempotent,
904 VergenKey::GitCommitDate,
905 cargo_rustc_env,
906 cargo_warning,
907 );
908 }
909
910 if self.commit_timestamp && !timestamp_override {
911 add_default_map_entry(
912 idempotent,
913 VergenKey::GitCommitTimestamp,
914 cargo_rustc_env,
915 cargo_warning,
916 );
917 }
918
919 if self.commit_timestamp_unix && !timestamp_unix_override {
920 add_default_map_entry(
921 idempotent,
922 VergenKey::GitCommitTimestampUnix,
923 cargo_rustc_env,
924 cargo_warning,
925 );
926 }
927 }
928
929 Ok(())
930 }
931
932 #[cfg_attr(coverage_nightly, coverage(off))]
933 fn compute_local_offset(&self, stdout: &str) -> Result<OffsetDateTime> {
935 let no_offset = OffsetDateTime::parse(stdout, &Rfc3339)?;
936 if self.use_local {
937 let local = UtcOffset::local_offset_at(no_offset)?;
938 let local_offset = no_offset.checked_to_offset(local).unwrap_or(no_offset);
939 Ok(local_offset)
940 } else {
941 Ok(no_offset)
942 }
943 }
944
945 fn compute_dirty(repo_path: Option<&PathBuf>, include_untracked: bool) -> Result<bool> {
946 let mut dirty_cmd = String::from(DIRTY);
947 if !include_untracked {
948 dirty_cmd.push_str(" --untracked-files=no");
949 }
950 let stdout = Self::run_cmd_checked(&dirty_cmd, repo_path)?;
951 Ok(!stdout.is_empty())
952 }
953
954 #[cfg(not(feature = "allow_remote"))]
955 #[allow(clippy::unused_self)]
956 fn cleanup(&self) {}
957
958 #[cfg(feature = "allow_remote")]
959 fn cleanup(&self) {
960 if let Some(_remote_url) = self.remote_url.as_ref() {
961 let temp_dir = temp_dir().join("vergen-gitcl");
962 if let Some(path) = &self.remote_repo_path {
964 if path.exists() {
965 let _ = std::fs::remove_dir_all(path).ok();
966 }
967 } else if temp_dir.exists() {
968 let _ = std::fs::remove_dir_all(temp_dir).ok();
969 }
970 }
971 }
972
973 #[cfg(all(not(test), feature = "allow_remote"))]
974 #[allow(clippy::unused_self)]
975 fn try_local(&self) -> bool {
976 true
977 }
978
979 #[cfg(all(test, feature = "allow_remote"))]
980 fn try_local(&self) -> bool {
981 self.force_local || !self.force_remote
982 }
983
984 #[cfg(all(not(test), feature = "allow_remote"))]
985 fn try_remote(&self) -> bool {
986 !self.force_local
987 }
988
989 #[cfg(all(test, feature = "allow_remote"))]
990 fn try_remote(&self) -> bool {
991 self.force_remote || !self.force_local
992 }
993
994 #[cfg(feature = "allow_remote")]
995 fn setup_repo_path(
996 &self,
997 repo_path: Option<&PathBuf>,
998 cargo_warning: &mut CargoWarning,
999 ) -> Result<Option<PathBuf>> {
1000 if self.try_local() && Self::check_inside_git_worktree(repo_path).is_ok() {
1001 Ok(repo_path.cloned())
1002 } else if self.try_remote()
1003 && let Some(remote_url) = &self.remote_url
1004 {
1005 let remote_path = if let Some(remote_path) = &self.remote_repo_path {
1006 remote_path.clone()
1007 } else {
1008 temp_dir().join("vergen-gitcl")
1009 };
1010 create_dir_all(&remote_path)?;
1011 if !self.clone_repo(remote_url, Some(&remote_path)) {
1012 return Err(anyhow!("Failed to clone git repository"));
1013 }
1014 cargo_warning.push(format!(
1015 "Using remote repository from '{remote_url}' at '{}'",
1016 remote_path.display()
1017 ));
1018 Ok(Some(remote_path))
1019 } else {
1020 Ok(repo_path.cloned())
1021 }
1022 }
1023
1024 #[cfg(not(feature = "allow_remote"))]
1025 #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
1026 fn setup_repo_path(
1027 &self,
1028 repo_path: Option<&PathBuf>,
1029 _cargo_warning: &mut CargoWarning,
1030 ) -> Result<Option<PathBuf>> {
1031 Ok(repo_path.cloned())
1032 }
1033
1034 #[cfg(feature = "vcs_info")]
1037 fn vcs_fallback(&self) -> Option<(String, bool)> {
1038 if self.vcs_info_fallback {
1039 vergen_lib::vcs_info()
1040 } else {
1041 None
1042 }
1043 }
1044
1045 #[cfg(not(feature = "vcs_info"))]
1046 #[allow(clippy::unused_self)]
1047 fn vcs_fallback(&self) -> Option<(String, bool)> {
1048 None
1049 }
1050}
1051
1052impl AddEntries for Gitcl {
1053 fn add_map_entries(
1054 &self,
1055 idempotent: bool,
1056 cargo_rustc_env: &mut CargoRustcEnvMap,
1057 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
1058 cargo_warning: &mut CargoWarning,
1059 ) -> Result<()> {
1060 if self.any() {
1061 let git_cmd = self.git_cmd.unwrap_or("git --version");
1062 Self::check_git(git_cmd)?;
1063
1064 let repo_path = self.setup_repo_path(self.local_repo_path.as_ref(), cargo_warning)?;
1065 let repo_path = repo_path.as_ref();
1066 Self::check_inside_git_worktree(repo_path)?;
1067
1068 self.inner_add_git_map_entries(
1069 repo_path,
1070 idempotent,
1071 cargo_rustc_env,
1072 cargo_rerun_if_changed,
1073 cargo_warning,
1074 )?;
1075
1076 self.cleanup();
1077 }
1078 Ok(())
1079 }
1080
1081 #[allow(clippy::too_many_lines)]
1082 fn add_default_entries(
1083 &self,
1084 config: &DefaultConfig,
1085 cargo_rustc_env_map: &mut CargoRustcEnvMap,
1086 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
1087 cargo_warning: &mut CargoWarning,
1088 ) -> Result<()> {
1089 if *config.fail_on_error() {
1090 let error = Error::msg(format!("{}", config.error()));
1091 Err(error)
1092 } else {
1093 cargo_warning.clear();
1096 cargo_rerun_if_changed.clear();
1097
1098 cargo_warning.push(format!("{}", config.error()));
1099
1100 let vcs = self.vcs_fallback();
1103
1104 if self.branch {
1105 add_default_map_entry(
1106 *config.idempotent(),
1107 VergenKey::GitBranch,
1108 cargo_rustc_env_map,
1109 cargo_warning,
1110 );
1111 }
1112 if self.commit_author_email {
1113 add_default_map_entry(
1114 *config.idempotent(),
1115 VergenKey::GitCommitAuthorEmail,
1116 cargo_rustc_env_map,
1117 cargo_warning,
1118 );
1119 }
1120 if self.commit_author_name {
1121 add_default_map_entry(
1122 *config.idempotent(),
1123 VergenKey::GitCommitAuthorName,
1124 cargo_rustc_env_map,
1125 cargo_warning,
1126 );
1127 }
1128 if self.commit_count {
1129 add_default_map_entry(
1130 *config.idempotent(),
1131 VergenKey::GitCommitCount,
1132 cargo_rustc_env_map,
1133 cargo_warning,
1134 );
1135 }
1136 if self.commit_date {
1137 add_default_map_entry(
1138 *config.idempotent(),
1139 VergenKey::GitCommitDate,
1140 cargo_rustc_env_map,
1141 cargo_warning,
1142 );
1143 }
1144 if self.commit_message {
1145 add_default_map_entry(
1146 *config.idempotent(),
1147 VergenKey::GitCommitMessage,
1148 cargo_rustc_env_map,
1149 cargo_warning,
1150 );
1151 }
1152 if self.commit_timestamp {
1153 add_default_map_entry(
1154 *config.idempotent(),
1155 VergenKey::GitCommitTimestamp,
1156 cargo_rustc_env_map,
1157 cargo_warning,
1158 );
1159 }
1160 if self.commit_timestamp_unix {
1161 add_default_map_entry(
1162 *config.idempotent(),
1163 VergenKey::GitCommitTimestampUnix,
1164 cargo_rustc_env_map,
1165 cargo_warning,
1166 );
1167 }
1168 if self.describe.is_some() {
1169 add_default_map_entry(
1170 *config.idempotent(),
1171 VergenKey::GitDescribe,
1172 cargo_rustc_env_map,
1173 cargo_warning,
1174 );
1175 }
1176 if self.sha.is_some() {
1177 if let Some((sha, _)) = &vcs {
1178 add_map_entry(VergenKey::GitSha, sha.clone(), cargo_rustc_env_map);
1179 } else {
1180 add_default_map_entry(
1181 *config.idempotent(),
1182 VergenKey::GitSha,
1183 cargo_rustc_env_map,
1184 cargo_warning,
1185 );
1186 }
1187 }
1188 if self.dirty.is_some() {
1189 if let Some((_, dirty)) = &vcs {
1190 add_map_entry(VergenKey::GitDirty, dirty.to_string(), cargo_rustc_env_map);
1191 } else {
1192 add_default_map_entry(
1193 *config.idempotent(),
1194 VergenKey::GitDirty,
1195 cargo_rustc_env_map,
1196 cargo_warning,
1197 );
1198 }
1199 }
1200 Ok(())
1201 }
1202 }
1203}
1204
1205#[cfg(test)]
1206mod test {
1207 use super::Gitcl;
1208 use crate::Emitter;
1209 use anyhow::Result;
1210 use serial_test::serial;
1211 #[cfg(unix)]
1212 use std::io::stdout;
1213 use std::{collections::BTreeMap, env::temp_dir, io::Write};
1214 #[cfg(unix)]
1215 use test_util::TEST_MTIME;
1216 use test_util::TestRepos;
1217 use vergen_lib::{VergenKey, count_idempotent};
1218
1219 #[test]
1220 #[serial]
1221 #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
1222 fn gitcl_clone_works() {
1223 let gitcl = Gitcl::all_git();
1224 let another = gitcl.clone();
1225 assert_eq!(another, gitcl);
1226 }
1227
1228 #[test]
1229 #[serial]
1230 fn gitcl_debug_works() -> Result<()> {
1231 let gitcl = Gitcl::all_git();
1232 let mut buf = vec![];
1233 write!(buf, "{gitcl:?}")?;
1234 assert!(!buf.is_empty());
1235 Ok(())
1236 }
1237
1238 #[test]
1239 #[serial]
1240 fn gitcl_default() -> Result<()> {
1241 let gitcl = Gitcl::builder().build();
1242 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1243 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1244 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1245 assert_eq!(0, emitter.cargo_warning().len());
1246 Ok(())
1247 }
1248
1249 #[test]
1250 #[serial]
1251 fn bad_command_is_error() -> Result<()> {
1252 let mut map = BTreeMap::new();
1253 assert!(
1254 Gitcl::add_git_cmd_entry(
1255 "such_a_terrible_cmd",
1256 None,
1257 VergenKey::GitCommitMessage,
1258 &mut map
1259 )
1260 .is_err()
1261 );
1262 Ok(())
1263 }
1264
1265 #[test]
1266 #[serial]
1267 fn non_working_tree_is_error() -> Result<()> {
1268 assert!(Gitcl::check_inside_git_worktree(Some(&temp_dir())).is_err());
1269 Ok(())
1270 }
1271
1272 #[test]
1273 #[serial]
1274 fn invalid_git_is_error() -> Result<()> {
1275 assert!(Gitcl::check_git("such_a_terrible_cmd -v").is_err());
1276 Ok(())
1277 }
1278
1279 #[cfg(not(target_family = "windows"))]
1280 #[test]
1281 #[serial]
1282 fn shell_env_works() -> Result<()> {
1283 temp_env::with_var("SHELL", Some("bash"), || {
1284 let mut map = BTreeMap::new();
1285 assert!(
1286 Gitcl::add_git_cmd_entry("git -v", None, VergenKey::GitCommitMessage, &mut map)
1287 .is_ok()
1288 );
1289 });
1290 Ok(())
1291 }
1292
1293 #[test]
1294 #[serial]
1295 fn git_all_idempotent() -> Result<()> {
1296 let gitcl = Gitcl::all_git();
1297 let emitter = Emitter::default()
1298 .idempotent()
1299 .add_instructions(&gitcl)?
1300 .test_emit();
1301 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1302 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1303 assert_eq!(2, emitter.cargo_warning().len());
1304 Ok(())
1305 }
1306
1307 #[test]
1308 #[serial]
1309 fn git_all_idempotent_no_warn() -> Result<()> {
1310 let gitcl = Gitcl::all_git();
1311 let emitter = Emitter::default()
1312 .idempotent()
1313 .quiet()
1314 .add_instructions(&gitcl)?
1315 .test_emit();
1316 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1317 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1318 assert_eq!(2, emitter.cargo_warning().len());
1319 Ok(())
1320 }
1321
1322 #[test]
1323 #[serial]
1324 fn git_all_at_path() -> Result<()> {
1325 let repo = TestRepos::new(false, false, false)?;
1326 let mut gitcl = Gitcl::all_git();
1327 let _ = gitcl.at_path(repo.path());
1328 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1329 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1330 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1331 assert_eq!(0, emitter.cargo_warning().len());
1332 Ok(())
1333 }
1334
1335 #[test]
1336 #[serial]
1337 fn git_all() -> Result<()> {
1338 let gitcl = Gitcl::all_git();
1339 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1340 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1341 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1342 assert_eq!(0, emitter.cargo_warning().len());
1343 Ok(())
1344 }
1345
1346 #[test]
1347 #[serial]
1348 fn git_all_shallow_clone() -> Result<()> {
1349 let repo = TestRepos::new(false, false, true)?;
1350 let mut gitcl = Gitcl::all_git();
1351 let _ = gitcl.at_path(repo.path());
1352 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1353 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1354 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1355 assert_eq!(0, emitter.cargo_warning().len());
1356 Ok(())
1357 }
1358
1359 #[test]
1360 #[serial]
1361 fn git_all_dirty_tags_short() -> Result<()> {
1362 let gitcl = Gitcl::builder()
1363 .all()
1364 .describe(true, true, None)
1365 .sha(true)
1366 .build();
1367 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1368 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1369 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1370 assert_eq!(0, emitter.cargo_warning().len());
1371 Ok(())
1372 }
1373
1374 #[test]
1375 #[serial]
1376 fn fails_on_bad_git_command() -> Result<()> {
1377 let mut gitcl = Gitcl::all_git();
1378 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1379 assert!(
1380 Emitter::default()
1381 .fail_on_error()
1382 .add_instructions(&gitcl)
1383 .is_err()
1384 );
1385 Ok(())
1386 }
1387
1388 #[test]
1389 #[serial]
1390 fn defaults_on_bad_git_command() -> Result<()> {
1391 let mut gitcl = Gitcl::all_git();
1392 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1393 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1394 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1395 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1396 assert_eq!(11, emitter.cargo_warning().len());
1397 Ok(())
1398 }
1399
1400 #[test]
1401 #[serial]
1402 fn idempotent_on_bad_git_command() -> Result<()> {
1403 let mut gitcl = Gitcl::all_git();
1404 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1405 let emitter = Emitter::default()
1406 .idempotent()
1407 .add_instructions(&gitcl)?
1408 .test_emit();
1409 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1410 assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
1411 assert_eq!(11, emitter.cargo_warning().len());
1412 Ok(())
1413 }
1414
1415 #[test]
1416 #[serial]
1417 fn bad_timestamp_defaults() -> Result<()> {
1418 let mut map = BTreeMap::new();
1419 let mut warnings = vec![];
1420 let gitcl = Gitcl::all_git();
1421 assert!(
1422 gitcl
1423 .add_git_timestamp_entries(
1424 "this_is_not_a_git_cmd",
1425 None,
1426 false,
1427 &mut map,
1428 &mut warnings
1429 )
1430 .is_ok()
1431 );
1432 assert_eq!(0, map.len());
1433 assert_eq!(2, warnings.len());
1434 Ok(())
1435 }
1436
1437 #[test]
1438 #[serial]
1439 fn bad_timestamp_idempotent() -> Result<()> {
1440 let mut map = BTreeMap::new();
1441 let mut warnings = vec![];
1442 let gitcl = Gitcl::all_git();
1443 assert!(
1444 gitcl
1445 .add_git_timestamp_entries(
1446 "this_is_not_a_git_cmd",
1447 None,
1448 true,
1449 &mut map,
1450 &mut warnings
1451 )
1452 .is_ok()
1453 );
1454 assert_eq!(2, map.len());
1455 assert_eq!(2, warnings.len());
1456 Ok(())
1457 }
1458
1459 #[test]
1460 #[serial]
1461 fn source_date_epoch_does_not_affect_git() {
1462 fn emit() -> Result<String> {
1466 let mut stdout_buf = vec![];
1467 let gitcl = Gitcl::builder()
1468 .commit_date(true)
1469 .commit_timestamp(true)
1470 .build();
1471 _ = Emitter::new()
1472 .add_instructions(&gitcl)?
1473 .emit_to(&mut stdout_buf)?;
1474 Ok(String::from_utf8_lossy(&stdout_buf).into_owned())
1475 }
1476 let without = temp_env::with_var("SOURCE_DATE_EPOCH", None::<&str>, emit);
1477 let with = temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), emit);
1478 assert_eq!(without.unwrap(), with.unwrap());
1479 }
1480
1481 #[test]
1482 #[serial]
1483 #[cfg(unix)]
1484 fn bad_source_date_epoch_ignored_by_git() {
1485 use std::ffi::OsStr;
1486 use std::os::unix::prelude::OsStrExt;
1487
1488 let source = [0x66, 0x6f, 0x80, 0x6f];
1491 let os_str = OsStr::from_bytes(&source[..]);
1492 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1493 let result = || -> Result<bool> {
1494 let mut stdout_buf = vec![];
1495 let gitcl = Gitcl::builder().commit_date(true).build();
1496 Emitter::new()
1497 .idempotent()
1498 .fail_on_error()
1499 .add_instructions(&gitcl)?
1500 .emit_to(&mut stdout_buf)
1501 }();
1502 assert!(result.is_ok());
1503 });
1504 }
1505
1506 #[test]
1507 #[serial]
1508 #[cfg(unix)]
1509 fn bad_source_date_epoch_defaults() {
1510 use std::ffi::OsStr;
1511 use std::os::unix::prelude::OsStrExt;
1512
1513 let source = [0x66, 0x6f, 0x80, 0x6f];
1514 let os_str = OsStr::from_bytes(&source[..]);
1515 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1516 let result = || -> Result<bool> {
1517 let mut stdout_buf = vec![];
1518 let gitcl = Gitcl::builder().commit_date(true).build();
1519 Emitter::new()
1520 .idempotent()
1521 .add_instructions(&gitcl)?
1522 .emit_to(&mut stdout_buf)
1523 }();
1524 assert!(result.is_ok());
1525 });
1526 }
1527
1528 #[test]
1529 #[serial]
1530 #[cfg(windows)]
1531 fn bad_source_date_epoch_ignored_by_git() {
1532 use std::ffi::OsString;
1533 use std::os::windows::prelude::OsStringExt;
1534
1535 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1538 let os_string = OsString::from_wide(&source[..]);
1539 let os_str = os_string.as_os_str();
1540 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1541 let result = || -> Result<bool> {
1542 let mut stdout_buf = vec![];
1543 let gitcl = Gitcl::builder().commit_date(true).build();
1544 Emitter::new()
1545 .fail_on_error()
1546 .idempotent()
1547 .add_instructions(&gitcl)?
1548 .emit_to(&mut stdout_buf)
1549 }();
1550 assert!(result.is_ok());
1551 });
1552 }
1553
1554 #[test]
1555 #[serial]
1556 #[cfg(windows)]
1557 fn bad_source_date_epoch_defaults() {
1558 use std::ffi::OsString;
1559 use std::os::windows::prelude::OsStringExt;
1560
1561 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1562 let os_string = OsString::from_wide(&source[..]);
1563 let os_str = os_string.as_os_str();
1564 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1565 let result = || -> Result<bool> {
1566 let mut stdout_buf = vec![];
1567 let gitcl = Gitcl::builder().commit_date(true).build();
1568 Emitter::new()
1569 .idempotent()
1570 .add_instructions(&gitcl)?
1571 .emit_to(&mut stdout_buf)
1572 }();
1573 assert!(result.is_ok());
1574 });
1575 }
1576
1577 #[test]
1578 #[serial]
1579 #[cfg(unix)]
1580 fn git_no_index_update() -> Result<()> {
1581 let repo = TestRepos::new(true, true, false)?;
1582 repo.set_index_magic_mtime()?;
1583
1584 let mut gitcl = Gitcl::builder().all().describe(true, true, None).build();
1586 let _ = gitcl.at_path(repo.path());
1587 let failed = Emitter::default()
1588 .add_instructions(&gitcl)?
1589 .emit_to(&mut stdout())?;
1590 assert!(!failed);
1591
1592 assert_eq!(*TEST_MTIME, repo.get_index_magic_mtime()?);
1593 Ok(())
1594 }
1595
1596 #[test]
1597 #[serial]
1598 #[cfg(feature = "allow_remote")]
1599 fn remote_clone_works() -> Result<()> {
1600 let gitcl = Gitcl::all()
1601 .force_remote(true)
1603 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1604 .describe(true, true, None)
1605 .build();
1606 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1607 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1608 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1609 assert_eq!(1, emitter.cargo_warning().len());
1610 Ok(())
1611 }
1612
1613 #[test]
1614 #[serial]
1615 #[cfg(feature = "allow_remote")]
1616 fn remote_clone_with_path_works() -> Result<()> {
1617 let remote_path = temp_dir().join("blah");
1618 let gitcl = Gitcl::all()
1619 .force_remote(true)
1621 .remote_repo_path(&remote_path)
1622 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1623 .describe(true, true, None)
1624 .build();
1625 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1626 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1627 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1628 assert_eq!(1, emitter.cargo_warning().len());
1629 Ok(())
1630 }
1631
1632 #[test]
1633 #[serial]
1634 #[cfg(feature = "allow_remote")]
1635 fn remote_clone_with_force_local_works() -> Result<()> {
1636 let gitcl = Gitcl::all()
1637 .force_local(true)
1638 .force_remote(true)
1640 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1641 .describe(true, true, None)
1642 .build();
1643 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1644 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1645 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1646 assert_eq!(0, emitter.cargo_warning().len());
1647 Ok(())
1648 }
1649
1650 #[test]
1651 #[serial]
1652 #[cfg(feature = "allow_remote")]
1653 fn remote_clone_with_tag_works() -> Result<()> {
1654 let gitcl = Gitcl::all()
1655 .force_remote(true)
1657 .remote_tag("0.3.9")
1658 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1659 .describe(true, true, None)
1660 .build();
1661 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1662 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1663 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1664 assert_eq!(1, emitter.cargo_warning().len());
1665 Ok(())
1666 }
1667
1668 #[test]
1669 #[serial]
1670 #[cfg(feature = "allow_remote")]
1671 fn remote_clone_with_depth_works() -> Result<()> {
1672 let gitcl = Gitcl::all()
1673 .force_remote(true)
1675 .fetch_depth(200)
1676 .remote_tag("0.3.9")
1677 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1678 .describe(true, true, None)
1679 .build();
1680 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1681 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1682 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1683 assert_eq!(1, emitter.cargo_warning().len());
1684 Ok(())
1685 }
1686
1687 #[test]
1688 #[serial]
1689 #[cfg(feature = "vcs_info")]
1690 fn vcs_info_fallback_populates_sha_and_dirty() -> Result<()> {
1691 let tmp = temp_dir().join(format!("vergen_gitcl_vcs_info_{}", std::process::id()));
1692 std::fs::create_dir_all(&tmp)?;
1693 let mut file = std::fs::File::create(tmp.join(".cargo_vcs_info.json"))?;
1694 write!(
1695 file,
1696 r#"{{"git":{{"sha1":"deadbeef","dirty":true}},"path_in_vcs":""}}"#
1697 )?;
1698
1699 let gitcl = Gitcl::builder()
1700 .sha(false)
1701 .dirty(false)
1702 .vcs_info_fallback(true)
1703 .local_repo_path(tmp.join("not-a-repo"))
1706 .build();
1707
1708 let mut stdout_buf = vec![];
1709 temp_env::with_var(
1710 "CARGO_MANIFEST_DIR",
1711 Some(tmp.as_os_str()),
1712 || -> Result<()> {
1713 let mut emitter = Emitter::default();
1714 _ = emitter.add_instructions(&gitcl)?.emit_to(&mut stdout_buf)?;
1715 Ok(())
1716 },
1717 )?;
1718
1719 let output = String::from_utf8_lossy(&stdout_buf);
1720 let _ = std::fs::remove_dir_all(&tmp).ok();
1721 assert!(
1722 output.contains("cargo:rustc-env=VERGEN_GIT_SHA=deadbeef"),
1723 "{output}"
1724 );
1725 assert!(
1726 output.contains("cargo:rustc-env=VERGEN_GIT_DIRTY=true"),
1727 "{output}"
1728 );
1729 Ok(())
1730 }
1731
1732 #[test]
1733 #[serial]
1734 fn commit_timestamp_unix_works() -> Result<()> {
1735 let gitcl = Gitcl::builder().commit_timestamp_unix(true).build();
1736 let mut stdout_buf = vec![];
1737 _ = Emitter::default()
1738 .add_instructions(&gitcl)?
1739 .emit_to(&mut stdout_buf)?;
1740 let output = String::from_utf8_lossy(&stdout_buf);
1741 let line = output
1742 .lines()
1743 .find(|l| l.starts_with("cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP_UNIX="))
1744 .expect("unix commit timestamp emitted");
1745 let value = line.trim_start_matches("cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP_UNIX=");
1746 assert!(value.parse::<i64>().is_ok(), "value: {value}");
1747 Ok(())
1748 }
1749
1750 #[test]
1751 #[serial]
1752 fn commit_timestamp_unix_idempotent() -> Result<()> {
1753 let gitcl = Gitcl::builder().commit_timestamp_unix(true).build();
1754 let emitter = Emitter::default()
1755 .idempotent()
1756 .add_instructions(&gitcl)?
1757 .test_emit();
1758 assert_eq!(1, emitter.cargo_rustc_env_map().len());
1759 assert_eq!(1, count_idempotent(emitter.cargo_rustc_env_map()));
1760 Ok(())
1761 }
1762
1763 #[test]
1764 #[serial]
1765 fn commit_timestamp_unix_override_works() {
1766 temp_env::with_var("VERGEN_GIT_COMMIT_TIMESTAMP_UNIX", Some("12345"), || {
1767 let result = || -> Result<()> {
1768 let gitcl = Gitcl::builder().commit_timestamp_unix(true).build();
1769 let mut stdout_buf = vec![];
1770 _ = Emitter::default()
1771 .add_instructions(&gitcl)?
1772 .emit_to(&mut stdout_buf)?;
1773 let output = String::from_utf8_lossy(&stdout_buf);
1774 assert!(
1775 output.contains("cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP_UNIX=12345"),
1776 "{output}"
1777 );
1778 Ok(())
1779 }();
1780 assert!(result.is_ok());
1781 });
1782 }
1783}