Skip to main content

omni_dev/cli/atlassian/
jira.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 (mirrors the `jira_read` MCP tool).
39    Read(read::ReadCommand),
40    /// Pushes content to a JIRA issue (mirrors the `jira_write` MCP tool).
41    Write(write::WriteCommand),
42    /// Interactive fetch-edit-push cycle for a JIRA issue.
43    Edit(edit::EditCommand),
44    /// Searches JIRA issues using JQL (mirrors the `jira_search` MCP tool).
45    Search(search::SearchCommand),
46    /// Creates a new JIRA issue (mirrors the `jira_create` MCP tool).
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 (mirrors the `jira_delete` MCP tool).
53    Delete(delete::DeleteCommand),
54    /// Shows development status (linked PRs, branches, repositories) for a JIRA issue (mirrors the `jira_dev` MCP tool).
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 (mirrors the `jira_changelog` MCP tool).
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                out_file: None,
119                output: None,
120                format: ContentFormat::Jfm,
121                fields: vec![],
122                all_fields: false,
123            }),
124        };
125        assert!(matches!(cmd.command, JiraSubcommands::Read(_)));
126    }
127
128    #[test]
129    fn jira_subcommands_write_variant() {
130        let cmd = JiraCommand {
131            command: JiraSubcommands::Write(write::WriteCommand {
132                key: "PROJ-1".to_string(),
133                file: None,
134                format: ContentFormat::Jfm,
135                no_content: false,
136                assignee: None,
137                reporter: None,
138                set_fields: vec![],
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                instance: None,
163                project: Some("PROJ".to_string()),
164                r#type: None,
165                summary: Some("Test".to_string()),
166                set_fields: vec![],
167                dry_run: false,
168            }),
169        };
170        assert!(matches!(cmd.command, JiraSubcommands::Create(_)));
171    }
172
173    #[test]
174    fn jira_subcommands_search_variant() {
175        let cmd = JiraCommand {
176            command: JiraSubcommands::Search(search::SearchCommand {
177                jql: Some("project = PROJ".to_string()),
178                project: None,
179                assignee: None,
180                status: None,
181                limit: 50,
182                output: OutputFormat::Table,
183            }),
184        };
185        assert!(matches!(cmd.command, JiraSubcommands::Search(_)));
186    }
187
188    #[test]
189    fn jira_subcommands_transition_variant() {
190        let cmd = JiraCommand {
191            command: JiraSubcommands::Transition(transition::TransitionCommand {
192                command: transition::TransitionSubcommands::Execute(transition::ExecuteCommand {
193                    key: "PROJ-1".to_string(),
194                    transition: "Done".to_string(),
195                    set_fields: vec![],
196                    resolution: None,
197                    comment: None,
198                }),
199            }),
200        };
201        assert!(matches!(cmd.command, JiraSubcommands::Transition(_)));
202    }
203
204    #[test]
205    fn jira_subcommands_comment_variant() {
206        let cmd = JiraCommand {
207            command: JiraSubcommands::Comment(comment::CommentCommand {
208                command: comment::CommentSubcommands::List(comment::ListCommand {
209                    key: "PROJ-1".to_string(),
210                    output: OutputFormat::Table,
211                    limit: 0,
212                }),
213            }),
214        };
215        assert!(matches!(cmd.command, JiraSubcommands::Comment(_)));
216    }
217
218    #[test]
219    fn jira_subcommands_delete_variant() {
220        let cmd = JiraCommand {
221            command: JiraSubcommands::Delete(delete::DeleteCommand {
222                key: "PROJ-1".to_string(),
223                force: true,
224                dry_run: false,
225            }),
226        };
227        assert!(matches!(cmd.command, JiraSubcommands::Delete(_)));
228    }
229
230    #[test]
231    fn jira_subcommands_dev_variant() {
232        let cmd = JiraCommand {
233            command: JiraSubcommands::Dev(dev::DevCommand {
234                key: "PROJ-1".to_string(),
235                r#type: None,
236                app: None,
237                summary: false,
238                output: OutputFormat::Table,
239            }),
240        };
241        assert!(matches!(cmd.command, JiraSubcommands::Dev(_)));
242    }
243
244    #[test]
245    fn jira_subcommands_project_variant() {
246        let cmd = JiraCommand {
247            command: JiraSubcommands::Project(project::ProjectCommand {
248                command: project::ProjectSubcommands::List(project::ListCommand {
249                    limit: 50,
250                    output: OutputFormat::Table,
251                }),
252            }),
253        };
254        assert!(matches!(cmd.command, JiraSubcommands::Project(_)));
255    }
256
257    #[test]
258    fn jira_subcommands_field_variant() {
259        let cmd = JiraCommand {
260            command: JiraSubcommands::Field(field::FieldCommand {
261                command: field::FieldSubcommands::List(field::ListCommand {
262                    search: None,
263                    output: OutputFormat::Table,
264                }),
265            }),
266        };
267        assert!(matches!(cmd.command, JiraSubcommands::Field(_)));
268    }
269
270    #[test]
271    fn jira_subcommands_board_variant() {
272        let cmd = JiraCommand {
273            command: JiraSubcommands::Board(board::BoardCommand {
274                command: board::BoardSubcommands::List(board::ListCommand {
275                    project: None,
276                    r#type: None,
277                    limit: 50,
278                    output: OutputFormat::Table,
279                }),
280            }),
281        };
282        assert!(matches!(cmd.command, JiraSubcommands::Board(_)));
283    }
284
285    #[test]
286    fn jira_subcommands_sprint_variant() {
287        let cmd = JiraCommand {
288            command: JiraSubcommands::Sprint(sprint::SprintCommand {
289                command: sprint::SprintSubcommands::List(sprint::ListCommand {
290                    board_id: 1,
291                    state: None,
292                    limit: 50,
293                    output: OutputFormat::Table,
294                }),
295            }),
296        };
297        assert!(matches!(cmd.command, JiraSubcommands::Sprint(_)));
298    }
299
300    #[test]
301    fn jira_subcommands_link_variant() {
302        let cmd = JiraCommand {
303            command: JiraSubcommands::Link(link::LinkCommand {
304                command: link::LinkSubcommands::Types(link::TypesCommand {
305                    output: OutputFormat::Table,
306                }),
307            }),
308        };
309        assert!(matches!(cmd.command, JiraSubcommands::Link(_)));
310    }
311
312    #[test]
313    fn jira_subcommands_changelog_variant() {
314        let cmd = JiraCommand {
315            command: JiraSubcommands::Changelog(changelog::ChangelogCommand {
316                keys: "PROJ-1".to_string(),
317                limit: 50,
318                output: OutputFormat::Table,
319            }),
320        };
321        assert!(matches!(cmd.command, JiraSubcommands::Changelog(_)));
322    }
323
324    #[test]
325    fn jira_subcommands_attachment_variant() {
326        let cmd = JiraCommand {
327            command: JiraSubcommands::Attachment(attachment::AttachmentCommand {
328                command: attachment::AttachmentSubcommands::Download(attachment::DownloadCommand {
329                    key: "PROJ-1".to_string(),
330                    output_dir: ".".to_string(),
331                    filter: None,
332                }),
333            }),
334        };
335        assert!(matches!(cmd.command, JiraSubcommands::Attachment(_)));
336    }
337
338    #[test]
339    fn jira_subcommands_watcher_variant() {
340        let cmd = JiraCommand {
341            command: JiraSubcommands::Watcher(watcher::WatcherCommand {
342                command: watcher::WatcherSubcommands::List(watcher::ListCommand {
343                    key: "PROJ-1".to_string(),
344                    output: OutputFormat::Table,
345                }),
346            }),
347        };
348        assert!(matches!(cmd.command, JiraSubcommands::Watcher(_)));
349    }
350
351    #[test]
352    fn jira_subcommands_worklog_variant() {
353        let cmd = JiraCommand {
354            command: JiraSubcommands::Worklog(worklog::WorklogCommand {
355                command: worklog::WorklogSubcommands::List(worklog::ListCommand {
356                    key: "PROJ-1".to_string(),
357                    limit: 50,
358                    output: OutputFormat::Table,
359                }),
360            }),
361        };
362        assert!(matches!(cmd.command, JiraSubcommands::Worklog(_)));
363    }
364
365    #[test]
366    fn jira_subcommands_user_variant() {
367        let cmd = JiraCommand {
368            command: JiraSubcommands::User(user::UserCommand {
369                command: user::UserSubcommands::Search(user::UserSearchCommand {
370                    query: "alice".to_string(),
371                    limit: 25,
372                    output: OutputFormat::Table,
373                }),
374            }),
375        };
376        assert!(matches!(cmd.command, JiraSubcommands::User(_)));
377    }
378
379    #[test]
380    fn jira_subcommands_version_variant() {
381        let cmd = JiraCommand {
382            command: JiraSubcommands::Version(version::VersionCommand {
383                command: version::VersionSubcommands::List(version::ListCommand {
384                    project: "PROJ".to_string(),
385                    released: false,
386                    unreleased: false,
387                    archived: false,
388                    unarchived: false,
389                    output: OutputFormat::Table,
390                }),
391            }),
392        };
393        assert!(matches!(cmd.command, JiraSubcommands::Version(_)));
394    }
395
396    /// Drives `JiraCommand::execute` end-to-end through the new `Version`
397    /// arm so the dispatch line in the match block is exercised. Other
398    /// arms remain pre-existing convention gaps.
399    #[tokio::test]
400    async fn jira_command_execute_version_arm() {
401        use crate::atlassian::auth::test_util::EnvGuard;
402
403        let server = wiremock::MockServer::start().await;
404        wiremock::Mock::given(wiremock::matchers::method("GET"))
405            .and(wiremock::matchers::path(
406                "/rest/api/3/project/PROJ/versions",
407            ))
408            .respond_with(
409                wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!([
410                    {"id": "1", "name": "1.0", "released": false, "archived": false}
411                ])),
412            )
413            .mount(&server)
414            .await;
415
416        let guard = EnvGuard::take();
417        let _home = guard.set_credentials(&server.uri());
418
419        let cmd = JiraCommand {
420            command: JiraSubcommands::Version(version::VersionCommand {
421                command: version::VersionSubcommands::List(version::ListCommand {
422                    project: "PROJ".to_string(),
423                    released: false,
424                    unreleased: false,
425                    archived: false,
426                    unarchived: false,
427                    output: OutputFormat::Yaml,
428                }),
429            }),
430        };
431        assert!(cmd.execute().await.is_ok());
432    }
433}