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::{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/// The `VERGEN_GIT_*` configuration features
43///
44/// | Variable | Sample |
45/// | -------  | ------ |
46/// | `VERGEN_GIT_BRANCH` | feature/fun |
47/// | `VERGEN_GIT_COMMIT_AUTHOR_EMAIL` | janedoe@email.com |
48/// | `VERGEN_GIT_COMMIT_AUTHOR_NAME` | Jane Doe |
49/// | `VERGEN_GIT_COMMIT_COUNT` | 330 |
50/// | `VERGEN_GIT_COMMIT_DATE` | 2021-02-24 |
51/// | `VERGEN_GIT_COMMIT_MESSAGE` | feat: add commit messages |
52/// | `VERGEN_GIT_COMMIT_TIMESTAMP` | 2021-02-24T20:55:21+00:00 |
53/// | `VERGEN_GIT_DESCRIBE` | 5.0.0-2-gf49246c |
54/// | `VERGEN_GIT_SHA` | f49246ce334567bff9f950bfd0f3078184a2738a |
55/// | `VERGEN_GIT_DIRTY` | true |
56///
57/// # Example
58///
59/// ```
60/// # use anyhow::Result;
61/// # use vergen_git2::{Emitter, Git2};
62/// #
63/// # fn main() -> Result<()> {
64/// let git2 = Git2::all_git();
65/// Emitter::default().add_instructions(&git2)?.emit()?;
66/// #   Ok(())
67/// # }
68/// ```
69///
70/// Override output with your own value
71///
72/// ```
73/// # use anyhow::Result;
74/// # use vergen_git2::{Emitter, Git2};
75/// #
76/// # fn main() -> Result<()> {
77/// temp_env::with_var("VERGEN_GIT_BRANCH", Some("this is the branch I want output"), || {
78///     let result = || -> Result<()> {
79///         let git2 = Git2::all_git();
80///         Emitter::default().add_instructions(&git2)?.emit()?;
81///         Ok(())
82///     }();
83///     assert!(result.is_ok());
84/// });
85/// #   Ok(())
86/// # }
87/// ```
88///
89#[derive(Builder, Clone, Debug, PartialEq)]
90#[allow(clippy::struct_excessive_bools)]
91pub struct Git2 {
92    /// Configures the default values.
93    /// If set to `true` all defaults are in "enabled" state.
94    /// If set to `false` all defaults are in "disabled" state.
95    #[builder(field)]
96    all: bool,
97    /// An optional path to a local repository.
98    #[builder(into)]
99    local_repo_path: Option<PathBuf>,
100    /// Force the use of a local repository, ignoring any remote configuration
101    #[builder(default = false)]
102    force_local: bool,
103    /// Force the use of a remote repository (testing only)
104    #[cfg(test)]
105    #[builder(default = false)]
106    force_remote: bool,
107    /// An optional remote URL to use in lieu of a local repository
108    #[builder(into)]
109    remote_url: Option<String>,
110    /// An optional path to place the git repository if grabbed remotely
111    /// defaults to the temp directory on the system
112    #[builder(into)]
113    remote_repo_path: Option<PathBuf>,
114    /// An optional tag to clone from the remote
115    #[builder(into)]
116    remote_tag: Option<String>,
117    /// The depth to use when fetching the repository
118    #[builder(default = 100)]
119    fetch_depth: usize,
120    /// Emit the current git branch
121    ///
122    /// ```text
123    /// cargo:rustc-env=VERGEN_GIT_BRANCH=<BRANCH_NAME>
124    /// ```
125    ///
126    #[builder(default = all)]
127    branch: bool,
128    /// Emit the author email of the most recent commit
129    ///
130    /// ```text
131    /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_EMAIL=<AUTHOR_EMAIL>
132    /// ```
133    ///
134    #[builder(default = all)]
135    commit_author_name: bool,
136    /// Emit the author name of the most recent commit
137    ///
138    /// ```text
139    /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_NAME=<AUTHOR_NAME>
140    /// ```
141    ///
142    #[builder(default = all)]
143    commit_author_email: bool,
144    /// Emit the total commit count to HEAD
145    ///
146    /// ```text
147    /// cargo:rustc-env=VERGEN_GIT_COMMIT_COUNT=<COUNT>
148    /// ```
149    #[builder(default = all)]
150    commit_count: bool,
151    /// Emit the commit message of the latest commit
152    ///
153    /// ```text
154    /// cargo:rustc-env=VERGEN_GIT_COMMIT_MESSAGE=<MESSAGE>
155    /// ```
156    ///
157    #[builder(default = all)]
158    commit_message: bool,
159    /// Emit the commit date of the latest commit
160    ///
161    /// ```text
162    /// cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=<YYYY-MM-DD>
163    /// ```
164    ///
165    #[builder(default = all)]
166    commit_date: bool,
167    /// Emit the commit timestamp of the latest commit
168    ///
169    /// ```text
170    /// cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=<YYYY-MM-DDThh:mm:ssZ>
171    /// ```
172    ///
173    #[builder(default = all)]
174    commit_timestamp: bool,
175    /// Emit the describe output
176    ///
177    /// ```text
178    /// cargo:rustc-env=VERGEN_GIT_DESCRIBE=<DESCRIBE>
179    /// ```
180    ///
181    /// Optionally, add the `dirty` or `tags` flag to describe.
182    /// See [`git describe`](https://git-scm.com/docs/git-describe#_options) for more details
183    ///
184    /// ## `tags`
185    /// Instead of using only the annotated tags, use any tag found in refs/tags namespace.
186    ///
187    /// ## `dirty`
188    /// If the working tree has local modification "-dirty" is appended to it.
189    ///
190    /// ## `match_pattern`
191    /// Only consider tags matching the given glob pattern, excluding the "refs/tags/" prefix.
192    #[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    /// Emit the SHA of the latest commit
201    ///
202    /// ```text
203    /// cargo:rustc-env=VERGEN_GIT_SHA=<SHA>
204    /// ```
205    ///
206    /// Optionally, add the `short` flag to rev-parse.
207    /// See [`git rev-parse`](https://git-scm.com/docs/git-rev-parse#_options_for_output) for more details.
208    ///
209    /// ## `short`
210    /// Shortens the object name to a unique prefix
211    #[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    /// Emit the dirty state of the git repository
218    /// ```text
219    /// cargo:rustc-env=VERGEN_GIT_DIRTY=(true|false)
220    /// ```
221    ///
222    /// Optionally, include untracked files when determining the dirty status of the repository.
223    ///
224    /// # `include_tracked`
225    /// Should we include/ignore untracked files in deciding whether the repository is dirty.
226    #[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    /// Enable local offset date/timestamp output
233    #[builder(default = false)]
234    use_local: bool,
235    #[cfg(test)]
236    /// Fail
237    #[builder(default = false)]
238    fail: bool,
239}
240
241impl<S: git2_builder::State> Git2Builder<S> {
242    /// Convenience method that switches the defaults of [`Git2Builder`]
243    /// to enable all of the `VERGEN_GIT_*` instructions. It can only be
244    /// called at the start of the building process, i.e. when no config
245    /// has been set yet to avoid overwrites.
246    fn all(mut self) -> Self {
247        self.all = true;
248        self
249    }
250}
251
252impl Git2 {
253    /// Emit all of the `VERGEN_GIT_*` instructions
254    #[must_use]
255    pub fn all_git() -> Git2 {
256        Self::builder().all().build()
257    }
258
259    /// Convenience method to setup the [`Git2`] builder with all of the `VERGEN_GIT_*` instructions on
260    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    /// Use the repository location at the given path to determine the git instruction output.
278    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                    // detached head
391                    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 we used a remote URL, we should clean up the repo we cloned
416            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        // Setup the head path
626        let mut head_path = git_path.to_path_buf();
627        head_path.push("HEAD");
628
629        // Check whether the path exists in the filesystem before emitting it
630        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            // Check whether the path exists in the filesystem before emitting it
640            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    // this in not included in coverage, because on *nix the local offset is always unsafe
761    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            // Clear any previous warnings.  This should be it.
859            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            // For testing only
1313            .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            // For testing only
1331            .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            // For testing only
1350            .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            // For testing only
1367            .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            // For testing only
1385            .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}