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