summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPietro Albini <pietro@pietroalbini.org>2019-07-11 20:49:49 +0200
committerPietro Albini <pietro@pietroalbini.org>2019-07-13 21:19:01 +0200
commit8578b7e3cdc93b583d5c25762ac36ec512c779f3 (patch)
tree78c5fde9f4f9cd6ea037a5ce0917bd1c09ace8a9 /src
parent474d58efe82228cafd02f025274eb411bbe0596a (diff)
add github ids to the people tomls
Diffstat (limited to 'src')
-rw-r--r--src/github.rs86
-rw-r--r--src/schema.rs7
-rw-r--r--src/validate.rs36
3 files changed, 126 insertions, 3 deletions
diff --git a/src/github.rs b/src/github.rs
index 9a62994..55bfc19 100644
--- a/src/github.rs
+++ b/src/github.rs
@@ -1,7 +1,8 @@
-use failure::{Error, ResultExt};
+use failure::{bail, Error, ResultExt};
use reqwest::header::{self, HeaderValue};
use reqwest::{Client, Method, RequestBuilder};
use std::borrow::Cow;
+use std::collections::HashMap;
static API_BASE: &str = "https://api.github.com/";
static TOKEN_VAR: &str = "GITHUB_TOKEN";
@@ -13,6 +14,23 @@ pub(crate) struct User {
pub(crate) email: Option<String>,
}
+#[derive(serde::Deserialize)]
+struct GraphResult<T> {
+ data: Option<T>,
+ #[serde(default)]
+ errors: Vec<GraphError>,
+}
+
+#[derive(serde::Deserialize)]
+struct GraphError {
+ message: String,
+}
+
+#[derive(serde::Deserialize)]
+struct GraphNodes<T> {
+ nodes: Vec<Option<T>>,
+}
+
pub(crate) struct GitHubApi {
http: Client,
token: String,
@@ -40,6 +58,31 @@ impl GitHubApi {
))
}
+ fn graphql<R, V>(&self, query: &str, variables: V) -> Result<R, Error>
+ where
+ R: for<'de> serde::Deserialize<'de>,
+ V: serde::Serialize,
+ {
+ #[derive(serde::Serialize)]
+ struct Request<'a, V> {
+ query: &'a str,
+ variables: V,
+ }
+ let res: GraphResult<R> = self
+ .prepare(Method::POST, "graphql")?
+ .json(&Request { query, variables })
+ .send()?
+ .error_for_status()?
+ .json()?;
+ if let Some(error) = res.errors.iter().next() {
+ bail!("graphql error: {}", error.message);
+ } else if let Some(data) = res.data {
+ Ok(data)
+ } else {
+ bail!("missing graphql data");
+ }
+ }
+
pub(crate) fn user(&self, login: &str) -> Result<User, Error> {
Ok(self
.prepare(Method::GET, &format!("users/{}", login))?
@@ -47,4 +90,45 @@ impl GitHubApi {
.error_for_status()?
.json()?)
}
+
+ pub(crate) fn usernames(&self, ids: &[usize]) -> Result<HashMap<usize, String>, Error> {
+ #[derive(serde::Deserialize)]
+ #[serde(rename_all = "camelCase")]
+ struct Usernames {
+ database_id: usize,
+ login: String,
+ }
+ #[derive(serde::Serialize)]
+ struct Params {
+ ids: Vec<String>,
+ }
+ static QUERY: &str = "
+ query($ids: [ID!]!) {
+ nodes(ids: $ids) {
+ ... on User {
+ databaseId
+ login
+ }
+ }
+ }
+ ";
+
+ let mut result = HashMap::new();
+ for chunk in ids.chunks(100) {
+ let res: GraphNodes<Usernames> = self.graphql(
+ QUERY,
+ Params {
+ ids: chunk.iter().map(|id| user_node_id(*id)).collect(),
+ },
+ )?;
+ for node in res.nodes.into_iter().filter_map(|n| n) {
+ result.insert(node.database_id, node.login);
+ }
+ }
+ Ok(result)
+ }
+}
+
+fn user_node_id(id: usize) -> String {
+ base64::encode(&format!("04:User{}", id))
}
diff --git a/src/schema.rs b/src/schema.rs
index c7a1b48..570b212 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -38,10 +38,11 @@ pub(crate) enum Email<'a> {
}
#[derive(serde_derive::Deserialize, Debug)]
-#[serde(deny_unknown_fields)]
+#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub(crate) struct Person {
name: String,
github: String,
+ github_id: usize,
irc: Option<String>,
#[serde(default)]
email: EmailField,
@@ -59,6 +60,10 @@ impl Person {
&self.github
}
+ pub(crate) fn github_id(&self) -> usize {
+ self.github_id
+ }
+
#[allow(unused)]
pub(crate) fn irc(&self) -> &str {
if let Some(irc) = &self.irc {
diff --git a/src/validate.rs b/src/validate.rs
index 87f2354..3965a58 100644
--- a/src/validate.rs
+++ b/src/validate.rs
@@ -1,6 +1,8 @@
use crate::data::Data;
+use crate::github::GitHubApi;
use crate::schema::{Email, Permissions};
use failure::{bail, Error};
+use log::{error, warn};
use regex::Regex;
use std::collections::{HashMap, HashSet};
@@ -23,6 +25,8 @@ static CHECKS: &[fn(&Data, &mut Vec<String>)] = &[
validate_team_names,
];
+static GITHUB_CHECKS: &[fn(&Data, &GitHubApi, &mut Vec<String>)] = &[validate_github_usernames];
+
pub(crate) fn validate(data: &Data) -> Result<(), Error> {
let mut errors = Vec::new();
@@ -30,12 +34,24 @@ pub(crate) fn validate(data: &Data) -> Result<(), Error> {
check(data, &mut errors);
}
+ match GitHubApi::new() {
+ Ok(github) => {
+ for check in GITHUB_CHECKS {
+ check(data, &github, &mut errors);
+ }
+ }
+ Err(err) => {
+ warn!("couldn't perform checks relying on the GitHub API, some errors will not be detected");
+ warn!("cause: {}", err);
+ }
+ }
+
if !errors.is_empty() {
errors.sort();
errors.dedup_by(|a, b| a == b);
for err in &errors {
- eprintln!("validation error: {}", err);
+ error!("validation error: {}", err);
}
bail!("{} validation errors found", errors.len());
@@ -363,6 +379,24 @@ fn validate_team_names(data: &Data, errors: &mut Vec<String>) {
});
}
+/// Ensure there are no misspelled GitHub account names
+fn validate_github_usernames(data: &Data, github: &GitHubApi, errors: &mut Vec<String>) {
+ let people = data
+ .people()
+ .map(|p| (p.github_id(), p))
+ .collect::<HashMap<_, _>>();
+ match github.usernames(&people.keys().cloned().collect::<Vec<_>>()) {
+ Ok(res) => wrapper(res.iter(), errors, |(id, name), _| {
+ let original = people[id].github();
+ if original != name {
+ bail!("user `{}` changed username to `{}`", original, name);
+ }
+ Ok(())
+ }),
+ Err(err) => errors.push(format!("couldn't verify GitHub usernames: {}", err)),
+ }
+}
+
fn wrapper<T, I, F>(iter: I, errors: &mut Vec<String>, mut func: F)
where
I: Iterator<Item = T>,