Skip to main content

vergen_git2/git2/
mod.rs

1// Copyright (c) 2022 pud developers
2//
3// Licensed under the Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. All files in the project carrying such notice may not be copied,
7// modified, or distributed except according to those terms.
8
9use 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/// The `VERGEN_GIT_*` configuration features
42///
43/// | Variable | Sample |
44/// | -------  | ------ |
45/// | `VERGEN_GIT_BRANCH` | feature/fun |
46/// | `VERGEN_GIT_COMMIT_AUTHOR_EMAIL` | janedoe@email.com |
47/// | `VERGEN_GIT_COMMIT_AUTHOR_NAME` | Jane Doe |
48/// | `VERGEN_GIT_COMMIT_COUNT` | 330 |
49/// | `VERGEN_GIT_COMMIT_DATE` | 2021-02-24 |
50/// | `VERGEN_GIT_COMMIT_MESSAGE` | feat: add commit messages |
51/// | `VERGEN_GIT_COMMIT_TIMESTAMP` | 2021-02-24T20:55:21+00:00 |
52/// | `VERGEN_GIT_DESCRIBE` | 5.0.0-2-gf49246c |
53/// | `VERGEN_GIT_SHA` | f49246ce334567bff9f950bfd0f3078184a2738a |
54/// | `VERGEN_GIT_DIRTY` | true |
55///
56/// # Example
57///
58/// ```
59/// # use anyhow::Result;
60/// # use vergen_git2::{Emitter, Git2};
61/// #
62/// # fn main() -> Result<()> {
63/// let git2 = Git2::all_git();
64/// Emitter::default().add_instructions(&git2)?.emit()?;
65/// #   Ok(())
66/// # }
67/// ```
68///
69/// Override output with your own value
70///
71/// ```
72/// # use anyhow::Result;
73/// # use vergen_git2::{Emitter, Git2};
74/// #
75/// # fn main() -> Result<()> {
76/// temp_env::with_var("VERGEN_GIT_BRANCH", Some("this is the branch I want output"), || {
77///     let result = || -> Result<()> {
78///         let git2 = Git2::all_git();
79///         Emitter::default().add_instructions(&git2)?.emit()?;
80///         Ok(())
81///     }();
82///     assert!(result.is_ok());
83/// });
84/// #   Ok(())
85/// # }
86/// ```
87///
88#[derive(Builder, Clone, Debug, PartialEq)]
89#[allow(clippy::struct_excessive_bools)]
90pub struct Git2 {
91    /// Configures the default values.
92    /// If set to `true` all defaults are in "enabled" state.
93    /// If set to `false` all defaults are in "disabled" state.
94    #[builder(field)]
95    all: bool,
96    /// An optional path to a local repository.
97    #[builder(into)]
98    local_repo_path: Option<PathBuf>,
99    /// Force the use of a local repository, ignoring any remote configuration
100    #[builder(default = false)]
101    force_local: bool,
102    /// Force the use of a remote repository (testing only)
103    #[cfg(test)]
104    #[builder(default = false)]
105    force_remote: bool,
106    /// An optional remote URL to use in lieu of a local repository
107    #[builder(into)]
108    remote_url: Option<String>,
109    /// An optional path to place the git repository if grabbed remotely
110    /// defaults to the temp directory on the system
111    #[builder(into)]
112    remote_repo_path: Option<PathBuf>,
113    /// An optional tag to clone from the remote
114    #[builder(into)]
115    remote_tag: Option<String>,
116    /// The depth to use when fetching the repository
117    #[builder(default = 100)]
118    fetch_depth: usize,
119    /// Fall back to `.cargo_vcs_info.json` for the git SHA and dirty flag when no
120    /// repository is available (e.g. building from a published crate or via
121    /// `cargo install`).  Requires the `vcs_info` feature.
122    #[cfg(feature = "vcs_info")]
123    #[builder(default = false)]
124    vcs_info_fallback: bool,
125    /// Emit the current git branch
126    ///
127    /// ```text
128    /// cargo:rustc-env=VERGEN_GIT_BRANCH=<BRANCH_NAME>
129    /// ```
130    ///
131    #[builder(default = all)]
132    branch: bool,
133    /// Emit the author email of the most recent commit
134    ///
135    /// ```text
136    /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_EMAIL=<AUTHOR_EMAIL>
137    /// ```
138    ///
139    #[builder(default = all)]
140    commit_author_name: bool,
141    /// Emit the author name of the most recent commit
142    ///
143    /// ```text
144    /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_NAME=<AUTHOR_NAME>
145    /// ```
146    ///
147    #[builder(default = all)]
148    commit_author_email: bool,
149    /// Emit the total commit count to HEAD
150    ///
151    /// ```text
152    /// cargo:rustc-env=VERGEN_GIT_COMMIT_COUNT=<COUNT>
153    /// ```
154    #[builder(default = all)]
155    commit_count: bool,
156    /// Emit the commit message of the latest commit
157    ///
158    /// ```text
159    /// cargo:rustc-env=VERGEN_GIT_COMMIT_MESSAGE=<MESSAGE>
160    /// ```
161    ///
162    #[builder(default = all)]
163    commit_message: bool,
164    /// Emit the commit date of the latest commit
165    ///
166    /// ```text
167    /// cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=<YYYY-MM-DD>
168    /// ```
169    ///
170    #[builder(default = all)]
171    commit_date: bool,
172    /// Emit the commit timestamp of the latest commit
173    ///
174    /// ```text
175    /// cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=<YYYY-MM-DDThh:mm:ssZ>
176    /// ```
177    ///
178    #[builder(default = all)]
179    commit_timestamp: bool,
180    /// Emit the commit timestamp of the latest commit as Unix seconds
181    ///
182    /// ```text
183    /// cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP_UNIX=<SECONDS>
184    /// ```
185    ///
186    /// This is opt-in and is not enabled by [`Git2::all_git`].
187    #[builder(default = false)]
188    commit_timestamp_unix: bool,
189    /// Emit the describe output
190    ///
191    /// ```text
192    /// cargo:rustc-env=VERGEN_GIT_DESCRIBE=<DESCRIBE>
193    /// ```
194    ///
195    /// Optionally, add the `dirty` or `tags` flag to describe.
196    /// See [`git describe`](https://git-scm.com/docs/git-describe#_options) for more details
197    ///
198    /// ## `tags`
199    /// Instead of using only the annotated tags, use any tag found in refs/tags namespace.
200    ///
201    /// ## `dirty`
202    /// If the working tree has local modification "-dirty" is appended to it.
203    ///
204    /// ## `match_pattern`
205    /// Only consider tags matching the given glob pattern, excluding the "refs/tags/" prefix.
206    #[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    /// Emit the SHA of the latest commit
215    ///
216    /// ```text
217    /// cargo:rustc-env=VERGEN_GIT_SHA=<SHA>
218    /// ```
219    ///
220    /// Optionally, add the `short` flag to rev-parse.
221    /// See [`git rev-parse`](https://git-scm.com/docs/git-rev-parse#_options_for_output) for more details.
222    ///
223    /// ## `short`
224    /// Shortens the object name to a unique prefix
225    #[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    /// Emit the dirty state of the git repository
232    /// ```text
233    /// cargo:rustc-env=VERGEN_GIT_DIRTY=(true|false)
234    /// ```
235    ///
236    /// Optionally, include untracked files when determining the dirty status of the repository.
237    ///
238    /// # `include_tracked`
239    /// Should we include/ignore untracked files in deciding whether the repository is dirty.
240    #[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    /// Enable local offset date/timestamp output
247    #[builder(default = false)]
248    use_local: bool,
249    #[cfg(test)]
250    /// Fail
251    #[builder(default = false)]
252    fail: bool,
253}
254
255impl<S: git2_builder::State> Git2Builder<S> {
256    /// Convenience method that switches the defaults of [`Git2Builder`]
257    /// to enable all of the `VERGEN_GIT_*` instructions. It can only be
258    /// called at the start of the building process, i.e. when no config
259    /// has been set yet to avoid overwrites.
260    fn all(mut self) -> Self {
261        self.all = true;
262        self
263    }
264}
265
266impl Git2 {
267    /// Emit all of the `VERGEN_GIT_*` instructions
268    #[must_use]
269    pub fn all_git() -> Git2 {
270        Self::builder().all().build()
271    }
272
273    /// Convenience method to setup the [`Git2`] builder with all of the `VERGEN_GIT_*` instructions on
274    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    /// Use the repository location at the given path to determine the git instruction output.
293    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                    // detached head
406                    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 we used a remote URL, we should clean up the repo we cloned
431            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        // Setup the head path
641        let mut head_path = git_path.to_path_buf();
642        head_path.push("HEAD");
643
644        // Check whether the path exists in the filesystem before emitting it
645        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            // Check whether the path exists in the filesystem before emitting it
655            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        // NOTE: SOURCE_DATE_EPOCH intentionally does NOT influence the git
743        // commit date/timestamp. Those derive from the commit itself and are
744        // already reproducible; SOURCE_DATE_EPOCH only affects the build
745        // timestamp (see issue #452).
746        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    // this in not included in coverage, because on *nix the local offset is always unsafe
808    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    /// The git SHA and dirty flag recovered from `.cargo_vcs_info.json`, if the
873    /// `vcs_info` fallback is enabled and the file is present. #448
874    #[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            // Clear any previous warnings.  This should be it.
922            cargo_warning.clear();
923            cargo_rerun_if_changed.clear();
924
925            cargo_warning.push(format!("{}", config.error()));
926
927            // With no repository available, optionally recover the SHA and dirty
928            // flag from .cargo_vcs_info.json (e.g. a published crate). #448
929            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        // CI checks out a detached HEAD. With a detached HEAD and a
1143        // non-idempotent default, add_branch_name emits only a warning and no
1144        // map entry; on a normal branch checkout the branch name is added.
1145        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        // SOURCE_DATE_EPOCH must not influence the git commit date/timestamp;
1257        // those derive from the commit and are already reproducible (#452). The
1258        // git output must therefore be identical whether or not it is set.
1259        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        // A malformed SOURCE_DATE_EPOCH no longer affects git output (#452), so
1283        // emission succeeds even with fail_on_error.
1284        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        // A malformed SOURCE_DATE_EPOCH no longer affects git output (#452), so
1330        // emission succeeds even with fail_on_error.
1331        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            // For testing only
1395            .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            // For testing only
1413            .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            // For testing only
1432            .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            // For testing only
1449            .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            // For testing only
1467            .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            // Point at a non-repository so the git lookup fails and the
1499            // .cargo_vcs_info.json fallback is exercised.
1500            .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}