vergen_gitcl/gitcl/
mod.rs

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