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