1use slog::Logger;
2use std::collections::HashMap;
3
4use codealong::{AuthorConfig, Config, Identity, RepoEntry, RepoInfo, WorkspaceConfig};
5
6use crate::client::Client;
7use crate::cursor::Cursor;
8use crate::error::{retry_when_rate_limited, Result};
9use crate::repo::Repo;
10use crate::team::Team;
11use crate::user::User;
12
13pub fn config_from_org(
17 client: &Client,
18 github_org: &str,
19 logger: &Logger,
20) -> Result<WorkspaceConfig> {
21 let config = default_config_with_authors(client, github_org, logger)?;
22 let repos = build_repo_entries(client, github_org, logger)?;
23 Ok(WorkspaceConfig { config, repos })
24}
25
26fn default_config_with_authors(
27 client: &Client,
28 github_org: &str,
29 logger: &Logger,
30) -> Result<Config> {
31 let all_teams = get_all_teams(client, github_org, logger)?;
32 let url = format!("https://api.github.com/orgs/{}/members", github_org);
33 let cursor: Cursor<User> = Cursor::new(&client, &url, &logger);
34 let mut config = Config::default();
35 for user in cursor {
36 let teams = all_teams.get(&user.login);
37 add_user_to_config(&client, &mut config, user, teams, logger)?;
38 }
39 Ok(config)
40}
41
42fn get_all_teams(
43 client: &Client,
44 github_org: &str,
45 logger: &Logger,
46) -> Result<HashMap<String, Vec<Team>>> {
47 let url = format!("https://api.github.com/orgs/{}/teams", github_org);
48 let cursor: Cursor<Team> = Cursor::new(&client, &url, logger);
49 let mut res: HashMap<String, Vec<Team>> = HashMap::new();
50 for team in cursor {
51 let url = format!("https://api.github.com/teams/{}/members", &team.id);
52 let cursor: Cursor<User> = Cursor::new(&client, &url, logger);
53 for user in cursor {
54 let teams = res.entry(user.login).or_insert_with(|| Vec::new());
55 teams.push(team.clone());
56 }
57 }
58 Ok(res)
59}
60
61fn add_user_to_config(
62 client: &Client,
63 config: &mut Config,
64 mut user: User,
65 teams: Option<&Vec<Team>>,
66 logger: &Logger,
67) -> Result<()> {
68 augment_with_search_data(client, &mut user, logger)?;
69
70 let formatted_teams = teams
71 .map(|teams| teams.iter().map(|team| team.name.clone()).collect())
72 .unwrap_or_else(|| Vec::new());
73
74 let author_config = AuthorConfig {
75 github_logins: vec![user.login.clone()],
76 teams: formatted_teams,
77 ..Default::default()
78 };
79
80 let key = if user.email.is_some() || user.name.is_some() {
83 Identity {
84 name: user.name,
85 email: user.email,
86 }
87 .to_string()
88 } else {
89 user.login
90 };
91
92 config.authors.insert(key, author_config);
93 Ok(())
94}
95
96fn augment_with_search_data(client: &Client, user: &mut User, logger: &Logger) -> Result<()> {
98 let url = format!(
99 "https://api.github.com/search/commits?q=author:{}",
100 &user.login
101 );
102 let mut resp = retry_when_rate_limited(
103 &mut || client.get_with_content_type(&url, "application/vnd.github.cloak-preview"),
104 Some(&mut |seconds| warn!(logger, "Rate limit reached, sleeping {} seconds", seconds)),
105 )?;
106 let results = resp.json::<SearchResults>()?;
107
108 if let Some(r) = results.items.first() {
109 user.email = user.email.take().or_else(|| r.commit.author.email.clone());
110 user.name = user.name.take().or_else(|| r.commit.author.name.clone());
111 }
112 Ok(())
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116struct SearchResults {
117 items: Vec<SearchResult>,
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121struct SearchResult {
122 commit: Commit,
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126struct Commit {
127 author: Signature,
128}
129
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131struct Signature {
132 pub name: Option<String>,
133 pub email: Option<String>,
134}
135
136fn build_repo_entries(
137 client: &Client,
138 github_org: &str,
139 logger: &Logger,
140) -> Result<Vec<RepoEntry>> {
141 let url = format!("https://api.github.com/orgs/{}/repos", github_org);
142 let cursor: Cursor<Repo> = Cursor::new(&client, &url, logger);
143 let res = cursor.map(|repo| RepoEntry {
144 repo_info: RepoInfo {
145 name: repo.full_name.clone(),
146 github_name: Some(repo.full_name.clone()),
147 clone_url: repo.ssh_url,
148 fork: repo.fork,
149 ..Default::default()
150 },
151 path: Some(format!("{}.git", repo.full_name)),
152 ignore: false,
153 });
154 Ok(res.collect())
155}
156
157#[cfg(test)]
158mod test {
159 use super::*;
160 use codealong::test::build_test_logger;
161
162 #[test]
163 fn test_config_from_org() -> Result<()> {
164 let client = Client::from_env();
165 let workspace_config = config_from_org(&client, "codealong", &build_test_logger())?;
166 assert!(workspace_config.config.authors.len() >= 1);
167 assert!(workspace_config.repos.len() >= 1);
168 assert_eq!(
169 workspace_config
170 .config
171 .authors
172 .iter()
173 .next()
174 .unwrap()
175 .1
176 .tags,
177 vec!["team:Devs".to_owned(), "team:Ninjas".to_owned()]
178 );
179 Ok(())
180 }
181}