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
9#[cfg(test)]
10use anyhow::anyhow;
11use anyhow::{Error, Result};
12use derive_builder::Builder as DeriveBuilder;
13use git2_rs::{
14    BranchType, Commit, DescribeFormatOptions, DescribeOptions, Reference, Repository,
15    StatusOptions,
16};
17use std::{
18    env::{self, VarError},
19    path::{Path, PathBuf},
20    str::FromStr,
21};
22use time::{
23    format_description::{self, well_known::Iso8601},
24    OffsetDateTime, UtcOffset,
25};
26use vergen_lib::{
27    add_default_map_entry, add_map_entry,
28    constants::{
29        GIT_BRANCH_NAME, GIT_COMMIT_AUTHOR_EMAIL, GIT_COMMIT_AUTHOR_NAME, GIT_COMMIT_COUNT,
30        GIT_COMMIT_DATE_NAME, GIT_COMMIT_MESSAGE, GIT_COMMIT_TIMESTAMP_NAME, GIT_DESCRIBE_NAME,
31        GIT_DIRTY_NAME, GIT_SHA_NAME,
32    },
33    AddEntries, CargoRerunIfChanged, CargoRustcEnvMap, CargoWarning, DefaultConfig, VergenKey,
34};
35
36/// The `VERGEN_GIT_*` configuration features
37///
38/// | Variable | Sample |
39/// | -------  | ------ |
40/// | `VERGEN_GIT_BRANCH` | feature/fun |
41/// | `VERGEN_GIT_COMMIT_AUTHOR_EMAIL` | janedoe@email.com |
42/// | `VERGEN_GIT_COMMIT_AUTHOR_NAME` | Jane Doe |
43/// | `VERGEN_GIT_COMMIT_COUNT` | 330 |
44/// | `VERGEN_GIT_COMMIT_DATE` | 2021-02-24 |
45/// | `VERGEN_GIT_COMMIT_MESSAGE` | feat: add commit messages |
46/// | `VERGEN_GIT_COMMIT_TIMESTAMP` | 2021-02-24T20:55:21+00:00 |
47/// | `VERGEN_GIT_DESCRIBE` | 5.0.0-2-gf49246c |
48/// | `VERGEN_GIT_SHA` | f49246ce334567bff9f950bfd0f3078184a2738a |
49/// | `VERGEN_GIT_DIRTY` | true |
50///
51/// # Example
52///
53/// ```
54/// # use anyhow::Result;
55/// # use vergen_git2::{Emitter, Git2Builder};
56/// #
57/// # fn main() -> Result<()> {
58/// let git2 = Git2Builder::all_git()?;
59/// Emitter::default().add_instructions(&git2)?.emit()?;
60/// #   Ok(())
61/// # }
62/// ```
63///
64/// Override output with your own value
65///
66/// ```
67/// # use anyhow::Result;
68/// # use vergen_git2::{Emitter, Git2Builder};
69/// #
70/// # fn main() -> Result<()> {
71/// temp_env::with_var("VERGEN_GIT_BRANCH", Some("this is the branch I want output"), || {
72///     let result = || -> Result<()> {
73///         let git2 = Git2Builder::all_git()?;
74///         Emitter::default().add_instructions(&git2)?.emit()?;
75///         Ok(())
76///     }();
77///     assert!(result.is_ok());
78/// });
79/// #   Ok(())
80/// # }
81/// ```
82///
83#[derive(Clone, Debug, DeriveBuilder, PartialEq)]
84#[allow(clippy::struct_excessive_bools)]
85pub struct Git2 {
86    /// An optional path to a repository.
87    #[builder(default = "None")]
88    repo_path: Option<PathBuf>,
89    /// Emit the current git branch
90    ///
91    /// ```text
92    /// cargo:rustc-env=VERGEN_GIT_BRANCH=<BRANCH_NAME>
93    /// ```
94    ///
95    #[builder(default = "false")]
96    branch: bool,
97    /// Emit the author email of the most recent commit
98    ///
99    /// ```text
100    /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_EMAIL=<AUTHOR_EMAIL>
101    /// ```
102    ///
103    #[builder(default = "false")]
104    commit_author_name: bool,
105    /// Emit the author name of the most recent commit
106    ///
107    /// ```text
108    /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_NAME=<AUTHOR_NAME>
109    /// ```
110    ///
111    #[builder(default = "false")]
112    commit_author_email: bool,
113    /// Emit the total commit count to HEAD
114    ///
115    /// ```text
116    /// cargo:rustc-env=VERGEN_GIT_COMMIT_COUNT=<COUNT>
117    /// ```
118    #[builder(default = "false")]
119    commit_count: bool,
120    /// Emit the commit message of the latest commit
121    ///
122    /// ```text
123    /// cargo:rustc-env=VERGEN_GIT_COMMIT_MESSAGE=<MESSAGE>
124    /// ```
125    ///
126    #[builder(default = "false")]
127    commit_message: bool,
128    /// Emit the commit date of the latest commit
129    ///
130    /// ```text
131    /// cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=<YYYY-MM-DD>
132    /// ```
133    ///
134    #[builder(default = "false")]
135    commit_date: bool,
136    /// Emit the commit timestamp of the latest commit
137    ///
138    /// ```text
139    /// cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=<YYYY-MM-DDThh:mm:ssZ>
140    /// ```
141    ///
142    #[builder(default = "false")]
143    commit_timestamp: bool,
144    /// Emit the describe output
145    ///
146    /// ```text
147    /// cargo:rustc-env=VERGEN_GIT_DESCRIBE=<DESCRIBE>
148    /// ```
149    ///
150    /// Optionally, add the `dirty` or `tags` flag to describe.
151    /// See [`git describe`](https://git-scm.com/docs/git-describe#_options) for more details
152    ///
153    #[builder(default = "false", setter(custom))]
154    describe: bool,
155    /// Instead of using only the annotated tags, use any tag found in refs/tags namespace.
156    #[builder(default = "false", private)]
157    describe_tags: bool,
158    /// If the working tree has local modification "-dirty" is appended to it.
159    #[builder(default = "false", private)]
160    describe_dirty: bool,
161    /// Only consider tags matching the given glob pattern, excluding the "refs/tags/" prefix.
162    #[builder(default = "None", private)]
163    describe_match_pattern: Option<&'static str>,
164    /// Emit the SHA of the latest commit
165    ///
166    /// ```text
167    /// cargo:rustc-env=VERGEN_GIT_SHA=<SHA>
168    /// ```
169    ///
170    /// Optionally, add the `short` flag to rev-parse.
171    /// See [`git rev-parse`](https://git-scm.com/docs/git-rev-parse#_options_for_output) for more details.
172    ///
173    #[builder(default = "false", setter(custom))]
174    sha: bool,
175    /// Shortens the object name to a unique prefix
176    #[builder(default = "false", private)]
177    sha_short: bool,
178    /// Emit the dirty state of the git repository
179    /// ```text
180    /// cargo:rustc-env=VERGEN_GIT_DIRTY=(true|false)
181    /// ```
182    ///
183    /// Optionally, include untracked files when determining the dirty status of the repository.
184    ///
185    #[builder(default = "false", setter(custom))]
186    dirty: bool,
187    /// Should we include/ignore untracked files in deciding whether the repository is dirty.
188    #[builder(default = "false", private)]
189    dirty_include_untracked: bool,
190    /// Enable local offset date/timestamp output
191    #[builder(default = "false")]
192    use_local: bool,
193    #[cfg(test)]
194    /// Fail
195    #[builder(default = "false")]
196    fail: bool,
197}
198
199impl Git2Builder {
200    /// Emit all of the `VERGEN_GIT_*` instructions
201    ///
202    /// # Errors
203    /// The underlying build function can error
204    ///
205    pub fn all_git() -> Result<Git2> {
206        Self::default()
207            .branch(true)
208            .commit_author_email(true)
209            .commit_author_name(true)
210            .commit_count(true)
211            .commit_date(true)
212            .commit_message(true)
213            .commit_timestamp(true)
214            .describe(false, false, None)
215            .sha(false)
216            .dirty(false)
217            .build()
218            .map_err(Into::into)
219    }
220
221    /// Convenience method to setup the [`Git2Builder`] with all of the `VERGEN_GIT_*` instructions on
222    pub fn all(&mut self) -> &mut Self {
223        self.branch(true)
224            .commit_author_email(true)
225            .commit_author_name(true)
226            .commit_count(true)
227            .commit_date(true)
228            .commit_message(true)
229            .commit_timestamp(true)
230            .describe(false, false, None)
231            .sha(false)
232            .dirty(false)
233    }
234
235    /// Emit the describe output
236    ///
237    /// ```text
238    /// cargo:rustc-env=VERGEN_GIT_DESCRIBE=<DESCRIBE>
239    /// ```
240    ///
241    /// Optionally, add the `dirty` or `tags` flag to describe.
242    /// See [`git describe`](https://git-scm.com/docs/git-describe#_options) for more details
243    ///
244    pub fn describe(
245        &mut self,
246        tags: bool,
247        dirty: bool,
248        matches: Option<&'static str>,
249    ) -> &mut Self {
250        self.describe = Some(true);
251        let _ = self.describe_tags(tags);
252        let _ = self.describe_dirty(dirty);
253        let _ = self.describe_match_pattern(matches);
254        self
255    }
256
257    /// Emit the dirty state of the git repository
258    /// ```text
259    /// cargo:rustc-env=VERGEN_GIT_DIRTY=(true|false)
260    /// ```
261    ///
262    /// Optionally, include untracked files when determining the dirty status of the repository.
263    ///
264    pub fn dirty(&mut self, include_untracked: bool) -> &mut Self {
265        self.dirty = Some(true);
266        let _ = self.dirty_include_untracked(include_untracked);
267        self
268    }
269
270    /// Emit the SHA of the latest commit
271    ///
272    /// ```text
273    /// cargo:rustc-env=VERGEN_GIT_SHA=<SHA>
274    /// ```
275    ///
276    /// Optionally, add the `short` flag to rev-parse.
277    /// See [`git rev-parse`](https://git-scm.com/docs/git-rev-parse#_options_for_output) for more details.
278    ///
279    pub fn sha(&mut self, short: bool) -> &mut Self {
280        self.sha = Some(true);
281        let _ = self.sha_short(short);
282        self
283    }
284}
285
286impl Git2 {
287    fn any(&self) -> bool {
288        self.branch
289            || self.commit_author_email
290            || self.commit_author_name
291            || self.commit_count
292            || self.commit_date
293            || self.commit_message
294            || self.commit_timestamp
295            || self.describe
296            || self.sha
297            || self.dirty
298    }
299
300    /// Use the repository location at the given path to determine the git instruction output.
301    pub fn at_path(&mut self, path: PathBuf) -> &mut Self {
302        self.repo_path = Some(path);
303        self
304    }
305
306    #[cfg(test)]
307    pub(crate) fn fail(&mut self) -> &mut Self {
308        self.fail = true;
309        self
310    }
311
312    #[cfg(not(test))]
313    fn add_entries(
314        &self,
315        idempotent: bool,
316        cargo_rustc_env: &mut CargoRustcEnvMap,
317        cargo_rerun_if_changed: &mut CargoRerunIfChanged,
318        cargo_warning: &mut CargoWarning,
319    ) -> Result<()> {
320        self.inner_add_entries(
321            idempotent,
322            cargo_rustc_env,
323            cargo_rerun_if_changed,
324            cargo_warning,
325        )
326    }
327
328    #[cfg(test)]
329    fn add_entries(
330        &self,
331        idempotent: bool,
332        cargo_rustc_env: &mut CargoRustcEnvMap,
333        cargo_rerun_if_changed: &mut CargoRerunIfChanged,
334        cargo_warning: &mut CargoWarning,
335    ) -> Result<()> {
336        if self.fail {
337            return Err(anyhow!("failed to create entries"));
338        }
339        self.inner_add_entries(
340            idempotent,
341            cargo_rustc_env,
342            cargo_rerun_if_changed,
343            cargo_warning,
344        )
345    }
346
347    #[allow(clippy::too_many_lines)]
348    fn inner_add_entries(
349        &self,
350        idempotent: bool,
351        cargo_rustc_env: &mut CargoRustcEnvMap,
352        cargo_rerun_if_changed: &mut CargoRerunIfChanged,
353        cargo_warning: &mut CargoWarning,
354    ) -> Result<()> {
355        let repo_dir = if let Some(path) = &self.repo_path {
356            path.clone()
357        } else {
358            env::current_dir()?
359        };
360        let repo = Repository::discover(repo_dir)?;
361        let ref_head = repo.find_reference("HEAD")?;
362        let git_path = repo.path().to_path_buf();
363        let commit = ref_head.peel_to_commit()?;
364
365        if !idempotent && self.any() {
366            Self::add_rerun_if_changed(&ref_head, &git_path, cargo_rerun_if_changed);
367        }
368
369        if self.branch {
370            if let Ok(_value) = env::var(GIT_BRANCH_NAME) {
371                add_default_map_entry(VergenKey::GitBranch, cargo_rustc_env, cargo_warning);
372            } else {
373                Self::add_branch_name(false, &repo, cargo_rustc_env, cargo_warning)?;
374            }
375        }
376
377        if self.commit_author_email {
378            if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_EMAIL) {
379                add_default_map_entry(
380                    VergenKey::GitCommitAuthorEmail,
381                    cargo_rustc_env,
382                    cargo_warning,
383                );
384            } else {
385                Self::add_opt_value(
386                    commit.author().email(),
387                    VergenKey::GitCommitAuthorEmail,
388                    cargo_rustc_env,
389                    cargo_warning,
390                );
391            }
392        }
393
394        if self.commit_author_name {
395            if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_NAME) {
396                add_default_map_entry(
397                    VergenKey::GitCommitAuthorName,
398                    cargo_rustc_env,
399                    cargo_warning,
400                );
401            } else {
402                Self::add_opt_value(
403                    commit.author().name(),
404                    VergenKey::GitCommitAuthorName,
405                    cargo_rustc_env,
406                    cargo_warning,
407                );
408            }
409        }
410
411        if self.commit_count {
412            if let Ok(_value) = env::var(GIT_COMMIT_COUNT) {
413                add_default_map_entry(VergenKey::GitCommitCount, cargo_rustc_env, cargo_warning);
414            } else {
415                Self::add_commit_count(false, &repo, cargo_rustc_env, cargo_warning);
416            }
417        }
418
419        self.add_git_timestamp_entries(&commit, idempotent, cargo_rustc_env, cargo_warning)?;
420
421        if self.commit_message {
422            if let Ok(_value) = env::var(GIT_COMMIT_MESSAGE) {
423                add_default_map_entry(VergenKey::GitCommitMessage, cargo_rustc_env, cargo_warning);
424            } else {
425                Self::add_opt_value(
426                    commit.message(),
427                    VergenKey::GitCommitMessage,
428                    cargo_rustc_env,
429                    cargo_warning,
430                );
431            }
432        }
433
434        if self.sha {
435            if let Ok(_value) = env::var(GIT_SHA_NAME) {
436                add_default_map_entry(VergenKey::GitSha, cargo_rustc_env, cargo_warning);
437            } else if self.sha_short {
438                let obj = repo.revparse_single("HEAD")?;
439                Self::add_opt_value(
440                    obj.short_id()?.as_str(),
441                    VergenKey::GitSha,
442                    cargo_rustc_env,
443                    cargo_warning,
444                );
445            } else {
446                add_map_entry(VergenKey::GitSha, commit.id().to_string(), cargo_rustc_env);
447            }
448        }
449
450        if self.dirty {
451            if let Ok(_value) = env::var(GIT_DIRTY_NAME) {
452                add_default_map_entry(VergenKey::GitDirty, cargo_rustc_env, cargo_warning);
453            } else {
454                let mut status_options = StatusOptions::new();
455
456                _ = status_options.include_untracked(self.dirty_include_untracked);
457                let statuses = repo.statuses(Some(&mut status_options))?;
458
459                let n_dirty = statuses
460                    .iter()
461                    .filter(|each_status| !each_status.status().is_ignored())
462                    .count();
463
464                add_map_entry(
465                    VergenKey::GitDirty,
466                    format!("{}", n_dirty > 0),
467                    cargo_rustc_env,
468                );
469            }
470        }
471
472        if self.describe {
473            if let Ok(_value) = env::var(GIT_DESCRIBE_NAME) {
474                add_default_map_entry(VergenKey::GitDescribe, cargo_rustc_env, cargo_warning);
475            } else {
476                let mut describe_opts = DescribeOptions::new();
477                let mut format_opts = DescribeFormatOptions::new();
478
479                _ = describe_opts.show_commit_oid_as_fallback(true);
480
481                if self.describe_dirty {
482                    _ = format_opts.dirty_suffix("-dirty");
483                }
484
485                if self.describe_tags {
486                    _ = describe_opts.describe_tags();
487                }
488
489                if let Some(pattern) = self.describe_match_pattern {
490                    _ = describe_opts.pattern(pattern);
491                }
492
493                let describe = repo
494                    .describe(&describe_opts)
495                    .map(|x| x.format(Some(&format_opts)).map_err(Error::from))??;
496                add_map_entry(VergenKey::GitDescribe, describe, cargo_rustc_env);
497            }
498        }
499
500        Ok(())
501    }
502
503    fn add_rerun_if_changed(
504        ref_head: &Reference<'_>,
505        git_path: &Path,
506        cargo_rerun_if_changed: &mut CargoRerunIfChanged,
507    ) {
508        // Setup the head path
509        let mut head_path = git_path.to_path_buf();
510        head_path.push("HEAD");
511
512        // Check whether the path exists in the filesystem before emitting it
513        if head_path.exists() {
514            cargo_rerun_if_changed.push(format!("{}", head_path.display()));
515        }
516
517        if let Ok(resolved) = ref_head.resolve() {
518            if let Some(name) = resolved.name() {
519                let ref_path = git_path.to_path_buf();
520                let path = ref_path.join(name);
521                // Check whether the path exists in the filesystem before emitting it
522                if path.exists() {
523                    cargo_rerun_if_changed.push(format!("{}", path.display()));
524                }
525            }
526        }
527    }
528
529    fn add_branch_name(
530        add_default: bool,
531        repo: &Repository,
532        cargo_rustc_env: &mut CargoRustcEnvMap,
533        cargo_warning: &mut CargoWarning,
534    ) -> Result<()> {
535        if repo.head_detached()? {
536            if add_default {
537                add_default_map_entry(VergenKey::GitBranch, cargo_rustc_env, cargo_warning);
538            } else {
539                add_map_entry(VergenKey::GitBranch, "HEAD", cargo_rustc_env);
540            }
541        } else {
542            let locals = repo.branches(Some(BranchType::Local))?;
543            let mut found_head = false;
544            for (local, _bt) in locals.filter_map(std::result::Result::ok) {
545                if local.is_head() {
546                    if let Some(name) = local.name()? {
547                        add_map_entry(VergenKey::GitBranch, name, cargo_rustc_env);
548                        found_head = !add_default;
549                        break;
550                    }
551                }
552            }
553            if !found_head {
554                add_default_map_entry(VergenKey::GitBranch, cargo_rustc_env, cargo_warning);
555            }
556        }
557        Ok(())
558    }
559
560    #[allow(clippy::map_unwrap_or)]
561    fn add_opt_value(
562        value: Option<&str>,
563        key: VergenKey,
564        cargo_rustc_env: &mut CargoRustcEnvMap,
565        cargo_warning: &mut CargoWarning,
566    ) {
567        value
568            .map(|val| add_map_entry(key, val, cargo_rustc_env))
569            .unwrap_or_else(|| add_default_map_entry(key, cargo_rustc_env, cargo_warning));
570    }
571
572    fn add_commit_count(
573        add_default: bool,
574        repo: &Repository,
575        cargo_rustc_env: &mut CargoRustcEnvMap,
576        cargo_warning: &mut CargoWarning,
577    ) {
578        let key = VergenKey::GitCommitCount;
579        if !add_default {
580            if let Ok(mut revwalk) = repo.revwalk() {
581                if revwalk.push_head().is_ok() {
582                    add_map_entry(key, revwalk.count().to_string(), cargo_rustc_env);
583                    return;
584                }
585            }
586        }
587        add_default_map_entry(key, cargo_rustc_env, cargo_warning);
588    }
589
590    fn add_git_timestamp_entries(
591        &self,
592        commit: &Commit<'_>,
593        idempotent: bool,
594        cargo_rustc_env: &mut CargoRustcEnvMap,
595        cargo_warning: &mut CargoWarning,
596    ) -> Result<()> {
597        let (sde, ts) = match env::var("SOURCE_DATE_EPOCH") {
598            Ok(v) => (
599                true,
600                OffsetDateTime::from_unix_timestamp(i64::from_str(&v)?)?,
601            ),
602            Err(VarError::NotPresent) => self.compute_local_offset(commit)?,
603            Err(e) => return Err(e.into()),
604        };
605
606        if let Ok(_value) = env::var(GIT_COMMIT_DATE_NAME) {
607            add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env, cargo_warning);
608        } else {
609            self.add_git_date_entry(idempotent, sde, &ts, cargo_rustc_env, cargo_warning)?;
610        }
611        if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
612            add_default_map_entry(
613                VergenKey::GitCommitTimestamp,
614                cargo_rustc_env,
615                cargo_warning,
616            );
617        } else {
618            self.add_git_timestamp_entry(idempotent, sde, &ts, cargo_rustc_env, cargo_warning)?;
619        }
620        Ok(())
621    }
622
623    #[cfg_attr(coverage_nightly, coverage(off))]
624    // this in not included in coverage, because on *nix the local offset is always unsafe
625    fn compute_local_offset(&self, commit: &Commit<'_>) -> Result<(bool, OffsetDateTime)> {
626        let no_offset = OffsetDateTime::from_unix_timestamp(commit.time().seconds())?;
627        if self.use_local {
628            let local = UtcOffset::local_offset_at(no_offset)?;
629            let local_offset = no_offset.checked_to_offset(local).unwrap_or(no_offset);
630            Ok((false, local_offset))
631        } else {
632            Ok((false, no_offset))
633        }
634    }
635
636    fn add_git_date_entry(
637        &self,
638        idempotent: bool,
639        source_date_epoch: bool,
640        ts: &OffsetDateTime,
641        cargo_rustc_env: &mut CargoRustcEnvMap,
642        cargo_warning: &mut CargoWarning,
643    ) -> Result<()> {
644        if self.commit_date {
645            if idempotent && !source_date_epoch {
646                add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env, cargo_warning);
647            } else {
648                let format = format_description::parse("[year]-[month]-[day]")?;
649                add_map_entry(
650                    VergenKey::GitCommitDate,
651                    ts.format(&format)?,
652                    cargo_rustc_env,
653                );
654            }
655        }
656        Ok(())
657    }
658
659    fn add_git_timestamp_entry(
660        &self,
661        idempotent: bool,
662        source_date_epoch: bool,
663        ts: &OffsetDateTime,
664        cargo_rustc_env: &mut CargoRustcEnvMap,
665        cargo_warning: &mut CargoWarning,
666    ) -> Result<()> {
667        if self.commit_timestamp {
668            if idempotent && !source_date_epoch {
669                add_default_map_entry(
670                    VergenKey::GitCommitTimestamp,
671                    cargo_rustc_env,
672                    cargo_warning,
673                );
674            } else {
675                add_map_entry(
676                    VergenKey::GitCommitTimestamp,
677                    ts.format(&Iso8601::DEFAULT)?,
678                    cargo_rustc_env,
679                );
680            }
681        }
682        Ok(())
683    }
684}
685
686impl AddEntries for Git2 {
687    fn add_map_entries(
688        &self,
689        idempotent: bool,
690        cargo_rustc_env: &mut CargoRustcEnvMap,
691        cargo_rerun_if_changed: &mut CargoRerunIfChanged,
692        cargo_warning: &mut CargoWarning,
693    ) -> Result<()> {
694        if self.any() {
695            self.add_entries(
696                idempotent,
697                cargo_rustc_env,
698                cargo_rerun_if_changed,
699                cargo_warning,
700            )?;
701        }
702        Ok(())
703    }
704
705    fn add_default_entries(
706        &self,
707        config: &DefaultConfig,
708        cargo_rustc_env_map: &mut CargoRustcEnvMap,
709        cargo_rerun_if_changed: &mut CargoRerunIfChanged,
710        cargo_warning: &mut CargoWarning,
711    ) -> Result<()> {
712        if *config.fail_on_error() {
713            let error = Error::msg(format!("{}", config.error()));
714            Err(error)
715        } else {
716            // Clear any previous warnings.  This should be it.
717            cargo_warning.clear();
718            cargo_rerun_if_changed.clear();
719
720            cargo_warning.push(format!("{}", config.error()));
721
722            if self.branch {
723                add_default_map_entry(VergenKey::GitBranch, cargo_rustc_env_map, cargo_warning);
724            }
725            if self.commit_author_email {
726                add_default_map_entry(
727                    VergenKey::GitCommitAuthorEmail,
728                    cargo_rustc_env_map,
729                    cargo_warning,
730                );
731            }
732            if self.commit_author_name {
733                add_default_map_entry(
734                    VergenKey::GitCommitAuthorName,
735                    cargo_rustc_env_map,
736                    cargo_warning,
737                );
738            }
739            if self.commit_count {
740                add_default_map_entry(
741                    VergenKey::GitCommitCount,
742                    cargo_rustc_env_map,
743                    cargo_warning,
744                );
745            }
746            if self.commit_date {
747                add_default_map_entry(VergenKey::GitCommitDate, cargo_rustc_env_map, cargo_warning);
748            }
749            if self.commit_message {
750                add_default_map_entry(
751                    VergenKey::GitCommitMessage,
752                    cargo_rustc_env_map,
753                    cargo_warning,
754                );
755            }
756            if self.commit_timestamp {
757                add_default_map_entry(
758                    VergenKey::GitCommitTimestamp,
759                    cargo_rustc_env_map,
760                    cargo_warning,
761                );
762            }
763            if self.describe {
764                add_default_map_entry(VergenKey::GitDescribe, cargo_rustc_env_map, cargo_warning);
765            }
766            if self.sha {
767                add_default_map_entry(VergenKey::GitSha, cargo_rustc_env_map, cargo_warning);
768            }
769            if self.dirty {
770                add_default_map_entry(VergenKey::GitDirty, cargo_rustc_env_map, cargo_warning);
771            }
772            Ok(())
773        }
774    }
775}
776
777#[cfg(test)]
778mod test {
779    use super::{Git2, Git2Builder};
780    use anyhow::Result;
781    use git2_rs::Repository;
782    use serial_test::serial;
783    #[cfg(unix)]
784    use std::io::stdout;
785    use std::{collections::BTreeMap, env::current_dir, io::Write};
786    use test_util::TestRepos;
787    #[cfg(unix)]
788    use test_util::TEST_MTIME;
789    use vergen::Emitter;
790    use vergen_lib::{count_idempotent, VergenKey};
791
792    #[test]
793    #[serial]
794    #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
795    fn git2_clone_works() -> Result<()> {
796        let git2 = Git2Builder::all_git()?;
797        let another = git2.clone();
798        assert_eq!(another, git2);
799        Ok(())
800    }
801
802    #[test]
803    #[serial]
804    fn git2_debug_works() -> Result<()> {
805        let git2 = Git2Builder::all_git()?;
806        let mut buf = vec![];
807        write!(buf, "{git2:?}")?;
808        assert!(!buf.is_empty());
809        Ok(())
810    }
811
812    #[test]
813    #[serial]
814    fn git2_default() -> Result<()> {
815        let git2 = Git2Builder::default().build()?;
816        let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
817        assert_eq!(0, emitter.cargo_rustc_env_map().len());
818        assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
819        assert_eq!(0, emitter.cargo_warning().len());
820        Ok(())
821    }
822
823    #[test]
824    #[serial]
825    fn empty_email_is_default() -> Result<()> {
826        let mut cargo_rustc_env = BTreeMap::new();
827        let mut cargo_warning = vec![];
828        Git2::add_opt_value(
829            None,
830            VergenKey::GitCommitAuthorEmail,
831            &mut cargo_rustc_env,
832            &mut cargo_warning,
833        );
834        assert_eq!(1, cargo_rustc_env.len());
835        assert_eq!(1, cargo_warning.len());
836        Ok(())
837    }
838
839    #[test]
840    #[serial]
841    fn bad_revwalk_is_default() -> Result<()> {
842        let mut cargo_rustc_env = BTreeMap::new();
843        let mut cargo_warning = vec![];
844        let repo = Repository::discover(current_dir()?)?;
845        Git2::add_commit_count(true, &repo, &mut cargo_rustc_env, &mut cargo_warning);
846        assert_eq!(1, cargo_rustc_env.len());
847        assert_eq!(1, cargo_warning.len());
848        Ok(())
849    }
850
851    #[test]
852    #[serial]
853    fn head_not_found_is_default() -> Result<()> {
854        let test_repo = TestRepos::new(false, false, false)?;
855        let mut map = BTreeMap::new();
856        let mut cargo_warning = vec![];
857        let repo = Repository::discover(current_dir()?)?;
858        Git2::add_branch_name(true, &repo, &mut map, &mut cargo_warning)?;
859        assert_eq!(1, map.len());
860        assert_eq!(1, cargo_warning.len());
861        let mut map = BTreeMap::new();
862        let mut cargo_warning = vec![];
863        let repo = Repository::discover(test_repo.path())?;
864        Git2::add_branch_name(true, &repo, &mut map, &mut cargo_warning)?;
865        assert_eq!(1, map.len());
866        assert_eq!(1, cargo_warning.len());
867        Ok(())
868    }
869
870    #[test]
871    #[serial]
872    fn git_all_idempotent() -> Result<()> {
873        let git2 = Git2Builder::all_git()?;
874        let emitter = Emitter::default()
875            .idempotent()
876            .add_instructions(&git2)?
877            .test_emit();
878        assert_eq!(10, emitter.cargo_rustc_env_map().len());
879        assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
880        assert_eq!(2, emitter.cargo_warning().len());
881        Ok(())
882    }
883
884    #[test]
885    #[serial]
886    fn git_all_shallow_clone() -> Result<()> {
887        let repo = TestRepos::new(false, false, true)?;
888        let mut git2 = Git2Builder::all_git()?;
889        let _ = git2.at_path(repo.path());
890        let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
891        assert_eq!(10, emitter.cargo_rustc_env_map().len());
892        assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
893        assert_eq!(0, emitter.cargo_warning().len());
894        Ok(())
895    }
896
897    #[test]
898    #[serial]
899    fn git_all_idempotent_no_warn() -> Result<()> {
900        let git2 = Git2Builder::all_git()?;
901        let emitter = Emitter::default()
902            .idempotent()
903            .quiet()
904            .add_instructions(&git2)?
905            .test_emit();
906
907        assert_eq!(10, emitter.cargo_rustc_env_map().len());
908        assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
909        assert_eq!(2, emitter.cargo_warning().len());
910        Ok(())
911    }
912
913    #[test]
914    #[serial]
915    fn git_all() -> Result<()> {
916        let git2 = Git2Builder::all_git()?;
917        let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
918        assert_eq!(10, emitter.cargo_rustc_env_map().len());
919        assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
920        assert_eq!(0, emitter.cargo_warning().len());
921        Ok(())
922    }
923
924    #[test]
925    #[serial]
926    fn git_error_fails() -> Result<()> {
927        let mut git2 = Git2Builder::all_git()?;
928        let _ = git2.fail();
929        assert!(Emitter::default()
930            .fail_on_error()
931            .add_instructions(&git2)
932            .is_err());
933        Ok(())
934    }
935
936    #[test]
937    #[serial]
938    fn git_error_defaults() -> Result<()> {
939        let mut git2 = Git2Builder::all_git()?;
940        let _ = git2.fail();
941        let emitter = Emitter::default().add_instructions(&git2)?.test_emit();
942        assert_eq!(10, emitter.cargo_rustc_env_map().len());
943        assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
944        assert_eq!(11, emitter.cargo_warning().len());
945        Ok(())
946    }
947
948    #[test]
949    #[serial]
950    fn source_date_epoch_works() {
951        temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), || {
952            let result = || -> Result<()> {
953                let mut stdout_buf = vec![];
954                let gix = Git2Builder::default()
955                    .commit_date(true)
956                    .commit_timestamp(true)
957                    .build()?;
958                _ = Emitter::new()
959                    .idempotent()
960                    .add_instructions(&gix)?
961                    .emit_to(&mut stdout_buf)?;
962                let output = String::from_utf8_lossy(&stdout_buf);
963                for (idx, line) in output.lines().enumerate() {
964                    if idx == 0 {
965                        assert_eq!("cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2022-12-23", line);
966                    } else if idx == 1 {
967                        assert_eq!(
968                            "cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2022-12-23T15:29:20.000000000Z",
969                            line
970                        );
971                    }
972                }
973                Ok(())
974            }();
975            assert!(result.is_ok());
976        });
977    }
978
979    #[test]
980    #[serial]
981    #[cfg(unix)]
982    fn bad_source_date_epoch_fails() {
983        use std::ffi::OsStr;
984        use std::os::unix::prelude::OsStrExt;
985
986        let source = [0x66, 0x6f, 0x80, 0x6f];
987        let os_str = OsStr::from_bytes(&source[..]);
988        temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
989            let result = || -> Result<bool> {
990                let mut stdout_buf = vec![];
991                let gix = Git2Builder::default().commit_date(true).build()?;
992                Emitter::new()
993                    .idempotent()
994                    .fail_on_error()
995                    .add_instructions(&gix)?
996                    .emit_to(&mut stdout_buf)
997            }();
998            assert!(result.is_err());
999        });
1000    }
1001
1002    #[test]
1003    #[serial]
1004    #[cfg(unix)]
1005    fn bad_source_date_epoch_defaults() {
1006        use std::ffi::OsStr;
1007        use std::os::unix::prelude::OsStrExt;
1008
1009        let source = [0x66, 0x6f, 0x80, 0x6f];
1010        let os_str = OsStr::from_bytes(&source[..]);
1011        temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1012            let result = || -> Result<bool> {
1013                let mut stdout_buf = vec![];
1014                let gix = Git2Builder::default().commit_date(true).build()?;
1015                Emitter::new()
1016                    .idempotent()
1017                    .add_instructions(&gix)?
1018                    .emit_to(&mut stdout_buf)
1019            }();
1020            assert!(result.is_ok());
1021        });
1022    }
1023
1024    #[test]
1025    #[serial]
1026    #[cfg(windows)]
1027    fn bad_source_date_epoch_fails() {
1028        use std::ffi::OsString;
1029        use std::os::windows::prelude::OsStringExt;
1030
1031        let source = [0x0066, 0x006f, 0xD800, 0x006f];
1032        let os_string = OsString::from_wide(&source[..]);
1033        let os_str = os_string.as_os_str();
1034        temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1035            let result = || -> Result<bool> {
1036                let mut stdout_buf = vec![];
1037                let gix = Git2Builder::default().commit_date(true).build()?;
1038                Emitter::new()
1039                    .fail_on_error()
1040                    .idempotent()
1041                    .add_instructions(&gix)?
1042                    .emit_to(&mut stdout_buf)
1043            }();
1044            assert!(result.is_err());
1045        });
1046    }
1047
1048    #[test]
1049    #[serial]
1050    #[cfg(windows)]
1051    fn bad_source_date_epoch_defaults() {
1052        use std::ffi::OsString;
1053        use std::os::windows::prelude::OsStringExt;
1054
1055        let source = [0x0066, 0x006f, 0xD800, 0x006f];
1056        let os_string = OsString::from_wide(&source[..]);
1057        let os_str = os_string.as_os_str();
1058        temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1059            let result = || -> Result<bool> {
1060                let mut stdout_buf = vec![];
1061                let gix = Git2Builder::default().commit_date(true).build()?;
1062                Emitter::new()
1063                    .idempotent()
1064                    .add_instructions(&gix)?
1065                    .emit_to(&mut stdout_buf)
1066            }();
1067            assert!(result.is_ok());
1068        });
1069    }
1070
1071    #[test]
1072    #[serial]
1073    #[cfg(unix)]
1074    fn git_no_index_update() -> Result<()> {
1075        let repo = TestRepos::new(true, true, false)?;
1076        repo.set_index_magic_mtime()?;
1077
1078        let mut git2 = Git2Builder::default()
1079            .all()
1080            .describe(true, true, None)
1081            .build()?;
1082        let _ = git2.at_path(repo.path());
1083        let failed = Emitter::default()
1084            .add_instructions(&git2)?
1085            .emit_to(&mut stdout())?;
1086        assert!(!failed);
1087
1088        assert_eq!(*TEST_MTIME, repo.get_index_magic_mtime()?);
1089        Ok(())
1090    }
1091}