1#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct GitFacts {
15 pub branch: Option<String>,
17 pub commit: String,
19 pub describe: Option<String>,
21 pub modified: bool,
23}
24
25fn parse_describe(describe: &str) -> Option<(&str, u32)> {
28 let (rest, _hash) = describe.rsplit_once('-')?;
29 let (tag, count) = rest.rsplit_once('-')?;
30 Some((tag, count.parse().ok()?))
31}
32
33pub fn format_version(package_version: &str, facts: Option<&GitFacts>) -> String {
37 let Some(facts) = facts else {
38 return package_version.to_string();
39 };
40 let since = facts.describe.as_deref().and_then(parse_describe);
41 if !facts.modified && since == Some((format!("v{package_version}").as_str(), 0)) {
42 return package_version.to_string();
43 }
44 let branch = facts.branch.as_deref().unwrap_or("detached");
45 let mut details = format!("{branch}@{}", facts.commit);
46 if let Some((tag, count)) = since {
47 let noun = if count == 1 { "commit" } else { "commits" };
48 details.push_str(&format!(", {count} {noun} after {tag}"));
49 }
50 if facts.modified {
51 details.push_str(", modified");
52 }
53 format!("{package_version}-dev ({details})")
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 fn facts(describe: Option<&str>, modified: bool) -> GitFacts {
61 GitFacts {
62 branch: Some("main".into()),
63 commit: "5e94647".into(),
64 describe: describe.map(String::from),
65 modified,
66 }
67 }
68
69 #[test]
70 fn no_git_is_the_plain_version() {
71 assert_eq!(format_version("2.0.0", None), "2.0.0");
72 }
73
74 #[test]
75 fn the_clean_release_tag_is_the_plain_version() {
76 let f = facts(Some("v2.0.0-0-g5e94647"), false);
77 assert_eq!(format_version("2.0.0", Some(&f)), "2.0.0");
78 }
79
80 #[test]
81 fn commits_after_the_tag_are_counted() {
82 let f = facts(Some("v2.0.0-3-g5e94647"), false);
83 assert_eq!(
84 format_version("2.0.0", Some(&f)),
85 "2.0.0-dev (main@5e94647, 3 commits after v2.0.0)"
86 );
87 let f = facts(Some("v2.0.0-1-g5e94647"), false);
88 assert_eq!(
89 format_version("2.0.0", Some(&f)),
90 "2.0.0-dev (main@5e94647, 1 commit after v2.0.0)"
91 );
92 }
93
94 #[test]
95 fn main_before_the_release_counts_from_the_last_tag() {
96 let f = facts(Some("v1.14.5-12-g5e94647"), false);
97 assert_eq!(
98 format_version("1.14.5", Some(&f)),
99 "1.14.5-dev (main@5e94647, 12 commits after v1.14.5)"
100 );
101 }
102
103 #[test]
104 fn a_modified_tree_says_so_even_on_the_tag() {
105 let f = facts(Some("v2.0.0-0-g5e94647"), true);
106 assert_eq!(
107 format_version("2.0.0", Some(&f)),
108 "2.0.0-dev (main@5e94647, 0 commits after v2.0.0, modified)"
109 );
110 let f = facts(Some("v2.0.0-3-g5e94647"), true);
111 assert_eq!(
112 format_version("2.0.0", Some(&f)),
113 "2.0.0-dev (main@5e94647, 3 commits after v2.0.0, modified)"
114 );
115 }
116
117 #[test]
118 fn a_detached_head_is_named() {
119 let mut f = facts(Some("v2.0.0-3-g5e94647"), false);
120 f.branch = None;
121 assert_eq!(
122 format_version("2.0.0", Some(&f)),
123 "2.0.0-dev (detached@5e94647, 3 commits after v2.0.0)"
124 );
125 }
126
127 #[test]
128 fn a_tag_for_another_version_is_not_this_release() {
129 let f = facts(Some("v1.14.5-0-g5e94647"), false);
130 assert_eq!(
131 format_version("2.0.0", Some(&f)),
132 "2.0.0-dev (main@5e94647, 0 commits after v1.14.5)"
133 );
134 }
135
136 #[test]
137 fn no_tag_reachable_leaves_out_the_count() {
138 let f = facts(None, false);
139 assert_eq!(
140 format_version("2.0.0", Some(&f)),
141 "2.0.0-dev (main@5e94647)"
142 );
143 let f = facts(Some("not-a-describe"), false);
144 assert_eq!(
145 format_version("2.0.0", Some(&f)),
146 "2.0.0-dev (main@5e94647)"
147 );
148 let f = facts(Some("nodash"), true);
149 assert_eq!(
150 format_version("2.0.0", Some(&f)),
151 "2.0.0-dev (main@5e94647, modified)"
152 );
153 }
154}