vergen_gitcl/gitcl/
mod.rs

1// Copyright (c) 2022 vergen 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::gitcl_builder::Empty;
10use anyhow::{Error, Result, anyhow};
11use bon::Builder;
12use std::{
13    env::{self, VarError},
14    path::PathBuf,
15    process::{Command, Output, Stdio},
16    str::FromStr,
17};
18use time::{
19    OffsetDateTime, UtcOffset,
20    format_description::{
21        self,
22        well_known::{Iso8601, Rfc3339},
23    },
24};
25use vergen_lib::{
26    AddEntries, CargoRerunIfChanged, CargoRustcEnvMap, CargoWarning, DefaultConfig, Describe,
27    Dirty, Sha, VergenKey, 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};
34
35// This funkiness allows the command to be output in the docs
36macro_rules! branch_cmd {
37    () => {
38        "git rev-parse --abbrev-ref --symbolic-full-name HEAD"
39    };
40}
41const BRANCH_CMD: &str = branch_cmd!();
42macro_rules! author_email {
43    () => {
44        "git log -1 --pretty=format:'%ae'"
45    };
46}
47const COMMIT_AUTHOR_EMAIL: &str = author_email!();
48macro_rules! author_name {
49    () => {
50        "git log -1 --pretty=format:'%an'"
51    };
52}
53const COMMIT_AUTHOR_NAME: &str = author_name!();
54macro_rules! commit_count {
55    () => {
56        "git rev-list --count HEAD"
57    };
58}
59const COMMIT_COUNT: &str = commit_count!();
60macro_rules! commit_date {
61    () => {
62        "git log -1 --pretty=format:'%cs'"
63    };
64}
65macro_rules! commit_message {
66    () => {
67        "git log -1 --format=%s"
68    };
69}
70const COMMIT_MESSAGE: &str = commit_message!();
71macro_rules! commit_timestamp {
72    () => {
73        "git log -1 --pretty=format:'%cI'"
74    };
75}
76const COMMIT_TIMESTAMP: &str = commit_timestamp!();
77macro_rules! describe {
78    () => {
79        "git describe --always"
80    };
81}
82const DESCRIBE: &str = describe!();
83macro_rules! sha {
84    () => {
85        "git rev-parse"
86    };
87}
88const SHA: &str = sha!();
89macro_rules! dirty {
90    () => {
91        "git status --porcelain"
92    };
93}
94const DIRTY: &str = dirty!();
95
96/// The `VERGEN_GIT_*` configuration features
97///
98/// | Variable | Sample |
99/// | -------  | ------ |
100/// | `VERGEN_GIT_BRANCH` | feature/fun |
101/// | `VERGEN_GIT_COMMIT_AUTHOR_EMAIL` | janedoe@email.com |
102/// | `VERGEN_GIT_COMMIT_AUTHOR_NAME` | Jane Doe |
103/// | `VERGEN_GIT_COMMIT_COUNT` | 330 |
104/// | `VERGEN_GIT_COMMIT_DATE` | 2021-02-24 |
105/// | `VERGEN_GIT_COMMIT_MESSAGE` | feat: add commit messages |
106/// | `VERGEN_GIT_COMMIT_TIMESTAMP` | 2021-02-24T20:55:21+00:00 |
107/// | `VERGEN_GIT_DESCRIBE` | 5.0.0-2-gf49246c |
108/// | `VERGEN_GIT_SHA` | f49246ce334567bff9f950bfd0f3078184a2738a |
109/// | `VERGEN_GIT_DIRTY` | true |
110///
111/// # Example
112/// Emit all of the git instructions
113///
114/// ```
115/// # use anyhow::Result;
116/// # use vergen_gitcl::{Emitter, Gitcl};
117/// #
118/// # fn main() -> Result<()> {
119/// let gitcl = Gitcl::all_git();
120/// Emitter::default().add_instructions(&gitcl)?.emit()?;
121/// #   Ok(())
122/// # }
123/// ```
124///
125/// Emit some of the git instructions
126///
127/// ```
128/// # use anyhow::Result;
129/// # use vergen_gitcl::{Emitter, Gitcl};
130/// #
131/// # fn main() -> Result<()> {
132/// let gitcl = Gitcl::builder().describe(true, false, None).build();
133/// Emitter::default().add_instructions(&gitcl)?.emit()?;
134/// #   Ok(())
135/// # }
136/// ```
137///
138/// Override output with your own value
139///
140/// ```
141/// # use anyhow::Result;
142/// # use vergen_gitcl::{Emitter, Gitcl};
143/// #
144/// # fn main() -> Result<()> {
145/// temp_env::with_var("VERGEN_GIT_BRANCH", Some("this is the branch I want output"), || {
146///     let result = || -> Result<()> {
147///         let gitcl = Gitcl::all_git();
148///         Emitter::default().add_instructions(&gitcl)?.emit()?;
149///         Ok(())
150///     }();
151///     assert!(result.is_ok());
152/// });
153/// #   Ok(())
154/// # }
155/// ```
156///
157/// # Example
158/// This feature can also be used in conjuction with the [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/)
159/// environment variable to generate deterministic timestamps based off the
160/// last modification time of the source/package
161///
162/// ```
163/// # use anyhow::Result;
164/// # use vergen_gitcl::{Emitter, Gitcl};
165/// #
166/// # fn main() -> Result<()> {
167/// temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), || {
168///     let result = || -> Result<()> {
169///         let gitcl = Gitcl::all_git();
170///         Emitter::default().add_instructions(&gitcl)?.emit()?;
171///         Ok(())
172///     }();
173///     assert!(result.is_ok());
174/// });
175/// #   Ok(())
176/// # }
177/// ```
178///
179/// The above will always generate the following output for the timestamp
180/// related instructions
181///
182/// ```text
183/// ...
184/// cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2022-12-23
185/// ...
186/// cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2022-12-23T15:29:20.000000000Z
187/// ...
188/// ```
189///
190/// # Example
191/// This feature also recognizes the idempotent flag.
192///
193/// **NOTE** - `SOURCE_DATE_EPOCH` takes precedence over the idempotent flag. If you
194/// use both, the output will be based off `SOURCE_DATE_EPOCH`.  This would still be
195/// deterministic.
196///
197/// # Example
198/// ```
199/// # use anyhow::Result;
200/// # use vergen_gitcl::{Emitter, Gitcl};
201/// #
202/// # fn main() -> Result<()> {
203/// let gitcl = Gitcl::all_git();
204/// Emitter::default().idempotent().add_instructions(&gitcl)?.emit()?;
205/// #   Ok(())
206/// # }
207/// ```
208///
209/// The above will always generate the following instructions
210///
211/// ```text
212/// cargo:rustc-env=VERGEN_GIT_BRANCH=VERGEN_IDEMPOTENT_OUTPUT
213/// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_EMAIL=VERGEN_IDEMPOTENT_OUTPUT
214/// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_NAME=VERGEN_IDEMPOTENT_OUTPUT
215/// cargo:rustc-env=VERGEN_GIT_COMMIT_COUNT=VERGEN_IDEMPOTENT_OUTPUT
216/// cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=VERGEN_IDEMPOTENT_OUTPUT
217/// cargo:rustc-env=VERGEN_GIT_COMMIT_MESSAGE=VERGEN_IDEMPOTENT_OUTPUT
218/// cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=VERGEN_IDEMPOTENT_OUTPUT
219/// cargo:rustc-env=VERGEN_GIT_DESCRIBE=VERGEN_IDEMPOTENT_OUTPUT
220/// cargo:rustc-env=VERGEN_GIT_SHA=VERGEN_IDEMPOTENT_OUTPUT
221/// cargo:warning=VERGEN_GIT_BRANCH set to default
222/// cargo:warning=VERGEN_GIT_COMMIT_AUTHOR_EMAIL set to default
223/// cargo:warning=VERGEN_GIT_COMMIT_AUTHOR_NAME set to default
224/// cargo:warning=VERGEN_GIT_COMMIT_COUNT set to default
225/// cargo:warning=VERGEN_GIT_COMMIT_DATE set to default
226/// cargo:warning=VERGEN_GIT_COMMIT_MESSAGE set to default
227/// cargo:warning=VERGEN_GIT_COMMIT_TIMESTAMP set to default
228/// cargo:warning=VERGEN_GIT_DESCRIBE set to default
229/// cargo:warning=VERGEN_GIT_SHA set to default
230/// cargo:rerun-if-changed=build.rs
231/// cargo:rerun-if-env-changed=VERGEN_IDEMPOTENT
232/// cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH
233/// ```
234///
235#[derive(Builder, Clone, Debug, PartialEq)]
236#[allow(clippy::struct_excessive_bools)]
237pub struct Gitcl {
238    /// Configures the default values.
239    /// If set to `true` all defaults are in "enabled" state.
240    /// If set to `false` all defaults are in "disabled" state.
241    #[builder(field)]
242    all: bool,
243    /// An optional path to a repository.
244    #[builder(into)]
245    repo_path: Option<PathBuf>,
246    /// Emit the current git branch
247    ///
248    /// ```text
249    /// cargo:rustc-env=VERGEN_GIT_BRANCH=<BRANCH_NAME>
250    /// ```
251    ///
252    #[builder(default = all)]
253    branch: bool,
254    /// Emit the author email of the most recent commit
255    ///
256    /// ```text
257    /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_EMAIL=<AUTHOR_EMAIL>
258    /// ```
259    ///
260    #[builder(default = all)]
261    commit_author_name: bool,
262    /// Emit the author name of the most recent commit
263    ///
264    /// ```text
265    /// cargo:rustc-env=VERGEN_GIT_COMMIT_AUTHOR_NAME=<AUTHOR_NAME>
266    /// ```
267    ///
268    #[builder(default = all)]
269    commit_author_email: bool,
270    /// Emit the total commit count to HEAD
271    ///
272    /// ```text
273    /// cargo:rustc-env=VERGEN_GIT_COMMIT_COUNT=<COUNT>
274    /// ```
275    #[builder(default = all)]
276    commit_count: bool,
277    /// Emit the commit message of the latest commit
278    ///
279    /// ```text
280    /// cargo:rustc-env=VERGEN_GIT_COMMIT_MESSAGE=<MESSAGE>
281    /// ```
282    ///
283    #[builder(default = all)]
284    commit_message: bool,
285    /// Emit the commit date of the latest commit
286    ///
287    /// ```text
288    /// cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=<YYYY-MM-DD>
289    /// ```
290    ///
291    /// The value is determined with the following command
292    /// ```text
293    #[doc = concat!(commit_date!())]
294    /// ```
295    #[builder(default = all)]
296    commit_date: bool,
297    /// Emit the commit timestamp of the latest commit
298    ///
299    /// ```text
300    /// cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=<YYYY-MM-DDThh:mm:ssZ>
301    /// ```
302    ///
303    #[builder(default = all)]
304    commit_timestamp: bool,
305    /// Emit the describe output
306    ///
307    /// ```text
308    /// cargo:rustc-env=VERGEN_GIT_DESCRIBE=<DESCRIBE>
309    /// ```
310    ///
311    /// Optionally, add the `dirty` or `tags` flag to describe.
312    /// See [`git describe`](https://git-scm.com/docs/git-describe#_options) for more details
313    ///
314    /// ## `tags`
315    /// Instead of using only the annotated tags, use any tag found in refs/tags namespace.
316    ///
317    /// ## `dirty`
318    /// If the working tree has local modification "-dirty" is appended to it.
319    ///
320    /// ## `match_pattern`
321    /// Only consider tags matching the given glob pattern, excluding the "refs/tags/" prefix.
322    #[builder(
323        required,
324        default = all.then(|| Describe::builder().build()),
325        with = |tags: bool, dirty: bool, match_pattern: Option<&'static str>| {
326            Some(Describe::builder().tags(tags).dirty(dirty).maybe_match_pattern(match_pattern).build())
327        }
328    )]
329    describe: Option<Describe>,
330    /// Emit the SHA of the latest commit
331    ///
332    /// ```text
333    /// cargo:rustc-env=VERGEN_GIT_SHA=<SHA>
334    /// ```
335    ///
336    /// Optionally, add the `short` flag to rev-parse.
337    /// See [`git rev-parse`](https://git-scm.com/docs/git-rev-parse#_options_for_output) for more details.
338    ///
339    /// ## `short`
340    /// Shortens the object name to a unique prefix
341    #[builder(
342        required,
343        default = all.then(|| Sha::builder().build()),
344        with = |short: bool| Some(Sha::builder().short(short).build())
345    )]
346    sha: Option<Sha>,
347    /// Emit the dirty state of the git repository
348    /// ```text
349    /// cargo:rustc-env=VERGEN_GIT_DIRTY=(true|false)
350    /// ```
351    ///
352    /// Optionally, include untracked files when determining the dirty status of the repository.
353    ///
354    /// # `include_tracked`
355    /// Should we include/ignore untracked files in deciding whether the repository is dirty.
356    #[builder(
357        required,
358        default = all.then(|| Dirty::builder().build()),
359        with = |include_untracked: bool| Some(Dirty::builder().include_untracked(include_untracked).build())
360    )]
361    dirty: Option<Dirty>,
362    /// Enable local offset date/timestamp output
363    #[builder(default = false)]
364    use_local: bool,
365    /// Specify the git cmd you wish to use, i.e. `/usr/bin/git`
366    git_cmd: Option<&'static str>,
367}
368
369impl<S: gitcl_builder::State> GitclBuilder<S> {
370    /// Convenience method that switches the defaults of [`GitclBuilder`]
371    /// to enable all of the `VERGEN_GIT_*` instructions. It can only be
372    /// called at the start of the building process, i.e. when no config
373    /// has been set yet to avoid overwrites.
374    fn all(mut self) -> Self {
375        self.all = true;
376        self
377    }
378}
379
380impl Gitcl {
381    /// Emit all of the `VERGEN_GIT_*` instructions
382    #[must_use]
383    pub fn all_git() -> Gitcl {
384        Self::builder().all().build()
385    }
386
387    /// Convenience method to setup the [`Gitcl`] builder with all of the `VERGEN_GIT_*` instructions on
388    pub fn all() -> GitclBuilder<Empty> {
389        Self::builder().all()
390    }
391
392    fn any(&self) -> bool {
393        self.branch
394            || self.commit_author_email
395            || self.commit_author_name
396            || self.commit_count
397            || self.commit_date
398            || self.commit_message
399            || self.commit_timestamp
400            || self.describe.is_some()
401            || self.sha.is_some()
402            || self.dirty.is_some()
403    }
404
405    /// Run at the given path
406    pub fn at_path(&mut self, path: PathBuf) -> &mut Self {
407        self.repo_path = Some(path);
408        self
409    }
410
411    // #[cfg(test)]
412    // pub(crate) fn fail(&mut self) -> &mut Self {
413    //     self.fail = true;
414    //     self
415    // }
416
417    /// Set the command used to test if git exists on the path.
418    /// Defaults to `git --version` if not set explicitly.
419    pub fn git_cmd(&mut self, cmd: Option<&'static str>) -> &mut Self {
420        self.git_cmd = cmd;
421        self
422    }
423
424    fn check_git(cmd: &str) -> Result<()> {
425        if Self::git_cmd_exists(cmd) {
426            Ok(())
427        } else {
428            Err(anyhow!("no suitable 'git' command found!"))
429        }
430    }
431
432    fn check_inside_git_worktree(path: Option<&PathBuf>) -> Result<()> {
433        if Self::inside_git_worktree(path) {
434            Ok(())
435        } else {
436            Err(anyhow!("not within a suitable 'git' worktree!"))
437        }
438    }
439
440    fn git_cmd_exists(cmd: &str) -> bool {
441        Self::run_cmd(cmd, None)
442            .map(|output| output.status.success())
443            .unwrap_or(false)
444    }
445
446    fn inside_git_worktree(path: Option<&PathBuf>) -> bool {
447        Self::run_cmd("git rev-parse --is-inside-work-tree", path)
448            .map(|output| {
449                let stdout = String::from_utf8_lossy(&output.stdout);
450                output.status.success() && stdout.contains("true")
451            })
452            .unwrap_or(false)
453    }
454
455    #[cfg(not(target_env = "msvc"))]
456    fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
457        let shell = if let Some(shell_path) = env::var_os("SHELL") {
458            shell_path.to_string_lossy().into_owned()
459        } else {
460            // Fallback to sh if SHELL not defined
461            "sh".to_string()
462        };
463        let mut cmd = Command::new(shell);
464        if let Some(path) = path_opt {
465            _ = cmd.current_dir(path);
466        }
467        // https://git-scm.com/docs/git-status#_background_refresh
468        _ = cmd.env("GIT_OPTIONAL_LOCKS", "0");
469        _ = cmd.arg("-c");
470        _ = cmd.arg(command);
471        _ = cmd.stdout(Stdio::piped());
472        _ = cmd.stderr(Stdio::piped());
473
474        let output = cmd.output()?;
475        if !output.status.success() {
476            eprintln!("Command failed: `{command}`");
477            eprintln!("--- stdout:\n{}\n", String::from_utf8_lossy(&output.stdout));
478            eprintln!("--- stderr:\n{}\n", String::from_utf8_lossy(&output.stderr));
479        }
480
481        Ok(output)
482    }
483
484    #[cfg(target_env = "msvc")]
485    fn run_cmd(command: &str, path_opt: Option<&PathBuf>) -> Result<Output> {
486        let mut cmd = Command::new("cmd");
487        if let Some(path) = path_opt {
488            _ = cmd.current_dir(path);
489        }
490        // https://git-scm.com/docs/git-status#_background_refresh
491        _ = cmd.env("GIT_OPTIONAL_LOCKS", "0");
492        _ = cmd.arg("/c");
493        _ = cmd.arg(command);
494        _ = cmd.stdout(Stdio::piped());
495        _ = cmd.stderr(Stdio::piped());
496
497        let output = cmd.output()?;
498        if !output.status.success() {
499            eprintln!("Command failed: `{command}`");
500            eprintln!("--- stdout:\n{}\n", String::from_utf8_lossy(&output.stdout));
501            eprintln!("--- stderr:\n{}\n", String::from_utf8_lossy(&output.stderr));
502        }
503
504        Ok(output)
505    }
506
507    fn run_cmd_checked(command: &str, path_opt: Option<&PathBuf>) -> Result<Vec<u8>> {
508        let output = Self::run_cmd(command, path_opt)?;
509        if output.status.success() {
510            Ok(output.stdout)
511        } else {
512            let stderr = String::from_utf8_lossy(&output.stderr);
513            Err(anyhow!("Failed to run '{command}'!  {stderr}"))
514        }
515    }
516
517    #[allow(clippy::too_many_lines)]
518    fn inner_add_git_map_entries(
519        &self,
520        idempotent: bool,
521        cargo_rustc_env: &mut CargoRustcEnvMap,
522        cargo_rerun_if_changed: &mut CargoRerunIfChanged,
523        cargo_warning: &mut CargoWarning,
524    ) -> Result<()> {
525        if !idempotent && self.any() {
526            Self::add_rerun_if_changed(cargo_rerun_if_changed, self.repo_path.as_ref())?;
527        }
528
529        if self.branch {
530            if let Ok(_value) = env::var(GIT_BRANCH_NAME) {
531                add_default_map_entry(
532                    idempotent,
533                    VergenKey::GitBranch,
534                    cargo_rustc_env,
535                    cargo_warning,
536                );
537            } else {
538                Self::add_git_cmd_entry(
539                    BRANCH_CMD,
540                    self.repo_path.as_ref(),
541                    VergenKey::GitBranch,
542                    cargo_rustc_env,
543                )?;
544            }
545        }
546
547        if self.commit_author_email {
548            if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_EMAIL) {
549                add_default_map_entry(
550                    idempotent,
551                    VergenKey::GitCommitAuthorEmail,
552                    cargo_rustc_env,
553                    cargo_warning,
554                );
555            } else {
556                Self::add_git_cmd_entry(
557                    COMMIT_AUTHOR_EMAIL,
558                    self.repo_path.as_ref(),
559                    VergenKey::GitCommitAuthorEmail,
560                    cargo_rustc_env,
561                )?;
562            }
563        }
564
565        if self.commit_author_name {
566            if let Ok(_value) = env::var(GIT_COMMIT_AUTHOR_NAME) {
567                add_default_map_entry(
568                    idempotent,
569                    VergenKey::GitCommitAuthorName,
570                    cargo_rustc_env,
571                    cargo_warning,
572                );
573            } else {
574                Self::add_git_cmd_entry(
575                    COMMIT_AUTHOR_NAME,
576                    self.repo_path.as_ref(),
577                    VergenKey::GitCommitAuthorName,
578                    cargo_rustc_env,
579                )?;
580            }
581        }
582
583        if self.commit_count {
584            if let Ok(_value) = env::var(GIT_COMMIT_COUNT) {
585                add_default_map_entry(
586                    idempotent,
587                    VergenKey::GitCommitCount,
588                    cargo_rustc_env,
589                    cargo_warning,
590                );
591            } else {
592                Self::add_git_cmd_entry(
593                    COMMIT_COUNT,
594                    self.repo_path.as_ref(),
595                    VergenKey::GitCommitCount,
596                    cargo_rustc_env,
597                )?;
598            }
599        }
600
601        self.add_git_timestamp_entries(
602            COMMIT_TIMESTAMP,
603            self.repo_path.as_ref(),
604            idempotent,
605            cargo_rustc_env,
606            cargo_warning,
607        )?;
608
609        if self.commit_message {
610            if let Ok(_value) = env::var(GIT_COMMIT_MESSAGE) {
611                add_default_map_entry(
612                    idempotent,
613                    VergenKey::GitCommitMessage,
614                    cargo_rustc_env,
615                    cargo_warning,
616                );
617            } else {
618                Self::add_git_cmd_entry(
619                    COMMIT_MESSAGE,
620                    self.repo_path.as_ref(),
621                    VergenKey::GitCommitMessage,
622                    cargo_rustc_env,
623                )?;
624            }
625        }
626
627        let mut dirty_cache = None; // attempt to re-use dirty status later if possible
628        if let Some(dirty) = self.dirty {
629            if let Ok(_value) = env::var(GIT_DIRTY_NAME) {
630                add_default_map_entry(
631                    idempotent,
632                    VergenKey::GitDirty,
633                    cargo_rustc_env,
634                    cargo_warning,
635                );
636            } else {
637                let use_dirty = self.compute_dirty(dirty.include_untracked())?;
638                if !dirty.include_untracked() {
639                    dirty_cache = Some(use_dirty);
640                }
641                add_map_entry(
642                    VergenKey::GitDirty,
643                    bool::to_string(&use_dirty),
644                    cargo_rustc_env,
645                );
646            }
647        }
648
649        if let Some(describe) = self.describe {
650            // `git describe --dirty` does not support `GIT_OPTIONAL_LOCKS=0`
651            // (see https://github.com/gitgitgadget/git/pull/1872)
652            //
653            // Instead, always compute the dirty status with `git status`
654            if let Ok(_value) = env::var(GIT_DESCRIBE_NAME) {
655                add_default_map_entry(
656                    idempotent,
657                    VergenKey::GitDescribe,
658                    cargo_rustc_env,
659                    cargo_warning,
660                );
661            } else {
662                let mut describe_cmd = String::from(DESCRIBE);
663                if describe.tags() {
664                    describe_cmd.push_str(" --tags");
665                }
666                if let Some(pattern) = *describe.match_pattern() {
667                    Self::match_pattern_cmd_str(&mut describe_cmd, pattern);
668                }
669                let stdout = Self::run_cmd_checked(&describe_cmd, self.repo_path.as_ref())?;
670                let mut describe_value = String::from_utf8_lossy(&stdout).trim().to_string();
671                if describe.dirty()
672                    && (dirty_cache.is_some_and(|dirty| dirty) || self.compute_dirty(false)?)
673                {
674                    describe_value.push_str("-dirty");
675                }
676                add_map_entry(VergenKey::GitDescribe, describe_value, cargo_rustc_env);
677            }
678        }
679
680        if let Some(sha) = self.sha {
681            if let Ok(_value) = env::var(GIT_SHA_NAME) {
682                add_default_map_entry(
683                    idempotent,
684                    VergenKey::GitSha,
685                    cargo_rustc_env,
686                    cargo_warning,
687                );
688            } else {
689                let mut sha_cmd = String::from(SHA);
690                if sha.short() {
691                    sha_cmd.push_str(" --short");
692                }
693                sha_cmd.push_str(" HEAD");
694                Self::add_git_cmd_entry(
695                    &sha_cmd,
696                    self.repo_path.as_ref(),
697                    VergenKey::GitSha,
698                    cargo_rustc_env,
699                )?;
700            }
701        }
702
703        Ok(())
704    }
705
706    fn add_rerun_if_changed(
707        rerun_if_changed: &mut Vec<String>,
708        path: Option<&PathBuf>,
709    ) -> Result<()> {
710        let git_path = Self::run_cmd("git rev-parse --git-dir", path)?;
711        if git_path.status.success() {
712            let git_path_str = String::from_utf8_lossy(&git_path.stdout).trim().to_string();
713            let git_path = PathBuf::from(&git_path_str);
714
715            // Setup the head path
716            let mut head_path = git_path.clone();
717            head_path.push("HEAD");
718
719            if head_path.exists() {
720                rerun_if_changed.push(format!("{}", head_path.display()));
721            }
722
723            // Setup the ref path
724            let refp = Self::setup_ref_path(path)?;
725            if refp.status.success() {
726                let ref_path_str = String::from_utf8_lossy(&refp.stdout).trim().to_string();
727                let mut ref_path = git_path;
728                ref_path.push(ref_path_str);
729                if ref_path.exists() {
730                    rerun_if_changed.push(format!("{}", ref_path.display()));
731                }
732            }
733        }
734        Ok(())
735    }
736
737    #[cfg(not(target_os = "windows"))]
738    fn match_pattern_cmd_str(describe_cmd: &mut String, pattern: &str) {
739        describe_cmd.push_str(" --match \"");
740        describe_cmd.push_str(pattern);
741        describe_cmd.push('\"');
742    }
743
744    #[cfg(target_os = "windows")]
745    fn match_pattern_cmd_str(describe_cmd: &mut String, pattern: &str) {
746        describe_cmd.push_str(" --match ");
747        describe_cmd.push_str(pattern);
748    }
749
750    #[cfg(not(test))]
751    fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
752        Self::run_cmd("git symbolic-ref HEAD", path)
753    }
754
755    #[cfg(all(test, not(target_os = "windows")))]
756    fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
757        Self::run_cmd("pwd", path)
758    }
759
760    #[cfg(all(test, target_os = "windows"))]
761    fn setup_ref_path(path: Option<&PathBuf>) -> Result<Output> {
762        Self::run_cmd("cd", path)
763    }
764
765    fn add_git_cmd_entry(
766        cmd: &str,
767        path: Option<&PathBuf>,
768        key: VergenKey,
769        cargo_rustc_env: &mut CargoRustcEnvMap,
770    ) -> Result<()> {
771        let stdout = Self::run_cmd_checked(cmd, path)?;
772        let stdout = String::from_utf8_lossy(&stdout)
773            .trim()
774            .trim_matches('\'')
775            .to_string();
776        add_map_entry(key, stdout, cargo_rustc_env);
777        Ok(())
778    }
779
780    fn add_git_timestamp_entries(
781        &self,
782        cmd: &str,
783        path: Option<&PathBuf>,
784        idempotent: bool,
785        cargo_rustc_env: &mut CargoRustcEnvMap,
786        cargo_warning: &mut CargoWarning,
787    ) -> Result<()> {
788        let mut date_override = false;
789        if let Ok(_value) = env::var(GIT_COMMIT_DATE_NAME) {
790            add_default_map_entry(
791                idempotent,
792                VergenKey::GitCommitDate,
793                cargo_rustc_env,
794                cargo_warning,
795            );
796            date_override = true;
797        }
798
799        let mut timestamp_override = false;
800        if let Ok(_value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
801            add_default_map_entry(
802                idempotent,
803                VergenKey::GitCommitTimestamp,
804                cargo_rustc_env,
805                cargo_warning,
806            );
807            timestamp_override = true;
808        }
809
810        let output = Self::run_cmd(cmd, path)?;
811        if output.status.success() {
812            let stdout = String::from_utf8_lossy(&output.stdout)
813                .lines()
814                .last()
815                .ok_or_else(|| anyhow!("invalid 'git log' output"))?
816                .trim()
817                .trim_matches('\'')
818                .to_string();
819
820            let (sde, ts) = match env::var("SOURCE_DATE_EPOCH") {
821                Ok(v) => (
822                    true,
823                    OffsetDateTime::from_unix_timestamp(i64::from_str(&v)?)?,
824                ),
825                Err(VarError::NotPresent) => self.compute_local_offset(&stdout)?,
826                Err(e) => return Err(e.into()),
827            };
828
829            if idempotent && !sde {
830                if self.commit_date && !date_override {
831                    add_default_map_entry(
832                        idempotent,
833                        VergenKey::GitCommitDate,
834                        cargo_rustc_env,
835                        cargo_warning,
836                    );
837                }
838
839                if self.commit_timestamp && !timestamp_override {
840                    add_default_map_entry(
841                        idempotent,
842                        VergenKey::GitCommitTimestamp,
843                        cargo_rustc_env,
844                        cargo_warning,
845                    );
846                }
847            } else {
848                if self.commit_date && !date_override {
849                    let format = format_description::parse("[year]-[month]-[day]")?;
850                    add_map_entry(
851                        VergenKey::GitCommitDate,
852                        ts.format(&format)?,
853                        cargo_rustc_env,
854                    );
855                }
856
857                if self.commit_timestamp && !timestamp_override {
858                    add_map_entry(
859                        VergenKey::GitCommitTimestamp,
860                        ts.format(&Iso8601::DEFAULT)?,
861                        cargo_rustc_env,
862                    );
863                }
864            }
865        } else {
866            if self.commit_date && !date_override {
867                add_default_map_entry(
868                    idempotent,
869                    VergenKey::GitCommitDate,
870                    cargo_rustc_env,
871                    cargo_warning,
872                );
873            }
874
875            if self.commit_timestamp && !timestamp_override {
876                add_default_map_entry(
877                    idempotent,
878                    VergenKey::GitCommitTimestamp,
879                    cargo_rustc_env,
880                    cargo_warning,
881                );
882            }
883        }
884
885        Ok(())
886    }
887
888    #[cfg_attr(coverage_nightly, coverage(off))]
889    // this in not included in coverage, because on *nix the local offset is always unsafe
890    fn compute_local_offset(&self, stdout: &str) -> Result<(bool, OffsetDateTime)> {
891        let no_offset = OffsetDateTime::parse(stdout, &Rfc3339)?;
892        if self.use_local {
893            let local = UtcOffset::local_offset_at(no_offset)?;
894            let local_offset = no_offset.checked_to_offset(local).unwrap_or(no_offset);
895            Ok((false, local_offset))
896        } else {
897            Ok((false, no_offset))
898        }
899    }
900
901    fn compute_dirty(&self, include_untracked: bool) -> Result<bool> {
902        let mut dirty_cmd = String::from(DIRTY);
903        if !include_untracked {
904            dirty_cmd.push_str(" --untracked-files=no");
905        }
906        let stdout = Self::run_cmd_checked(&dirty_cmd, self.repo_path.as_ref())?;
907        Ok(!stdout.is_empty())
908    }
909}
910
911impl AddEntries for Gitcl {
912    fn add_map_entries(
913        &self,
914        idempotent: bool,
915        cargo_rustc_env: &mut CargoRustcEnvMap,
916        cargo_rerun_if_changed: &mut CargoRerunIfChanged,
917        cargo_warning: &mut CargoWarning,
918    ) -> Result<()> {
919        if self.any() {
920            let git_cmd = self.git_cmd.unwrap_or("git --version");
921            Self::check_git(git_cmd)
922                .and_then(|()| Self::check_inside_git_worktree(self.repo_path.as_ref()))?;
923            self.inner_add_git_map_entries(
924                idempotent,
925                cargo_rustc_env,
926                cargo_rerun_if_changed,
927                cargo_warning,
928            )?;
929        }
930        Ok(())
931    }
932
933    fn add_default_entries(
934        &self,
935        config: &DefaultConfig,
936        cargo_rustc_env_map: &mut CargoRustcEnvMap,
937        cargo_rerun_if_changed: &mut CargoRerunIfChanged,
938        cargo_warning: &mut CargoWarning,
939    ) -> Result<()> {
940        if *config.fail_on_error() {
941            let error = Error::msg(format!("{}", config.error()));
942            Err(error)
943        } else {
944            // Clear any previous data.  We are re-populating
945            // map isn't cleared because keys will overwrite.
946            cargo_warning.clear();
947            cargo_rerun_if_changed.clear();
948
949            cargo_warning.push(format!("{}", config.error()));
950
951            if self.branch {
952                add_default_map_entry(
953                    *config.idempotent(),
954                    VergenKey::GitBranch,
955                    cargo_rustc_env_map,
956                    cargo_warning,
957                );
958            }
959            if self.commit_author_email {
960                add_default_map_entry(
961                    *config.idempotent(),
962                    VergenKey::GitCommitAuthorEmail,
963                    cargo_rustc_env_map,
964                    cargo_warning,
965                );
966            }
967            if self.commit_author_name {
968                add_default_map_entry(
969                    *config.idempotent(),
970                    VergenKey::GitCommitAuthorName,
971                    cargo_rustc_env_map,
972                    cargo_warning,
973                );
974            }
975            if self.commit_count {
976                add_default_map_entry(
977                    *config.idempotent(),
978                    VergenKey::GitCommitCount,
979                    cargo_rustc_env_map,
980                    cargo_warning,
981                );
982            }
983            if self.commit_date {
984                add_default_map_entry(
985                    *config.idempotent(),
986                    VergenKey::GitCommitDate,
987                    cargo_rustc_env_map,
988                    cargo_warning,
989                );
990            }
991            if self.commit_message {
992                add_default_map_entry(
993                    *config.idempotent(),
994                    VergenKey::GitCommitMessage,
995                    cargo_rustc_env_map,
996                    cargo_warning,
997                );
998            }
999            if self.commit_timestamp {
1000                add_default_map_entry(
1001                    *config.idempotent(),
1002                    VergenKey::GitCommitTimestamp,
1003                    cargo_rustc_env_map,
1004                    cargo_warning,
1005                );
1006            }
1007            if self.describe.is_some() {
1008                add_default_map_entry(
1009                    *config.idempotent(),
1010                    VergenKey::GitDescribe,
1011                    cargo_rustc_env_map,
1012                    cargo_warning,
1013                );
1014            }
1015            if self.sha.is_some() {
1016                add_default_map_entry(
1017                    *config.idempotent(),
1018                    VergenKey::GitSha,
1019                    cargo_rustc_env_map,
1020                    cargo_warning,
1021                );
1022            }
1023            if self.dirty.is_some() {
1024                add_default_map_entry(
1025                    *config.idempotent(),
1026                    VergenKey::GitDirty,
1027                    cargo_rustc_env_map,
1028                    cargo_warning,
1029                );
1030            }
1031            Ok(())
1032        }
1033    }
1034}
1035
1036#[cfg(test)]
1037mod test {
1038    use super::Gitcl;
1039    use crate::Emitter;
1040    use anyhow::Result;
1041    use serial_test::serial;
1042    #[cfg(unix)]
1043    use std::io::stdout;
1044    use std::{collections::BTreeMap, env::temp_dir, io::Write};
1045    #[cfg(unix)]
1046    use test_util::TEST_MTIME;
1047    use test_util::TestRepos;
1048    use vergen_lib::{VergenKey, count_idempotent};
1049
1050    #[test]
1051    #[serial]
1052    #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
1053    fn gitcl_clone_works() {
1054        let gitcl = Gitcl::all_git();
1055        let another = gitcl.clone();
1056        assert_eq!(another, gitcl);
1057    }
1058
1059    #[test]
1060    #[serial]
1061    fn gitcl_debug_works() -> Result<()> {
1062        let gitcl = Gitcl::all_git();
1063        let mut buf = vec![];
1064        write!(buf, "{gitcl:?}")?;
1065        assert!(!buf.is_empty());
1066        Ok(())
1067    }
1068
1069    #[test]
1070    #[serial]
1071    fn gix_default() -> Result<()> {
1072        let gitcl = Gitcl::builder().build();
1073        let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1074        assert_eq!(0, emitter.cargo_rustc_env_map().len());
1075        assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1076        assert_eq!(0, emitter.cargo_warning().len());
1077        Ok(())
1078    }
1079
1080    #[test]
1081    #[serial]
1082    fn bad_command_is_error() -> Result<()> {
1083        let mut map = BTreeMap::new();
1084        assert!(
1085            Gitcl::add_git_cmd_entry(
1086                "such_a_terrible_cmd",
1087                None,
1088                VergenKey::GitCommitMessage,
1089                &mut map
1090            )
1091            .is_err()
1092        );
1093        Ok(())
1094    }
1095
1096    #[test]
1097    #[serial]
1098    fn non_working_tree_is_error() -> Result<()> {
1099        assert!(Gitcl::check_inside_git_worktree(Some(&temp_dir())).is_err());
1100        Ok(())
1101    }
1102
1103    #[test]
1104    #[serial]
1105    fn invalid_git_is_error() -> Result<()> {
1106        assert!(Gitcl::check_git("such_a_terrible_cmd -v").is_err());
1107        Ok(())
1108    }
1109
1110    #[cfg(not(target_family = "windows"))]
1111    #[test]
1112    #[serial]
1113    fn shell_env_works() -> Result<()> {
1114        temp_env::with_var("SHELL", Some("bash"), || {
1115            let mut map = BTreeMap::new();
1116            assert!(
1117                Gitcl::add_git_cmd_entry("git -v", None, VergenKey::GitCommitMessage, &mut map)
1118                    .is_ok()
1119            );
1120        });
1121        Ok(())
1122    }
1123
1124    #[test]
1125    #[serial]
1126    fn git_all_idempotent() -> Result<()> {
1127        let gitcl = Gitcl::all_git();
1128        let emitter = Emitter::default()
1129            .idempotent()
1130            .add_instructions(&gitcl)?
1131            .test_emit();
1132        assert_eq!(10, emitter.cargo_rustc_env_map().len());
1133        assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1134        assert_eq!(2, emitter.cargo_warning().len());
1135        Ok(())
1136    }
1137
1138    #[test]
1139    #[serial]
1140    fn git_all_idempotent_no_warn() -> Result<()> {
1141        let gitcl = Gitcl::all_git();
1142        let emitter = Emitter::default()
1143            .idempotent()
1144            .quiet()
1145            .add_instructions(&gitcl)?
1146            .test_emit();
1147        assert_eq!(10, emitter.cargo_rustc_env_map().len());
1148        assert_eq!(2, count_idempotent(emitter.cargo_rustc_env_map()));
1149        assert_eq!(2, emitter.cargo_warning().len());
1150        Ok(())
1151    }
1152
1153    #[test]
1154    #[serial]
1155    fn git_all_at_path() -> Result<()> {
1156        let repo = TestRepos::new(false, false, false)?;
1157        let mut gitcl = Gitcl::all_git();
1158        let _ = gitcl.at_path(repo.path());
1159        let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1160        assert_eq!(10, emitter.cargo_rustc_env_map().len());
1161        assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1162        assert_eq!(0, emitter.cargo_warning().len());
1163        Ok(())
1164    }
1165
1166    #[test]
1167    #[serial]
1168    fn git_all() -> Result<()> {
1169        let gitcl = Gitcl::all_git();
1170        let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1171        assert_eq!(10, emitter.cargo_rustc_env_map().len());
1172        assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1173        assert_eq!(0, emitter.cargo_warning().len());
1174        Ok(())
1175    }
1176
1177    #[test]
1178    #[serial]
1179    fn git_all_shallow_clone() -> Result<()> {
1180        let repo = TestRepos::new(false, false, true)?;
1181        let mut gitcl = Gitcl::all_git();
1182        let _ = gitcl.at_path(repo.path());
1183        let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1184        assert_eq!(10, emitter.cargo_rustc_env_map().len());
1185        assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1186        assert_eq!(0, emitter.cargo_warning().len());
1187        Ok(())
1188    }
1189
1190    #[test]
1191    #[serial]
1192    fn git_all_dirty_tags_short() -> Result<()> {
1193        let gitcl = Gitcl::builder()
1194            .all()
1195            .describe(true, true, None)
1196            .sha(true)
1197            .build();
1198        let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1199        assert_eq!(10, emitter.cargo_rustc_env_map().len());
1200        assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1201        assert_eq!(0, emitter.cargo_warning().len());
1202        Ok(())
1203    }
1204
1205    #[test]
1206    #[serial]
1207    fn fails_on_bad_git_command() -> Result<()> {
1208        let mut gitcl = Gitcl::all_git();
1209        let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1210        assert!(
1211            Emitter::default()
1212                .fail_on_error()
1213                .add_instructions(&gitcl)
1214                .is_err()
1215        );
1216        Ok(())
1217    }
1218
1219    #[test]
1220    #[serial]
1221    fn defaults_on_bad_git_command() -> Result<()> {
1222        let mut gitcl = Gitcl::all_git();
1223        let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1224        let emitter = Emitter::default().add_instructions(&gitcl)?.test_emit();
1225        assert_eq!(0, emitter.cargo_rustc_env_map().len());
1226        assert_eq!(0, count_idempotent(emitter.cargo_rustc_env_map()));
1227        assert_eq!(11, emitter.cargo_warning().len());
1228        Ok(())
1229    }
1230
1231    #[test]
1232    #[serial]
1233    fn idempotent_on_bad_git_command() -> Result<()> {
1234        let mut gitcl = Gitcl::all_git();
1235        let _ = gitcl.git_cmd(Some("this_is_not_a_git_cmd"));
1236        let emitter = Emitter::default()
1237            .idempotent()
1238            .add_instructions(&gitcl)?
1239            .test_emit();
1240        assert_eq!(10, emitter.cargo_rustc_env_map().len());
1241        assert_eq!(10, count_idempotent(emitter.cargo_rustc_env_map()));
1242        assert_eq!(11, emitter.cargo_warning().len());
1243        Ok(())
1244    }
1245
1246    #[test]
1247    #[serial]
1248    fn bad_timestamp_defaults() -> Result<()> {
1249        let mut map = BTreeMap::new();
1250        let mut warnings = vec![];
1251        let gitcl = Gitcl::all_git();
1252        assert!(
1253            gitcl
1254                .add_git_timestamp_entries(
1255                    "this_is_not_a_git_cmd",
1256                    None,
1257                    false,
1258                    &mut map,
1259                    &mut warnings
1260                )
1261                .is_ok()
1262        );
1263        assert_eq!(0, map.len());
1264        assert_eq!(2, warnings.len());
1265        Ok(())
1266    }
1267
1268    #[test]
1269    #[serial]
1270    fn bad_timestamp_idempotent() -> Result<()> {
1271        let mut map = BTreeMap::new();
1272        let mut warnings = vec![];
1273        let gitcl = Gitcl::all_git();
1274        assert!(
1275            gitcl
1276                .add_git_timestamp_entries(
1277                    "this_is_not_a_git_cmd",
1278                    None,
1279                    true,
1280                    &mut map,
1281                    &mut warnings
1282                )
1283                .is_ok()
1284        );
1285        assert_eq!(2, map.len());
1286        assert_eq!(2, warnings.len());
1287        Ok(())
1288    }
1289
1290    #[test]
1291    #[serial]
1292    fn source_date_epoch_works() {
1293        temp_env::with_var("SOURCE_DATE_EPOCH", Some("1671809360"), || {
1294            let result = || -> Result<()> {
1295                let mut stdout_buf = vec![];
1296                let gitcl = Gitcl::builder()
1297                    .commit_date(true)
1298                    .commit_timestamp(true)
1299                    .build();
1300                _ = Emitter::new()
1301                    .idempotent()
1302                    .add_instructions(&gitcl)?
1303                    .emit_to(&mut stdout_buf)?;
1304                let output = String::from_utf8_lossy(&stdout_buf);
1305                for (idx, line) in output.lines().enumerate() {
1306                    if idx == 0 {
1307                        assert_eq!("cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=2022-12-23", line);
1308                    } else if idx == 1 {
1309                        assert_eq!(
1310                            "cargo:rustc-env=VERGEN_GIT_COMMIT_TIMESTAMP=2022-12-23T15:29:20.000000000Z",
1311                            line
1312                        );
1313                    }
1314                }
1315                Ok(())
1316            }();
1317            assert!(result.is_ok());
1318        });
1319    }
1320
1321    #[test]
1322    #[serial]
1323    #[cfg(unix)]
1324    fn bad_source_date_epoch_fails() {
1325        use std::ffi::OsStr;
1326        use std::os::unix::prelude::OsStrExt;
1327
1328        let source = [0x66, 0x6f, 0x80, 0x6f];
1329        let os_str = OsStr::from_bytes(&source[..]);
1330        temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1331            let result = || -> Result<bool> {
1332                let mut stdout_buf = vec![];
1333                let gitcl = Gitcl::builder().commit_date(true).build();
1334                Emitter::new()
1335                    .idempotent()
1336                    .fail_on_error()
1337                    .add_instructions(&gitcl)?
1338                    .emit_to(&mut stdout_buf)
1339            }();
1340            assert!(result.is_err());
1341        });
1342    }
1343
1344    #[test]
1345    #[serial]
1346    #[cfg(unix)]
1347    fn bad_source_date_epoch_defaults() {
1348        use std::ffi::OsStr;
1349        use std::os::unix::prelude::OsStrExt;
1350
1351        let source = [0x66, 0x6f, 0x80, 0x6f];
1352        let os_str = OsStr::from_bytes(&source[..]);
1353        temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1354            let result = || -> Result<bool> {
1355                let mut stdout_buf = vec![];
1356                let gitcl = Gitcl::builder().commit_date(true).build();
1357                Emitter::new()
1358                    .idempotent()
1359                    .add_instructions(&gitcl)?
1360                    .emit_to(&mut stdout_buf)
1361            }();
1362            assert!(result.is_ok());
1363        });
1364    }
1365
1366    #[test]
1367    #[serial]
1368    #[cfg(windows)]
1369    fn bad_source_date_epoch_fails() {
1370        use std::ffi::OsString;
1371        use std::os::windows::prelude::OsStringExt;
1372
1373        let source = [0x0066, 0x006f, 0xD800, 0x006f];
1374        let os_string = OsString::from_wide(&source[..]);
1375        let os_str = os_string.as_os_str();
1376        temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1377            let result = || -> Result<bool> {
1378                let mut stdout_buf = vec![];
1379                let gitcl = Gitcl::builder().commit_date(true).build();
1380                Emitter::new()
1381                    .fail_on_error()
1382                    .idempotent()
1383                    .add_instructions(&gitcl)?
1384                    .emit_to(&mut stdout_buf)
1385            }();
1386            assert!(result.is_err());
1387        });
1388    }
1389
1390    #[test]
1391    #[serial]
1392    #[cfg(windows)]
1393    fn bad_source_date_epoch_defaults() {
1394        use std::ffi::OsString;
1395        use std::os::windows::prelude::OsStringExt;
1396
1397        let source = [0x0066, 0x006f, 0xD800, 0x006f];
1398        let os_string = OsString::from_wide(&source[..]);
1399        let os_str = os_string.as_os_str();
1400        temp_env::with_var("SOURCE_DATE_EPOCH", Some(os_str), || {
1401            let result = || -> Result<bool> {
1402                let mut stdout_buf = vec![];
1403                let gitcl = Gitcl::builder().commit_date(true).build();
1404                Emitter::new()
1405                    .idempotent()
1406                    .add_instructions(&gitcl)?
1407                    .emit_to(&mut stdout_buf)
1408            }();
1409            assert!(result.is_ok());
1410        });
1411    }
1412
1413    #[test]
1414    #[serial]
1415    #[cfg(unix)]
1416    fn git_no_index_update() -> Result<()> {
1417        let repo = TestRepos::new(true, true, false)?;
1418        repo.set_index_magic_mtime()?;
1419
1420        // The GIT_OPTIONAL_LOCKS=0 environment variable should prevent modifications to the index
1421        let mut gitcl = Gitcl::builder().all().describe(true, true, None).build();
1422        let _ = gitcl.at_path(repo.path());
1423        let failed = Emitter::default()
1424            .add_instructions(&gitcl)?
1425            .emit_to(&mut stdout())?;
1426        assert!(!failed);
1427
1428        assert_eq!(*TEST_MTIME, repo.get_index_magic_mtime()?);
1429        Ok(())
1430    }
1431}