use crate::actions::GlobalArgs;
use crate::actions::repo::clone::RepoCloneArgs;
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::context::BergContext;
use crate::types::git::{Git, MaybeOwnerRepo, OwnerRepo};
use crate::types::output::OutputMode;
use clap::Parser;
use forgejo_api::structs::CreateRepoOption;
use forgejo_api::{ApiError, ApiErrorKind, ForgejoError};
use miette::{Context, IntoDiagnostic};
use strum::{Display, VariantArray};
#[derive(Parser, Debug)]
pub struct RepoCreateArgs {
#[arg(id = "default-branch", long)]
pub default_branch: Option<String>,
#[arg(short, long)]
pub description: Option<String>,
#[arg(long)]
pub org: Option<String>,
#[arg(short, long)]
pub name: Option<String>,
#[arg(short, long, value_enum, value_name = "VISIBILITY")]
pub private: Option<Privacy>,
#[arg(long)]
pub and_clone: bool,
#[arg(long)]
pub and_push_from: Option<std::path::PathBuf>,
}
#[derive(Display, PartialEq, Eq, VariantArray)]
enum CreatableFields {
DefaultBranch,
Description,
Private,
}
impl RepoCreateArgs {
pub async fn run(self, global_args: GlobalArgs) -> miette::Result<()> {
let ctx = BergContext::new(self, global_args.clone()).await?;
if ctx.args.and_clone && ctx.args.and_push_from.is_some() {
miette::bail!(
help = "Please choose either `--and-clone` OR `--and-push-from`",
"Detected both `--and-clone` AND `--and-push-from` which are mutually exclusive!"
);
}
let options = create_options(&ctx).await?;
let repo = options.name.clone();
let repository = if let Some(org) = ctx.args.org.as_ref() {
ctx.client.create_org_repo(org, options).await
} else {
ctx.client.create_current_user_repo(options).await
}
.map_err(|e| match e {
ForgejoError::ApiError(ApiError {
kind: ApiErrorKind::Other(code),
..
}) if code.as_u16() == 409 => {
miette::miette!("Repository with the same name already exists!, got: {e}")
}
_ => miette::miette!("{e}"),
})?;
match ctx.global_args.output_mode {
OutputMode::Pretty => {
tracing::debug!("{repository:?}");
}
OutputMode::Json => repository.print_json()?,
}
if ctx.args.and_clone {
let user = ctx
.args
.org
.or(ctx.client.user_get_current().await.into_diagnostic()?.login)
.wrap_err_with(|| {
miette::miette!(
help = "Check your user settings",
help = "Check your forgejo instance or contact its admins",
"Currently logged in user has no valid username, aborting!"
)
})?;
let owner_repo = MaybeOwnerRepo::ExplicitOwner(OwnerRepo { owner: user, repo });
let ssh_clone = RepoCloneArgs {
owner_and_repo: owner_repo.clone(),
use_ssh: true,
destination: None,
}
.run(global_args.clone())
.await
.context("Cloning with ssh failed ... trying https as fallback");
if ssh_clone.is_err() {
RepoCloneArgs {
owner_and_repo: owner_repo,
use_ssh: false,
destination: None,
}
.run(global_args)
.await
.wrap_err(miette::miette!(
help = "This shouldn't happen. Please open an issue!",
"Cloning via ssh and via https both failed!"
))?;
}
}
if let Some(repo_path) = ctx.args.and_push_from {
let url = repository
.ssh_url
.context("Created repository is missing URL!")?;
let git = Git::new(repo_path);
let remote_name = if !ctx.global_args.non_interactive {
let help_msg = [
"Please choose a remote that doesn't exist already!",
"Otherwise we'll try both 'origin' and 'origin-new'!",
]
.join("\n");
Some(
inquire::Text::new(input_prompt_for("Remote Name").as_str())
.with_help_message(help_msg.as_str())
.prompt()
.into_diagnostic()?,
)
} else {
None
};
git.push(remote_name, &url)
.context("Failed to push git repository after remote creation")?;
println!("Successfully pushed to {url}", url = url.as_str());
}
Ok(())
}
}
async fn create_options(ctx: &BergContext<RepoCreateArgs>) -> miette::Result<CreateRepoOption> {
let name = match ctx.args.name.as_ref() {
Some(name) => name.clone(),
None => inquire::Text::new(input_prompt_for("New Repository Name").as_str())
.prompt()
.into_diagnostic()?,
};
let mut options = CreateRepoOption {
name,
auto_init: None,
default_branch: ctx.args.default_branch.clone(),
description: ctx.args.description.clone(),
gitignores: None,
issue_labels: None,
license: None,
private: ctx.args.private.map(|p| match p {
Privacy::Private => true,
Privacy::Public => false,
}),
readme: None,
template: None,
trust_model: None,
object_format_name: None,
};
let optional_data = {
use CreatableFields::*;
[
(DefaultBranch, ctx.args.default_branch.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() && !ctx.global_args.non_interactive {
let chosen_optionals = multi_fuzzy_select_with_key(
&optional_data,
"Choose optional properties",
|_| false,
|o| o.to_string(),
)?;
{
use CreatableFields::*;
options.default_branch.replace(repo_default_branch(
ctx,
chosen_optionals.contains(&&DefaultBranch),
)?);
options.private = repo_private(ctx, chosen_optionals.contains(&&Private)).await?;
options.description =
repo_description(ctx, chosen_optionals.contains(&&Description)).await?;
}
}
Ok(options)
}
fn repo_default_branch(
ctx: &BergContext<RepoCreateArgs>,
interactive: bool,
) -> miette::Result<String> {
let branch = match ctx.args.default_branch.as_ref() {
Some(branch) => branch.clone(),
None => {
if !interactive {
return Ok(String::from("main"));
}
inquire::Text::new(input_prompt_for("Default Branch Name").as_str())
.prompt()
.into_diagnostic()?
}
};
Ok(branch)
}
async fn repo_private(
ctx: &BergContext<RepoCreateArgs>,
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<RepoCreateArgs>,
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))
}