gitlab/api/projects/merge_requests/
create.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use 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/// Parameters for setting the reviewer(s) of a merge request.
40#[derive(Debug, Clone)]
41#[non_exhaustive]
42pub(crate) enum Reviewer {
43    /// Unset all reviewers.
44    Unassigned,
45    /// A set of reviewers.
46    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/// Create a new merge request on project.
63#[derive(Debug, Builder, Clone)]
64#[builder(setter(strip_option))]
65pub struct CreateMergeRequest<'a> {
66    /// The project to open the merge requset *from*.
67    #[builder(setter(into))]
68    project: NameOrId<'a>,
69    /// The name of the source branch for the merge request.
70    #[builder(setter(into))]
71    source_branch: Cow<'a, str>,
72    /// The name of the target branch for the merge request.
73    #[builder(setter(into))]
74    target_branch: Cow<'a, str>,
75    /// The title for the merge request.
76    #[builder(setter(into))]
77    title: Cow<'a, str>,
78
79    /// The assignee of the merge request.
80    #[builder(setter(name = "_assignee"), default, private)]
81    assignee: Option<Assignee>,
82    #[builder(setter(name = "_reviewer"), default, private)]
83    reviewer: Option<Reviewer>,
84    /// The description of the merge request.
85    #[builder(setter(into), default)]
86    description: Option<Cow<'a, str>>,
87    /// The ID of the target project for the merge request.
88    #[builder(default)]
89    target_project_id: Option<u64>,
90    /// Labels to add to the merge request.
91    #[builder(setter(name = "_labels"), default, private)]
92    labels: Option<CommaSeparatedList<Cow<'a, str>>>,
93    /// The ID of the milestone to add the merge request to.
94    #[builder(default)]
95    milestone_id: Option<u64>,
96    /// Whether to remove the source branch once merged or not.
97    #[builder(default)]
98    remove_source_branch: Option<bool>,
99    /// Whether to allow collaboration with maintainers of the target project or not.
100    #[builder(default)]
101    allow_collaboration: Option<bool>,
102    /// Whether to squash the branch when merging or not.
103    #[builder(default)]
104    squash: Option<bool>,
105}
106
107impl<'a> CreateMergeRequest<'a> {
108    /// Create a builder for the endpoint.
109    pub fn builder() -> CreateMergeRequestBuilder<'a> {
110        CreateMergeRequestBuilder::default()
111    }
112}
113
114impl<'a> CreateMergeRequestBuilder<'a> {
115    /// Filter unassigned merge requests.
116    pub fn unassigned(&mut self) -> &mut Self {
117        self.assignee = Some(Some(Assignee::Unassigned));
118        self
119    }
120
121    /// Filter merge requests assigned to a user (by ID).
122    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    /// Filter merge requests assigned to a users (by ID).
139    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    /// Filter merge requests without a reviewer.
159    pub fn without_reviewer(&mut self) -> &mut Self {
160        self.reviewer = Some(Some(Reviewer::Unassigned));
161        self
162    }
163
164    /// Filter merge requests reviewed by a user (by ID).
165    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    /// Filter merge requests reviewed by users (by ID).
178    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    /// Add a label.
194    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    /// Add multiple labels.
206    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}