codeberg_cli/actions/repo/
create.rs

1use crate::actions::GlobalArgs;
2use crate::actions::repo::clone::RepoCloneArgs;
3use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
4use crate::render::json::JsonToStdout;
5use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key};
6use crate::types::api::privacy_type::Privacy;
7use crate::types::context::BergContext;
8use crate::types::git::{MaybeOwnerRepo, OwnerRepo};
9use crate::types::output::OutputMode;
10use clap::Parser;
11use forgejo_api::structs::CreateRepoOption;
12use forgejo_api::{ApiError, ApiErrorKind, ForgejoError};
13use miette::{Context, IntoDiagnostic};
14use strum::{Display, VariantArray};
15
16/// Create a new repository
17#[derive(Parser, Debug)]
18pub struct RepoCreateArgs {
19    /// Main branch to init repository with (usually "main")
20    #[arg(id = "default-branch", long)]
21    pub default_branch: Option<String>,
22
23    /// Repository description
24    #[arg(short, long)]
25    pub description: Option<String>,
26
27    /// Organization name
28    #[arg(long)]
29    pub org: Option<String>,
30
31    /// Repository name
32    #[arg(short, long)]
33    pub name: Option<String>,
34
35    /// Repository visibility
36    #[arg(short, long, value_enum, value_name = "VISIBILITY")]
37    pub private: Option<Privacy>,
38
39    /// Whether or not to clone the repo after creating it remotely
40    #[arg(short, long)]
41    pub and_clone: bool,
42}
43
44#[derive(Display, PartialEq, Eq, VariantArray)]
45enum CreatableFields {
46    DefaultBranch,
47    Description,
48    Private,
49}
50
51impl RepoCreateArgs {
52    pub async fn run(self, global_args: GlobalArgs) -> miette::Result<()> {
53        let ctx = BergContext::new(self, global_args.clone()).await?;
54
55        let options = create_options(&ctx).await?;
56        let repo = options.name.clone();
57        let repository = if let Some(org) = ctx.args.org.as_ref() {
58            ctx.client.create_org_repo(org, options).await
59        } else {
60            ctx.client.create_current_user_repo(options).await
61        }
62        .map_err(|e| match e {
63            ForgejoError::ApiError(ApiError {
64                kind: ApiErrorKind::Other(code),
65                ..
66            }) if code.as_u16() == 409 => {
67                miette::miette!("Repository with the same name already exists!, got: {e}")
68            }
69            _ => miette::miette!("{e}"),
70        })?;
71        match ctx.global_args.output_mode {
72            OutputMode::Pretty => {
73                tracing::debug!("{repository:?}");
74            }
75            OutputMode::Json => repository.print_json()?,
76        }
77
78        // optionally clone the repository if user wishes so
79        if ctx.args.and_clone {
80            let user = ctx
81                .args
82                .org
83                .or(ctx.client.user_get_current().await.into_diagnostic()?.login)
84                .wrap_err_with(|| {
85                    miette::miette!(
86                        help = "Check your user settings",
87                        help = "Check your forgejo instance or contact its admins",
88                        "Currently logged in user has no valid username, aborting!"
89                    )
90                })?;
91            let owner_repo = MaybeOwnerRepo::ExplicitOwner(OwnerRepo { owner: user, repo });
92            let ssh_clone = RepoCloneArgs {
93                owner_and_repo: owner_repo.clone(),
94                use_ssh: true,
95                destination: None,
96            }
97            .run(global_args.clone())
98            .await
99            .context("Cloning with ssh failed ... trying https as fallback");
100
101            if ssh_clone.is_err() {
102                RepoCloneArgs {
103                    owner_and_repo: owner_repo,
104                    use_ssh: false,
105                    destination: None,
106                }
107                .run(global_args)
108                .await
109                .wrap_err(miette::miette!(
110                    help = "This shouldn't happen. Please open an issue!",
111                    "Cloning via ssh and via https both failed!"
112                ))?;
113            }
114        }
115        Ok(())
116    }
117}
118
119async fn create_options(ctx: &BergContext<RepoCreateArgs>) -> miette::Result<CreateRepoOption> {
120    let name = match ctx.args.name.as_ref() {
121        Some(name) => name.clone(),
122        None => inquire::Text::new(input_prompt_for("New Repository Name").as_str())
123            .prompt()
124            .into_diagnostic()?,
125    };
126
127    let mut options = CreateRepoOption {
128        name,
129        auto_init: None,
130        default_branch: ctx.args.default_branch.clone(),
131        description: ctx.args.description.clone(),
132        gitignores: None,
133        issue_labels: None,
134        license: None,
135        private: ctx.args.private.map(|p| match p {
136            Privacy::Private => true,
137            Privacy::Public => false,
138        }),
139        readme: None,
140        template: None,
141        trust_model: None,
142        object_format_name: None,
143    };
144
145    let optional_data = {
146        use CreatableFields::*;
147        [
148            (DefaultBranch, ctx.args.default_branch.is_none()),
149            (Description, ctx.args.description.is_none()),
150            (Private, ctx.args.private.is_none()),
151        ]
152        .into_iter()
153        .filter_map(|(name, missing)| missing.then_some(name))
154        .collect::<Vec<_>>()
155    };
156
157    if !optional_data.is_empty() && !ctx.global_args.non_interactive {
158        let chosen_optionals = multi_fuzzy_select_with_key(
159            &optional_data,
160            "Choose optional properties",
161            |_| false,
162            |o| o.to_string(),
163        )?;
164
165        {
166            use CreatableFields::*;
167            options.default_branch.replace(repo_default_branch(
168                ctx,
169                chosen_optionals.contains(&&DefaultBranch),
170            )?);
171            options.private = repo_private(ctx, chosen_optionals.contains(&&Private)).await?;
172            options.description =
173                repo_description(ctx, chosen_optionals.contains(&&Description)).await?;
174        }
175    }
176
177    Ok(options)
178}
179
180fn repo_default_branch(
181    ctx: &BergContext<RepoCreateArgs>,
182    interactive: bool,
183) -> miette::Result<String> {
184    let branch = match ctx.args.default_branch.as_ref() {
185        Some(branch) => branch.clone(),
186        None => {
187            if !interactive {
188                return Ok(String::from("main"));
189            }
190            inquire::Text::new(input_prompt_for("Default Branch Name").as_str())
191                .prompt()
192                .into_diagnostic()?
193        }
194    };
195    Ok(branch)
196}
197
198async fn repo_private(
199    ctx: &BergContext<RepoCreateArgs>,
200    interactive: bool,
201) -> miette::Result<Option<bool>> {
202    let privacy = match ctx.args.private {
203        Some(privacy) => match privacy {
204            Privacy::Private => true,
205            Privacy::Public => false,
206        },
207        None => {
208            if !interactive {
209                return Ok(None);
210            }
211            fuzzy_select_with_key(
212                &[true, false],
213                select_prompt_for("repo privacy"),
214                |private| {
215                    if *private {
216                        String::from("Private")
217                    } else {
218                        String::from("Public")
219                    }
220                },
221            )
222            .copied()?
223        }
224    };
225    Ok(Some(privacy))
226}
227
228async fn repo_description(
229    ctx: &BergContext<RepoCreateArgs>,
230    interactive: bool,
231) -> miette::Result<Option<String>> {
232    let description = match ctx.args.description.as_ref() {
233        Some(desc) => desc.clone(),
234        None => {
235            if !interactive {
236                return Ok(None);
237            }
238            ctx.editor_for("a description", "Enter Repository description")?
239        }
240    };
241    Ok(Some(description))
242}