diff --git a/src/api/mod.rs b/src/api/mod.rs index 41f6bd8..94d9e93 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -3,6 +3,7 @@ use reqwest::{Client, RequestBuilder}; use std::time::Duration; pub mod search; +pub mod pull_request; fn base_request(client: &Client, credentials: &Credentials, url: &str) -> RequestBuilder { client @@ -11,3 +12,11 @@ fn base_request(client: &Client, credentials: &Credentials, url: &str) -> Reques .header("Authorization", format!("token {}", credentials.token)) .header("User-Agent", "timothyandrew/gh-stack") } + +fn base_patch_request(client: &Client, credentials: &Credentials, url: &str) -> RequestBuilder { + client + .patch(url) + .timeout(Duration::from_secs(5)) + .header("Authorization", format!("token {}", credentials.token)) + .header("User-Agent", "timothyandrew/gh-stack") +} \ No newline at end of file diff --git a/src/api/pull_request.rs b/src/api/pull_request.rs new file mode 100644 index 0000000..1030417 --- /dev/null +++ b/src/api/pull_request.rs @@ -0,0 +1,20 @@ +use std::error::Error; +use serde::{Serialize}; +use std::rc::Rc; + +use crate::{Credentials, api}; +use crate::api::search::PullRequest; + + +#[derive(Serialize, Debug)] +struct UpdateDescriptionRequest<'a> { + body: &'a str +} + +pub async fn update_description(description: String, pr: Rc, c: &Credentials) -> Result<(), Box> { + let client = reqwest::Client::new(); + let body = UpdateDescriptionRequest { body: &description }; + let request = api::base_patch_request(&client, &c, pr.url()).json(&body); + request.send().await?; + Ok(()) +} \ No newline at end of file diff --git a/src/api/search.rs b/src/api/search.rs index c38a99b..91cc027 100644 --- a/src/api/search.rs +++ b/src/api/search.rs @@ -24,6 +24,8 @@ pub struct PullRequest { head: PullRequestRef, base: PullRequestRef, title: String, + url: String, + body: String } impl PullRequest { @@ -35,12 +37,20 @@ impl PullRequest { &self.base.label } + pub fn url(&self) -> &str { + &self.url + } + pub fn number(&self) -> usize { self.number } pub fn title(&self) -> &str { &self.title } + + pub fn body(&self) -> &str { + &self.body + } } #[derive(Deserialize, Debug)] diff --git a/src/graph.rs b/src/graph.rs index 31b0a09..6bac1d6 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use crate::api::search::PullRequest; -pub fn build(prs: Vec>) -> Graph, usize> { +pub fn build(prs: &Vec>) -> Graph, usize> { let mut tree = Graph::, usize>::new(); let heads = prs.iter().map(|pr| pr.head()); let handles: Vec<_> = prs.iter().map(|pr| tree.add_node(pr.clone())).collect(); @@ -18,4 +18,4 @@ pub fn build(prs: Vec>) -> Graph, usize> { } tree -} +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index acb7cc6..24cf1cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod api; pub mod graph; pub mod markdown; +pub mod persist; pub struct Credentials { // Personal access token diff --git a/src/main.rs b/src/main.rs index 6b0123c..a5de6a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,11 +4,21 @@ use std::error::Error; use std::process; use std::rc::Rc; -use gh_stack::api; -use gh_stack::graph; -use gh_stack::markdown; +use gh_stack::{api, persist, graph, markdown}; use gh_stack::Credentials; +use std::io::{self, Write}; + +pub fn read_cli_input(message: &str) -> String { + print!("{}", message); + io::stdout().flush().unwrap(); + + let mut buf = String::new(); + io::stdin().read_line(&mut buf).unwrap(); + + buf.trim().to_owned() +} + #[tokio::main] async fn main() -> Result<(), Box> { let env: HashMap = env::vars().collect(); @@ -28,10 +38,23 @@ async fn main() -> Result<(), Box> { let prs = api::search::fetch_pull_requests_matching(&pattern, &credentials).await?; let prs = prs.into_iter().map(|pr| Rc::new(pr)).collect(); - let tree = graph::build(prs); - let table = markdown::build_table(tree); - println!("{}", table); + let tree = graph::build(&prs); + let table = markdown::build_table(tree, pattern); + for pr in prs.iter() { + println!("{}: {}", pr.number(), pr.title()); + } + + let response = read_cli_input("Going to update these PRs ☝️ (y/n): "); + match &response[..] { + "y" => persist::persist(&prs, &table, &credentials).await?, + _ => std::process::exit(1) + } + + persist::persist(&prs, &table, &credentials).await?; + + println!("Done!"); + Ok(()) /* # TODO diff --git a/src/markdown.rs b/src/markdown.rs index bb23969..15858e9 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -12,9 +12,9 @@ fn process(row: String) -> String { regex.replace_all(&row, "").into_owned() } -pub fn build_table(graph: Graph, usize>) -> String { +pub fn build_table(graph: Graph, usize>, title: &str) -> String { let mut out = String::new(); - out.push_str("## Stacked PR Chain"); + out.push_str(&format!("### Stacked PR Chain: {}\n", title)); out.push_str("| PR | Title | Merges Into |\n"); out.push_str("|:--:|:------|:-------------:|\n"); diff --git a/src/persist.rs b/src/persist.rs new file mode 100644 index 0000000..50a5432 --- /dev/null +++ b/src/persist.rs @@ -0,0 +1,41 @@ +use regex::Regex; +use std::error::Error; +use futures::future::join_all; +use std::rc::Rc; + +use crate::api::search::PullRequest; +use crate::Credentials; +use crate::api::pull_request; + +const SHIELD_OPEN: &str = ""; +const SHIELD_CLOSE: &str = ""; + +fn safe_replace(body: &str, table: &str) -> String { + let new = format!("\n{}\n{}\n{}\n", SHIELD_OPEN, table, SHIELD_CLOSE); + + if body.contains(SHIELD_OPEN) { + let matcher = format!("(?s){}.*{}", regex::escape(SHIELD_OPEN), regex::escape(SHIELD_CLOSE)); + let re = Regex::new(&matcher).unwrap(); + re.replace_all(body, &new[..]).into_owned() + } else { + let mut body: String = body.to_owned(); + body.push_str(&new); + body + } +} + +pub async fn persist(prs: &Vec>, table: &str, c: &Credentials) -> Result<(), Box> { + let futures = prs.iter().map(|pr| { + let description = safe_replace(pr.body(), table); + pull_request::update_description(description, pr.clone(), c) + }); + + let results = join_all(futures.collect::>()).await; + + for result in results { + result.unwrap(); + } + + Ok(()) + +} \ No newline at end of file