diff --git a/Cargo.lock b/Cargo.lock index 7c94292..abf70df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,6 +149,46 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "colorchoice" version = "1.0.3" @@ -330,8 +370,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git-mover" -version = "0.1.1" +version = "1.0.0" dependencies = [ + "clap", "dotenv", "env_logger", "git2", @@ -386,6 +427,12 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "home" version = "0.5.11" @@ -1289,6 +1336,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index da90bae..62de7a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "git-mover" -version = "0.1.1" +version = "1.0.0" edition = "2021" keywords = ["utility", "git", "cli"] categories = ["command-line-utilities"] @@ -11,6 +11,7 @@ readme = "./README.md" repository = "https://github.com/Its-Just-Nans/git-mover" [dependencies] +clap = { version = "4.5.23", features = ["derive"] } dotenv = "0.15.0" env_logger = "0.11.5" git2 = "0.19.0" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..100e683 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,34 @@ +use std::path::PathBuf; + +use clap::Parser; +use serde::Deserialize; + +use crate::{config::Config, utils::main_sync}; + +#[derive(Parser, Deserialize, Default, Clone, Debug)] +pub struct GitMoverCli { + #[arg(short, long, visible_alias = "from")] + pub source: Option, + #[arg(short, long, visible_alias = "to")] + pub destination: Option, + #[arg(short, long = "no-forks")] + pub no_forks: bool, + #[arg(short, long)] + pub config: Option, + #[arg(short, long, action = clap::ArgAction::Count)] + pub verbose: u8, +} + +pub async fn cli_main() { + let args = GitMoverCli::parse(); + let mut config = match &args.config { + Some(path_str) => { + let path = PathBuf::from(path_str); + Config::new_from_path(&path) + } + None => Config::new(), + } + .set_debug(args.verbose) + .with_cli_args(args); + main_sync(&mut config).await; +} diff --git a/src/codeberg/platform.rs b/src/codeberg/platform.rs index ce570f2..99b4ed0 100644 --- a/src/codeberg/platform.rs +++ b/src/codeberg/platform.rs @@ -172,7 +172,7 @@ impl Platform for CodebergPlatform { let url = format!("https://{}/api/v1/user/repos", CODEBERG_URL); let mut page: usize = 1; let limit = 100; - let mut repos = Vec::new(); + let mut all_repos = Vec::new(); loop { let request = client .get(&url) @@ -183,17 +183,20 @@ impl Platform for CodebergPlatform { let response = request.await?; if !response.status().is_success() { - return Err(GitMoverError::new(GitMoverErrorKind::GetAllRepos)); + let text = response.text().await?; + return Err(GitMoverError::new(GitMoverErrorKind::GetAllRepos).with_text(&text)); } - let mut page_repos: Vec = response.json().await?; + let text = response.text().await?; + let repos: Vec = serde_json::from_str(&text)?; + let mut page_repos: Vec = repos.into_iter().map(|r| r.into()).collect(); if page_repos.is_empty() { break; } println!("Requested codeberg (page {}): {}", page, page_repos.len()); - repos.append(&mut page_repos); + all_repos.append(&mut page_repos); page += 1; } - Ok(repos) + Ok(all_repos) }) } diff --git a/src/config.rs b/src/config.rs index 8aaaa7a..50aefaa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,9 @@ use std::{ use home::home_dir; use serde::{Deserialize, Serialize}; -use crate::{codeberg::CodebergConfig, github::GithubConfig, gitlab::GitlabConfig}; +use crate::{ + cli::GitMoverCli, codeberg::CodebergConfig, github::GithubConfig, gitlab::GitlabConfig, +}; #[derive(Deserialize, Default, Clone, Debug)] pub struct Config { @@ -19,6 +21,8 @@ pub struct Config { /// actual configuration data pub config_data: ConfigData, + + pub cli_args: Option, } #[derive(Deserialize, Serialize, Default, Clone, Debug)] @@ -36,6 +40,7 @@ impl Config { Config { debug: 0, config_path: path_config, + cli_args: None, config_data: match toml::from_str(str_config) { Ok(config) => config, Err(e) => { @@ -47,6 +52,11 @@ impl Config { } } + pub fn with_cli_args(mut self, cli_args: GitMoverCli) -> Self { + self.cli_args = Some(cli_args); + self + } + /// Create a new Config object from the default path pub fn new() -> Config { let config_path = Config::get_config_path(); @@ -71,8 +81,9 @@ impl Config { } /// Set the debug value - pub fn set_debug(&mut self, value: u8) { + pub fn set_debug(mut self, value: u8) -> Self { self.debug = value; + self } /// Get the path to the config file diff --git a/src/github/platform.rs b/src/github/platform.rs index 39a8450..d931d46 100644 --- a/src/github/platform.rs +++ b/src/github/platform.rs @@ -3,7 +3,7 @@ use crate::{ utils::{Platform, Repo}, }; use reqwest::{ - header::{ACCEPT, AUTHORIZATION}, + header::{ACCEPT, AUTHORIZATION, USER_AGENT}, Client, }; use serde::{Deserialize, Serialize}; @@ -74,11 +74,13 @@ impl Platform for GithubPlatform { ]) .header(AUTHORIZATION, format!("Bearer {}", token)) .header(ACCEPT, "application/vnd.github+json") + .header(USER_AGENT, "reqwest") .header("X-GitHub-Api-Version", "2022-11-28") .send(); let response = request.await?; if !response.status().is_success() { - return Err(GitMoverError::new(GitMoverErrorKind::GetAllRepos)); + let text = response.text().await?; + return Err(GitMoverError::new(GitMoverErrorKind::GetAllRepos).with_text(&text)); } let text = response.text().await?; let repos: Vec = serde_json::from_str(&text)?; @@ -96,7 +98,7 @@ impl Platform for GithubPlatform { fn delete_repo( &self, - name: &str, + _name: &str, ) -> Pin> + Send + '_>> { unimplemented!("GitlabConfig::delete_repo"); Box::pin(async { Err(GitMoverError::new(GitMoverErrorKind::Unimplemented)) }) diff --git a/src/gitlab/platform.rs b/src/gitlab/platform.rs index c479ef8..3caeb44 100644 --- a/src/gitlab/platform.rs +++ b/src/gitlab/platform.rs @@ -160,7 +160,8 @@ impl Platform for GitlabPlatform { let response = request.await?; if !response.status().is_success() { - return Err(GitMoverError::new(GitMoverErrorKind::GetAllRepos)); + let text = response.text().await?; + return Err(GitMoverError::new(GitMoverErrorKind::GetAllRepos).with_text(&text)); } let text = response.text().await?; let repos: Vec = match serde_json::from_str(&text) { diff --git a/src/lib.rs b/src/lib.rs index cbc89d5..fb69ad3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub(crate) mod cli; pub(crate) mod config; pub(crate) mod errors; pub(crate) mod macros; @@ -10,4 +11,4 @@ mod codeberg; mod github; mod gitlab; -pub use utils::{cli_main, PlatformType}; +pub use cli::cli_main; diff --git a/src/main.rs b/src/main.rs index bb8eb65..6005b3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ async fn main() { env!("CARGO_PKG_VERSION") )); setup(); - cli_main(None).await; + cli_main().await; } #[cfg(test)] @@ -26,6 +26,6 @@ mod tests { #[tokio::test] async fn test_main() { setup(); - cli_main(None).await; + cli_main().await; } } diff --git a/src/sync.rs b/src/sync.rs index 5ac428d..160cea9 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -23,12 +23,7 @@ pub(crate) async fn sync_repos( let mut set = JoinSet::new(); let mut private_repos = vec![]; - let mut fork_repos = vec![]; for one_repo in repos { - if one_repo.fork { - fork_repos.push(one_repo); - continue; - } if one_repo.private { private_repos.push(one_repo); continue; @@ -46,7 +41,6 @@ pub(crate) async fn sync_repos( } }); } - println!("Skipping {} forked repos", fork_repos.len()); let temp_folder_priv = temp_folder.clone(); set.spawn(async move { match sync_private_repos( diff --git a/src/utils.rs b/src/utils.rs index 06488fc..7460ba4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,10 @@ use std::collections::HashSet; -use std::{fmt::Debug, path::PathBuf, pin::Pin, sync::Arc}; +use std::{fmt::Debug, pin::Pin, sync::Arc}; use serde::Deserialize; use tokio::join; +use crate::cli::GitMoverCli; use crate::errors::GitMoverError; use crate::sync::{delete_repos, sync_repos}; use crate::{codeberg::CodebergConfig, config::Config, github::GithubConfig, gitlab::GitlabConfig}; @@ -45,13 +46,39 @@ pub trait Platform: Debug + Sync + Send { fn get_remote_url(&self) -> &str; } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum PlatformType { Gitlab, Github, Codeberg, } +impl std::fmt::Display for PlatformType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlatformType::Gitlab => write!(f, "gitlab"), + PlatformType::Github => write!(f, "github"), + PlatformType::Codeberg => write!(f, "codeberg"), + } + } +} + +impl From for PlatformType { + fn from(s: String) -> Self { + match s.to_lowercase().as_str() { + "gitlab" => PlatformType::Gitlab, + "github" => PlatformType::Github, + "codeberg" => PlatformType::Codeberg, + _ => panic!("Invalid platform"), + } + } +} + +pub enum Direction { + Source, + Destination, +} + pub fn input_number() -> usize { loop { match input().parse::() { @@ -63,36 +90,69 @@ pub fn input_number() -> usize { } } -pub fn get_plateform(config: &mut Config, input_name: &str) -> Arc> { - println!("Choose a platform {}", input_name); - let platforms = [ - PlatformType::Gitlab, - PlatformType::Github, - PlatformType::Codeberg, - ]; - for (i, platform) in platforms.iter().enumerate() { - println!("{}: {:?}", i, platform); - } - let plateform = input_number(); - let correct: Box = match platforms[plateform] { +pub(crate) fn get_plateform( + config: &mut Config, + direction: Direction, +) -> (Arc>, PlatformType) { + let plateform_from_cli: Option = match direction { + Direction::Source => match &config.cli_args { + Some(GitMoverCli { + source: Some(source), + .. + }) => Some(source.clone().into()), + _ => None, + }, + Direction::Destination => match &config.cli_args { + Some(GitMoverCli { + destination: Some(destination), + .. + }) => Some(destination.clone().into()), + _ => None, + }, + }; + let chosen_platform = match plateform_from_cli { + Some(platform) => platform, + None => { + println!( + "Choose a platform {}", + match direction { + Direction::Source => "for source", + Direction::Destination => "for destination", + } + ); + let platforms = [ + PlatformType::Github, + PlatformType::Gitlab, + PlatformType::Codeberg, + ]; + for (i, platform) in platforms.iter().enumerate() { + println!("{}: {:?}", i, platform); + } + let plateform = input_number(); + platforms[plateform] + } + }; + let correct: Box = match chosen_platform { PlatformType::Gitlab => Box::new(GitlabConfig::get_plateform(config)), PlatformType::Github => Box::new(GithubConfig::get_plateform(config)), PlatformType::Codeberg => Box::new(CodebergConfig::get_plateform(config)), }; - Arc::new(correct) + (Arc::new(correct), chosen_platform) } -pub async fn cli_main(conf_path: Option) { - let mut config = match conf_path { - Some(path) => Config::new_from_path(&path), - None => Config::new(), - }; - - let source_plateform = get_plateform(&mut config, "for source"); - println!("Chosen {}", source_plateform.get_remote_url()); +pub(crate) async fn main_sync(config: &mut Config) { + let (source_plateform, type_source) = get_plateform(config, Direction::Source); + println!("Chosen {} as source", source_plateform.get_remote_url()); - let destination_platform = get_plateform(&mut config, "for destination"); - println!("Chosen {}", destination_platform.get_remote_url()); + let (destination_platform, type_dest) = get_plateform(config, Direction::Destination); + println!( + "Chosen {} as destination", + destination_platform.get_remote_url() + ); + if type_source == type_dest { + eprintln!("Source and destination can't be the same"); + return; + } let (repos_source, repos_destination) = join!( source_plateform.get_all_repos(), @@ -116,11 +176,22 @@ pub async fn cli_main(conf_path: Option) { }; let repos_source_without_fork = repos_source + .clone() .into_iter() .filter(|repo| !repo.fork) .collect::>(); + let repos_source_forks = repos_source + .clone() + .into_iter() + .filter(|repo| repo.fork) + .collect::>(); + println!("Number of repos in source: {}", repos_source.len()); + println!( + "- Number of forked repos in source: {}", + repos_source_forks.len() + ); println!( - "Number of repos in source: {}", + "- Number of (non-forked) repos in source: {}", repos_source_without_fork.len() ); println!( @@ -142,7 +213,13 @@ pub async fn cli_main(conf_path: Option) { println!("Number of repos to sync: {}", difference.len()); println!("Number of repos to delete: {}", missing_dest.len()); if !difference.is_empty() && yes_no_input("Do you want to start syncing ? (y/n)") { - match sync_repos(source_plateform, destination_platform.clone(), difference).await { + match sync_repos( + source_plateform.clone(), + destination_platform.clone(), + difference, + ) + .await + { Ok(_) => { println!("All repos synced"); } @@ -151,7 +228,38 @@ pub async fn cli_main(conf_path: Option) { } } } - if !missing_dest.is_empty() && yes_no_input("Do you want to delete the missing repos? (y/n)") { + if let Some(GitMoverCli { + no_forks: false, .. + }) = &config.cli_args + { + if !repos_source_forks.is_empty() + && yes_no_input( + format!( + "Do you want to sync forks ({})? (y/n)", + repos_source_forks.len() + ) + .as_str(), + ) + { + match sync_repos( + source_plateform, + destination_platform.clone(), + repos_source_forks, + ) + .await + { + Ok(_) => { + println!("All forks synced"); + } + Err(e) => { + eprintln!("Error syncing forks: {:?}", e); + } + } + } + } + if !missing_dest.is_empty() + && yes_no_input("Do you want to delete the missing repos (manually)? (y/n)") + { match delete_repos(destination_platform, missing_dest).await { Ok(_) => { println!("All repos deleted");