Skip to content

Commit b834190

Browse files
committed
use git log instead of git blame for listing contributors
1 parent aefc892 commit b834190

File tree

3 files changed

+122
-20
lines changed

3 files changed

+122
-20
lines changed

tools/list-contributors/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ edition = "2018"
88
name = "cargo-list-contributors"
99
path = "main.rs"
1010

11+
[dependencies.regex]
12+
version = "1"
13+
1114
[dependencies.walkdir]
1215
version = "2"
1316

tools/list-contributors/main.rs

Lines changed: 118 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![feature(once_cell, const_in_array_repeat_expressions)]
2+
13
use std::{
24
collections::BTreeMap,
35
env,
@@ -6,8 +8,10 @@ use std::{
68
path::{Path, PathBuf},
79
process::Command,
810
io,
11+
lazy::SyncLazy,
912
};
1013

14+
use regex::RegexSet;
1115
use structopt::StructOpt;
1216
use walkdir::WalkDir;
1317

@@ -39,13 +43,44 @@ fn main() {
3943
drop(serde_json::to_writer_pretty(stdout, &top_contributors));
4044
}
4145

42-
/// Recurse through the `/library` directory, listing contributors
4346
pub fn top_contributors(
4447
repo_root: impl AsRef<Path>,
4548
subpath: Option<impl AsRef<Path>>,
4649
since: impl fmt::Display,
4750
) -> BTreeMap<String, BTreeMap<String, usize>> {
4851
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();
4984
let lib_root = {
5085
let mut lib_root = repo_root.to_owned();
5186
lib_root.push("library");
@@ -87,15 +122,16 @@ pub fn top_contributors(
87122
.filter_map(|entry| entry.ok())
88123
.filter(|entry| entry.path().is_file())
89124
.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)
99135
})
100136
.filter(|(_, contributors)| contributors.len() > 0)
101137
.fold(BTreeMap::new(), |mut map, (path, contributors)| {
@@ -104,16 +140,31 @@ pub fn top_contributors(
104140
})
105141
}
106142

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(
109158
repo_root: impl AsRef<Path>,
110159
dir: impl AsRef<Path>,
111160
since: impl fmt::Display,
112161
) -> BTreeMap<String, usize> {
113162
let stdout = Command::new("git")
114163
.args(&[
115-
"blame",
116-
"--line-porcelain",
164+
"log",
165+
"--format=%an:gitsplit:%s",
166+
"--numstat",
167+
"--no-merges",
117168
&format!("--since={}", since),
118169
"--follow", // follow renames
119170
dir.as_ref().to_str().expect("non UTF8 path"),
@@ -123,15 +174,62 @@ fn blame(
123174
.expect("failed to run git blame")
124175
.stdout;
125176

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");
128178

129179
stdout
130180
.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;
135203
map
136204
})
137205
}
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 {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nightly

0 commit comments

Comments
 (0)