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