1use crate::api_traits::{CommentMergeRequest, MergeRequest, RemoteProject, Timestamp};
2use crate::cli::merge_request::MergeRequestOptions;
3use crate::config::ConfigProperties;
4use crate::display::{Column, DisplayBody};
5use crate::error::{AddContext, GRError};
6use crate::git::Repo;
7use crate::io::{CmdInfo, ShellResponse, TaskRunner};
8use crate::remote::{CacheCliArgs, CacheType, GetRemoteCliArgs, ListBodyArgs, ListRemoteCliArgs};
9use crate::shell::BlockingCommand;
10use crate::{dialog, display, exec, git, remote, Cmd, Result};
11use std::fmt::{self, Display, Formatter};
12use std::{
13 fs::File,
14 io::{BufRead, BufReader, Cursor, Write},
15 sync::Arc,
16};
17
18use super::common::{self, get_user};
19use super::project::{Member, Project};
20
21const GPT_PROMPT: &str = r#"
24Act like a professional software engineer. You have finished a new feature and
25want to create a new pull request for someone else to review your changes. You
26will be provided by a list of commit messages with the following format:
27
28- Newest commit is at the very bottom.
29- The subject of the commit message is followed by a dash `-` and its short SHA.
30- If there is a body, it will be immediately below the subject `-` short SHA line.
31
32You will provide a description of these changes. The description must not
33mention commit SHAs and it must not mention the number of commits included. Be
34concise and provide at most two paragraphs describing the changes. Use
35imperative mode, be short and to the point. The description for the pull
36request will begin with the sentence `This merge request`.
37
38The formatted output that you will provide is as follows:
39
40- First line will be the title. Keep it within 80 characters width
41- Next line is blank
42- The following lines will be the description of the pull request
43
44Below are the changes:"#;
45
46#[derive(Builder, Clone, Debug, Default)]
47#[builder(default)]
48pub struct MergeRequestResponse {
49 pub id: i64,
50 pub web_url: String,
51 pub author: String,
52 pub updated_at: String,
53 pub source_branch: String,
54 pub sha: String,
55 pub created_at: String,
56 pub title: String,
57 pub pull_request: String,
59 pub description: String,
61 pub merged_at: String,
62 pub pipeline_id: Option<i64>,
63 pub pipeline_url: Option<String>,
64}
65
66impl MergeRequestResponse {
67 pub fn builder() -> MergeRequestResponseBuilder {
68 MergeRequestResponseBuilder::default()
69 }
70}
71
72impl From<MergeRequestResponse> for DisplayBody {
73 fn from(mr: MergeRequestResponse) -> DisplayBody {
74 DisplayBody {
75 columns: vec![
76 Column::new("ID", mr.id.to_string()),
77 Column::new("Title", mr.title),
78 Column::new("Source Branch", mr.source_branch),
79 Column::builder()
80 .name("SHA".to_string())
81 .value(mr.sha)
82 .optional(true)
83 .build()
84 .unwrap(),
85 Column::builder()
86 .name("Description".to_string())
87 .value(mr.description)
88 .optional(true)
89 .build()
90 .unwrap(),
91 Column::new("Author", mr.author),
92 Column::new("URL", mr.web_url),
93 Column::new("Updated at", mr.updated_at),
94 Column::builder()
95 .name("Merged at".to_string())
96 .value(mr.merged_at)
97 .optional(true)
98 .build()
99 .unwrap(),
100 Column::builder()
101 .name("Pipeline ID".to_string())
102 .value(mr.pipeline_id.map_or("".to_string(), |id| id.to_string()))
103 .optional(true)
104 .build()
105 .unwrap(),
106 Column::builder()
107 .name("Pipeline URL".to_string())
108 .value(mr.pipeline_url.unwrap_or("".to_string()))
109 .optional(true)
110 .build()
111 .unwrap(),
112 ],
113 }
114 }
115}
116
117impl Timestamp for MergeRequestResponse {
118 fn created_at(&self) -> String {
119 self.created_at.clone()
120 }
121}
122
123#[derive(Clone, Copy, PartialEq, Debug)]
124pub enum MergeRequestState {
125 Opened,
126 Closed,
127 Merged,
128}
129
130impl TryFrom<&str> for MergeRequestState {
131 type Error = String;
132
133 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
134 match s {
135 "opened" => Ok(MergeRequestState::Opened),
136 "closed" => Ok(MergeRequestState::Closed),
137 "merged" => Ok(MergeRequestState::Merged),
138 _ => Err(format!("Invalid merge request state: {}", s)),
139 }
140 }
141}
142
143impl Display for MergeRequestState {
144 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
145 match self {
146 MergeRequestState::Opened => write!(f, "opened"),
147 MergeRequestState::Closed => write!(f, "closed"),
148 MergeRequestState::Merged => write!(f, "merged"),
149 }
150 }
151}
152
153#[derive(Builder, Debug)]
154pub struct MergeRequestBodyArgs {
155 #[builder(default)]
156 pub title: String,
157 #[builder(default)]
158 pub description: String,
159 #[builder(default)]
160 pub source_branch: String,
161 #[builder(default)]
162 pub target_repo: String,
163 #[builder(default)]
164 pub target_branch: String,
165 #[builder(default)]
166 pub assignee: Member,
167 #[builder(default)]
168 pub reviewer: Member,
169 #[builder(default = "String::from(\"true\")")]
170 pub remove_source_branch: String,
171 #[builder(default)]
172 pub draft: bool,
173 #[builder(default)]
174 pub amend: bool,
175}
176
177impl MergeRequestBodyArgs {
178 pub fn builder() -> MergeRequestBodyArgsBuilder {
179 MergeRequestBodyArgsBuilder::default()
180 }
181}
182
183#[derive(Builder, Clone)]
184pub struct MergeRequestListBodyArgs {
185 pub state: MergeRequestState,
186 pub list_args: Option<ListBodyArgs>,
187 #[builder(default)]
188 pub assignee: Option<Member>,
189 #[builder(default)]
190 pub author: Option<Member>,
191 #[builder(default)]
192 pub reviewer: Option<Member>,
193}
194
195impl MergeRequestListBodyArgs {
196 pub fn builder() -> MergeRequestListBodyArgsBuilder {
197 MergeRequestListBodyArgsBuilder::default()
198 }
199}
200
201#[derive(Builder, Clone)]
202pub struct MergeRequestCliArgs {
203 pub title: Option<String>,
204 pub body_from_commit: Option<String>,
205 #[builder(default)]
206 pub body_from_file: Option<String>,
207 pub description: Option<String>,
208 pub description_from_file: Option<String>,
209 #[builder(default)]
210 pub assignee: Option<String>,
211 #[builder(default)]
212 pub reviewer: Option<String>,
213 #[builder(default)]
214 pub rand_reviewer: bool,
215 pub target_branch: Option<String>,
216 #[builder(default)]
217 pub target_repo: Option<String>,
218 #[builder(default)]
219 pub fetch: Option<String>,
220 #[builder(default)]
221 pub rebase: Option<String>,
222 pub auto: bool,
223 pub cache_args: CacheCliArgs,
224 pub open_browser: bool,
225 pub accept_summary: bool,
226 pub commit: Option<String>,
227 pub amend: bool,
228 pub force: bool,
229 pub draft: bool,
230 pub dry_run: bool,
231 #[builder(default)]
232 pub summary: SummaryOptions,
233 #[builder(default)]
234 pub patch: bool,
235 #[builder(default)]
236 pub gpt_prompt: bool,
237}
238
239#[derive(Clone, Debug, Default, PartialEq)]
240pub enum SummaryOptions {
241 Short,
242 Long,
243 #[default]
244 None,
245}
246
247impl MergeRequestCliArgs {
248 pub fn builder() -> MergeRequestCliArgsBuilder {
249 MergeRequestCliArgsBuilder::default()
250 }
251}
252
253#[derive(Clone, Debug, PartialEq)]
257pub enum MergeRequestUser {
258 Me,
259 Other(String),
260}
261
262#[derive(Builder)]
263pub struct MergeRequestListCliArgs {
264 pub state: MergeRequestState,
265 pub list_args: ListRemoteCliArgs,
266 #[builder(default)]
268 pub assignee: Option<MergeRequestUser>,
269 #[builder(default)]
270 pub author: Option<MergeRequestUser>,
271 #[builder(default)]
272 pub reviewer: Option<MergeRequestUser>,
273}
274
275impl MergeRequestListCliArgs {
276 pub fn new(state: MergeRequestState, args: ListRemoteCliArgs) -> MergeRequestListCliArgs {
277 MergeRequestListCliArgs {
278 state,
279 list_args: args,
280 assignee: None,
281 author: None,
282 reviewer: None,
283 }
284 }
285 pub fn builder() -> MergeRequestListCliArgsBuilder {
286 MergeRequestListCliArgsBuilder::default()
287 }
288}
289
290#[derive(Builder)]
291pub struct MergeRequestGetCliArgs {
292 pub id: i64,
293 pub get_args: GetRemoteCliArgs,
294}
295
296impl MergeRequestGetCliArgs {
297 pub fn builder() -> MergeRequestGetCliArgsBuilder {
298 MergeRequestGetCliArgsBuilder::default()
299 }
300}
301
302#[derive(Builder)]
303pub struct CommentMergeRequestCliArgs {
304 pub id: i64,
305 pub comment: Option<String>,
306 pub comment_from_file: Option<String>,
307}
308
309impl CommentMergeRequestCliArgs {
310 pub fn builder() -> CommentMergeRequestCliArgsBuilder {
311 CommentMergeRequestCliArgsBuilder::default()
312 }
313}
314
315#[derive(Builder)]
316pub struct CommentMergeRequestListCliArgs {
317 pub id: i64,
318 pub list_args: ListRemoteCliArgs,
319}
320
321impl CommentMergeRequestListCliArgs {
322 pub fn builder() -> CommentMergeRequestListCliArgsBuilder {
323 CommentMergeRequestListCliArgsBuilder::default()
324 }
325}
326
327#[derive(Builder)]
328pub struct CommentMergeRequestListBodyArgs {
329 pub id: i64,
330 pub list_args: Option<ListBodyArgs>,
331}
332
333impl CommentMergeRequestListBodyArgs {
334 pub fn builder() -> CommentMergeRequestListBodyArgsBuilder {
335 CommentMergeRequestListBodyArgsBuilder::default()
336 }
337}
338
339#[derive(Builder)]
340pub struct CommentMergeRequestBodyArgs {
341 pub id: i64,
342 pub comment: String,
343}
344
345impl CommentMergeRequestBodyArgs {
346 pub fn builder() -> CommentMergeRequestBodyArgsBuilder {
347 CommentMergeRequestBodyArgsBuilder::default()
348 }
349}
350
351#[derive(Builder, Clone)]
352pub struct Comment {
353 pub id: i64,
354 pub body: String,
355 pub author: String,
356 pub created_at: String,
357}
358
359impl Comment {
360 pub fn builder() -> CommentBuilder {
361 CommentBuilder::default()
362 }
363}
364
365impl Timestamp for Comment {
366 fn created_at(&self) -> String {
367 self.created_at.clone()
368 }
369}
370
371impl From<Comment> for DisplayBody {
372 fn from(comment: Comment) -> Self {
373 DisplayBody::new(vec![
374 Column::new("ID", comment.id.to_string()),
375 Column::new("Body", comment.body),
376 Column::new("Author", comment.author),
377 Column::new("Created at", comment.created_at),
378 ])
379 }
380}
381
382pub fn execute(
383 options: MergeRequestOptions,
384 config: Arc<dyn ConfigProperties>,
385 domain: String,
386 path: String,
387) -> Result<()> {
388 match options {
389 MergeRequestOptions::Create(cli_args) => {
390 let mr_remote = remote::get_mr(
391 domain.clone(),
392 path.clone(),
393 config.clone(),
394 Some(&cli_args.cache_args),
395 CacheType::File,
396 )?;
397 let project_remote = remote::get_project(
398 domain,
399 path,
400 config.clone(),
401 Some(&cli_args.cache_args),
402 CacheType::File,
403 )?;
404 if let Some(commit_message) = &cli_args.commit {
405 git::add(&BlockingCommand)?;
406 git::commit(&BlockingCommand, commit_message)?;
407 }
408 let cmds = if let Some(description_file) = &cli_args.description_from_file {
409 let reader = get_reader_file_cli(description_file)?;
410 cmds(
411 project_remote,
412 &cli_args,
413 Arc::new(BlockingCommand),
414 Some(reader),
415 )
416 } else if let Some(body_from_file) = &cli_args.body_from_file {
417 let reader = get_reader_file_cli(body_from_file)?;
418 cmds(
419 project_remote,
420 &cli_args,
421 Arc::new(BlockingCommand),
422 Some(reader),
423 )
424 } else {
425 cmds(
426 project_remote,
427 &cli_args,
428 Arc::new(BlockingCommand),
429 None::<Cursor<&str>>,
430 )
431 };
432 let mr_body = get_repo_project_info(cmds)?;
433 if cli_args.summary != SummaryOptions::None {
434 return summary(mr_body, &cli_args);
435 }
436 if cli_args.patch {
437 return patch(mr_body, &cli_args);
438 }
439 open(mr_remote, config, mr_body, &cli_args)
440 }
441 MergeRequestOptions::List(cli_args) => list_merge_requests(domain, path, config, cli_args),
442 MergeRequestOptions::Merge { id } => {
443 let remote = remote::get_mr(domain, path, config, None, CacheType::None)?;
444 merge(remote, id)
445 }
446 MergeRequestOptions::Checkout { id } => {
447 let remote = remote::get_mr(domain, path, config, None, CacheType::File)?;
449 checkout(remote, id)
450 }
451 MergeRequestOptions::Close { id } => {
452 let remote = remote::get_mr(domain, path, config, None, CacheType::None)?;
453 close(remote, id)
454 }
455 MergeRequestOptions::CreateComment(cli_args) => {
456 let remote = remote::get_comment_mr(domain, path, config, None, CacheType::None)?;
457 if let Some(comment_file) = &cli_args.comment_from_file {
458 let reader = get_reader_file_cli(comment_file)?;
459 create_comment(remote, cli_args, Some(reader))
460 } else {
461 create_comment(remote, cli_args, None::<Cursor<&str>>)
462 }
463 }
464 MergeRequestOptions::ListComment(cli_args) => {
465 let remote = remote::get_comment_mr(
466 domain,
467 path,
468 config,
469 Some(&cli_args.list_args.get_args.cache_args),
470 CacheType::File,
471 )?;
472 let from_to_args = remote::validate_from_to_page(&cli_args.list_args)?;
473 let body_args = CommentMergeRequestListBodyArgs::builder()
474 .id(cli_args.id)
475 .list_args(from_to_args)
476 .build()?;
477 if cli_args.list_args.num_pages {
478 return common::num_comment_merge_request_pages(
479 remote,
480 body_args,
481 std::io::stdout(),
482 );
483 }
484 if cli_args.list_args.num_resources {
485 return common::num_comment_merge_request_resources(
486 remote,
487 body_args,
488 std::io::stdout(),
489 );
490 }
491 list_comments(remote, body_args, cli_args, std::io::stdout())
492 }
493 MergeRequestOptions::Get(cli_args) => {
494 let remote = remote::get_mr(
495 domain,
496 path,
497 config,
498 Some(&cli_args.get_args.cache_args),
499 CacheType::File,
500 )?;
501 get_merge_request_details(remote, cli_args, std::io::stdout())
502 }
503 MergeRequestOptions::Approve { id } => {
504 let remote = remote::get_mr(domain, path, config, None, CacheType::None)?;
505 approve(remote, id, std::io::stdout())
506 }
507 }
508}
509
510pub fn get_reader_file_cli(file_path: &str) -> Result<Box<dyn BufRead + Send + Sync>> {
511 if file_path == "-" {
512 Ok(Box::new(BufReader::new(std::io::stdin())))
513 } else {
514 let file = File::open(file_path).err_context(GRError::PreconditionNotMet(format!(
515 "Cannot open file {}",
516 file_path
517 )))?;
518 Ok(Box::new(BufReader::new(file)))
519 }
520}
521
522fn get_filter_user(
523 user: &Option<MergeRequestUser>,
524 domain: &str,
525 path: &str,
526 config: &Arc<dyn ConfigProperties>,
527 list_args: &ListRemoteCliArgs,
528) -> Result<Option<Member>> {
529 let member = match user {
530 Some(MergeRequestUser::Me) => Some(get_user(domain, path, config, list_args)?),
531 _ => None,
534 };
535 Ok(member)
536}
537
538pub fn list_merge_requests(
539 domain: String,
540 path: String,
541 config: Arc<dyn ConfigProperties>,
542 cli_args: MergeRequestListCliArgs,
543) -> Result<()> {
544 let author = get_filter_user(
548 &cli_args.author,
549 &domain,
550 &path,
551 &config,
552 &cli_args.list_args,
553 )?;
554
555 let assignee = get_filter_user(
556 &cli_args.assignee,
557 &domain,
558 &path,
559 &config,
560 &cli_args.list_args,
561 )?;
562
563 let reviewer = get_filter_user(
564 &cli_args.reviewer,
565 &domain,
566 &path,
567 &config,
568 &cli_args.list_args,
569 )?;
570
571 let remote = remote::get_mr(
572 domain,
573 path,
574 config,
575 Some(&cli_args.list_args.get_args.cache_args),
576 CacheType::File,
577 )?;
578
579 let from_to_args = remote::validate_from_to_page(&cli_args.list_args)?;
580 let body_args = MergeRequestListBodyArgs::builder()
581 .list_args(from_to_args)
582 .state(cli_args.state)
583 .assignee(assignee)
584 .author(author)
585 .reviewer(reviewer)
586 .build()?;
587 if cli_args.list_args.num_pages {
588 return common::num_merge_request_pages(remote, body_args, std::io::stdout());
589 }
590 if cli_args.list_args.num_resources {
591 return common::num_merge_request_resources(remote, body_args, std::io::stdout());
592 }
593 list(remote, body_args, cli_args, std::io::stdout())
594}
595
596fn get_member(members: &[Member], username: &str) -> Option<Member> {
597 members
598 .iter()
599 .find(|member| member.username == username)
600 .cloned()
601}
602
603fn user_prompt_confirmation(
604 mr_body: &MergeRequestBody,
605 config: Arc<dyn ConfigProperties>,
606 description: String,
607 target_branch: &String,
608 cli_args: &MergeRequestCliArgs,
609) -> Result<MergeRequestBodyArgs> {
610 let mut title = mr_body.repo.title().to_string();
611 if cli_args.draft {
612 title = format!("DRAFT: {}", title);
613 }
614 let members = config.merge_request_members();
617 let assignee = if cli_args.assignee.is_some() {
618 get_member(&members, &cli_args.assignee.clone().unwrap())
619 } else {
620 None
621 };
622
623 let reviewer = if cli_args.reviewer.is_some() {
624 get_member(&members, &cli_args.reviewer.clone().unwrap())
625 } else if cli_args.rand_reviewer {
626 let members = config.merge_request_members();
627 let num_members = members.len();
628 if num_members == 0 {
629 None
630 } else {
631 let rand_index = rand::random::<usize>();
632 let rand_user = members[rand_index % num_members].clone();
633 Some(rand_user)
634 }
635 } else {
636 None
637 };
638
639 let user_input = if cli_args.auto {
640 let preferred_assignee_members =
641 [assignee.unwrap_or(config.preferred_assignee_username().unwrap_or_default())];
642 dialog::MergeRequestUserInput::builder()
643 .title(title)
644 .description(description)
645 .assignee(preferred_assignee_members[0].clone())
646 .reviewer(reviewer.unwrap_or_default())
647 .build()
648 .unwrap()
649 } else {
650 dialog::prompt_user_merge_request_info(
651 &title,
652 &description,
653 assignee.as_ref(),
654 reviewer.as_ref(),
655 &config,
656 )?
657 };
658
659 Ok(MergeRequestBodyArgs::builder()
660 .title(user_input.title)
661 .description(user_input.description)
662 .source_branch(mr_body.repo.current_branch().to_string())
663 .target_branch(target_branch.to_string())
664 .assignee(user_input.assignee)
665 .reviewer(user_input.reviewer)
666 .remove_source_branch("true".to_string())
668 .draft(cli_args.draft)
669 .amend(cli_args.amend)
670 .build()?)
671}
672
673fn open(
675 remote: Arc<dyn MergeRequest>,
676 config: Arc<dyn ConfigProperties>,
677 mr_body: MergeRequestBody,
678 cli_args: &MergeRequestCliArgs,
679) -> Result<()> {
680 let source_branch = &mr_body.repo.current_branch();
681 let target_branch = cli_args.target_branch.clone();
682 let target_branch = target_branch.unwrap_or(mr_body.project.default_branch().to_string());
683
684 let description = build_description(
685 mr_body.repo.last_commit_message(),
686 config.merge_request_description_signature(),
687 );
688
689 in_feature_branch(source_branch, &target_branch)?;
691
692 let args = user_prompt_confirmation(&mr_body, config, description, &target_branch, cli_args)?;
694
695 if cli_args.rebase.is_some() {
696 git::rebase(&BlockingCommand, cli_args.rebase.as_ref().unwrap())?;
697 }
698
699 let outgoing_commits = git::outgoing_commits(
700 &BlockingCommand,
701 "origin",
702 &target_branch,
703 &SummaryOptions::Short,
704 )?;
705
706 if outgoing_commits.is_empty() {
707 return Err(GRError::PreconditionNotMet(
708 "No outgoing commits found. Please commit your changes.".to_string(),
709 )
710 .into());
711 }
712
713 if let Ok(()) =
715 dialog::show_summary_merge_request(&outgoing_commits, &args, cli_args.accept_summary)
716 {
717 println!("\nTaking off... 🚀\n");
718 if cli_args.dry_run {
719 println!("Dry run completed. No changes were made.");
720 return Ok(());
721 }
722 git::push(&BlockingCommand, "origin", &mr_body.repo, cli_args.force)?;
723 let merge_request_response = remote.open(args)?;
724 println!("Merge request opened: {}", merge_request_response.web_url);
725 if cli_args.open_browser {
726 open::that(merge_request_response.web_url)?;
727 }
728 }
729 Ok(())
730}
731
732fn summary(mr_body: MergeRequestBody, cli_args: &MergeRequestCliArgs) -> Result<()> {
734 let source_branch = mr_body.repo.current_branch();
735 let target_branch = cli_args.target_branch.clone();
736 let target_branch = target_branch.unwrap_or(mr_body.project.default_branch().to_string());
737
738 in_feature_branch(source_branch, &target_branch)?;
739
740 if cli_args.rebase.is_some() {
741 git::rebase(&BlockingCommand, cli_args.rebase.as_ref().unwrap())?;
742 }
743
744 let outgoing_commits = git::outgoing_commits(
745 &BlockingCommand,
746 "origin",
747 &target_branch,
748 &cli_args.summary,
749 )?;
750
751 if outgoing_commits.is_empty() {
752 return Err(GRError::PreconditionNotMet(
753 "No outgoing commits found. Please commit your changes.".to_string(),
754 )
755 .into());
756 }
757 if cli_args.gpt_prompt {
758 println!("{}", GPT_PROMPT);
759 }
760 println!("\n{}", outgoing_commits);
761 Ok(())
762}
763
764fn patch(mr_body: MergeRequestBody, cli_args: &MergeRequestCliArgs) -> Result<()> {
766 let source_branch = mr_body.repo.current_branch();
767 let target_branch = cli_args.target_branch.clone();
768 let target_branch = target_branch.unwrap_or(mr_body.project.default_branch().to_string());
769
770 in_feature_branch(source_branch, &target_branch)?;
771
772 if cli_args.rebase.is_some() {
773 git::rebase(&BlockingCommand, cli_args.rebase.as_ref().unwrap())?;
774 }
775 println!(
776 "{}",
777 git::patch(&BlockingCommand, source_branch, target_branch)?
778 );
779 Ok(())
780}
781
782fn cmds<R: BufRead + Send + Sync + 'static>(
784 remote: Arc<dyn RemoteProject + Send + Sync + 'static>,
785 cli_args: &MergeRequestCliArgs,
786 task_runner: Arc<impl TaskRunner<Response = ShellResponse> + Send + Sync + 'static>,
787 reader: Option<R>,
788) -> Vec<Cmd<CmdInfo>> {
789 let remote_cl = remote.clone();
790 let remote_project_cmd = move || -> Result<CmdInfo> { remote_cl.get_project_data(None, None) };
791 let status_runner = task_runner.clone();
792 let git_status_cmd = || -> Result<CmdInfo> { git::status(status_runner) };
793 let current_branch_runner = task_runner.clone();
794 let git_current_branch = || -> Result<CmdInfo> { git::current_branch(current_branch_runner) };
795 let mut cmds: Vec<Cmd<CmdInfo>> = vec![
796 Box::new(remote_project_cmd),
797 Box::new(git_status_cmd),
798 Box::new(git_current_branch),
799 ];
800
801 if cli_args.body_from_file.is_some() {
802 let reader = reader.unwrap();
803 let body_from_file_cmd = move || -> Result<CmdInfo> {
804 let mut description = String::new();
805 let mut lines = reader.lines();
806 let title = lines.next().unwrap_or_else(|| Ok("".to_string()))?;
807 lines.next();
809 for line in lines {
810 let line = line?;
811 description.push_str(&line);
812 description.push('\n');
813 }
814 Ok(CmdInfo::CommitBody(title, description))
815 };
816 cmds.push(Box::new(body_from_file_cmd));
817 } else {
818 let title = cli_args.title.clone();
820 let title = title.unwrap_or("".to_string());
821 let body_from_commit = cli_args.body_from_commit.clone();
822 let description_commit = cli_args.body_from_commit.clone();
826 let commit_summary_runner = task_runner.clone();
827 let git_title_cmd = move || -> Result<CmdInfo> {
828 if title.is_empty() {
829 git::commit_summary(commit_summary_runner, &body_from_commit)
830 } else {
831 Ok(CmdInfo::CommitSummary(title.clone()))
832 }
833 };
834 let description = cli_args.description.clone();
835 let description = description.unwrap_or("".to_string());
836 let commit_msg_runner = task_runner.clone();
837 let git_last_commit_message = move || -> Result<CmdInfo> {
838 if description.is_empty() {
839 if let Some(reader) = reader {
840 let mut description = String::new();
841 for line in reader.lines() {
842 let line = line?;
843 description.push_str(&line);
844 description.push('\n');
845 }
846 Ok(CmdInfo::CommitMessage(description))
847 } else {
848 git::commit_message(commit_msg_runner, &description_commit)
849 }
850 } else {
851 Ok(CmdInfo::CommitMessage(description.clone()))
852 }
853 };
854 cmds.push(Box::new(git_title_cmd));
855 cmds.push(Box::new(git_last_commit_message));
856 }
857 if cli_args.fetch.is_some() {
858 let fetch_runner = task_runner.clone();
859 let remote_alias = cli_args.fetch.as_ref().unwrap().clone();
860 let git_fetch_cmd = || -> Result<CmdInfo> { git::fetch(fetch_runner, remote_alias) };
861 cmds.push(Box::new(git_fetch_cmd));
862 }
863 cmds
864}
865
866fn build_description(description: &str, signature: &str) -> String {
868 if description.is_empty() && signature.is_empty() {
869 return "".to_string();
870 }
871 if description.is_empty() {
872 return signature.to_string();
873 }
874 if signature.is_empty() {
875 return description.to_string();
876 }
877 format!("{}\n\n{}", description, signature)
878}
879
880#[derive(Builder)]
881struct MergeRequestBody {
882 repo: Repo,
883 project: Project,
884}
885
886impl MergeRequestBody {
887 fn builder() -> MergeRequestBodyBuilder {
888 MergeRequestBodyBuilder::default()
889 }
890}
891
892fn get_repo_project_info(cmds: Vec<Cmd<CmdInfo>>) -> Result<MergeRequestBody> {
893 let mut project = Project::default();
894 let mut repo = git::Repo::default();
895 let cmd_results = exec::parallel_stream(cmds);
896 for cmd_result in cmd_results {
897 match cmd_result {
898 Ok(CmdInfo::Project(project_data)) => {
899 project = project_data;
900 }
901 Ok(CmdInfo::StatusModified(status)) => repo.with_status(status),
902 Ok(CmdInfo::Branch(branch)) => repo.with_branch(&branch),
903 Ok(CmdInfo::CommitSummary(title)) => repo.with_title(&title),
904 Ok(CmdInfo::CommitMessage(message)) => repo.with_last_commit_message(&message),
905 Ok(CmdInfo::CommitBody(title, description)) => {
906 repo.with_title(&title);
907 repo.with_last_commit_message(&description);
908 }
909 Err(e) => return Err(e),
911 _ => {}
912 }
913 }
914 Ok(MergeRequestBody::builder()
915 .repo(repo)
916 .project(project)
917 .build()?)
918}
919
920fn in_feature_branch(current_branch: &str, upstream_branch: &str) -> Result<()> {
922 if current_branch == upstream_branch {
923 let trace = format!(
924 "Current branch {} is the same as the upstream \
925 remote {}. Please use a feature branch",
926 current_branch, upstream_branch
927 );
928 return Err(GRError::PreconditionNotMet(trace).into());
929 }
930 match current_branch {
933 "master" | "main" | "develop" => {
934 let trace = format!(
935 "Current branch is {}, which could be a release upstream branch. \
936 Please use a different feature branch name",
937 current_branch
938 );
939 Err(GRError::PreconditionNotMet(trace).into())
940 }
941 _ => Ok(()),
942 }
943}
944
945fn list<W: Write>(
946 remote: Arc<dyn MergeRequest>,
947 body_args: MergeRequestListBodyArgs,
948 cli_args: MergeRequestListCliArgs,
949 mut writer: W,
950) -> Result<()> {
951 common::list_merge_requests(remote, body_args, cli_args, &mut writer)
952}
953
954fn merge(remote: Arc<dyn MergeRequest>, merge_request_id: i64) -> Result<()> {
955 let merge_request = remote.merge(merge_request_id)?;
956 println!("Merge request merged: {}", merge_request.web_url);
957 Ok(())
958}
959
960fn checkout(remote: Arc<dyn MergeRequest>, id: i64) -> Result<()> {
961 let merge_request = remote.get(id)?;
962 git::fetch(Arc::new(BlockingCommand), "origin".to_string())?;
964 git::checkout(&BlockingCommand, &merge_request.source_branch)
965}
966
967fn close(remote: Arc<dyn MergeRequest>, id: i64) -> Result<()> {
968 let merge_request = remote.close(id)?;
969 println!("Merge request closed: {}", merge_request.web_url);
970 Ok(())
971}
972
973fn approve<W: Write>(remote: Arc<dyn MergeRequest>, id: i64, mut writer: W) -> Result<()> {
974 let merge_request = remote.approve(id)?;
975 writer.write_all(format!("Merge request approved: {}\n", merge_request.web_url).as_bytes())?;
976 Ok(())
977}
978
979fn create_comment<R: BufRead>(
980 remote: Arc<dyn CommentMergeRequest>,
981 args: CommentMergeRequestCliArgs,
982 reader: Option<R>,
983) -> Result<()> {
984 let comment = if let Some(comment) = args.comment {
985 comment
986 } else {
987 let mut comment = String::new();
988 reader.unwrap().read_to_string(&mut comment)?;
991 comment
992 };
993 remote.create(
994 CommentMergeRequestBodyArgs::builder()
995 .id(args.id)
996 .comment(comment)
997 .build()
998 .unwrap(),
999 )
1000}
1001
1002pub fn get_merge_request_details<W: Write>(
1003 remote: Arc<dyn MergeRequest>,
1004 args: MergeRequestGetCliArgs,
1005 mut writer: W,
1006) -> Result<()> {
1007 let response = remote.get(args.id)?;
1008 display::print(&mut writer, vec![response], args.get_args)?;
1009 Ok(())
1010}
1011
1012fn list_comments<W: Write>(
1013 remote: Arc<dyn CommentMergeRequest>,
1014 body_args: CommentMergeRequestListBodyArgs,
1015 cli_args: CommentMergeRequestListCliArgs,
1016 writer: W,
1017) -> Result<()> {
1018 common::list_merge_request_comments(remote, body_args, cli_args, writer)
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use std::{
1024 io::{Cursor, Read},
1025 sync::Mutex,
1026 };
1027
1028 use crate::{
1029 api_traits::CommentMergeRequest, cli::browse::BrowseOptions,
1030 cmds::project::ProjectListBodyArgs, error,
1031 };
1032
1033 use super::*;
1034
1035 #[test]
1036 fn test_merge_request_args_with_custom_title() {
1037 let args = MergeRequestBodyArgs::builder()
1038 .source_branch("source".to_string())
1039 .target_branch("target".to_string())
1040 .title("title".to_string())
1041 .build()
1042 .unwrap();
1043
1044 assert_eq!(args.source_branch, "source");
1045 assert_eq!(args.target_branch, "target");
1046 assert_eq!(args.title, "title");
1047 assert_eq!(args.remove_source_branch, "true");
1048 assert_eq!(args.description, "");
1049 }
1050
1051 #[test]
1052 fn test_merge_request_get_all_fields() {
1053 let assignee = Member::builder()
1054 .id(1)
1055 .username("username".to_string())
1056 .build()
1057 .unwrap();
1058 let args = MergeRequestBodyArgs::builder()
1059 .source_branch("source".to_string())
1060 .target_branch("target".to_string())
1061 .title("title".to_string())
1062 .description("description".to_string())
1063 .assignee(assignee)
1064 .remove_source_branch("false".to_string())
1065 .build()
1066 .unwrap();
1067
1068 assert_eq!(args.source_branch, "source");
1069 assert_eq!(args.target_branch, "target");
1070 assert_eq!(args.title, "title");
1071 assert_eq!(args.description, "description");
1072 assert_eq!(args.assignee.id, 1);
1073 assert_eq!(args.assignee.username, "username");
1074 assert_eq!(args.remove_source_branch, "false");
1075 }
1076
1077 #[test]
1078 fn test_current_branch_should_not_be_the_upstream_branch() {
1079 let current_branch = "current-branch";
1080 let target_branch = "current-branch";
1081 let result = in_feature_branch(current_branch, target_branch);
1082 assert!(result.is_err());
1083 }
1084
1085 #[test]
1086 fn test_feature_branch_not_main_master_or_develop_is_ok() {
1087 let current_branch = "newfeature";
1088 let target_branch = "main";
1089 let result = in_feature_branch(current_branch, target_branch);
1090 assert!(result.is_ok());
1091 }
1092
1093 #[test]
1094 fn test_feature_branch_is_main_master_or_develop_should_err() {
1095 let test_cases = [
1096 ("main", "upstream-branch"),
1097 ("master", "upstream-branch"),
1098 ("develop", "upstream-branch"),
1099 ];
1100
1101 for (current_branch, upstream_branch) in test_cases {
1102 let result = in_feature_branch(current_branch, upstream_branch);
1103 assert!(result.is_err());
1104 }
1105 }
1106
1107 fn get_cmds_mock(cmd: Arc<CmdMock>) -> Vec<Cmd<CmdInfo>> {
1108 let cmd_status = cmd.clone();
1109 let git_status_cmd =
1110 move || -> Result<CmdInfo> { Ok(CmdInfo::StatusModified(cmd_status.status_modified)) };
1111 let title_cmd = cmd.clone();
1112 let git_title_cmd = move || -> Result<CmdInfo> {
1113 Ok(CmdInfo::CommitSummary(
1114 title_cmd.last_commit_summary.clone(),
1115 ))
1116 };
1117 let message_cmd = cmd.clone();
1118 let git_message_cmd = move || -> Result<CmdInfo> {
1119 Ok(CmdInfo::CommitMessage(
1120 message_cmd.last_commit_message.clone(),
1121 ))
1122 };
1123 let branch_cmd = cmd.clone();
1124 let git_current_branch =
1125 move || -> Result<CmdInfo> { Ok(CmdInfo::Branch(branch_cmd.current_branch.clone())) };
1126 let project_cmd = cmd.clone();
1127 let remote_project_cmd =
1128 move || -> Result<CmdInfo> { Ok(CmdInfo::Project(project_cmd.project.clone())) };
1129 let members_cmd = cmd.clone();
1130 let remote_members_cmd =
1131 move || -> Result<CmdInfo> { Ok(CmdInfo::Members(members_cmd.members.clone())) };
1132 let mut cmds: Vec<Cmd<CmdInfo>> = vec![
1133 Box::new(remote_project_cmd),
1134 Box::new(remote_members_cmd),
1135 Box::new(git_status_cmd),
1136 Box::new(git_title_cmd),
1137 Box::new(git_message_cmd),
1138 Box::new(git_current_branch),
1139 ];
1140 if cmd.error {
1141 let error_cmd =
1142 move || -> Result<CmdInfo> { Err(error::gen("Failure retrieving data")) };
1143 cmds.push(Box::new(error_cmd));
1144 }
1145 cmds
1146 }
1147
1148 #[derive(Clone, Builder)]
1149 struct CmdMock {
1150 #[builder(default = "false")]
1151 status_modified: bool,
1152 last_commit_summary: String,
1153 current_branch: String,
1154 last_commit_message: String,
1155 members: Vec<Member>,
1156 project: Project,
1157 #[builder(default = "false")]
1158 error: bool,
1159 }
1160
1161 #[test]
1162 fn test_get_repo_project_info() {
1163 let cmd_mock = CmdMockBuilder::default()
1164 .status_modified(true)
1165 .current_branch("current-branch".to_string())
1166 .last_commit_summary("title".to_string())
1167 .last_commit_message("last-commit-message".to_string())
1168 .members(Vec::new())
1169 .project(Project::default())
1170 .build()
1171 .unwrap();
1172 let cmds = get_cmds_mock(Arc::new(cmd_mock));
1173 let result = get_repo_project_info(cmds);
1174 assert!(result.is_ok());
1175 let result = result.unwrap();
1176 assert_eq!(result.repo.title(), "title");
1177 assert_eq!(result.repo.current_branch(), "current-branch");
1178 assert_eq!(result.repo.last_commit_message(), "last-commit-message");
1179 }
1180
1181 #[test]
1182 fn test_get_repo_project_info_cmds_error() {
1183 let cmd_mock = CmdMockBuilder::default()
1184 .status_modified(true)
1185 .current_branch("current-branch".to_string())
1186 .last_commit_summary("title".to_string())
1187 .last_commit_message("last-commit-message".to_string())
1188 .members(Vec::new())
1189 .project(Project::default())
1190 .error(true)
1191 .build()
1192 .unwrap();
1193 let cmds = get_cmds_mock(Arc::new(cmd_mock));
1194 let result = get_repo_project_info(cmds);
1195 assert!(result.is_err());
1196 }
1197
1198 #[test]
1199 fn test_get_description_signature() {
1200 let description_signature_table = [
1201 ("", "", ""),
1202 ("", "signature", "signature"),
1203 ("description", "", "description"),
1204 ("description", "signature", "description\n\nsignature"),
1205 ];
1206 for (description, signature, expected) in description_signature_table {
1207 let result = build_description(description, signature);
1208 assert_eq!(result, expected);
1209 }
1210 }
1211
1212 #[test]
1213 fn test_list_merge_requests() {
1214 let remote = Arc::new(
1215 MergeRequestRemoteMock::builder()
1216 .merge_requests(vec![MergeRequestResponse::builder()
1217 .id(1)
1218 .title("New feature".to_string())
1219 .web_url("https://gitlab.com/owner/repo/-/merge_requests/1".to_string())
1220 .author("author".to_string())
1221 .updated_at("2021-01-01".to_string())
1222 .build()
1223 .unwrap()])
1224 .build()
1225 .unwrap(),
1226 );
1227 let mut buf = Vec::new();
1228 let body_args = MergeRequestListBodyArgs::builder()
1229 .list_args(None)
1230 .state(MergeRequestState::Opened)
1231 .assignee(None)
1232 .build()
1233 .unwrap();
1234 let cli_args = MergeRequestListCliArgs::new(
1235 MergeRequestState::Opened,
1236 ListRemoteCliArgs::builder().build().unwrap(),
1237 );
1238 list(remote, body_args, cli_args, &mut buf).unwrap();
1239 assert_eq!(
1240 "ID|Title|Source Branch|Author|URL|Updated at\n\
1241 1|New feature||author|https://gitlab.com/owner/repo/-/merge_requests/1|2021-01-01\n",
1242 String::from_utf8(buf).unwrap(),
1243 )
1244 }
1245
1246 #[test]
1247 fn test_if_no_merge_requests_are_available_list_should_return_no_merge_requests_found() {
1248 let remote = Arc::new(MergeRequestRemoteMock::builder().build().unwrap());
1249 let mut buf = Vec::new();
1250 let body_args = MergeRequestListBodyArgs::builder()
1251 .list_args(None)
1252 .state(MergeRequestState::Opened)
1253 .assignee(None)
1254 .build()
1255 .unwrap();
1256 let cli_args = MergeRequestListCliArgs::new(
1257 MergeRequestState::Opened,
1258 ListRemoteCliArgs::builder().build().unwrap(),
1259 );
1260 list(remote, body_args, cli_args, &mut buf).unwrap();
1261 assert_eq!("No resources found.\n", String::from_utf8(buf).unwrap(),)
1262 }
1263
1264 #[test]
1265 fn test_list_merge_requests_empty_with_flush_option_no_warn_message() {
1266 let remote = Arc::new(MergeRequestRemoteMock::builder().build().unwrap());
1267 let mut buf = Vec::new();
1268 let body_args = MergeRequestListBodyArgs::builder()
1269 .list_args(None)
1270 .state(MergeRequestState::Opened)
1271 .assignee(None)
1272 .build()
1273 .unwrap();
1274 let cli_args = MergeRequestListCliArgs::new(
1275 MergeRequestState::Opened,
1276 ListRemoteCliArgs::builder().flush(true).build().unwrap(),
1277 );
1278 list(remote, body_args, cli_args, &mut buf).unwrap();
1279 assert_eq!("", String::from_utf8(buf).unwrap());
1280 }
1281
1282 #[test]
1283 fn test_list_merge_requests_no_headers() {
1284 let remote = Arc::new(
1285 MergeRequestRemoteMock::builder()
1286 .merge_requests(vec![MergeRequestResponse::builder()
1287 .id(1)
1288 .title("New feature".to_string())
1289 .web_url("https://gitlab.com/owner/repo/-/merge_requests/1".to_string())
1290 .author("author".to_string())
1291 .updated_at("2021-01-01".to_string())
1292 .build()
1293 .unwrap()])
1294 .build()
1295 .unwrap(),
1296 );
1297 let mut buf = Vec::new();
1298 let body_args = MergeRequestListBodyArgs::builder()
1299 .list_args(None)
1300 .state(MergeRequestState::Opened)
1301 .assignee(None)
1302 .build()
1303 .unwrap();
1304 let cli_args = MergeRequestListCliArgs::new(
1305 MergeRequestState::Opened,
1306 ListRemoteCliArgs::builder()
1307 .get_args(
1308 GetRemoteCliArgs::builder()
1309 .no_headers(true)
1310 .build()
1311 .unwrap(),
1312 )
1313 .build()
1314 .unwrap(),
1315 );
1316 list(remote, body_args, cli_args, &mut buf).unwrap();
1317 assert_eq!(
1318 "1|New feature||author|https://gitlab.com/owner/repo/-/merge_requests/1|2021-01-01\n",
1319 String::from_utf8(buf).unwrap(),
1320 )
1321 }
1322
1323 #[derive(Clone, Builder)]
1324 struct MergeRequestRemoteMock {
1325 #[builder(default = "Vec::new()")]
1326 merge_requests: Vec<MergeRequestResponse>,
1327 }
1328
1329 impl MergeRequestRemoteMock {
1330 pub fn builder() -> MergeRequestRemoteMockBuilder {
1331 MergeRequestRemoteMockBuilder::default()
1332 }
1333 }
1334
1335 impl MergeRequest for MergeRequestRemoteMock {
1336 fn open(&self, _args: MergeRequestBodyArgs) -> Result<MergeRequestResponse> {
1337 Ok(MergeRequestResponse::builder().build().unwrap())
1338 }
1339 fn list(&self, _args: MergeRequestListBodyArgs) -> Result<Vec<MergeRequestResponse>> {
1340 Ok(self.merge_requests.clone())
1341 }
1342 fn merge(&self, _id: i64) -> Result<MergeRequestResponse> {
1343 Ok(MergeRequestResponse::builder().build().unwrap())
1344 }
1345 fn get(&self, _id: i64) -> Result<MergeRequestResponse> {
1346 Ok(self.merge_requests[0].clone())
1347 }
1348 fn close(&self, _id: i64) -> Result<MergeRequestResponse> {
1349 Ok(MergeRequestResponse::builder().build().unwrap())
1350 }
1351 fn num_pages(&self, _args: MergeRequestListBodyArgs) -> Result<Option<u32>> {
1352 Ok(None)
1353 }
1354 fn approve(&self, _id: i64) -> Result<MergeRequestResponse> {
1355 Ok(self.merge_requests[0].clone())
1356 }
1357
1358 fn num_resources(
1359 &self,
1360 _args: MergeRequestListBodyArgs,
1361 ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
1362 todo!()
1363 }
1364 }
1365
1366 #[derive(Default)]
1367 struct MockRemoteProject {
1368 comment_called: Mutex<bool>,
1369 comment_argument: Mutex<String>,
1370 list_comments: Vec<Comment>,
1371 }
1372
1373 impl MockRemoteProject {
1374 fn new(comments: Vec<Comment>) -> MockRemoteProject {
1375 MockRemoteProject {
1376 comment_called: Mutex::new(false),
1377 comment_argument: Mutex::new("".to_string()),
1378 list_comments: comments,
1379 }
1380 }
1381 }
1382
1383 impl RemoteProject for MockRemoteProject {
1384 fn get_project_data(&self, _id: Option<i64>, _path: Option<&str>) -> Result<CmdInfo> {
1385 let project = Project::new(1, "main");
1386 Ok(CmdInfo::Project(project))
1387 }
1388
1389 fn get_project_members(&self) -> Result<CmdInfo> {
1390 let members = vec![
1391 Member::builder()
1392 .id(1)
1393 .username("user1".to_string())
1394 .name("User 1".to_string())
1395 .build()
1396 .unwrap(),
1397 Member::builder()
1398 .id(2)
1399 .username("user2".to_string())
1400 .name("User 2".to_string())
1401 .build()
1402 .unwrap(),
1403 ];
1404 Ok(CmdInfo::Members(members))
1405 }
1406
1407 fn get_url(&self, _option: BrowseOptions) -> String {
1408 todo!()
1409 }
1410
1411 fn list(&self, _args: ProjectListBodyArgs) -> Result<Vec<Project>> {
1412 todo!()
1413 }
1414
1415 fn num_pages(&self, _args: ProjectListBodyArgs) -> Result<Option<u32>> {
1416 todo!()
1417 }
1418
1419 fn num_resources(
1420 &self,
1421 _args: ProjectListBodyArgs,
1422 ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
1423 todo!()
1424 }
1425 }
1426
1427 impl CommentMergeRequest for MockRemoteProject {
1428 fn create(&self, args: CommentMergeRequestBodyArgs) -> Result<()> {
1429 let mut called = self.comment_called.lock().unwrap();
1430 *called = true;
1431 let mut argument = self.comment_argument.lock().unwrap();
1432 *argument = args.comment;
1433 Ok(())
1434 }
1435
1436 fn list(&self, _args: CommentMergeRequestListBodyArgs) -> Result<Vec<Comment>> {
1437 Ok(self.list_comments.clone())
1438 }
1439
1440 fn num_pages(&self, _args: CommentMergeRequestListBodyArgs) -> Result<Option<u32>> {
1441 todo!()
1442 }
1443
1444 fn num_resources(
1445 &self,
1446 _args: CommentMergeRequestListBodyArgs,
1447 ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
1448 todo!()
1449 }
1450 }
1451
1452 struct MockShellRunner {
1453 responses: Mutex<Vec<ShellResponse>>,
1454 }
1455
1456 impl MockShellRunner {
1457 pub fn new(response: Vec<ShellResponse>) -> MockShellRunner {
1458 MockShellRunner {
1459 responses: Mutex::new(response),
1460 }
1461 }
1462 }
1463
1464 impl TaskRunner for MockShellRunner {
1465 type Response = ShellResponse;
1466
1467 fn run<T>(&self, _cmd: T) -> Result<Self::Response>
1468 where
1469 T: IntoIterator,
1470 T::Item: AsRef<std::ffi::OsStr>,
1471 {
1472 let response = self.responses.lock().unwrap().pop().unwrap();
1473 Ok(ShellResponse::builder()
1474 .body(response.body)
1475 .build()
1476 .unwrap())
1477 }
1478 }
1479
1480 fn gen_cmd_responses() -> Vec<ShellResponse> {
1481 let responses = vec![
1482 ShellResponse::builder()
1483 .body("fetch cmd".to_string())
1484 .build()
1485 .unwrap(),
1486 ShellResponse::builder()
1487 .body("last commit message cmd".to_string())
1488 .build()
1489 .unwrap(),
1490 ShellResponse::builder()
1491 .body("title git cmd".to_string())
1492 .build()
1493 .unwrap(),
1494 ShellResponse::builder()
1495 .body("current branch cmd".to_string())
1496 .build()
1497 .unwrap(),
1498 ShellResponse::builder()
1499 .body("status cmd".to_string())
1500 .build()
1501 .unwrap(),
1502 ];
1503 responses
1504 }
1505
1506 #[test]
1507 fn test_cmds_gather_title_from_cli_arg() {
1508 let remote = Arc::new(MockRemoteProject::default());
1509 let cli_args = MergeRequestCliArgs::builder()
1510 .title(Some("title cli".to_string()))
1511 .body_from_commit(None)
1512 .description(None)
1513 .description_from_file(None)
1514 .target_branch(Some("target-branch".to_string()))
1515 .auto(false)
1516 .cache_args(CacheCliArgs::default())
1517 .open_browser(false)
1518 .accept_summary(false)
1519 .commit(Some("commit".to_string()))
1520 .draft(false)
1521 .force(false)
1522 .amend(false)
1523 .dry_run(false)
1524 .build()
1525 .unwrap();
1526
1527 let responses = gen_cmd_responses();
1528
1529 let task_runner = Arc::new(MockShellRunner::new(responses));
1530 let cmds = cmds(remote, &cli_args, task_runner, None::<Cursor<&str>>);
1531 assert_eq!(cmds.len(), 5);
1532 let cmds = cmds
1533 .into_iter()
1534 .map(|cmd| cmd())
1535 .collect::<Result<Vec<CmdInfo>>>()
1536 .unwrap();
1537 let title_result = cmds[3].clone();
1538 let title = match title_result {
1539 CmdInfo::CommitSummary(title) => title,
1540 _ => "".to_string(),
1541 };
1542 assert_eq!("title cli", title);
1543 }
1544
1545 #[test]
1546 fn test_cmds_gather_title_from_git_commit_summary() {
1547 let remote = Arc::new(MockRemoteProject::default());
1548 let cli_args = MergeRequestCliArgs::builder()
1549 .title(None)
1550 .body_from_commit(None)
1551 .description(None)
1552 .description_from_file(None)
1553 .target_branch(Some("target-branch".to_string()))
1554 .auto(false)
1555 .cache_args(CacheCliArgs::default())
1556 .open_browser(false)
1557 .accept_summary(false)
1558 .commit(None)
1559 .draft(false)
1560 .force(false)
1561 .amend(false)
1562 .dry_run(false)
1563 .build()
1564 .unwrap();
1565
1566 let responses = gen_cmd_responses();
1567 let task_runner = Arc::new(MockShellRunner::new(responses));
1568 let cmds = cmds(remote, &cli_args, task_runner, None::<Cursor<&str>>);
1569 let results = cmds
1570 .into_iter()
1571 .map(|cmd| cmd())
1572 .collect::<Result<Vec<CmdInfo>>>()
1573 .unwrap();
1574 let title_result = results[3].clone();
1575 let title = match title_result {
1576 CmdInfo::CommitSummary(title) => title,
1577 _ => "".to_string(),
1578 };
1579 assert_eq!("title git cmd", title);
1580 }
1581
1582 #[test]
1583 fn test_read_description_from_file() {
1584 let remote = Arc::new(MockRemoteProject::default());
1585 let cli_args = MergeRequestCliArgs::builder()
1586 .title(None)
1587 .body_from_commit(None)
1588 .description(None)
1589 .description_from_file(Some("description_file.txt".to_string()))
1590 .target_branch(Some("target-branch".to_string()))
1591 .auto(false)
1592 .cache_args(CacheCliArgs::default())
1593 .open_browser(false)
1594 .accept_summary(false)
1595 .commit(None)
1596 .draft(false)
1597 .force(false)
1598 .amend(false)
1599 .dry_run(false)
1600 .build()
1601 .unwrap();
1602
1603 let responses = gen_cmd_responses();
1604
1605 let task_runner = Arc::new(MockShellRunner::new(responses));
1606
1607 let description_contents = "This merge requests adds a new feature\n";
1608 let reader = Cursor::new(description_contents);
1609 let cmds = cmds(remote, &cli_args, task_runner, Some(reader));
1610 let results = cmds
1611 .into_iter()
1612 .map(|cmd| cmd())
1613 .collect::<Result<Vec<CmdInfo>>>()
1614 .unwrap();
1615 let description_result = results[4].clone();
1616 let description = match description_result {
1617 CmdInfo::CommitMessage(description) => description,
1618 _ => "".to_string(),
1619 };
1620 assert_eq!(description_contents, description);
1621 }
1622
1623 #[test]
1624 fn test_create_comment_on_a_merge_request_with_cli_comment_ok() {
1625 let remote = Arc::new(MockRemoteProject::default());
1626 let cli_args = CommentMergeRequestCliArgs::builder()
1627 .id(1)
1628 .comment(Some("All features complete, ship it".to_string()))
1629 .comment_from_file(None)
1630 .build()
1631 .unwrap();
1632 let reader = Cursor::new("comment");
1633 assert!(create_comment(remote.clone(), cli_args, Some(reader)).is_ok());
1634 assert!(remote.comment_called.lock().unwrap().clone());
1635 assert_eq!(
1636 "All features complete, ship it",
1637 remote.comment_argument.lock().unwrap().clone(),
1638 );
1639 }
1640
1641 #[test]
1642 fn test_create_comment_on_a_merge_request_with_comment_from_file_ok() {
1643 let remote = Arc::new(MockRemoteProject::default());
1644 let cli_args = CommentMergeRequestCliArgs::builder()
1645 .id(1)
1646 .comment(None)
1647 .comment_from_file(Some("comment_file.txt".to_string()))
1648 .build()
1649 .unwrap();
1650 let reader = Cursor::new("Just a long, long comment from a file");
1651 assert!(create_comment(remote.clone(), cli_args, Some(reader)).is_ok());
1652 assert!(remote.comment_called.lock().unwrap().clone());
1653 assert_eq!(
1654 "Just a long, long comment from a file",
1655 remote.comment_argument.lock().unwrap().clone(),
1656 );
1657 }
1658
1659 struct ErrorReader {}
1660
1661 impl Read for ErrorReader {
1662 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
1663 Err(std::io::Error::new(
1664 std::io::ErrorKind::Other,
1665 "Error reading from reader",
1666 ))
1667 }
1668 }
1669
1670 impl BufRead for ErrorReader {
1671 fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
1672 Err(std::io::Error::new(
1673 std::io::ErrorKind::Other,
1674 "Error reading from reader",
1675 ))
1676 }
1677 fn consume(&mut self, _amt: usize) {}
1678 }
1679
1680 #[test]
1681 fn test_create_comment_on_a_merge_request_fail_to_read_comment_from_file() {
1682 let remote = Arc::new(MockRemoteProject::default());
1683 let cli_args = CommentMergeRequestCliArgs::builder()
1684 .id(1)
1685 .comment(None)
1686 .comment_from_file(Some("comment_file.txt".to_string()))
1687 .build()
1688 .unwrap();
1689 let reader = ErrorReader {};
1690 assert!(create_comment(remote.clone(), cli_args, Some(reader)).is_err());
1691 }
1692
1693 #[test]
1694 fn test_get_merge_request_details() {
1695 let cli_args = MergeRequestGetCliArgs::builder()
1696 .id(1)
1697 .get_args(
1698 GetRemoteCliArgs::builder()
1699 .display_optional(true)
1700 .build()
1701 .unwrap(),
1702 )
1703 .build()
1704 .unwrap();
1705 let response = MergeRequestResponse::builder()
1706 .id(1)
1707 .title("New feature".to_string())
1708 .web_url("https://gitlab.com/owner/repo/-/merge_requests/1".to_string())
1709 .description("Implement get merge request".to_string())
1710 .merged_at("2024-03-03T00:00:00Z".to_string())
1711 .pipeline_id(Some(1))
1712 .pipeline_url(Some(
1713 "https://gitlab.com/owner/repo/-/pipelines/1".to_string(),
1714 ))
1715 .build()
1716 .unwrap();
1717 let remote = Arc::new(
1718 MergeRequestRemoteMock::builder()
1719 .merge_requests(vec![response])
1720 .build()
1721 .unwrap(),
1722 );
1723 let mut writer = Vec::new();
1724 get_merge_request_details(remote, cli_args, &mut writer).unwrap();
1725 assert_eq!(
1726 "ID|Title|Source Branch|SHA|Description|Author|URL|Updated at|Merged at|Pipeline ID|Pipeline URL\n\
1727 1|New feature|||Implement get merge request||https://gitlab.com/owner/repo/-/merge_requests/1||2024-03-03T00:00:00Z|1|https://gitlab.com/owner/repo/-/pipelines/1\n",
1728 String::from_utf8(writer).unwrap(),
1729 )
1730 }
1731
1732 #[test]
1733 fn test_approve_merge_request_ok() {
1734 let approve_response = MergeRequestResponse::builder()
1735 .id(1)
1736 .web_url("https://gitlab.com/owner/repo/-/merge_requests/1".to_string())
1737 .build()
1738 .unwrap();
1739 let remote = Arc::new(
1740 MergeRequestRemoteMock::builder()
1741 .merge_requests(vec![approve_response])
1742 .build()
1743 .unwrap(),
1744 );
1745 let mut writer = Vec::new();
1746 approve(remote, 1, &mut writer).unwrap();
1747 assert_eq!(
1748 "Merge request approved: https://gitlab.com/owner/repo/-/merge_requests/1\n",
1749 String::from_utf8(writer).unwrap(),
1750 );
1751 }
1752
1753 #[test]
1754 fn test_cmds_fetch_cli_arg() {
1755 let remote = Arc::new(MockRemoteProject::default());
1756 let cli_args = MergeRequestCliArgs::builder()
1757 .title(Some("title cli".to_string()))
1758 .body_from_commit(None)
1759 .description(None)
1760 .description_from_file(None)
1761 .target_branch(Some("target-branch".to_string()))
1762 .fetch(Some("origin".to_string()))
1763 .auto(false)
1764 .cache_args(CacheCliArgs::default())
1765 .open_browser(false)
1766 .accept_summary(false)
1767 .commit(Some("commit".to_string()))
1768 .draft(false)
1769 .force(false)
1770 .amend(false)
1771 .dry_run(false)
1772 .build()
1773 .unwrap();
1774
1775 let responses = gen_cmd_responses();
1776
1777 let task_runner = Arc::new(MockShellRunner::new(responses));
1778 let cmds = cmds(remote, &cli_args, task_runner, None::<Cursor<&str>>);
1779 assert_eq!(cmds.len(), 6);
1780 let cmds = cmds
1781 .into_iter()
1782 .map(|cmd| cmd())
1783 .collect::<Result<Vec<CmdInfo>>>()
1784 .unwrap();
1785 let fetch_result = cmds[5].clone();
1786 match fetch_result {
1787 CmdInfo::Ignore => {}
1788 _ => panic!("Expected ignore cmdinfo variant on fetch"),
1789 };
1790 }
1791
1792 #[test]
1793 fn test_list_merge_request_comments() {
1794 let comments = vec![
1795 Comment::builder()
1796 .id(1)
1797 .body("Great work!".to_string())
1798 .author("user1".to_string())
1799 .created_at("2021-01-01".to_string())
1800 .build()
1801 .unwrap(),
1802 Comment::builder()
1803 .id(2)
1804 .body("Keep it up!".to_string())
1805 .author("user2".to_string())
1806 .created_at("2021-01-02".to_string())
1807 .build()
1808 .unwrap(),
1809 ];
1810 let remote = Arc::new(MockRemoteProject::new(comments));
1811 let body_args = CommentMergeRequestListBodyArgs::builder()
1812 .id(1)
1813 .list_args(None)
1814 .build()
1815 .unwrap();
1816 let cli_args = CommentMergeRequestListCliArgs::builder()
1817 .id(1)
1818 .list_args(ListRemoteCliArgs::builder().build().unwrap())
1819 .build()
1820 .unwrap();
1821 let mut buf = Vec::new();
1822 list_comments(remote, body_args, cli_args, &mut buf).unwrap();
1823 assert_eq!(
1824 "ID|Body|Author|Created at\n\
1825 1|Great work!|user1|2021-01-01\n\
1826 2|Keep it up!|user2|2021-01-02\n",
1827 String::from_utf8(buf).unwrap(),
1828 );
1829 }
1830
1831 #[test]
1832 fn test_gather_member_from_members_list() {
1833 let members = vec![
1834 Member::builder()
1835 .id(1)
1836 .username("user1".to_string())
1837 .name("User 1".to_string())
1838 .build()
1839 .unwrap(),
1840 Member::builder()
1841 .id(2)
1842 .username("user2".to_string())
1843 .name("User 2".to_string())
1844 .build()
1845 .unwrap(),
1846 ];
1847 let assignee_username = "user2";
1848 let member = get_member(&members, assignee_username).unwrap();
1849 assert_eq!(member.username, assignee_username);
1850 assert_eq!(member.id, 2);
1851 }
1852}