1use self::git2_builder::Empty;
10#[cfg(test)]
11use anyhow::anyhow;
12use anyhow::{Error, Result};
13use bon::Builder;
14use git2_rs::{
15 BranchType, Commit, DescribeFormatOptions, DescribeOptions, Reference, Repository,
16 StatusOptions,
17};
18use std::{
19 env,
20 path::{Path, PathBuf},
21};
22use time::{
23 OffsetDateTime, UtcOffset,
24 format_description::{self, well_known::Iso8601},
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#[cfg(feature = "allow_remote")]
36use {
37 git2_rs::{FetchOptions, build::RepoBuilder},
38 std::env::temp_dir,
39};
40
41#[derive(Builder, Clone, Debug, PartialEq)]
89#[allow(clippy::struct_excessive_bools)]
90pub struct Git2 {
91 #[builder(field)]
95 all: bool,
96 #[builder(into)]
98 local_repo_path: Option<PathBuf>,
99 #[builder(default = false)]
101 force_local: bool,
102 #[cfg(test)]
104 #[builder(default = false)]
105 force_remote: bool,
106 #[builder(into)]
108 remote_url: Option<String>,
109 #[builder(into)]
112 remote_repo_path: Option<PathBuf>,
113 #[builder(into)]
115 remote_tag: Option<String>,
116 #[builder(default = 100)]
118 fetch_depth: usize,
119 #[cfg(feature = "vcs_info")]
123 #[builder(default = false)]
124 vcs_info_fallback: bool,
125 #[builder(default = all)]
132 branch: bool,
133 #[builder(default = all)]
140 commit_author_name: bool,
141 #[builder(default = all)]
148 commit_author_email: bool,
149 #[builder(default = all)]
155 commit_count: bool,
156 #[builder(default = all)]
163 commit_message: bool,
164 #[builder(default = all)]
171 commit_date: bool,
172 #[builder(default = all)]
179 commit_timestamp: bool,
180 #[builder(default = false)]
188 commit_timestamp_unix: bool,
189 #[builder(
207 required,
208 default = all.then(|| Describe::builder().build()),
209 with = |tags: bool, dirty: bool, match_pattern: Option<&'static str>| {
210 Some(Describe::builder().tags(tags).dirty(dirty).maybe_match_pattern(match_pattern).build())
211 }
212 )]
213 describe: Option<Describe>,
214 #[builder(
226 required,
227 default = all.then(|| Sha::builder().build()),
228 with = |short: bool| Some(Sha::builder().short(short).build())
229 )]
230 sha: Option<Sha>,
231 #[builder(
241 required,
242 default = all.then(|| Dirty::builder().build()),
243 with = |include_untracked: bool| Some(Dirty::builder().include_untracked(include_untracked).build())
244 )]
245 dirty: Option<Dirty>,
246 #[builder(default = false)]
248 use_local: bool,
249 #[cfg(test)]
250 #[builder(default = false)]
252 fail: bool,
253}
254
255impl<S: git2_builder::State> Git2Builder<S> {
256 fn all(mut self) -> Self {
261 self.all = true;
262 self
263 }
264}
265
266impl Git2 {
267 #[must_use]
269 pub fn all_git() -> Git2 {
270 Self::builder().all().build()
271 }
272
273 pub fn all() -> Git2Builder<Empty> {
275 Self::builder().all()
276 }
277
278 fn any(&self) -> bool {
279 self.branch
280 || self.commit_author_email
281 || self.commit_author_name
282 || self.commit_count
283 || self.commit_date
284 || self.commit_message
285 || self.commit_timestamp
286 || self.commit_timestamp_unix
287 || self.describe.is_some()
288 || self.sha.is_some()
289 || self.dirty.is_some()
290 }
291
292 pub fn at_path(&mut self, path: PathBuf) -> &mut Self {
294 self.local_repo_path = Some(path);
295 self
296 }
297
298 #[cfg(test)]
299 pub(crate) fn fail(&mut self) -> &mut Self {
300 self.fail = true;
301 self
302 }
303
304 #[cfg(not(test))]
305 fn add_entries(
306 &self,
307 idempotent: bool,
308 cargo_rustc_env: &mut CargoRustcEnvMap,
309 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
310 cargo_warning: &mut CargoWarning,
311 ) -> Result<()> {
312 self.inner_add_entries(
313 idempotent,
314 cargo_rustc_env,
315 cargo_rerun_if_changed,
316 cargo_warning,
317 )
318 }
319
320 #[cfg(test)]
321 fn add_entries(
322 &self,
323 idempotent: bool,
324 cargo_rustc_env: &mut CargoRustcEnvMap,
325 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
326 cargo_warning: &mut CargoWarning,
327 ) -> Result<()> {
328 if self.fail {
329 return Err(anyhow!("failed to create entries"));
330 }
331 self.inner_add_entries(
332 idempotent,
333 cargo_rustc_env,
334 cargo_rerun_if_changed,
335 cargo_warning,
336 )
337 }
338
339 #[cfg(all(not(test), feature = "allow_remote"))]
340 #[allow(clippy::unused_self)]
341 fn try_local(&self) -> bool {
342 true
343 }
344
345 #[cfg(all(test, feature = "allow_remote"))]
346 fn try_local(&self) -> bool {
347 self.force_local || !self.force_remote
348 }
349
350 #[cfg(all(not(test), feature = "allow_remote"))]
351 fn try_remote(&self) -> bool {
352 !self.force_local
353 }
354
355 #[cfg(all(test, feature = "allow_remote"))]
356 fn try_remote(&self) -> bool {
357 self.force_remote || !self.force_local
358 }
359
360 #[cfg(not(feature = "allow_remote"))]
361 #[allow(clippy::unused_self)]
362 fn get_repository(
363 &self,
364 repo_dir: &PathBuf,
365 _warnings: &mut CargoWarning,
366 ) -> Result<Repository> {
367 Repository::discover(repo_dir).map_err(Into::into)
368 }
369
370 #[cfg(feature = "allow_remote")]
371 fn get_repository(
372 &self,
373 repo_dir: &PathBuf,
374 warnings: &mut CargoWarning,
375 ) -> Result<Repository> {
376 if self.try_local()
377 && let Ok(repo) = Repository::discover(repo_dir)
378 {
379 Ok(repo)
380 } else if self.try_remote()
381 && let Some(remote_url) = &self.remote_url
382 {
383 use git2_rs::build::CheckoutBuilder;
384
385 let repo_path = if let Some(path) = &self.remote_repo_path {
386 path.clone()
387 } else {
388 temp_dir().join("vergen-git2")
389 };
390 std::fs::create_dir_all(&repo_path)?;
391 let mut fetch_opts = FetchOptions::new();
392 let _ = fetch_opts.download_tags(git2_rs::AutotagOption::All);
393 let _ = fetch_opts.depth(self.fetch_depth.try_into()?);
394 let mut repo_builder = RepoBuilder::new();
395 let _ = repo_builder.fetch_options(fetch_opts);
396 let repo = repo_builder.clone(remote_url, &repo_path)?;
397
398 if let Some(remote_tag) = self.remote_tag.as_deref() {
399 let spec = format!("refs/tags/{remote_tag}");
400 let (obj, reference) = repo.revparse_ext(&spec)?;
401 repo.checkout_tree(&obj, Some(CheckoutBuilder::new().force()))?;
402 if let Some(gref) = reference {
403 repo.set_head(gref.name().unwrap())?;
404 } else {
405 repo.set_head_detached(obj.id())?;
407 }
408 }
409 warnings.push(format!(
410 "Using remote repository from '{remote_url}' at '{}'",
411 repo.path().display()
412 ));
413 Ok(repo)
414 } else {
415 Err(anyhow::anyhow!(
416 "Could not find a git repository at '{}'",
417 repo_dir.display()
418 ))
419 }
420 }
421
422 #[cfg(not(feature = "allow_remote"))]
423 #[allow(clippy::unused_self)]
424 fn cleanup(&self) {}
425
426 #[cfg(feature = "allow_remote")]
427 fn cleanup(&self) {
428 if let Some(_remote_url) = self.remote_url.as_ref() {
429 let temp_dir = temp_dir().join("vergen-git2");
430 if let Some(path) = &self.remote_repo_path {
432 if path.exists() {
433 let _ = std::fs::remove_dir_all(path).ok();
434 }
435 } else if temp_dir.exists() {
436 let _ = std::fs::remove_dir_all(temp_dir).ok();
437 }
438 }
439 }
440
441 #[allow(clippy::too_many_lines)]
442 fn inner_add_entries(
443 &self,
444 idempotent: bool,
445 cargo_rustc_env: &mut CargoRustcEnvMap,
446 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
447 cargo_warning: &mut CargoWarning,
448 ) -> Result<()> {
449 let repo_dir = if let Some(path) = &self.local_repo_path {
450 path.clone()
451 } else {
452 env::current_dir()?
453 };
454 let repo = self.get_repository(&repo_dir, cargo_warning)?;
455 let ref_head = repo.find_reference("HEAD")?;
456 let git_path = repo.path().to_path_buf();
457 let commit = ref_head.peel_to_commit()?;
458
459 if !idempotent && self.any() {
460 Self::add_rerun_if_changed(&ref_head, &git_path, cargo_rerun_if_changed);
461 }
462
463 if self.branch {
464 if let Ok(_value) = env::var(GIT_BRANCH_NAME) {
465 add_default_map_entry(
466 idempotent,
467 VergenKey::GitBranch,
468 cargo_rustc_env,
469 cargo_warning,
470 );
471 } else {
472 Self::add_branch_name(idempotent, false, &repo, cargo_rustc_env, cargo_warning)?;
473 }
474 }
475
476 if self.commit_author_email {
477 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_EMAIL) {
478 add_default_map_entry(
479 idempotent,
480 VergenKey::GitCommitAuthorEmail,
481 cargo_rustc_env,
482 cargo_warning,
483 );
484 } else {
485 Self::add_opt_value(
486 idempotent,
487 commit.author().email().ok(),
488 VergenKey::GitCommitAuthorEmail,
489 cargo_rustc_env,
490 cargo_warning,
491 );
492 }
493 }
494
495 if self.commit_author_name {
496 if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_NAME) {
497 add_default_map_entry(
498 idempotent,
499 VergenKey::GitCommitAuthorName,
500 cargo_rustc_env,
501 cargo_warning,
502 );
503 } else {
504 Self::add_opt_value(
505 idempotent,
506 commit.author().name().ok(),
507 VergenKey::GitCommitAuthorName,
508 cargo_rustc_env,
509 cargo_warning,
510 );
511 }
512 }
513
514 if self.commit_count {
515 if let Ok(_value) = env::var(GIT_COMMIT_COUNT) {
516 add_default_map_entry(
517 idempotent,
518 VergenKey::GitCommitCount,
519 cargo_rustc_env,
520 cargo_warning,
521 );
522 } else {
523 Self::add_commit_count(idempotent, false, &repo, cargo_rustc_env, cargo_warning);
524 }
525 }
526
527 self.add_git_timestamp_entries(&commit, idempotent, cargo_rustc_env, cargo_warning)?;
528
529 if self.commit_message {
530 if let Ok(_value) = env::var(GIT_COMMIT_MESSAGE) {
531 add_default_map_entry(
532 idempotent,
533 VergenKey::GitCommitMessage,
534 cargo_rustc_env,
535 cargo_warning,
536 );
537 } else {
538 Self::add_opt_value(
539 idempotent,
540 commit.message().ok(),
541 VergenKey::GitCommitMessage,
542 cargo_rustc_env,
543 cargo_warning,
544 );
545 }
546 }
547
548 if let Some(sha) = self.sha {
549 if let Ok(_value) = env::var(GIT_SHA_NAME) {
550 add_default_map_entry(
551 idempotent,
552 VergenKey::GitSha,
553 cargo_rustc_env,
554 cargo_warning,
555 );
556 } else if sha.short() {
557 let obj = repo.revparse_single("HEAD")?;
558 Self::add_opt_value(
559 idempotent,
560 obj.short_id()?.as_str().ok(),
561 VergenKey::GitSha,
562 cargo_rustc_env,
563 cargo_warning,
564 );
565 } else {
566 add_map_entry(VergenKey::GitSha, commit.id().to_string(), cargo_rustc_env);
567 }
568 }
569
570 if let Some(dirty) = self.dirty {
571 if let Ok(_value) = env::var(GIT_DIRTY_NAME) {
572 add_default_map_entry(
573 idempotent,
574 VergenKey::GitDirty,
575 cargo_rustc_env,
576 cargo_warning,
577 );
578 } else {
579 let mut status_options = StatusOptions::new();
580
581 _ = status_options.include_untracked(dirty.include_untracked());
582 let statuses = repo.statuses(Some(&mut status_options))?;
583
584 let n_dirty = statuses
585 .iter()
586 .filter(|each_status| !each_status.status().is_ignored())
587 .count();
588
589 add_map_entry(
590 VergenKey::GitDirty,
591 format!("{}", n_dirty > 0),
592 cargo_rustc_env,
593 );
594 }
595 }
596
597 if let Some(describe) = self.describe {
598 if let Ok(_value) = env::var(GIT_DESCRIBE_NAME) {
599 add_default_map_entry(
600 idempotent,
601 VergenKey::GitDescribe,
602 cargo_rustc_env,
603 cargo_warning,
604 );
605 } else {
606 let mut describe_opts = DescribeOptions::new();
607 let mut format_opts = DescribeFormatOptions::new();
608
609 _ = describe_opts.show_commit_oid_as_fallback(true);
610
611 if describe.dirty() {
612 _ = format_opts.dirty_suffix("-dirty");
613 }
614
615 if describe.tags() {
616 _ = describe_opts.describe_tags();
617 }
618
619 if let Some(pattern) = *describe.match_pattern() {
620 _ = describe_opts.pattern(pattern);
621 }
622
623 let describe = repo
624 .describe(&describe_opts)
625 .map(|x| x.format(Some(&format_opts)).map_err(Error::from))??;
626 add_map_entry(VergenKey::GitDescribe, describe, cargo_rustc_env);
627 }
628 }
629
630 self.cleanup();
631
632 Ok(())
633 }
634
635 fn add_rerun_if_changed(
636 ref_head: &Reference<'_>,
637 git_path: &Path,
638 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
639 ) {
640 let mut head_path = git_path.to_path_buf();
642 head_path.push("HEAD");
643
644 if head_path.exists() {
646 cargo_rerun_if_changed.push(format!("{}", head_path.display()));
647 }
648
649 if let Ok(resolved) = ref_head.resolve()
650 && let Ok(name) = resolved.name()
651 {
652 let ref_path = git_path.to_path_buf();
653 let path = ref_path.join(name);
654 if path.exists() {
656 cargo_rerun_if_changed.push(format!("{}", path.display()));
657 }
658 }
659 }
660
661 fn add_branch_name(
662 idempotent: bool,
663 add_default: bool,
664 repo: &Repository,
665 cargo_rustc_env: &mut CargoRustcEnvMap,
666 cargo_warning: &mut CargoWarning,
667 ) -> Result<()> {
668 if repo.head_detached()? {
669 if add_default {
670 add_default_map_entry(
671 idempotent,
672 VergenKey::GitBranch,
673 cargo_rustc_env,
674 cargo_warning,
675 );
676 } else {
677 add_map_entry(VergenKey::GitBranch, "HEAD", cargo_rustc_env);
678 }
679 } else {
680 let locals = repo.branches(Some(BranchType::Local))?;
681 let mut found_head = false;
682 for (local, _bt) in locals.filter_map(std::result::Result::ok) {
683 if local.is_head()
684 && let Some(name) = local.name()?
685 {
686 add_map_entry(VergenKey::GitBranch, name, cargo_rustc_env);
687 found_head = !add_default;
688 break;
689 }
690 }
691 if !found_head {
692 add_default_map_entry(
693 idempotent,
694 VergenKey::GitBranch,
695 cargo_rustc_env,
696 cargo_warning,
697 );
698 }
699 }
700 Ok(())
701 }
702
703 fn add_opt_value(
704 idempotent: bool,
705 value: Option<&str>,
706 key: VergenKey,
707 cargo_rustc_env: &mut CargoRustcEnvMap,
708 cargo_warning: &mut CargoWarning,
709 ) {
710 if let Some(val) = value {
711 add_map_entry(key, val, cargo_rustc_env);
712 } else {
713 add_default_map_entry(idempotent, key, cargo_rustc_env, cargo_warning);
714 }
715 }
716
717 fn add_commit_count(
718 idempotent: bool,
719 add_default: bool,
720 repo: &Repository,
721 cargo_rustc_env: &mut CargoRustcEnvMap,
722 cargo_warning: &mut CargoWarning,
723 ) {
724 let key = VergenKey::GitCommitCount;
725 if !add_default
726 && let Ok(mut revwalk) = repo.revwalk()
727 && revwalk.push_head().is_ok()
728 {
729 add_map_entry(key, revwalk.count().to_string(), cargo_rustc_env);
730 return;
731 }
732 add_default_map_entry(idempotent, key, cargo_rustc_env, cargo_warning);
733 }
734
735 fn add_git_timestamp_entries(
736 &self,
737 commit: &Commit<'_>,
738 idempotent: bool,
739 cargo_rustc_env: &mut CargoRustcEnvMap,
740 cargo_warning: &mut CargoWarning,
741 ) -> Result<()> {
742 let ts = self.compute_local_offset(commit)?;
747
748 if let Ok(_value) = env::var(GIT_COMMIT_DATE_NAME) {
749 add_default_map_entry(
750 idempotent,
751 VergenKey::GitCommitDate,
752 cargo_rustc_env,
753 cargo_warning,
754 );
755 } else {
756 self.add_git_date_entry(idempotent, &ts, cargo_rustc_env, cargo_warning)?;
757 }
758 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
759 add_default_map_entry(
760 idempotent,
761 VergenKey::GitCommitTimestamp,
762 cargo_rustc_env,
763 cargo_warning,
764 );
765 } else {
766 self.add_git_timestamp_entry(idempotent, &ts, cargo_rustc_env, cargo_warning)?;
767 }
768 if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_UNIX_NAME) {
769 add_default_map_entry(
770 idempotent,
771 VergenKey::GitCommitTimestampUnix,
772 cargo_rustc_env,
773 cargo_warning,
774 );
775 } else {
776 self.add_git_timestamp_unix_entry(idempotent, &ts, cargo_rustc_env, cargo_warning);
777 }
778 Ok(())
779 }
780
781 fn add_git_timestamp_unix_entry(
782 &self,
783 idempotent: bool,
784 ts: &OffsetDateTime,
785 cargo_rustc_env: &mut CargoRustcEnvMap,
786 cargo_warning: &mut CargoWarning,
787 ) {
788 if self.commit_timestamp_unix {
789 if idempotent {
790 add_default_map_entry(
791 idempotent,
792 VergenKey::GitCommitTimestampUnix,
793 cargo_rustc_env,
794 cargo_warning,
795 );
796 } else {
797 add_map_entry(
798 VergenKey::GitCommitTimestampUnix,
799 ts.unix_timestamp().to_string(),
800 cargo_rustc_env,
801 );
802 }
803 }
804 }
805
806 #[cfg_attr(coverage_nightly, coverage(off))]
807 fn compute_local_offset(&self, commit: &Commit<'_>) -> Result<OffsetDateTime> {
809 let no_offset = OffsetDateTime::from_unix_timestamp(commit.time().seconds())?;
810 if self.use_local {
811 let local = UtcOffset::local_offset_at(no_offset)?;
812 let local_offset = no_offset.checked_to_offset(local).unwrap_or(no_offset);
813 Ok(local_offset)
814 } else {
815 Ok(no_offset)
816 }
817 }
818
819 fn add_git_date_entry(
820 &self,
821 idempotent: bool,
822 ts: &OffsetDateTime,
823 cargo_rustc_env: &mut CargoRustcEnvMap,
824 cargo_warning: &mut CargoWarning,
825 ) -> Result<()> {
826 if self.commit_date {
827 if idempotent {
828 add_default_map_entry(
829 idempotent,
830 VergenKey::GitCommitDate,
831 cargo_rustc_env,
832 cargo_warning,
833 );
834 } else {
835 let format = format_description::parse_borrowed::<1>("[year]-[month]-[day]")?;
836 add_map_entry(
837 VergenKey::GitCommitDate,
838 ts.format(&format)?,
839 cargo_rustc_env,
840 );
841 }
842 }
843 Ok(())
844 }
845
846 fn add_git_timestamp_entry(
847 &self,
848 idempotent: bool,
849 ts: &OffsetDateTime,
850 cargo_rustc_env: &mut CargoRustcEnvMap,
851 cargo_warning: &mut CargoWarning,
852 ) -> Result<()> {
853 if self.commit_timestamp {
854 if idempotent {
855 add_default_map_entry(
856 idempotent,
857 VergenKey::GitCommitTimestamp,
858 cargo_rustc_env,
859 cargo_warning,
860 );
861 } else {
862 add_map_entry(
863 VergenKey::GitCommitTimestamp,
864 ts.format(&Iso8601::DEFAULT)?,
865 cargo_rustc_env,
866 );
867 }
868 }
869 Ok(())
870 }
871
872 #[cfg(feature = "vcs_info")]
875 fn vcs_fallback(&self) -> Option<(String, bool)> {
876 if self.vcs_info_fallback {
877 vergen_lib::vcs_info()
878 } else {
879 None
880 }
881 }
882
883 #[cfg(not(feature = "vcs_info"))]
884 #[allow(clippy::unused_self)]
885 fn vcs_fallback(&self) -> Option<(String, bool)> {
886 None
887 }
888}
889
890impl AddEntries for Git2 {
891 fn add_map_entries(
892 &self,
893 idempotent: bool,
894 cargo_rustc_env: &mut CargoRustcEnvMap,
895 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
896 cargo_warning: &mut CargoWarning,
897 ) -> Result<()> {
898 if self.any() {
899 self.add_entries(
900 idempotent,
901 cargo_rustc_env,
902 cargo_rerun_if_changed,
903 cargo_warning,
904 )?;
905 }
906 Ok(())
907 }
908
909 #[allow(clippy::too_many_lines)]
910 fn add_default_entries(
911 &self,
912 config: &DefaultConfig,
913 cargo_rustc_env_map: &mut CargoRustcEnvMap,
914 cargo_rerun_if_changed: &mut CargoRerunIfChanged,
915 cargo_warning: &mut CargoWarning,
916 ) -> Result<()> {
917 if *config.fail_on_error() {
918 let error = Error::msg(format!("{}", config.error()));
919 Err(error)
920 } else {
921 cargo_warning.clear();
923 cargo_rerun_if_changed.clear();
924
925 cargo_warning.push(format!("{}", config.error()));
926
927 let vcs = self.vcs_fallback();
930
931 if self.branch {
932 add_default_map_entry(
933 *config.idempotent(),
934 VergenKey::GitBranch,
935 cargo_rustc_env_map,
936 cargo_warning,
937 );
938 }
939 if self.commit_author_email {
940 add_default_map_entry(
941 *config.idempotent(),
942 VergenKey::GitCommitAuthorEmail,
943 cargo_rustc_env_map,
944 cargo_warning,
945 );
946 }
947 if self.commit_author_name {
948 add_default_map_entry(
949 *config.idempotent(),
950 VergenKey::GitCommitAuthorName,
951 cargo_rustc_env_map,
952 cargo_warning,
953 );
954 }
955 if self.commit_count {
956 add_default_map_entry(
957 *config.idempotent(),
958 VergenKey::GitCommitCount,
959 cargo_rustc_env_map,
960 cargo_warning,
961 );
962 }
963 if self.commit_date {
964 add_default_map_entry(
965 *config.idempotent(),
966 VergenKey::GitCommitDate,
967 cargo_rustc_env_map,
968 cargo_warning,
969 );
970 }
971 if self.commit_message {
972 add_default_map_entry(
973 *config.idempotent(),
974 VergenKey::GitCommitMessage,
975 cargo_rustc_env_map,
976 cargo_warning,
977 );
978 }
979 if self.commit_timestamp {
980 add_default_map_entry(
981 *config.idempotent(),
982 VergenKey::GitCommitTimestamp,
983 cargo_rustc_env_map,
984 cargo_warning,
985 );
986 }
987 if self.commit_timestamp_unix {
988 add_default_map_entry(
989 *config.idempotent(),
990 VergenKey::GitCommitTimestampUnix,
991 cargo_rustc_env_map,
992 cargo_warning,
993 );
994 }
995 if self.describe.is_some() {
996 add_default_map_entry(
997 *config.idempotent(),
998 VergenKey::GitDescribe,
999 cargo_rustc_env_map,
1000 cargo_warning,
1001 );
1002 }
1003 if self.sha.is_some() {
1004 if let Some((sha, _)) = &vcs {
1005 add_map_entry(VergenKey::GitSha, sha.clone(), cargo_rustc_env_map);
1006 } else {
1007 add_default_map_entry(
1008 *config.idempotent(),
1009 VergenKey::GitSha,
1010 cargo_rustc_env_map,
1011 cargo_warning,
1012 );
1013 }
1014 }
1015 if self.dirty.is_some() {
1016 if let Some((_, dirty)) = &vcs {
1017 add_map_entry(VergenKey::GitDirty, dirty.to_string(), cargo_rustc_env_map);
1018 } else {
1019 add_default_map_entry(
1020 *config.idempotent(),
1021 VergenKey::GitDirty,
1022 cargo_rustc_env_map,
1023 cargo_warning,
1024 );
1025 }
1026 }
1027 Ok(())
1028 }
1029 }
1030}
1031
1032#[cfg(test)]
1033mod test {
1034 use super::Git2;
1035 use anyhow::Result;
1036 use git2_rs::Repository;
1037 use serial_test::serial;
1038 #[cfg(unix)]
1039 use std::io::stdout;
1040 use std::{collections::BTreeMap, env::current_dir, io::Write};
1041 #[cfg(unix)]
1042 use test_util::TEST_MTIME;
1043 use test_util::TestRepos;
1044 use vergen::Emitter;
1045 use vergen_lib::{VergenKey, count_idempotent};
1046
1047 #[test]
1048 #[serial]
1049 #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
1050 fn git2_clone_works() {
1051 let git2 = Git2::all_git();
1052 let another = git2.clone();
1053 assert_eq!(another, git2);
1054 }
1055
1056 #[test]
1057 #[serial]
1058 fn git2_debug_works() -> Result<()> {
1059 let git2 = Git2::all_git();
1060 let mut buf = vec![];
1061 write!(buf, "{git2:?}")?;
1062 assert!(!buf.is_empty());
1063 Ok(())
1064 }
1065
1066 #[test]
1067 #[serial]
1068 fn git2_default() -> Result<()> {
1069 let git2 = Git2::builder().build();
1070 let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
1071 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1072 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1073 assert_eq!(0, emitter.cargo_warning().len());
1074 Ok(())
1075 }
1076
1077 #[test]
1078 #[serial]
1079 fn empty_email_is_warning() -> Result<()> {
1080 let mut cargo_rustc_env = BTreeMap::new();
1081 let mut cargo_warning = vec![];
1082 Git2::add_opt_value(
1083 false,
1084 None,
1085 VergenKey::GitCommitAuthorEmail,
1086 &mut cargo_rustc_env,
1087 &mut cargo_warning,
1088 );
1089 assert_eq!(0, cargo_rustc_env.len());
1090 assert_eq!(1, cargo_warning.len());
1091 Ok(())
1092 }
1093
1094 #[test]
1095 #[serial]
1096 fn empty_email_idempotent() -> Result<()> {
1097 let mut cargo_rustc_env = BTreeMap::new();
1098 let mut cargo_warning = vec![];
1099 Git2::add_opt_value(
1100 true,
1101 None,
1102 VergenKey::GitCommitAuthorEmail,
1103 &mut cargo_rustc_env,
1104 &mut cargo_warning,
1105 );
1106 assert_eq!(1, cargo_rustc_env.len());
1107 assert_eq!(1, cargo_warning.len());
1108 Ok(())
1109 }
1110
1111 #[test]
1112 #[serial]
1113 fn bad_revwalk_is_warning() -> Result<()> {
1114 let mut cargo_rustc_env = BTreeMap::new();
1115 let mut cargo_warning = vec![];
1116 let repo = Repository::discover(current_dir()?)?;
1117 Git2::add_commit_count(false, true, &repo, &mut cargo_rustc_env, &mut cargo_warning);
1118 assert_eq!(0, cargo_rustc_env.len());
1119 assert_eq!(1, cargo_warning.len());
1120 Ok(())
1121 }
1122
1123 #[test]
1124 #[serial]
1125 fn bad_revwalk_idempotent() -> Result<()> {
1126 let mut cargo_rustc_env = BTreeMap::new();
1127 let mut cargo_warning = vec![];
1128 let repo = Repository::discover(current_dir()?)?;
1129 Git2::add_commit_count(true, true, &repo, &mut cargo_rustc_env, &mut cargo_warning);
1130 assert_eq!(1, cargo_rustc_env.len());
1131 assert_eq!(1, cargo_warning.len());
1132 Ok(())
1133 }
1134
1135 #[test]
1136 #[serial]
1137 fn head_not_found_is_default() -> Result<()> {
1138 let test_repo = TestRepos::new(false, false, false)?;
1139 let mut map = BTreeMap::new();
1140 let mut cargo_warning = vec![];
1141 let repo = Repository::discover(current_dir()?)?;
1142 let detached = repo.head_detached()?;
1146 Git2::add_branch_name(false, true, &repo, &mut map, &mut cargo_warning)?;
1147 assert_eq!(usize::from(!detached), map.len());
1148 assert_eq!(1, cargo_warning.len());
1149 let mut map = BTreeMap::new();
1150 let mut cargo_warning = vec![];
1151 let repo = Repository::discover(test_repo.path())?;
1152 Git2::add_branch_name(false, true, &repo, &mut map, &mut cargo_warning)?;
1153 assert_eq!(1, map.len());
1154 assert_eq!(1, cargo_warning.len());
1155 Ok(())
1156 }
1157
1158 #[test]
1159 #[serial]
1160 fn git_all_idempotent() -> Result<()> {
1161 let git2 = Git2::all_git();
1162 let emitter = Emitter::default()
1163 .idempotent()
1164 .add_instructions(&git2)?
1165 .test_emit();
1166 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1167 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1168 assert_eq!(2, emitter.cargo_warning().len());
1169 Ok(())
1170 }
1171
1172 #[test]
1173 #[serial]
1174 fn git_all_shallow_clone() -> Result<()> {
1175 let repo = TestRepos::new(false, false, true)?;
1176 let mut git2 = Git2::all_git();
1177 let _ = git2.at_path(repo.path());
1178 let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
1179 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1180 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1181 assert_eq!(0, emitter.cargo_warning().len());
1182 Ok(())
1183 }
1184
1185 #[test]
1186 #[serial]
1187 fn git_all_idempotent_no_warn() -> Result<()> {
1188 let git2 = Git2::all_git();
1189 let emitter = Emitter::default()
1190 .idempotent()
1191 .quiet()
1192 .add_instructions(&git2)?
1193 .test_emit();
1194
1195 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1196 assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1197 assert_eq!(2, emitter.cargo_warning().len());
1198 Ok(())
1199 }
1200
1201 #[test]
1202 #[serial]
1203 fn git_all() -> Result<()> {
1204 let git2 = Git2::all_git();
1205 let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
1206 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1207 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1208 assert_eq!(0, emitter.cargo_warning().len());
1209 Ok(())
1210 }
1211
1212 #[test]
1213 #[serial]
1214 fn git_error_fails() -> Result<()> {
1215 let mut git2 = Git2::all_git();
1216 let _ = git2.fail();
1217 assert!(
1218 Emitter::default()
1219 .fail_on_error()
1220 .add_instructions(&git2)
1221 .is_err()
1222 );
1223 Ok(())
1224 }
1225
1226 #[test]
1227 #[serial]
1228 fn git_error_warnings() -> Result<()> {
1229 let mut git2 = Git2::all_git();
1230 let _ = git2.fail();
1231 let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
1232 assert_eq!(0, emitter.cargo_rustc_env_map().len());
1233 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1234 assert_eq!(11, emitter.cargo_warning().len());
1235 Ok(())
1236 }
1237
1238 #[test]
1239 #[serial]
1240 fn git_error_idempotent() -> Result<()> {
1241 let mut git2 = Git2::all_git();
1242 let _ = git2.fail();
1243 let emitter = Emitter::default()
1244 .idempotent()
1245 .add_instructions(&git2)?
1246 .test_emit();
1247 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1248 assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
1249 assert_eq!(11, emitter.cargo_warning().len());
1250 Ok(())
1251 }
1252
1253 #[test]
1254 #[serial]
1255 fn source_date_epoch_does_not_affect_git() {
1256 fn emit() -> Result<String> {
1260 let mut stdout_buf = vec![];
1261 let git2 = Git2::builder()
1262 .commit_date(true)
1263 .commit_timestamp(true)
1264 .build();
1265 _ = Emitter::new()
1266 .add_instructions(&git2)?
1267 .emit_to(&mut stdout_buf)?;
1268 Ok(String::from_utf8_lossy(&stdout_buf).into_owned())
1269 }
1270 let without = temp_env::with_var("SOURCE_DATE_EPOCH", None::<&str>, emit);
1271 let with = temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), emit);
1272 assert_eq!(without.unwrap(), with.unwrap());
1273 }
1274
1275 #[test]
1276 #[serial]
1277 #[cfg(unix)]
1278 fn bad_source_date_epoch_ignored_by_git() {
1279 use std::ffi::OsStr;
1280 use std::os::unix::prelude::OsStrExt;
1281
1282 let source = [0x66, 0x6f, 0x80, 0x6f];
1285 let os_str = OsStr::from_bytes(&source[..]);
1286 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1287 let result = || -> Result<bool> {
1288 let mut stdout_buf = vec![];
1289 let gix = Git2::builder().commit_date(true).build();
1290 Emitter::new()
1291 .idempotent()
1292 .fail_on_error()
1293 .add_instructions(&gix)?
1294 .emit_to(&mut stdout_buf)
1295 }();
1296 assert!(result.is_ok());
1297 });
1298 }
1299
1300 #[test]
1301 #[serial]
1302 #[cfg(unix)]
1303 fn bad_source_date_epoch_defaults() {
1304 use std::ffi::OsStr;
1305 use std::os::unix::prelude::OsStrExt;
1306
1307 let source = [0x66, 0x6f, 0x80, 0x6f];
1308 let os_str = OsStr::from_bytes(&source[..]);
1309 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1310 let result = || -> Result<bool> {
1311 let mut stdout_buf = vec![];
1312 let gix = Git2::builder().commit_date(true).build();
1313 Emitter::new()
1314 .idempotent()
1315 .add_instructions(&gix)?
1316 .emit_to(&mut stdout_buf)
1317 }();
1318 assert!(result.is_ok());
1319 });
1320 }
1321
1322 #[test]
1323 #[serial]
1324 #[cfg(windows)]
1325 fn bad_source_date_epoch_ignored_by_git() {
1326 use std::ffi::OsString;
1327 use std::os::windows::prelude::OsStringExt;
1328
1329 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1332 let os_string = OsString::from_wide(&source[..]);
1333 let os_str = os_string.as_os_str();
1334 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1335 let result = || -> Result<bool> {
1336 let mut stdout_buf = vec![];
1337 let gix = Git2::builder().commit_date(true).build();
1338 Emitter::new()
1339 .fail_on_error()
1340 .idempotent()
1341 .add_instructions(&gix)?
1342 .emit_to(&mut stdout_buf)
1343 }();
1344 assert!(result.is_ok());
1345 });
1346 }
1347
1348 #[test]
1349 #[serial]
1350 #[cfg(windows)]
1351 fn bad_source_date_epoch_defaults() {
1352 use std::ffi::OsString;
1353 use std::os::windows::prelude::OsStringExt;
1354
1355 let source = [0x0066, 0x006f, 0xD800, 0x006f];
1356 let os_string = OsString::from_wide(&source[..]);
1357 let os_str = os_string.as_os_str();
1358 temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1359 let result = || -> Result<bool> {
1360 let mut stdout_buf = vec![];
1361 let gix = Git2::builder().commit_date(true).build();
1362 Emitter::new()
1363 .idempotent()
1364 .add_instructions(&gix)?
1365 .emit_to(&mut stdout_buf)
1366 }();
1367 assert!(result.is_ok());
1368 });
1369 }
1370
1371 #[test]
1372 #[serial]
1373 #[cfg(unix)]
1374 fn git_no_index_update() -> Result<()> {
1375 let repo = TestRepos::new(true, true, false)?;
1376 repo.set_index_magic_mtime()?;
1377
1378 let mut git2 = Git2::builder().all().describe(true, true, None).build();
1379 let _ = git2.at_path(repo.path());
1380 let failed = Emitter::default()
1381 .add_instructions(&git2)?
1382 .emit_to(&mut stdout())?;
1383 assert!(!failed);
1384
1385 assert_eq!(*TEST_MTIME, repo.get_index_magic_mtime()?);
1386 Ok(())
1387 }
1388
1389 #[test]
1390 #[serial]
1391 #[cfg(feature = "allow_remote")]
1392 fn remote_clone_works() -> Result<()> {
1393 let git2 = Git2::all()
1394 .force_remote(true)
1396 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1397 .describe(true, true, None)
1398 .build();
1399 let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
1400 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1401 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1402 assert_eq!(1, emitter.cargo_warning().len());
1403 Ok(())
1404 }
1405
1406 #[test]
1407 #[serial]
1408 #[cfg(feature = "allow_remote")]
1409 fn remote_clone_with_path_works() -> Result<()> {
1410 let remote_path = std::env::temp_dir().join("blah");
1411 let git2 = Git2::all()
1412 .force_remote(true)
1414 .remote_repo_path(&remote_path)
1415 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1416 .describe(true, true, None)
1417 .build();
1418 let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
1419 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1420 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1421 assert_eq!(1, emitter.cargo_warning().len());
1422 Ok(())
1423 }
1424
1425 #[test]
1426 #[serial]
1427 #[cfg(feature = "allow_remote")]
1428 fn remote_clone_with_force_local_works() -> Result<()> {
1429 let git2 = Git2::all()
1430 .force_local(true)
1431 .force_remote(true)
1433 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1434 .describe(true, true, None)
1435 .build();
1436 let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
1437 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1438 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1439 assert_eq!(0, emitter.cargo_warning().len());
1440 Ok(())
1441 }
1442
1443 #[test]
1444 #[serial]
1445 #[cfg(feature = "allow_remote")]
1446 fn remote_clone_with_tag_works() -> Result<()> {
1447 let git2 = Git2::all()
1448 .force_remote(true)
1450 .remote_tag("0.3.9")
1451 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1452 .describe(true, true, None)
1453 .build();
1454 let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
1455 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1456 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1457 assert_eq!(1, emitter.cargo_warning().len());
1458 Ok(())
1459 }
1460
1461 #[test]
1462 #[serial]
1463 #[cfg(feature = "allow_remote")]
1464 fn remote_clone_with_depth_works() -> Result<()> {
1465 let git2 = Git2::all()
1466 .force_remote(true)
1468 .fetch_depth(200)
1469 .remote_tag("0.3.9")
1470 .remote_url("https://github.com/rustyhorde/vergen-cl.git")
1471 .describe(true, true, None)
1472 .build();
1473 let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
1474 assert_eq!(10, emitter.cargo_rustc_env_map().len());
1475 assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1476 assert_eq!(1, emitter.cargo_warning().len());
1477 Ok(())
1478 }
1479
1480 #[test]
1481 #[serial]
1482 #[cfg(feature = "vcs_info")]
1483 fn vcs_info_fallback_populates_sha_and_dirty() -> Result<()> {
1484 use std::io::Write as _;
1485
1486 let tmp = std::env::temp_dir().join(format!("vergen_vcs_info_{}", std::process::id()));
1487 std::fs::create_dir_all(&tmp)?;
1488 let mut file = std::fs::File::create(tmp.join(".cargo_vcs_info.json"))?;
1489 write!(
1490 file,
1491 r#"{{"git":{{"sha1":"deadbeef","dirty":true}},"path_in_vcs":""}}"#
1492 )?;
1493
1494 let git2 = Git2::builder()
1495 .sha(false)
1496 .dirty(false)
1497 .vcs_info_fallback(true)
1498 .local_repo_path(tmp.join("not-a-repo"))
1501 .build();
1502
1503 let mut stdout_buf = vec![];
1504 temp_env::with_var(
1505 "CARGO_MANIFEST_DIR",
1506 Some(tmp.as_os_str()),
1507 || -> Result<()> {
1508 let mut emitter = Emitter::default();
1509 _ = emitter.add_instructions(&git2)?.emit_to(&mut stdout_buf)?;
1510 Ok(())
1511 },
1512 )?;
1513
1514 let output = String::from_utf8_lossy(&stdout_buf);
1515 let _ = std::fs::remove_dir_all(&tmp).ok();
1516 assert!(
1517 output.contains("cargo:rustc-env=VERGEN_GIT_SHA=deadbeef"),
1518 "{output}"
1519 );
1520 assert!(
1521 output.contains("cargo:rustc-env=VERGEN_GIT_DIRTY=true"),
1522 "{output}"
1523 );
1524 Ok(())
1525 }
1526
1527 #[test]
1528 #[serial]
1529 fn commit_timestamp_unix_works() -> Result<()> {
1530 let git2 = Git2::builder().commit_timestamp_unix(true).build();
1531 let mut stdout_buf = vec![];
1532 _ = Emitter::default()
1533 .add_instructions(&git2)?
1534 .emit_to(&mut stdout_buf)?;
1535 let output = String::from_utf8_lossy(&stdout_buf);
1536 let line = output
1537 .lines()
1538 .find(|l| l.starts_with("cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP_UNIX="))
1539 .expect("unix commit timestamp emitted");
1540 let value = line.trim_start_matches("cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP_UNIX=");
1541 assert!(value.parse::<i64>().is_ok(), "value: {value}");
1542 Ok(())
1543 }
1544
1545 #[test]
1546 #[serial]
1547 fn commit_timestamp_unix_idempotent() -> Result<()> {
1548 let git2 = Git2::builder().commit_timestamp_unix(true).build();
1549 let emitter = Emitter::default()
1550 .idempotent()
1551 .add_instructions(&git2)?
1552 .test_emit();
1553 assert_eq!(1, emitter.cargo_rustc_env_map().len());
1554 assert_eq!(1, count_idempotent(emitter.cargo_rustc_env_map()));
1555 Ok(())
1556 }
1557
1558 #[test]
1559 #[serial]
1560 fn commit_timestamp_unix_override_works() {
1561 temp_env::with_var("VERGEN_GIT_COMMIT_TIMESTAMP_UNIX", Some("12345"), || {
1562 let result = || -> Result<()> {
1563 let git2 = Git2::builder().commit_timestamp_unix(true).build();
1564 let mut stdout_buf = vec![];
1565 _ = Emitter::default()
1566 .add_instructions(&git2)?
1567 .emit_to(&mut stdout_buf)?;
1568 let output = String::from_utf8_lossy(&stdout_buf);
1569 assert!(
1570 output.contains("cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP_UNIX=12345"),
1571 "{output}"
1572 );
1573 Ok(())
1574 }();
1575 assert!(result.is_ok());
1576 });
1577 }
1578}