Skip to content

Commit c630bf2

Browse files
committed
feat: flesh out log command
1 parent 221e6cb commit c630bf2

File tree

1 file changed

+111
-16
lines changed

1 file changed

+111
-16
lines changed

src/main.rs

+111-16
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
use clap::{App, Arg, ArgMatches, SubCommand};
1+
use clap::{App, Arg, SubCommand};
22
use codeowners::Owner;
33
use codeowners::Owners;
44
use git2::{Error, Repository};
5-
use std::collections::HashSet;
5+
use std::collections::{HashMap, HashSet};
66
use std::env::current_dir;
77
use std::io::{self, BufRead};
88
use std::path::Path;
99
use std::path::PathBuf;
1010

1111
type ExitError = (Option<String>, i32);
12+
type Settings = (bool, bool, bool);
1213

1314
fn main() {
1415
if let Err((msg, code)) = run() {
@@ -96,26 +97,33 @@ fn run() -> Result<(), ExitError> {
9697

9798
let owners = codeowners::from_path(ownersfile);
9899

100+
let settings = (
101+
matches.is_present("teams"),
102+
matches.is_present("users"),
103+
matches.is_present("emails"),
104+
);
105+
99106
match matches.subcommand() {
100107
("path", Some(matches)) => match matches.value_of("path").unwrap().as_ref() {
101108
"-" => {
102109
let stdin = io::stdin();
103110
for path in stdin.lock().lines().filter_map(Result::ok) {
104-
if !resolve(&owners, &matches, &path) {
111+
if !resolve(&owners, settings, &path) {
105112
return Err((None, 2));
106113
}
107114
}
108115
}
109116
path => {
110-
if !resolve(&owners, &matches, path) {
117+
if !resolve(&owners, settings, path) {
111118
return Err((None, 2));
112119
}
113120
}
114121
},
115122
("log", Some(matches)) => {
116123
let repo = Repository::discover(".").expect("dir");
117124
for revspec in matches.values_of("revspec").expect("required") {
118-
print_for_revspec(&repo, revspec).map_err(|e| (Some(format!("{:?}", e)), 1))?;
125+
print_for_revspec(&repo, &owners, settings, revspec)
126+
.map_err(|e| (Some(format!("{:?}", e)), 1))?;
119127
}
120128
}
121129
(_, _) => unreachable!("invalid subcommand"),
@@ -124,17 +132,13 @@ fn run() -> Result<(), ExitError> {
124132
Ok(())
125133
}
126134

127-
fn resolve(owners: &Owners, matches: &ArgMatches, path: &str) -> bool {
128-
let (teams, users, emails) = (
129-
matches.occurrences_of("teams") > 0,
130-
matches.occurrences_of("users") > 0,
131-
matches.occurrences_of("emails") > 0,
132-
);
135+
fn find_owners<P: AsRef<Path>>(owners: &Owners, settings: Settings, path: P) -> Vec<String> {
136+
let (teams, users, emails) = settings;
133137
let owners = match owners.of(path) {
134138
Some(owners) => owners,
135-
None => return false,
139+
None => return Vec::new(),
136140
};
137-
let owned = owners
141+
owners
138142
.iter()
139143
.filter_map(|owner| {
140144
if teams {
@@ -156,7 +160,11 @@ fn resolve(owners: &Owners, matches: &ArgMatches, path: &str) -> bool {
156160
Some(owner.to_string())
157161
}
158162
})
159-
.collect::<Vec<_>>();
163+
.collect()
164+
}
165+
166+
fn resolve(owners: &Owners, settings: Settings, path: &str) -> bool {
167+
let owned = find_owners(owners, settings, path);
160168

161169
if owned.is_empty() {
162170
return false;
@@ -192,9 +200,26 @@ fn discover_codeowners() -> Option<PathBuf> {
192200
codeowners::locate(&curr_dir)
193201
}
194202

195-
fn print_for_revspec(repo: &git2::Repository, revspec: &str) -> Result<(), Error> {
203+
#[derive(Default, Debug)]
204+
struct Stats {
205+
files: u64,
206+
commits: u64,
207+
example: String,
208+
}
209+
210+
fn print_for_revspec(
211+
repo: &git2::Repository,
212+
owners: &Owners,
213+
settings: Settings,
214+
revspec: &str,
215+
) -> Result<(), Error> {
196216
let mut revwalk = repo.revwalk()?;
197217
revwalk.push_range(revspec)?;
218+
219+
let mut summary = HashMap::with_capacity(2);
220+
221+
let mut unowned = Stats::default();
222+
198223
while let Some(commit) = revwalk.next() {
199224
let commit = commit?;
200225
let commit = repo.find_commit(commit).expect("commit");
@@ -215,7 +240,77 @@ fn print_for_revspec(repo: &git2::Repository, revspec: &str) -> Result<(), Error
215240
files.insert(path.to_path_buf());
216241
}
217242
}
218-
println!("{:?} touched files: {:?}", commit.id(), files);
243+
244+
let hash = &format!("{}", commit.id())[..6];
245+
let first_line = commit
246+
.message()
247+
.unwrap_or("")
248+
.split("\n")
249+
.next()
250+
.unwrap_or("");
251+
252+
let commit = format!("{} {}", hash, first_line);
253+
254+
if files.is_empty() {
255+
println!("{}", commit);
256+
continue;
257+
}
258+
259+
println!("{}", commit);
260+
261+
// repo.branches(None)?.next().unwrap().unwrap().0.get().peel_to_commit()
262+
263+
let mut files = files.into_iter().collect::<Vec<_>>();
264+
files.sort();
265+
266+
let mut commit_owners = HashSet::with_capacity(files.len());
267+
268+
for file in files {
269+
let owners = find_owners(owners, settings, &file);
270+
println!(" * {:?} {}", file, owners.join(" "));
271+
commit_owners.extend(owners.iter().cloned());
272+
273+
if owners.is_empty() {
274+
unowned.files += 1;
275+
if unowned.example.is_empty() {
276+
unowned.example = format!("{:?} in {}", file, commit);
277+
}
278+
}
279+
280+
for owner in owners {
281+
let stats = summary.entry(owner).or_insert_with(Stats::default);
282+
stats.files += 1;
283+
284+
if stats.example.is_empty() {
285+
stats.example = format!("{:?} in {}", file, commit);
286+
}
287+
}
288+
}
289+
290+
for owner in commit_owners {
291+
summary.entry(owner).or_insert_with(Stats::default).commits += 1;
292+
}
293+
294+
println!();
295+
}
296+
297+
println!();
298+
println!("Summary:");
299+
let mut summary = summary.into_iter().collect::<Vec<_>>();
300+
summary.sort_by_key(|(owner, _)| owner.to_string());
301+
302+
for (owner, stats) in summary {
303+
println!(
304+
" * {}: {} files in {} commits, including: {}",
305+
owner, stats.files, stats.commits, stats.example
306+
);
307+
}
308+
309+
if unowned.files != 0 {
310+
println!(
311+
" * no owner: {} files, including: {}",
312+
unowned.files, unowned.example
313+
);
219314
}
220315

221316
Ok(())

0 commit comments

Comments
 (0)