codeberg_cli/actions/release/
create.rs1use crate::actions::GeneralArgs;
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, RepoListTagsQuery};
8use strum::*;
9
10use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
11
12use clap::Parser;
13
14#[derive(Parser, Debug)]
16pub struct CreateReleaseArgs {
17 #[arg(id = "description", short, long)]
19 pub body: Option<String>,
20
21 #[arg(short, long)]
23 pub name: Option<String>,
24
25 #[arg(short, long)]
27 pub tag: Option<String>,
28}
29
30#[derive(Display, PartialEq, Eq, VariantArray)]
31enum CreatableFields {
32 Body,
33 Name,
34}
35
36impl CreateReleaseArgs {
37 pub async fn run(self, general_args: GeneralArgs) -> anyhow::Result<()> {
38 let _ = general_args;
39 let ctx = BergContext::new(self, general_args).await?;
40
41 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
42 let options = create_options(&ctx).await?;
43 let release = ctx
44 .client
45 .repo_create_release(owner.as_str(), repo.as_str(), options)
46 .await?;
47 match general_args.output_mode {
48 crate::types::output::OutputMode::Pretty => {
49 tracing::debug!("{release:?}");
50 }
51 crate::types::output::OutputMode::Json => {
52 release.print_json()?;
53 }
54 }
55 Ok(())
56 }
57}
58
59async fn create_options(
60 ctx: &BergContext<CreateReleaseArgs>,
61) -> anyhow::Result<CreateReleaseOption> {
62 let tag_name = if ctx.general_args.non_interactive {
63 ctx.args
64 .tag
65 .clone()
66 .context("You have to specify a valid tag name in non-interactive mode!")?
67 } else {
68 release_tag(ctx).await?
69 };
70
71 let mut options = CreateReleaseOption {
72 tag_name,
73 body: ctx.args.body.clone(),
74 draft: None,
75 hide_archive_links: None,
76 name: ctx.args.name.clone(),
77 prerelease: None,
78 target_commitish: None,
79 };
80
81 if !ctx.general_args.non_interactive {
82 let optional_data = {
83 use CreatableFields::*;
84 [
85 (Body, ctx.args.body.is_none()),
86 (Name, ctx.args.name.is_none()),
87 ]
88 .into_iter()
89 .filter_map(|(name, missing)| missing.then_some(name))
90 .collect::<Vec<_>>()
91 };
92
93 let chosen_optionals = multi_fuzzy_select_with_key(
94 &optional_data,
95 "Choose optional properties",
96 |_| false,
97 |o| o.to_string(),
98 )?;
99
100 {
101 use CreatableFields::*;
102 options.body = release_body(ctx, chosen_optionals.contains(&&Body)).await?;
103 options.name = release_name(ctx, chosen_optionals.contains(&&Name)).await?;
104 }
105 }
106
107 Ok(options)
108}
109
110async fn release_tag(ctx: &BergContext<CreateReleaseArgs>) -> anyhow::Result<String> {
111 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
112
113 let (_, all_tags) = ctx
114 .client
115 .repo_list_tags(owner.as_str(), repo.as_str(), RepoListTagsQuery::default())
116 .await?;
117
118 let all_tags = all_tags
119 .iter()
120 .filter(|tag| tag.name.as_ref().is_some_and(|name| name.starts_with("v")))
121 .collect::<Vec<_>>();
122
123 let tag = match &ctx.args.tag {
124 Some(tag_name) => all_tags
125 .iter()
126 .find(|t| t.name.as_ref().is_some_and(|name| name == tag_name))
127 .and_then(|tag| tag.id.clone())
128 .context(format!(
129 "Tag with name {tag_name} wasn't found. Check the spelling"
130 ))?,
131 None => {
132 let selected_tag = fuzzy_select_with_key(&all_tags, select_prompt_for("tags"), |t| {
133 t.name
134 .as_ref()
135 .cloned()
136 .unwrap_or_else(|| String::from("???"))
137 })?;
138
139 selected_tag
140 .id
141 .clone()
142 .context("Selected tag's id is missing")?
143 }
144 };
145
146 Ok(tag)
147}
148
149async fn release_body(
150 ctx: &BergContext<CreateReleaseArgs>,
151 interactive: bool,
152) -> anyhow::Result<Option<String>> {
153 let body = match ctx.args.body.as_ref() {
154 Some(body) => body.clone(),
155 None => {
156 if !interactive {
157 return Ok(None);
158 }
159 ctx.editor_for("a description", "Enter a release description")?
160 }
161 };
162 Ok(Some(body))
163}
164
165async fn release_name(
166 ctx: &BergContext<CreateReleaseArgs>,
167 interactive: bool,
168) -> anyhow::Result<Option<String>> {
169 let name = match ctx.args.name.as_ref() {
170 Some(name) => name.clone(),
171 None => {
172 if !interactive {
173 return Ok(None);
174 }
175 inquire::Text::new(input_prompt_for("Release Name").as_str()).prompt()?
176 }
177 };
178 Ok(Some(name))
179}