Skip to main content

omni_dev/cli/atlassian/jira/
mod.rs

1//! JIRA CLI subcommands.
2
3pub(crate) mod attachment;
4pub(crate) mod board;
5pub(crate) mod changelog;
6pub(crate) mod comment;
7pub(crate) mod create;
8pub(crate) mod delete;
9pub(crate) mod dev;
10pub(crate) mod edit;
11pub(crate) mod field;
12pub(crate) mod link;
13pub(crate) mod project;
14pub(crate) mod read;
15pub(crate) mod search;
16pub(crate) mod sprint;
17pub(crate) mod transition;
18pub(crate) mod user;
19pub(crate) mod version;
20pub(crate) mod watcher;
21pub(crate) mod worklog;
22pub(crate) mod write;
23
24use anyhow::Result;
25use clap::{Parser, Subcommand};
26
27/// JIRA issue management, search, agile boards, and more.
28#[derive(Parser)]
29pub struct JiraCommand {
30    /// The JIRA subcommand to execute.
31    #[command(subcommand)]
32    pub command: JiraSubcommands,
33}
34
35/// JIRA subcommands.
36#[derive(Subcommand)]
37pub enum JiraSubcommands {
38    /// Fetches a JIRA issue and outputs it as JFM markdown or ADF JSON.
39    Read(read::ReadCommand),
40    /// Pushes content to a JIRA issue.
41    Write(write::WriteCommand),
42    /// Interactive fetch-edit-push cycle for a JIRA issue.
43    Edit(edit::EditCommand),
44    /// Searches JIRA issues using JQL.
45    Search(search::SearchCommand),
46    /// Creates a new JIRA issue.
47    Create(create::CreateCommand),
48    /// Lists or executes workflow transitions on a JIRA issue.
49    Transition(transition::TransitionCommand),
50    /// Manages comments on a JIRA issue.
51    Comment(comment::CommentCommand),
52    /// Deletes a JIRA issue.
53    Delete(delete::DeleteCommand),
54    /// Shows development status (linked PRs, branches, repositories) for a JIRA issue.
55    Dev(dev::DevCommand),
56    /// Lists JIRA projects.
57    Project(project::ProjectCommand),
58    /// Manages JIRA field definitions and options.
59    Field(field::FieldCommand),
60    /// Manages JIRA agile boards.
61    Board(board::BoardCommand),
62    /// Manages JIRA agile sprints.
63    Sprint(sprint::SprintCommand),
64    /// Manages JIRA issue links.
65    Link(link::LinkCommand),
66    /// Shows change history for JIRA issues.
67    Changelog(changelog::ChangelogCommand),
68    /// Downloads JIRA issue attachments.
69    Attachment(attachment::AttachmentCommand),
70    /// Manages watchers on a JIRA issue.
71    Watcher(watcher::WatcherCommand),
72    /// Manages worklogs (time tracking) on a JIRA issue.
73    Worklog(worklog::WorklogCommand),
74    /// JIRA user operations (search by name or email).
75    User(user::UserCommand),
76    /// Manages JIRA project versions (release versions).
77    Version(version::VersionCommand),
78}
79
80impl JiraCommand {
81    /// Executes the JIRA command.
82    pub async fn execute(self) -> Result<()> {
83        match self.command {
84            JiraSubcommands::Read(cmd) => cmd.execute().await,
85            JiraSubcommands::Write(cmd) => cmd.execute().await,
86            JiraSubcommands::Edit(cmd) => cmd.execute().await,
87            JiraSubcommands::Search(cmd) => cmd.execute().await,
88            JiraSubcommands::Create(cmd) => cmd.execute().await,
89            JiraSubcommands::Transition(cmd) => cmd.execute().await,
90            JiraSubcommands::Comment(cmd) => cmd.execute().await,
91            JiraSubcommands::Delete(cmd) => cmd.execute().await,
92            JiraSubcommands::Dev(cmd) => cmd.execute().await,
93            JiraSubcommands::Project(cmd) => cmd.execute().await,
94            JiraSubcommands::Field(cmd) => cmd.execute().await,
95            JiraSubcommands::Board(cmd) => cmd.execute().await,
96            JiraSubcommands::Sprint(cmd) => cmd.execute().await,
97            JiraSubcommands::Link(cmd) => cmd.execute().await,
98            JiraSubcommands::Changelog(cmd) => cmd.execute().await,
99            JiraSubcommands::Attachment(cmd) => cmd.execute().await,
100            JiraSubcommands::Watcher(cmd) => cmd.execute().await,
101            JiraSubcommands::Worklog(cmd) => cmd.execute().await,
102            JiraSubcommands::User(cmd) => cmd.execute().await,
103            JiraSubcommands::Version(cmd) => cmd.execute().await,
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::cli::atlassian::format::{ContentFormat, OutputFormat};
112
113    #[test]
114    fn jira_subcommands_read_variant() {
115        let cmd = JiraCommand {
116            command: JiraSubcommands::Read(read::ReadCommand {
117                key: "PROJ-1".to_string(),
118                output: None,
119                format: ContentFormat::Jfm,
120                fields: vec![],
121                all_fields: false,
122            }),
123        };
124        assert!(matches!(cmd.command, JiraSubcommands::Read(_)));
125    }
126
127    #[test]
128    fn jira_subcommands_write_variant() {
129        let cmd = JiraCommand {
130            command: JiraSubcommands::Write(write::WriteCommand {
131                key: "PROJ-1".to_string(),
132                file: None,
133                format: ContentFormat::Jfm,
134                no_content: false,
135                assignee: None,
136                reporter: None,
137                set_fields: vec![],
138                parent: None,
139                force: false,
140                dry_run: false,
141            }),
142        };
143        assert!(matches!(cmd.command, JiraSubcommands::Write(_)));
144    }
145
146    #[test]
147    fn jira_subcommands_edit_variant() {
148        let cmd = JiraCommand {
149            command: JiraSubcommands::Edit(edit::EditCommand {
150                key: "PROJ-1".to_string(),
151            }),
152        };
153        assert!(matches!(cmd.command, JiraSubcommands::Edit(_)));
154    }
155
156    #[test]
157    fn jira_subcommands_create_variant() {
158        let cmd = JiraCommand {
159            command: JiraSubcommands::Create(create::CreateCommand {
160                file: None,
161                format: ContentFormat::Jfm,
162                project: Some("PROJ".to_string()),
163                r#type: None,
164                summary: Some("Test".to_string()),
165                set_fields: vec![],
166                dry_run: false,
167            }),
168        };
169        assert!(matches!(cmd.command, JiraSubcommands::Create(_)));
170    }
171
172    #[test]
173    fn jira_subcommands_search_variant() {
174        let cmd = JiraCommand {
175            command: JiraSubcommands::Search(search::SearchCommand {
176                jql: Some("project = PROJ".to_string()),
177                project: None,
178                assignee: None,
179                status: None,
180                limit: 50,
181                output: OutputFormat::Table,
182            }),
183        };
184        assert!(matches!(cmd.command, JiraSubcommands::Search(_)));
185    }
186
187    #[test]
188    fn jira_subcommands_transition_variant() {
189        let cmd = JiraCommand {
190            command: JiraSubcommands::Transition(transition::TransitionCommand {
191                command: transition::TransitionSubcommands::Execute(transition::ExecuteCommand {
192                    key: "PROJ-1".to_string(),
193                    transition: "Done".to_string(),
194                }),
195            }),
196        };
197        assert!(matches!(cmd.command, JiraSubcommands::Transition(_)));
198    }
199
200    #[test]
201    fn jira_subcommands_comment_variant() {
202        let cmd = JiraCommand {
203            command: JiraSubcommands::Comment(comment::CommentCommand {
204                command: comment::CommentSubcommands::List(comment::ListCommand {
205                    key: "PROJ-1".to_string(),
206                    output: OutputFormat::Table,
207                    limit: 0,
208                }),
209            }),
210        };
211        assert!(matches!(cmd.command, JiraSubcommands::Comment(_)));
212    }
213
214    #[test]
215    fn jira_subcommands_delete_variant() {
216        let cmd = JiraCommand {
217            command: JiraSubcommands::Delete(delete::DeleteCommand {
218                key: "PROJ-1".to_string(),
219                force: true,
220                dry_run: false,
221            }),
222        };
223        assert!(matches!(cmd.command, JiraSubcommands::Delete(_)));
224    }
225
226    #[test]
227    fn jira_subcommands_dev_variant() {
228        let cmd = JiraCommand {
229            command: JiraSubcommands::Dev(dev::DevCommand {
230                key: "PROJ-1".to_string(),
231                r#type: None,
232                app: None,
233                summary: false,
234                output: OutputFormat::Table,
235            }),
236        };
237        assert!(matches!(cmd.command, JiraSubcommands::Dev(_)));
238    }
239
240    #[test]
241    fn jira_subcommands_project_variant() {
242        let cmd = JiraCommand {
243            command: JiraSubcommands::Project(project::ProjectCommand {
244                command: project::ProjectSubcommands::List(project::ListCommand {
245                    limit: 50,
246                    output: OutputFormat::Table,
247                }),
248            }),
249        };
250        assert!(matches!(cmd.command, JiraSubcommands::Project(_)));
251    }
252
253    #[test]
254    fn jira_subcommands_field_variant() {
255        let cmd = JiraCommand {
256            command: JiraSubcommands::Field(field::FieldCommand {
257                command: field::FieldSubcommands::List(field::ListCommand {
258                    search: None,
259                    output: OutputFormat::Table,
260                }),
261            }),
262        };
263        assert!(matches!(cmd.command, JiraSubcommands::Field(_)));
264    }
265
266    #[test]
267    fn jira_subcommands_board_variant() {
268        let cmd = JiraCommand {
269            command: JiraSubcommands::Board(board::BoardCommand {
270                command: board::BoardSubcommands::List(board::ListCommand {
271                    project: None,
272                    r#type: None,
273                    limit: 50,
274                    output: OutputFormat::Table,
275                }),
276            }),
277        };
278        assert!(matches!(cmd.command, JiraSubcommands::Board(_)));
279    }
280
281    #[test]
282    fn jira_subcommands_sprint_variant() {
283        let cmd = JiraCommand {
284            command: JiraSubcommands::Sprint(sprint::SprintCommand {
285                command: sprint::SprintSubcommands::List(sprint::ListCommand {
286                    board_id: 1,
287                    state: None,
288                    limit: 50,
289                    output: OutputFormat::Table,
290                }),
291            }),
292        };
293        assert!(matches!(cmd.command, JiraSubcommands::Sprint(_)));
294    }
295
296    #[test]
297    fn jira_subcommands_link_variant() {
298        let cmd = JiraCommand {
299            command: JiraSubcommands::Link(link::LinkCommand {
300                command: link::LinkSubcommands::Types(link::TypesCommand {
301                    output: OutputFormat::Table,
302                }),
303            }),
304        };
305        assert!(matches!(cmd.command, JiraSubcommands::Link(_)));
306    }
307
308    #[test]
309    fn jira_subcommands_changelog_variant() {
310        let cmd = JiraCommand {
311            command: JiraSubcommands::Changelog(changelog::ChangelogCommand {
312                keys: "PROJ-1".to_string(),
313                limit: 50,
314                output: OutputFormat::Table,
315            }),
316        };
317        assert!(matches!(cmd.command, JiraSubcommands::Changelog(_)));
318    }
319
320    #[test]
321    fn jira_subcommands_attachment_variant() {
322        let cmd = JiraCommand {
323            command: JiraSubcommands::Attachment(attachment::AttachmentCommand {
324                command: attachment::AttachmentSubcommands::Download(attachment::DownloadCommand {
325                    key: "PROJ-1".to_string(),
326                    output_dir: ".".to_string(),
327                    filter: None,
328                }),
329            }),
330        };
331        assert!(matches!(cmd.command, JiraSubcommands::Attachment(_)));
332    }
333
334    #[test]
335    fn jira_subcommands_watcher_variant() {
336        let cmd = JiraCommand {
337            command: JiraSubcommands::Watcher(watcher::WatcherCommand {
338                command: watcher::WatcherSubcommands::List(watcher::ListCommand {
339                    key: "PROJ-1".to_string(),
340                    output: OutputFormat::Table,
341                }),
342            }),
343        };
344        assert!(matches!(cmd.command, JiraSubcommands::Watcher(_)));
345    }
346
347    #[test]
348    fn jira_subcommands_worklog_variant() {
349        let cmd = JiraCommand {
350            command: JiraSubcommands::Worklog(worklog::WorklogCommand {
351                command: worklog::WorklogSubcommands::List(worklog::ListCommand {
352                    key: "PROJ-1".to_string(),
353                    limit: 50,
354                    output: OutputFormat::Table,
355                }),
356            }),
357        };
358        assert!(matches!(cmd.command, JiraSubcommands::Worklog(_)));
359    }
360
361    #[test]
362    fn jira_subcommands_user_variant() {
363        let cmd = JiraCommand {
364            command: JiraSubcommands::User(user::UserCommand {
365                command: user::UserSubcommands::Search(user::UserSearchCommand {
366                    query: "alice".to_string(),
367                    limit: 25,
368                    output: OutputFormat::Table,
369                }),
370            }),
371        };
372        assert!(matches!(cmd.command, JiraSubcommands::User(_)));
373    }
374
375    #[test]
376    fn jira_subcommands_version_variant() {
377        let cmd = JiraCommand {
378            command: JiraSubcommands::Version(version::VersionCommand {
379                command: version::VersionSubcommands::List(version::ListCommand {
380                    project: "PROJ".to_string(),
381                    released: false,
382                    unreleased: false,
383                    archived: false,
384                    unarchived: false,
385                    output: OutputFormat::Table,
386                }),
387            }),
388        };
389        assert!(matches!(cmd.command, JiraSubcommands::Version(_)));
390    }
391
392    /// Drives `JiraCommand::execute` end-to-end through the new `Version`
393    /// arm so the dispatch line in the match block is exercised. Other
394    /// arms remain pre-existing convention gaps.
395    #[tokio::test]
396    async fn jira_command_execute_version_arm() {
397        use crate::atlassian::auth::test_util::EnvGuard;
398
399        let server = wiremock::MockServer::start().await;
400        wiremock::Mock::given(wiremock::matchers::method("GET"))
401            .and(wiremock::matchers::path(
402                "/rest/api/3/project/PROJ/versions",
403            ))
404            .respond_with(
405                wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!([
406                    {"id": "1", "name": "1.0", "released": false, "archived": false}
407                ])),
408            )
409            .mount(&server)
410            .await;
411
412        let guard = EnvGuard::take();
413        let _home = guard.set_credentials(&server.uri());
414
415        let cmd = JiraCommand {
416            command: JiraSubcommands::Version(version::VersionCommand {
417                command: version::VersionSubcommands::List(version::ListCommand {
418                    project: "PROJ".to_string(),
419                    released: false,
420                    unreleased: false,
421                    archived: false,
422                    unarchived: false,
423                    output: OutputFormat::Yaml,
424                }),
425            }),
426        };
427        assert!(cmd.execute().await.is_ok());
428    }
429}