codeberg-cli 0.5.5

CLI Tool for codeberg similar to gh and glab
Documentation
use crate::actions::GlobalArgs;
use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
use crate::render::json::JsonToStdout;
use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key};
use crate::types::api::privacy_type::Privacy;
use crate::types::api::service_type::Service;
use crate::types::context::BergContext;
use clap::Parser;
use forgejo_api::structs::{MigrateRepoOptions, MigrateRepoOptionsService};
use miette::IntoDiagnostic;
use strum::{Display, VariantArray};

/// Create a new repository
#[derive(Parser, Debug)]
pub struct RepoMigrateArgs {
    /// The repository that's going to be migrated to forgejo
    #[arg(short, long)]
    pub clone_addr: Option<String>,
    /// The description of the repository *after* migration
    #[arg(long)]
    pub description: Option<String>,
    /// The visibility of the repository *after* migration
    #[arg(short, long, value_enum, value_name = "VISIBILITY")]
    pub private: Option<Privacy>,
    /// The repository name *after* the migration
    #[arg(id = "repo-name", short, long)]
    pub repo_name: Option<String>,
    /// The kind of service, we're going to migrate *from* (source location of repository)
    #[arg(short, long, value_enum, value_name = "SERVICE")]
    pub service: Option<Service>,
}

#[derive(Display, PartialEq, Eq, VariantArray)]
enum CreatableFields {
    Service,
    Description,
    Private,
}

impl RepoMigrateArgs {
    pub async fn run(self, global_args: GlobalArgs) -> miette::Result<()> {
        let ctx = BergContext::new(self, global_args).await?;

        let options = create_options(&ctx).await?;
        let repo = ctx.client.repo_migrate(options).await.into_diagnostic()?;
        match ctx.global_args.output_mode {
            crate::types::output::OutputMode::Pretty => {
                tracing::debug!("{repo:?}");
            }
            crate::types::output::OutputMode::Json => {
                repo.print_json()?;
            }
        }
        Ok(())
    }
}

async fn create_options(ctx: &BergContext<RepoMigrateArgs>) -> miette::Result<MigrateRepoOptions> {
    let repo_name = match ctx.args.repo_name.as_ref() {
        Some(name) => name.clone(),
        None => {
            if ctx.global_args.non_interactive {
                miette::bail!(
                    "You need to provide a target repository name address in non-interactive mode!"
                );
            }
            inquire::Text::new(input_prompt_for("New Repository Name").as_str())
                .prompt()
                .into_diagnostic()?
        }
    };

    let clone_addr = match ctx.args.clone_addr.as_ref() {
        Some(name) => name.clone(),
        None => {
            if ctx.global_args.non_interactive {
                miette::bail!("You need to provide a clone address in non-interactive mode!");
            }
            inquire::Text::new(input_prompt_for("Repository clone address").as_str())
                .prompt()
                .into_diagnostic()?
        }
    };

    let mut options = MigrateRepoOptions {
        auth_password: None,
        auth_token: None,
        auth_username: None,
        clone_addr,
        description: ctx.args.description.clone(),
        issues: None,
        labels: None,
        lfs: None,
        lfs_endpoint: None,
        milestones: None,
        mirror: None,
        mirror_interval: None,
        pull_requests: None,
        releases: None,
        repo_name,
        repo_owner: None,
        service: ctx.args.service.map(|service| service.into()),
        uid: None,
        wiki: None,
        private: ctx.args.private.map(|privacy| match privacy {
            Privacy::Private => true,
            Privacy::Public => false,
        }),
    };

    if !ctx.global_args.non_interactive {
        let optional_data = {
            use CreatableFields::*;
            [
                (Service, ctx.args.service.is_none()),
                (Description, ctx.args.description.is_none()),
                (Private, ctx.args.private.is_none()),
            ]
            .into_iter()
            .filter_map(|(name, missing)| missing.then_some(name))
            .collect::<Vec<_>>()
        };

        if !optional_data.is_empty() {
            let chosen_optionals = multi_fuzzy_select_with_key(
                &optional_data,
                "Choose optional properties",
                |_| false,
                |o| o.to_string(),
            )?;

            {
                use CreatableFields::*;
                options.service =
                    migrate_service(ctx, chosen_optionals.contains(&&Service)).await?;
                options.private = repo_private(ctx, chosen_optionals.contains(&&Private)).await?;
                options.description =
                    repo_description(ctx, chosen_optionals.contains(&&Description)).await?;
            }
        }
    }

    Ok(options)
}

async fn migrate_service(
    ctx: &BergContext<RepoMigrateArgs>,
    interactive: bool,
) -> miette::Result<Option<MigrateRepoOptionsService>> {
    let service = match ctx.args.service {
        Some(service) => service.to_string(),
        None => {
            use Service::*;
            if !interactive {
                return Ok(None);
            }
            fuzzy_select_with_key(
                &[
                    Git, Github, Gitea, Gitlab, Gogs, Onedev, Gitbucket, Codebase,
                ],
                select_prompt_for("service"),
                |service| service.to_string(),
            )
            .copied()?
            .to_string()
        }
    };
    let migraterepooptionsservice = serde_json::from_str(&service).unwrap();
    Ok(Some(migraterepooptionsservice))
}

async fn repo_private(
    ctx: &BergContext<RepoMigrateArgs>,
    interactive: bool,
) -> miette::Result<Option<bool>> {
    let privacy = match ctx.args.private {
        Some(privacy) => match privacy {
            Privacy::Private => true,
            Privacy::Public => false,
        },
        None => {
            if !interactive {
                return Ok(None);
            }
            fuzzy_select_with_key(
                &[true, false],
                select_prompt_for("repo privacy"),
                |private| {
                    if *private {
                        String::from("Private")
                    } else {
                        String::from("Public")
                    }
                },
            )
            .copied()?
        }
    };
    Ok(Some(privacy))
}

async fn repo_description(
    ctx: &BergContext<RepoMigrateArgs>,
    interactive: bool,
) -> miette::Result<Option<String>> {
    let description = match ctx.args.description.as_ref() {
        Some(desc) => desc.clone(),
        None => {
            if !interactive {
                return Ok(None);
            }
            ctx.editor_for("a description", "Enter Repository description")?
        }
    };
    Ok(Some(description))
}