diff --git a/Cargo.lock b/Cargo.lock index c8731cf..e1feb56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "1.3.2" @@ -43,6 +58,65 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "comfy-table" +version = "6.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b787b0dc42e8111badfdbe4c3059158ccb2db8780352fa1b01e8ccf45cc4d" +dependencies = [ + "crossterm", + "strum", + "strum_macros", + "unicode-width", +] + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "dirs" version = "4.0.0" @@ -63,6 +137,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "errno" version = "0.2.8" @@ -94,6 +180,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "getrandom" version = "0.2.8" @@ -110,11 +211,22 @@ name = "github" version = "0.1.0" dependencies = [ "clap", + "comfy-table", "dirs", "eyre", + "mockall", + "pretty_assertions", + "serde", + "serde_json", "util", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.2.6" @@ -152,6 +264,27 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.138" @@ -164,6 +297,85 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "mockall" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4a1c770583dac7ab5e2f6c139153b783a53a1bbee9729613f193e59828326" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832663583d5fa284ca8810bf7015e46c9fff9622d3cf34bd1eea5003fec06dd0" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.16.0" @@ -176,6 +388,80 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" + +[[package]] +name = "predicates-tree" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" +dependencies = [ + "ctor", + "diff", + "output_vt100", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.49" @@ -214,6 +500,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + [[package]] name = "rustix" version = "0.36.5" @@ -228,6 +531,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + [[package]] name = "same-file" version = "1.0.6" @@ -237,6 +552,79 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + [[package]] name = "sourcegraph" version = "0.1.0" @@ -263,6 +651,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "1.0.107" @@ -283,6 +690,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" + [[package]] name = "thiserror" version = "1.0.38" @@ -333,6 +746,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "util" version = "0.1.0" @@ -445,3 +864,9 @@ name = "windows_x86_64_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/crates/github/Cargo.toml b/crates/github/Cargo.toml index 0548883..2af2caf 100644 --- a/crates/github/Cargo.toml +++ b/crates/github/Cargo.toml @@ -11,3 +11,11 @@ util = { path = "../util" } eyre.workspace = true clap.workspace = true dirs.workspace = true + +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.91" +comfy-table = "6.1.4" +pretty_assertions = "1.3.0" + +[dev-dependencies] +mockall = "0.11.2" diff --git a/crates/github/src/lib.rs b/crates/github/src/lib.rs index 3071590..b7e118d 100644 --- a/crates/github/src/lib.rs +++ b/crates/github/src/lib.rs @@ -1,6 +1,8 @@ mod auth; mod fuzzy_clone; mod gh; +mod review; +pub(crate) mod review_backend; pub struct GitHub; @@ -31,6 +33,7 @@ impl util::Cmd for GitHub { auth::Auth::cmd()?, gh::Gh::cmd()?, fuzzy_clone::FuzzyClone::cmd()?, + review::Review::cmd()?, ]) .allow_external_subcommands(true)) } @@ -41,6 +44,7 @@ impl util::Cmd for GitHub { Some(("fuzzy-clone", subm)) => fuzzy_clone::FuzzyClone::exec(subm), Some(("fc", subm)) => fuzzy_clone::FuzzyClone::exec(subm), Some(("gh", subm)) => gh::Gh::exec(subm), + Some(("review", subm)) => review::Review::exec(subm), Some((external, args)) => Self::run(external, args), _ => Err(eyre::anyhow!("missing argument")), } diff --git a/crates/github/src/review.rs b/crates/github/src/review.rs new file mode 100644 index 0000000..745030f --- /dev/null +++ b/crates/github/src/review.rs @@ -0,0 +1,140 @@ +use crate::review_backend::{models::PullRequest, DefaultReviewBackend, DynReviewBackend}; + +use comfy_table::{presets::UTF8_HORIZONTAL_ONLY, Cell, Row, Table}; +#[cfg(test)] +use mockall::{automock, mock, predicate::*}; + +pub struct Review { + backend: DynReviewBackend, +} + +impl Default for Review { + fn default() -> Self { + Self::new(std::sync::Arc::new(DefaultReviewBackend::new())) + } +} + +impl Review { + fn new(backend: DynReviewBackend) -> Self { + Self { backend } + } + + // Workflow + // 1. Fetch list of repos + // 2. Present menu + // 3. Choose begin quick review + // 4. Present pr and use delta to view changes + // 5. Approve, open, skip or quit + // 6. Repeat from 4 + fn run(&self, review_requested: Option) -> eyre::Result<()> { + let prs = self.backend.get_prs(review_requested)?; + + let prs_table = Self::generate_prs_table(&prs); + + self.backend.present_prs(prs_table)?; + + Ok(()) + } + + fn generate_prs_table(prs: &[PullRequest]) -> String { + let mut table = Table::new(); + let table = table + .load_preset(UTF8_HORIZONTAL_ONLY) + .set_content_arrangement(comfy_table::ContentArrangement::Dynamic) + .set_header(vec![ + Cell::new("repo"), + Cell::new("title"), + Cell::new("number"), + ]) + .add_rows(prs.iter().map(|pr| { + let pr = pr.clone(); + vec![pr.repository.name, pr.title, pr.number.to_string()] + })); + + table.to_string() + } +} + +impl util::Cmd for Review { + fn cmd() -> eyre::Result { + Ok(clap::Command::new("review")) + } + + fn exec(_: &clap::ArgMatches) -> eyre::Result<()> { + Self::default().run(Some("lunarway/squad-aura".into())) + } +} + +#[cfg(test)] +mod tests { + use crate::review_backend::{models::Repository, MockReviewBackend}; + + use super::*; + + use pretty_assertions::{assert_eq, assert_ne}; + + #[test] + fn can_fetch_prs() { + let mut backend = MockReviewBackend::new(); + let prs = vec![ + PullRequest { + title: "some-title".into(), + number: 0, + repository: Repository { + name: "some-name".into(), + }, + }, + PullRequest { + title: "some-other-title".into(), + number: 1, + repository: Repository { + name: "some-other-name".into(), + }, + }, + ]; + let backendprs = prs.clone(); + backend + .expect_get_prs() + .with(eq(Some("kjuulh".into()))) + .times(1) + .returning(move |_| Ok(backendprs.clone())); + + 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"); + } + + #[test] + fn can_generate_table() { + let prs = vec![ + PullRequest { + title: "some-title".into(), + number: 0, + repository: Repository { + name: "some-name".into(), + }, + }, + PullRequest { + title: "some-other-title".into(), + number: 1, + repository: Repository { + name: "some-other-name".into(), + }, + }, + ]; + let expected_table = "───────────────────────────────────────────── + repo title number +═════════════════════════════════════════════ + some-name some-title 0 +───────────────────────────────────────────── + some-other-name some-other-title 1 +─────────────────────────────────────────────"; + + let output = Review::generate_prs_table(&prs); + + assert_eq!(output, expected_table.to_string()) + } +} diff --git a/crates/github/src/review_backend/mod.rs b/crates/github/src/review_backend/mod.rs new file mode 100644 index 0000000..203bc56 --- /dev/null +++ b/crates/github/src/review_backend/mod.rs @@ -0,0 +1,53 @@ +pub mod models; + +use self::models::PullRequest; + +#[cfg(test)] +use mockall::{automock, predicate::*}; +#[cfg_attr(test, automock)] +pub trait ReviewBackend { + fn get_prs(&self, review_request: Option) -> eyre::Result>; + fn present_prs(&self, table: String) -> eyre::Result<()>; +} + +pub type DynReviewBackend = std::sync::Arc; + +#[derive(Default)] +pub struct DefaultReviewBackend; + +impl ReviewBackend for DefaultReviewBackend { + fn get_prs(&self, review_request: Option) -> eyre::Result> { + let raw_prs = util::shell::run_with_input_and_output( + &[ + "gh", + "search", + "prs", + "--state=open", + "--review-requested", + review_request.unwrap().as_str(), + "--label", + "dependencies", + "--checks=pending", + "--json", + "repository,number,title", + ], + "".into(), + )?; + + let prs_json = std::str::from_utf8(raw_prs.stdout.as_slice())?; + + let prs: Vec = serde_json::from_str(prs_json)?; + + Ok(prs) + } + + fn present_prs(&self, table: String) -> eyre::Result<()> { + Ok(()) + } +} + +impl DefaultReviewBackend { + pub fn new() -> Self { + Self {} + } +} diff --git a/crates/github/src/review_backend/models.rs b/crates/github/src/review_backend/models.rs new file mode 100644 index 0000000..5974acd --- /dev/null +++ b/crates/github/src/review_backend/models.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct Repository { + #[serde(rename(deserialize = "nameWithOwner"))] + pub name: String, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct PullRequest { + pub title: String, + pub number: usize, + pub repository: Repository, +}