codeberg_cli/actions/repo/
create.rs1use 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#[derive(Parser, Debug)]
18pub struct RepoCreateArgs {
19 #[arg(id = "default-branch", long)]
21 pub default_branch: Option<String>,
22
23 #[arg(short, long)]
25 pub description: Option<String>,
26
27 #[arg(long)]
29 pub org: Option<String>,
30
31 #[arg(short, long)]
33 pub name: Option<String>,
34
35 #[arg(short, long, value_enum, value_name = "VISIBILITY")]
37 pub private: Option<Privacy>,
38
39 #[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 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}