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