use std::{
collections::{
btree_map::Entry,
BTreeMap,
BTreeSet,
},
time::{
Duration,
SystemTime,
UNIX_EPOCH,
},
};
use anyhow::Context;
use sequoia_openpgp as openpgp;
use openpgp::{
KeyID,
Packet,
parse::{
PacketParser,
PacketParserResult,
Parse,
},
};
use crate::Config;
use crate::Result;
use crate::cli::InitSubcommand;
use crate::git_repo;
const HORIZON: Duration = Duration::new(183 * 24 * 60 * 60, 0);
const MIN_COMMITS: usize = 10;
pub fn dispatch(config: &Config, _c: InitSubcommand) -> Result<()> {
let git = git_repo()?;
let head_id = git.head().context("Looking up HEAD")?
.resolve().context("Resolving HEAD to a commit")?
.target().expect("resolved to direct reference");
let now = SystemTime::now();
let cutoff = now - HORIZON;
let mut cutoff_exceeded = false;
let mut processed: BTreeSet<git2::Oid> = Default::default();
let mut last_processed = None;
let mut pending: BTreeSet<git2::Oid> = Default::default();
pending.insert(head_id.clone());
let mut unsigned: Vec<String> = Vec::new();
struct Committer {
commit_count: usize,
signing_keys: BTreeMap<KeyID, usize>,
}
let mut committers: BTreeMap<String, Committer> = Default::default();
while let Some(commit_id) = pending.pop_first() {
processed.insert(commit_id.clone());
last_processed = Some(commit_id.clone());
let commit = git.find_commit(commit_id)
.with_context(|| {
format!("Getting commit data for {}", commit_id)
})?;
if config.verbose() {
println!("{}: {}", commit_id, commit.summary().unwrap_or(""));
}
let mut signing_keys: BTreeSet<KeyID> = Default::default();
let mut have_sig = false;
if let Ok((sig, _data)) = git.extract_signature(&commit_id, None) {
let mut ok = true;
if let Ok(mut ppr) = PacketParser::from_bytes(&sig[..]) {
while let PacketParserResult::Some(pp) = ppr {
match pp.next() {
Ok((packet, next_ppr)) => {
ppr = next_ppr;
if let Packet::Signature(sig) = packet {
have_sig = true;
let issuers = sig.get_issuers();
if config.verbose() {
for issuer in issuers.iter() {
println!(" Allegedly signed by {}", issuer);
}
}
signing_keys.extend(
issuers.into_iter().map(KeyID::from));
} else {
ok = false;
break;
}
}
Err(err) => {
eprintln!("Warning: {} contains an invalid \
signature: {}",
commit_id, err);
ok = false;
break;
}
}
}
}
if ! ok {
have_sig = false;
signing_keys.clear();
}
}
if ! have_sig {
unsigned.push(
commit.as_object()
.short_id().ok()
.and_then(|id| id.as_str().map(|id| id.to_string()))
.unwrap_or_else(|| commit.id().to_string()));
}
let committer = commit.committer();
let committer = format!(
"{}{}{}",
committer.name().unwrap_or(""),
if committer.name().is_some() && committer.email().is_some() {
" "
} else {
""
},
committer
.email()
.map(|e| format!("<{}>", e))
.unwrap_or("".to_string()));
match committers.entry(committer) {
Entry::Occupied(mut oe) => {
let e = oe.get_mut();
e.commit_count += 1;
for signing_key in signing_keys.into_iter() {
e.signing_keys.entry(signing_key)
.and_modify(|e| {
*e += 1;
})
.or_insert(1);
}
}
Entry::Vacant(e) => {
let mut info = Committer {
commit_count: 1,
signing_keys: BTreeMap::new(),
};
for signing_key in signing_keys.into_iter() {
info.signing_keys.insert(signing_key, 1);
}
e.insert(info);
}
}
for parent in commit.parents() {
let parent_id = parent.id();
if processed.contains(&parent_id)
|| pending.contains(&parent_id)
{
continue;
}
let commit_time = parent.time();
let commit_time
= UNIX_EPOCH + Duration::new(commit_time.seconds() as u64, 0);
if commit_time < cutoff {
if processed.len() + pending.len() > MIN_COMMITS {
continue;
} else {
cutoff_exceeded = true;
}
}
pending.insert(parent_id.clone());
}
}
if cutoff_exceeded {
println!("# Examined {} recent commits.", processed.len());
} else {
println!("# Examined the {} commits in the last {} days.",
processed.len(), HORIZON.as_secs() / 24 / 60 / 60);
}
if let Some(commit_id) = last_processed {
println!("# Stopped at commit {}.", commit_id);
}
println!();
print!("# Encountered {} unsigned commits", unsigned.len());
if ! unsigned.is_empty() {
print!(" (");
for (i, id) in unsigned.iter().enumerate() {
if i == 3 && ! config.verbose() {
print!("...");
break;
}
if i > 0 {
print!(" ");
}
print!("{}", id);
}
print!(")");
}
println!();
let mut committers: Vec<(String, Committer)> = committers.into_iter().collect();
committers.sort_by_key(|(committer, info)| {
(usize::MAX - info.commit_count, committer.clone())
});
for (i, (committer, info)) in committers.iter().enumerate() {
println!();
println!("\
# {} added {} commits ({}%).
#",
committer, info.commit_count,
(100 * info.commit_count) / processed.len());
if info.signing_keys.is_empty() {
println!("\
# Never signed any commits. To authorize them, you'll need their OpenPGP
# certificate.");
continue;
}
println!("\
# After checking that they really control the following OpenPGP keys:
#
# {}
#",
info
.signing_keys.iter()
.map(|(keyid, count)| {
format!("{} ({} commits)", keyid, count)
})
.collect::<Vec<String>>()
.join("# "));
if i == 0 {
println!("\
# You can make them a project maintainer (someone who can add and
# remove committers) by running:");
} else {
println!("\
# You can make them a committer by running:");
}
for (keyid, _count) in info.signing_keys.iter() {
println!("sq-git policy authorize {} {:?} {}",
if i == 0 {
" --project-maintainer"
} else {
"--committer"
},
committer,
keyid);
}
}
Ok(())
}