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