codeberg_cli/actions/repo/
create.rs

1use crate::actions::GlobalArgs;
2use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
3use crate::render::json::JsonToStdout;
4use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key};
5use crate::types::api::privacy_type::Privacy;
6use crate::types::context::BergContext;
7use crate::types::output::OutputMode;
8use clap::Parser;
9use forgejo_api::structs::CreateRepoOption;
10use forgejo_api::{ApiError, ApiErrorKind, ForgejoError};
11use strum::{Display, VariantArray};
12
13/// Create a new repository
14#[derive(Parser, Debug)]
15pub struct RepoCreateArgs {
16    /// Main branch to init repository with (usually "main")
17    #[arg(id = "default-branch", long)]
18    pub default_branch: Option<String>,
19
20    /// Repository description
21    #[arg(short, long)]
22    pub description: Option<String>,
23
24    /// Organization name
25    #[arg(long)]
26    pub org: Option<String>,
27
28    /// Repository name
29    #[arg(short, long)]
30    pub name: Option<String>,
31
32    /// Repository visibility
33    #[arg(short, long, value_enum, value_name = "VISIBILITY")]
34    pub private: Option<Privacy>,
35}
36
37#[derive(Display, PartialEq, Eq, VariantArray)]
38enum CreatableFields {
39    DefaultBranch,
40    Description,
41    Private,
42}
43
44impl RepoCreateArgs {
45    pub async fn run(self, global_args: GlobalArgs) -> anyhow::Result<()> {
46        let ctx = BergContext::new(self, global_args).await?;
47
48        let options = create_options(&ctx).await?;
49        let pull_request = if let Some(org) = ctx.args.org.as_ref() {
50            ctx.client.create_org_repo(org, options).await
51        } else {
52            ctx.client.create_current_user_repo(options).await
53        }
54        .map_err(|e| match e {
55            ForgejoError::ApiError(ApiError {
56                kind: ApiErrorKind::Other(code),
57                ..
58            }) if code.as_u16() == 409 => {
59                anyhow::anyhow!("Repository with the same name already exists!, got: {e}")
60            }
61            _ => anyhow::anyhow!("{e}"),
62        })?;
63        match ctx.global_args.output_mode {
64            OutputMode::Pretty => {
65                tracing::debug!("{pull_request:?}");
66            }
67            OutputMode::Json => pull_request.print_json()?,
68        }
69        Ok(())
70    }
71}
72
73async fn create_options(ctx: &BergContext<RepoCreateArgs>) -> anyhow::Result<CreateRepoOption> {
74    let name = match ctx.args.name.as_ref() {
75        Some(name) => name.clone(),
76        None => inquire::Text::new(input_prompt_for("New Repository Name").as_str()).prompt()?,
77    };
78
79    let mut options = CreateRepoOption {
80        name,
81        auto_init: None,
82        default_branch: ctx.args.default_branch.clone(),
83        description: ctx.args.description.clone(),
84        gitignores: None,
85        issue_labels: None,
86        license: None,
87        private: ctx.args.private.map(|p| match p {
88            Privacy::Private => true,
89            Privacy::Public => false,
90        }),
91        readme: None,
92        template: None,
93        trust_model: None,
94        object_format_name: None,
95    };
96
97    let optional_data = {
98        use CreatableFields::*;
99        [
100            (DefaultBranch, ctx.args.default_branch.is_none()),
101            (Description, ctx.args.description.is_none()),
102            (Private, ctx.args.private.is_none()),
103        ]
104        .into_iter()
105        .filter_map(|(name, missing)| missing.then_some(name))
106        .collect::<Vec<_>>()
107    };
108
109    if !optional_data.is_empty() && !ctx.global_args.non_interactive {
110        let chosen_optionals = multi_fuzzy_select_with_key(
111            &optional_data,
112            "Choose optional properties",
113            |_| false,
114            |o| o.to_string(),
115        )?;
116
117        {
118            use CreatableFields::*;
119            options.default_branch.replace(repo_default_branch(
120                ctx,
121                chosen_optionals.contains(&&DefaultBranch),
122            )?);
123            options.private = repo_private(ctx, chosen_optionals.contains(&&Private)).await?;
124            options.description =
125                repo_description(ctx, chosen_optionals.contains(&&Description)).await?;
126        }
127    }
128
129    Ok(options)
130}
131
132fn repo_default_branch(
133    ctx: &BergContext<RepoCreateArgs>,
134    interactive: bool,
135) -> anyhow::Result<String> {
136    let branch = match ctx.args.default_branch.as_ref() {
137        Some(branch) => branch.clone(),
138        None => {
139            if !interactive {
140                return Ok(String::from("main"));
141            }
142            inquire::Text::new(input_prompt_for("Default Branch Name").as_str()).prompt()?
143        }
144    };
145    Ok(branch)
146}
147
148async fn repo_private(
149    ctx: &BergContext<RepoCreateArgs>,
150    interactive: bool,
151) -> anyhow::Result<Option<bool>> {
152    let privacy = match ctx.args.private {
153        Some(privacy) => match privacy {
154            Privacy::Private => true,
155            Privacy::Public => false,
156        },
157        None => {
158            if !interactive {
159                return Ok(None);
160            }
161            fuzzy_select_with_key(
162                &[true, false],
163                select_prompt_for("repo privacy"),
164                |private| {
165                    if *private {
166                        String::from("Private")
167                    } else {
168                        String::from("Public")
169                    }
170                },
171            )
172            .copied()?
173        }
174    };
175    Ok(Some(privacy))
176}
177
178async fn repo_description(
179    ctx: &BergContext<RepoCreateArgs>,
180    interactive: bool,
181) -> anyhow::Result<Option<String>> {
182    let description = match ctx.args.description.as_ref() {
183        Some(desc) => desc.clone(),
184        None => {
185            if !interactive {
186                return Ok(None);
187            }
188            ctx.editor_for("a description", "Enter Repository description")?
189        }
190    };
191    Ok(Some(description))
192}