Skip to main content

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 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/// 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) -> 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    // we want to show tags with a "v" prefix first
131    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}