feature/review #6
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -225,6 +225,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"util",
|
||||
]
|
||||
|
||||
|
@ -17,6 +17,7 @@ serde_json = "1.0.91"
|
||||
comfy-table = "6.1.4"
|
||||
pretty_assertions = "1.3.0"
|
||||
base64 = "0.21.0"
|
||||
thiserror = "1.0.38"
|
||||
|
||||
[dev-dependencies]
|
||||
mockall = "0.11.2"
|
||||
|
@ -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};
|
||||
#[cfg(test)]
|
||||
use mockall::{automock, mock, predicate::*};
|
||||
use comfy_table::{presets::UTF8_HORIZONTAL_ONLY, Cell, Table};
|
||||
use thiserror::Error;
|
||||
|
||||
pub struct Review {
|
||||
backend: DynReviewBackend,
|
||||
@ -14,6 +16,12 @@ impl Default for Review {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ReviewErrors {
|
||||
#[error("user chose to exit")]
|
||||
UserExit,
|
||||
}
|
||||
|
||||
impl Review {
|
||||
fn new(backend: DynReviewBackend) -> Self {
|
||||
Self { backend }
|
||||
@ -27,12 +35,25 @@ impl Review {
|
||||
/// 5. Approve, open, skip or quit
|
||||
/// 6. Repeat from 4
|
||||
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);
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -57,6 +78,63 @@ impl Review {
|
||||
|
||||
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 {
|
||||
@ -71,12 +149,16 @@ impl util::Cmd for Review {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::review_backend::{models::Repository, MockReviewBackend};
|
||||
use crate::review_backend::{
|
||||
models::{self, Repository},
|
||||
MockReviewBackend,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
use base64::Engine;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use mockall::predicate::eq;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn can_fetch_prs() {
|
||||
@ -104,12 +186,17 @@ mod tests {
|
||||
.times(1)
|
||||
.returning(move |_| Ok(backendprs.clone()));
|
||||
|
||||
backend
|
||||
.expect_present_menu()
|
||||
.times(1)
|
||||
.returning(|| Ok(models::MenuChoice::Exit));
|
||||
|
||||
backend.expect_present_prs().times(1).returning(|_| Ok(()));
|
||||
|
||||
let review = Review::new(std::sync::Arc::new(backend));
|
||||
review
|
||||
.run(Some("kjuulh".into()))
|
||||
.expect("to return a list of pull requests");
|
||||
let res = review.run(Some("kjuulh".into()));
|
||||
|
||||
assert_err::<ReviewErrors, _>(res)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -156,4 +243,15 @@ mod tests {
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
pub mod models;
|
||||
|
||||
use self::models::PullRequest;
|
||||
use std::io::Write;
|
||||
|
||||
use self::models::{MenuChoice, PullRequest, ReviewMenuChoice};
|
||||
|
||||
#[cfg(test)]
|
||||
use mockall::{automock, predicate::*};
|
||||
@ -8,6 +10,14 @@ use mockall::{automock, predicate::*};
|
||||
pub trait ReviewBackend {
|
||||
fn get_prs(&self, review_request: Option<String>) -> eyre::Result<Vec<PullRequest>>;
|
||||
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>;
|
||||
@ -27,7 +37,7 @@ impl ReviewBackend for DefaultReviewBackend {
|
||||
review_request.unwrap().as_str(),
|
||||
"--label",
|
||||
"dependencies",
|
||||
"--checks=pending",
|
||||
//"--checks=pending",
|
||||
"--json",
|
||||
"repository,number,title",
|
||||
],
|
||||
@ -45,6 +55,135 @@ impl ReviewBackend for DefaultReviewBackend {
|
||||
println!("{table}");
|
||||
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 {
|
||||
|
@ -12,3 +12,20 @@ pub struct PullRequest {
|
||||
pub number: usize,
|
||||
pub repository: Repository,
|
||||
}
|
||||
|
||||
pub enum MenuChoice {
|
||||
Exit,
|
||||
Begin,
|
||||
Search,
|
||||
List,
|
||||
}
|
||||
|
||||
pub enum ReviewMenuChoice {
|
||||
Exit,
|
||||
List,
|
||||
Approve,
|
||||
Open,
|
||||
Skip,
|
||||
Merge,
|
||||
ApproveAndMerge,
|
||||
}
|
||||
|
Reference in New Issue
Block a user