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