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