git_testament/
lib.rs

1//! # Generate a testament of the git working tree state for a build
2//!
3//! You likely want to see either the [git_testament] macro, or if you
4//! are in a no-std type situation, the [git_testament_macros] macro instead.
5//!
6//! [git_testament]: macro.git_testament.html
7//! [git_testament_macros]: macro.git_testament_macros.html
8//!
9//! If you build this library with the default `alloc` feature disabled then while
10//! the non-macro form of the testaments are offered, they cannot be rendered
11//! and the [render_testament] macro will not be provided.
12//!
13//! [render_testament]: macro.render_testament.html
14//!
15//! ## Trusted branches
16//!
17//! In both [render_testament] and [git_testament_macros] you will find mention
18//! of the concept of a "trusted" branch.  This exists as a way to allow releases
19//! to be made from branches which are not yet tagged.  For example, if your
20//! release process requires that the release binaries be built and tested
21//! before tagging the repository then by nominating a particular branch as
22//! trusted, you can cause the rendered testament to trust the crate's version
23//! rather than being quite noisy about how the crate version and the tag
24//! version do not match up.
25#![no_std]
26#[cfg(feature = "alloc")]
27extern crate alloc;
28#[doc(hidden)]
29pub extern crate core as __core;
30#[doc(hidden)]
31pub extern crate git_testament_derive as __derive;
32
33use core::fmt::{self, Display, Formatter};
34
35// Clippy thinks our fn main() is needless, but it is needed because otherwise
36// we cannot have the invocation of the procedural macro (yet)
37#[allow(clippy::needless_doctest_main)]
38/// Generate a testament for the working tree.
39///
40/// This macro declares a static data structure which represents a testament
41/// to the state of a git repository at the point that a crate was built.
42///
43/// The intention is that the macro should be used at the top level of a binary
44/// crate to provide information about the state of the codebase that the output
45/// program was built from.  This includes a number of things such as the commit
46/// SHA, any related tag, how many commits since the tag, the date of the commit,
47/// and if there are any "dirty" parts to the working tree such as modified files,
48/// uncommitted files, etc.
49///
50/// ```
51/// // Bring the procedural macro into scope
52/// use git_testament::git_testament;
53///
54/// // Declare a testament, it'll end up as a static, so give it a capital
55/// // letters name or it'll result in a warning.
56/// git_testament!(TESTAMENT);
57/// # fn main() {
58///
59/// // ... later, you can display the testament.
60/// println!("app version {TESTAMENT}");
61/// # }
62/// ```
63///
64/// See [`GitTestament`] for the type of the defined `TESTAMENT`.
65#[macro_export]
66macro_rules! git_testament {
67    ($vis:vis $name:ident) => {
68        $crate::__derive::git_testament! {
69            $crate $name $vis
70        }
71    };
72    ($name:ident) => {
73        $crate::__derive::git_testament! {
74            $crate $name
75        }
76    };
77}
78
79// Clippy thinks our fn main() is needless, but it is needed because otherwise
80// we cannot have the invocation of the procedural macro (yet)
81#[allow(clippy::needless_doctest_main)]
82/// Generate a testament for the working tree as a set of static string macros.
83///
84/// This macro declares a set of macros which provide you with your testament
85/// as static strings.
86///
87/// The intention is that the macro should be used at the top level of a binary
88/// crate to provide information about the state of the codebase that the output
89/// program was built from.  This includes a number of things such as the commit
90/// SHA, any related tag, how many commits since the tag, the date of the commit,
91/// and if there are any "dirty" parts to the working tree such as modified files,
92/// uncommitted files, etc.
93///
94/// ```
95/// // Bring the procedural macro into scope
96/// use git_testament::git_testament_macros;
97///
98/// // Declare a testament, it'll end up as pile of macros, so you can
99/// // give it whatever ident-like name you want.  The name will prefix the
100/// // macro names.  Also you can optionally specify
101/// // a branch name which will be considered the "trusted" branch like in
102/// // `git_testament::render_testament!()`
103/// git_testament_macros!(version);
104/// # fn main() {
105///
106/// // ... later, you can display the testament.
107/// println!("app version {}", version_testament!());
108/// # }
109/// ```
110///
111/// The macros all resolve to string literals, boolean literals, or in the case
112/// of `NAME_tag_distance!()` a number.  This is most valuable when you are
113/// wanting to include the information into a compile-time-constructed string
114///
115/// ```
116/// // Bring the procedural macro into scope
117/// use git_testament::git_testament_macros;
118///
119/// // Declare a testament, it'll end up as pile of macros, so you can
120/// // give it whatever ident-like name you want.  The name will prefix the
121/// // macro names.  Also you can optionally specify
122/// // a branch name which will be considered the "trusted" branch like in
123/// // `git_testament::render_testament!()`
124/// git_testament_macros!(version, "stable");
125///
126/// const APP_VERSION: &str = concat!("app version ", version_testament!());
127/// # fn main() {
128///
129/// // ... later, you can display the testament.
130/// println!("{APP_VERSION}");
131/// # }
132/// ```
133///
134/// The set of macros defined is:
135///
136/// * `NAME_testament!()` -> produces a string similar but not guaranteed to be
137///   identical to the result of `Display` formatting a normal testament.
138/// * `NAME_branch!()` -> An Option<&str> of the current branch name
139/// * `NAME_repo_present!()` -> A boolean indicating if there is a repo at all
140/// * `NAME_commit_present!()` -> A boolean indicating if there is a commit present at all
141/// * `NAME_tag_present!()` -> A boolean indicating if there is a tag present
142/// * `NAME_commit_hash!()` -> A string of the commit hash (or crate version if commit not present)
143/// * `NAME_commit_date!()` -> A string of the commit date (or build date if no commit present)
144/// * `NAME_tag_name!()` -> The tag name if present (or crate version if commit not present)
145/// * `NAME_tag_distance!()` -> The number of commits since the tag if present (zero otherwise)
146#[macro_export]
147macro_rules! git_testament_macros {
148    ($name:ident $(, $trusted:literal)?) => {
149        $crate::__derive::git_testament_macros! {
150            $crate $name $($trusted)?
151        }
152    };
153}
154
155/// A modification to a working tree, recorded when the testament was created.
156#[derive(Debug)]
157pub enum GitModification<'a> {
158    /// A file or directory was added but not committed
159    Added(&'a [u8]),
160    /// A file or directory was removed but not committed
161    Removed(&'a [u8]),
162    /// A file was modified in some way, either content or permissions
163    Modified(&'a [u8]),
164    /// A file or directory was present but untracked
165    Untracked(&'a [u8]),
166}
167
168/// The kind of commit available at the point that the testament was created.
169#[derive(Debug)]
170pub enum CommitKind<'a> {
171    /// No repository was present.  Instead the crate's version and the
172    /// build date are recorded.
173    NoRepository(&'a str, &'a str),
174    /// No commit was present, though it was a repository.  Instead the crate's
175    /// version and the build date are recorded.
176    NoCommit(&'a str, &'a str),
177    /// There are no tags in the repository in the history of the commit.
178    /// The commit hash and commit date are recorded.
179    NoTags(&'a str, &'a str),
180    /// There were tags in the history of the commit.
181    /// The tag name, commit hash, commit date, and distance from the tag to
182    /// the commit are recorded.
183    FromTag(&'a str, &'a str, &'a str, usize),
184}
185
186/// A testament to the state of a git repository when a crate is built.
187///
188/// This is the type returned by the [`git_testament_derive::git_testament`]
189/// macro when used to record the state of a git tree when a crate is built.
190///
191/// The structure contains information about the commit from which the crate
192/// was built, along with information about any modifications to the working
193/// tree which could be considered "dirty" as a result.
194///
195/// By default, the `Display` implementation for this structure attempts to
196/// produce something pleasant but useful to humans.  For example it might
197/// produce a string along the lines of `"1.0.0 (763aa159d 2019-04-02)"` for
198/// a clean build from a 1.0.0 tag.  Alternatively if the working tree is dirty
199/// and there have been some commits since the last tag, you might get something
200/// more like `"1.0.0+14 (651af89ed 2019-04-02) dirty 4 modifications"`
201///
202/// If your program wishes to go into more detail, then the `commit` and the
203/// `modifications` members are available for rendering as the program author
204/// sees fit.
205///
206/// In general this is only of use for binaries, since libraries will generally
207/// be built from `crates.io` provided tarballs and as such won't carry the
208/// information needed.  In such a fallback position the string will be something
209/// along the lines of `"x.y (somedate)"` where `x.y` is the crate's version and
210/// `somedate` is the date of the build.  You'll get similar information if the
211/// crate is built in a git repository on a branch with no commits yet (e.g.
212/// when you first have run `cargo init`) though that will include the string
213/// `uncommitted` to indicate that once commits are made the information will be
214/// of more use.
215#[derive(Debug)]
216pub struct GitTestament<'a> {
217    pub commit: CommitKind<'a>,
218    pub modifications: &'a [GitModification<'a>],
219    pub branch_name: Option<&'a str>,
220}
221
222/// An empty testament.
223///
224/// This is used by the derive macro to fill in defaults
225/// in the case that an older derive macro is used with a newer version
226/// of git_testament.
227///
228/// Typically this will not be used directly by a user.
229pub const EMPTY_TESTAMENT: GitTestament = GitTestament {
230    commit: CommitKind::NoRepository("unknown", "unknown"),
231    modifications: &[],
232    branch_name: None,
233};
234
235#[cfg(feature = "alloc")]
236impl<'a> GitTestament<'a> {
237    #[doc(hidden)]
238    pub fn _render_with_version(
239        &self,
240        pkg_version: &str,
241        trusted_branch: Option<&'static str>,
242    ) -> alloc::string::String {
243        match self.commit {
244            CommitKind::FromTag(tag, hash, date, _) => {
245                let trusted = match trusted_branch {
246                    Some(_) => {
247                        if self.branch_name == trusted_branch {
248                            self.modifications.is_empty()
249                        } else {
250                            false
251                        }
252                    }
253                    None => false,
254                };
255                if trusted {
256                    // We trust our branch, so construct an equivalent
257                    // testament to render
258                    alloc::format!(
259                        "{}",
260                        GitTestament {
261                            commit: CommitKind::FromTag(pkg_version, hash, date, 0),
262                            ..*self
263                        }
264                    )
265                } else if tag.contains(pkg_version) {
266                    alloc::format!("{self}")
267                } else {
268                    alloc::format!("{pkg_version} :: {self}")
269                }
270            }
271            _ => alloc::format!("{self}"),
272        }
273    }
274}
275
276/// Render a testament
277///
278/// This macro can be used to render a testament created with the `git_testament`
279/// macro.  It renders a testament with the added benefit of indicating if the
280/// tag does not match the version (by substring) then the crate's version and
281/// the tag will be displayed in the form: "crate-ver :: testament..."
282///
283/// For situations where the crate version MUST override the tag, for example
284/// if you have a release process where you do not make the tag unless the CI
285/// constructing the release artifacts passes, then you can pass a second
286/// argument to this macro stating a branch name to trust.  If the working
287/// tree is clean and the branch name matches then the testament is rendered
288/// as though the tag had been pushed at the built commit.  Since this overrides
289/// a fundamental part of the behaviour of `git_testament` it is recommended that
290/// this *ONLY* be used if you have a trusted CI release branch process.
291///
292/// ```
293/// use git_testament::{git_testament, render_testament};
294///
295/// git_testament!(TESTAMENT);
296///
297/// # fn main() {
298/// println!("The testament is: {}", render_testament!(TESTAMENT));
299/// println!("The fiddled testament is: {}", render_testament!(TESTAMENT, "trusted-branch"));
300/// # }
301#[cfg(feature = "alloc")]
302#[macro_export]
303macro_rules! render_testament {
304    ( $testament:expr ) => {
305        $crate::GitTestament::_render_with_version(
306            &$testament,
307            $crate::__core::env!("CARGO_PKG_VERSION"),
308            $crate::__core::option::Option::None,
309        )
310    };
311    ( $testament:expr, $trusted_branch:expr ) => {
312        $crate::GitTestament::_render_with_version(
313            &$testament,
314            $crate::__core::env!("CARGO_PKG_VERSION"),
315            $crate::__core::option::Option::Some($trusted_branch),
316        )
317    };
318}
319
320impl<'a> Display for CommitKind<'a> {
321    fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
322        match self {
323            CommitKind::NoRepository(crate_ver, build_date) => {
324                write!(fmt, "{crate_ver} ({build_date})")
325            }
326            CommitKind::NoCommit(crate_ver, build_date) => {
327                write!(fmt, "{crate_ver} (uncommitted {build_date})")
328            }
329            CommitKind::NoTags(commit, when) => {
330                write!(fmt, "unknown ({} {})", &commit[..9], when)
331            }
332            CommitKind::FromTag(tag, commit, when, depth) => {
333                if *depth > 0 {
334                    write!(fmt, "{}+{} ({} {})", tag, depth, &commit[..9], when)
335                } else {
336                    write!(fmt, "{} ({} {})", tag, &commit[..9], when)
337                }
338            }
339        }
340    }
341}
342
343impl<'a> Display for GitTestament<'a> {
344    fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
345        self.commit.fmt(fmt)?;
346        if !self.modifications.is_empty() {
347            write!(
348                fmt,
349                " dirty {} modification{}",
350                self.modifications.len(),
351                if self.modifications.len() > 1 {
352                    "s"
353                } else {
354                    ""
355                }
356            )?;
357        }
358        Ok(())
359    }
360}