with actual review functionality

This commit is contained in:
Kasper Juul Hermansen 2023-01-11 22:39:42 +01:00
parent e4e33ebda2
commit ab464fbb6b
Signed by: kjuulh
GPG Key ID: 57B6E1465221F912
5 changed files with 269 additions and 13 deletions

1
Cargo.lock generated
View File

@ -225,6 +225,7 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"serde", "serde",
"serde_json", "serde_json",
"thiserror",
"util", "util",
] ]

View File

@ -17,6 +17,7 @@ serde_json = "1.0.91"
comfy-table = "6.1.4" comfy-table = "6.1.4"
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"
base64 = "0.21.0" base64 = "0.21.0"
thiserror = "1.0.38"
[dev-dependencies] [dev-dependencies]
mockall = "0.11.2" mockall = "0.11.2"

View File

@ -1,8 +1,10 @@
use crate::review_backend::{models::PullRequest, DefaultReviewBackend, DynReviewBackend}; use crate::review_backend::{
models::{MenuChoice, PullRequest, ReviewMenuChoice},
DefaultReviewBackend, DynReviewBackend,
};
use comfy_table::{presets::UTF8_HORIZONTAL_ONLY, Cell, Row, Table}; use comfy_table::{presets::UTF8_HORIZONTAL_ONLY, Cell, Table};
#[cfg(test)] use thiserror::Error;
use mockall::{automock, mock, predicate::*};
pub struct Review { pub struct Review {
backend: DynReviewBackend, backend: DynReviewBackend,
@ -14,6 +16,12 @@ impl Default for Review {
} }
} }
#[derive(Debug, Error)]
pub enum ReviewErrors {
#[error("user chose to exit")]
UserExit,
}
impl Review { impl Review {
fn new(backend: DynReviewBackend) -> Self { fn new(backend: DynReviewBackend) -> Self {
Self { backend } Self { backend }
@ -27,12 +35,25 @@ impl Review {
/// 5. Approve, open, skip or quit /// 5. Approve, open, skip or quit
/// 6. Repeat from 4 /// 6. Repeat from 4
fn run(&self, review_requested: Option<String>) -> eyre::Result<()> { fn run(&self, review_requested: Option<String>) -> eyre::Result<()> {
let prs = self.backend.get_prs(review_requested)?; let prs = self.backend.get_prs(review_requested.clone())?;
let prs_table = Self::generate_prs_table(&prs); let prs_table = Self::generate_prs_table(&prs);
self.backend.present_prs(prs_table)?; self.backend.present_prs(prs_table)?;
match self.backend.present_menu()? {
MenuChoice::Exit => eyre::bail!(ReviewErrors::UserExit),
MenuChoice::Begin => match self.review(&prs)? {
Some(choice) => match choice {
MenuChoice::Exit => eyre::bail!(ReviewErrors::UserExit),
MenuChoice::List => return self.run(review_requested.clone()),
_ => eyre::bail!("invalid choice"),
},
None => {}
},
MenuChoice::Search => todo!(),
MenuChoice::List => return self.run(review_requested.clone()),
}
Ok(()) Ok(())
} }
@ -57,6 +78,63 @@ impl Review {
table.to_string() table.to_string()
} }
fn review(&self, prs: &Vec<PullRequest>) -> eyre::Result<Option<MenuChoice>> {
for pr in prs {
self.backend.clear()?;
self.backend.present_pr(pr)?;
self.review_pr(pr)?;
if let Some(choice) = self.present_pr_menu(pr)? {
return Ok(Some(choice));
}
}
Ok(None)
}
fn review_pr(&self, pr: &PullRequest) -> eyre::Result<()> {
self.backend.present_diff(pr)?;
Ok(())
}
fn approve(&self, pr: &PullRequest) -> eyre::Result<()> {
self.backend.approve(pr)?;
Ok(())
}
fn open_browser(&self, pr: &PullRequest) -> eyre::Result<Option<MenuChoice>> {
self.backend.pr_open_browser(pr)?;
self.present_pr_menu(pr)
}
fn present_pr_menu(&self, pr: &PullRequest) -> eyre::Result<Option<MenuChoice>> {
self.backend.present_pr(pr)?;
match self.backend.present_review_menu(pr)? {
ReviewMenuChoice::Exit => return Ok(Some(MenuChoice::Exit)),
ReviewMenuChoice::List => return Ok(Some(MenuChoice::List)),
ReviewMenuChoice::Approve => {
self.approve(pr)?;
return self.present_pr_menu(pr);
}
ReviewMenuChoice::Open => return self.open_browser(pr),
ReviewMenuChoice::Skip => {}
ReviewMenuChoice::Merge => self.merge(pr)?,
ReviewMenuChoice::ApproveAndMerge => {
self.approve(pr)?;
self.merge(pr)?;
}
}
Ok(None)
}
fn merge(&self, pr: &PullRequest) -> eyre::Result<()> {
self.backend.enable_auto_merge(pr);
Ok(())
}
} }
impl util::Cmd for Review { impl util::Cmd for Review {
@ -71,12 +149,16 @@ impl util::Cmd for Review {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::review_backend::{models::Repository, MockReviewBackend}; use crate::review_backend::{
models::{self, Repository},
MockReviewBackend,
};
use super::*; use super::*;
use base64::Engine; use base64::Engine;
use pretty_assertions::{assert_eq, assert_ne}; use mockall::predicate::eq;
use pretty_assertions::assert_eq;
#[test] #[test]
fn can_fetch_prs() { fn can_fetch_prs() {
@ -104,12 +186,17 @@ mod tests {
.times(1) .times(1)
.returning(move |_| Ok(backendprs.clone())); .returning(move |_| Ok(backendprs.clone()));
backend
.expect_present_menu()
.times(1)
.returning(|| Ok(models::MenuChoice::Exit));
backend.expect_present_prs().times(1).returning(|_| Ok(())); backend.expect_present_prs().times(1).returning(|_| Ok(()));
let review = Review::new(std::sync::Arc::new(backend)); let review = Review::new(std::sync::Arc::new(backend));
review let res = review.run(Some("kjuulh".into()));
.run(Some("kjuulh".into()))
.expect("to return a list of pull requests"); assert_err::<ReviewErrors, _>(res)
} }
#[test] #[test]
@ -156,4 +243,15 @@ mod tests {
assert_eq!(b64.encode(actual), snapshot); assert_eq!(b64.encode(actual), snapshot);
} }
fn assert_err<TExpected, TVal>(res: eyre::Result<TVal>) {
match res {
Err(e) => {
if !e.is::<ReviewErrors>() {
panic!("invalid error: {}", e)
}
}
_ => panic!("error not thrown"),
}
}
} }

View File

@ -1,6 +1,8 @@
pub mod models; pub mod models;
use self::models::PullRequest; use std::io::Write;
use self::models::{MenuChoice, PullRequest, ReviewMenuChoice};
#[cfg(test)] #[cfg(test)]
use mockall::{automock, predicate::*}; use mockall::{automock, predicate::*};
@ -8,6 +10,14 @@ use mockall::{automock, predicate::*};
pub trait ReviewBackend { pub trait ReviewBackend {
fn get_prs(&self, review_request: Option<String>) -> eyre::Result<Vec<PullRequest>>; fn get_prs(&self, review_request: Option<String>) -> eyre::Result<Vec<PullRequest>>;
fn present_prs(&self, table: String) -> eyre::Result<()>; fn present_prs(&self, table: String) -> eyre::Result<()>;
fn present_menu(&self) -> eyre::Result<MenuChoice>;
fn present_diff(&self, pr: &PullRequest) -> eyre::Result<()>;
fn present_review_menu(&self, pr: &PullRequest) -> eyre::Result<ReviewMenuChoice>;
fn approve(&self, pr: &PullRequest) -> eyre::Result<()>;
fn pr_open_browser(&self, pr: &PullRequest) -> eyre::Result<()>;
fn clear(&self) -> eyre::Result<()>;
fn enable_auto_merge(&self, pr: &PullRequest) -> eyre::Result<()>;
fn present_pr(&self, pr: &PullRequest) -> eyre::Result<()>;
} }
pub type DynReviewBackend = std::sync::Arc<dyn ReviewBackend + Send + Sync>; pub type DynReviewBackend = std::sync::Arc<dyn ReviewBackend + Send + Sync>;
@ -27,7 +37,7 @@ impl ReviewBackend for DefaultReviewBackend {
review_request.unwrap().as_str(), review_request.unwrap().as_str(),
"--label", "--label",
"dependencies", "dependencies",
"--checks=pending", //"--checks=pending",
"--json", "--json",
"repository,number,title", "repository,number,title",
], ],
@ -45,6 +55,135 @@ impl ReviewBackend for DefaultReviewBackend {
println!("{table}"); println!("{table}");
Ok(()) Ok(())
} }
fn present_menu(&self) -> eyre::Result<MenuChoice> {
println!("Menu");
println!("Begin (b), Exit (q), Menu (m), Search (s), List (l)");
print!("> ");
std::io::stdout().flush()?;
let mut raw_choice = String::new();
std::io::stdin().read_line(&mut raw_choice)?;
let choice = match raw_choice.chars().take(1).next() {
None => models::MenuChoice::Exit,
Some(raw_choice) => match raw_choice {
'b' => models::MenuChoice::Begin,
'q' => models::MenuChoice::Exit,
'm' => self.present_menu()?,
's' => models::MenuChoice::Search,
'l' => models::MenuChoice::List,
_ => self.present_menu()?,
},
};
Ok(choice)
}
fn present_diff(&self, pr: &PullRequest) -> eyre::Result<()> {
util::shell::run(
&[
"gh",
"pr",
"diff",
pr.number.to_string().as_str(),
"--repo",
pr.repository.name.as_str(),
],
None,
)?;
Ok(())
}
fn present_review_menu(&self, pr: &PullRequest) -> eyre::Result<ReviewMenuChoice> {
println!("");
println!("Review - Menu");
println!("Approve (a), Merge (m), Approve and auto-merge (c), Skip (s), List (l), Open in browser (o), Exit (q)");
print!("> ");
std::io::stdout().flush()?;
let mut raw_choice = String::new();
std::io::stdin().read_line(&mut raw_choice)?;
let choice = match raw_choice.chars().take(1).next() {
None => ReviewMenuChoice::Exit,
Some(raw_choice) => match raw_choice {
'q' => ReviewMenuChoice::Exit,
'l' => ReviewMenuChoice::List,
'a' => ReviewMenuChoice::Approve,
'o' => ReviewMenuChoice::Open,
's' | 'n' => ReviewMenuChoice::Skip,
'm' => ReviewMenuChoice::Merge,
'c' => ReviewMenuChoice::ApproveAndMerge,
_ => self.present_review_menu(pr)?,
},
};
Ok(choice)
}
fn approve(&self, pr: &PullRequest) -> eyre::Result<()> {
util::shell::run(
&[
"gh",
"pr",
"review",
pr.number.to_string().as_str(),
"--approve",
"--repo",
pr.repository.name.as_str(),
],
None,
)?;
Ok(())
}
fn pr_open_browser(&self, pr: &PullRequest) -> eyre::Result<()> {
util::shell::run(
&[
"gh",
"pr",
"view",
pr.number.to_string().as_str(),
"-w",
"--repo",
pr.repository.name.as_str(),
],
None,
)?;
Ok(())
}
fn clear(&self) -> eyre::Result<()> {
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
std::io::stdout().flush()?;
Ok(())
}
fn enable_auto_merge(&self, pr: &PullRequest) -> eyre::Result<()> {
util::shell::run(
&[
"gh",
"pr",
"merge",
pr.number.to_string().as_str(),
"--auto",
"--repo",
pr.repository.name.as_str(),
],
None,
)?;
Ok(())
}
fn present_pr(&self, pr: &PullRequest) -> eyre::Result<()> {
println!("repo: {} - title: {}", pr.repository.name, pr.title);
Ok(())
}
} }
impl DefaultReviewBackend { impl DefaultReviewBackend {

View File

@ -12,3 +12,20 @@ pub struct PullRequest {
pub number: usize, pub number: usize,
pub repository: Repository, pub repository: Repository,
} }
pub enum MenuChoice {
Exit,
Begin,
Search,
List,
}
pub enum ReviewMenuChoice {
Exit,
List,
Approve,
Open,
Skip,
Merge,
ApproveAndMerge,
}