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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
//! # Generate a testament of the git working tree state for a build
//!
//! You likely want to see either the [git_testament] macro, or if you
//! are in a no-std type situation, the [git_testament_macros] macro instead.
//!
//! [git_testament]: macro.git_testament.html
//! [git_testament_macros]: macro.git_testament_macros.html
//!
//! If you build this library with the default `alloc` feature disabled then while
//! the non-macro form of the testaments are offered, they cannot be rendered
//! and the [render_testament] macro will not be provided.
//!
//! [render_testament]: macro.render_testament.html
//!
//! ## Trusted branches
//!
//! In both [render_testament] and [git_testament_macros] you will find mention
//! of the concept of a "trusted" branch.  This exists as a way to allow releases
//! to be made from branches which are not yet tagged.  For example, if your
//! release process requires that the release binaries be built and tested
//! before tagging the repository then by nominating a particular branch as
//! trusted, you can cause the rendered testament to trust the crate's version
//! rather than being quite noisy about how the crate version and the tag
//! version do not match up.
#![no_std]
#[cfg(feature = "alloc")]
extern crate alloc;
#[doc(hidden)]
pub extern crate core as __core;
#[doc(hidden)]
pub extern crate git_testament_derive as __derive;

use core::fmt::{self, Display, Formatter};

// Clippy thinks our fn main() is needless, but it is needed because otherwise
// we cannot have the invocation of the procedural macro (yet)
#[allow(clippy::needless_doctest_main)]
/// Generate a testament for the working tree.
///
/// This macro declares a static data structure which represents a testament
/// to the state of a git repository at the point that a crate was built.
///
/// The intention is that the macro should be used at the top level of a binary
/// crate to provide information about the state of the codebase that the output
/// program was built from.  This includes a number of things such as the commit
/// SHA, any related tag, how many commits since the tag, the date of the commit,
/// and if there are any "dirty" parts to the working tree such as modified files,
/// uncommitted files, etc.
///
/// ```
/// // Bring the procedural macro into scope
/// use git_testament::git_testament;
///
/// // Declare a testament, it'll end up as a static, so give it a capital
/// // letters name or it'll result in a warning.
/// git_testament!(TESTAMENT);
/// # fn main() {
///
/// // ... later, you can display the testament.
/// println!("app version {TESTAMENT}");
/// # }
/// ```
///
/// See [`GitTestament`] for the type of the defined `TESTAMENT`.
#[macro_export]
macro_rules! git_testament {
    ($name:ident) => {
        $crate::__derive::git_testament! {
            $crate $name
        }
    };
}

// Clippy thinks our fn main() is needless, but it is needed because otherwise
// we cannot have the invocation of the procedural macro (yet)
#[allow(clippy::needless_doctest_main)]
/// Generate a testament for the working tree as a set of static string macros.
///
/// This macro declares a set of macros which provide you with your testament
/// as static strings.
///
/// The intention is that the macro should be used at the top level of a binary
/// crate to provide information about the state of the codebase that the output
/// program was built from.  This includes a number of things such as the commit
/// SHA, any related tag, how many commits since the tag, the date of the commit,
/// and if there are any "dirty" parts to the working tree such as modified files,
/// uncommitted files, etc.
///
/// ```
/// // Bring the procedural macro into scope
/// use git_testament::git_testament_macros;
///
/// // Declare a testament, it'll end up as pile of macros, so you can
/// // give it whatever ident-like name you want.  The name will prefix the
/// // macro names.  Also you can optionally specify
/// // a branch name which will be considered the "trusted" branch like in
/// // `git_testament::render_testament!()`
/// git_testament_macros!(version);
/// # fn main() {
///
/// // ... later, you can display the testament.
/// println!("app version {}", version_testament!());
/// # }
/// ```
///
/// The macros all resolve to string literals, boolean literals, or in the case
/// of `NAME_tag_distance!()` a number.  This is most valuable when you are
/// wanting to include the information into a compile-time-constructed string
///
/// ```
/// // Bring the procedural macro into scope
/// use git_testament::git_testament_macros;
///
/// // Declare a testament, it'll end up as pile of macros, so you can
/// // give it whatever ident-like name you want.  The name will prefix the
/// // macro names.  Also you can optionally specify
/// // a branch name which will be considered the "trusted" branch like in
/// // `git_testament::render_testament!()`
/// git_testament_macros!(version, "stable");
///
/// const APP_VERSION: &str = concat!("app version ", version_testament!());
/// # fn main() {
///
/// // ... later, you can display the testament.
/// println!("{APP_VERSION}");
/// # }
/// ```
///
/// The set of macros defined is:
///
/// * `NAME_testament!()` -> produces a string similar but not guaranteed to be
///   identical to the result of `Display` formatting a normal testament.
/// * `NAME_branch!()` -> An Option<&str> of the current branch name
/// * `NAME_repo_present!()` -> A boolean indicating if there is a repo at all
/// * `NAME_commit_present!()` -> A boolean indicating if there is a commit present at all
/// * `NAME_tag_present!()` -> A boolean indicating if there is a tag present
/// * `NAME_commit_hash!()` -> A string of the commit hash (or crate version if commit not present)
/// * `NAME_commit_date!()` -> A string of the commit date (or build date if no commit present)
/// * `NAME_tag_name!()` -> The tag name if present (or crate version if commit not present)
/// * `NAME_tag_distance!()` -> The number of commits since the tag if present (zero otherwise)
#[macro_export]
macro_rules! git_testament_macros {
    ($name:ident $(, $trusted:literal)?) => {
        $crate::__derive::git_testament_macros! {
            $crate $name $($trusted)?
        }
    };
}

/// A modification to a working tree, recorded when the testament was created.
#[derive(Debug)]
pub enum GitModification<'a> {
    /// A file or directory was added but not committed
    Added(&'a [u8]),
    /// A file or directory was removed but not committed
    Removed(&'a [u8]),
    /// A file was modified in some way, either content or permissions
    Modified(&'a [u8]),
    /// A file or directory was present but untracked
    Untracked(&'a [u8]),
}

/// The kind of commit available at the point that the testament was created.
#[derive(Debug)]
pub enum CommitKind<'a> {
    /// No repository was present.  Instead the crate's version and the
    /// build date are recorded.
    NoRepository(&'a str, &'a str),
    /// No commit was present, though it was a repository.  Instead the crate's
    /// version and the build date are recorded.
    NoCommit(&'a str, &'a str),
    /// There are no tags in the repository in the history of the commit.
    /// The commit hash and commit date are recorded.
    NoTags(&'a str, &'a str),
    /// There were tags in the history of the commit.
    /// The tag name, commit hash, commit date, and distance from the tag to
    /// the commit are recorded.
    FromTag(&'a str, &'a str, &'a str, usize),
}

/// A testament to the state of a git repository when a crate is built.
///
/// This is the type returned by the [`git_testament_derive::git_testament`]
/// macro when used to record the state of a git tree when a crate is built.
///
/// The structure contains information about the commit from which the crate
/// was built, along with information about any modifications to the working
/// tree which could be considered "dirty" as a result.
///
/// By default, the `Display` implementation for this structure attempts to
/// produce something pleasant but useful to humans.  For example it might
/// produce a string along the lines of `"1.0.0 (763aa159d 2019-04-02)"` for
/// a clean build from a 1.0.0 tag.  Alternatively if the working tree is dirty
/// and there have been some commits since the last tag, you might get something
/// more like `"1.0.0+14 (651af89ed 2019-04-02) dirty 4 modifications"`
///
/// If your program wishes to go into more detail, then the `commit` and the
/// `modifications` members are available for rendering as the program author
/// sees fit.
///
/// In general this is only of use for binaries, since libraries will generally
/// be built from `crates.io` provided tarballs and as such won't carry the
/// information needed.  In such a fallback position the string will be something
/// along the lines of `"x.y (somedate)"` where `x.y` is the crate's version and
/// `somedate` is the date of the build.  You'll get similar information if the
/// crate is built in a git repository on a branch with no commits yet (e.g.
/// when you first have run `cargo init`) though that will include the string
/// `uncommitted` to indicate that once commits are made the information will be
/// of more use.
#[derive(Debug)]
pub struct GitTestament<'a> {
    pub commit: CommitKind<'a>,
    pub modifications: &'a [GitModification<'a>],
    pub branch_name: Option<&'a str>,
}

/// An empty testament.
///
/// This is used by the derive macro to fill in defaults
/// in the case that an older derive macro is used with a newer version
/// of git_testament.
///
/// Typically this will not be used directly by a user.
pub const EMPTY_TESTAMENT: GitTestament = GitTestament {
    commit: CommitKind::NoRepository("unknown", "unknown"),
    modifications: &[],
    branch_name: None,
};

#[cfg(feature = "alloc")]
impl<'a> GitTestament<'a> {
    #[doc(hidden)]
    pub fn _render_with_version(
        &self,
        pkg_version: &str,
        trusted_branch: Option<&'static str>,
    ) -> alloc::string::String {
        match self.commit {
            CommitKind::FromTag(tag, hash, date, _) => {
                let trusted = match trusted_branch {
                    Some(_) => {
                        if self.branch_name == trusted_branch {
                            self.modifications.is_empty()
                        } else {
                            false
                        }
                    }
                    None => false,
                };
                if trusted {
                    // We trust our branch, so construct an equivalent
                    // testament to render
                    alloc::format!(
                        "{}",
                        GitTestament {
                            commit: CommitKind::FromTag(pkg_version, hash, date, 0),
                            ..*self
                        }
                    )
                } else if tag.contains(pkg_version) {
                    alloc::format!("{self}")
                } else {
                    alloc::format!("{pkg_version} :: {self}")
                }
            }
            _ => alloc::format!("{self}"),
        }
    }
}

/// Render a testament
///
/// This macro can be used to render a testament created with the `git_testament`
/// macro.  It renders a testament with the added benefit of indicating if the
/// tag does not match the version (by substring) then the crate's version and
/// the tag will be displayed in the form: "crate-ver :: testament..."
///
/// For situations where the crate version MUST override the tag, for example
/// if you have a release process where you do not make the tag unless the CI
/// constructing the release artifacts passes, then you can pass a second
/// argument to this macro stating a branch name to trust.  If the working
/// tree is clean and the branch name matches then the testament is rendered
/// as though the tag had been pushed at the built commit.  Since this overrides
/// a fundamental part of the behaviour of `git_testament` it is recommended that
/// this *ONLY* be used if you have a trusted CI release branch process.
///
/// ```
/// use git_testament::{git_testament, render_testament};
///
/// git_testament!(TESTAMENT);
///
/// # fn main() {
/// println!("The testament is: {}", render_testament!(TESTAMENT));
/// println!("The fiddled testament is: {}", render_testament!(TESTAMENT, "trusted-branch"));
/// # }
#[cfg(feature = "alloc")]
#[macro_export]
macro_rules! render_testament {
    ( $testament:expr ) => {
        $crate::GitTestament::_render_with_version(
            &$testament,
            $crate::__core::env!("CARGO_PKG_VERSION"),
            $crate::__core::option::Option::None,
        )
    };
    ( $testament:expr, $trusted_branch:expr ) => {
        $crate::GitTestament::_render_with_version(
            &$testament,
            $crate::__core::env!("CARGO_PKG_VERSION"),
            $crate::__core::option::Option::Some($trusted_branch),
        )
    };
}

impl<'a> Display for CommitKind<'a> {
    fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
        match self {
            CommitKind::NoRepository(crate_ver, build_date) => {
                write!(fmt, "{crate_ver} ({build_date})")
            }
            CommitKind::NoCommit(crate_ver, build_date) => {
                write!(fmt, "{crate_ver} (uncommitted {build_date})")
            }
            CommitKind::NoTags(commit, when) => {
                write!(fmt, "unknown ({} {})", &commit[..9], when)
            }
            CommitKind::FromTag(tag, commit, when, depth) => {
                if *depth > 0 {
                    write!(fmt, "{}+{} ({} {})", tag, depth, &commit[..9], when)
                } else {
                    write!(fmt, "{} ({} {})", tag, &commit[..9], when)
                }
            }
        }
    }
}

impl<'a> Display for GitTestament<'a> {
    fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
        self.commit.fmt(fmt)?;
        if !self.modifications.is_empty() {
            write!(
                fmt,
                " dirty {} modification{}",
                self.modifications.len(),
                if self.modifications.len() > 1 {
                    "s"
                } else {
                    ""
                }
            )?;
        }
        Ok(())
    }
}