git-mover 1.2.3

Move git repositories to a new location
Documentation
//! Gitlab platform implementation
use reqwest::header::ACCEPT;
use reqwest::header::CONTENT_TYPE;
use std::pin::Pin;
use urlencoding::encode;

use super::repo::GitlabRepo;
use super::repo::GitlabRepoEdition;
use super::GITLAB_URL;

use crate::errors::GitMoverError;
use crate::platform::Platform;
use crate::platform::PlatformType;
use crate::utils::Repo;

/// Gitlab platform
#[derive(Default, Debug, Clone)]
pub struct GitlabPlatform {
    /// Gitlab username
    username: String,

    /// Gitlab token
    token: String,

    /// Reqwest client
    client: reqwest::Client,

    /// Custom url,
    custom_url: Option<String>,
}

impl GitlabPlatform {
    /// Create a new Gitlab platform
    pub fn new(username: String, token: String, custom_url: Option<String>) -> Self {
        Self {
            username,
            token,
            client: reqwest::Client::new(),
            custom_url,
        }
    }
}

impl Platform for GitlabPlatform {
    fn get_remote_url(&self) -> String {
        match &self.custom_url {
            Some(cust_url) => cust_url.clone(),
            None => GITLAB_URL.to_string(),
        }
    }

    fn get_username(&self) -> &str {
        &self.username
    }

    fn get_type(&self) -> PlatformType {
        PlatformType::Gitlab
    }

    fn create_repo(
        &self,
        repo: Repo,
    ) -> Pin<Box<dyn std::future::Future<Output = Result<(), GitMoverError>> + Send + '_>> {
        let token = self.token.clone();
        let repo = repo.clone();
        let client = self.client.clone();
        Box::pin(async move {
            let url = format!("https://{GITLAB_URL}/api/v4/projects");
            let visibility = if repo.private { "private" } else { "public" };
            let json_body = GitlabRepo {
                name: repo.name.clone(),
                path: repo.path.clone(),
                description: Some(repo.description.clone()),
                visibility: visibility.to_string(),
                forked_from_project: None, // unused
            };
            let request = client
                .post(url)
                .header("PRIVATE-TOKEN", &token)
                .header(ACCEPT, "application/json")
                .header(CONTENT_TYPE, "application/json")
                .json(&json_body)
                .send();

            let response = request.await?;
            if !response.status().is_success() {
                let text = response.text().await?;
                let get_repo = match self.get_repo(repo.path.as_str()).await {
                    Ok(repo) => repo,
                    Err(e) => {
                        return Err(GitMoverError::new(format!(
                            "{text} for {}: {e}",
                            PlatformType::Gitlab
                        )));
                    }
                };
                let json_body_as_repo = json_body.into();
                if get_repo != json_body_as_repo {
                    return self.edit_repo(json_body_as_repo).await;
                }
            }
            Ok(())
        })
    }

    fn edit_repo(
        &self,
        repo: Repo,
    ) -> Pin<Box<dyn std::future::Future<Output = Result<(), GitMoverError>> + Send + '_>> {
        let token = self.token.clone();
        let repo = repo.clone();
        let client = self.client.clone();
        Box::pin(async move {
            let repo_url = format!("{}/{}", self.get_username(), repo.path);
            let url = format!(
                "https://{}/api/v4/projects/{}",
                GITLAB_URL,
                encode(&repo_url),
            );
            let json_body = GitlabRepoEdition {
                description: repo.description.clone(),
                visibility: (if repo.private { "private" } else { "public" }).to_string(),
            };
            let request = client
                .put(url)
                .header("PRIVATE-TOKEN", &token)
                .header(ACCEPT, "application/json")
                .header(CONTENT_TYPE, "application/json")
                .json(&json_body)
                .send();

            let response = request.await?;
            if !response.status().is_success() {
                let text = response.text().await?;
                return Err(GitMoverError::new(format!(
                    "{text} for {}",
                    PlatformType::Gitlab
                )));
            }
            Ok(())
        })
    }

    fn get_repo(
        &self,
        name: &str,
    ) -> Pin<Box<dyn std::future::Future<Output = Result<Repo, GitMoverError>> + Send>> {
        let token = self.token.clone();
        let name = name.to_string();
        let client = self.client.clone();
        Box::pin(async move {
            let url = format!("https://{GITLAB_URL}/api/v4/projects");
            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(format!(
                    "{text} for {}",
                    PlatformType::Gitlab
                )));
            }
            let text = response.text().await?;
            let repos = serde_json::from_str::<Vec<GitlabRepo>>(&text)?;
            match repos.into_iter().next() {
                Some(repo) => Ok(repo.into()),
                None => Err(GitMoverError::new(format!(
                    "{text} for {}",
                    PlatformType::Gitlab
                ))),
            }
        })
    }

    fn get_all_repos(
        &self,
    ) -> Pin<Box<dyn std::future::Future<Output = Result<Vec<Repo>, GitMoverError>> + Send>> {
        let token = self.token.clone();
        let client = self.client.clone();
        Box::pin(async move {
            let url = format!("https://{GITLAB_URL}/api/v4/projects");
            let mut need_request = true;
            let mut page: usize = 1;
            let mut all_repos = vec![];
            while need_request {
                let request = client
                    .get(&url)
                    .header("PRIVATE-TOKEN", &token)
                    .header(ACCEPT, "application/json")
                    .query(&[
                        ("per_page", "100"),
                        ("page", &page.to_string()),
                        ("owned", "true"),
                    ])
                    .send();

                let response = request.await?;
                if !response.status().is_success() {
                    let text = response.text().await?;
                    return Err(GitMoverError::new(format!(
                        "{text} for {}",
                        PlatformType::Gitlab
                    )));
                }
                let text = response.text().await?;
                let repos: Vec<GitlabRepo> = match serde_json::from_str(&text) {
                    Ok(repos) => repos,
                    Err(e) => return Err(e.into()),
                };
                let repos: Vec<Repo> = repos.into_iter().map(std::convert::Into::into).collect();
                if repos.is_empty() {
                    need_request = false;
                }
                println!("Requested gitlab (page {}): {}", page, repos.len());
                all_repos.extend(repos);
                page += 1;
            }
            Ok(all_repos)
        })
    }

    fn delete_repo(
        &self,
        name: &str,
    ) -> Pin<Box<dyn std::future::Future<Output = Result<(), GitMoverError>> + Send + '_>> {
        let token = self.token.clone();
        let name = name.to_string();
        let client = self.client.clone();
        Box::pin(async move {
            let repo_url = format!("{}/{}", self.get_username(), name);
            let url = format!(
                "https://{}/api/v4/projects/{}",
                GITLAB_URL,
                encode(&repo_url),
            );
            let request = client
                .delete(&url)
                .header("PRIVATE-TOKEN", &token)
                .header(ACCEPT, "application/json")
                .send();

            let response = request.await?;

            if !response.status().is_success() {
                let text = response.text().await?;
                return Err(GitMoverError::new(format!(
                    "{text} for {}",
                    PlatformType::Gitlab
                )));
            }
            Ok(())
        })
    }
}