From 936e2bdba89e82f568d0336a8e81fe224358dbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Sun, 23 Jan 2022 16:33:10 +0100 Subject: [PATCH] WIP: Add github integration --- Cargo.lock | 363 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 10 ++ src/grm/cmd.rs | 139 +++++++++++++--- src/grm/main.rs | 327 ++++++++++++++++++++++++++++--------- src/lib.rs | 3 +- src/provider/github.rs | 199 ++++++++++++++++++++++ src/provider/mod.rs | 30 ++++ 7 files changed, 969 insertions(+), 102 deletions(-) create mode 100644 src/provider/github.rs create mode 100644 src/provider/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8f34e9b..dd8da63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + [[package]] name = "atty" version = "0.2.14" @@ -34,6 +45,24 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + +[[package]] +name = "castaway" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" + [[package]] name = "cc" version = "1.0.72" @@ -91,6 +120,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + [[package]] name = "console" version = "0.15.0" @@ -106,6 +144,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" +dependencies = [ + "cfg-if", + "lazy_static", +] + [[package]] name = "crossterm" version = "0.22.1" @@ -131,6 +179,37 @@ dependencies = [ "winapi", ] +[[package]] +name = "curl" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de97b894edd5b5bcceef8b78d7da9b75b1d2f2f9a910569d0bde3dd31d84939" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "winapi", +] + +[[package]] +name = "curl-sys" +version = "0.4.52+curl-7.81.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b8c2d1023ea5fded5b7b892e4b8e95f70038a421126a056761a84246a28971" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "winapi", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -158,6 +237,36 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -174,6 +283,33 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "futures-core" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" + +[[package]] +name = "futures-io" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "getrandom" version = "0.2.4" @@ -193,8 +329,11 @@ dependencies = [ "comfy-table", "console", "git2", + "isahc", + "parse_link_header", "regex", "serde", + "serde_json", "serde_yaml", "shellexpand", "tempdir", @@ -246,6 +385,17 @@ dependencies = [ "libc", ] +[[package]] +name = "http" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "idna" version = "0.2.3" @@ -276,6 +426,41 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "isahc" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d140e84730d325378912ede32d7cd53ef1542725503b3353e5ec8113c7c6f588" +dependencies = [ + "async-channel", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener", + "futures-lite", + "http", + "log", + "mime", + "once_cell", + "polling", + "serde", + "serde_json", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "jobserver" version = "0.1.24" @@ -311,6 +496,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libnghttp2-sys" +version = "0.1.7+1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "libssh2-sys" version = "0.2.23" @@ -373,6 +568,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "mio" version = "0.7.14" @@ -438,6 +639,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.11.2" @@ -463,18 +670,68 @@ dependencies = [ "winapi", ] +[[package]] +name = "parse_link_header" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40728c9c01de984c45f49385ab054fdc31cd3322658a6934347887e72cb48df9" +dependencies = [ + "http", + "lazy_static", + "regex", +] + [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + [[package]] name = "pkg-config" version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -605,6 +862,16 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -631,6 +898,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.8.23" @@ -682,12 +960,39 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel", + "futures-core", + "futures-io", +] + [[package]] name = "smallvec" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "socket2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "strsim" version = "0.10.0" @@ -782,6 +1087,49 @@ dependencies = [ "serde", ] +[[package]] +name = "tracing" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + [[package]] name = "unicode-bidi" version = "0.3.7" @@ -839,12 +1187,27 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 844af51..862009a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,5 +65,15 @@ version = "=5.0.0" [dependencies.serde_yaml] version = "=0.8.23" +[dependencies.serde_json] +version = "=1.0.78" + +[dependencies.isahc] +version = "=1.6.0" +features = ["json"] + +[dependencies.parse_link_header] +version = "=0.3.2" + [dev-dependencies.tempdir] version = "=0.3.7" diff --git a/src/grm/cmd.rs b/src/grm/cmd.rs index 1ea9a02..4bd0a7f 100644 --- a/src/grm/cmd.rs +++ b/src/grm/cmd.rs @@ -31,20 +31,94 @@ pub struct Repos { #[derive(Parser)] pub enum ReposAction { - #[clap( - visible_alias = "run", - about = "Synchronize the repositories to the configured values" - )] - Sync(Sync), - #[clap(about = "Generate a repository configuration from an existing file tree")] - Find(Find), + #[clap(subcommand)] + Sync(SyncAction), + #[clap(subcommand)] + Find(FindAction), #[clap(about = "Show status of configured repositories")] Status(OptionalConfig), } +#[derive(Parser)] +#[clap(about = "Sync local repositories with a configured list")] +pub enum SyncAction { + #[clap( + visible_alias = "run", + about = "Synchronize the repositories to the configured values" + )] + Config(Config), + #[clap(about = "Synchronize the repositories from a remote provider")] + Remote(Remote), +} + +#[derive(Parser)] +#[clap(about = "Generate a repository configuration from existing repositories")] +pub enum FindAction { + #[clap(about = "Find local repositories")] + Local(FindLocalArgs), + #[clap(about = "Find repositories on remote provider")] + Remote(FindRemoteArgs), +} + +#[derive(Parser)] +pub struct FindLocalArgs { + #[clap(help = "The path to search through")] + pub path: String, + + #[clap( + arg_enum, + short, + long, + help = "Format to produce", + default_value_t = ConfigFormat::Toml, + )] + pub format: ConfigFormat, +} + #[derive(Parser)] #[clap()] -pub struct Sync { +pub struct FindRemoteArgs { + #[clap(arg_enum, short, long, help = "Remote provider to use")] + pub provider: RemoteProvider, + + #[clap( + multiple_occurrences = true, + name = "user", + long, + help = "Users to get repositories from" + )] + pub users: Vec, + + #[clap( + multiple_occurrences = true, + name = "group", + long, + help = "Groups to get repositories from" + )] + pub groups: Vec, + + #[clap(long, help = "Get repositories that belong to the requesting user")] + pub owner: bool, + + #[clap(long, help = "Command to get API token")] + pub token_command: String, + + #[clap( + arg_enum, + short, + long, + help = "Format to produce", + default_value_t = ConfigFormat::Toml, + )] + pub format: ConfigFormat, + + #[clap(long, help = "Root of the repo tree to produce")] + pub root: String, +} + +#[derive(Parser)] +#[clap()] +pub struct Config { #[clap( short, long, @@ -54,6 +128,40 @@ pub struct Sync { pub config: String, } +#[derive(clap::ArgEnum, Clone)] +pub enum RemoteProvider { + Github, +} + +#[derive(Parser)] +#[clap()] +pub struct Remote { + #[clap(arg_enum, short, long, help = "Remote provider to use")] + pub provider: RemoteProvider, + + #[clap( + multiple_occurrences = true, + name = "user", + long, + help = "Users to get repositories from" + )] + pub users: Vec, + + #[clap( + multiple_occurrences = true, + name = "group", + long, + help = "Groups to get repositories from" + )] + pub groups: Vec, + + #[clap(long, help = "Get repositories that belong to the requesting user")] + pub owner: bool, + + #[clap(long, help = "Command to get API token")] + pub token_command: String, +} + #[derive(Parser)] #[clap()] pub struct OptionalConfig { @@ -67,21 +175,6 @@ pub enum ConfigFormat { Toml, } -#[derive(Parser)] -pub struct Find { - #[clap(help = "The path to search through")] - pub path: String, - - #[clap( - arg_enum, - short, - long, - help = "Format to produce", - default_value_t = ConfigFormat::Toml, - )] - pub format: ConfigFormat, -} - #[derive(Parser)] pub struct Worktree { #[clap(subcommand, name = "action")] diff --git a/src/grm/main.rs b/src/grm/main.rs index 50547da..45051ce 100644 --- a/src/grm/main.rs +++ b/src/grm/main.rs @@ -5,6 +5,7 @@ mod cmd; use grm::config; use grm::output::*; +use grm::provider::Provider; use grm::repo; fn main() { @@ -12,26 +13,88 @@ fn main() { match opts.subcmd { cmd::SubCommand::Repos(repos) => match repos.action { - cmd::ReposAction::Sync(sync) => { - let config = match config::read_config(&sync.config) { - Ok(config) => config, - Err(error) => { - print_error(&error); - process::exit(1); - } - }; - match grm::sync_trees(config) { - Ok(success) => { - if !success { - process::exit(1) + cmd::ReposAction::Sync(sync) => match sync { + cmd::SyncAction::Config(args) => { + let config = match config::read_config(&args.config) { + Ok(config) => config, + Err(error) => { + print_error(&error); + process::exit(1); + } + }; + match grm::sync_trees(config) { + Ok(success) => { + if !success { + process::exit(1) + } + } + Err(error) => { + print_error(&format!("Error syncing trees: {}", error)); + process::exit(1); } } - Err(error) => { - print_error(&format!("Error syncing trees: {}", error)); - process::exit(1); + } + cmd::SyncAction::Remote(args) => { + let users = if args.users.is_empty() { + None + } else { + Some(args.users) + }; + + let groups = if args.groups.is_empty() { + None + } else { + Some(args.groups) + }; + + let token_process = std::process::Command::new("/usr/bin/env") + .arg("sh") + .arg("-c") + .arg(args.token_command) + .output(); + + let token: String = match token_process { + Err(error) => { + print_error(&format!("Failed to run token-command: {}", error)); + process::exit(1); + } + Ok(output) => { + let stderr = String::from_utf8(output.stderr).unwrap(); + let stdout = String::from_utf8(output.stdout).unwrap(); + + if !output.status.success() { + if !stderr.is_empty() { + print_error(&format!("Token command failed: {}", stderr)); + } else { + print_error("Token command failed."); + } + } + if !stderr.is_empty() { + print_error(&format!("Token command produced stderr: {}", stderr)); + } + + if stdout.is_empty() { + print_error("Token command did not produce output"); + } + + let token = stdout.split('\n').next().unwrap(); + + token.to_string() + } + }; + + let filter = grm::provider::Filter::new(users, groups, args.owner); + let github = grm::provider::Github::new(filter, token); + + match github.get_repos() { + Ok(repos) => println!("{:?}", repos), + Err(error) => { + print_error(&format!("Error: {}", error)); + process::exit(1); + } } } - } + }, cmd::ReposAction::Status(args) => match &args.config { Some(config_path) => { let config = match config::read_config(config_path) { @@ -79,79 +142,187 @@ fn main() { } } }, - cmd::ReposAction::Find(find) => { - let path = Path::new(&find.path); - if !path.exists() { - print_error(&format!("Path \"{}\" does not exist", path.display())); - process::exit(1); - } - if !path.is_dir() { - print_error(&format!("Path \"{}\" is not a directory", path.display())); - process::exit(1); - } - - let path = match path.canonicalize() { - Ok(path) => path, - Err(error) => { - print_error(&format!( - "Failed to canonicalize path \"{}\". This is a bug. Error message: {}", - &path.display(), - error - )); + cmd::ReposAction::Find(find) => match find { + cmd::FindAction::Local(args) => { + let path = Path::new(&args.path); + if !path.exists() { + print_error(&format!("Path \"{}\" does not exist", path.display())); process::exit(1); } - }; - - let (found_repos, warnings) = match grm::find_in_tree(&path) { - Ok((repos, warnings)) => (repos, warnings), - Err(error) => { - print_error(&error); + if !path.is_dir() { + print_error(&format!("Path \"{}\" is not a directory", path.display())); process::exit(1); } - }; - let trees = grm::config::Trees::from_vec(vec![found_repos]); - if trees.as_vec_ref().iter().all(|t| match &t.repos { - None => false, - Some(r) => r.is_empty(), - }) { - print_warning("No repositories found"); - } else { - let config = trees.to_config(); - - match find.format { - cmd::ConfigFormat::Toml => { - let toml = match config.as_toml() { - Ok(toml) => toml, - Err(error) => { - print_error(&format!( - "Failed converting config to TOML: {}", - &error - )); - process::exit(1); - } - }; - print!("{}", toml); + let path = match path.canonicalize() { + Ok(path) => path, + Err(error) => { + print_error(&format!( + "Failed to canonicalize path \"{}\". This is a bug. Error message: {}", + &path.display(), + error + )); + process::exit(1); } - cmd::ConfigFormat::Yaml => { - let yaml = match config.as_yaml() { - Ok(yaml) => yaml, - Err(error) => { - print_error(&format!( - "Failed converting config to YAML: {}", - &error - )); - process::exit(1); + }; + + let (found_repos, warnings) = match grm::find_in_tree(&path) { + Ok((repos, warnings)) => (repos, warnings), + Err(error) => { + print_error(&error); + process::exit(1); + } + }; + + let trees = grm::config::Trees::from_vec(vec![found_repos]); + if trees.as_vec_ref().iter().all(|t| match &t.repos { + None => false, + Some(r) => r.is_empty(), + }) { + print_warning("No repositories found"); + } else { + let config = trees.to_config(); + + match args.format { + cmd::ConfigFormat::Toml => { + let toml = match config.as_toml() { + Ok(toml) => toml, + Err(error) => { + print_error(&format!( + "Failed converting config to TOML: {}", + &error + )); + process::exit(1); + } + }; + print!("{}", toml); + } + cmd::ConfigFormat::Yaml => { + let yaml = match config.as_yaml() { + Ok(yaml) => yaml, + Err(error) => { + print_error(&format!( + "Failed converting config to YAML: {}", + &error + )); + process::exit(1); + } + }; + print!("{}", yaml); + } + } + } + for warning in warnings { + print_warning(&warning); + } + } + cmd::FindAction::Remote(args) => { + let users = if args.users.is_empty() { + None + } else { + Some(args.users) + }; + + let groups = if args.groups.is_empty() { + None + } else { + Some(args.groups) + }; + + let token_process = std::process::Command::new("/usr/bin/env") + .arg("sh") + .arg("-c") + .arg(args.token_command) + .output(); + + let token: String = match token_process { + Err(error) => { + print_error(&format!("Failed to run token-command: {}", error)); + process::exit(1); + } + Ok(output) => { + let stderr = String::from_utf8(output.stderr).unwrap(); + let stdout = String::from_utf8(output.stdout).unwrap(); + + if !output.status.success() { + if !stderr.is_empty() { + print_error(&format!("Token command failed: {}", stderr)); + } else { + print_error("Token command failed."); } + } + if !stderr.is_empty() { + print_error(&format!("Token command produced stderr: {}", stderr)); + } + + if stdout.is_empty() { + print_error("Token command did not produce output"); + } + + let token = stdout.split('\n').next().unwrap(); + + token.to_string() + } + }; + + let filter = grm::provider::Filter::new(users, groups, args.owner); + let github = grm::provider::Github::new(filter, token); + + match github.get_repos() { + Ok(repos) => { + let mut trees: Vec = vec![]; + + for (namespace, repolist) in repos { + let tree = config::Tree { + root: Path::new(&args.root) + .join(namespace) + .display() + .to_string(), + repos: Some(repolist), + }; + trees.push(tree); + } + + let config = config::Config { + trees: config::Trees::from_vec(trees), }; - print!("{}", yaml); + + match args.format { + cmd::ConfigFormat::Toml => { + let toml = match config.as_toml() { + Ok(toml) => toml, + Err(error) => { + print_error(&format!( + "Failed converting config to TOML: {}", + &error + )); + process::exit(1); + } + }; + print!("{}", toml); + } + cmd::ConfigFormat::Yaml => { + let yaml = match config.as_yaml() { + Ok(yaml) => yaml, + Err(error) => { + print_error(&format!( + "Failed converting config to YAML: {}", + &error + )); + process::exit(1); + } + }; + print!("{}", yaml); + } + } + } + Err(error) => { + print_error(&format!("Error: {}", error)); + process::exit(1); } } } - for warning in warnings { - print_warning(&warning); - } - } + }, }, cmd::SubCommand::Worktree(args) => { let cwd = std::env::current_dir().unwrap_or_else(|error| { diff --git a/src/lib.rs b/src/lib.rs index 3bc321b..ffe6db4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,13 +6,14 @@ use std::process; pub mod config; pub mod output; +pub mod provider; pub mod repo; pub mod table; use config::{Config, Tree}; use output::*; -use repo::{clone_repo, detect_remote_type, Remote, RepoConfig}; +use repo::{clone_repo, detect_remote_type, Remote, RemoteType, RepoConfig}; pub use repo::{RemoteTrackingStatus, Repo, RepoErrorKind, WorktreeRemoveFailureReason}; diff --git a/src/provider/github.rs b/src/provider/github.rs new file mode 100644 index 0000000..1b113e0 --- /dev/null +++ b/src/provider/github.rs @@ -0,0 +1,199 @@ +use std::collections::HashMap; + +use isahc::prelude::*; +use serde::Deserialize; + +use crate::{Remote, RemoteType, RepoConfig}; + +use super::Filter; +use super::Provider; +use super::SecretToken; + +#[derive(Deserialize)] +#[serde(untagged)] +enum GithubUserProjectResponse { + Success(Vec), + Failure(GithubFailureResponse), +} + +#[derive(Deserialize)] +struct GithubProject { + pub name: String, + pub full_name: String, + pub clone_url: String, + pub ssh_url: String, + pub private: bool, +} + +impl GithubProject { + fn into_repo_config(self) -> RepoConfig { + RepoConfig { + name: self.name, + worktree_setup: false, + remotes: Some(vec![Remote { + name: String::from("github"), + url: match self.private { + true => self.ssh_url, + false => self.clone_url, + }, + remote_type: match self.private { + true => RemoteType::Ssh, + false => RemoteType::Https, + }, + }]), + } + } +} + +#[derive(Deserialize)] +struct GithubFailureResponse { + pub message: String, +} + +pub struct Github { + filter: Filter, + secret_token: SecretToken, +} + +impl Github { + fn get_repo_list_from_uri( + uri: &str, + secret_token: &SecretToken, + ) -> Result, String> { + let mut repos: Vec<(String, GithubProject)> = vec![]; + + let client = isahc::HttpClient::new().map_err(|error| error.to_string())?; + + let request = isahc::Request::builder() + .uri(uri) + .header("accept", " application/vnd.github.v3+json") + .header("authorization", format!("token {}", secret_token)) + .body(()) + .map_err(|error| error.to_string())?; + + let mut response = client.send(request).map_err(|error| error.to_string())?; + + let success = response.status().is_success(); + + { + let response: GithubUserProjectResponse = response + .json() + .map_err(|error| format!("Failed deserializing response: {}", error))?; + + if !success { + match response { + GithubUserProjectResponse::Failure(error) => return Err(error.message), + _ => return Err(String::from("Unknown response error")), + } + } + + match response { + GithubUserProjectResponse::Failure(error) => { + return Err(format!( + "Received error response but no error code: {}", + error.message + )) + } + GithubUserProjectResponse::Success(repo_list) => { + for repo in repo_list { + let (namespace, _name) = repo + .full_name + .rsplit_once('/') + .unwrap_or(("", &repo.full_name)); + repos.push((namespace.to_string(), repo)); + } + } + } + } + + let headers = response.headers(); + + if let Some(link_header) = headers.get("link") { + let link_header = link_header.to_str().map_err(|error| error.to_string())?; + + let link_header = + parse_link_header::parse(link_header).map_err(|error| error.to_string())?; + + let next_page = link_header.get(&Some(String::from("next"))); + + if let Some(page) = next_page { + let following_repos = Github::get_repo_list_from_uri(&page.raw_uri, secret_token)?; + repos.extend(following_repos); + } + } + + Ok(repos) + } +} + +impl Provider for Github { + fn new(filter: Filter, secret_token: SecretToken) -> Self { + Github { + filter, + secret_token, + } + } + + fn get_repos(&self) -> Result>, String> { + let mut namespaces: HashMap> = HashMap::new(); + + let mut register = |namespace: String, repo: GithubProject| { + let name = repo.name.clone(); + let repo_config = repo.into_repo_config(); + match namespaces.get_mut(&namespace) { + Some(ns) => match ns.get_mut(&name) { + Some(_entry) => {} + None => { + ns.insert(name, repo_config); + } + }, + None => { + let mut ns = HashMap::new(); + ns.insert(name, repo_config); + namespaces.insert(namespace, ns); + } + } + }; + + if let Some(users) = &self.filter.users { + for user in users { + let repos = Github::get_repo_list_from_uri( + &format!("https://api.github.com/users/{}/repos", user), + &self.secret_token, + )?; + for (namespace, repo) in repos { + register(namespace, repo); + } + } + } + + if let Some(groups) = &self.filter.groups { + for group in groups { + let repos = Github::get_repo_list_from_uri( + &format!("https://api.github.com/orgs/{}/repos", group), + &self.secret_token, + )?; + for (namespace, repo) in repos { + register(namespace, repo); + } + } + } + + if self.filter.owner { + let repos = Github::get_repo_list_from_uri( + "https://api.github.com/user/repos?affiliation=owner", + &self.secret_token, + )?; + for (namespace, repo) in repos { + register(namespace, repo); + } + } + + let mut ret: HashMap> = HashMap::new(); + for (namespace, repos) in namespaces { + ret.insert(namespace, repos.into_values().collect()); + } + + Ok(ret) + } +} diff --git a/src/provider/mod.rs b/src/provider/mod.rs new file mode 100644 index 0000000..e582b68 --- /dev/null +++ b/src/provider/mod.rs @@ -0,0 +1,30 @@ +pub mod github; + +pub use github::Github; + +use super::RepoConfig; + +use std::collections::HashMap; + +pub struct Filter { + users: Option>, + groups: Option>, + owner: bool, +} + +type SecretToken = String; + +impl Filter { + pub fn new(users: Option>, groups: Option>, owner: bool) -> Self { + Filter { + users, + groups, + owner, + } + } +} + +pub trait Provider { + fn new(filter: Filter, secret_token: SecretToken) -> Self; + fn get_repos(&self) -> Result>, String>; +}