diff --git a/Cargo.lock b/Cargo.lock index 5eec0c4..799041f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,7 +370,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git-mover" -version = "1.0.4" +version = "1.0.6" dependencies = [ "clap", "dotenv", diff --git a/Cargo.toml b/Cargo.toml index 6c73c4f..2a71c18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "git-mover" -version = "1.0.4" +version = "1.0.6" edition = "2021" keywords = ["utility", "git", "cli"] categories = ["command-line-utilities"] diff --git a/README.md b/README.md index c74de90..53a8284 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Move git repositories to a new location +[![asciicast](https://asciinema.org/a/Lfge8LlwsR9A2dKYMNT3v9Nh4.svg)](https://asciinema.org/a/Lfge8LlwsR9A2dKYMNT3v9Nh4) + ## Usage ```sh diff --git a/src/cli.rs b/src/cli.rs index 26d9c15..d159bcb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,6 +19,10 @@ pub struct GitMoverCli { #[arg(short, long = "no-forks")] pub no_forks: bool, + /// Resync all repositories + #[arg(short, long)] + pub resync: bool, + /// Custom configuration file #[arg(short, long)] pub config: Option, diff --git a/src/codeberg/platform.rs b/src/codeberg/platform.rs index 7b13ce1..93e5dc0 100644 --- a/src/codeberg/platform.rs +++ b/src/codeberg/platform.rs @@ -1,6 +1,5 @@ //! Codeberg platform implementation use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; -use serde::{Deserialize, Serialize}; use std::pin::Pin; use urlencoding::encode; @@ -12,18 +11,25 @@ use crate::{ }; /// Codeberg platform -#[derive(Deserialize, Serialize, Default, Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct CodebergPlatform { /// Codeberg username - pub username: String, + username: String, /// Codeberg token - pub token: String, + token: String, + + /// Reqwest client + client: reqwest::Client, } impl CodebergPlatform { /// Create a new codeberg platform pub fn new(username: String, token: String) -> Self { - Self { username, token } + Self { + username, + token, + client: reqwest::Client::new(), + } } } @@ -44,8 +50,8 @@ impl Platform for CodebergPlatform { let repo_name = repo.name.to_string(); let description = repo.description.to_string(); let private = repo.private; + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!("https://{}/api/v1/user/repos", CODEBERG_URL); let json_body = CodebergRepo { name: repo_name.to_string(), @@ -66,10 +72,11 @@ impl Platform for CodebergPlatform { let text = response.text().await?; let get_repo = match self.get_repo(repo_name.as_str()).await { Ok(repo) => repo, - Err(_e) => { + Err(e) => { + let text_error = format!("{} - {:?}", &text, e); return Err(GitMoverError::new(GitMoverErrorKind::RepoCreation) .with_platform(PlatformType::Codeberg) - .with_text(&text)); + .with_text(&text_error)); } }; let json_body_as_repo = json_body.clone().into(); @@ -94,8 +101,8 @@ impl Platform for CodebergPlatform { ) -> Pin> + Send + '_>> { let token = self.token.clone(); let repo_name = repo_name.to_string(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!( "https://{}/api/v1/repos/{}/{}", CODEBERG_URL, @@ -127,8 +134,8 @@ impl Platform for CodebergPlatform { ) -> Pin> + Send + '_>> { let repo = repo.clone(); let token = self.token.clone(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!( "https://{}/api/v1/repos/{}/{}", CODEBERG_URL, @@ -164,8 +171,8 @@ impl Platform for CodebergPlatform { &self, ) -> Pin, GitMoverError>> + Send>> { let token = self.token.clone(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!("https://{}/api/v1/user/repos", CODEBERG_URL); let mut page: usize = 1; let limit = 100; @@ -205,8 +212,8 @@ impl Platform for CodebergPlatform { ) -> Pin> + Send + '_>> { let token = self.token.clone(); let name = name.to_string(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!( "https://{}/api/v1/repos/{}/{}", CODEBERG_URL, diff --git a/src/errors.rs b/src/errors.rs index 88dcd58..8574f0a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -47,11 +47,11 @@ struct Inner { /// Error kind. kind: GitMoverErrorKind, - /// Source error. - source: Option, - /// Platform error platform: Option, + + /// Source error. + source: Option, } #[derive(Debug)] diff --git a/src/github/platform.rs b/src/github/platform.rs index 39611ab..a85a3b6 100644 --- a/src/github/platform.rs +++ b/src/github/platform.rs @@ -6,28 +6,31 @@ use crate::{ platform::{Platform, PlatformType}, utils::Repo, }; -use reqwest::{ - header::{ACCEPT, AUTHORIZATION, USER_AGENT}, - Client, -}; -use serde::{Deserialize, Serialize}; +use reqwest::header::{ACCEPT, AUTHORIZATION, USER_AGENT}; use std::pin::Pin; use urlencoding::encode; /// Github Platform -#[derive(Deserialize, Serialize, Default, Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct GithubPlatform { /// Github username - pub username: String, + username: String, /// Github token - pub token: String, + token: String, + + /// Reqwest client + client: reqwest::Client, } impl GithubPlatform { /// Create a new GithubPlatform - pub fn new(username: String, token: String) -> Self { - Self { username, token } + pub(crate) fn new(username: String, token: String) -> Self { + Self { + username, + token, + client: reqwest::Client::new(), + } } } @@ -46,8 +49,8 @@ impl Platform for GithubPlatform { ) -> Pin> + Send + '_>> { let token = self.token.clone(); let repo = repo.clone(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!("https://{}/user/repos", GITHUB_API_URL); let request = client .post(&url) @@ -63,10 +66,11 @@ impl Platform for GithubPlatform { let text = response.text().await?; let get_repo = match self.get_repo(repo.name.as_str()).await { Ok(repo) => repo, - Err(_) => { + Err(e) => { + let text_error = format!("{} - {:?}", &text, e); return Err(GitMoverError::new(GitMoverErrorKind::RepoCreation) .with_platform(PlatformType::Github) - .with_text(&text)) + .with_text(&text_error)); } }; if get_repo != repo { @@ -82,8 +86,8 @@ impl Platform for GithubPlatform { repo: Repo, ) -> Pin> + Send + '_>> { let token = self.token.clone(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!( "https://{}/repos/{}/{}", GITHUB_API_URL, @@ -116,8 +120,8 @@ impl Platform for GithubPlatform { let token = self.token.clone(); let username = self.username.clone(); let repo_name = repo_name.to_string(); + let client = self.client.clone(); Box::pin(async move { - let client = Client::new(); let url = format!( "https://{}/repos/{}/{}", GITHUB_API_URL, @@ -148,8 +152,8 @@ impl Platform for GithubPlatform { &self, ) -> Pin, GitMoverError>> + Send>> { let token = self.token.clone(); + let client = self.client.clone(); Box::pin(async move { - let client = Client::new(); let url = &format!("https://{}/user/repos", GITHUB_API_URL); let mut need_request = true; let mut page: usize = 1; @@ -194,8 +198,8 @@ impl Platform for GithubPlatform { ) -> Pin> + Send + '_>> { let token = self.token.clone(); let name = repo_name.to_string(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!( "https://{}/repos/{}/{}", GITHUB_API_URL, diff --git a/src/gitlab/platform.rs b/src/gitlab/platform.rs index 3219b36..2a35ef0 100644 --- a/src/gitlab/platform.rs +++ b/src/gitlab/platform.rs @@ -6,7 +6,6 @@ use crate::platform::PlatformType; use crate::utils::Repo; use reqwest::header::ACCEPT; use reqwest::header::CONTENT_TYPE; -use serde::{Deserialize, Serialize}; use std::pin::Pin; use urlencoding::encode; @@ -15,19 +14,26 @@ use super::repo::GitlabRepoEdition; use super::GITLAB_URL; /// Gitlab platform -#[derive(Deserialize, Serialize, Default, Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct GitlabPlatform { /// Gitlab username username: String, /// Gitlab token token: String, + + /// Reqwest client + client: reqwest::Client, } impl GitlabPlatform { /// Create a new Gitlab platform pub fn new(username: String, token: String) -> Self { - Self { username, token } + Self { + username, + token, + client: reqwest::Client::new(), + } } } @@ -46,8 +52,8 @@ impl Platform for GitlabPlatform { ) -> Pin> + Send + '_>> { let token = self.token.clone(); let repo = repo.clone(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!("https://{}/api/v4/projects", GITLAB_URL); let visibility = if repo.private { "private" } else { "public" }; let json_body = GitlabRepo { @@ -69,10 +75,11 @@ impl Platform for GitlabPlatform { let text = response.text().await?; let get_repo = match self.get_repo(repo.name.as_str()).await { Ok(repo) => repo, - Err(_e) => { + Err(e) => { + let text_error = format!("{} - {:?}", &text, e); return Err(GitMoverError::new(GitMoverErrorKind::RepoCreation) .with_platform(PlatformType::Gitlab) - .with_text(&text)) + .with_text(&text_error)); } }; let json_body_as_repo = json_body.into(); @@ -90,8 +97,8 @@ impl Platform for GitlabPlatform { ) -> Pin> + Send + '_>> { let token = self.token.clone(); let repo = repo.clone(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let repo_url = format!("{}/{}", self.get_username(), repo.name); let url = format!( "https://{}/api/v4/projects/{}", @@ -127,17 +134,15 @@ impl Platform for GitlabPlatform { ) -> Pin> + Send>> { let token = self.token.clone(); let name = name.to_string(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!("https://{}/api/v4/projects", GITLAB_URL); let request = client .get(&url) .header("PRIVATE-TOKEN", &token) .query(&[("owned", "true"), ("search", name.as_str())]) .send(); - let response = request.await?; - if !response.status().is_success() { let text = response.text().await?; return Err(GitMoverError::new(GitMoverErrorKind::GetRepo) @@ -159,8 +164,8 @@ impl Platform for GitlabPlatform { &self, ) -> Pin, GitMoverError>> + Send>> { let token = self.token.clone(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let url = format!("https://{}/api/v4/projects", GITLAB_URL); let mut need_request = true; let mut page: usize = 1; @@ -207,8 +212,8 @@ impl Platform for GitlabPlatform { ) -> Pin> + Send + '_>> { let token = self.token.clone(); let name = name.to_string(); + let client = self.client.clone(); Box::pin(async move { - let client = reqwest::Client::new(); let repo_url = format!("{}/{}", self.get_username(), name); let url = format!( "https://{}/api/v4/projects/{}", diff --git a/src/sync.rs b/src/sync.rs index 275011a..2e919c0 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -14,6 +14,7 @@ pub(crate) async fn sync_repos( source_platform: Arc>, destination_platform: Arc>, repos: Vec, + verbose: u8, ) -> Result<(), GitMoverError> { let rand_string: String = thread_rng() .sample_iter(&Alphanumeric) @@ -36,8 +37,13 @@ pub(crate) async fn sync_repos( let temp_dir_ref = temp_folder.clone(); set.spawn(async move { let repo_name = one_repo.name.clone(); - match sync_one_repo(source_ref, destination_ref, one_repo, temp_dir_ref).await { - Ok(_) => {} + match sync_one_repo(source_ref, destination_ref, one_repo, temp_dir_ref, verbose).await + { + Ok(_) => { + if verbose > 0 { + println!("({}) Successfully synced", repo_name); + } + } Err(e) => { eprintln!("Error syncing '{}': {:?}", repo_name, e); } @@ -51,6 +57,7 @@ pub(crate) async fn sync_repos( destination_platform, private_repos, temp_folder_priv, + verbose, ) .await { @@ -73,6 +80,7 @@ async fn sync_private_repos( destination_platform: Arc>, private_repos: Vec, temp_folder: PathBuf, + verbose: u8, ) -> Result<(), GitMoverError> { for one_repo in private_repos { let question = format!("Should sync private repo {} (y/n)", one_repo.name); @@ -80,7 +88,14 @@ async fn sync_private_repos( true => { let source_ref = source_platform.clone(); let destination_ref = destination_platform.clone(); - sync_one_repo(source_ref, destination_ref, one_repo, temp_folder.clone()).await?; + sync_one_repo( + source_ref, + destination_ref, + one_repo, + temp_folder.clone(), + verbose, + ) + .await?; } false => { println!("Skipping {}", one_repo.name); @@ -96,10 +111,13 @@ async fn sync_one_repo( destination_platform: Arc>, repo: Repo, temp_folder: PathBuf, -) -> Result { + verbose: u8, +) -> Result<(), GitMoverError> { let repo_cloned = repo.clone(); let repo_name = repo.name.clone(); - let log_text = format!("(background) Start syncing '{}'", repo_name); + if verbose > 1 { + println!("({}) Start syncing", repo_name); + } let tmp_repo_path = temp_folder.join(format!("{}.git", repo_name)); destination_platform.create_repo(repo_cloned).await?; @@ -122,13 +140,15 @@ async fn sync_one_repo( source_platform.get_username(), &repo_name ); - let log_text = format!( - "{}\n(background) Cloning '{}' at '{}'", - log_text, - url, - tmp_repo_path.display() - ); - let log_text = &format!("{}\n(background) Cloning from '{}'", log_text, url); + + if verbose > 3 { + println!( + "({}) Cloning from '{}' to '{}'", + repo_name, + url, + tmp_repo_path.display(), + ); + } let repo = builder.clone(&url, &tmp_repo_path)?; let next_remote = format!( @@ -147,14 +167,15 @@ async fn sync_one_repo( remote.connect_auth(git2::Direction::Push, Some(callbacks), None)?; let refs = repo.references()?; - let mut log_text = log_text.clone(); for reference in refs { let reference = reference?; let ref_name = match reference.name() { Some(name) => name, None => continue, }; - log_text.push_str(&format!("\n(background) Pushing '{}'", ref_name)); + if verbose > 3 { + println!("({}) Pushing '{}'", repo_name, ref_name); + } let ref_remote = format!("+{}:{}", ref_name, ref_name); let mut callbacks = git2::RemoteCallbacks::new(); callbacks.credentials(move |_url, username_from_url, _allowed| { @@ -171,11 +192,7 @@ async fn sync_one_repo( } } remove_dir_all(tmp_repo_path)?; - let log_text = format!( - "{}\n(background) Finished syncing '{}'", - log_text, repo_name - ); - Ok(log_text) + Ok(()) } /// Delete repositories from a platform @@ -183,8 +200,13 @@ pub(crate) async fn delete_repos( destination_platform: Arc>, repos: Vec, ) -> Result<(), GitMoverError> { - for one_repo in repos { - let question = format!("Should delete repo {} (y/n)", one_repo.name); + for (idx, one_repo) in repos.iter().enumerate() { + let question = format!( + "Should delete repo '{}' ({}/{}) (y/n)", + one_repo.name, + idx, + repos.len() + ); match yes_no_input(&question) { true => { destination_platform.delete_repo(&one_repo.name).await?; diff --git a/src/utils.rs b/src/utils.rs index 3401786..beae025 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -166,10 +166,15 @@ pub async fn main_sync(config: &mut Config) { .into_iter() .filter(|item| !item_source_set.contains(item)) .collect(); - let difference: Vec = cloned_repos_source_without_fork - .into_iter() - .filter(|item| !item_destination_set.contains(item)) - .collect(); + let resync = matches!(&config.cli_args, Some(GitMoverCli { resync: true, .. })); + let difference: Vec = if resync { + cloned_repos_source_without_fork + } else { + cloned_repos_source_without_fork + .into_iter() + .filter(|item| !item_destination_set.contains(item)) + .collect() + }; 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)") { @@ -177,6 +182,7 @@ pub async fn main_sync(config: &mut Config) { source_plateform.clone(), destination_platform.clone(), difference, + config.debug, ) .await { @@ -205,6 +211,7 @@ pub async fn main_sync(config: &mut Config) { source_plateform, destination_platform.clone(), repos_source_forks, + config.debug, ) .await {