gr/cli/
merge_request.rs

1use std::option::Option;
2
3use clap::{Parser, ValueEnum};
4
5use crate::cmds::merge_request::{
6    CommentMergeRequestCliArgs, CommentMergeRequestListCliArgs, MergeRequestCliArgs,
7    MergeRequestGetCliArgs, MergeRequestListCliArgs, MergeRequestState, SummaryOptions,
8};
9
10use super::common::{validate_project_repo_path, CacheArgs, GetArgs, ListArgs};
11
12#[derive(Parser)]
13pub struct MergeRequestCommand {
14    #[clap(subcommand)]
15    subcommand: MergeRequestSubcommand,
16}
17
18#[derive(Parser)]
19enum MergeRequestSubcommand {
20    #[clap(about = "Creates a merge request", visible_alias = "cr")]
21    Create(CreateMergeRequest),
22    #[clap(about = "Approve a merge request", visible_alias = "ap")]
23    Approve(ApproveMergeRequest),
24    #[clap(about = "Merge a merge request")]
25    Merge(MergeMergeRequest),
26    #[clap(about = "Git checkout a merge request branch for review")]
27    Checkout(CheckoutMergeRequest),
28    #[clap(
29        subcommand,
30        about = "Merge request comment operations",
31        visible_alias = "cm"
32    )]
33    Comment(CommentSubCommand),
34    #[clap(about = "Close a merge request")]
35    Close(CloseMergeRequest),
36    /// Get a merge request
37    Get(GetMergeRequest),
38    #[clap(about = "List merge requests", visible_alias = "ls")]
39    List(ListMergeRequest),
40}
41
42#[derive(Parser)]
43struct GetMergeRequest {
44    /// Id of the merge request
45    #[clap()]
46    id: i64,
47    #[clap(flatten)]
48    get_args: GetArgs,
49}
50
51#[derive(Parser)]
52enum CommentSubCommand {
53    /// Create a comment to a given merge request
54    Create(CreateCommentMergeRequest),
55    /// List comments of a given merge request
56    List(ListCommentMergeRequest),
57}
58
59#[derive(Parser)]
60struct CreateCommentMergeRequest {
61    /// Id of the merge request
62    #[clap(long)]
63    pub id: i64,
64    /// Comment to add to the merge request
65    #[clap(group = "comment_msg")]
66    pub comment: Option<String>,
67    /// Gather comment from the specified file. If "-" is provided, read from STDIN
68    #[clap(long, value_name = "FILE", group = "comment_msg")]
69    pub comment_from_file: Option<String>,
70}
71
72#[derive(Parser)]
73struct ListCommentMergeRequest {
74    /// Id of the merge request
75    #[clap()]
76    pub id: i64,
77    #[command(flatten)]
78    pub list_args: ListArgs,
79}
80
81#[derive(Clone, Debug, Parser, ValueEnum)]
82enum SummaryCliOptions {
83    Short,
84    Long,
85}
86
87impl From<Option<SummaryCliOptions>> for SummaryOptions {
88    fn from(options: Option<SummaryCliOptions>) -> Self {
89        match options {
90            Some(SummaryCliOptions::Short) => SummaryOptions::Short,
91            Some(SummaryCliOptions::Long) => SummaryOptions::Long,
92            None => SummaryOptions::None,
93        }
94    }
95}
96
97#[derive(Parser)]
98struct CreateMergeRequest {
99    /// Title of the merge request
100    #[clap(long, group = "title_input")]
101    pub title: Option<String>,
102    /// Gather title and description from the specified commit message
103    #[clap(
104        long,
105        group = "title_input",
106        group = "description_input",
107        value_name = "SHA"
108    )]
109    pub body_from_commit: Option<String>,
110    /// Gather merge request title and description from the specified file. If "-" is
111    /// provided, read from STDIN. Title and description are separated by a blank line.
112    #[clap(
113        long,
114        group = "title_input",
115        group = "description_input",
116        value_name = "FILE"
117    )]
118    pub body_from_file: Option<String>,
119    /// Description of the merge request
120    #[clap(long, group = "description_input")]
121    pub description: Option<String>,
122    /// Gather merge request description from the specified file. If "-" is
123    /// provided, read from STDIN
124    #[clap(long, group = "description_input", value_name = "FILE")]
125    pub description_from_file: Option<String>,
126    /// Assignee username
127    #[clap(long, short = 'A', value_name = "USERNAME")]
128    pub assignee: Option<String>,
129    /// Reviewer username
130    #[clap(long, short = 'R', value_name = "USERNAME", group = "reviewer_args")]
131    pub reviewer: Option<String>,
132    /// Randomly assigns a reviewer from the list of members defined in the merge requests
133    /// configuration section.
134    #[clap(long, group = "reviewer_args")]
135    pub rand_reviewer: bool,
136    /// Provides a list of outgoing commit SHAs and messages with subject
137    /// (short) and body (long) to STDOUT, then exits. No merge request is created.
138    #[clap(short, long, group = "summary_args", value_name = "OPTION")]
139    pub summary: Option<SummaryCliOptions>,
140    /// Provides a patch/diff of the outgoing changes to STDOUT, then exits. No merge
141    /// request is created.
142    #[clap(short, long, group = "summary_args")]
143    pub patch: bool,
144    /// Accept the default title, description, and target branch
145    #[clap(long, short)]
146    pub auto: bool,
147    /// Provide a GPT prompt with the summary of the outgoing changes. This can
148    /// be used to automatically create a title and a description for the
149    /// outgoing merge request. Requires `--summary short` or `--summary long`.
150    #[clap(long, short, requires = "summary")]
151    pub gpt_prompt: bool,
152    /// Automatically fetch the latest changes from the remote repository
153    #[clap(long, value_name = "REMOTE_ALIAS")]
154    pub fetch: Option<String>,
155    /// Automatically rebase the current branch on top of the target branch
156    #[clap(long, value_name = "REMOTE_ALIAS/BRANCH")]
157    pub rebase: Option<String>,
158    /// Open merge request in another `OWNER/PROJECT_NAME` instead of current
159    /// origin.
160    #[clap(long, value_name = "OWNER/PROJECT_NAME", value_parser=validate_project_repo_path, requires = "target_branch")]
161    pub target_repo: Option<String>,
162    /// Target branch of the merge request instead of default project's upstream
163    /// branch. If targeting another repository, target branch is required.
164    #[clap(long)]
165    pub target_branch: Option<String>,
166    /// Automatically open the browser after creating the merge request
167    #[clap(long, short)]
168    pub browse: bool,
169    /// Open the merge request automatically without prompting for confirmation
170    #[clap(long, short)]
171    pub yes: bool,
172    /// Adds and commits all changes before creating the merge request
173    #[clap(long, value_name = "COMMIT_MSG")]
174    pub commit: Option<String>,
175    /// Update the merge request title and description with latest summary
176    #[clap(long)]
177    pub amend: bool,
178    /// Force push the current branch to the remote repository
179    #[clap(long, short)]
180    pub force: bool,
181    /// Set up the merge request as draft
182    #[clap(long, visible_alias = "wip")]
183    pub draft: bool,
184    /// Dry run. Does not push the branch and does not create the merge request
185    #[clap(long)]
186    pub dry_run: bool,
187    #[clap(flatten)]
188    pub cache_args: CacheArgs,
189}
190
191#[derive(ValueEnum, Clone, PartialEq, Debug)]
192pub enum MergeRequestStateStateCli {
193    Opened,
194    Closed,
195    Merged,
196}
197
198impl From<MergeRequestStateStateCli> for MergeRequestState {
199    fn from(state: MergeRequestStateStateCli) -> Self {
200        match state {
201            MergeRequestStateStateCli::Opened => MergeRequestState::Opened,
202            MergeRequestStateStateCli::Closed => MergeRequestState::Closed,
203            MergeRequestStateStateCli::Merged => MergeRequestState::Merged,
204        }
205    }
206}
207
208#[derive(Parser)]
209pub struct ListMergeRequest {
210    #[clap()]
211    pub state: MergeRequestStateStateCli,
212    #[command(flatten)]
213    pub list_args: ListArgs,
214}
215
216#[derive(Parser)]
217struct MergeMergeRequest {
218    /// Id of the merge request
219    #[clap()]
220    pub id: i64,
221}
222
223#[derive(Parser)]
224struct CheckoutMergeRequest {
225    /// Id of the merge request
226    #[clap()]
227    pub id: i64,
228}
229
230#[derive(Parser)]
231struct CloseMergeRequest {
232    /// Id of the merge request
233    #[clap()]
234    pub id: i64,
235}
236
237#[derive(Parser)]
238struct ApproveMergeRequest {
239    /// Id of the merge request
240    #[clap()]
241    pub id: i64,
242}
243
244impl From<ListMergeRequest> for MergeRequestOptions {
245    fn from(options: ListMergeRequest) -> Self {
246        MergeRequestOptions::List(MergeRequestListCliArgs::new(
247            options.state.into(),
248            options.list_args.into(),
249        ))
250    }
251}
252
253impl From<MergeMergeRequest> for MergeRequestOptions {
254    fn from(options: MergeMergeRequest) -> Self {
255        MergeRequestOptions::Merge { id: options.id }
256    }
257}
258
259impl From<CheckoutMergeRequest> for MergeRequestOptions {
260    fn from(options: CheckoutMergeRequest) -> Self {
261        MergeRequestOptions::Checkout { id: options.id }
262    }
263}
264
265impl From<CloseMergeRequest> for MergeRequestOptions {
266    fn from(options: CloseMergeRequest) -> Self {
267        MergeRequestOptions::Close { id: options.id }
268    }
269}
270
271impl From<ApproveMergeRequest> for MergeRequestOptions {
272    fn from(options: ApproveMergeRequest) -> Self {
273        MergeRequestOptions::Approve { id: options.id }
274    }
275}
276
277impl From<MergeRequestCommand> for MergeRequestOptions {
278    fn from(options: MergeRequestCommand) -> Self {
279        match options.subcommand {
280            MergeRequestSubcommand::Create(options) => options.into(),
281            MergeRequestSubcommand::List(options) => options.into(),
282            MergeRequestSubcommand::Merge(options) => options.into(),
283            MergeRequestSubcommand::Checkout(options) => options.into(),
284            MergeRequestSubcommand::Close(options) => options.into(),
285            MergeRequestSubcommand::Comment(options) => options.into(),
286            MergeRequestSubcommand::Get(options) => options.into(),
287            MergeRequestSubcommand::Approve(options) => options.into(),
288        }
289    }
290}
291
292impl From<CommentSubCommand> for MergeRequestOptions {
293    fn from(options: CommentSubCommand) -> Self {
294        match options {
295            CommentSubCommand::Create(options) => options.into(),
296            CommentSubCommand::List(options) => options.into(),
297        }
298    }
299}
300
301impl From<CreateMergeRequest> for MergeRequestOptions {
302    fn from(options: CreateMergeRequest) -> Self {
303        MergeRequestOptions::Create(
304            MergeRequestCliArgs::builder()
305                .title(options.title)
306                .body_from_commit(options.body_from_commit)
307                .body_from_file(options.body_from_file)
308                .description(options.description)
309                .description_from_file(options.description_from_file)
310                .assignee(options.assignee)
311                .reviewer(options.reviewer)
312                .rand_reviewer(options.rand_reviewer)
313                .target_branch(options.target_branch)
314                .target_repo(options.target_repo)
315                .fetch(options.fetch)
316                .rebase(options.rebase)
317                .auto(options.auto)
318                .cache_args(options.cache_args.into())
319                .open_browser(options.browse)
320                .accept_summary(options.yes)
321                .commit(options.commit)
322                .draft(options.draft)
323                .amend(options.amend)
324                .force(options.force)
325                .dry_run(options.dry_run)
326                .summary(options.summary.into())
327                .patch(options.patch)
328                .gpt_prompt(options.gpt_prompt)
329                .build()
330                .unwrap(),
331        )
332    }
333}
334
335impl From<ListCommentMergeRequest> for MergeRequestOptions {
336    fn from(options: ListCommentMergeRequest) -> Self {
337        MergeRequestOptions::ListComment(
338            CommentMergeRequestListCliArgs::builder()
339                .id(options.id)
340                .list_args(options.list_args.into())
341                .build()
342                .unwrap(),
343        )
344    }
345}
346
347impl From<CreateCommentMergeRequest> for MergeRequestOptions {
348    fn from(options: CreateCommentMergeRequest) -> Self {
349        MergeRequestOptions::CreateComment(
350            CommentMergeRequestCliArgs::builder()
351                .id(options.id)
352                .comment(options.comment)
353                .comment_from_file(options.comment_from_file)
354                .build()
355                .unwrap(),
356        )
357    }
358}
359
360impl From<GetMergeRequest> for MergeRequestOptions {
361    fn from(options: GetMergeRequest) -> Self {
362        MergeRequestOptions::Get(
363            MergeRequestGetCliArgs::builder()
364                .id(options.id)
365                .get_args(options.get_args.into())
366                .build()
367                .unwrap(),
368        )
369    }
370}
371
372pub enum MergeRequestOptions {
373    Create(MergeRequestCliArgs),
374    Get(MergeRequestGetCliArgs),
375    List(MergeRequestListCliArgs),
376    CreateComment(CommentMergeRequestCliArgs),
377    ListComment(CommentMergeRequestListCliArgs),
378    Approve { id: i64 },
379    Merge { id: i64 },
380    // TODO: Checkout is a read operation, so we should propagate MergeRequestGetCliArgs
381    Checkout { id: i64 },
382    Close { id: i64 },
383}
384
385#[cfg(test)]
386mod test {
387    use crate::cli::{Args, Command};
388
389    use super::*;
390
391    #[test]
392    fn test_list_merge_requests_cli_args() {
393        let args = Args::parse_from(vec!["gr", "mr", "list", "opened"]);
394        let list_merge_request = match args.command {
395            Command::MergeRequest(MergeRequestCommand {
396                subcommand: MergeRequestSubcommand::List(options),
397            }) => {
398                assert_eq!(options.state, MergeRequestStateStateCli::Opened);
399                options
400            }
401            _ => panic!("Expected MergeRequestCommand::List"),
402        };
403
404        let options: MergeRequestOptions = list_merge_request.into();
405        match options {
406            MergeRequestOptions::List(args) => {
407                assert_eq!(args.state, MergeRequestState::Opened);
408            }
409            _ => panic!("Expected MergeRequestOptions::List"),
410        }
411    }
412
413    #[test]
414    fn test_merge_merge_request_cli_args() {
415        let args = Args::parse_from(vec!["gr", "mr", "merge", "123"]);
416        let merge_merge_request = match args.command {
417            Command::MergeRequest(MergeRequestCommand {
418                subcommand: MergeRequestSubcommand::Merge(options),
419            }) => {
420                assert_eq!(options.id, 123);
421                options
422            }
423            _ => panic!("Expected MergeRequestCommand::Merge"),
424        };
425
426        let options: MergeRequestOptions = merge_merge_request.into();
427        match options {
428            MergeRequestOptions::Merge { id } => {
429                assert_eq!(id, 123);
430            }
431            _ => panic!("Expected MergeRequestOptions::Merge"),
432        }
433    }
434
435    #[test]
436    fn test_checkout_merge_request_cli_args() {
437        let args = Args::parse_from(vec!["gr", "mr", "checkout", "123"]);
438        let checkout_merge_request = match args.command {
439            Command::MergeRequest(MergeRequestCommand {
440                subcommand: MergeRequestSubcommand::Checkout(options),
441            }) => {
442                assert_eq!(options.id, 123);
443                options
444            }
445            _ => panic!("Expected MergeRequestCommand::Checkout"),
446        };
447
448        let options: MergeRequestOptions = checkout_merge_request.into();
449        match options {
450            MergeRequestOptions::Checkout { id } => {
451                assert_eq!(id, 123);
452            }
453            _ => panic!("Expected MergeRequestOptions::Checkout"),
454        }
455    }
456
457    #[test]
458    fn test_close_merge_request_cli_args() {
459        let args = Args::parse_from(vec!["gr", "mr", "close", "123"]);
460        let close_merge_request = match args.command {
461            Command::MergeRequest(MergeRequestCommand {
462                subcommand: MergeRequestSubcommand::Close(options),
463            }) => {
464                assert_eq!(options.id, 123);
465                options
466            }
467            _ => panic!("Expected MergeRequestCommand::Close"),
468        };
469
470        let options: MergeRequestOptions = close_merge_request.into();
471        match options {
472            MergeRequestOptions::Close { id } => {
473                assert_eq!(id, 123);
474            }
475            _ => panic!("Expected MergeRequestOptions::Close"),
476        }
477    }
478
479    #[test]
480    fn test_comment_merge_request_cli_args() {
481        let args = Args::parse_from(vec!["gr", "mr", "comment", "create", "--id", "123", "LGTM"]);
482        let comment_merge_request = match args.command {
483            Command::MergeRequest(MergeRequestCommand {
484                subcommand: MergeRequestSubcommand::Comment(options),
485            }) => match options {
486                CommentSubCommand::Create(args) => {
487                    assert_eq!(args.id, 123);
488                    assert_eq!(args.comment, Some("LGTM".to_string()));
489                    args
490                }
491                _ => panic!("Expected CommentSubCommand::Create"),
492            },
493            _ => panic!("Expected MergeRequestCommand::Comment"),
494        };
495
496        let options: MergeRequestOptions = comment_merge_request.into();
497        match options {
498            MergeRequestOptions::CreateComment(args) => {
499                assert_eq!(args.id, 123);
500                assert_eq!(args.comment, Some("LGTM".to_string()));
501            }
502            _ => panic!("Expected MergeRequestOptions::Comment"),
503        }
504    }
505
506    #[test]
507    fn test_list_all_comments_in_merge_request_cli_args() {
508        let args = Args::parse_from(vec!["gr", "mr", "comment", "list", "123"]);
509        let list_comment_merge_request = match args.command {
510            Command::MergeRequest(MergeRequestCommand {
511                subcommand: MergeRequestSubcommand::Comment(options),
512            }) => match options {
513                CommentSubCommand::List(args) => {
514                    assert_eq!(args.id, 123);
515                    args
516                }
517                _ => panic!("Expected CommentSubCommand::List"),
518            },
519            _ => panic!("Expected MergeRequestCommand::Comment"),
520        };
521
522        let options: MergeRequestOptions = list_comment_merge_request.into();
523        match options {
524            MergeRequestOptions::ListComment(args) => {
525                assert_eq!(args.id, 123);
526            }
527            _ => panic!("Expected MergeRequestOptions::ListComment"),
528        }
529    }
530
531    #[test]
532    fn test_create_merge_request_cli_args() {
533        let args = Args::parse_from(vec!["gr", "mr", "create", "--auto", "-y", "--browse"]);
534        let create_merge_request = match args.command {
535            Command::MergeRequest(MergeRequestCommand {
536                subcommand: MergeRequestSubcommand::Create(options),
537            }) => {
538                assert!(options.auto);
539                assert!(options.yes);
540                assert!(options.browse);
541                options
542            }
543            _ => panic!("Expected MergeRequestCommand::Create"),
544        };
545
546        let options: MergeRequestOptions = create_merge_request.into();
547        match options {
548            MergeRequestOptions::Create(args) => {
549                assert!(args.auto);
550                assert!(args.accept_summary);
551                assert!(args.open_browser);
552            }
553            _ => panic!("Expected MergeRequestOptions::Create"),
554        }
555    }
556
557    #[test]
558    fn test_get_merge_request_details_cli_args() {
559        let args = Args::parse_from(vec!["gr", "mr", "get", "123"]);
560        let get_merge_request = match args.command {
561            Command::MergeRequest(MergeRequestCommand {
562                subcommand: MergeRequestSubcommand::Get(options),
563            }) => {
564                assert_eq!(options.id, 123);
565                options
566            }
567            _ => panic!("Expected MergeRequestCommand::Get"),
568        };
569
570        let options: MergeRequestOptions = get_merge_request.into();
571        match options {
572            MergeRequestOptions::Get(args) => {
573                assert_eq!(args.id, 123);
574            }
575            _ => panic!("Expected MergeRequestOptions::Get"),
576        }
577    }
578
579    #[test]
580    fn test_wip_alias_as_draft() {
581        let args = Args::parse_from(vec!["gr", "mr", "create", "--auto", "--wip"]);
582        let create_merge_request = match args.command {
583            Command::MergeRequest(MergeRequestCommand {
584                subcommand: MergeRequestSubcommand::Create(options),
585            }) => {
586                assert!(options.draft);
587                options
588            }
589            _ => panic!("Expected MergeRequestCommand::Create"),
590        };
591
592        let options: MergeRequestOptions = create_merge_request.into();
593        match options {
594            MergeRequestOptions::Create(args) => {
595                assert!(args.draft);
596            }
597            _ => panic!("Expected MergeRequestOptions::Create"),
598        }
599    }
600
601    #[test]
602    fn test_title_description_cli_combinations() {
603        // Valid combinations
604        assert!(Args::try_parse_from(["gr", "mr", "create", "--title", "test"]).is_ok());
605        assert!(Args::try_parse_from(["gr", "mr", "create", "--description", "test"]).is_ok());
606        assert!(Args::try_parse_from([
607            "gr",
608            "mr",
609            "create",
610            "--title",
611            "test",
612            "--description",
613            "test"
614        ])
615        .is_ok());
616        assert!(Args::try_parse_from([
617            "gr",
618            "mr",
619            "create",
620            "--title",
621            "test",
622            "--description-from-file",
623            "file.txt"
624        ])
625        .is_ok());
626        assert!(
627            Args::try_parse_from(["gr", "mr", "create", "--body-from-commit", "abc123"]).is_ok()
628        );
629        assert!(
630            Args::try_parse_from(["gr", "mr", "create", "--body-from-file", "file.txt"]).is_ok()
631        );
632
633        // Invalid combinations
634        assert!(Args::try_parse_from([
635            "gr",
636            "mr",
637            "create",
638            "--body-from-commit",
639            "abc123",
640            "--body-from-file",
641            "file.txt"
642        ])
643        .is_err());
644
645        assert!(Args::try_parse_from([
646            "gr",
647            "mr",
648            "create",
649            "--body-from-commit",
650            "abc123",
651            "--title",
652            "test"
653        ])
654        .is_err());
655
656        assert!(Args::try_parse_from([
657            "gr",
658            "mr",
659            "create",
660            "--body-from-file",
661            "/tmp/file.txt",
662            "--title",
663            "test"
664        ])
665        .is_err());
666
667        assert!(Args::try_parse_from([
668            "gr",
669            "mr",
670            "create",
671            "--body-from-file",
672            "file.txt",
673            "--description",
674            "test"
675        ])
676        .is_err());
677
678        assert!(Args::try_parse_from([
679            "gr",
680            "mr",
681            "create",
682            "--body-from-commit",
683            "file.txt",
684            "--description",
685            "test"
686        ])
687        .is_err());
688
689        assert!(Args::try_parse_from([
690            "gr",
691            "mr",
692            "create",
693            "--description",
694            "test",
695            "--description-from-file",
696            "file.txt"
697        ])
698        .is_err());
699
700        assert!(Args::try_parse_from([
701            "gr",
702            "mr",
703            "create",
704            "--body-from-file",
705            "file.txt",
706            "--description-from-file",
707            "file.txt"
708        ])
709        .is_err());
710    }
711
712    #[test]
713    fn test_reviewer_flag() {
714        let args = Args::parse_from(vec!["gr", "mr", "create", "--reviewer", "john_doe"]);
715
716        match args.command {
717            Command::MergeRequest(MergeRequestCommand {
718                subcommand: MergeRequestSubcommand::Create(options),
719            }) => {
720                assert_eq!(options.reviewer, Some("john_doe".to_string()));
721                assert!(!options.rand_reviewer);
722
723                let mr_options: MergeRequestOptions = options.into();
724                match mr_options {
725                    MergeRequestOptions::Create(args) => {
726                        assert_eq!(args.reviewer, Some("john_doe".to_string()));
727                        assert!(!args.rand_reviewer);
728                    }
729                    _ => panic!("Expected MergeRequestOptions::Create"),
730                }
731            }
732            _ => panic!("Expected MergeRequestCommand::Create"),
733        }
734    }
735
736    #[test]
737    fn test_random_reviewer_flag() {
738        let args = Args::parse_from(vec!["gr", "mr", "create", "--rand-reviewer"]);
739
740        match args.command {
741            Command::MergeRequest(MergeRequestCommand {
742                subcommand: MergeRequestSubcommand::Create(options),
743            }) => {
744                assert!(options.rand_reviewer);
745                assert_eq!(options.reviewer, None);
746
747                let mr_options: MergeRequestOptions = options.into();
748                match mr_options {
749                    MergeRequestOptions::Create(args) => {
750                        assert!(args.rand_reviewer);
751                        assert_eq!(args.reviewer, None);
752                    }
753                    _ => panic!("Expected MergeRequestOptions::Create"),
754                }
755            }
756            _ => panic!("Expected MergeRequestCommand::Create"),
757        }
758    }
759
760    #[test]
761    fn test_mutually_exclusive_reviewer_flags() {
762        let result = Args::try_parse_from(vec![
763            "gr",
764            "mr",
765            "create",
766            "--reviewer",
767            "john_doe",
768            "--rand-reviewer",
769        ]);
770
771        assert!(result.is_err());
772    }
773
774    #[test]
775    fn test_reviewer_short_flag() {
776        let args = Args::parse_from(vec!["gr", "mr", "create", "-R", "jane_doe"]);
777
778        match args.command {
779            Command::MergeRequest(MergeRequestCommand {
780                subcommand: MergeRequestSubcommand::Create(options),
781            }) => {
782                assert_eq!(options.reviewer, Some("jane_doe".to_string()));
783                assert!(!options.rand_reviewer);
784
785                let mr_options: MergeRequestOptions = options.into();
786                match mr_options {
787                    MergeRequestOptions::Create(args) => {
788                        assert_eq!(args.reviewer, Some("jane_doe".to_string()));
789                        assert!(!args.rand_reviewer);
790                    }
791                    _ => panic!("Expected MergeRequestOptions::Create"),
792                }
793            }
794            _ => panic!("Expected MergeRequestCommand::Create"),
795        }
796    }
797}