1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! Embed the git revision of a crate in its build.
//!
//! Supports embedding the version from a local or remote git repository the build
//! is occurring in, as well as when `cargo install` or depending on a crate
//! published to crates.io.
//!
//! It extracts the git revision in two ways:
//! - From the `.cargo_vcs_info.json` file embedded in published crates.
//! - From the git repository the build is occurring from in unpublished crates.
//!
//! Injects an environment variable `GIT_REVISION` into the build that contains
//! the full git revision, with a `-dirty` suffix if the working directory is
//! dirty.
//!
//! Requires the use of a build.rs build script. See [Build Scripts]() for more
//! details on how Rust build scripts work.
//!
//! [Build Scripts]: https://doc.rust-lang.org/cargo/reference/build-scripts.html
//!
//! ### Examples
//!
//! Add the following to the crate's `Cargo.toml` file:
//!
//! ```toml
//! [build_dependencies]
//! crate-git-revision = "0.0.2"
//! ```
//!
//! Add the following to the crate's `build.rs` file:
//!
//! ```rust
//! crate_git_revision::init();
//! ```
//!
//! Add the following to the crate's `lib.rs` or `main.rs` file:
//!
//! ```ignore
//! pub const GIT_REVISION: &str = env!("GIT_REVISION");
//! ```

use std::{fs::read_to_string, path::Path, process::Command, str};

/// Initialize the GIT_REVISION environment variable with the git revision of
/// the current crate.
///
/// Intended to be called from within a build script, `build.rs` file, for the
/// crate.
pub fn init() {
    let _res = __init(&mut std::io::stdout(), &std::env::current_dir().unwrap());
}

fn __init(w: &mut impl std::io::Write, current_dir: &Path) -> std::io::Result<()> {
    let mut git_sha: Option<String> = None;

    // Read the git revision from the JSON file embedded by cargo publish. This
    // will get the version from published crates.
    if let Ok(vcs_info) = read_to_string(current_dir.join(".cargo_vcs_info.json")) {
        let vcs_info: Result<CargoVcsInfo, _> = serde_json::from_str(&vcs_info);
        if let Ok(vcs_info) = vcs_info {
            git_sha = Some(vcs_info.git.sha1);
        }
    }

    // Read the git revision from the git repository containing the code being
    // built.
    if git_sha.is_none() {
        if let Ok(git_dir) = Command::new("git")
            .current_dir(current_dir)
            .arg("rev-parse")
            .arg("--git-dir")
            .output()
            .map(|o| o.stdout)
        {
            let git_dir = String::from_utf8_lossy(&git_dir);
            let git_dir = git_dir.trim();

            // Require the build script to rerun if relavent git state changes which
            // changes the current git commit.
            //  - .git/index: Changes if the index/staged files changes, which will
            //  cause the repo to be dirty.
            //  - .git/HEAD: Changes if the ref currently in the working directory,
            //  and potentially the commit, to change.
            //  - .git/refs: Changes to any files in refs could cause the current
            //  commit to have changed if the ref in .git/HEAD is changed.
            // Note: That changes in the above files may not result in material
            // changes to the crate, but changes in any should invalidate the
            // revision since the revision can be changed by any of the above.
            writeln!(w, "cargo:rerun-if-changed={}/index", git_dir)?;
            writeln!(w, "cargo:rerun-if-changed={}/HEAD", git_dir)?;
            writeln!(w, "cargo:rerun-if-changed={}/refs", git_dir)?;

            if let Ok(git_describe) = Command::new("git")
                .current_dir(current_dir)
                .arg("describe")
                .arg("--always")
                .arg("--exclude='*'")
                .arg("--long")
                .arg("--abbrev=1000")
                .arg("--dirty")
                .output()
                .map(|o| o.stdout)
            {
                git_sha = str::from_utf8(&git_describe).ok().map(str::to_string);
            }
        }
    }

    if let Some(git_sha) = git_sha {
        writeln!(w, "cargo:rustc-env=GIT_REVISION={git_sha}")?;
    }

    Ok(())
}

#[derive(serde_derive::Serialize, serde_derive::Deserialize, Default)]
struct CargoVcsInfo {
    git: CargoVcsInfoGit,
}

#[derive(serde_derive::Serialize, serde_derive::Deserialize, Default)]
struct CargoVcsInfoGit {
    sha1: String,
}

mod test;