1use std::collections::BTreeSet;
8use std::iter;
9
10use derive_builder::Builder;
11
12use crate::api::common::{CommaSeparatedList, NameOrId};
13use crate::api::endpoint_prelude::*;
14
15#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub(crate) enum Assignee {
18 Unassigned,
19 Id(u64),
20 Ids(BTreeSet<u64>),
21}
22
23impl Assignee {
24 pub(crate) fn add_params<'a>(&'a self, params: &mut FormParams<'a>) {
25 match self {
26 Assignee::Unassigned => {
27 params.push("assignee_ids", "0");
28 },
29 Assignee::Id(id) => {
30 params.push("assignee_id", *id);
31 },
32 Assignee::Ids(ids) => {
33 params.extend(ids.iter().map(|&id| ("assignee_ids[]", id)));
34 },
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
41#[non_exhaustive]
42pub(crate) enum Reviewer {
43 Unassigned,
45 Ids(BTreeSet<u64>),
47}
48
49impl Reviewer {
50 pub(crate) fn add_params<'a>(&'a self, params: &mut FormParams<'a>) {
51 match self {
52 Reviewer::Unassigned => {
53 params.push("reviewer_ids", "0");
54 },
55 Reviewer::Ids(ids) => {
56 params.extend(ids.iter().map(|&id| ("reviewer_ids[]", id)));
57 },
58 }
59 }
60}
61
62#[derive(Debug, Builder, Clone)]
64#[builder(setter(strip_option))]
65pub struct CreateMergeRequest<'a> {
66 #[builder(setter(into))]
68 project: NameOrId<'a>,
69 #[builder(setter(into))]
71 source_branch: Cow<'a, str>,
72 #[builder(setter(into))]
74 target_branch: Cow<'a, str>,
75 #[builder(setter(into))]
77 title: Cow<'a, str>,
78
79 #[builder(setter(name = "_assignee"), default, private)]
81 assignee: Option<Assignee>,
82 #[builder(setter(name = "_reviewer"), default, private)]
83 reviewer: Option<Reviewer>,
84 #[builder(setter(into), default)]
86 description: Option<Cow<'a, str>>,
87 #[builder(default)]
89 target_project_id: Option<u64>,
90 #[builder(setter(name = "_labels"), default, private)]
92 labels: Option<CommaSeparatedList<Cow<'a, str>>>,
93 #[builder(default)]
95 milestone_id: Option<u64>,
96 #[builder(default)]
98 remove_source_branch: Option<bool>,
99 #[builder(default)]
101 allow_collaboration: Option<bool>,
102 #[builder(default)]
104 squash: Option<bool>,
105}
106
107impl<'a> CreateMergeRequest<'a> {
108 pub fn builder() -> CreateMergeRequestBuilder<'a> {
110 CreateMergeRequestBuilder::default()
111 }
112}
113
114impl<'a> CreateMergeRequestBuilder<'a> {
115 pub fn unassigned(&mut self) -> &mut Self {
117 self.assignee = Some(Some(Assignee::Unassigned));
118 self
119 }
120
121 pub fn assignee(&mut self, assignee: u64) -> &mut Self {
123 let assignee = match self.assignee.take() {
124 Some(Some(Assignee::Ids(mut set))) => {
125 set.insert(assignee);
126 Assignee::Ids(set)
127 },
128 Some(Some(Assignee::Id(old_id))) => {
129 let set = [old_id, assignee].iter().copied().collect();
130 Assignee::Ids(set)
131 },
132 _ => Assignee::Id(assignee),
133 };
134 self.assignee = Some(Some(assignee));
135 self
136 }
137
138 pub fn assignees<I>(&mut self, iter: I) -> &mut Self
140 where
141 I: Iterator<Item = u64>,
142 {
143 let assignee = match self.assignee.take() {
144 Some(Some(Assignee::Ids(mut set))) => {
145 set.extend(iter);
146 Assignee::Ids(set)
147 },
148 Some(Some(Assignee::Id(old_id))) => {
149 let set = iter.chain(iter::once(old_id)).collect();
150 Assignee::Ids(set)
151 },
152 _ => Assignee::Ids(iter.collect()),
153 };
154 self.assignee = Some(Some(assignee));
155 self
156 }
157
158 pub fn without_reviewer(&mut self) -> &mut Self {
160 self.reviewer = Some(Some(Reviewer::Unassigned));
161 self
162 }
163
164 pub fn reviewer(&mut self, reviewer: u64) -> &mut Self {
166 let reviewer = match self.reviewer.take() {
167 Some(Some(Reviewer::Ids(mut set))) => {
168 set.insert(reviewer);
169 Reviewer::Ids(set)
170 },
171 _ => Reviewer::Ids(iter::once(reviewer).collect()),
172 };
173 self.reviewer = Some(Some(reviewer));
174 self
175 }
176
177 pub fn reviewers<I>(&mut self, iter: I) -> &mut Self
179 where
180 I: Iterator<Item = u64>,
181 {
182 let reviewer = match self.reviewer.take() {
183 Some(Some(Reviewer::Ids(mut set))) => {
184 set.extend(iter);
185 Reviewer::Ids(set)
186 },
187 _ => Reviewer::Ids(iter.collect()),
188 };
189 self.reviewer = Some(Some(reviewer));
190 self
191 }
192
193 pub fn label<L>(&mut self, label: L) -> &mut Self
195 where
196 L: Into<Cow<'a, str>>,
197 {
198 self.labels
199 .get_or_insert(None)
200 .get_or_insert_with(CommaSeparatedList::new)
201 .push(label.into());
202 self
203 }
204
205 pub fn labels<I, L>(&mut self, iter: I) -> &mut Self
207 where
208 I: Iterator<Item = L>,
209 L: Into<Cow<'a, str>>,
210 {
211 self.labels
212 .get_or_insert(None)
213 .get_or_insert_with(CommaSeparatedList::new)
214 .extend(iter.map(Into::into));
215 self
216 }
217}
218
219impl Endpoint for CreateMergeRequest<'_> {
220 fn method(&self) -> Method {
221 Method::POST
222 }
223
224 fn endpoint(&self) -> Cow<'static, str> {
225 format!("projects/{}/merge_requests", self.project).into()
226 }
227
228 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
229 let mut params = FormParams::default();
230
231 params
232 .push("source_branch", self.source_branch.as_ref())
233 .push("target_branch", self.target_branch.as_ref())
234 .push("title", self.title.as_ref())
235 .push_opt("description", self.description.as_ref())
236 .push_opt("target_project_id", self.target_project_id)
237 .push_opt("milestone_id", self.milestone_id)
238 .push_opt("labels", self.labels.as_ref())
239 .push_opt("remove_source_branch", self.remove_source_branch)
240 .push_opt("allow_collaboration", self.allow_collaboration)
241 .push_opt("squash", self.squash);
242
243 if let Some(assignee) = self.assignee.as_ref() {
244 assignee.add_params(&mut params);
245 }
246 if let Some(reviewer) = self.reviewer.as_ref() {
247 reviewer.add_params(&mut params);
248 }
249
250 params.into_body()
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use http::Method;
257
258 use crate::api::projects::merge_requests::{
259 CreateMergeRequest, CreateMergeRequestBuilderError,
260 };
261 use crate::api::{self, Query};
262 use crate::test::client::{ExpectedUrl, SingleTestClient};
263
264 #[test]
265 fn project_source_branch_target_branch_and_title_are_necessary() {
266 let err = CreateMergeRequest::builder().build().unwrap_err();
267 crate::test::assert_missing_field!(err, CreateMergeRequestBuilderError, "project");
268 }
269
270 #[test]
271 fn project_is_necessary() {
272 let err = CreateMergeRequest::builder()
273 .source_branch("source")
274 .target_branch("target")
275 .title("title")
276 .build()
277 .unwrap_err();
278 crate::test::assert_missing_field!(err, CreateMergeRequestBuilderError, "project");
279 }
280
281 #[test]
282 fn source_branch_is_necessary() {
283 let err = CreateMergeRequest::builder()
284 .project(1)
285 .target_branch("target")
286 .title("title")
287 .build()
288 .unwrap_err();
289 crate::test::assert_missing_field!(err, CreateMergeRequestBuilderError, "source_branch");
290 }
291
292 #[test]
293 fn target_branch_is_necessary() {
294 let err = CreateMergeRequest::builder()
295 .project(1)
296 .source_branch("source")
297 .title("title")
298 .build()
299 .unwrap_err();
300 crate::test::assert_missing_field!(err, CreateMergeRequestBuilderError, "target_branch");
301 }
302
303 #[test]
304 fn title_is_necessary() {
305 let err = CreateMergeRequest::builder()
306 .project(1)
307 .source_branch("source")
308 .target_branch("target")
309 .build()
310 .unwrap_err();
311 crate::test::assert_missing_field!(err, CreateMergeRequestBuilderError, "title");
312 }
313
314 #[test]
315 fn project_source_branch_target_branch_and_title_are_sufficient() {
316 CreateMergeRequest::builder()
317 .project(1)
318 .source_branch("source")
319 .target_branch("target")
320 .title("title")
321 .build()
322 .unwrap();
323 }
324
325 #[test]
326 fn endpoint() {
327 let endpoint = ExpectedUrl::builder()
328 .method(Method::POST)
329 .endpoint("projects/simple%2Fproject/merge_requests")
330 .content_type("application/x-www-form-urlencoded")
331 .body_str(concat!(
332 "source_branch=source%2Fbranch",
333 "&target_branch=target%2Fbranch",
334 "&title=title",
335 ))
336 .build()
337 .unwrap();
338 let client = SingleTestClient::new_raw(endpoint, "");
339
340 let endpoint = CreateMergeRequest::builder()
341 .project("simple/project")
342 .source_branch("source/branch")
343 .target_branch("target/branch")
344 .title("title")
345 .build()
346 .unwrap();
347 api::ignore(endpoint).query(&client).unwrap();
348 }
349
350 #[test]
351 fn endpoint_unassigned() {
352 let endpoint = ExpectedUrl::builder()
353 .method(Method::POST)
354 .endpoint("projects/simple%2Fproject/merge_requests")
355 .content_type("application/x-www-form-urlencoded")
356 .body_str(concat!(
357 "source_branch=source%2Fbranch",
358 "&target_branch=target%2Fbranch",
359 "&title=title",
360 "&assignee_ids=0",
361 ))
362 .build()
363 .unwrap();
364 let client = SingleTestClient::new_raw(endpoint, "");
365
366 let endpoint = CreateMergeRequest::builder()
367 .project("simple/project")
368 .source_branch("source/branch")
369 .target_branch("target/branch")
370 .title("title")
371 .unassigned()
372 .build()
373 .unwrap();
374 api::ignore(endpoint).query(&client).unwrap();
375 }
376
377 #[test]
378 fn endpoint_assignee() {
379 let endpoint = ExpectedUrl::builder()
380 .method(Method::POST)
381 .endpoint("projects/simple%2Fproject/merge_requests")
382 .content_type("application/x-www-form-urlencoded")
383 .body_str(concat!(
384 "source_branch=source%2Fbranch",
385 "&target_branch=target%2Fbranch",
386 "&title=title",
387 "&assignee_id=1",
388 ))
389 .build()
390 .unwrap();
391 let client = SingleTestClient::new_raw(endpoint, "");
392
393 let endpoint = CreateMergeRequest::builder()
394 .project("simple/project")
395 .source_branch("source/branch")
396 .target_branch("target/branch")
397 .title("title")
398 .assignee(1)
399 .build()
400 .unwrap();
401 api::ignore(endpoint).query(&client).unwrap();
402 }
403
404 #[test]
405 fn endpoint_assignees() {
406 let endpoint = ExpectedUrl::builder()
407 .method(Method::POST)
408 .endpoint("projects/simple%2Fproject/merge_requests")
409 .content_type("application/x-www-form-urlencoded")
410 .body_str(concat!(
411 "source_branch=source%2Fbranch",
412 "&target_branch=target%2Fbranch",
413 "&title=title",
414 "&assignee_ids%5B%5D=1",
415 "&assignee_ids%5B%5D=2",
416 ))
417 .build()
418 .unwrap();
419 let client = SingleTestClient::new_raw(endpoint, "");
420
421 let endpoint = CreateMergeRequest::builder()
422 .project("simple/project")
423 .source_branch("source/branch")
424 .target_branch("target/branch")
425 .title("title")
426 .assignee(1)
427 .assignees([1, 2].iter().copied())
428 .build()
429 .unwrap();
430 api::ignore(endpoint).query(&client).unwrap();
431 }
432
433 #[test]
434 fn endpoint_unreviewed() {
435 let endpoint = ExpectedUrl::builder()
436 .method(Method::POST)
437 .endpoint("projects/simple%2Fproject/merge_requests")
438 .content_type("application/x-www-form-urlencoded")
439 .body_str(concat!(
440 "source_branch=source%2Fbranch",
441 "&target_branch=target%2Fbranch",
442 "&title=title",
443 "&reviewer_ids=0",
444 ))
445 .build()
446 .unwrap();
447 let client = SingleTestClient::new_raw(endpoint, "");
448
449 let endpoint = CreateMergeRequest::builder()
450 .project("simple/project")
451 .source_branch("source/branch")
452 .target_branch("target/branch")
453 .title("title")
454 .without_reviewer()
455 .build()
456 .unwrap();
457 api::ignore(endpoint).query(&client).unwrap();
458 }
459
460 #[test]
461 fn endpoint_reviewer() {
462 let endpoint = ExpectedUrl::builder()
463 .method(Method::POST)
464 .endpoint("projects/simple%2Fproject/merge_requests")
465 .content_type("application/x-www-form-urlencoded")
466 .body_str(concat!(
467 "source_branch=source%2Fbranch",
468 "&target_branch=target%2Fbranch",
469 "&title=title",
470 "&reviewer_ids%5B%5D=1",
471 ))
472 .build()
473 .unwrap();
474 let client = SingleTestClient::new_raw(endpoint, "");
475
476 let endpoint = CreateMergeRequest::builder()
477 .project("simple/project")
478 .source_branch("source/branch")
479 .target_branch("target/branch")
480 .title("title")
481 .reviewer(1)
482 .build()
483 .unwrap();
484 api::ignore(endpoint).query(&client).unwrap();
485 }
486
487 #[test]
488 fn endpoint_reviewers() {
489 let endpoint = ExpectedUrl::builder()
490 .method(Method::POST)
491 .endpoint("projects/simple%2Fproject/merge_requests")
492 .content_type("application/x-www-form-urlencoded")
493 .body_str(concat!(
494 "source_branch=source%2Fbranch",
495 "&target_branch=target%2Fbranch",
496 "&title=title",
497 "&reviewer_ids%5B%5D=1",
498 "&reviewer_ids%5B%5D=2",
499 ))
500 .build()
501 .unwrap();
502 let client = SingleTestClient::new_raw(endpoint, "");
503
504 let endpoint = CreateMergeRequest::builder()
505 .project("simple/project")
506 .source_branch("source/branch")
507 .target_branch("target/branch")
508 .title("title")
509 .reviewer(1)
510 .reviewers([1, 2].iter().copied())
511 .build()
512 .unwrap();
513 api::ignore(endpoint).query(&client).unwrap();
514 }
515
516 #[test]
517 fn endpoint_description() {
518 let endpoint = ExpectedUrl::builder()
519 .method(Method::POST)
520 .endpoint("projects/simple%2Fproject/merge_requests")
521 .content_type("application/x-www-form-urlencoded")
522 .body_str(concat!(
523 "source_branch=source%2Fbranch",
524 "&target_branch=target%2Fbranch",
525 "&title=title",
526 "&description=description",
527 ))
528 .build()
529 .unwrap();
530 let client = SingleTestClient::new_raw(endpoint, "");
531
532 let endpoint = CreateMergeRequest::builder()
533 .project("simple/project")
534 .source_branch("source/branch")
535 .target_branch("target/branch")
536 .title("title")
537 .description("description")
538 .build()
539 .unwrap();
540 api::ignore(endpoint).query(&client).unwrap();
541 }
542
543 #[test]
544 fn endpoint_target_project_id() {
545 let endpoint = ExpectedUrl::builder()
546 .method(Method::POST)
547 .endpoint("projects/simple%2Fproject/merge_requests")
548 .content_type("application/x-www-form-urlencoded")
549 .body_str(concat!(
550 "source_branch=source%2Fbranch",
551 "&target_branch=target%2Fbranch",
552 "&title=title",
553 "&target_project_id=1",
554 ))
555 .build()
556 .unwrap();
557 let client = SingleTestClient::new_raw(endpoint, "");
558
559 let endpoint = CreateMergeRequest::builder()
560 .project("simple/project")
561 .source_branch("source/branch")
562 .target_branch("target/branch")
563 .title("title")
564 .target_project_id(1)
565 .build()
566 .unwrap();
567 api::ignore(endpoint).query(&client).unwrap();
568 }
569
570 #[test]
571 fn endpoint_labels() {
572 let endpoint = ExpectedUrl::builder()
573 .method(Method::POST)
574 .endpoint("projects/simple%2Fproject/merge_requests")
575 .content_type("application/x-www-form-urlencoded")
576 .body_str(concat!(
577 "source_branch=source%2Fbranch",
578 "&target_branch=target%2Fbranch",
579 "&title=title",
580 "&labels=label%2Clabel1%2Clabel2",
581 ))
582 .build()
583 .unwrap();
584 let client = SingleTestClient::new_raw(endpoint, "");
585
586 let endpoint = CreateMergeRequest::builder()
587 .project("simple/project")
588 .source_branch("source/branch")
589 .target_branch("target/branch")
590 .title("title")
591 .label("label")
592 .labels(["label1", "label2"].iter().cloned())
593 .build()
594 .unwrap();
595 api::ignore(endpoint).query(&client).unwrap();
596 }
597
598 #[test]
599 fn endpoint_milestone_id() {
600 let endpoint = ExpectedUrl::builder()
601 .method(Method::POST)
602 .endpoint("projects/simple%2Fproject/merge_requests")
603 .content_type("application/x-www-form-urlencoded")
604 .body_str(concat!(
605 "source_branch=source%2Fbranch",
606 "&target_branch=target%2Fbranch",
607 "&title=title",
608 "&milestone_id=1",
609 ))
610 .build()
611 .unwrap();
612 let client = SingleTestClient::new_raw(endpoint, "");
613
614 let endpoint = CreateMergeRequest::builder()
615 .project("simple/project")
616 .source_branch("source/branch")
617 .target_branch("target/branch")
618 .title("title")
619 .milestone_id(1)
620 .build()
621 .unwrap();
622 api::ignore(endpoint).query(&client).unwrap();
623 }
624
625 #[test]
626 fn endpoint_remove_source_branch() {
627 let endpoint = ExpectedUrl::builder()
628 .method(Method::POST)
629 .endpoint("projects/simple%2Fproject/merge_requests")
630 .content_type("application/x-www-form-urlencoded")
631 .body_str(concat!(
632 "source_branch=source%2Fbranch",
633 "&target_branch=target%2Fbranch",
634 "&title=title",
635 "&remove_source_branch=true",
636 ))
637 .build()
638 .unwrap();
639 let client = SingleTestClient::new_raw(endpoint, "");
640
641 let endpoint = CreateMergeRequest::builder()
642 .project("simple/project")
643 .source_branch("source/branch")
644 .target_branch("target/branch")
645 .title("title")
646 .remove_source_branch(true)
647 .build()
648 .unwrap();
649 api::ignore(endpoint).query(&client).unwrap();
650 }
651
652 #[test]
653 fn endpoint_allow_collaboration() {
654 let endpoint = ExpectedUrl::builder()
655 .method(Method::POST)
656 .endpoint("projects/simple%2Fproject/merge_requests")
657 .content_type("application/x-www-form-urlencoded")
658 .body_str(concat!(
659 "source_branch=source%2Fbranch",
660 "&target_branch=target%2Fbranch",
661 "&title=title",
662 "&allow_collaboration=true",
663 ))
664 .build()
665 .unwrap();
666 let client = SingleTestClient::new_raw(endpoint, "");
667
668 let endpoint = CreateMergeRequest::builder()
669 .project("simple/project")
670 .source_branch("source/branch")
671 .target_branch("target/branch")
672 .title("title")
673 .allow_collaboration(true)
674 .build()
675 .unwrap();
676 api::ignore(endpoint).query(&client).unwrap();
677 }
678
679 #[test]
680 fn endpoint_squash() {
681 let endpoint = ExpectedUrl::builder()
682 .method(Method::POST)
683 .endpoint("projects/simple%2Fproject/merge_requests")
684 .content_type("application/x-www-form-urlencoded")
685 .body_str(concat!(
686 "source_branch=source%2Fbranch",
687 "&target_branch=target%2Fbranch",
688 "&title=title",
689 "&squash=false",
690 ))
691 .build()
692 .unwrap();
693 let client = SingleTestClient::new_raw(endpoint, "");
694
695 let endpoint = CreateMergeRequest::builder()
696 .project("simple/project")
697 .source_branch("source/branch")
698 .target_branch("target/branch")
699 .title("title")
700 .squash(false)
701 .build()
702 .unwrap();
703 api::ignore(endpoint).query(&client).unwrap();
704 }
705}