Skip to content

Commit bf4fe84

Browse files
authored
Merge pull request #63 from pnkfelix/use-github-api
Add support for Github API for querying repository info
2 parents 1ed199d + b579487 commit bf4fe84

File tree

6 files changed

+373
-63
lines changed

6 files changed

+373
-63
lines changed

Cargo.lock

Lines changed: 23 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ git2 = "0.11"
2121
log = "0.4"
2222
pbr = "1.0.2"
2323
regex = "1.3.4"
24-
reqwest = { version = "0.10.2", features = ["blocking"] }
24+
reqwest = { version = "0.10.2", features = ["blocking", "json"] }
2525
rustc_version = "0.2"
26+
serde = { version = "1.0.104", features = ["derive"] }
27+
serde_json = "1.0"
2628
structopt = "0.3.9"
2729
tar = "0.4"
2830
tee = "0.1"

src/git.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,13 @@ const RUST_SRC_REPO: Option<&str> = option_env!("RUST_SRC_REPO");
1212

1313
use std::path::Path;
1414

15-
use chrono::{DateTime, TimeZone, Utc};
15+
use chrono::{TimeZone, Utc};
1616
use failure::{bail, Error};
1717
use git2::build::RepoBuilder;
1818
use git2::{Commit as Git2Commit, Repository};
1919
use log::debug;
2020

21-
#[derive(Debug, Clone, PartialEq)]
22-
pub struct Commit {
23-
pub sha: String,
24-
pub date: DateTime<Utc>,
25-
pub summary: String,
26-
}
21+
use crate::Commit;
2722

2823
impl Commit {
2924
// Takes &mut because libgit2 internally caches summaries
@@ -67,10 +62,10 @@ fn get_repo() -> Result<Repository, Error> {
6762
}
6863
}
6964

70-
pub fn expand_commit(sha: &str) -> Result<String, Error> {
65+
pub(crate) fn get_commit(sha: &str) -> Result<Commit, Error> {
7166
let repo = get_repo()?;
72-
let rev = lookup_rev(&repo, sha)?;
73-
Ok(rev.id().to_string())
67+
let mut rev = lookup_rev(&repo, sha)?;
68+
Ok(Commit::from_git2_commit(&mut rev))
7469
}
7570

7671
/// Returns the bors merge commits between the two specified boundaries

src/github.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use failure::Error;
2+
use reqwest::{self, blocking::Client, blocking::Response};
3+
use serde::{Deserialize, Serialize};
4+
5+
use crate::Commit;
6+
7+
#[derive(Serialize, Deserialize, Debug)]
8+
struct GithubCommitElem { commit: GithubCommit, sha: String }
9+
#[derive(Serialize, Deserialize, Debug)]
10+
struct GithubCommit { author: GithubAuthor, committer: GithubAuthor, message: String, }
11+
#[derive(Serialize, Deserialize, Debug)]
12+
struct GithubAuthor { date: String, email: String, name: String }
13+
14+
type GitDate = chrono::DateTime<chrono::Utc>;
15+
16+
impl GithubCommitElem {
17+
fn date(&self) -> Result<GitDate, Error> {
18+
Ok(self.commit.committer.date.parse()?)
19+
}
20+
21+
fn git_commit(self) -> Result<Commit, Error> {
22+
let date = self.date()?;
23+
Ok(Commit {
24+
sha: self.sha,
25+
date,
26+
summary: self.commit.message,
27+
})
28+
}
29+
}
30+
31+
fn headers() -> Result<reqwest::header::HeaderMap, Error> {
32+
let mut headers = reqwest::header::HeaderMap::new();
33+
let user_agent = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
34+
let user_agent = reqwest::header::HeaderValue::from_static(user_agent);
35+
headers.insert(reqwest::header::USER_AGENT, user_agent);
36+
if let Ok(token) = std::env::var("GITHUB_TOKEN") {
37+
eprintln!("adding local env GITHUB_TOKEN value to headers in github query");
38+
let value = reqwest::header::HeaderValue::from_str(&format!("token {}", token))?;
39+
headers.insert(reqwest::header::AUTHORIZATION, value);
40+
}
41+
Ok(headers)
42+
}
43+
44+
pub(crate) fn get_commit(sha: &str) -> Result<Commit, Error> {
45+
let url = SingleCommitUrl { sha }.url();
46+
let client = Client::builder()
47+
.default_headers(headers()?)
48+
.build()?;
49+
let response: Response = client.get(&url).send()?;
50+
let elem: GithubCommitElem = response.json()?;
51+
elem.git_commit()
52+
}
53+
54+
#[derive(Copy, Clone, Debug)]
55+
pub(crate) struct CommitsQuery<'a> {
56+
pub since_date: &'a str,
57+
pub most_recent_sha: &'a str,
58+
pub earliest_sha: &'a str,
59+
}
60+
61+
/// Returns the bors merge commits between the two specified boundaries
62+
/// (boundaries inclusive).
63+
64+
impl<'a> CommitsQuery<'a> {
65+
pub fn get_commits(&self) -> Result<Vec<Commit>, Error> {
66+
get_commits(*self)
67+
}
68+
}
69+
70+
const PER_PAGE: usize = 100;
71+
const OWNER: &'static str = "rust-lang";
72+
const REPO: &'static str = "rust";
73+
74+
75+
trait ToUrl { fn url(&self) -> String; }
76+
struct CommitsUrl<'a> { page: usize, author: &'a str, since: &'a str, sha: &'a str }
77+
struct SingleCommitUrl<'a> { sha: &'a str }
78+
79+
impl<'a> ToUrl for CommitsUrl<'a> {
80+
fn url(&self) -> String {
81+
format!("https://api.github.com/repos/{OWNER}/{REPO}/commits\
82+
?page={PAGE}&per_page={PER_PAGE}\
83+
&author={AUTHOR}&since={SINCE}&sha={SHA}",
84+
OWNER=OWNER, REPO=REPO,
85+
PAGE=self.page, PER_PAGE=PER_PAGE,
86+
AUTHOR=self.author, SINCE=self.since, SHA=self.sha)
87+
}
88+
}
89+
90+
impl<'a> ToUrl for SingleCommitUrl<'a> {
91+
fn url(&self) -> String {
92+
format!("https://api.github.com/repos/{OWNER}/{REPO}/commits/{REF}",
93+
OWNER=OWNER, REPO=REPO, REF=self.sha)
94+
}
95+
}
96+
97+
fn get_commits(q: CommitsQuery) -> Result<Vec<Commit>, Error> {
98+
// build up commit sequence, by feeding in `sha` as the starting point, and
99+
// working way backwards to max(`q.since_date`, `q.earliest_sha`).
100+
let mut commits = Vec::new();
101+
102+
// focus on Pull Request merges, all authored and committed by bors.
103+
let author = "bors";
104+
105+
let client = Client::builder()
106+
.default_headers(headers()?)
107+
.build()?;
108+
for page in 1.. {
109+
let url = CommitsUrl { page, author, since: q.since_date, sha: q.most_recent_sha }.url();
110+
111+
let response: Response = client.get(&url).send()?;
112+
113+
let action = parse_paged_elems(response, |elem: GithubCommitElem| {
114+
let date: chrono::DateTime<chrono::Utc> = match elem.commit.committer.date.parse() {
115+
Ok(date) => date,
116+
Err(err) => return Loop::Err(err.into()),
117+
};
118+
let sha = elem.sha.clone();
119+
let summary = elem.commit.message;
120+
let commit = Commit { sha, date, summary };
121+
commits.push(commit);
122+
123+
if elem.sha == q.earliest_sha {
124+
eprintln!("ending github query because we found starting sha: {}", elem.sha);
125+
return Loop::Break;
126+
}
127+
128+
Loop::Next
129+
})?;
130+
131+
if let Loop::Break = action { break; }
132+
}
133+
134+
eprintln!("get_commits_between returning commits, len: {}", commits.len());
135+
136+
// reverse to obtain chronological order
137+
commits.reverse();
138+
Ok(commits)
139+
}
140+
141+
enum Loop<E> { Break, Next, Err(E) }
142+
enum Void { }
143+
144+
fn parse_paged_elems<Elem: for<'a> serde::Deserialize<'a>>(response: Response,
145+
mut k: impl FnMut(Elem) -> Loop<Error>)
146+
-> Result<Loop<Void>, Error>
147+
{
148+
// parse the JSON into an array of the expected Elem type
149+
let elems: Vec<Elem> = response.json()?;
150+
151+
// if `elems` is empty, then we've run out of useful pages to lookup.
152+
if elems.len() == 0 { return Ok(Loop::Break); }
153+
154+
for elem in elems.into_iter() {
155+
let act = k(elem);
156+
157+
// the callback will tell us if we should terminate loop early (e.g. due to matching `sha`)
158+
match act {
159+
Loop::Break => return Ok(Loop::Break),
160+
Loop::Err(e) => return Err(e),
161+
Loop::Next => continue,
162+
}
163+
}
164+
165+
// by default, we keep searching on next page from github.
166+
Ok(Loop::Next)
167+
}

0 commit comments

Comments
 (0)