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)]
205#[allow(clippy::struct_excessive_bools)]
206pub struct Gitcl {
207 #[builder(field)]
211 all: bool,
212 #[builder(into)]
214 local_repo_path: Option<PathBuf>,
215 #[builder(default = false)]
217 force_local: bool,
218 #[cfg(test)]
220 #[builder(default = false)]
221 force_remote: bool,
222 #[builder(into)]
224 remote_url: Option<String>,
225 #[builder(into)]
228 remote_repo_path: Option<PathBuf>,
229 #[builder(into)]
231 remote_tag: Option<String>,
232 #[builder(default = 100)]
234 fetch_depth: usize,
235 #[cfg(feature = "vcs_info")]
239 #[builder(default = false)]
240 vcs_info_fallback: bool,
241 #[builder(default = all)]
248 branch: bool,
249 #[builder(default = all)]
256 commit_author_name: bool,
257 #[builder(default = all)]
264 commit_author_email: bool,
265 #[builder(default = all)]
271 commit_count: bool,
272 #[builder(default = all)]
279 commit_message: bool,
280 #[doc = concat!(commit_date!())]
289 #[builder(default = all)]
291 commit_date: bool,
292 #[builder(default = all)]
299 commit_timestamp: bool,
300 #[builder(default = false)]
308 commit_timestamp_unix: bool,
309 #[builder(
327 required,
328 default = all.then(|| Describe::builder().build()),
329 with = |tags: bool, dirty: bool, match_pattern: Option<&'static str>| {
330 Some(Describe::builder().tags(tags).dirty(dirty).maybe_match_pattern(match_pattern).build())
331 }
332 )]
333 describe: Option<Describe>,
334 #[builder(
346 required,
347 default = all.then(|| Sha::builder().build()),
348 with = |short: bool| Some(Sha::builder().short(short).build())
349 )]
350 sha: Option<Sha>,
351 #[builder(
361 required,
362 default = all.then(|| Dirty::builder().build()),
363 with = |include_untracked: bool| Some(Dirty::builder().include_untracked(include_untracked).build())
364 )]
365 dirty: Option<Dirty>,
366 #[builder(default = false)]
368 use_local: bool,
369 git_cmd: Option<&'static str>,
371}
372
373impl<S: gitcl_builder::State> GitclBuilder<S> {
374 fn all(mut self) -> Self {
379 self.all = true;
380 self
381 }
382}
383
384impl Gitcl {
385 #[must_use]
387 pub fn all_git() -> Gitcl {
388 Self::builder().all().build()
389 }
390
391 pub fn all() -> GitclBuilder<Empty> {
393 Self::builder().all()
394 }
395
396 fn any(&self) -> bool {
397 self.branch
398 || self.commit_author_email
399 || self.commit_author_name
400 || self.commit_count
401 || self.commit_date
402 || self.commit_message
403 || self.commit_timestamp
404 || self.commit_timestamp_unix
405 || self.describe.is_some()
406 || self.sha.is_some()
407 || self.dirty.is_some()
408 }
409
410 pub fn at_path(&mut self, path: PathBuf) -> &mut Self {
412 self.local_repo_path = Some(path);
413 self
414 }
415
416 pub fn git_cmd(&mut self, cmd: Option<&'static str>) -> &mut Self {
425 self.git_cmd = cmd;
426 self
427 }
428
429 fn check_git(cmd: &str) -> Result<()> {
430 if Self::git_cmd_exists(cmd) {
431 Ok(())
432 } else {
433 Err(anyhow!("no suitable 'git' command found!"))
434 }
435 }
436
437 fn check_inside_git_worktree(path: Option<&PathBuf>) -> Result<()> {
438 if Self::inside_git_worktree(path) {
439 Ok(())
440 } else {
441 Err(anyhow!("not within a suitable 'git' worktree!"))
442 }
443 }
444
445 fn git_cmd_exists(cmd: &str) -> bool {
446 Self::run_cmd(cmd, None).is_ok_and(|output| output.status.success())
447 }
448
449 #[cfg(feature = "allow_remote")]
450 fn clone_repo(&self, remote_url: &str, path: Option<&PathBuf>) -> bool {
451 let cmd = if let Some(remote_tag) = self.remote_tag.as_deref() {
452 format!("git clone --branch {remote_tag} --single-branch {remote_url} .")
453 } else {
454 format!("git clone --depth {} {remote_url} .", self.fetch_depth)
455 };
456
457 Self::run_cmd(&cmd, path).is_ok_and(|output| output.status.success())
458 }
459
460 fn inside_git_worktree(path: Option<&PathBuf>) -> bool {
461 Self::run_cmd("git rev-parse --is-inside-work-tree", path).is_ok_and(|output| {
462 let stdout = String::from_utf8_lossy(&output.stdout);
463 output.status.success() && stdout.contains("true")
464 })
465 }
466
467 #[cfg(not(target_env = "msvc"))]
468 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
469 let shell = if let Some(shell_path) = env::var_os("SHELL") {
470 shell_path.to_string_lossy().into_owned()
471 } else {
472 "sh".to_string()
474 };
475 let mut cmd = Command::new(shell);
476 if let Some(path) = path_opt {
477 _ = cmd.current_dir(path);
478 }
479 _ = cmd.env("GIT_OPTIONAL_LOCKS", "0");
481 _ = cmd.arg("-c");
482 _ = cmd.arg(command);
483 _ = cmd.stdout(Stdio::piped());
484 _ = cmd.stderr(Stdio::piped());
485
486 let output = cmd.output()?;
487 if !output.status.success() {
488 eprintln!("Command failed: `{command}`");
489 eprintln!("--- stdout:\n{}\n", String::from_utf8_lossy(&output.stdout));
490 eprintln!("--- stderr:\n{}\n", String::from_utf8_lossy(&output.stderr));
491 }
492
493 Ok(output)
494 }
495
496 #[cfg(target_env = "msvc")]
497 fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
498 let mut cmd = Command::new("cmd");
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 fn run_cmd_checked(command: &str, path_opt: Option<&PathBuf>) -> Result<Vec<u8>> {
520 let output = Self::run_cmd(command, path_opt)?;
521 if output.status.success() {
522 Ok(output.stdout)
523 } else {
524 let stderr = String::from_utf8_lossy(&output.stderr);
525 Err(anyhow!("Failed to run '{command}'! {stderr}"))
526 }
527 }
528
529 #[allow(clippy::too_many_lines)]
530 fn inner_add_git_map_entries(
531 &self,
532 repo_path: Option<&PathBuf>,
533 idempotent: bool,
534 cargo_rustc_env: &mut CargoRustcEnvMap,
535 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
536 cargo_warning: &mut CargoWarning,
537 ) -> Result<()> {
538 if !idempotent && self.any() {
539 Self::add_rerun_if_changed(cargo_rerun_if_changed, repo_path)?;
540 }
541
542 if self.branch {
543 if let Ok(_value) = env::var(GIT_BRANCH_NAME) {
544 add_default_map_entry(
545 idempotent,
546 VergenKey::GitBranch,
547 cargo_rustc_env,
548 cargo_warning,
549 );
550 } else {
551 Self::add_git_cmd_entry(
552 BRANCH_CMD,
553 repo_path,
554 VergenKey::GitBranch,
555 cargo_rustc_env,
556 )?;
557 }
558 }
559
560 if self.commit_author_email {
561 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_EMAIL) {
562 add_default_map_entry(
563 idempotent,
564 VergenKey::GitCommitAuthorEmail,
565 cargo_rustc_env,
566 cargo_warning,
567 );
568 } else {
569 Self::add_git_cmd_entry(
570 COMMIT_AUTHOR_EMAIL,
571 repo_path,
572 VergenKey::GitCommitAuthorEmail,
573 cargo_rustc_env,
574 )?;
575 }
576 }
577
578 if self.commit_author_name {
579 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_NAME) {
580 add_default_map_entry(
581 idempotent,
582 VergenKey::GitCommitAuthorName,
583 cargo_rustc_env,
584 cargo_warning,
585 );
586 } else {
587 Self::add_git_cmd_entry(
588 COMMIT_AUTHOR_NAME,
589 repo_path,
590 VergenKey::GitCommitAuthorName,
591 cargo_rustc_env,
592 )?;
593 }
594 }
595
596 if self.commit_count {
597 if let Ok(_value) = env::var(GIT_COMMIT_COUNT) {
598 add_default_map_entry(
599 idempotent,
600 VergenKey::GitCommitCount,
601 cargo_rustc_env,
602 cargo_warning,
603 );
604 } else {
605 Self::add_git_cmd_entry(
606 COMMIT_COUNT,
607 repo_path,
608 VergenKey::GitCommitCount,
609 cargo_rustc_env,
610 )?;
611 }
612 }
613
614 self.add_git_timestamp_entries(
615 COMMIT_TIMESTAMP,
616 repo_path,
617 idempotent,
618 cargo_rustc_env,
619 cargo_warning,
620 )?;
621
622 if self.commit_message {
623 if let Ok(_value) = env::var(GIT_COMMIT_MESSAGE) {
624 add_default_map_entry(
625 idempotent,
626 VergenKey::GitCommitMessage,
627 cargo_rustc_env,
628 cargo_warning,
629 );
630 } else {
631 Self::add_git_cmd_entry(
632 COMMIT_MESSAGE,
633 repo_path,
634 VergenKey::GitCommitMessage,
635 cargo_rustc_env,
636 )?;
637 }
638 }
639
640 let mut dirty_cache = None; if let Some(dirty) = self.dirty {
642 if let Ok(_value) = env::var(GIT_DIRTY_NAME) {
643 add_default_map_entry(
644 idempotent,
645 VergenKey::GitDirty,
646 cargo_rustc_env,
647 cargo_warning,
648 );
649 } else {
650 let use_dirty = Self::compute_dirty(repo_path, dirty.include_untracked())?;
651 if !dirty.include_untracked() {
652 dirty_cache = Some(use_dirty);
653 }
654 add_map_entry(
655 VergenKey::GitDirty,
656 bool::to_string(&use_dirty),
657 cargo_rustc_env,
658 );
659 }
660 }
661
662 if let Some(describe) = self.describe {
663 if let Ok(_value) = env::var(GIT_DESCRIBE_NAME) {
668 add_default_map_entry(
669 idempotent,
670 VergenKey::GitDescribe,
671 cargo_rustc_env,
672 cargo_warning,
673 );
674 } else {
675 let mut describe_cmd = String::from(DESCRIBE);
676 if describe.tags() {
677 describe_cmd.push_str(" --tags");
678 }
679 if let Some(pattern) = *describe.match_pattern() {
680 Self::match_pattern_cmd_str(&mut describe_cmd, pattern);
681 }
682 let stdout = Self::run_cmd_checked(&describe_cmd, repo_path)?;
683 let mut describe_value = String::from_utf8_lossy(&stdout).trim().to_string();
684 if describe.dirty()
685 && (dirty_cache.is_some_and(|dirty| dirty)
686 || Self::compute_dirty(repo_path, false)?)
687 {
688 describe_value.push_str("-dirty");
689 }
690 add_map_entry(VergenKey::GitDescribe, describe_value, cargo_rustc_env);
691 }
692 }
693
694 if let Some(sha) = self.sha {
695 if let Ok(_value) = env::var(GIT_SHA_NAME) {
696 add_default_map_entry(
697 idempotent,
698 VergenKey::GitSha,
699 cargo_rustc_env,
700 cargo_warning,
701 );
702 } else {
703 let mut sha_cmd = String::from(SHA);
704 if sha.short() {
705 sha_cmd.push_str(" --short");
706 }
707 sha_cmd.push_str(" HEAD");
708 Self::add_git_cmd_entry(&sha_cmd, repo_path, VergenKey::GitSha, cargo_rustc_env)?;
709 }
710 }
711
712 Ok(())
713 }
714
715 fn add_rerun_if_changed(
716 rerun_if_changed: &mut Vec<String>,
717 path: Option<&PathBuf>,
718 ) -> Result<()> {
719 let git_path = Self::run_cmd("git rev-parse --git-dir", path)?;
720 if git_path.status.success() {
721 let git_path_str = String::from_utf8_lossy(&git_path.stdout).trim().to_string();
722 let git_path = PathBuf::from(&git_path_str);
723
724 let mut head_path = git_path.clone();
726 head_path.push("HEAD");
727
728 if head_path.exists() {
729 rerun_if_changed.push(format!("{}", head_path.display()));
730 }
731
732 let refp = Self::setup_ref_path(path)?;
734 if refp.status.success() {
735 let ref_path_str = String::from_utf8_lossy(&refp.stdout).trim().to_string();
736 let mut ref_path = git_path;
737 ref_path.push(ref_path_str);
738 if ref_path.exists() {
739 rerun_if_changed.push(format!("{}", ref_path.display()));
740 }
741 }
742 }
743 Ok(())
744 }
745
746 #[cfg(not(target_os = "windows"))]
747 fn match_pattern_cmd_str(describe_cmd: &mut String, pattern: &str) {
748 describe_cmd.push_str(" --match \"");
749 describe_cmd.push_str(pattern);
750 describe_cmd.push('\"');
751 }
752
753 #[cfg(target_os = "windows")]
754 fn match_pattern_cmd_str(describe_cmd: &mut String, pattern: &str) {
755 describe_cmd.push_str(" --match ");
756 describe_cmd.push_str(pattern);
757 }
758
759 #[cfg(not(test))]
760 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
761 Self::run_cmd("git symbolic-ref HEAD", path)
762 }
763
764 #[cfg(all(test, not(target_os = "windows")))]
765 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
766 Self::run_cmd("pwd", path)
767 }
768
769 #[cfg(all(test, target_os = "windows"))]
770 fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
771 Self::run_cmd("cd", path)
772 }
773
774 fn add_git_cmd_entry(
775 cmd: &str,
776 path: Option<&PathBuf>,
777 key: VergenKey,
778 cargo_rustc_env: &mut CargoRustcEnvMap,
779 ) -> Result<()> {
780 let stdout = Self::run_cmd_checked(cmd, path)?;
781 let stdout = String::from_utf8_lossy(&stdout)
782 .trim()
783 .trim_matches('\'')
784 .to_string();
785 add_map_entry(key, stdout, cargo_rustc_env);
786 Ok(())
787 }
788
789 #[allow(clippy::too_many_lines)]
790 fn add_git_timestamp_entries(
791 &self,
792 cmd: &str,
793 path: Option<&PathBuf>,
794 idempotent: bool,
795 cargo_rustc_env: &mut CargoRustcEnvMap,
796 cargo_warning: &mut CargoWarning,
797 ) -> Result<()> {
798 let mut date_override = false;
799 if let Ok(_value) = env::var(GIT_COMMIT_DATE_NAME) {
800 add_default_map_entry(
801 idempotent,
802 VergenKey::GitCommitDate,
803 cargo_rustc_env,
804 cargo_warning,
805 );
806 date_override = true;
807 }
808
809 let mut timestamp_override = false;
810 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
811 add_default_map_entry(
812 idempotent,
813 VergenKey::GitCommitTimestamp,
814 cargo_rustc_env,
815 cargo_warning,
816 );
817 timestamp_override = true;
818 }
819
820 let mut timestamp_unix_override = false;
821 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_UNIX_NAME) {
822 add_default_map_entry(
823 idempotent,
824 VergenKey::GitCommitTimestampUnix,
825 cargo_rustc_env,
826 cargo_warning,
827 );
828 timestamp_unix_override = true;
829 }
830
831 let output = Self::run_cmd(cmd, path)?;
832 if output.status.success() {
833 let stdout = String::from_utf8_lossy(&output.stdout)
834 .lines()
835 .last()
836 .ok_or_else(|| anyhow!("invalid 'git log' output"))?
837 .trim()
838 .trim_matches('\'')
839 .to_string();
840
841 let ts = self.compute_local_offset(&stdout)?;
845
846 if idempotent {
847 if self.commit_date && !date_override {
848 add_default_map_entry(
849 idempotent,
850 VergenKey::GitCommitDate,
851 cargo_rustc_env,
852 cargo_warning,
853 );
854 }
855
856 if self.commit_timestamp && !timestamp_override {
857 add_default_map_entry(
858 idempotent,
859 VergenKey::GitCommitTimestamp,
860 cargo_rustc_env,
861 cargo_warning,
862 );
863 }
864
865 if self.commit_timestamp_unix && !timestamp_unix_override {
866 add_default_map_entry(
867 idempotent,
868 VergenKey::GitCommitTimestampUnix,
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 if self.commit_timestamp_unix && !timestamp_unix_override {
892 add_map_entry(
893 VergenKey::GitCommitTimestampUnix,
894 ts.unix_timestamp().to_string(),
895 cargo_rustc_env,
896 );
897 }
898 }
899 } else {
900 if self.commit_date && !date_override {
901 add_default_map_entry(
902 idempotent,
903 VergenKey::GitCommitDate,
904 cargo_rustc_env,
905 cargo_warning,
906 );
907 }
908
909 if self.commit_timestamp && !timestamp_override {
910 add_default_map_entry(
911 idempotent,
912 VergenKey::GitCommitTimestamp,
913 cargo_rustc_env,
914 cargo_warning,
915 );
916 }
917
918 if self.commit_timestamp_unix && !timestamp_unix_override {
919 add_default_map_entry(
920 idempotent,
921 VergenKey::GitCommitTimestampUnix,
922 cargo_rustc_env,
923 cargo_warning,
924 );
925 }
926 }
927
928 Ok(())
929 }
930
931 #[cfg_attr(coverage_nightly, coverage(off))]
932 fn compute_local_offset(&self, stdout: &str) -> Result<OffsetDateTime> {
934 let no_offset = OffsetDateTime::parse(stdout, &Rfc3339)?;
935 if self.use_local {
936 let local = UtcOffset::local_offset_at(no_offset)?;
937 let local_offset = no_offset.checked_to_offset(local).unwrap_or(no_offset);
938 Ok(local_offset)
939 } else {
940 Ok(no_offset)
941 }
942 }
943
944 fn compute_dirty(repo_path: Option<&PathBuf>, include_untracked: bool) -> Result<bool> {
945 let mut dirty_cmd = String::from(DIRTY);
946 if !include_untracked {
947 dirty_cmd.push_str(" --untracked-files=no");
948 }
949 let stdout = Self::run_cmd_checked(&dirty_cmd, repo_path)?;
950 Ok(!stdout.is_empty())
951 }
952
953 #[cfg(not(feature = "allow_remote"))]
954 #[allow(clippy::unused_self)]
955 fn cleanup(&self) {}
956
957 #[cfg(feature = "allow_remote")]
958 fn cleanup(&self) {
959 if let Some(_remote_url) = self.remote_url.as_ref() {
960 let temp_dir = temp_dir().join("vergen-gitcl");
961 if let Some(path) = &self.remote_repo_path {
963 if path.exists() {
964 let _ = std::fs::remove_dir_all(path).ok();
965 }
966 } else if temp_dir.exists() {
967 let _ = std::fs::remove_dir_all(temp_dir).ok();
968 }
969 }
970 }
971
972 #[cfg(all(not(test), feature = "allow_remote"))]
973 #[allow(clippy::unused_self)]
974 fn try_local(&self) -> bool {
975 true
976 }
977
978 #[cfg(all(test, feature = "allow_remote"))]
979 fn try_local(&self) -> bool {
980 self.force_local || !self.force_remote
981 }
982
983 #[cfg(all(not(test), feature = "allow_remote"))]
984 fn try_remote(&self) -> bool {
985 !self.force_local
986 }
987
988 #[cfg(all(test, feature = "allow_remote"))]
989 fn try_remote(&self) -> bool {
990 self.force_remote || !self.force_local
991 }
992
993 #[cfg(feature = "allow_remote")]
994 fn setup_repo_path(
995 &self,
996 repo_path: Option<&PathBuf>,
997 cargo_warning: &mut CargoWarning,
998 ) -> Result<Option<PathBuf>> {
999 if self.try_local() && Self::check_inside_git_worktree(repo_path).is_ok() {
1000 Ok(repo_path.cloned())
1001 } else if self.try_remote()
1002 && let Some(remote_url) = &self.remote_url
1003 {
1004 let remote_path = if let Some(remote_path) = &self.remote_repo_path {
1005 remote_path.clone()
1006 } else {
1007 temp_dir().join("vergen-gitcl")
1008 };
1009 create_dir_all(&remote_path)?;
1010 if !self.clone_repo(remote_url, Some(&remote_path)) {
1011 return Err(anyhow!("Failed to clone git repository"));
1012 }
1013 cargo_warning.push(format!(
1014 "Using remote repository from '{remote_url}' at '{}'",
1015 remote_path.display()
1016 ));
1017 Ok(Some(remote_path))
1018 } else {
1019 Ok(repo_path.cloned())
1020 }
1021 }
1022
1023 #[cfg(not(feature = "allow_remote"))]
1024 #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
1025 fn setup_repo_path(
1026 &self,
1027 repo_path: Option<&PathBuf>,
1028 _cargo_warning: &mut CargoWarning,
1029 ) -> Result<Option<PathBuf>> {
1030 Ok(repo_path.cloned())
1031 }
1032
1033 #[cfg(feature = "vcs_info")]
1036 fn vcs_fallback(&self) -> Option<(String, bool)> {
1037 if self.vcs_info_fallback {
1038 vergen_lib::vcs_info()
1039 } else {
1040 None
1041 }
1042 }
1043
1044 #[cfg(not(feature = "vcs_info"))]
1045 #[allow(clippy::unused_self)]
1046 fn vcs_fallback(&self) -> Option<(String, bool)> {
1047 None
1048 }
1049}
1050
1051impl AddEntries for Gitcl {
1052 fn add_map_entries(
1053 &self,
1054 idempotent: bool,
1055 cargo_rustc_env: &mut CargoRustcEnvMap,
1056 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
1057 cargo_warning: &mut CargoWarning,
1058 ) -> Result<()> {
1059 if self.any() {
1060 let git_cmd = self.git_cmd.unwrap_or("git --version");
1061 Self::check_git(git_cmd)?;
1062
1063 let repo_path = self.setup_repo_path(self.local_repo_path.as_ref(), cargo_warning)?;
1064 let repo_path = repo_path.as_ref();
1065 Self::check_inside_git_worktree(repo_path)?;
1066
1067 self.inner_add_git_map_entries(
1068 repo_path,
1069 idempotent,
1070 cargo_rustc_env,
1071 cargo_rerun_if_changed,
1072 cargo_warning,
1073 )?;
1074
1075 self.cleanup();
1076 }
1077 Ok(())
1078 }
1079
1080 #[allow(clippy::too_many_lines)]
1081 fn add_default_entries(
1082 &self,
1083 config: &DefaultConfig,
1084 cargo_rustc_env_map: &mut CargoRustcEnvMap,
1085 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
1086 cargo_warning: &mut CargoWarning,
1087 ) -> Result<()> {
1088 if *config.fail_on_error() {
1089 let error = Error::msg(format!("{}", config.error()));
1090 Err(error)
1091 } else {
1092 cargo_warning.clear();
1095 cargo_rerun_if_changed.clear();
1096
1097 cargo_warning.push(format!("{}", config.error()));
1098
1099 let vcs = self.vcs_fallback();
1102
1103 if self.branch {
1104 add_default_map_entry(
1105 *config.idempotent(),
1106 VergenKey::GitBranch,
1107 cargo_rustc_env_map,
1108 cargo_warning,
1109 );
1110 }
1111 if self.commit_author_email {
1112 add_default_map_entry(
1113 *config.idempotent(),
1114 VergenKey::GitCommitAuthorEmail,
1115 cargo_rustc_env_map,
1116 cargo_warning,
1117 );
1118 }
1119 if self.commit_author_name {
1120 add_default_map_entry(
1121 *config.idempotent(),
1122 VergenKey::GitCommitAuthorName,
1123 cargo_rustc_env_map,
1124 cargo_warning,
1125 );
1126 }
1127 if self.commit_count {
1128 add_default_map_entry(
1129 *config.idempotent(),
1130 VergenKey::GitCommitCount,
1131 cargo_rustc_env_map,
1132 cargo_warning,
1133 );
1134 }
1135 if self.commit_date {
1136 add_default_map_entry(
1137 *config.idempotent(),
1138 VergenKey::GitCommitDate,
1139 cargo_rustc_env_map,
1140 cargo_warning,
1141 );
1142 }
1143 if self.commit_message {
1144 add_default_map_entry(
1145 *config.idempotent(),
1146 VergenKey::GitCommitMessage,
1147 cargo_rustc_env_map,
1148 cargo_warning,
1149 );
1150 }
1151 if self.commit_timestamp {
1152 add_default_map_entry(
1153 *config.idempotent(),
1154 VergenKey::GitCommitTimestamp,
1155 cargo_rustc_env_map,
1156 cargo_warning,
1157 );
1158 }
1159 if self.commit_timestamp_unix {
1160 add_default_map_entry(
1161 *config.idempotent(),
1162 VergenKey::GitCommitTimestampUnix,
1163 cargo_rustc_env_map,
1164 cargo_warning,
1165 );
1166 }
1167 if self.describe.is_some() {
1168 add_default_map_entry(
1169 *config.idempotent(),
1170 VergenKey::GitDescribe,
1171 cargo_rustc_env_map,
1172 cargo_warning,
1173 );
1174 }
1175 if self.sha.is_some() {
1176 if let Some((sha, _)) = &vcs {
1177 add_map_entry(VergenKey::GitSha, sha.clone(), cargo_rustc_env_map);
1178 } else {
1179 add_default_map_entry(
1180 *config.idempotent(),
1181 VergenKey::GitSha,
1182 cargo_rustc_env_map,
1183 cargo_warning,
1184 );
1185 }
1186 }
1187 if self.dirty.is_some() {
1188 if let Some((_, dirty)) = &vcs {
1189 add_map_entry(VergenKey::GitDirty, dirty.to_string(), cargo_rustc_env_map);
1190 } else {
1191 add_default_map_entry(
1192 *config.idempotent(),
1193 VergenKey::GitDirty,
1194 cargo_rustc_env_map,
1195 cargo_warning,
1196 );
1197 }
1198 }
1199 Ok(())
1200 }
1201 }
1202}
1203
1204#[cfg(test)]
1205mod test {
1206 use super::Gitcl;
1207 use crate::Emitter;
1208 use anyhow::Result;
1209 use serial_test::serial;
1210 #[cfg(unix)]
1211 use std::io::stdout;
1212 use std::{collections::BTreeMap, env::temp_dir, io::Write};
1213 #[cfg(unix)]
1214 use test_util::TEST_MTIME;
1215 use test_util::TestRepos;
1216 use vergen_lib::{VergenKey, count_idempotent};
1217
1218 #[test]
1219 #[serial]
1220 #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
1221 fn gitcl_clone_works() {
1222 let gitcl = Gitcl::all_git();
1223 let another = gitcl.clone();
1224 assert_eq!(another, gitcl);
1225 }
1226
1227 #[test]
1228 #[serial]
1229 fn gitcl_debug_works() -> Result<()> {
1230 let gitcl = Gitcl::all_git();
1231 let mut buf = vec![];
1232 write!(buf, "{gitcl:?}")?;
1233 assert!(!buf.is_empty());
1234 Ok(())
1235 }
1236
1237 #[test]
1238 #[serial]
1239 fn gitcl_default() -> Result<()> {
1240 let gitcl = Gitcl::builder().build();
1241 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1242 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1243 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1244 assert_eq!(0, emitter.cargo_warning().len());
1245 Ok(())
1246 }
1247
1248 #[test]
1249 #[serial]
1250 fn bad_command_is_error() -> Result<()> {
1251 let mut map = BTreeMap::new();
1252 assert!(
1253 Gitcl::add_git_cmd_entry(
1254 "such_a_terrible_cmd",
1255 None,
1256 VergenKey::GitCommitMessage,
1257 &mut map
1258 )
1259 .is_err()
1260 );
1261 Ok(())
1262 }
1263
1264 #[test]
1265 #[serial]
1266 fn non_working_tree_is_error() -> Result<()> {
1267 assert!(Gitcl::check_inside_git_worktree(Some(&temp_dir())).is_err());
1268 Ok(())
1269 }
1270
1271 #[test]
1272 #[serial]
1273 fn invalid_git_is_error() -> Result<()> {
1274 assert!(Gitcl::check_git("such_a_terrible_cmd -v").is_err());
1275 Ok(())
1276 }
1277
1278 #[cfg(not(target_family = "windows"))]
1279 #[test]
1280 #[serial]
1281 fn shell_env_works() -> Result<()> {
1282 temp_env::with_var("SHELL", Some("bash"), || {
1283 let mut map = BTreeMap::new();
1284 assert!(
1285 Gitcl::add_git_cmd_entry("git -v", None, VergenKey::GitCommitMessage, &mut map)
1286 .is_ok()
1287 );
1288 });
1289 Ok(())
1290 }
1291
1292 #[test]
1293 #[serial]
1294 fn git_all_idempotent() -> Result<()> {
1295 let gitcl = Gitcl::all_git();
1296 let emitter = Emitter::default()
1297 .idempotent()
1298 .add_instructions(&gitcl)?
1299 .test_emit();
1300 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1301 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1302 assert_eq!(2, emitter.cargo_warning().len());
1303 Ok(())
1304 }
1305
1306 #[test]
1307 #[serial]
1308 fn git_all_idempotent_no_warn() -> Result<()> {
1309 let gitcl = Gitcl::all_git();
1310 let emitter = Emitter::default()
1311 .idempotent()
1312 .quiet()
1313 .add_instructions(&gitcl)?
1314 .test_emit();
1315 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1316 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1317 assert_eq!(2, emitter.cargo_warning().len());
1318 Ok(())
1319 }
1320
1321 #[test]
1322 #[serial]
1323 fn git_all_at_path() -> Result<()> {
1324 let repo = TestRepos::new(false, false, false)?;
1325 let mut gitcl = Gitcl::all_git();
1326 let _ = gitcl.at_path(repo.path());
1327 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1328 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1329 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1330 assert_eq!(0, emitter.cargo_warning().len());
1331 Ok(())
1332 }
1333
1334 #[test]
1335 #[serial]
1336 fn git_all() -> Result<()> {
1337 let gitcl = Gitcl::all_git();
1338 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1339 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1340 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1341 assert_eq!(0, emitter.cargo_warning().len());
1342 Ok(())
1343 }
1344
1345 #[test]
1346 #[serial]
1347 fn git_all_shallow_clone() -> Result<()> {
1348 let repo = TestRepos::new(false, false, true)?;
1349 let mut gitcl = Gitcl::all_git();
1350 let _ = gitcl.at_path(repo.path());
1351 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1352 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1353 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1354 assert_eq!(0, emitter.cargo_warning().len());
1355 Ok(())
1356 }
1357
1358 #[test]
1359 #[serial]
1360 fn git_all_dirty_tags_short() -> Result<()> {
1361 let gitcl = Gitcl::builder()
1362 .all()
1363 .describe(true, true, None)
1364 .sha(true)
1365 .build();
1366 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1367 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1368 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1369 assert_eq!(0, emitter.cargo_warning().len());
1370 Ok(())
1371 }
1372
1373 #[test]
1374 #[serial]
1375 fn fails_on_bad_git_command() -> Result<()> {
1376 let mut gitcl = Gitcl::all_git();
1377 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1378 assert!(
1379 Emitter::default()
1380 .fail_on_error()
1381 .add_instructions(&gitcl)
1382 .is_err()
1383 );
1384 Ok(())
1385 }
1386
1387 #[test]
1388 #[serial]
1389 fn defaults_on_bad_git_command() -> Result<()> {
1390 let mut gitcl = Gitcl::all_git();
1391 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1392 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1393 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1394 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1395 assert_eq!(11, emitter.cargo_warning().len());
1396 Ok(())
1397 }
1398
1399 #[test]
1400 #[serial]
1401 fn idempotent_on_bad_git_command() -> Result<()> {
1402 let mut gitcl = Gitcl::all_git();
1403 let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1404 let emitter = Emitter::default()
1405 .idempotent()
1406 .add_instructions(&gitcl)?
1407 .test_emit();
1408 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1409 assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
1410 assert_eq!(11, emitter.cargo_warning().len());
1411 Ok(())
1412 }
1413
1414 #[test]
1415 #[serial]
1416 fn bad_timestamp_defaults() -> Result<()> {
1417 let mut map = BTreeMap::new();
1418 let mut warnings = vec![];
1419 let gitcl = Gitcl::all_git();
1420 assert!(
1421 gitcl
1422 .add_git_timestamp_entries(
1423 "this_is_not_a_git_cmd",
1424 None,
1425 false,
1426 &mut map,
1427 &mut warnings
1428 )
1429 .is_ok()
1430 );
1431 assert_eq!(0, map.len());
1432 assert_eq!(2, warnings.len());
1433 Ok(())
1434 }
1435
1436 #[test]
1437 #[serial]
1438 fn bad_timestamp_idempotent() -> Result<()> {
1439 let mut map = BTreeMap::new();
1440 let mut warnings = vec![];
1441 let gitcl = Gitcl::all_git();
1442 assert!(
1443 gitcl
1444 .add_git_timestamp_entries(
1445 "this_is_not_a_git_cmd",
1446 None,
1447 true,
1448 &mut map,
1449 &mut warnings
1450 )
1451 .is_ok()
1452 );
1453 assert_eq!(2, map.len());
1454 assert_eq!(2, warnings.len());
1455 Ok(())
1456 }
1457
1458 #[test]
1459 #[serial]
1460 fn source_date_epoch_does_not_affect_git() {
1461 fn emit() -> Result<String> {
1465 let mut stdout_buf = vec![];
1466 let gitcl = Gitcl::builder()
1467 .commit_date(true)
1468 .commit_timestamp(true)
1469 .build();
1470 _ = Emitter::new()
1471 .add_instructions(&gitcl)?
1472 .emit_to(&mut stdout_buf)?;
1473 Ok(String::from_utf8_lossy(&stdout_buf).into_owned())
1474 }
1475 let without = temp_env::with_var("SOURCE_DATE_EPOCH", None::<&str>, emit);
1476 let with = temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), emit);
1477 assert_eq!(without.unwrap(), with.unwrap());
1478 }
1479
1480 #[test]
1481 #[serial]
1482 #[cfg(unix)]
1483 fn bad_source_date_epoch_ignored_by_git() {
1484 use std::ffi::OsStr;
1485 use std::os::unix::prelude::OsStrExt;
1486
1487 let source = [0x66, 0x6f, 0x80, 0x6f];
1490 let os_str = OsStr::from_bytes(&source[..]);
1491 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1492 let result = || -> Result<bool> {
1493 let mut stdout_buf = vec![];
1494 let gitcl = Gitcl::builder().commit_date(true).build();
1495 Emitter::new()
1496 .idempotent()
1497 .fail_on_error()
1498 .add_instructions(&gitcl)?
1499 .emit_to(&mut stdout_buf)
1500 }();
1501 assert!(result.is_ok());
1502 });
1503 }
1504
1505 #[test]
1506 #[serial]
1507 #[cfg(unix)]
1508 fn bad_source_date_epoch_defaults() {
1509 use std::ffi::OsStr;
1510 use std::os::unix::prelude::OsStrExt;
1511
1512 let source = [0x66, 0x6f, 0x80, 0x6f];
1513 let os_str = OsStr::from_bytes(&source[..]);
1514 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1515 let result = || -> Result<bool> {
1516 let mut stdout_buf = vec![];
1517 let gitcl = Gitcl::builder().commit_date(true).build();
1518 Emitter::new()
1519 .idempotent()
1520 .add_instructions(&gitcl)?
1521 .emit_to(&mut stdout_buf)
1522 }();
1523 assert!(result.is_ok());
1524 });
1525 }
1526
1527 #[test]
1528 #[serial]
1529 #[cfg(windows)]
1530 fn bad_source_date_epoch_ignored_by_git() {
1531 use std::ffi::OsString;
1532 use std::os::windows::prelude::OsStringExt;
1533
1534 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1537 let os_string = OsString::from_wide(&source[..]);
1538 let os_str = os_string.as_os_str();
1539 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1540 let result = || -> Result<bool> {
1541 let mut stdout_buf = vec![];
1542 let gitcl = Gitcl::builder().commit_date(true).build();
1543 Emitter::new()
1544 .fail_on_error()
1545 .idempotent()
1546 .add_instructions(&gitcl)?
1547 .emit_to(&mut stdout_buf)
1548 }();
1549 assert!(result.is_ok());
1550 });
1551 }
1552
1553 #[test]
1554 #[serial]
1555 #[cfg(windows)]
1556 fn bad_source_date_epoch_defaults() {
1557 use std::ffi::OsString;
1558 use std::os::windows::prelude::OsStringExt;
1559
1560 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1561 let os_string = OsString::from_wide(&source[..]);
1562 let os_str = os_string.as_os_str();
1563 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1564 let result = || -> Result<bool> {
1565 let mut stdout_buf = vec![];
1566 let gitcl = Gitcl::builder().commit_date(true).build();
1567 Emitter::new()
1568 .idempotent()
1569 .add_instructions(&gitcl)?
1570 .emit_to(&mut stdout_buf)
1571 }();
1572 assert!(result.is_ok());
1573 });
1574 }
1575
1576 #[test]
1577 #[serial]
1578 #[cfg(unix)]
1579 fn git_no_index_update() -> Result<()> {
1580 let repo = TestRepos::new(true, true, false)?;
1581 repo.set_index_magic_mtime()?;
1582
1583 let mut gitcl = Gitcl::builder().all().describe(true, true, None).build();
1585 let _ = gitcl.at_path(repo.path());
1586 let failed = Emitter::default()
1587 .add_instructions(&gitcl)?
1588 .emit_to(&mut stdout())?;
1589 assert!(!failed);
1590
1591 assert_eq!(*TEST_MTIME, repo.get_index_magic_mtime()?);
1592 Ok(())
1593 }
1594
1595 #[test]
1596 #[serial]
1597 #[cfg(feature = "allow_remote")]
1598 fn remote_clone_works() -> Result<()> {
1599 let gitcl = Gitcl::all()
1600 .force_remote(true)
1602 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1603 .describe(true, true, None)
1604 .build();
1605 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1606 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1607 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1608 assert_eq!(1, emitter.cargo_warning().len());
1609 Ok(())
1610 }
1611
1612 #[test]
1613 #[serial]
1614 #[cfg(feature = "allow_remote")]
1615 fn remote_clone_with_path_works() -> Result<()> {
1616 let remote_path = temp_dir().join("blah");
1617 let gitcl = Gitcl::all()
1618 .force_remote(true)
1620 .remote_repo_path(&remote_path)
1621 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1622 .describe(true, true, None)
1623 .build();
1624 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1625 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1626 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1627 assert_eq!(1, emitter.cargo_warning().len());
1628 Ok(())
1629 }
1630
1631 #[test]
1632 #[serial]
1633 #[cfg(feature = "allow_remote")]
1634 fn remote_clone_with_force_local_works() -> Result<()> {
1635 let gitcl = Gitcl::all()
1636 .force_local(true)
1637 .force_remote(true)
1639 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1640 .describe(true, true, None)
1641 .build();
1642 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1643 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1644 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1645 assert_eq!(0, emitter.cargo_warning().len());
1646 Ok(())
1647 }
1648
1649 #[test]
1650 #[serial]
1651 #[cfg(feature = "allow_remote")]
1652 fn remote_clone_with_tag_works() -> Result<()> {
1653 let gitcl = Gitcl::all()
1654 .force_remote(true)
1656 .remote_tag("0.3.9")
1657 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1658 .describe(true, true, None)
1659 .build();
1660 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1661 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1662 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1663 assert_eq!(1, emitter.cargo_warning().len());
1664 Ok(())
1665 }
1666
1667 #[test]
1668 #[serial]
1669 #[cfg(feature = "allow_remote")]
1670 fn remote_clone_with_depth_works() -> Result<()> {
1671 let gitcl = Gitcl::all()
1672 .force_remote(true)
1674 .fetch_depth(200)
1675 .remote_tag("0.3.9")
1676 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1677 .describe(true, true, None)
1678 .build();
1679 let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1680 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1681 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1682 assert_eq!(1, emitter.cargo_warning().len());
1683 Ok(())
1684 }
1685
1686 #[test]
1687 #[serial]
1688 #[cfg(feature = "vcs_info")]
1689 fn vcs_info_fallback_populates_sha_and_dirty() -> Result<()> {
1690 let tmp = temp_dir().join(format!("vergen_gitcl_vcs_info_{}", std::process::id()));
1691 std::fs::create_dir_all(&tmp)?;
1692 let mut file = std::fs::File::create(tmp.join(".cargo_vcs_info.json"))?;
1693 write!(
1694 file,
1695 r#"{{"git":{{"sha1":"deadbeef","dirty":true}},"path_in_vcs":""}}"#
1696 )?;
1697
1698 let gitcl = Gitcl::builder()
1699 .sha(false)
1700 .dirty(false)
1701 .vcs_info_fallback(true)
1702 .local_repo_path(tmp.join("not-a-repo"))
1705 .build();
1706
1707 let mut stdout_buf = vec![];
1708 temp_env::with_var(
1709 "CARGO_MANIFEST_DIR",
1710 Some(tmp.as_os_str()),
1711 || -> Result<()> {
1712 let mut emitter = Emitter::default();
1713 _ = emitter.add_instructions(&gitcl)?.emit_to(&mut stdout_buf)?;
1714 Ok(())
1715 },
1716 )?;
1717
1718 let output = String::from_utf8_lossy(&stdout_buf);
1719 let _ = std::fs::remove_dir_all(&tmp).ok();
1720 assert!(
1721 output.contains("cargo:rustc-env=VERGEN_GIT_SHA=deadbeef"),
1722 "{output}"
1723 );
1724 assert!(
1725 output.contains("cargo:rustc-env=VERGEN_GIT_DIRTY=true"),
1726 "{output}"
1727 );
1728 Ok(())
1729 }
1730
1731 #[test]
1732 #[serial]
1733 fn commit_timestamp_unix_works() -> Result<()> {
1734 let gitcl = Gitcl::builder().commit_timestamp_unix(true).build();
1735 let mut stdout_buf = vec![];
1736 _ = Emitter::default()
1737 .add_instructions(&gitcl)?
1738 .emit_to(&mut stdout_buf)?;
1739 let output = String::from_utf8_lossy(&stdout_buf);
1740 let line = output
1741 .lines()
1742 .find(|l| l.starts_with("cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP_UNIX="))
1743 .expect("unix commit timestamp emitted");
1744 let value = line.trim_start_matches("cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP_UNIX=");
1745 assert!(value.parse::<i64>().is_ok(), "value: {value}");
1746 Ok(())
1747 }
1748
1749 #[test]
1750 #[serial]
1751 fn commit_timestamp_unix_idempotent() -> Result<()> {
1752 let gitcl = Gitcl::builder().commit_timestamp_unix(true).build();
1753 let emitter = Emitter::default()
1754 .idempotent()
1755 .add_instructions(&gitcl)?
1756 .test_emit();
1757 assert_eq!(1, emitter.cargo_rustc_env_map().len());
1758 assert_eq!(1, count_idempotent(emitter.cargo_rustc_env_map()));
1759 Ok(())
1760 }
1761
1762 #[test]
1763 #[serial]
1764 fn commit_timestamp_unix_override_works() {
1765 temp_env::with_var("VERGEN_GIT_COMMIT_TIMESTAMP_UNIX", Some("12345"), || {
1766 let result = || -> Result<()> {
1767 let gitcl = Gitcl::builder().commit_timestamp_unix(true).build();
1768 let mut stdout_buf = vec![];
1769 _ = Emitter::default()
1770 .add_instructions(&gitcl)?
1771 .emit_to(&mut stdout_buf)?;
1772 let output = String::from_utf8_lossy(&stdout_buf);
1773 assert!(
1774 output.contains("cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP_UNIX=12345"),
1775 "{output}"
1776 );
1777 Ok(())
1778 }();
1779 assert!(result.is_ok());
1780 });
1781 }
1782}