use slog::Logger;
use std::collections::HashMap;
use codealong::{AuthorConfig, Config, Identity, RepoEntry, RepoInfo, WorkspaceConfig};
use crate::client::Client;
use crate::cursor::Cursor;
use crate::error::{retry_when_rate_limited, Result};
use crate::repo::Repo;
use crate::team::Team;
use crate::user::User;
pub fn config_from_org(
client: &Client,
github_org: &str,
logger: &Logger,
) -> Result<WorkspaceConfig> {
let config = default_config_with_authors(client, github_org, logger)?;
let repos = build_repo_entries(client, github_org, logger)?;
Ok(WorkspaceConfig { config, repos })
}
fn default_config_with_authors(
client: &Client,
github_org: &str,
logger: &Logger,
) -> Result<Config> {
let all_teams = get_all_teams(client, github_org, logger)?;
let url = format!("https://api.github.com/orgs/{}/members", github_org);
let cursor: Cursor<User> = Cursor::new(&client, &url, &logger);
let mut config = Config::default();
for user in cursor {
let teams = all_teams.get(&user.login);
add_user_to_config(&client, &mut config, user, teams, logger)?;
}
Ok(config)
}
fn get_all_teams(
client: &Client,
github_org: &str,
logger: &Logger,
) -> Result<HashMap<String, Vec<Team>>> {
let url = format!("https://api.github.com/orgs/{}/teams", github_org);
let cursor: Cursor<Team> = Cursor::new(&client, &url, logger);
let mut res: HashMap<String, Vec<Team>> = HashMap::new();
for team in cursor {
let url = format!("https://api.github.com/teams/{}/members", &team.id);
let cursor: Cursor<User> = Cursor::new(&client, &url, logger);
for user in cursor {
let teams = res.entry(user.login).or_insert_with(|| Vec::new());
teams.push(team.clone());
}
}
Ok(res)
}
fn add_user_to_config(
client: &Client,
config: &mut Config,
mut user: User,
teams: Option<&Vec<Team>>,
logger: &Logger,
) -> Result<()> {
augment_with_search_data(client, &mut user, logger)?;
let formatted_teams = teams
.map(|teams| teams.iter().map(|team| team.name.clone()).collect())
.unwrap_or_else(|| Vec::new());
let author_config = AuthorConfig {
github_logins: vec![user.login.clone()],
teams: formatted_teams,
..Default::default()
};
let key = if user.email.is_some() || user.name.is_some() {
Identity {
name: user.name,
email: user.email,
}
.to_string()
} else {
user.login
};
config.authors.insert(key, author_config);
Ok(())
}
fn augment_with_search_data(client: &Client, user: &mut User, logger: &Logger) -> Result<()> {
let url = format!(
"https://api.github.com/search/commits?q=author:{}",
&user.login
);
let mut resp = retry_when_rate_limited(
&mut || client.get_with_content_type(&url, "application/vnd.github.cloak-preview"),
Some(&mut |seconds| warn!(logger, "Rate limit reached, sleeping {} seconds", seconds)),
)?;
let results = resp.json::<SearchResults>()?;
if let Some(r) = results.items.first() {
user.email = user.email.take().or_else(|| r.commit.author.email.clone());
user.name = user.name.take().or_else(|| r.commit.author.name.clone());
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct SearchResults {
items: Vec<SearchResult>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct SearchResult {
commit: Commit,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct Commit {
author: Signature,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct Signature {
pub name: Option<String>,
pub email: Option<String>,
}
fn build_repo_entries(
client: &Client,
github_org: &str,
logger: &Logger,
) -> Result<Vec<RepoEntry>> {
let url = format!("https://api.github.com/orgs/{}/repos", github_org);
let cursor: Cursor<Repo> = Cursor::new(&client, &url, logger);
let res = cursor.map(|repo| RepoEntry {
repo_info: RepoInfo {
name: repo.full_name.clone(),
github_name: Some(repo.full_name.clone()),
clone_url: repo.ssh_url,
fork: repo.fork,
..Default::default()
},
path: Some(format!("{}.git", repo.full_name)),
ignore: false,
});
Ok(res.collect())
}
#[cfg(test)]
mod test {
use super::*;
use codealong::test::build_test_logger;
#[test]
fn test_config_from_org() -> Result<()> {
let client = Client::from_env();
let workspace_config = config_from_org(&client, "codealong", &build_test_logger())?;
assert!(workspace_config.config.authors.len() >= 1);
assert!(workspace_config.repos.len() >= 1);
assert_eq!(
workspace_config
.config
.authors
.iter()
.next()
.unwrap()
.1
.tags,
vec!["team:Devs".to_owned(), "team:Ninjas".to_owned()]
);
Ok(())
}
}