codeberg_cli/actions/release/
create.rs1use crate::actions::GlobalArgs;
2use crate::render::json::JsonToStdout;
3use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key};
4use crate::types::context::BergContext;
5use crate::types::git::OwnerRepo;
6use forgejo_api::structs::CreateReleaseOption;
7use itertools::Itertools;
8use miette::{Context, IntoDiagnostic};
9use strum::*;
10
11use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
12
13use clap::Parser;
14
15#[derive(Parser, Debug)]
17pub struct CreateReleaseArgs {
18 #[arg(id = "description", short, long)]
20 pub body: Option<String>,
21
22 #[arg(short, long)]
24 pub name: Option<String>,
25
26 #[arg(short, long)]
28 pub tag: Option<String>,
29}
30
31#[derive(Display, PartialEq, Eq, VariantArray)]
32enum CreatableFields {
33 Body,
34 Name,
35}
36
37impl CreateReleaseArgs {
38 pub async fn run(self, global_args: GlobalArgs) -> miette::Result<()> {
39 let _ = global_args;
40 let ctx = BergContext::new(self, global_args).await?;
41
42 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
43 let options = create_options(&ctx).await?;
44 let tag_str = format!(
45 "{name} ({commit})",
46 name = options.name.as_ref().map_or("", |v| v),
47 commit = options.tag_name
48 );
49 let release = ctx
50 .client
51 .repo_create_release(owner.as_str(), repo.as_str(), options)
52 .await
53 .into_diagnostic()?;
54 match ctx.global_args.output_mode {
55 crate::types::output::OutputMode::Pretty => {
56 tracing::debug!("{release:?}");
57 println!("Successfully released {tag_str}");
58 }
59 crate::types::output::OutputMode::Json => {
60 release.print_json()?;
61 }
62 }
63 Ok(())
64 }
65}
66
67async fn create_options(
68 ctx: &BergContext<CreateReleaseArgs>,
69) -> miette::Result<CreateReleaseOption> {
70 let tag_name = if ctx.global_args.non_interactive {
71 ctx.args
72 .tag
73 .clone()
74 .context("You have to specify a valid tag name in non-interactive mode!")?
75 } else {
76 release_tag(ctx).await?
77 };
78
79 let mut options = CreateReleaseOption {
80 tag_name,
81 body: ctx.args.body.clone(),
82 draft: None,
83 hide_archive_links: None,
84 name: ctx.args.name.clone(),
85 prerelease: None,
86 target_commitish: None,
87 };
88
89 if !ctx.global_args.non_interactive {
90 let optional_data = {
91 use CreatableFields::*;
92 [
93 (Body, ctx.args.body.is_none()),
94 (Name, ctx.args.name.is_none()),
95 ]
96 .into_iter()
97 .filter_map(|(name, missing)| missing.then_some(name))
98 .collect::<Vec<_>>()
99 };
100
101 if !optional_data.is_empty() {
102 let chosen_optionals = multi_fuzzy_select_with_key(
103 &optional_data,
104 "Choose optional properties",
105 |_| false,
106 |o| o.to_string(),
107 )
108 .context("No optional fields exist that need to be set!")?;
109
110 {
111 use CreatableFields::*;
112 options.body = release_body(ctx, chosen_optionals.contains(&&Body)).await?;
113 options.name = release_name(ctx, chosen_optionals.contains(&&Name)).await?;
114 }
115 }
116 }
117
118 Ok(options)
119}
120
121async fn release_tag(ctx: &BergContext<CreateReleaseArgs>) -> miette::Result<String> {
122 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
123
124 let (_, all_tags) = ctx
125 .client
126 .repo_list_tags(owner.as_str(), repo.as_str())
127 .await
128 .into_diagnostic()?;
129
130 let (version_tags, non_version_tags): (Vec<_>, Vec<_>) = all_tags
132 .iter()
133 .filter(|tag| tag.name.as_ref().is_some())
134 .partition(|tag| tag.name.as_ref().is_some_and(|name| name.starts_with("v")));
135
136 let all_tags = version_tags
137 .into_iter()
138 .sorted_by_key(|tag| tag.name.as_ref().map_or("", |v| v))
139 .chain(
140 non_version_tags
141 .into_iter()
142 .sorted_by_key(|tag| tag.name.as_ref().map_or("", |v| v)),
143 )
144 .collect::<Vec<_>>();
145
146 miette::ensure!(
147 !all_tags.is_empty(),
148 "You need to push some at least one tag manually before releasing!"
149 );
150
151 let tag = match &ctx.args.tag {
152 Some(tag_name) => all_tags
153 .iter()
154 .find(|t| t.name.as_ref().is_some_and(|name| name == tag_name))
155 .and_then(|tag| tag.id.clone())
156 .context(format!(
157 "Tag with name {tag_name} wasn't found. Check the spelling"
158 ))?,
159 None => {
160 let selected_tag = fuzzy_select_with_key(&all_tags, select_prompt_for("tags"), |t| {
161 t.name
162 .as_ref()
163 .cloned()
164 .unwrap_or_else(|| String::from("???"))
165 })
166 .context("No tags exist!")?;
167
168 selected_tag
169 .id
170 .clone()
171 .context("Selected tag's id is missing")?
172 }
173 };
174
175 Ok(tag)
176}
177
178async fn release_body(
179 ctx: &BergContext<CreateReleaseArgs>,
180 interactive: bool,
181) -> miette::Result<Option<String>> {
182 let body = match ctx.args.body.as_ref() {
183 Some(body) => body.clone(),
184 None => {
185 if !interactive {
186 return Ok(None);
187 }
188 ctx.editor_for("a description", "Enter a release description")?
189 }
190 };
191 Ok(Some(body))
192}
193
194async fn release_name(
195 ctx: &BergContext<CreateReleaseArgs>,
196 interactive: bool,
197) -> miette::Result<Option<String>> {
198 let name = match ctx.args.name.as_ref() {
199 Some(name) => name.clone(),
200 None => {
201 if !interactive {
202 return Ok(None);
203 }
204 inquire::Text::new(input_prompt_for("Release Name").as_str())
205 .prompt()
206 .into_diagnostic()?
207 }
208 };
209 Ok(Some(name))
210}