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 anyhow::Context;
7use forgejo_api::structs::CreateReleaseOption;
8use itertools::Itertools;
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) -> anyhow::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 match ctx.global_args.output_mode {
54 crate::types::output::OutputMode::Pretty => {
55 tracing::debug!("{release:?}");
56 println!("Successfully released {tag_str}");
57 }
58 crate::types::output::OutputMode::Json => {
59 release.print_json()?;
60 }
61 }
62 Ok(())
63 }
64}
65
66async fn create_options(
67 ctx: &BergContext<CreateReleaseArgs>,
68) -> anyhow::Result<CreateReleaseOption> {
69 let tag_name = if ctx.global_args.non_interactive {
70 ctx.args
71 .tag
72 .clone()
73 .context("You have to specify a valid tag name in non-interactive mode!")?
74 } else {
75 release_tag(ctx).await?
76 };
77
78 let mut options = CreateReleaseOption {
79 tag_name,
80 body: ctx.args.body.clone(),
81 draft: None,
82 hide_archive_links: None,
83 name: ctx.args.name.clone(),
84 prerelease: None,
85 target_commitish: None,
86 };
87
88 if !ctx.global_args.non_interactive {
89 let optional_data = {
90 use CreatableFields::*;
91 [
92 (Body, ctx.args.body.is_none()),
93 (Name, ctx.args.name.is_none()),
94 ]
95 .into_iter()
96 .filter_map(|(name, missing)| missing.then_some(name))
97 .collect::<Vec<_>>()
98 };
99
100 if !optional_data.is_empty() {
101 let chosen_optionals = multi_fuzzy_select_with_key(
102 &optional_data,
103 "Choose optional properties",
104 |_| false,
105 |o| o.to_string(),
106 )
107 .context("No optional fields exist that need to be set!")?;
108
109 {
110 use CreatableFields::*;
111 options.body = release_body(ctx, chosen_optionals.contains(&&Body)).await?;
112 options.name = release_name(ctx, chosen_optionals.contains(&&Name)).await?;
113 }
114 }
115 }
116
117 Ok(options)
118}
119
120async fn release_tag(ctx: &BergContext<CreateReleaseArgs>) -> anyhow::Result<String> {
121 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
122
123 let (_, all_tags) = ctx
124 .client
125 .repo_list_tags(owner.as_str(), repo.as_str())
126 .await?;
127
128 let (version_tags, non_version_tags): (Vec<_>, Vec<_>) = all_tags
130 .iter()
131 .filter(|tag| tag.name.as_ref().is_some())
132 .partition(|tag| tag.name.as_ref().is_some_and(|name| name.starts_with("v")));
133
134 let all_tags = version_tags
135 .into_iter()
136 .sorted_by_key(|tag| tag.name.as_ref().map_or("", |v| v))
137 .chain(
138 non_version_tags
139 .into_iter()
140 .sorted_by_key(|tag| tag.name.as_ref().map_or("", |v| v)),
141 )
142 .collect::<Vec<_>>();
143
144 anyhow::ensure!(
145 !all_tags.is_empty(),
146 "You need to push some at least one tag manually before releasing!"
147 );
148
149 let tag = match &ctx.args.tag {
150 Some(tag_name) => all_tags
151 .iter()
152 .find(|t| t.name.as_ref().is_some_and(|name| name == tag_name))
153 .and_then(|tag| tag.id.clone())
154 .context(format!(
155 "Tag with name {tag_name} wasn't found. Check the spelling"
156 ))?,
157 None => {
158 let selected_tag = fuzzy_select_with_key(&all_tags, select_prompt_for("tags"), |t| {
159 t.name
160 .as_ref()
161 .cloned()
162 .unwrap_or_else(|| String::from("???"))
163 })
164 .context("No tags exist!")?;
165
166 selected_tag
167 .id
168 .clone()
169 .context("Selected tag's id is missing")?
170 }
171 };
172
173 Ok(tag)
174}
175
176async fn release_body(
177 ctx: &BergContext<CreateReleaseArgs>,
178 interactive: bool,
179) -> anyhow::Result<Option<String>> {
180 let body = match ctx.args.body.as_ref() {
181 Some(body) => body.clone(),
182 None => {
183 if !interactive {
184 return Ok(None);
185 }
186 ctx.editor_for("a description", "Enter a release description")?
187 }
188 };
189 Ok(Some(body))
190}
191
192async fn release_name(
193 ctx: &BergContext<CreateReleaseArgs>,
194 interactive: bool,
195) -> anyhow::Result<Option<String>> {
196 let name = match ctx.args.name.as_ref() {
197 Some(name) => name.clone(),
198 None => {
199 if !interactive {
200 return Ok(None);
201 }
202 inquire::Text::new(input_prompt_for("Release Name").as_str()).prompt()?
203 }
204 };
205 Ok(Some(name))
206}