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