codeberg_cli/actions/release/
create.rs

1use 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/// Create a release
16#[derive(Parser, Debug)]
17pub struct CreateReleaseArgs {
18    /// Main description of release
19    #[arg(id = "description", short, long)]
20    pub body: Option<String>,
21
22    /// Release name
23    #[arg(short, long)]
24    pub name: Option<String>,
25
26    /// Name of the tag to be released
27    #[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    // we want to show tags with a "v" prefix first
129    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}