1use clap::{Parser, Subcommand};
2use std::ffi::OsString;
3
4#[derive(Parser)]
5#[command(name = "gitee", version, about = "Gitee CLI (gh-like)")]
6pub struct Cli {
7 #[arg(long, global = true)]
9 pub repo: Option<String>,
10 #[arg(long, global = true)]
12 pub remote: Option<String>,
13 #[arg(long, global = true, default_value = "gitee.com")]
15 pub host: String,
16 #[arg(long, short = 'j', global = true, num_args = 0..=1, default_missing_value = "")]
19 pub json: Option<String>,
20 #[arg(long, global = true, requires = "json")]
24 pub jq: Option<String>,
25 #[arg(long, global = true)]
27 pub debug: bool,
28 #[arg(long, global = true)]
32 pub preview: bool,
33 #[command(subcommand)]
34 pub cmd: Command,
35}
36
37#[derive(Subcommand)]
38pub enum Command {
39 #[command(subcommand)]
40 Pr(PrCmd),
41 #[command(subcommand)]
42 Issue(IssueCmd),
43 #[command(subcommand)]
44 Release(ReleaseCmd),
45 #[command(subcommand)]
46 Milestone(MilestoneCmd),
47 #[command(subcommand)]
48 Repo(RepoCmd),
49 #[command(subcommand)]
50 Label(LabelCmd),
51 #[command(subcommand)]
52 Auth(AuthCmd),
53 #[command(subcommand)]
54 Search(SearchCmd),
55 Api(ApiArgs),
57 #[command(subcommand)]
58 Gist(GistCmd),
59 #[command(subcommand)]
60 Org(OrgCmd),
61 #[command(name = "ssh-key", subcommand)]
62 SshKey(SshKeyCmd),
63 #[command(subcommand)]
64 Collaborator(CollaboratorCmd),
65 #[command(subcommand)]
66 Webhook(WebhookCmd),
67 #[command(subcommand)]
68 Config(ConfigCmd),
69 #[command(subcommand)]
70 Alias(AliasCmd),
71 Browse,
73 Status {
75 #[command(flatten)]
76 limit: LimitArgs,
77 },
78 Completions { shell: Option<String> },
80 #[command(subcommand)]
81 Extension(ExtensionCmd),
82 #[command(external_subcommand)]
84 External(Vec<OsString>),
85}
86
87#[derive(clap::Args, Clone, Debug)]
88pub struct ApiArgs {
89 pub endpoint: String,
91 #[arg(short = 'X', long = "method")]
93 pub method: Option<String>,
94 #[arg(short = 'F', long = "field")]
98 pub fields: Vec<String>,
99 #[arg(short = 'f', long = "raw-field")]
101 pub raw_fields: Vec<String>,
102 #[arg(short = 'H', long = "header")]
104 pub headers: Vec<String>,
105 #[arg(long, conflicts_with_all = ["fields", "raw_fields"])]
107 pub input: Option<String>,
108 #[arg(long)]
110 pub paginate: bool,
111}
112
113#[derive(clap::Args, Clone, Debug)]
114pub struct ListArgs {
115 #[arg(long)]
116 pub state: Option<String>,
117 #[arg(long, default_value_t = 30)]
118 pub limit: usize,
119}
120
121#[derive(clap::Args, Clone, Debug)]
122pub struct LimitArgs {
123 #[arg(long, default_value_t = 30)]
124 pub limit: usize,
125}
126
127#[derive(clap::Args, Clone, Debug)]
128pub struct CommentArgs {
129 #[arg(long, short = 'm')]
130 pub body: String,
131}
132
133#[derive(Subcommand, Clone)]
134pub enum PrCmd {
135 List {
136 #[command(flatten)]
137 list: ListArgs,
138 #[arg(long)]
139 author: Option<String>,
140 },
141 Status {
143 #[command(flatten)]
144 limit: LimitArgs,
145 },
146 View {
148 number: i64,
149 #[arg(long)]
151 web: bool,
152 #[arg(long)]
154 merged: bool,
155 },
156 Diff {
158 number: i64,
159 },
160 Commits {
162 number: i64,
163 #[command(flatten)]
164 limit: LimitArgs,
165 },
166 Checkout {
168 number: i64,
169 },
170 Create {
173 #[arg(long)]
174 title: Option<String>,
175 #[arg(long)]
176 body: Option<String>,
177 #[arg(long)]
178 head: Option<String>,
179 #[arg(long)]
180 base: Option<String>,
181 #[arg(long)]
184 fill: bool,
185 #[arg(long)]
187 assignee: Vec<String>,
188 #[arg(long)]
190 tester: Vec<String>,
191 #[arg(long)]
193 label: Vec<String>,
194 #[arg(long)]
196 milestone: Option<String>,
197 #[arg(long = "close-issue")]
199 close_issue: Option<String>,
200 #[arg(long)]
202 draft: bool,
203 },
204 #[command(group = clap::ArgGroup::new("edit_flags").required(true).multiple(true).args(["title", "body", "assignee", "tester", "label", "milestone"]))]
206 Edit {
207 number: i64,
208 #[arg(long)]
209 title: Option<String>,
210 #[arg(long)]
211 body: Option<String>,
212 #[arg(long)]
215 assignee: Vec<String>,
216 #[arg(long)]
218 tester: Vec<String>,
219 #[arg(long)]
221 label: Vec<String>,
222 #[arg(long)]
224 milestone: Option<String>,
225 },
226 Merge {
227 number: i64,
228 #[arg(long)]
229 squash: bool,
230 #[arg(long)]
231 rebase: bool,
232 #[arg(long = "no-close-issue")]
233 no_close_issue: bool,
234 },
235 #[command(subcommand)]
237 Comment(PrCommentCmd),
238 #[command(subcommand)]
240 Label(PrLabelCmd),
241 #[command(subcommand)]
244 Assignee(PrAssigneeCmd),
245 #[command(subcommand)]
248 Tester(PrTesterCmd),
249 Approve {
250 number: i64,
251 #[arg(long)]
252 force: bool,
253 },
254 Test {
256 number: i64,
257 #[arg(long)]
258 force: bool,
259 },
260 Close {
261 number: i64,
262 },
263 Reopen {
265 number: i64,
266 },
267 Ready {
269 number: i64,
270 #[arg(long)]
272 undo: bool,
273 },
274 Link {
275 number: i64,
276 issue: String,
277 },
278 }
280
281#[derive(Subcommand, Clone)]
282pub enum PrCommentCmd {
283 Create {
285 number: i64,
286 #[command(flatten)]
287 body: CommentArgs,
288 #[arg(long)]
290 path: Option<String>,
291 #[arg(long)]
293 position: Option<i64>,
294 #[arg(long = "commit-id")]
296 commit_id: Option<String>,
297 },
298 List {
300 number: i64,
301 #[arg(long = "type", value_parser = ["diff", "general"])]
304 comment_type: Option<String>,
305 #[command(flatten)]
306 limit: LimitArgs,
307 },
308 Edit {
310 target: i64,
312 #[arg(long)]
315 last: bool,
316 #[arg(long, short = 'm')]
317 body: Option<String>,
318 },
319 Delete {
321 target: i64,
323 #[arg(long)]
326 last: bool,
327 #[arg(long, short = 'y')]
329 yes: bool,
330 },
331}
332
333
334#[derive(Subcommand, Clone)]
335pub enum SearchCmd {
336 Repos {
338 query: String,
339 #[arg(long)]
340 owner: Option<String>,
341 #[arg(long)]
342 language: Option<String>,
343 #[arg(long)]
345 fork: bool,
346 #[arg(long)]
347 sort: Option<String>,
348 #[arg(long)]
349 order: Option<String>,
350 #[command(flatten)]
351 limit: LimitArgs,
352 },
353 Issues {
355 query: String,
356 #[arg(long)]
357 state: Option<String>,
358 #[arg(long)]
359 author: Option<String>,
360 #[arg(long)]
361 assignee: Option<String>,
362 #[arg(long)]
363 label: Option<String>,
364 #[arg(long)]
365 language: Option<String>,
366 #[arg(long)]
367 sort: Option<String>,
368 #[arg(long)]
369 order: Option<String>,
370 #[command(flatten)]
371 limit: LimitArgs,
372 },
373 Users {
375 query: String,
376 #[arg(long)]
377 sort: Option<String>,
378 #[arg(long)]
379 order: Option<String>,
380 #[command(flatten)]
381 limit: LimitArgs,
382 },
383}
384
385#[derive(Subcommand, Clone)]
386pub enum IssueCmd {
387 List {
388 #[command(flatten)]
389 list: ListArgs,
390 #[arg(long)]
391 assignee: Option<String>,
392 },
393 Status {
395 #[command(flatten)]
396 limit: LimitArgs,
397 },
398 View {
400 number: String,
401 #[arg(long)]
403 web: bool,
404 },
405 Create {
406 #[arg(long)]
407 title: Option<String>,
408 #[arg(long)]
409 body: Option<String>,
410 #[arg(long)]
411 assignee: Option<String>,
412 #[arg(long)]
413 labels: Option<String>,
414 #[arg(long)]
416 milestone: Option<String>,
417 #[arg(long)]
419 security_hole: bool,
420 },
421 #[command(group = clap::ArgGroup::new("edit_flags").required(true).multiple(true).args(["title", "body", "assignee", "label", "milestone", "security_hole", "state"]))]
434 Edit {
435 number: String,
436 #[arg(long)]
437 title: Option<String>,
438 #[arg(long)]
439 body: Option<String>,
440 #[arg(long)]
441 assignee: Option<String>,
442 #[arg(long)]
444 label: Vec<String>,
445 #[arg(long)]
447 milestone: Option<String>,
448 #[arg(long)]
450 security_hole: bool,
451 #[arg(long, value_parser = ["open", "progressing", "closed"])]
456 state: Option<String>,
457 },
458 Close {
459 number: String,
460 },
461 Reopen {
463 number: String,
464 },
465 Link {
466 number: String,
467 pr: i64,
468 },
469 #[command(subcommand)]
471 Comment(IssueCommentCmd),
472 #[command(subcommand)]
474 Label(IssueLabelCmd),
475}
476
477#[derive(Subcommand, Clone)]
480pub enum IssueLabelCmd {
481 Add {
483 number: String,
485 #[arg(required = true, num_args = 1..)]
487 labels: Vec<String>,
488 },
489 Remove {
491 number: String,
493 #[arg(required = true, num_args = 1..)]
495 labels: Vec<String>,
496 },
497 List {
499 number: String,
501 },
502}
503
504#[derive(Subcommand, Clone)]
507pub enum PrLabelCmd {
508 Add {
510 number: i64,
511 #[arg(required = true, num_args = 1..)]
513 labels: Vec<String>,
514 },
515 Remove {
517 number: i64,
518 #[arg(required = true, num_args = 1..)]
520 labels: Vec<String>,
521 },
522 List {
524 number: i64,
525 },
526}
527
528#[derive(Subcommand, Clone)]
531pub enum PrAssigneeCmd {
532 Add {
534 number: i64,
535 #[arg(required = true, num_args = 1..)]
537 users: Vec<String>,
538 },
539 Remove {
541 number: i64,
542 #[arg(required = true, num_args = 1..)]
544 users: Vec<String>,
545 },
546 List {
548 number: i64,
549 },
550}
551
552#[derive(Subcommand, Clone)]
555pub enum PrTesterCmd {
556 Add {
558 number: i64,
559 #[arg(required = true, num_args = 1..)]
561 users: Vec<String>,
562 },
563 Remove {
565 number: i64,
566 #[arg(required = true, num_args = 1..)]
568 users: Vec<String>,
569 },
570 List {
572 number: i64,
573 },
574}
575
576#[derive(Subcommand, Clone)]
577pub enum IssueCommentCmd {
578 Create {
580 number: String,
581 #[command(flatten)]
582 body: CommentArgs,
583 },
584 List {
586 number: String,
587 #[command(flatten)]
588 limit: LimitArgs,
589 },
590 Edit {
592 target: String,
594 #[arg(long)]
597 last: bool,
598 #[arg(long, short = 'm')]
599 body: Option<String>,
600 },
601 Delete {
603 target: String,
605 #[arg(long)]
608 last: bool,
609 #[arg(long, short = 'y')]
611 yes: bool,
612 },
613}
614
615
616#[derive(Subcommand, Clone)]
617pub enum GistCmd {
618 List {
619 #[command(flatten)]
620 limit: LimitArgs,
621 },
622 View {
623 id: String,
624 #[arg(long)]
625 raw: bool,
626 },
627 Create {
631 #[arg(long)]
632 desc: Option<String>,
633 #[arg(long)]
634 public: bool,
635 #[arg(long)]
636 filename: Option<String>,
637 #[arg(required = true, num_args = 1..)]
638 files: Vec<String>,
639 },
640 Edit {
641 id: String,
642 file: String,
643 },
644 Delete {
645 id: String,
646 #[arg(long, short = 'y')]
647 yes: bool,
648 },
649}
650
651#[derive(Subcommand, Clone)]
652pub enum ReleaseCmd {
653 List {
654 #[command(flatten)]
655 limit: LimitArgs,
656 },
657 View {
658 tag: String,
659 #[arg(long)]
661 web: bool,
662 },
663 Create {
664 #[arg(long)]
665 tag: String,
666 #[arg(long)]
667 name: Option<String>,
668 #[arg(long)]
669 notes: Option<String>,
670 #[arg(long)]
671 target: Option<String>,
672 #[arg(long)]
673 prerelease: bool,
674 },
675 Upload {
676 tag: String,
677 files: Vec<String>,
678 },
679 Download {
680 tag: String,
681 #[arg(long, default_value = ".")]
682 dir: String,
683 #[arg(long)]
686 pattern: Option<String>,
687 },
688 #[command(group = clap::ArgGroup::new("edit_flags").required(true).multiple(true).args(["name", "notes", "prerelease"]))]
690 Edit {
691 tag: String,
692 #[arg(long)]
693 name: Option<String>,
694 #[arg(long)]
695 notes: Option<String>,
696 #[arg(long)]
697 prerelease: bool,
698 },
699 Delete {
700 tag: String,
701 #[arg(long)]
702 yes: bool,
703 },
704}
705
706#[derive(Subcommand, Clone)]
708pub enum LabelCmd {
709 List {
710 #[command(flatten)]
711 limit: LimitArgs,
712 },
713 Create {
714 name: String,
715 #[arg(long)]
716 color: String,
717 },
720 #[command(group = clap::ArgGroup::new("edit_flags").required(true).multiple(true).args(["new_name", "color"]))]
722 Edit {
723 name: String,
724 #[arg(long = "name")]
725 new_name: Option<String>,
726 #[arg(long)]
727 color: Option<String>,
728 },
729 Delete {
730 name: String,
731 #[arg(long, short = 'y')]
732 yes: bool,
733 },
734}
735
736#[derive(Subcommand, Clone)]
737pub enum RepoCmd {
738 View {
740 #[arg(value_name = "REPO")]
741 target: Option<String>,
742 #[arg(long)]
744 web: bool,
745 },
746 List {
749 owner: Option<String>,
750 #[command(flatten)]
751 limit: LimitArgs,
752 },
753 Clone {
755 spec: String,
757 dir: Option<String>,
759 #[arg(long)]
761 ssh: bool,
762 },
763 Fork {
765 #[arg(long = "add-remote")]
767 add_remote: Option<String>,
768 },
769 Create {
771 name: String,
772 #[arg(long)]
774 org: Option<String>,
775 #[arg(long)]
776 private: bool,
777 #[arg(long)]
778 description: Option<String>,
779 #[arg(long)]
780 homepage: Option<String>,
781 #[arg(long = "gitignore")]
782 gitignore: Option<String>,
783 #[arg(long)]
784 license: Option<String>,
785 },
786 #[command(group = clap::ArgGroup::new("edit_flags").required(true).multiple(true).args(["description", "homepage", "private", "public", "default_branch"]))]
788 Edit {
789 #[arg(long)]
790 description: Option<String>,
791 #[arg(long)]
792 homepage: Option<String>,
793 #[arg(long, conflicts_with = "public")]
794 private: bool,
795 #[arg(long, conflicts_with = "private")]
796 public: bool,
797 #[arg(long = "default-branch")]
798 default_branch: Option<String>,
799 },
800 Rename {
802 new_path: String,
803 },
804 Star,
806 Unstar,
808 Watch,
810 Unwatch,
812 Delete {
814 #[arg(long)]
815 yes: bool,
816 },
817}
818
819
820#[derive(Subcommand, Clone)]
821pub enum MilestoneCmd {
822 List {
823 #[command(flatten)]
824 list: ListArgs,
825 },
826 View {
828 number: i64,
829 },
830 Create {
831 #[arg(long)]
832 title: String,
833 #[arg(long = "due-on")]
836 due_on: String,
837 #[arg(long)]
838 description: Option<String>,
839 #[arg(long, value_parser = ["open", "closed"])]
840 state: Option<String>,
841 },
842 #[command(group = clap::ArgGroup::new("milestone_edit_flags").required(true).multiple(true).args(["title", "due_on", "description", "state"]))]
844 Edit {
845 number: i64,
846 #[arg(long)]
847 title: Option<String>,
848 #[arg(long = "due-on")]
849 due_on: Option<String>,
850 #[arg(long)]
851 description: Option<String>,
852 #[arg(long, value_parser = ["open", "closed"])]
853 state: Option<String>,
854 },
855}
856
857
858
859#[derive(Subcommand, Clone)]
860pub enum ConfigCmd {
861 List,
862 Get {
863 key: String,
864 },
865 Set {
866 key: String,
867 value: String,
868 },
869}
870
871#[derive(Clone, Debug, PartialEq, Eq)]
872pub enum BuildKind {
873 Cargo,
874 Npm,
875}
876
877#[derive(Subcommand, Clone)]
878pub enum ExtensionCmd {
879 List,
881 Install {
883 repo: String,
885 #[arg(long, value_parser = ["cargo", "npm"])]
887 build: Option<String>,
888 #[arg(long, short = 'y')]
890 yes: bool,
891 },
892 Create {
894 name: String,
896 #[arg(long)]
898 cargo: bool,
899 },
900 Remove {
902 name: String,
903 #[arg(long, short = 'y')]
904 yes: bool,
905 },
906 Upgrade {
908 name: Option<String>,
909 },
910}
911
912#[derive(Subcommand, Clone)]
913pub enum AliasCmd {
914 List,
915 Set {
916 name: String,
917 #[arg(trailing_var_arg = true, allow_hyphen_values = true, required = true)]
919 expansion: Vec<String>,
920 },
921 Delete {
922 name: String,
923 },
924}
925
926#[derive(Subcommand, Clone)]
927pub enum OrgCmd {
928 List {
929 #[command(flatten)]
930 limit: LimitArgs,
931 },
932}
933
934#[derive(Subcommand, Clone)]
935pub enum SshKeyCmd {
936 List {
937 #[command(flatten)]
938 limit: LimitArgs,
939 },
940 Add {
941 pubkey_file: String,
943 #[arg(long)]
944 title: Option<String>,
945 },
946 Delete {
947 id: i64,
948 #[arg(long, short = 'y')]
949 yes: bool,
950 },
951}
952
953#[derive(Subcommand, Clone)]
954pub enum CollaboratorCmd {
955 List {
956 #[command(flatten)]
957 limit: LimitArgs,
958 },
959 Add {
960 username: String,
961 #[arg(long, default_value = "push")]
963 permission: String,
964 },
965 Remove {
966 username: String,
967 #[arg(long, short = 'y')]
968 yes: bool,
969 },
970}
971
972#[derive(Subcommand, Clone)]
973pub enum WebhookCmd {
974 List {
975 #[command(flatten)]
976 limit: LimitArgs,
977 },
978 Create {
979 #[arg(long)]
980 url: String,
981 #[arg(long)]
984 events: Vec<String>,
985 #[arg(long)]
986 password: Option<String>,
987 },
988 Delete {
989 id: i64,
990 #[arg(long, short = 'y')]
991 yes: bool,
992 },
993}
994
995#[derive(Subcommand, Clone)]
996pub enum AuthCmd {
997 Login {
999 #[arg(long)]
1000 token: Option<String>,
1001 #[arg(long)]
1003 force: bool,
1004 },
1005 Status,
1007 Token,
1009 Logout,
1011 SetupGit,
1013 Switch {
1015 #[arg(long)]
1016 user: String,
1017 },
1018 #[command(subcommand)]
1020 GitCredential(GitCredentialCmd),
1021}
1022
1023#[derive(Subcommand, Clone)]
1024pub enum GitCredentialCmd {
1025 Get,
1026 Store,
1027 Erase,
1028}
1029
1030#[cfg(test)]
1031mod parse_tests {
1032 use super::{AliasCmd, AuthCmd, Cli, CollaboratorCmd, Command, ConfigCmd, ExtensionCmd, GistCmd, GitCredentialCmd, IssueCmd, IssueCommentCmd, IssueLabelCmd, MilestoneCmd, OrgCmd, PrAssigneeCmd, PrCmd, PrCommentCmd, PrLabelCmd, PrTesterCmd, ReleaseCmd, RepoCmd, SshKeyCmd, WebhookCmd};
1033 use clap::Parser;
1034
1035 #[test]
1036 fn pr_create_parses_without_title_for_interactive() {
1037 let cli = Cli::try_parse_from(["gitee", "pr", "create"])
1038 .expect("pr create without flags should parse (interactive or usage at runtime)");
1039 let Command::Pr(PrCmd::Create { title, fill, .. }) = cli.cmd else {
1040 panic!("expected pr create");
1041 };
1042 assert!(title.is_none());
1043 assert!(!fill);
1044
1045 let cli = Cli::try_parse_from(["gitee", "pr", "create", "--fill"])
1046 .expect("--fill alone should parse");
1047 let Command::Pr(PrCmd::Create { title, fill, .. }) = cli.cmd else {
1048 panic!("expected pr create");
1049 };
1050 assert!(title.is_none());
1051 assert!(fill);
1052 }
1053
1054 #[test]
1055 fn issue_create_parses_without_title_for_interactive() {
1056 let cli = Cli::try_parse_from(["gitee", "issue", "create"])
1057 .expect("issue create without flags should parse (interactive or usage at runtime)");
1058 let Command::Issue(IssueCmd::Create { title, .. }) = cli.cmd else {
1059 panic!("expected issue create");
1060 };
1061 assert!(title.is_none());
1062 }
1063
1064 #[test]
1065 fn search_requires_query_positional() {
1066 assert!(Cli::try_parse_from(["gitee", "search", "repos"]).is_err());
1067 assert!(Cli::try_parse_from(["gitee", "search", "issues"]).is_err());
1068 assert!(Cli::try_parse_from(["gitee", "search", "users"]).is_err());
1069 }
1070
1071 #[test]
1072 fn search_parses_flags() {
1073 let cli = Cli::try_parse_from([
1074 "gitee", "search", "repos", "gitee",
1075 "--owner", "oschina",
1076 "--language", "Rust",
1077 "--fork",
1078 "--sort", "stars_count",
1079 "--order", "desc",
1080 "--limit", "5",
1081 ])
1082 .expect("search repos should parse");
1083 let Command::Search(super::SearchCmd::Repos {
1084 query,
1085 owner,
1086 language,
1087 fork,
1088 sort,
1089 order,
1090 limit,
1091 }) = cli.cmd
1092 else {
1093 panic!("expected search repos");
1094 };
1095 assert_eq!(query, "gitee");
1096 assert_eq!(owner.as_deref(), Some("oschina"));
1097 assert_eq!(language.as_deref(), Some("Rust"));
1098 assert!(fork);
1099 assert_eq!(sort.as_deref(), Some("stars_count"));
1100 assert_eq!(order.as_deref(), Some("desc"));
1101 assert_eq!(limit.limit, 5);
1102
1103 let cli = Cli::try_parse_from([
1104 "gitee", "search", "issues", "login",
1105 "--state", "open",
1106 "--author", "alice",
1107 "--assignee", "bob",
1108 "--label", "bug",
1109 "--language", "Go",
1110 "--sort", "updated_at",
1111 "--order", "asc",
1112 "--limit", "3",
1113 ])
1114 .expect("search issues should parse");
1115 let Command::Search(super::SearchCmd::Issues {
1116 query,
1117 state,
1118 author,
1119 assignee,
1120 label,
1121 language,
1122 sort,
1123 order,
1124 limit,
1125 }) = cli.cmd
1126 else {
1127 panic!("expected search issues");
1128 };
1129 assert_eq!(query, "login");
1130 assert_eq!(state.as_deref(), Some("open"));
1131 assert_eq!(author.as_deref(), Some("alice"));
1132 assert_eq!(assignee.as_deref(), Some("bob"));
1133 assert_eq!(label.as_deref(), Some("bug"));
1134 assert_eq!(language.as_deref(), Some("Go"));
1135 assert_eq!(sort.as_deref(), Some("updated_at"));
1136 assert_eq!(order.as_deref(), Some("asc"));
1137 assert_eq!(limit.limit, 3);
1138
1139 let cli = Cli::try_parse_from([
1140 "gitee", "search", "users", "kip",
1141 "--sort", "followers_count",
1142 "--order", "desc",
1143 "--limit", "3",
1144 ])
1145 .expect("search users should parse");
1146 let Command::Search(super::SearchCmd::Users {
1147 query,
1148 sort,
1149 order,
1150 limit,
1151 }) = cli.cmd
1152 else {
1153 panic!("expected search users");
1154 };
1155 assert_eq!(query, "kip");
1156 assert_eq!(sort.as_deref(), Some("followers_count"));
1157 assert_eq!(order.as_deref(), Some("desc"));
1158 assert_eq!(limit.limit, 3);
1159 }
1160
1161 #[test]
1162 fn search_issues_accepts_global_repo_flag() {
1163 let cli = Cli::try_parse_from([
1164 "gitee",
1165 "--repo",
1166 "oschina/gitee-cli",
1167 "search",
1168 "issues",
1169 "bug",
1170 "--limit",
1171 "3",
1172 ])
1173 .expect("search issues with global --repo should parse");
1174 assert_eq!(cli.repo.as_deref(), Some("oschina/gitee-cli"));
1175 let Command::Search(super::SearchCmd::Issues { query, limit, .. }) = cli.cmd else {
1176 panic!("expected search issues");
1177 };
1178 assert_eq!(query, "bug");
1179 assert_eq!(limit.limit, 3);
1180 }
1181
1182
1183 #[test]
1184 fn pr_create_parses_full_flag_surface() {
1185 let cli = Cli::try_parse_from([
1186 "gitee", "pr", "create",
1187 "--title", "T",
1188 "--assignee", "me",
1189 "--tester", "qa1",
1190 "--label", "bug,ui",
1191 "--milestone", "v1.0",
1192 "--close-issue", "I1AB2C",
1193 ])
1194 .expect("pr create should parse");
1195 let Command::Pr(PrCmd::Create {
1196 assignee,
1197 tester,
1198 label,
1199 milestone,
1200 close_issue,
1201 ..
1202 }) = cli.cmd
1203 else {
1204 panic!("expected pr create");
1205 };
1206 assert_eq!(assignee, vec!["me".to_string()]);
1207 assert_eq!(tester, vec!["qa1".to_string()]);
1208 assert_eq!(label, vec!["bug,ui".to_string()]);
1209 assert_eq!(milestone.as_deref(), Some("v1.0"));
1210 assert_eq!(close_issue.as_deref(), Some("I1AB2C"));
1211 }
1212
1213 #[test]
1214 fn pr_create_parses_draft_flag() {
1215 let cli = Cli::try_parse_from(["gitee", "pr", "create", "--title", "T", "--draft"])
1216 .expect("pr create --draft should parse");
1217 let Command::Pr(PrCmd::Create { draft, .. }) = cli.cmd else {
1218 panic!("expected pr create");
1219 };
1220 assert!(draft);
1221 }
1222
1223 #[test]
1224 fn pr_ready_parses_number_and_undo() {
1225 let cli = Cli::try_parse_from(["gitee", "pr", "ready", "12"])
1226 .expect("pr ready should parse");
1227 let Command::Pr(PrCmd::Ready { number, undo }) = cli.cmd else {
1228 panic!("expected pr ready");
1229 };
1230 assert_eq!(number, 12);
1231 assert!(!undo);
1232
1233 let cli = Cli::try_parse_from(["gitee", "pr", "ready", "12", "--undo"])
1234 .expect("pr ready --undo should parse");
1235 let Command::Pr(PrCmd::Ready { number, undo }) = cli.cmd else {
1236 panic!("expected pr ready --undo");
1237 };
1238 assert_eq!(number, 12);
1239 assert!(undo);
1240 }
1241
1242 #[test]
1243 fn release_edit_requires_at_least_one_flag() {
1244 let r = Cli::try_parse_from(["gitee", "release", "edit", "v1.0"]);
1245 assert!(r.is_err(), "release edit with no flags must fail");
1246 }
1247
1248 #[test]
1249 fn release_download_parses_flags() {
1250 let cli = Cli::try_parse_from([
1251 "gitee",
1252 "release",
1253 "download",
1254 "v1.0",
1255 "--dir",
1256 "/tmp/out",
1257 "--pattern",
1258 "*.tar.xz",
1259 ])
1260 .expect("release download should parse");
1261 let Command::Release(ReleaseCmd::Download { tag, dir, pattern }) = cli.cmd else {
1262 panic!("expected release download");
1263 };
1264 assert_eq!(tag, "v1.0");
1265 assert_eq!(dir, "/tmp/out");
1266 assert_eq!(pattern.as_deref(), Some("*.tar.xz"));
1267 }
1268
1269 #[test]
1270 fn issue_edit_requires_at_least_one_flag() {
1271 let r = Cli::try_parse_from(["gitee", "issue", "edit", "I1AB"]);
1272 assert!(r.is_err(), "issue edit with no flags must fail");
1273 }
1274
1275 #[test]
1276 fn issue_edit_state_alone_parses() {
1277 let cli = Cli::try_parse_from(["gitee", "issue", "edit", "I1AB", "--state", "progressing"])
1278 .expect("issue edit --state alone should parse");
1279 let Command::Issue(IssueCmd::Edit { number, state, .. }) = cli.cmd else {
1280 panic!("expected issue edit");
1281 };
1282 assert_eq!(number, "I1AB");
1283 assert_eq!(state.as_deref(), Some("progressing"));
1284 }
1285
1286 #[test]
1287 fn gist_create_requires_at_least_one_file() {
1288 let r = Cli::try_parse_from(["gitee", "gist", "create"]);
1289 assert!(r.is_err(), "gist create with no files must fail");
1290 }
1291
1292 #[test]
1293 fn gist_flags_parse() {
1294 let cli = Cli::try_parse_from([
1295 "gitee", "gist", "create", "a.txt",
1296 "--desc", "my snippet",
1297 "--public",
1298 "--filename", "b.txt",
1299 ])
1300 .expect("gist create should parse");
1301 let Command::Gist(GistCmd::Create {
1302 files,
1303 desc,
1304 public,
1305 filename,
1306 ..
1307 }) = cli.cmd
1308 else {
1309 panic!("expected gist create");
1310 };
1311 assert_eq!(files, vec!["a.txt".to_string()]);
1312 assert_eq!(desc.as_deref(), Some("my snippet"));
1313 assert!(public);
1314 assert_eq!(filename.as_deref(), Some("b.txt"));
1315
1316 let cli = Cli::try_parse_from(["gitee", "gist", "delete", "abc123", "--yes"])
1317 .expect("gist delete should parse");
1318 let Command::Gist(GistCmd::Delete { id, yes }) = cli.cmd else {
1319 panic!("expected gist delete");
1320 };
1321 assert_eq!(id, "abc123");
1322 assert!(yes);
1323 }
1324
1325
1326 #[test]
1327 fn issue_edit_and_create_parse_new_flags() {
1328 let cli = Cli::try_parse_from([
1329 "gitee", "issue", "edit", "I1AB",
1330 "--title", "Retitle",
1331 "--label", "bug",
1332 "--assignee", "dev1",
1333 "--milestone", "v1.0",
1334 "--security-hole",
1335 ])
1336 .expect("issue edit should parse");
1337 let Command::Issue(IssueCmd::Edit {
1338 number,
1339 title,
1340 security_hole,
1341 ..
1342 }) = cli.cmd
1343 else {
1344 panic!("expected issue edit");
1345 };
1346 assert_eq!(number, "I1AB");
1347 assert_eq!(title.as_deref(), Some("Retitle"));
1348 assert!(security_hole);
1349
1350 let cli = Cli::try_parse_from([
1351 "gitee", "issue", "create",
1352 "--title", "T",
1353 "--milestone", "3",
1354 "--security-hole",
1355 ])
1356 .expect("issue create should parse");
1357 let Command::Issue(IssueCmd::Create {
1358 milestone,
1359 security_hole,
1360 ..
1361 }) = cli.cmd
1362 else {
1363 panic!("expected issue create");
1364 };
1365 assert_eq!(milestone.as_deref(), Some("3"));
1366 assert!(security_hole);
1367 }
1368
1369 #[test]
1370 fn repo_edit_requires_at_least_one_flag() {
1371 let r = Cli::try_parse_from(["gitee", "repo", "edit"]);
1372 assert!(r.is_err(), "repo edit with no flags must fail");
1373 }
1374
1375 #[test]
1376 fn repo_edit_private_public_conflict() {
1377 let r = Cli::try_parse_from(["gitee", "repo", "edit", "--private", "--public"]);
1378 assert!(r.is_err(), "--private and --public must conflict");
1379 }
1380
1381 #[test]
1382 fn repo_create_flags_parse() {
1383 let cli = Cli::try_parse_from([
1384 "gitee",
1385 "repo",
1386 "create",
1387 "my-repo",
1388 "--org",
1389 "acme",
1390 "--private",
1391 "--description",
1392 "desc",
1393 "--homepage",
1394 "https://example.com",
1395 "--gitignore",
1396 "Rust",
1397 "--license",
1398 "MIT",
1399 ])
1400 .expect("repo create should parse");
1401 let Command::Repo(RepoCmd::Create {
1402 name,
1403 org,
1404 private,
1405 description,
1406 homepage,
1407 gitignore,
1408 license,
1409 }) = cli.cmd
1410 else {
1411 panic!("expected repo create");
1412 };
1413 assert_eq!(name, "my-repo");
1414 assert_eq!(org.as_deref(), Some("acme"));
1415 assert!(private);
1416 assert_eq!(description.as_deref(), Some("desc"));
1417 assert_eq!(homepage.as_deref(), Some("https://example.com"));
1418 assert_eq!(gitignore.as_deref(), Some("Rust"));
1419 assert_eq!(license.as_deref(), Some("MIT"));
1420 }
1421
1422 #[test]
1423 fn pr_edit_requires_at_least_one_flag() {
1424 let r = Cli::try_parse_from(["gitee", "pr", "edit", "5"]);
1425 assert!(r.is_err(), "pr edit with no flags must fail");
1426 }
1427
1428 #[test]
1429 fn pr_test_parses() {
1430 let cli = Cli::try_parse_from(["gitee", "pr", "test", "12"])
1431 .expect("pr test should parse");
1432 let Command::Pr(PrCmd::Test { number, force }) = cli.cmd else {
1433 panic!("expected pr test");
1434 };
1435 assert_eq!(number, 12);
1436 assert!(!force);
1437
1438 let cli = Cli::try_parse_from(["gitee", "pr", "test", "12", "--force"])
1439 .expect("pr test --force should parse");
1440 let Command::Pr(PrCmd::Test { number, force }) = cli.cmd else {
1441 panic!("expected pr test");
1442 };
1443 assert_eq!(number, 12);
1444 assert!(force);
1445 }
1446
1447 #[test]
1448 fn label_edit_requires_at_least_one_flag() {
1449 let r = Cli::try_parse_from(["gitee", "label", "edit", "bug"]);
1450 assert!(r.is_err(), "label edit with no flags must fail");
1451 }
1452
1453 #[test]
1454 fn label_create_requires_color() {
1455 let r = Cli::try_parse_from(["gitee", "label", "create", "bug"]);
1456 assert!(r.is_err(), "label create without --color must fail");
1457 }
1458
1459 #[test]
1460 fn pr_edit_parses_accumulating_flags() {
1461 let cli = Cli::try_parse_from([
1462 "gitee", "pr", "edit", "5",
1463 "--title", "New title",
1464 "--label", "a,b",
1465 "--label", "c",
1466 "--assignee", "dev1",
1467 "--tester", "qa1",
1468 "--milestone", "v1.0",
1469 ])
1470 .expect("pr edit should parse");
1471 let Command::Pr(PrCmd::Edit {
1472 number,
1473 title,
1474 label,
1475 assignee,
1476 tester,
1477 milestone,
1478 ..
1479 }) = cli.cmd
1480 else {
1481 panic!("expected pr edit, got {:?}", std::mem::discriminant(&cli.cmd));
1482 };
1483 assert_eq!(number, 5);
1484 assert_eq!(title.as_deref(), Some("New title"));
1485 assert_eq!(label, vec!["a,b".to_string(), "c".to_string()]);
1486 assert_eq!(assignee, vec!["dev1".to_string()]);
1487 assert_eq!(tester, vec!["qa1".to_string()]);
1488 assert_eq!(milestone.as_deref(), Some("v1.0"));
1489 }
1490
1491 #[test]
1492 fn jq_without_json_is_a_usage_error() {
1493 let r = Cli::try_parse_from(["gitee", "pr", "list", "--jq", ".[0]"]);
1494 assert!(r.is_err(), "--jq without --json must fail");
1495 }
1496
1497 #[test]
1498 fn milestone_create_requires_title_and_due_on() {
1499 assert!(Cli::try_parse_from(["gitee", "milestone", "create"]).is_err());
1500 assert!(Cli::try_parse_from(["gitee", "milestone", "create", "--title", "T"]).is_err());
1501 let cli = Cli::try_parse_from([
1502 "gitee", "milestone", "create", "--title", "T", "--due-on", "2026-12-31",
1503 ])
1504 .expect("milestone create should parse");
1505 let Command::Milestone(MilestoneCmd::Create { title, due_on, .. }) = cli.cmd else {
1506 panic!("expected milestone create");
1507 };
1508 assert_eq!(title, "T");
1509 assert_eq!(due_on, "2026-12-31");
1510 }
1511
1512 #[test]
1513 fn milestone_edit_requires_at_least_one_flag() {
1514 let r = Cli::try_parse_from(["gitee", "milestone", "edit", "1"]);
1515 assert!(r.is_err(), "milestone edit with no flags must fail");
1516 }
1517
1518
1519 #[test]
1520 fn jq_parses_after_bare_json_flag() {
1521 let cli = Cli::try_parse_from(["gitee", "pr", "list", "--json", "--jq", ".[0].title"])
1522 .expect("--json --jq should parse");
1523 assert_eq!(cli.json.as_deref(), Some(""));
1524 assert_eq!(cli.jq.as_deref(), Some(".[0].title"));
1525 }
1526
1527 #[test]
1528 fn jq_parses_after_json_field_projection() {
1529 let cli = Cli::try_parse_from([
1530 "gitee",
1531 "issue",
1532 "list",
1533 "--json",
1534 "number,title",
1535 "--jq",
1536 "map(.number)",
1537 ])
1538 .expect("--json fields --jq should parse");
1539 assert_eq!(cli.json.as_deref(), Some("number,title"));
1540 assert_eq!(cli.jq.as_deref(), Some("map(.number)"));
1541 }
1542
1543 #[test]
1544 fn status_parses() {
1545 let cli = Cli::try_parse_from(["gitee", "status"]).expect("status should parse");
1546 let Command::Status { limit } = cli.cmd else {
1547 panic!("expected status");
1548 };
1549 assert_eq!(limit.limit, 30);
1550
1551 let cli = Cli::try_parse_from(["gitee", "status", "--limit", "5"])
1552 .expect("status with limit should parse");
1553 let Command::Status { limit } = cli.cmd else {
1554 panic!("expected status with limit");
1555 };
1556 assert_eq!(limit.limit, 5);
1557 }
1558
1559 #[test]
1560 fn pr_status_parses() {
1561 let cli = Cli::try_parse_from(["gitee", "pr", "status"]).expect("pr status should parse");
1562 let Command::Pr(PrCmd::Status { limit }) = cli.cmd else {
1563 panic!("expected pr status");
1564 };
1565 assert_eq!(limit.limit, 30);
1566
1567 let cli = Cli::try_parse_from(["gitee", "pr", "status", "--limit", "5"])
1568 .expect("pr status --limit should parse");
1569 let Command::Pr(PrCmd::Status { limit }) = cli.cmd else {
1570 panic!("expected pr status");
1571 };
1572 assert_eq!(limit.limit, 5);
1573 }
1574
1575 #[test]
1576 fn issue_status_parses() {
1577 let cli = Cli::try_parse_from(["gitee", "issue", "status"])
1578 .expect("issue status should parse");
1579 let Command::Issue(IssueCmd::Status { limit }) = cli.cmd else {
1580 panic!("expected issue status");
1581 };
1582 assert_eq!(limit.limit, 30);
1583
1584 let cli = Cli::try_parse_from(["gitee", "issue", "status", "--limit", "5"])
1585 .expect("issue status --limit should parse");
1586 let Command::Issue(IssueCmd::Status { limit }) = cli.cmd else {
1587 panic!("expected issue status");
1588 };
1589 assert_eq!(limit.limit, 5);
1590 }
1591
1592 #[test]
1593 fn org_list_parses_limit() {
1594 let cli = Cli::try_parse_from(["gitee", "org", "list", "--limit", "5"]).expect("org list");
1595 let Command::Org(OrgCmd::List { limit }) = cli.cmd else { panic!("expected org list") };
1596 assert_eq!(limit.limit, 5);
1597 }
1598
1599 #[test]
1600 fn ssh_key_commands_parse() {
1601 let cli = Cli::try_parse_from(["gitee", "ssh-key", "list"]).expect("ssh-key list");
1602 assert!(matches!(cli.cmd, Command::SshKey(SshKeyCmd::List { .. })));
1603
1604 let cli = Cli::try_parse_from([
1605 "gitee", "ssh-key", "add", "~/.ssh/id_ed25519.pub", "--title", "laptop",
1606 ]).expect("ssh-key add");
1607 let Command::SshKey(SshKeyCmd::Add { pubkey_file, title }) = cli.cmd else { panic!("add") };
1608 assert_eq!(pubkey_file, "~/.ssh/id_ed25519.pub");
1609 assert_eq!(title.as_deref(), Some("laptop"));
1610
1611 let cli = Cli::try_parse_from(["gitee", "ssh-key", "delete", "99", "--yes"]).expect("delete");
1612 let Command::SshKey(SshKeyCmd::Delete { id, yes }) = cli.cmd else { panic!("delete") };
1613 assert_eq!(id, 99);
1614 assert!(yes);
1615 }
1616
1617 #[test]
1618 fn collaborator_commands_parse() {
1619 let cli = Cli::try_parse_from([
1620 "gitee", "collaborator", "add", "alice", "--permission", "admin",
1621 ]).expect("collaborator add");
1622 let Command::Collaborator(CollaboratorCmd::Add { username, permission }) = cli.cmd else { panic!("add") };
1623 assert_eq!(username, "alice");
1624 assert_eq!(permission, "admin");
1625
1626 let cli = Cli::try_parse_from(["gitee", "collaborator", "remove", "alice", "-y"]).expect("remove");
1627 let Command::Collaborator(CollaboratorCmd::Remove { username, yes }) = cli.cmd else { panic!("remove") };
1628 assert_eq!(username, "alice");
1629 assert!(yes);
1630 }
1631
1632 #[test]
1633 fn webhook_commands_parse() {
1634 let cli = Cli::try_parse_from([
1635 "gitee", "webhook", "create",
1636 "--url", "https://example.com/hook",
1637 "--events", "push_events,issues_events",
1638 "--password", "s3cret",
1639 ]).expect("webhook create");
1640 let Command::Webhook(WebhookCmd::Create { url, events, password }) = cli.cmd else { panic!("create") };
1641 assert_eq!(url, "https://example.com/hook");
1642 assert_eq!(events, vec!["push_events,issues_events".to_string()]);
1643 assert_eq!(password.as_deref(), Some("s3cret"));
1644
1645 let cli = Cli::try_parse_from(["gitee", "webhook", "delete", "55", "--yes"]).expect("delete");
1646 let Command::Webhook(WebhookCmd::Delete { id, yes }) = cli.cmd else { panic!("delete") };
1647 assert_eq!(id, 55);
1648 assert!(yes);
1649
1650 let cli = Cli::try_parse_from([
1651 "gitee", "webhook", "create",
1652 "--url", "https://example.com/hook",
1653 "--events", "pull_requests_events",
1654 ]).expect("webhook alias event");
1655 let Command::Webhook(WebhookCmd::Create { events, .. }) = cli.cmd else { panic!("create") };
1656 assert_eq!(events, vec!["pull_requests_events".to_string()]);
1657 }
1658
1659 #[test]
1660 fn repo_star_watch_parse() {
1661 let cli = Cli::try_parse_from(["gitee", "repo", "star"]).expect("star");
1662 assert!(matches!(cli.cmd, Command::Repo(RepoCmd::Star)));
1663 let cli = Cli::try_parse_from(["gitee", "repo", "unstar"]).expect("unstar");
1664 assert!(matches!(cli.cmd, Command::Repo(RepoCmd::Unstar)));
1665 let cli = Cli::try_parse_from(["gitee", "repo", "watch"]).expect("watch");
1666 assert!(matches!(cli.cmd, Command::Repo(RepoCmd::Watch)));
1667 let cli = Cli::try_parse_from(["gitee", "repo", "unwatch"]).expect("unwatch");
1668 assert!(matches!(cli.cmd, Command::Repo(RepoCmd::Unwatch)));
1669 }
1670
1671 #[test]
1672 fn config_and_alias_parse() {
1673 let cli = Cli::try_parse_from(["gitee", "config", "set", "host", "gitee.com"]).unwrap();
1674 let Command::Config(ConfigCmd::Set { key, value }) = cli.cmd else { panic!("config set") };
1675 assert_eq!(key, "host");
1676 assert_eq!(value, "gitee.com");
1677
1678 let cli = Cli::try_parse_from(["gitee", "alias", "set", "co", "pr", "checkout"]).unwrap();
1679 let Command::Alias(AliasCmd::Set { name, expansion }) = cli.cmd else { panic!("alias set") };
1680 assert_eq!(name, "co");
1681 assert_eq!(expansion, vec!["pr", "checkout"]);
1682
1683 let cli = Cli::try_parse_from(["gitee", "alias", "delete", "co"]).unwrap();
1684 assert!(matches!(cli.cmd, Command::Alias(AliasCmd::Delete { .. })));
1685 }
1686
1687 #[test]
1688 fn auth_setup_switch_credential_parse() {
1689 let cli = Cli::try_parse_from(["gitee", "auth", "setup-git"]).unwrap();
1690 assert!(matches!(cli.cmd, Command::Auth(AuthCmd::SetupGit)));
1691 let cli = Cli::try_parse_from(["gitee", "auth", "switch", "--user", "kip"]).unwrap();
1692 let Command::Auth(AuthCmd::Switch { user }) = cli.cmd else { panic!("switch") };
1693 assert_eq!(user, "kip");
1694 let cli = Cli::try_parse_from(["gitee", "auth", "git-credential", "get"]).unwrap();
1695 assert!(matches!(
1696 cli.cmd,
1697 Command::Auth(AuthCmd::GitCredential(GitCredentialCmd::Get))
1698 ));
1699 }
1700
1701 #[test]
1702 fn extension_list_parse() {
1703 let cli = Cli::try_parse_from(["gitee", "extension", "list"]).unwrap();
1704 assert!(matches!(cli.cmd, Command::Extension(ExtensionCmd::List)));
1705 }
1706
1707 #[test]
1708 fn extension_install_parse() {
1709 let cli = Cli::try_parse_from([
1710 "gitee", "extension", "install", "owner/my-ext", "--build", "cargo", "--yes",
1711 ])
1712 .unwrap();
1713 let Command::Extension(ExtensionCmd::Install { repo, build, yes }) = cli.cmd else {
1714 panic!("expected install");
1715 };
1716 assert_eq!(repo, "owner/my-ext");
1717 assert_eq!(build.as_deref(), Some("cargo"));
1718 assert!(yes);
1719
1720 let cli = Cli::try_parse_from(["gitee", "extension", "install", "owner/ext"]).unwrap();
1721 let Command::Extension(ExtensionCmd::Install { build, yes, .. }) = cli.cmd else {
1722 panic!("expected install");
1723 };
1724 assert!(build.is_none());
1725 assert!(!yes);
1726 }
1727
1728 #[test]
1729 fn extension_install_rejects_bad_build_value() {
1730 assert!(Cli::try_parse_from([
1731 "gitee", "extension", "install", "owner/ext", "--build", "go"
1732 ])
1733 .is_err());
1734 }
1735
1736 #[test]
1737 fn extension_create_parse() {
1738 let cli = Cli::try_parse_from(["gitee", "extension", "create", "demo"]).unwrap();
1739 let Command::Extension(ExtensionCmd::Create { name, cargo }) = cli.cmd else {
1740 panic!("expected create");
1741 };
1742 assert_eq!(name, "demo");
1743 assert!(!cargo);
1744
1745 let cli = Cli::try_parse_from(["gitee", "extension", "create", "demo", "--cargo"]).unwrap();
1746 let Command::Extension(ExtensionCmd::Create { cargo, .. }) = cli.cmd else {
1747 panic!("expected create");
1748 };
1749 assert!(cargo);
1750 }
1751
1752 #[test]
1753 fn extension_remove_parse() {
1754 let cli = Cli::try_parse_from(["gitee", "extension", "remove", "demo", "--yes"]).unwrap();
1755 let Command::Extension(ExtensionCmd::Remove { name, yes }) = cli.cmd else {
1756 panic!("expected remove");
1757 };
1758 assert_eq!(name, "demo");
1759 assert!(yes);
1760 }
1761
1762 #[test]
1763 fn extension_upgrade_parse() {
1764 let cli = Cli::try_parse_from(["gitee", "extension", "upgrade"]).unwrap();
1765 let Command::Extension(ExtensionCmd::Upgrade { name }) = cli.cmd else {
1766 panic!("expected upgrade");
1767 };
1768 assert!(name.is_none());
1769
1770 let cli = Cli::try_parse_from(["gitee", "extension", "upgrade", "demo"]).unwrap();
1771 let Command::Extension(ExtensionCmd::Upgrade { name }) = cli.cmd else {
1772 panic!("expected upgrade");
1773 };
1774 assert_eq!(name.as_deref(), Some("demo"));
1775 }
1776
1777 #[test]
1778 fn external_extension_parse() {
1779 let cli = Cli::try_parse_from(["gitee", "myext", "arg1", "--flag"]).unwrap();
1780 let Command::External(args) = cli.cmd else { panic!("external") };
1781 assert_eq!(args.len(), 3);
1782 assert_eq!(args[0], "myext");
1783 assert_eq!(args[1], "arg1");
1784 assert_eq!(args[2], "--flag");
1785 }
1786
1787 #[test]
1788 fn browse_and_web_parse() {
1789 let cli = Cli::try_parse_from(["gitee", "browse"]).unwrap();
1790 assert!(matches!(cli.cmd, Command::Browse));
1791 let cli = Cli::try_parse_from(["gitee", "pr", "view", "12", "--web"]).unwrap();
1792 let Command::Pr(PrCmd::View { number, web, merged }) = cli.cmd else { panic!("pr view") };
1793 assert_eq!(number, 12);
1794 assert!(web);
1795 assert!(!merged);
1796 let cli = Cli::try_parse_from(["gitee", "pr", "view", "12", "--merged"]).unwrap();
1797 let Command::Pr(PrCmd::View { number, merged, web, .. }) = cli.cmd else { panic!("pr view --merged") };
1798 assert_eq!(number, 12);
1799 assert!(merged);
1800 assert!(!web);
1801 let cli = Cli::try_parse_from(["gitee", "issue", "view", "I1", "--web"]).unwrap();
1802 let Command::Issue(IssueCmd::View { number, web }) = cli.cmd else { panic!("issue view") };
1803 assert_eq!(number, "I1");
1804 assert!(web);
1805 let cli = Cli::try_parse_from(["gitee", "release", "view", "v1", "--web"]).unwrap();
1806 let Command::Release(ReleaseCmd::View { tag, web }) = cli.cmd else { panic!("release view") };
1807 assert_eq!(tag, "v1");
1808 assert!(web);
1809 let cli = Cli::try_parse_from(["gitee", "repo", "view", "--web"]).unwrap();
1810 let Command::Repo(RepoCmd::View { web, .. }) = cli.cmd else { panic!("repo view") };
1811 assert!(web);
1812 }
1813
1814 #[test]
1815 fn issue_comment_create_parses() {
1816 let cli = Cli::try_parse_from([
1817 "gitee", "issue", "comment", "create", "I88", "-m", "looking into it",
1818 ])
1819 .expect("issue comment create should parse");
1820 let Command::Issue(IssueCmd::Comment(IssueCommentCmd::Create { number, body })) = cli.cmd
1821 else {
1822 panic!("expected issue comment create");
1823 };
1824 assert_eq!(number, "I88");
1825 assert_eq!(body.body, "looking into it");
1826 }
1827
1828 #[test]
1829 fn pr_comment_create_parses() {
1830 let cli = Cli::try_parse_from([
1831 "gitee", "pr", "comment", "create", "42", "-m", "LGTM",
1832 ])
1833 .expect("pr comment create should parse");
1834 let Command::Pr(PrCmd::Comment(PrCommentCmd::Create {
1835 number,
1836 body,
1837 path,
1838 position,
1839 commit_id,
1840 })) = cli.cmd
1841 else {
1842 panic!("expected pr comment create");
1843 };
1844 assert_eq!(number, 42);
1845 assert_eq!(body.body, "LGTM");
1846 assert!(path.is_none());
1847 assert!(position.is_none());
1848 assert!(commit_id.is_none());
1849 }
1850
1851 #[test]
1852 fn pr_comment_create_positional_flags_parse() {
1853 let cli = Cli::try_parse_from([
1854 "gitee",
1855 "pr",
1856 "comment",
1857 "create",
1858 "12",
1859 "-m",
1860 "nit",
1861 "--path",
1862 "src/main.rs",
1863 "--position",
1864 "7",
1865 "--commit-id",
1866 "deadbeef",
1867 ])
1868 .expect("pr comment create with positional flags should parse");
1869 let Command::Pr(PrCmd::Comment(PrCommentCmd::Create {
1870 number,
1871 body,
1872 path,
1873 position,
1874 commit_id,
1875 })) = cli.cmd
1876 else {
1877 panic!("expected pr comment create");
1878 };
1879 assert_eq!(number, 12);
1880 assert_eq!(body.body, "nit");
1881 assert_eq!(path.as_deref(), Some("src/main.rs"));
1882 assert_eq!(position, Some(7));
1883 assert_eq!(commit_id.as_deref(), Some("deadbeef"));
1884 }
1885
1886 #[test]
1887 fn pr_comment_create_position_help_documents_diff_line_index() {
1888 use clap::CommandFactory;
1889
1890 let mut cmd = Cli::command();
1891 let help = cmd
1892 .find_subcommand_mut("pr")
1893 .and_then(|c| c.find_subcommand_mut("comment"))
1894 .and_then(|c| c.find_subcommand_mut("create"))
1895 .expect("pr comment create subcommand")
1896 .render_long_help()
1897 .ansi()
1898 .to_string();
1899 assert!(
1900 help.contains("diff-line index"),
1901 "help should document position is a diff-line index: {help}"
1902 );
1903 assert!(
1904 help.contains("not the file line number"),
1905 "help should warn position is not a file line number: {help}"
1906 );
1907 }
1908
1909 #[test]
1910 fn old_flat_comment_form_is_rejected() {
1911 assert!(
1912 Cli::try_parse_from(["gitee", "issue", "comment", "I88", "-m", "x"]).is_err(),
1913 "old issue comment form must fail"
1914 );
1915 assert!(
1916 Cli::try_parse_from(["gitee", "pr", "comment", "42", "-m", "x"]).is_err(),
1917 "old pr comment form must fail"
1918 );
1919 }
1920
1921 #[test]
1922 fn issue_comment_list_parses_limit() {
1923 let cli = Cli::try_parse_from([
1924 "gitee", "issue", "comment", "list", "I88", "--limit", "5",
1925 ])
1926 .expect("issue comment list should parse");
1927 let Command::Issue(IssueCmd::Comment(IssueCommentCmd::List { number, limit })) = cli.cmd
1928 else {
1929 panic!("expected issue comment list");
1930 };
1931 assert_eq!(number, "I88");
1932 assert_eq!(limit.limit, 5);
1933 }
1934
1935 #[test]
1936 fn pr_comment_list_parses_type_and_limit() {
1937 let cli = Cli::try_parse_from([
1938 "gitee", "pr", "comment", "list", "42", "--type", "diff", "--limit", "10",
1939 ])
1940 .expect("pr comment list should parse");
1941 let Command::Pr(PrCmd::Comment(PrCommentCmd::List {
1942 number,
1943 comment_type,
1944 limit,
1945 })) = cli.cmd
1946 else {
1947 panic!("expected pr comment list");
1948 };
1949 assert_eq!(number, 42);
1950 assert_eq!(comment_type.as_deref(), Some("diff"));
1951 assert_eq!(limit.limit, 10);
1952 }
1953
1954 #[test]
1955 fn issue_comment_edit_parses_by_id_and_last() {
1956 let cli = Cli::try_parse_from([
1957 "gitee", "issue", "comment", "edit", "7", "-m", "fixed",
1958 ])
1959 .expect("issue comment edit by id should parse");
1960 let Command::Issue(IssueCmd::Comment(IssueCommentCmd::Edit {
1961 target,
1962 last,
1963 body,
1964 })) = cli.cmd
1965 else {
1966 panic!("expected issue comment edit");
1967 };
1968 assert_eq!(target, "7");
1969 assert!(!last);
1970 assert_eq!(body.as_deref(), Some("fixed"));
1971
1972 let cli = Cli::try_parse_from([
1973 "gitee", "issue", "comment", "edit", "I88", "--last", "-m", "fixed",
1974 ])
1975 .expect("issue comment edit --last should parse");
1976 let Command::Issue(IssueCmd::Comment(IssueCommentCmd::Edit {
1977 target,
1978 last,
1979 body,
1980 })) = cli.cmd
1981 else {
1982 panic!("expected issue comment edit --last");
1983 };
1984 assert_eq!(target, "I88");
1985 assert!(last);
1986 assert_eq!(body.as_deref(), Some("fixed"));
1987 }
1988
1989 #[test]
1990 fn pr_comment_edit_parses_by_id_and_last() {
1991 let cli = Cli::try_parse_from([
1992 "gitee", "pr", "comment", "edit", "42", "-m", "fixed",
1993 ])
1994 .expect("pr comment edit by id should parse");
1995 let Command::Pr(PrCmd::Comment(PrCommentCmd::Edit {
1996 target,
1997 last,
1998 body,
1999 })) = cli.cmd
2000 else {
2001 panic!("expected pr comment edit");
2002 };
2003 assert_eq!(target, 42);
2004 assert!(!last);
2005 assert_eq!(body.as_deref(), Some("fixed"));
2006
2007 let cli = Cli::try_parse_from([
2008 "gitee", "pr", "comment", "edit", "12", "--last", "-m", "fixed",
2009 ])
2010 .expect("pr comment edit --last should parse");
2011 let Command::Pr(PrCmd::Comment(PrCommentCmd::Edit {
2012 target,
2013 last,
2014 body,
2015 })) = cli.cmd
2016 else {
2017 panic!("expected pr comment edit --last");
2018 };
2019 assert_eq!(target, 12);
2020 assert!(last);
2021 assert_eq!(body.as_deref(), Some("fixed"));
2022 }
2023
2024 #[test]
2025 fn issue_comment_delete_parses_by_id_and_last() {
2026 let cli = Cli::try_parse_from(["gitee", "issue", "comment", "delete", "7", "--yes"])
2027 .expect("issue comment delete by id should parse");
2028 let Command::Issue(IssueCmd::Comment(IssueCommentCmd::Delete {
2029 target,
2030 last,
2031 yes,
2032 })) = cli.cmd
2033 else {
2034 panic!("expected issue comment delete");
2035 };
2036 assert_eq!(target, "7");
2037 assert!(!last);
2038 assert!(yes);
2039
2040 let cli = Cli::try_parse_from([
2041 "gitee", "issue", "comment", "delete", "I88", "--last", "-y",
2042 ])
2043 .expect("issue comment delete --last should parse");
2044 let Command::Issue(IssueCmd::Comment(IssueCommentCmd::Delete {
2045 target,
2046 last,
2047 yes,
2048 })) = cli.cmd
2049 else {
2050 panic!("expected issue comment delete --last");
2051 };
2052 assert_eq!(target, "I88");
2053 assert!(last);
2054 assert!(yes);
2055 }
2056
2057 #[test]
2058 fn pr_comment_delete_parses_by_id_and_last() {
2059 let cli = Cli::try_parse_from(["gitee", "pr", "comment", "delete", "42", "--yes"])
2060 .expect("pr comment delete by id should parse");
2061 let Command::Pr(PrCmd::Comment(PrCommentCmd::Delete {
2062 target,
2063 last,
2064 yes,
2065 })) = cli.cmd
2066 else {
2067 panic!("expected pr comment delete");
2068 };
2069 assert_eq!(target, 42);
2070 assert!(!last);
2071 assert!(yes);
2072
2073 let cli = Cli::try_parse_from([
2074 "gitee", "pr", "comment", "delete", "12", "--last", "-y",
2075 ])
2076 .expect("pr comment delete --last should parse");
2077 let Command::Pr(PrCmd::Comment(PrCommentCmd::Delete {
2078 target,
2079 last,
2080 yes,
2081 })) = cli.cmd
2082 else {
2083 panic!("expected pr comment delete --last");
2084 };
2085 assert_eq!(target, 12);
2086 assert!(last);
2087 assert!(yes);
2088 }
2089
2090 #[test]
2091 fn issue_label_add_remove_list_parse() {
2092 let cli = Cli::try_parse_from([
2093 "gitee", "issue", "label", "add", "I88", "bug", "ui",
2094 ])
2095 .expect("issue label add should parse");
2096 let Command::Issue(IssueCmd::Label(IssueLabelCmd::Add { number, labels })) = cli.cmd
2097 else {
2098 panic!("expected issue label add");
2099 };
2100 assert_eq!(number, "I88");
2101 assert_eq!(labels, vec!["bug".to_string(), "ui".to_string()]);
2102
2103 let cli = Cli::try_parse_from(["gitee", "issue", "label", "remove", "I88", "bug"])
2104 .expect("issue label remove should parse");
2105 let Command::Issue(IssueCmd::Label(IssueLabelCmd::Remove { number, labels })) = cli.cmd
2106 else {
2107 panic!("expected issue label remove");
2108 };
2109 assert_eq!(number, "I88");
2110 assert_eq!(labels, vec!["bug".to_string()]);
2111
2112 let cli = Cli::try_parse_from(["gitee", "issue", "label", "list", "I88"])
2113 .expect("issue label list should parse");
2114 let Command::Issue(IssueCmd::Label(IssueLabelCmd::List { number })) = cli.cmd else {
2115 panic!("expected issue label list");
2116 };
2117 assert_eq!(number, "I88");
2118 }
2119
2120 #[test]
2121 fn pr_label_add_remove_list_parse() {
2122 let cli = Cli::try_parse_from(["gitee", "pr", "label", "add", "12", "bug", "ui"])
2123 .expect("pr label add should parse");
2124 let Command::Pr(PrCmd::Label(PrLabelCmd::Add { number, labels })) = cli.cmd else {
2125 panic!("expected pr label add");
2126 };
2127 assert_eq!(number, 12);
2128 assert_eq!(labels, vec!["bug".to_string(), "ui".to_string()]);
2129
2130 let cli = Cli::try_parse_from(["gitee", "pr", "label", "remove", "12", "bug"])
2131 .expect("pr label remove should parse");
2132 let Command::Pr(PrCmd::Label(PrLabelCmd::Remove { number, labels })) = cli.cmd else {
2133 panic!("expected pr label remove");
2134 };
2135 assert_eq!(number, 12);
2136 assert_eq!(labels, vec!["bug".to_string()]);
2137
2138 let cli = Cli::try_parse_from(["gitee", "pr", "label", "list", "12"])
2139 .expect("pr label list should parse");
2140 let Command::Pr(PrCmd::Label(PrLabelCmd::List { number })) = cli.cmd else {
2141 panic!("expected pr label list");
2142 };
2143 assert_eq!(number, 12);
2144 }
2145
2146 #[test]
2147 fn pr_commits_parse() {
2148 let cli = Cli::try_parse_from(["gitee", "pr", "commits", "12"])
2149 .expect("pr commits should parse");
2150 let Command::Pr(PrCmd::Commits { number, limit }) = cli.cmd else {
2151 panic!("expected pr commits");
2152 };
2153 assert_eq!(number, 12);
2154 assert_eq!(limit.limit, 30);
2155
2156 let cli = Cli::try_parse_from(["gitee", "pr", "commits", "12", "--limit", "5"])
2157 .expect("pr commits --limit should parse");
2158 let Command::Pr(PrCmd::Commits { number, limit }) = cli.cmd else {
2159 panic!("expected pr commits with limit");
2160 };
2161 assert_eq!(number, 12);
2162 assert_eq!(limit.limit, 5);
2163 }
2164
2165 #[test]
2166 fn pr_assignee_add_remove_list_parse() {
2167 let cli = Cli::try_parse_from(["gitee", "pr", "assignee", "add", "12", "dev1", "dev2"])
2168 .expect("pr assignee add should parse");
2169 let Command::Pr(PrCmd::Assignee(PrAssigneeCmd::Add { number, users })) = cli.cmd else {
2170 panic!("expected pr assignee add");
2171 };
2172 assert_eq!(number, 12);
2173 assert_eq!(users, vec!["dev1".to_string(), "dev2".to_string()]);
2174
2175 let cli = Cli::try_parse_from(["gitee", "pr", "assignee", "remove", "12", "dev1"])
2176 .expect("pr assignee remove should parse");
2177 let Command::Pr(PrCmd::Assignee(PrAssigneeCmd::Remove { number, users })) = cli.cmd else {
2178 panic!("expected pr assignee remove");
2179 };
2180 assert_eq!(number, 12);
2181 assert_eq!(users, vec!["dev1".to_string()]);
2182
2183 let cli = Cli::try_parse_from(["gitee", "pr", "assignee", "list", "12"])
2184 .expect("pr assignee list should parse");
2185 let Command::Pr(PrCmd::Assignee(PrAssigneeCmd::List { number })) = cli.cmd else {
2186 panic!("expected pr assignee list");
2187 };
2188 assert_eq!(number, 12);
2189 }
2190
2191 #[test]
2192 fn pr_tester_add_remove_list_parse() {
2193 let cli = Cli::try_parse_from(["gitee", "pr", "tester", "add", "12", "qa1", "qa2"])
2194 .expect("pr tester add should parse");
2195 let Command::Pr(PrCmd::Tester(PrTesterCmd::Add { number, users })) = cli.cmd else {
2196 panic!("expected pr tester add");
2197 };
2198 assert_eq!(number, 12);
2199 assert_eq!(users, vec!["qa1".to_string(), "qa2".to_string()]);
2200
2201 let cli = Cli::try_parse_from(["gitee", "pr", "tester", "remove", "12", "qa1"])
2202 .expect("pr tester remove should parse");
2203 let Command::Pr(PrCmd::Tester(PrTesterCmd::Remove { number, users })) = cli.cmd else {
2204 panic!("expected pr tester remove");
2205 };
2206 assert_eq!(number, 12);
2207 assert_eq!(users, vec!["qa1".to_string()]);
2208
2209 let cli = Cli::try_parse_from(["gitee", "pr", "tester", "list", "12"])
2210 .expect("pr tester list should parse");
2211 let Command::Pr(PrCmd::Tester(PrTesterCmd::List { number })) = cli.cmd else {
2212 panic!("expected pr tester list");
2213 };
2214 assert_eq!(number, 12);
2215 }
2216}