1
+ #![ feature( once_cell, const_in_array_repeat_expressions) ]
2
+
1
3
use std:: {
2
4
collections:: BTreeMap ,
3
5
env,
@@ -6,8 +8,10 @@ use std::{
6
8
path:: { Path , PathBuf } ,
7
9
process:: Command ,
8
10
io,
11
+ lazy:: SyncLazy ,
9
12
} ;
10
13
14
+ use regex:: RegexSet ;
11
15
use structopt:: StructOpt ;
12
16
use walkdir:: WalkDir ;
13
17
@@ -39,13 +43,44 @@ fn main() {
39
43
drop ( serde_json:: to_writer_pretty ( stdout, & top_contributors) ) ;
40
44
}
41
45
42
- /// Recurse through the `/library` directory, listing contributors
43
46
pub fn top_contributors (
44
47
repo_root : impl AsRef < Path > ,
45
48
subpath : Option < impl AsRef < Path > > ,
46
49
since : impl fmt:: Display ,
47
50
) -> BTreeMap < String , BTreeMap < String , usize > > {
48
51
let repo_root = repo_root. as_ref ( ) ;
52
+
53
+ // First, get the list of individual contributors to each file
54
+ // Then, combine the subpaths to get a combined list of contributors for each
55
+ // This is done _really_ inefficiently so it takes a little while to complete!
56
+ file_contributors ( repo_root, subpath, since)
57
+ . into_iter ( )
58
+ . fold ( BTreeMap :: new ( ) , |mut map, ( path, contributors) | {
59
+ for ancestor in Path :: new ( & path) . ancestors ( ) {
60
+ let map = map
61
+ . entry ( ancestor. to_string_lossy ( ) . into_owned ( ) )
62
+ . or_insert_with ( || BTreeMap :: new ( ) ) ;
63
+
64
+ for ( author, size) in & contributors {
65
+ let contributions = map
66
+ . entry ( author. clone ( ) )
67
+ . or_insert_with ( || 0 ) ;
68
+
69
+ * contributions += size;
70
+ }
71
+ }
72
+
73
+ map
74
+ } )
75
+ }
76
+
77
+ /// Recurse through the `/library` directory, listing contributors
78
+ pub fn file_contributors (
79
+ repo_root : impl AsRef < Path > ,
80
+ subpath : Option < impl AsRef < Path > > ,
81
+ since : impl fmt:: Display ,
82
+ ) -> BTreeMap < String , BTreeMap < String , usize > > {
83
+ let repo_root = repo_root. as_ref ( ) ;
49
84
let lib_root = {
50
85
let mut lib_root = repo_root. to_owned ( ) ;
51
86
lib_root. push ( "library" ) ;
@@ -87,15 +122,16 @@ pub fn top_contributors(
87
122
. filter_map ( |entry| entry. ok ( ) )
88
123
. filter ( |entry| entry. path ( ) . is_file ( ) )
89
124
. map ( |entry| {
90
- (
91
- entry
92
- . path ( )
93
- . strip_prefix ( repo_root)
94
- . expect ( "failed to strip path base" )
95
- . to_string_lossy ( )
96
- . into_owned ( ) ,
97
- blame ( repo_root, entry. path ( ) , & since) ,
98
- )
125
+ let author = entry
126
+ . path ( )
127
+ . strip_prefix ( repo_root)
128
+ . expect ( "failed to strip path base" )
129
+ . to_string_lossy ( )
130
+ . into_owned ( ) ;
131
+
132
+ let log = log ( repo_root, entry. path ( ) , & since) ;
133
+
134
+ ( author, log)
99
135
} )
100
136
. filter ( |( _, contributors) | contributors. len ( ) > 0 )
101
137
. fold ( BTreeMap :: new ( ) , |mut map, ( path, contributors) | {
@@ -104,16 +140,31 @@ pub fn top_contributors(
104
140
} )
105
141
}
106
142
107
- /// Run `git blame` on a given file and return a map of each author with the number of commits made.
108
- fn blame (
143
+ // This is just a grab-bag of filters for some changes that might be sweeping refactorings.
144
+ static EXCLUDES : SyncLazy < RegexSet > = SyncLazy :: new ( || RegexSet :: new ( & [
145
+ "(?i)rustfmt" ,
146
+ "(?i)doc" ,
147
+ "(?i)tidy" ,
148
+ "(?i)mv std libs to library/" ,
149
+ "(?i)deny unsafe ops in unsafe fns" ,
150
+ "(?i)unsafe_op_in_unsafe_fn" ,
151
+ "(?i)merge" ,
152
+ "(?i)split" ,
153
+ "(?i)move" ,
154
+ ] ) . expect ( "failed to compile regex set" ) ) ;
155
+
156
+ /// Run `git log` on a given file and return a map of each author with the number of commits made.
157
+ fn log (
109
158
repo_root : impl AsRef < Path > ,
110
159
dir : impl AsRef < Path > ,
111
160
since : impl fmt:: Display ,
112
161
) -> BTreeMap < String , usize > {
113
162
let stdout = Command :: new ( "git" )
114
163
. args ( & [
115
- "blame" ,
116
- "--line-porcelain" ,
164
+ "log" ,
165
+ "--format=%an:gitsplit:%s" ,
166
+ "--numstat" ,
167
+ "--no-merges" ,
117
168
& format ! ( "--since={}" , since) ,
118
169
"--follow" , // follow renames
119
170
dir. as_ref ( ) . to_str ( ) . expect ( "non UTF8 path" ) ,
@@ -123,15 +174,62 @@ fn blame(
123
174
. expect ( "failed to run git blame" )
124
175
. stdout ;
125
176
126
- let stdout = String :: from_utf8 ( stdout) . expect ( "nont UTF8 git blame output" ) ;
127
- let prefix = "author " ;
177
+ let stdout = String :: from_utf8 ( stdout) . expect ( "non UTF8 git blame output" ) ;
128
178
129
179
stdout
130
180
. lines ( )
131
- . filter ( |line| line. starts_with ( prefix) && !line. contains ( "bors" ) )
132
- . map ( |line| line[ prefix. len ( ) ..] . to_owned ( ) )
133
- . fold ( BTreeMap :: new ( ) , |mut map, author| {
134
- * map. entry ( author) . or_insert_with ( || 0 ) += 1 ;
181
+ . filter ( |line| !line. is_empty ( ) )
182
+ . chunk_by :: < 2 > ( )
183
+ . filter_map ( |lines| {
184
+ let summary = lines[ 0 ] . expect ( "missing summary" ) ;
185
+ let diff = lines[ 1 ] . expect ( "missing diff" ) ;
186
+
187
+ let mut summary_parts = summary. split ( ":gitsplit:" ) ;
188
+ let author = summary_parts. next ( ) . expect ( "missing author" ) ;
189
+ let summary = summary_parts. next ( ) . expect ( "missing summary" ) ;
190
+ assert ! ( summary_parts. next( ) . is_none( ) , "invalid log line" ) ;
191
+
192
+ let mut diff_parts = diff. split_whitespace ( ) ;
193
+ let additions: usize = diff_parts. next ( ) . expect ( "missing additions" ) . parse ( ) . expect ( "failed to parse additions" ) ;
194
+
195
+ if author != "bors" && !EXCLUDES . is_match ( summary) {
196
+ Some ( ( author. to_owned ( ) , additions) )
197
+ } else {
198
+ None
199
+ }
200
+ } )
201
+ . fold ( BTreeMap :: new ( ) , |mut map, ( author, contribution) | {
202
+ * map. entry ( author) . or_insert_with ( || 0 ) += contribution;
135
203
map
136
204
} )
137
205
}
206
+
207
+ struct ChunkBy < I , const N : usize > ( I ) ;
208
+
209
+ impl < I , const N : usize > Iterator for ChunkBy < I , N >
210
+ where
211
+ I : Iterator ,
212
+ {
213
+ type Item = [ Option < I :: Item > ; N ] ;
214
+
215
+ fn next ( & mut self ) -> Option < Self :: Item > {
216
+ let mut chunk = [ None ; N ] ;
217
+
218
+ for i in 0 ..N {
219
+ chunk[ i] = self . 0 . next ( ) ;
220
+ }
221
+
222
+ chunk. iter ( ) . any ( Option :: is_some) . then ( || chunk)
223
+ }
224
+ }
225
+
226
+ trait ChunkByExt {
227
+ fn chunk_by < const N : usize > ( self ) -> ChunkBy < Self , N >
228
+ where
229
+ Self : Sized ,
230
+ {
231
+ ChunkBy ( self )
232
+ }
233
+ }
234
+
235
+ impl < I > ChunkByExt for I where I : Iterator { }
0 commit comments