diff --git a/Cargo.lock b/Cargo.lock index 76b38b5..e2fd3cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,21 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "futures" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.5" @@ -123,6 +138,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -131,6 +147,35 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" +[[package]] +name = "futures-executor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" + +[[package]] +name = "futures-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.5" @@ -142,6 +187,9 @@ name = "futures-task" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" +dependencies = [ + "once_cell", +] [[package]] name = "futures-util" @@ -149,10 +197,18 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project", "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", ] [[package]] @@ -170,7 +226,9 @@ dependencies = [ name = "gh-stack" version = "0.1.0" dependencies = [ + "futures", "reqwest", + "serde", "tokio", ] @@ -472,6 +530,12 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" + [[package]] name = "openssl" version = "0.10.29" @@ -555,6 +619,18 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" +[[package]] +name = "proc-macro-hack" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4" + +[[package]] +name = "proc-macro-nested" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" + [[package]] name = "proc-macro2" version = "1.0.18" @@ -708,6 +784,20 @@ name = "serde" version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9124df5b40cbd380080b2cc6ab894c040a3070d995f5c9dc77e18c34a8ae37d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2c3ac8e6ca1e9c80b8be1023940162bf81ae3cffbb1809474152f2ce1eb250" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" diff --git a/Cargo.toml b/Cargo.toml index b1e9292..e15f57b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,5 @@ edition = "2018" [dependencies] reqwest = { version = "0.10.6", features = ["json"] } tokio = { version = "0.2", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +futures = "0.3.5" diff --git a/src/api/mod.rs b/src/api/mod.rs index e092c24..41f6bd8 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1 +1,13 @@ -pub mod search; \ No newline at end of file +use crate::Credentials; +use reqwest::{Client, RequestBuilder}; +use std::time::Duration; + +pub mod search; + +fn base_request(client: &Client, credentials: &Credentials, url: &str) -> RequestBuilder { + client + .get(url) + .timeout(Duration::from_secs(5)) + .header("Authorization", format!("token {}", credentials.token)) + .header("User-Agent", "timothyandrew/gh-stack") +} diff --git a/src/api/search.rs b/src/api/search.rs index 2e5122c..4830baa 100644 --- a/src/api/search.rs +++ b/src/api/search.rs @@ -1,18 +1,75 @@ +use futures::future::join_all; +use serde::Deserialize; use std::error::Error; -use crate::Credentials; +use crate::{api, Credentials}; + +#[derive(Deserialize, Debug)] +pub struct SearchItem { + url: String, + title: String, +} + +#[derive(Deserialize, Debug)] +pub struct PullRequestRef { + label: String, + r#ref: String, + sha: String, +} + +#[derive(Deserialize, Debug)] +pub struct PullRequest { + id: usize, + head: PullRequestRef, + base: PullRequestRef, + title: String, +} + +impl PullRequest { + pub fn head(&self) -> &str { + &self.head.label + } + + pub fn base(&self) -> &str { + &self.base.label + } +} + +#[derive(Deserialize, Debug)] +struct SearchResponse { + items: Vec, +} pub async fn fetch_pull_requests_matching( - pattern: &str, - credentials: &Credentials, -) -> Result<(), Box> { - let client = reqwest::Client::new(); - let request = client - .get("https://api.github.com/search/issues") - .query(&[("q", format!("{} in:title", pattern))]) - .header("Authorization", format!("token {}", credentials.token)) - .header("User-Agent", "timothyandrew/gh-stack"); - let response = request.send().await?.text().await?; - println!("{}", response); - Ok(()) -} \ No newline at end of file + pattern: &str, + credentials: &Credentials, +) -> Result, Box> { + let client = reqwest::Client::new(); + + let request = api::base_request( + &client, + &credentials, + "https://api.github.com/search/issues", + ) + .query(&[("q", format!("{} in:title", pattern))]); + + let items = request.send().await?.json::().await?.items; + + let item_futures = items.into_iter().map(|item| { + api::base_request(&client, &credentials, &item.url.replace("issues", "pulls")).send() + }); + + // The `unwrap`s are required here because both `reqwest::send` and `reqwest::json` return a `Result` which has + // to be unwrapped after the future has been `await`ed on. + let items = join_all(item_futures) + .await + .into_iter() + .map(|item| item.unwrap()); + let responses: Vec<_> = join_all(items.map(|item| item.json::())) + .await + .into_iter() + .map(|item| item.unwrap()) + .collect(); + + Ok(responses) +} diff --git a/src/graph.rs b/src/graph.rs new file mode 100644 index 0000000..e871e40 --- /dev/null +++ b/src/graph.rs @@ -0,0 +1,34 @@ +use std::collections::{HashMap, HashSet}; + +use crate::api::search::PullRequest; + +pub fn build(prs: &[PullRequest]) { + let mut heads = HashSet::new(); + let mut prs_by_base = HashMap::new(); + + for pr in prs.iter() { + heads.insert(pr.head()); + let entry = prs_by_base.entry(pr.base()).or_insert(Vec::new()); + entry.push(pr); + } + + let roots: Vec<&PullRequest> = prs.iter().filter(|pr| !heads.contains(pr.base())).collect(); + let results = resolve(&roots, &prs_by_base); +} + +fn resolve<'a>( + roots: &Vec<&'a PullRequest>, + prs_by_base: &'a HashMap<&str, Vec<&PullRequest>> +) -> Vec<&'a PullRequest> { + let mut results = Vec::new(); + + for &root in roots.iter() { + results.push(root); + if let Some(children) = prs_by_base.get(root.head()) { + let mut children = resolve(children, prs_by_base); + results.append(&mut children); + } + } + + results +} diff --git a/src/lib.rs b/src/lib.rs index 7c5472e..841d60f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ - pub mod api; +pub mod graph; pub struct Credentials { // Personal access token diff --git a/src/main.rs b/src/main.rs index a816fee..7d2dd50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ -use std::error::Error; -use std::env; use std::collections::HashMap; +use std::env; +use std::error::Error; use std::process; use gh_stack::api; +use gh_stack::graph; use gh_stack::Credentials; #[tokio::main] @@ -17,10 +18,14 @@ async fn main() -> Result<(), Box> { } let pattern = args.last().unwrap(); - let token = env.get("GHSTACK_OAUTH_TOKEN").expect("You didn't pass `GHSTACK_OAUTH_TOKEN`"); + let token = env + .get("GHSTACK_OAUTH_TOKEN") + .expect("You didn't pass `GHSTACK_OAUTH_TOKEN`"); let credentials = Credentials::new(token); - api::search::fetch_pull_requests_matching(&pattern, &credentials).await?; + + let prs = api::search::fetch_pull_requests_matching(&pattern, &credentials).await?; + graph::build(&prs); Ok(()) /*