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::{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(long)]
41 pub and_clone: bool,
42
43 #[arg(long)]
45 pub and_push_from: Option<std::path::PathBuf>,
46}
47
48#[derive(Display, PartialEq, Eq, VariantArray)]
49enum CreatableFields {
50 DefaultBranch,
51 Description,
52 Private,
53}
54
55impl RepoCreateArgs {
56 pub async fn run(self, global_args: GlobalArgs) -> miette::Result<()> {
57 let ctx = BergContext::new(self, global_args.clone()).await?;
58
59 if ctx.args.and_clone && ctx.args.and_push_from.is_some() {
60 miette::bail!(
61 help = "Please choose either `--and-clone` OR `--and-push-from`",
62 "Detected both `--and-clone` AND `--and-push-from` which are mutually exclusive!"
63 );
64 }
65
66 let options = create_options(&ctx).await?;
67 let repo = options.name.clone();
68 let repository = if let Some(org) = ctx.args.org.as_ref() {
69 ctx.client.create_org_repo(org, options).await
70 } else {
71 ctx.client.create_current_user_repo(options).await
72 }
73 .map_err(|e| match e {
74 ForgejoError::ApiError(ApiError {
75 kind: ApiErrorKind::Other(code),
76 ..
77 }) if code.as_u16() == 409 => {
78 miette::miette!("Repository with the same name already exists!, got: {e}")
79 }
80 _ => miette::miette!("{e}"),
81 })?;
82 match ctx.global_args.output_mode {
83 OutputMode::Pretty => {
84 tracing::debug!("{repository:?}");
85 }
86 OutputMode::Json => repository.print_json()?,
87 }
88
89 if ctx.args.and_clone {
91 let user = ctx
92 .args
93 .org
94 .or(ctx.client.user_get_current().await.into_diagnostic()?.login)
95 .wrap_err_with(|| {
96 miette::miette!(
97 help = "Check your user settings",
98 help = "Check your forgejo instance or contact its admins",
99 "Currently logged in user has no valid username, aborting!"
100 )
101 })?;
102 let owner_repo = MaybeOwnerRepo::ExplicitOwner(OwnerRepo { owner: user, repo });
103 let ssh_clone = RepoCloneArgs {
104 owner_and_repo: owner_repo.clone(),
105 use_ssh: true,
106 destination: None,
107 }
108 .run(global_args.clone())
109 .await
110 .context("Cloning with ssh failed ... trying https as fallback");
111
112 if ssh_clone.is_err() {
113 RepoCloneArgs {
114 owner_and_repo: owner_repo,
115 use_ssh: false,
116 destination: None,
117 }
118 .run(global_args)
119 .await
120 .wrap_err(miette::miette!(
121 help = "This shouldn't happen. Please open an issue!",
122 "Cloning via ssh and via https both failed!"
123 ))?;
124 }
125 }
126
127 if let Some(repo_path) = ctx.args.and_push_from {
128 let url = repository
129 .ssh_url
130 .context("Created repository is missing URL!")?;
131 let git = Git::new(repo_path);
132 let remote_name = if !ctx.global_args.non_interactive {
133 let help_msg = [
134 "Please choose a remote that doesn't exist already!",
135 "Otherwise we'll try both 'origin' and 'origin-new'!",
136 ]
137 .join("\n");
138 Some(
139 inquire::Text::new(input_prompt_for("Remote Name").as_str())
140 .with_help_message(help_msg.as_str())
141 .prompt()
142 .into_diagnostic()?,
143 )
144 } else {
145 None
146 };
147 git.push(remote_name, &url)
148 .context("Failed to push git repository after remote creation")?;
149 println!("Successfully pushed to {url}", url = url.as_str());
150 }
151 Ok(())
152 }
153}
154
155async fn create_options(ctx: &BergContext<RepoCreateArgs>) -> miette::Result<CreateRepoOption> {
156 let name = match ctx.args.name.as_ref() {
157 Some(name) => name.clone(),
158 None => inquire::Text::new(input_prompt_for("New Repository Name").as_str())
159 .prompt()
160 .into_diagnostic()?,
161 };
162
163 let mut options = CreateRepoOption {
164 name,
165 auto_init: None,
166 default_branch: ctx.args.default_branch.clone(),
167 description: ctx.args.description.clone(),
168 gitignores: None,
169 issue_labels: None,
170 license: None,
171 private: ctx.args.private.map(|p| match p {
172 Privacy::Private => true,
173 Privacy::Public => false,
174 }),
175 readme: None,
176 template: None,
177 trust_model: None,
178 object_format_name: None,
179 };
180
181 let optional_data = {
182 use CreatableFields::*;
183 [
184 (DefaultBranch, ctx.args.default_branch.is_none()),
185 (Description, ctx.args.description.is_none()),
186 (Private, ctx.args.private.is_none()),
187 ]
188 .into_iter()
189 .filter_map(|(name, missing)| missing.then_some(name))
190 .collect::<Vec<_>>()
191 };
192
193 if !optional_data.is_empty() && !ctx.global_args.non_interactive {
194 let chosen_optionals = multi_fuzzy_select_with_key(
195 &optional_data,
196 "Choose optional properties",
197 |_| false,
198 |o| o.to_string(),
199 )?;
200
201 {
202 use CreatableFields::*;
203 options.default_branch.replace(repo_default_branch(
204 ctx,
205 chosen_optionals.contains(&&DefaultBranch),
206 )?);
207 options.private = repo_private(ctx, chosen_optionals.contains(&&Private)).await?;
208 options.description =
209 repo_description(ctx, chosen_optionals.contains(&&Description)).await?;
210 }
211 }
212
213 Ok(options)
214}
215
216fn repo_default_branch(
217 ctx: &BergContext<RepoCreateArgs>,
218 interactive: bool,
219) -> miette::Result<String> {
220 let branch = match ctx.args.default_branch.as_ref() {
221 Some(branch) => branch.clone(),
222 None => {
223 if !interactive {
224 return Ok(String::from("main"));
225 }
226 inquire::Text::new(input_prompt_for("Default Branch Name").as_str())
227 .prompt()
228 .into_diagnostic()?
229 }
230 };
231 Ok(branch)
232}
233
234async fn repo_private(
235 ctx: &BergContext<RepoCreateArgs>,
236 interactive: bool,
237) -> miette::Result<Option<bool>> {
238 let privacy = match ctx.args.private {
239 Some(privacy) => match privacy {
240 Privacy::Private => true,
241 Privacy::Public => false,
242 },
243 None => {
244 if !interactive {
245 return Ok(None);
246 }
247 fuzzy_select_with_key(
248 &[true, false],
249 select_prompt_for("repo privacy"),
250 |private| {
251 if *private {
252 String::from("Private")
253 } else {
254 String::from("Public")
255 }
256 },
257 )
258 .copied()?
259 }
260 };
261 Ok(Some(privacy))
262}
263
264async fn repo_description(
265 ctx: &BergContext<RepoCreateArgs>,
266 interactive: bool,
267) -> miette::Result<Option<String>> {
268 let description = match ctx.args.description.as_ref() {
269 Some(desc) => desc.clone(),
270 None => {
271 if !interactive {
272 return Ok(None);
273 }
274 ctx.editor_for("a description", "Enter Repository description")?
275 }
276 };
277 Ok(Some(description))
278}