1
- use std:: collections:: HashMap ;
1
+ use std:: collections:: {
2
+ HashMap ,
3
+ HashSet ,
4
+ } ;
2
5
3
6
use crate :: commit:: commits_to_conventional_commits;
4
7
use crate :: error:: Result ;
5
8
use crate :: {
6
9
commit:: {
7
10
Commit ,
11
+ Link ,
8
12
Range ,
9
13
} ,
10
14
config:: Bump ,
11
15
config:: BumpType ,
16
+ config:: LinkParser ,
12
17
} ;
13
18
#[ cfg( feature = "remote" ) ]
14
19
use crate :: {
@@ -20,6 +25,10 @@ use crate::{
20
25
} ,
21
26
} ;
22
27
28
+ use chrono:: {
29
+ TimeZone ,
30
+ Utc ,
31
+ } ;
23
32
use next_version:: {
24
33
NextVersion ,
25
34
VersionUpdater ,
@@ -31,6 +40,24 @@ use serde::{
31
40
} ;
32
41
use serde_json:: value:: Value ;
33
42
43
+ /// Aggregated statistics about commits in the release.
44
+ #[ derive( Default , Debug , Clone , PartialEq , Eq ) ]
45
+ pub struct Statistics {
46
+ /// The total number of commits included in the release.
47
+ pub commit_count : usize ,
48
+ /// The time span, in days, from the first to the last commit in the
49
+ /// release. Only present if there is more than one commit.
50
+ pub commit_duration_days : Option < i32 > ,
51
+ /// The number of commits that follow the Conventional Commits
52
+ /// specification.
53
+ pub conventional_commit_count : usize ,
54
+ /// The links that were referenced in commit messages.
55
+ pub unique_links : Vec < Link > ,
56
+ /// The number of days since the previous release.
57
+ /// Only present if this is not the first release.
58
+ pub days_passed_since_last_release : Option < i32 > ,
59
+ }
60
+
34
61
/// Representation of a release.
35
62
#[ derive( Default , Debug , Clone , PartialEq , Serialize , Deserialize ) ]
36
63
#[ serde( rename_all( serialize = "camelCase" ) ) ]
@@ -96,6 +123,95 @@ impl Release<'_> {
96
123
self . calculate_next_version_with_config ( & Bump :: default ( ) )
97
124
}
98
125
126
+ /// Aggregates various statistics from the release data.
127
+ ///
128
+ /// This method computes several metrics based on the current release and
129
+ /// its commits:
130
+ ///
131
+ /// - Counts the total number of commits.
132
+ /// - Determines the number of days between the first and last commit.
133
+ /// - Counts the number of commits that follow the Conventional Commits
134
+ /// specification.
135
+ /// - Extracts and deduplicates all unique links found in commit messages
136
+ /// using the given link parsers.
137
+ /// - Calculates the number of days since the previous release, if
138
+ /// available.
139
+ pub fn aggregate_statistics (
140
+ & self ,
141
+ link_parsers : & [ LinkParser ] ,
142
+ ) -> Result < Statistics > {
143
+ let commit_count = self . commits . len ( ) ;
144
+ let commit_duration_days = if self . commits . is_empty ( ) {
145
+ trace ! ( "commit_duration_days: no commits to calculate duration" ) ;
146
+ None
147
+ } else {
148
+ self . commits
149
+ . iter ( )
150
+ . min_by_key ( |c| c. timestamp )
151
+ . zip ( self . commits . iter ( ) . max_by_key ( |c| c. timestamp ) )
152
+ . and_then ( |( first, last) | {
153
+ Utc . timestamp_opt ( first. timestamp , 0 )
154
+ . single ( )
155
+ . zip ( Utc . timestamp_opt ( last. timestamp , 0 ) . single ( ) )
156
+ . map ( |( start, end) | {
157
+ ( end. date_naive ( ) - start. date_naive ( ) ) . num_days ( ) as i32
158
+ } )
159
+ } )
160
+ . or_else ( || {
161
+ trace ! ( "commit_duration_days: timestamp conversion failed" ) ;
162
+ None
163
+ } )
164
+ } ;
165
+ let conventional_commit_count =
166
+ self . commits . iter ( ) . filter ( |c| c. conv . is_some ( ) ) . count ( ) ;
167
+ let unique_links: Vec < Link > = self
168
+ . commits
169
+ . iter ( )
170
+ . filter_map ( |c| match c. clone ( ) . parse_links ( link_parsers) {
171
+ Ok ( parsed) => Some ( parsed. links ) ,
172
+ Err ( err) => {
173
+ trace ! (
174
+ "unique_links: parse_links failed for commit {} - {} ({})" ,
175
+ c. id. chars( ) . take( 7 ) . collect:: <String >( ) ,
176
+ err,
177
+ c. message. lines( ) . next( ) . unwrap_or_default( ) . trim( )
178
+ ) ;
179
+ None
180
+ }
181
+ } )
182
+ . flatten ( )
183
+ . collect :: < HashSet < _ > > ( )
184
+ . into_iter ( )
185
+ . collect ( ) ;
186
+ let days_passed_since_last_release = match self . previous . as_ref ( ) {
187
+ Some ( prev) => Utc
188
+ . timestamp_opt ( self . timestamp , 0 )
189
+ . single ( )
190
+ . zip ( Utc . timestamp_opt ( prev. timestamp , 0 ) . single ( ) )
191
+ . map ( |( curr, prev) | {
192
+ ( curr. date_naive ( ) - prev. date_naive ( ) ) . num_days ( ) as i32
193
+ } )
194
+ . or_else ( || {
195
+ trace ! (
196
+ "days_passed_since_last_release: timestamp conversion \
197
+ failed"
198
+ ) ;
199
+ None
200
+ } ) ,
201
+ None => {
202
+ trace ! ( "days_passed_since_last_release: previous release not found" ) ;
203
+ None
204
+ }
205
+ } ;
206
+ Ok ( Statistics {
207
+ commit_count,
208
+ commit_duration_days,
209
+ conventional_commit_count,
210
+ unique_links,
211
+ days_passed_since_last_release,
212
+ } )
213
+ }
214
+
99
215
/// Calculates the next version based on the commits.
100
216
///
101
217
/// It uses the given bump version configuration to calculate the next
0 commit comments