Skip to main content

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