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