codeberg_cli/actions/issue/
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 crate::types::output::OutputMode;
7use anyhow::Context;
8use forgejo_api::structs::{CreateIssueOption, IssueGetMilestonesListQuery, IssueListLabelsQuery};
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 CreateIssueArgs {
18 #[arg(short, long)]
20 pub title: Option<String>,
21
22 #[arg(short, long, value_name = "LABEL,...", value_delimiter = ',')]
24 pub labels: Option<Vec<String>>,
25
26 #[arg(id = "description", short, long)]
28 pub body: Option<String>,
29
30 #[arg(short, long, value_name = "ASSIGNEE,...", value_delimiter = ',')]
32 pub assignees: Option<Vec<String>>,
33
34 #[arg(short, long)]
36 pub milestone: Option<String>,
37}
38
39#[derive(Display, PartialEq, Eq, VariantArray)]
40enum CreatableFields {
41 Labels,
42 Assignees,
43 Milestone,
44}
45
46impl CreateIssueArgs {
47 pub async fn run(self, general_args: GeneralArgs) -> anyhow::Result<()> {
48 let ctx = BergContext::new(self, general_args).await?;
49 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
50 let options = create_options(&ctx).await?;
51 tracing::debug!("{options:?}");
52 let issue = ctx
53 .client
54 .issue_create_issue(owner.as_str(), repo.as_str(), options)
55 .await?;
56 match general_args.output_mode {
57 OutputMode::Pretty => {
58 tracing::debug!("{issue:?}");
59 }
60 OutputMode::Json => issue.print_json()?,
61 }
62 Ok(())
63 }
64}
65
66async fn create_options(ctx: &BergContext<CreateIssueArgs>) -> anyhow::Result<CreateIssueOption> {
67 let title = ctx
68 .args
69 .title
70 .as_ref()
71 .cloned()
72 .map(Ok)
73 .unwrap_or_else(|| {
74 if ctx.general_args.non_interactive {
75 anyhow::bail!("You have to set an issue title!");
76 } else {
77 inquire::Text::new(input_prompt_for("Issue Title").as_str())
78 .prompt()
79 .map_err(anyhow::Error::from)
80 }
81 })?;
82
83 let mut options = CreateIssueOption {
84 title,
85 assignee: None,
86 assignees: None,
87 body: None,
88 closed: None,
89 due_date: None,
90 labels: None,
91 milestone: None,
92 r#ref: None,
93 };
94
95 options.body.replace(issue_body(ctx)?);
96
97 if !ctx.general_args.non_interactive {
98 let optional_data = {
99 use CreatableFields::*;
100 [
101 (Labels, ctx.args.labels.is_none()),
102 (Assignees, ctx.args.assignees.is_none()),
103 (Milestone, ctx.args.milestone.is_none()),
104 ]
105 .into_iter()
106 .filter_map(|(name, missing)| missing.then_some(name))
107 .collect::<Vec<_>>()
108 };
109
110 let chosen_optionals = multi_fuzzy_select_with_key(
111 &optional_data,
112 "Choose optional properties",
113 |_| false,
114 |o| o.to_string(),
115 )?;
116 {
117 use CreatableFields::*;
118 options.labels = issue_labels(ctx, chosen_optionals.contains(&&Labels)).await?;
119 options.assignees =
120 issue_assignees(ctx, chosen_optionals.contains(&&Assignees)).await?;
121 options.milestone =
122 issue_milestone(ctx, chosen_optionals.contains(&&Milestone)).await?;
123 }
124 }
125
126 Ok(options)
127}
128
129fn issue_body(ctx: &BergContext<CreateIssueArgs>) -> anyhow::Result<String> {
130 let body = match ctx.args.body.as_ref() {
131 Some(body) => body.clone(),
132 None => {
133 if ctx.general_args.non_interactive {
134 anyhow::bail!("You need to provide an issue description!");
135 } else {
136 ctx.editor_for("a description", "Enter a issue description")?
137 }
138 }
139 };
140 Ok(body)
141}
142
143async fn issue_labels(
144 ctx: &BergContext<CreateIssueArgs>,
145 interactive: bool,
146) -> anyhow::Result<Option<Vec<i64>>> {
147 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
148
149 let (_, all_labels) = ctx
150 .client
151 .issue_list_labels(
152 owner.as_str(),
153 repo.as_str(),
154 IssueListLabelsQuery::default(),
155 )
156 .await?;
157
158 let labels = match &ctx.args.labels {
159 Some(label_names) => {
160 let label_ids = all_labels
161 .iter()
162 .filter(|l| {
163 l.name
164 .as_ref()
165 .is_some_and(|name| label_names.contains(name))
166 })
167 .filter_map(|l| l.id)
168 .collect::<Vec<_>>();
169 Some(label_ids)
170 }
171 None => {
172 if !interactive {
173 return Ok(None);
174 }
175
176 let selected_labels = multi_fuzzy_select_with_key(
177 &all_labels,
178 select_prompt_for("labels"),
179 |_| false,
180 |l| {
181 l.name
182 .as_ref()
183 .cloned()
184 .unwrap_or_else(|| String::from("???"))
185 },
186 )?;
187
188 let label_ids = selected_labels
189 .iter()
190 .filter_map(|l| l.id)
191 .collect::<Vec<_>>();
192
193 Some(label_ids)
194 }
195 };
196 Ok(labels)
197}
198
199async fn issue_assignees(
200 ctx: &BergContext<CreateIssueArgs>,
201 interactive: bool,
202) -> anyhow::Result<Option<Vec<String>>> {
203 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
204
205 let (_, all_assignees) = ctx
206 .client
207 .repo_get_assignees(owner.as_str(), repo.as_str())
208 .await?;
209 let assignees = match &ctx.args.assignees {
210 Some(assignees_names) => Some(assignees_names.clone()),
211 None => {
212 tracing::debug!("Hey");
213 if !interactive {
214 return Ok(None);
215 }
216
217 let selected_assignees = multi_fuzzy_select_with_key(
218 &all_assignees,
219 select_prompt_for("assignees"),
220 |_| false,
221 |u| {
222 u.login
223 .as_ref()
224 .cloned()
225 .unwrap_or_else(|| String::from("???"))
226 },
227 )?;
228
229 Some(
230 selected_assignees
231 .into_iter()
232 .filter_map(|u| u.login.as_ref().cloned())
233 .collect::<Vec<_>>(),
234 )
235 }
236 };
237
238 Ok(assignees)
239}
240
241async fn issue_milestone(
242 ctx: &BergContext<CreateIssueArgs>,
243 interactive: bool,
244) -> anyhow::Result<Option<i64>> {
245 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
246
247 let (_, all_milestones) = ctx
248 .client
249 .issue_get_milestones_list(
250 owner.as_str(),
251 repo.as_str(),
252 IssueGetMilestonesListQuery::default(),
253 )
254 .await?;
255
256 let milestone = match &ctx.args.milestone {
257 Some(milestone_name) => Some(
258 all_milestones
259 .iter()
260 .find(|m| m.title.as_ref().is_some_and(|name| name == milestone_name))
261 .and_then(|milestone| milestone.id)
262 .context(format!(
263 "Milestone with name {milestone_name} wasn't found. Check the spelling"
264 ))?,
265 ),
266 None => {
267 if !interactive {
268 return Ok(None);
269 }
270 let selected_milestone =
271 fuzzy_select_with_key(&all_milestones, select_prompt_for("milestones"), |m| {
272 m.title
273 .as_ref()
274 .cloned()
275 .unwrap_or_else(|| String::from("???"))
276 })?;
277
278 Some(
279 selected_milestone
280 .id
281 .context("Selected milestone's id is missing")?,
282 )
283 }
284 };
285
286 Ok(milestone)
287}