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(GetMergeRequest),
38 #[clap(about = "List merge requests", visible_alias = "ls")]
39 List(ListMergeRequest),
40}
41
42#[derive(Parser)]
43struct GetMergeRequest {
44 #[clap()]
46 id: i64,
47 #[clap(flatten)]
48 get_args: GetArgs,
49}
50
51#[derive(Parser)]
52enum CommentSubCommand {
53 Create(CreateCommentMergeRequest),
55 List(ListCommentMergeRequest),
57}
58
59#[derive(Parser)]
60struct CreateCommentMergeRequest {
61 #[clap(long)]
63 pub id: i64,
64 #[clap(group = "comment_msg")]
66 pub comment: Option<String>,
67 #[clap(long, value_name = "FILE", group = "comment_msg")]
69 pub comment_from_file: Option<String>,
70}
71
72#[derive(Parser)]
73struct ListCommentMergeRequest {
74 #[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 #[clap(long, group = "title_input")]
101 pub title: Option<String>,
102 #[clap(
104 long,
105 group = "title_input",
106 group = "description_input",
107 value_name = "SHA"
108 )]
109 pub body_from_commit: Option<String>,
110 #[clap(
113 long,
114 group = "title_input",
115 group = "description_input",
116 value_name = "FILE"
117 )]
118 pub body_from_file: Option<String>,
119 #[clap(long, group = "description_input")]
121 pub description: Option<String>,
122 #[clap(long, group = "description_input", value_name = "FILE")]
125 pub description_from_file: Option<String>,
126 #[clap(long, short = 'A', value_name = "USERNAME")]
128 pub assignee: Option<String>,
129 #[clap(long, short = 'R', value_name = "USERNAME", group = "reviewer_args")]
131 pub reviewer: Option<String>,
132 #[clap(long, group = "reviewer_args")]
135 pub rand_reviewer: bool,
136 #[clap(short, long, group = "summary_args", value_name = "OPTION")]
139 pub summary: Option<SummaryCliOptions>,
140 #[clap(short, long, group = "summary_args")]
143 pub patch: bool,
144 #[clap(long, short)]
146 pub auto: bool,
147 #[clap(long, short, requires = "summary")]
151 pub gpt_prompt: bool,
152 #[clap(long, value_name = "REMOTE_ALIAS")]
154 pub fetch: Option<String>,
155 #[clap(long, value_name = "REMOTE_ALIAS/BRANCH")]
157 pub rebase: Option<String>,
158 #[clap(long, value_name = "OWNER/PROJECT_NAME", value_parser=validate_project_repo_path, requires = "target_branch")]
161 pub target_repo: Option<String>,
162 #[clap(long)]
165 pub target_branch: Option<String>,
166 #[clap(long, short)]
168 pub browse: bool,
169 #[clap(long, short)]
171 pub yes: bool,
172 #[clap(long, value_name = "COMMIT_MSG")]
174 pub commit: Option<String>,
175 #[clap(long)]
177 pub amend: bool,
178 #[clap(long, short)]
180 pub force: bool,
181 #[clap(long, visible_alias = "wip")]
183 pub draft: bool,
184 #[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 #[clap()]
220 pub id: i64,
221}
222
223#[derive(Parser)]
224struct CheckoutMergeRequest {
225 #[clap()]
227 pub id: i64,
228}
229
230#[derive(Parser)]
231struct CloseMergeRequest {
232 #[clap()]
234 pub id: i64,
235}
236
237#[derive(Parser)]
238struct ApproveMergeRequest {
239 #[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 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 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 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}