codeberg_cli/actions/release/
create.rs

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