gitlab.rs 48.2 KB
Newer Older
1
2
3
4
5
6
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

Ben Boeckel's avatar
Ben Boeckel committed
7
8
9
10
11
#[rustversion::since(1.38)]
use std::any;
use std::borrow::Borrow;
use std::fmt::{self, Debug, Display};

12
use crates::itertools::Itertools;
13
use crates::percent_encoding::{utf8_percent_encode, AsciiSet, PercentEncode, CONTROLS};
14
15
use crates::reqwest::header::{self, HeaderValue};
use crates::reqwest::{self, Client, RequestBuilder, Url};
16
use crates::serde::de::Error as SerdeError;
17
use crates::serde::de::{DeserializeOwned, Unexpected};
18
use crates::serde::ser::Serialize;
Ben Boeckel's avatar
Ben Boeckel committed
19
use crates::serde::{Deserialize, Deserializer, Serializer};
20
use crates::serde_json;
21
use crates::thiserror::Error;
22

23
use types::*;
24

25
26
27
28
29
30
31
32
33
34
35
36
37
const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
    .add(b' ')
    .add(b'"')
    .add(b'#')
    .add(b'<')
    .add(b'>')
    .add(b'`')
    .add(b'?')
    .add(b'{')
    .add(b'}')
    .add(b'%')
    .add(b'/');

38
39
40
41
42
43
44
45
46
47
48
#[derive(Debug, Error)]
pub enum TokenError {
    #[error("header value error: {}", source)]
    HeaderValue {
        #[from]
        source: header::InvalidHeaderValue,
    },
}

type TokenResult<T> = Result<T, TokenError>;

49
50
51
52
53
54
55
56
57
58
59
/// A Gitlab API token
///
/// Gitlab supports two kinds of tokens
#[derive(Debug, Clone)]
enum Token {
    /// A personal access token, obtained through Gitlab user settings
    Private(String),
    /// An OAuth2 token, obtained through the OAuth2 flow
    OAuth2(String),
}

60
impl Token {
Miha Čančula's avatar
Miha Čančula committed
61
    /// Sets the appropirate header on the request.
62
63
64
    ///
    /// Depending on the token type, this will be either the Private-Token header
    /// or the Authorization header.
Miha Čančula's avatar
Miha Čančula committed
65
    /// Returns an error if the token string cannot be parsed as a header value.
66
    pub fn set_header(&self, req: RequestBuilder) -> TokenResult<RequestBuilder> {
67
68
        Ok(match self {
            Token::Private(token) => {
69
                let mut token_header_value = HeaderValue::from_str(&token)?;
70
71
72
73
74
                token_header_value.set_sensitive(true);
                req.header("PRIVATE-TOKEN", token_header_value)
            },
            Token::OAuth2(token) => {
                let value = format!("Bearer {}", token);
75
                let mut token_header_value = HeaderValue::from_str(&value)?;
76
77
78
79
80
81
82
                token_header_value.set_sensitive(true);
                req.header("Authorization", token_header_value)
            },
        })
    }
}

83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#[derive(Debug, Error)]
pub enum GitlabError {
    #[error("failed to parse url: {}", source)]
    UrlParse {
        #[from]
        source: reqwest::UrlError,
    },
    #[error("no such user: {}", user)]
    NoSuchUser { user: String },
    #[error("error setting token header: {}", source)]
    TokenError {
        #[from]
        source: TokenError,
    },
    #[error("communication with gitlab: {}", source)]
    Communication {
        #[from]
        source: reqwest::Error,
    },
    #[error("gitlab HTTP error: {}", status)]
    Http { status: reqwest::StatusCode },
    #[error("could not parse JSON response: {}", source)]
    Json {
        #[source]
        source: serde_json::Error,
    },
    #[error("gitlab server error: {}", msg)]
    Gitlab { msg: String },
    #[error("could not parse {} data from JSON: {}", typename.unwrap_or("<unknown>"), source)]
    DataType {
        #[source]
        source: serde_json::Error,
        typename: Option<&'static str>,
    },
}

impl GitlabError {
    fn no_such_user(user: &str) -> Self {
        GitlabError::NoSuchUser {
            user: user.into(),
        }
    }

    fn http(status: reqwest::StatusCode) -> Self {
        GitlabError::Http {
            status,
        }
    }

    fn json(source: serde_json::Error) -> Self {
        GitlabError::Json {
            source,
        }
    }

    fn from_gitlab(value: serde_json::Value) -> Self {
        let msg = value
            .pointer("/message")
            .or_else(|| value.pointer("/error"))
            .and_then(|s| s.as_str())
            .unwrap_or_else(|| "<unknown error>");

        GitlabError::Gitlab {
            msg: msg.into(),
        }
    }

    #[rustversion::since(1.38)]
    fn data_type<T>(source: serde_json::Error) -> Self {
        GitlabError::DataType {
            source,
            typename: Some(any::type_name::<T>()),
        }
    }

    #[rustversion::before(1.38)]
    fn data_type<T>(source: serde_json::Error) -> Self {
        GitlabError::DataType {
            source,
            typename: None,
        }
    }
}

type GitlabResult<T> = Result<T, GitlabError>;

Ben Boeckel's avatar
Ben Boeckel committed
169
170
171
/// A representation of the Gitlab API for a single user.
///
/// Separate users should use separate instances of this.
172
pub struct Gitlab {
173
174
    /// The client to use for API calls.
    client: Client,
Ben Boeckel's avatar
Ben Boeckel committed
175
    /// The base URL to use for API calls.
176
    base_url: Url,
Ben Boeckel's avatar
Ben Boeckel committed
177
    /// The secret token to use when communicating with Gitlab.
178
    token: Token,
179
180
}

Ben Boeckel's avatar
Ben Boeckel committed
181
impl Debug for Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
182
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
183
184
185
        f.debug_struct("Gitlab")
            .field("base_url", &self.base_url)
            .finish()
Ben Boeckel's avatar
Ben Boeckel committed
186
187
188
189
    }
}

#[derive(Debug)]
190
191
192
193
194
195
196
197
198
199
200
201
/// Optional information for commit statuses.
pub struct CommitStatusInfo<'a> {
    /// The refname of the commit being tested.
    pub refname: Option<&'a str>,
    /// The name of the status (defaults to `"default"` on the Gitlab side).
    pub name: Option<&'a str>,
    /// A URL to associate with the status.
    pub target_url: Option<&'a str>,
    /// A description of the status check.
    pub description: Option<&'a str>,
}

202
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203
204
205
206
207
208
209
210
211
/// Optional information for merge requests.
pub enum MergeRequestStateFilter {
    /// Get the opened/reopened merge requests.
    Opened,
    /// Get the closes merge requests.
    Closed,
    /// Get the merged merge requests.
    Merged,
}
212
213
214
215
216
217

enum_serialize!(MergeRequestStateFilter -> "state",
    Opened => "opened",
    Closed => "closed",
    Merged => "merged",
);
218

219
impl Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
220
221
    /// Create a new Gitlab API representation.
    ///
222
    /// The `token` should be a valid [personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html).
Ben Boeckel's avatar
Ben Boeckel committed
223
    /// Errors out if `token` is invalid.
224
    pub fn new<H, T>(host: H, token: T) -> GitlabResult<Self>
Ben Boeckel's avatar
Ben Boeckel committed
225
226
227
    where
        H: AsRef<str>,
        T: ToString,
228
    {
229
        Self::new_impl("https", host.as_ref(), Token::Private(token.to_string()))
230
231
232
233
234
    }

    /// Create a new non-SSL Gitlab API representation.
    ///
    /// Errors out if `token` is invalid.
235
    pub fn new_insecure<H, T>(host: H, token: T) -> GitlabResult<Self>
Ben Boeckel's avatar
Ben Boeckel committed
236
237
238
    where
        H: AsRef<str>,
        T: ToString,
239
    {
240
241
242
243
244
245
246
        Self::new_impl("http", host.as_ref(), Token::Private(token.to_string()))
    }

    /// Create a new Gitlab API representation.
    ///
    /// The `token` should be a valid [OAuth2 token](https://docs.gitlab.com/ee/api/oauth2.html).
    /// Errors out if `token` is invalid.
247
    pub fn with_oauth2<H, T>(host: H, token: T) -> GitlabResult<Self>
248
249
250
251
252
253
254
255
256
257
258
    where
        H: AsRef<str>,
        T: ToString,
    {
        Self::new_impl("https", host.as_ref(), Token::OAuth2(token.to_string()))
    }

    /// Create a new non-SSL Gitlab API representation.
    ///
    /// The `token` should be a valid [OAuth2 token](https://docs.gitlab.com/ee/api/oauth2.html).
    /// Errors out if `token` is invalid.
259
    pub fn with_oauth2_insecure<H, T>(host: H, token: T) -> GitlabResult<Self>
260
261
262
263
264
    where
        H: AsRef<str>,
        T: ToString,
    {
        Self::new_impl("http", host.as_ref(), Token::OAuth2(token.to_string()))
265
266
    }

Ben Boeckel's avatar
Ben Boeckel committed
267
    /// Internal method to create a new Gitlab client.
268
269
    fn new_impl(protocol: &str, host: &str, token: Token) -> GitlabResult<Self> {
        let base_url = Url::parse(&format!("{}://{}/api/v4/", protocol, host))?;
Ben Boeckel's avatar
Ben Boeckel committed
270

271
        let api = Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
272
            client: Client::new(),
273
274
            base_url,
            token,
275
        };
276

277
        // Ensure the API is working.
Ben Boeckel's avatar
Ben Boeckel committed
278
        let _: UserPublic = api.current_user()?;
279

280
        Ok(api)
281
282
    }

283
284
    /// Create a new Gitlab API client builder.
    pub fn builder<H, T>(host: H, token: T) -> GitlabBuilder
Ben Boeckel's avatar
Ben Boeckel committed
285
286
287
    where
        H: ToString,
        T: ToString,
288
289
290
291
    {
        GitlabBuilder::new(host, token)
    }

Ben Boeckel's avatar
Ben Boeckel committed
292
    /// The user the API is acting as.
293
    pub fn current_user(&self) -> GitlabResult<UserPublic> {
294
        self.get_with_param("user", query_param_slice![])
295
296
    }

297
    /// Get all user accounts
298
    pub fn users<T, I, K, V>(&self, params: I) -> GitlabResult<Vec<T>>
Ben Boeckel's avatar
Ben Boeckel committed
299
300
    where
        T: UserResult,
301
302
303
304
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
Ben Boeckel's avatar
Ben Boeckel committed
305
    {
306
        self.get_paged_with_param("users", params)
307
308
309
    }

    /// Find a user by id.
310
    pub fn user<T, I, K, V>(&self, user: UserId, params: I) -> GitlabResult<T>
Ben Boeckel's avatar
Ben Boeckel committed
311
312
    where
        T: UserResult,
313
314
315
316
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
Ben Boeckel's avatar
Ben Boeckel committed
317
    {
318
        self.get_with_param(format!("users/{}", user), params)
319
320
    }

Ben Boeckel's avatar
Ben Boeckel committed
321
    /// Find a user by username.
322
    pub fn user_by_name<T, N>(&self, name: N) -> GitlabResult<T>
Ben Boeckel's avatar
Ben Boeckel committed
323
324
325
    where
        T: UserResult,
        N: AsRef<str>,
326
327
    {
        let mut users = self.get_paged_with_param("users", &[("username", name.as_ref())])?;
Ben Boeckel's avatar
Ben Boeckel committed
328
329
        users
            .pop()
330
            .ok_or_else(|| GitlabError::no_such_user(name.as_ref()))
331
332
    }

333
    /// Get all accessible projects.
334
    pub fn projects<I, K, V>(&self, params: I) -> GitlabResult<Vec<Project>>
335
336
337
338
339
340
341
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
        self.get_paged_with_param("projects", params)
342
343
344
    }

    /// Get all owned projects.
345
    pub fn owned_projects(&self) -> GitlabResult<Vec<Project>> {
346
        self.get_paged_with_param("projects", &[("owned", "true")])
347
348
349
    }

    /// Find a project by id.
350
    pub fn project<I, K, V>(&self, project: ProjectId, params: I) -> GitlabResult<Project>
351
352
353
354
355
356
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
357
        self.get_with_param(format!("projects/{}", project), params)
358
359
    }

360
    /// A URL-safe name for projects.
361
    fn url_name(name: &str) -> PercentEncode {
362
363
364
        utf8_percent_encode(name, PATH_SEGMENT_ENCODE_SET)
    }

Ben Boeckel's avatar
Ben Boeckel committed
365
    /// Find a project by name.
366
    pub fn project_by_name<N, I, K, V>(&self, name: N, params: I) -> GitlabResult<Project>
Ben Boeckel's avatar
Ben Boeckel committed
367
368
    where
        N: AsRef<str>,
369
370
371
372
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
373
    {
374
        self.get_with_param(
375
            format!("projects/{}", Self::url_name(name.as_ref())),
376
377
            params,
        )
378
379
    }

380
    /// Get all accessible groups.
381
    pub fn groups<I, K, V>(&self, params: I) -> GitlabResult<Vec<Group>>
382
383
384
385
386
387
388
389
390
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
        self.get_paged_with_param("groups", params)
    }

391
    /// Get a project's hooks.
392
    pub fn hooks<I, K, V>(&self, project: ProjectId, params: I) -> GitlabResult<Vec<ProjectHook>>
393
394
395
396
397
398
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
399
        self.get_paged_with_param(format!("projects/{}/hooks", project), params)
400
    }
401

402
    /// Get a project hook.
403
404
405
406
407
408
    pub fn hook<I, K, V>(
        &self,
        project: ProjectId,
        hook: HookId,
        params: I,
    ) -> GitlabResult<ProjectHook>
409
410
411
412
413
414
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
415
        self.get_with_param(format!("projects/{}/hooks/{}", project, hook), params)
416
417
    }

Ben Boeckel's avatar
Ben Boeckel committed
418
    /// Convert a boolean parameter into an HTTP request value.
Ben Boeckel's avatar
Ben Boeckel committed
419
420
421
422
423
424
425
426
    fn bool_param_value(value: bool) -> &'static str {
        if value {
            "true"
        } else {
            "false"
        }
    }

Ben Boeckel's avatar
Ben Boeckel committed
427
    /// HTTP parameters required to register to a project.
Makoto Nakashima's avatar
Makoto Nakashima committed
428
    fn event_flags(events: WebhookEvents) -> Vec<(&'static str, &'static str)> {
Ben Boeckel's avatar
Ben Boeckel committed
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
        vec![
            ("job_events", Self::bool_param_value(events.job())),
            ("issues_events", Self::bool_param_value(events.issues())),
            (
                "confidential_issues_events",
                Self::bool_param_value(events.confidential_issues()),
            ),
            (
                "merge_requests_events",
                Self::bool_param_value(events.merge_requests()),
            ),
            ("note_events", Self::bool_param_value(events.note())),
            ("pipeline_events", Self::bool_param_value(events.pipeline())),
            ("push_events", Self::bool_param_value(events.push())),
            (
                "wiki_page_events",
                Self::bool_param_value(events.wiki_page()),
            ),
        ]
Ben Boeckel's avatar
Ben Boeckel committed
448
449
450
    }

    /// Add a project hook.
Ben Boeckel's avatar
Ben Boeckel committed
451
452
453
454
455
    pub fn add_hook<U>(
        &self,
        project: ProjectId,
        url: U,
        events: WebhookEvents,
456
    ) -> GitlabResult<ProjectHook>
Ben Boeckel's avatar
Ben Boeckel committed
457
458
    where
        U: AsRef<str>,
459
    {
Makoto Nakashima's avatar
Makoto Nakashima committed
460
        let mut flags = Self::event_flags(events);
461
        flags.push(("url", url.as_ref()));
Ben Boeckel's avatar
Ben Boeckel committed
462

463
        self.post_with_param(format!("projects/{}/hooks", project), &flags)
Ben Boeckel's avatar
Ben Boeckel committed
464
465
    }

466
    /// Get the team members of a group.
467
    pub fn group_members<I, K, V>(&self, group: GroupId, params: I) -> GitlabResult<Vec<Member>>
468
469
470
471
472
473
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
474
        self.get_paged_with_param(format!("groups/{}/members", group), params)
475
476
477
    }

    /// Get a team member of a group.
478
479
480
481
482
483
    pub fn group_member<I, K, V>(
        &self,
        group: GroupId,
        user: UserId,
        params: I,
    ) -> GitlabResult<Member>
484
485
486
487
488
489
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
490
        self.get_with_param(format!("groups/{}/members/{}", group, user), params)
491
492
    }

493
    /// Get the team members of a project.
494
495
496
497
498
    pub fn project_members<I, K, V>(
        &self,
        project: ProjectId,
        params: I,
    ) -> GitlabResult<Vec<Member>>
499
500
501
502
503
504
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
505
        self.get_paged_with_param(format!("projects/{}/members", project), params)
506
507
508
    }

    /// Get a team member of a project.
509
510
511
512
513
    pub fn project_member<I, K, V>(
        &self,
        project: ProjectId,
        user: UserId,
        params: I,
514
    ) -> GitlabResult<Member>
515
516
517
518
519
520
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
521
        self.get_with_param(format!("projects/{}/members/{}", project, user), params)
522
523
    }

524
    /// Add a user to a project.
Ben Boeckel's avatar
Ben Boeckel committed
525
526
527
528
529
    pub fn add_user_to_project(
        &self,
        project: ProjectId,
        user: UserId,
        access: AccessLevel,
530
    ) -> GitlabResult<Member> {
531
532
533
        let user_str = format!("{}", user);
        let access_str = format!("{}", access);

Ben Boeckel's avatar
Ben Boeckel committed
534
        self.post_with_param(
535
            format!("projects/{}/members", project),
Ben Boeckel's avatar
Ben Boeckel committed
536
537
            &[("user", &user_str), ("access", &access_str)],
        )
538
539
    }

540
    /// Add a user to a project.
Ben Boeckel's avatar
Ben Boeckel committed
541
542
543
544
545
    pub fn add_user_to_project_by_name<P>(
        &self,
        project: P,
        user: UserId,
        access: AccessLevel,
546
    ) -> GitlabResult<Member>
Ben Boeckel's avatar
Ben Boeckel committed
547
548
    where
        P: AsRef<str>,
549
550
551
552
    {
        let user_str = format!("{}", user);
        let access_str = format!("{}", access);

Ben Boeckel's avatar
Ben Boeckel committed
553
        self.post_with_param(
554
            format!("projects/{}/members", Self::url_name(project.as_ref())),
555
            &[("user_id", &user_str), ("access_level", &access_str)],
Ben Boeckel's avatar
Ben Boeckel committed
556
        )
557
558
    }

559
    /// Get branches for a project.
560
    pub fn branches<I, K, V>(&self, project: ProjectId, params: I) -> GitlabResult<Vec<RepoBranch>>
561
562
563
564
565
566
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
567
        self.get_paged_with_param(format!("projects/{}/branches", project), params)
568
569
570
    }

    /// Get a branch.
571
572
573
574
575
576
    pub fn branch<B, I, K, V>(
        &self,
        project: ProjectId,
        branch: B,
        params: I,
    ) -> GitlabResult<RepoBranch>
Ben Boeckel's avatar
Ben Boeckel committed
577
578
    where
        B: AsRef<str>,
579
580
581
582
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
583
    {
584
        self.get_with_param(
585
            format!(
586
587
588
589
590
591
                "projects/{}/repository/branches/{}",
                project,
                Self::url_name(branch.as_ref()),
            ),
            params,
        )
592
593
594
    }

    /// Get a commit.
595
    pub fn commit<C>(&self, project: ProjectId, commit: C) -> GitlabResult<RepoCommitDetail>
Ben Boeckel's avatar
Ben Boeckel committed
596
597
    where
        C: AsRef<str>,
598
    {
Brad King's avatar
Brad King committed
599
        self.get_with_param(
600
            format!(
Ben Boeckel's avatar
Ben Boeckel committed
601
602
603
604
605
606
                "projects/{}/repository/commits/{}",
                project,
                commit.as_ref(),
            ),
            &[("stats", "true")],
        )
607
608
609
    }

    /// Get comments on a commit.
610
611
612
613
614
    pub fn commit_comments<C, I, K, V>(
        &self,
        project: ProjectId,
        commit: C,
        params: I,
615
    ) -> GitlabResult<Vec<CommitNote>>
Ben Boeckel's avatar
Ben Boeckel committed
616
617
    where
        C: AsRef<str>,
618
619
620
621
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
622
    {
623
        self.get_paged_with_param(
624
            format!(
625
626
627
628
629
630
                "projects/{}/repository/commits/{}/comments",
                project,
                commit.as_ref(),
            ),
            params,
        )
631
632
633
    }

    /// Get comments on a commit.
Ben Boeckel's avatar
Ben Boeckel committed
634
635
636
637
638
    pub fn create_commit_comment<C, B>(
        &self,
        project: ProjectId,
        commit: C,
        body: B,
639
    ) -> GitlabResult<CommitNote>
Ben Boeckel's avatar
Ben Boeckel committed
640
641
642
    where
        C: AsRef<str>,
        B: AsRef<str>,
643
    {
Ben Boeckel's avatar
Ben Boeckel committed
644
        self.post_with_param(
645
            format!(
Ben Boeckel's avatar
Ben Boeckel committed
646
647
648
649
650
651
                "projects/{}/repository/commits/{}/comment",
                project,
                commit.as_ref(),
            ),
            &[("note", body.as_ref())],
        )
652
653
    }

654
    /// Get comments on a commit.
Ben Boeckel's avatar
Ben Boeckel committed
655
656
657
658
659
    pub fn create_commit_comment_by_name<P, C, B>(
        &self,
        project: P,
        commit: C,
        body: B,
660
    ) -> GitlabResult<CommitNote>
Ben Boeckel's avatar
Ben Boeckel committed
661
662
663
664
    where
        P: AsRef<str>,
        C: AsRef<str>,
        B: AsRef<str>,
665
    {
Ben Boeckel's avatar
Ben Boeckel committed
666
        self.post_with_param(
667
            format!(
Ben Boeckel's avatar
Ben Boeckel committed
668
669
670
671
672
673
                "projects/{}/repository/commits/{}/comment",
                Self::url_name(project.as_ref()),
                commit.as_ref(),
            ),
            &[("note", body.as_ref())],
        )
674
675
    }

676
    /// Get comments on a commit.
Ben Boeckel's avatar
Ben Boeckel committed
677
678
679
680
681
682
683
    pub fn create_commit_line_comment(
        &self,
        project: ProjectId,
        commit: &str,
        body: &str,
        path: &str,
        line: u64,
684
    ) -> GitlabResult<CommitNote> {
685
        let line_str = format!("{}", line);
Ben Boeckel's avatar
Ben Boeckel committed
686
        let line_type = LineType::New;
687

Ben Boeckel's avatar
Ben Boeckel committed
688
        self.post_with_param(
689
            format!("projects/{}/repository/commits/{}/comment", project, commit),
Ben Boeckel's avatar
Ben Boeckel committed
690
691
692
693
694
695
696
            &[
                ("note", body),
                ("path", path),
                ("line", &line_str),
                ("line_type", line_type.as_str()),
            ],
        )
697
698
    }

699
    /// Get the latest statuses of a commit.
700
    pub fn commit_latest_statuses<C, I, K, V>(
Ben Boeckel's avatar
Ben Boeckel committed
701
702
703
        &self,
        project: ProjectId,
        commit: C,
704
        params: I,
705
    ) -> GitlabResult<Vec<CommitStatus>>
Ben Boeckel's avatar
Ben Boeckel committed
706
707
    where
        C: AsRef<str>,
708
709
710
711
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
712
    {
713
        self.get_paged_with_param(
714
            format!(
715
716
717
718
719
720
                "projects/{}/repository/commits/{}/statuses",
                project,
                commit.as_ref(),
            ),
            params,
        )
721
722
    }

723
    /// Get the latest statuses of a commit.
724
    pub fn commit_latest_statuses_by_name<P, C, I, K, V>(
Ben Boeckel's avatar
Ben Boeckel committed
725
726
727
        &self,
        project: P,
        commit: C,
728
        params: I,
729
    ) -> GitlabResult<Vec<CommitStatus>>
Ben Boeckel's avatar
Ben Boeckel committed
730
731
732
    where
        P: AsRef<str>,
        C: AsRef<str>,
733
734
735
736
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
737
    {
738
        self.get_paged_with_param(
739
            format!(
740
741
742
743
744
745
                "projects/{}/repository/commits/{}/statuses",
                Self::url_name(project.as_ref()),
                commit.as_ref(),
            ),
            params,
        )
746
747
    }

748
    /// Get the all statuses of a commit.
749
750
751
752
753
    pub fn commit_all_statuses<C>(
        &self,
        project: ProjectId,
        commit: C,
    ) -> GitlabResult<Vec<CommitStatus>>
Ben Boeckel's avatar
Ben Boeckel committed
754
755
    where
        C: AsRef<str>,
756
    {
Ben Boeckel's avatar
Ben Boeckel committed
757
        self.get_paged_with_param(
758
            format!(
Ben Boeckel's avatar
Ben Boeckel committed
759
760
761
762
763
764
                "projects/{}/repository/commits/{}/statuses",
                project,
                commit.as_ref(),
            ),
            &[("all", "true")],
        )
765
766
    }

767
    /// Get the latest builds of a commit.
768
769
770
771
772
    pub fn commit_latest_builds<C, I, K, V>(
        &self,
        project: ProjectId,
        commit: C,
        params: I,
773
    ) -> GitlabResult<Vec<Job>>
Ben Boeckel's avatar
Ben Boeckel committed
774
775
    where
        C: AsRef<str>,
776
777
778
779
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
780
    {
781
        self.get_paged_with_param(
782
            format!(
783
784
785
786
787
788
                "projects/{}/repository/commits/{}/builds",
                project,
                commit.as_ref(),
            ),
            params,
        )
Makoto Nakashima's avatar
Makoto Nakashima committed
789
790
    }

791
    /// Get the all builds of a commit.
792
    pub fn commit_all_builds<C>(&self, project: ProjectId, commit: C) -> GitlabResult<Vec<Job>>
Ben Boeckel's avatar
Ben Boeckel committed
793
794
    where
        C: AsRef<str>,
795
    {
Ben Boeckel's avatar
Ben Boeckel committed
796
        self.get_paged_with_param(
797
            format!(
Ben Boeckel's avatar
Ben Boeckel committed
798
799
800
801
802
803
                "projects/{}/repository/commits/{}/builds",
                project,
                commit.as_ref(),
            ),
            &[("all", "true")],
        )
Makoto Nakashima's avatar
Makoto Nakashima committed
804
805
    }

Ben Boeckel's avatar
Ben Boeckel committed
806
    /// Create a status message for a commit.
Ben Boeckel's avatar
Ben Boeckel committed
807
808
809
810
811
812
    pub fn create_commit_status<S>(
        &self,
        project: ProjectId,
        sha: S,
        state: StatusState,
        info: &CommitStatusInfo,
813
    ) -> GitlabResult<CommitStatus>
Ben Boeckel's avatar
Ben Boeckel committed
814
815
    where
        S: AsRef<str>,
816
    {
817
        let path = format!("projects/{}/statuses/{}", project, sha.as_ref());
818

Makoto Nakashima's avatar
Makoto Nakashima committed
819
        let mut params = vec![("state", state.as_str())];
820

821
822
823
824
825
826
827
828
829
830
831
832
        if let Some(v) = info.refname {
            params.push(("ref", v))
        }
        if let Some(v) = info.name {
            params.push(("name", v))
        }
        if let Some(v) = info.target_url {
            params.push(("target_url", v))
        }
        if let Some(v) = info.description {
            params.push(("description", v))
        }
833

834
        self.post_with_param(path, &params)
835
836
    }

837
    /// Create a status message for a commit.
Ben Boeckel's avatar
Ben Boeckel committed
838
839
840
841
842
843
    pub fn create_commit_status_by_name<P, S>(
        &self,
        project: P,
        sha: S,
        state: StatusState,
        info: &CommitStatusInfo,
844
    ) -> GitlabResult<CommitStatus>
Ben Boeckel's avatar
Ben Boeckel committed
845
846
847
    where
        P: AsRef<str>,
        S: AsRef<str>,
848
    {
849
        let path = format!(
Ben Boeckel's avatar
Ben Boeckel committed
850
851
852
853
            "projects/{}/statuses/{}",
            Self::url_name(project.as_ref()),
            sha.as_ref(),
        );
854
855
856

        let mut params = vec![("state", state.as_str())];

857
858
859
860
861
862
863
864
865
866
867
868
        if let Some(v) = info.refname {
            params.push(("ref", v))
        }
        if let Some(v) = info.name {
            params.push(("name", v))
        }
        if let Some(v) = info.target_url {
            params.push(("target_url", v))
        }
        if let Some(v) = info.description {
            params.push(("description", v))
        }
869

870
        self.post_with_param(path, &params)
871
872
    }

873
    /// Get the labels for a project.
874
    pub fn labels(&self, project: ProjectId) -> GitlabResult<Vec<Label>> {
875
        self.get_paged(format!("projects/{}/labels", project))
876
    }
877
878

    /// Get the labels with open/closed/merge requests count
879
    pub fn labels_with_counts(&self, project: ProjectId) -> GitlabResult<Vec<Label>> {
Ben Boeckel's avatar
Ben Boeckel committed
880
        self.get_paged_with_param(
881
            format!("projects/{}/labels", project),
882
            &[("with_counts", "true")],
Ben Boeckel's avatar
Ben Boeckel committed
883
        )
884
    }
885
886

    /// Get label by ID.
887
    pub fn label(&self, project: ProjectId, label: LabelId) -> GitlabResult<Label> {
888
        self.get(format!("projects/{}/labels/{}", project, label))
889
890
    }

Ben Boeckel's avatar
Ben Boeckel committed
891
    /// Get the issues for a project.
892
    pub fn issues<I, K, V>(&self, project: ProjectId, params: I) -> GitlabResult<Vec<Issue>>
893
894
895
896
897
898
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
899
        self.get_paged_with_param(format!("projects/{}/issues", project), params)
Ben Boeckel's avatar
Ben Boeckel committed
900
901
902
    }

    /// Get issues.
903
904
905
906
907
    pub fn issue<I, K, V>(
        &self,
        project: ProjectId,
        issue: IssueInternalId,
        params: I,
908
    ) -> GitlabResult<Issue>
909
910
911
912
913
914
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
915
        self.get_with_param(format!("projects/{}/issues/{}", project, issue), params)
Ben Boeckel's avatar
Ben Boeckel committed
916
917
918
    }

    /// Get the notes from a issue.
919
920
921
922
923
    pub fn issue_notes<I, K, V>(
        &self,
        project: ProjectId,
        issue: IssueInternalId,
        params: I,
924
    ) -> GitlabResult<Vec<Note>>
925
926
927
928
929
930
931
    where
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
        self.get_paged_with_param(
932
            format!("projects/{}/issues/{}/notes", project, issue),
933
934
            params,
        )
Ben Boeckel's avatar
Ben Boeckel committed
935
936
    }

937
    /// Get the notes from a issue.
938
939
940
941
942
    pub fn issue_notes_by_name<P, I, K, V>(
        &self,
        project: P,
        issue: IssueInternalId,
        params: I,
943
    ) -> GitlabResult<Vec<Note>>
Ben Boeckel's avatar
Ben Boeckel committed
944
945
    where
        P: AsRef<str>,
946
947
948
949
        I: IntoIterator,
        I::Item: Borrow<(K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
950
    {
951
        self.get_paged_with_param(
952
            format!(
953
954
955
956
957
958
                "projects/{}/issues/{}/notes",
                Self::url_name(project.as_ref()),
                issue,
            ),
            params,
        )
959
960
    }

961
    /// Create a new label
962
    pub fn create_label(&self, project: ProjectId, label: Label) -> GitlabResult<Label> {
963
        let path = format!("projects/{}/labels", project);
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980

        let mut params: Vec<(&str, String)> = Vec::new();

        params.push(("name", label.name));
        params.push(("color", label.color.value()));

        if let Some(d) = label.description {
            params.push(("description", d));
        }

        if let Some(p) = label.priority {
            params.push(("priority", p.to_string()));
        }

        self.post_with_param(path, &params)
    }

VC's avatar
VC committed
981
    /// Create a new milestone
982
983
984
985
986
    pub fn create_milestone(
        &self,
        project: ProjectId,
        milestone: Milestone,
    ) -> GitlabResult<Milestone> {
987
        let path = format!("projects/{}/milestones", project);
VC's avatar
VC committed
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007

        let mut params: Vec<(&str, String)> = Vec::new();

        params.push(("title", milestone.title));

        if let Some(d) = milestone.description {
            params.push(("description", d));
        }

        if let Some(d) = milestone.due_date {
            params.push(("due_date", d.to_string()))
        }

        if let Some(s) = milestone.start_date {
            params.push(("start_date", s.to_string()))
        }

        self.post_with_param(path, &params)
    }

VC's avatar
VC committed
1008
    /// Create a new issue
1009
    pub fn create_issue(&self, project: ProjectId, issue: Issue) -> GitlabResult<Issue> {
1010
        let path = format!("projects/{}/issues", project);
VC's avatar
VC committed
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026

        let mut params: Vec<(&str, String)> = Vec::new();

        if issue.iid.value() != 0 {
            params.push(("iid", issue.iid.value().to_string()));
        }

        params.push(("title", issue.title));

        if let Some(d) = issue.description {
            params.push(("description", d));
        }

        params.push(("confidential", issue.confidential.to_string()));

        if let Some(v) = issue.assignees {
Ben Boeckel's avatar
Ben Boeckel committed
1027
1028
1029
1030
            params.extend(
                v.into_iter()
                    .map(|x| ("assignee_ids[]", x.id.value().to_string())),
            );
VC's avatar
VC committed
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
        }

        if let Some(m) = issue.milestone {
            params.push(("milestone_id", m.id.value().to_string()))
        }

        if !issue.labels.is_empty() {
            params.push(("labels", issue.labels.join(",")));
        }

        params.push(("created_at", issue.created_at.to_string()));

        if let Some(d) = issue.due_date {
            params.push(("due_date", d.to_string()))
        }

        self.post_with_param(path, &params)
    }

Andrew Chin's avatar
Andrew Chin committed
1050
1051
1052
1053
1054
    /// Get the resource label events from an issue.
    pub fn issue_label_events(
        &self,
        project: ProjectId,
        issue: IssueInternalId,
1055
    ) -> GitlabResult<Vec<ResourceLabelEvent>> {
1056
        self.get_paged(format!(
Ben Boeckel's avatar
Ben Boeckel committed
1057
1058
1059
            "projects/{}/issues/{}/resource_label_events",
            project, issue,
        ))
Andrew Chin's avatar
Andrew Chin committed
1060
1061
    }

Ben Boeckel's avatar
Ben Boeckel committed
1062
    /// Create a note on a issue.
Ben Boeckel's avatar
Ben Boeckel committed
1063
1064
1065
1066
1067
    pub fn create_issue_note<C>(
        &self,
        project: ProjectId,
        issue: IssueInternalId,
        content: C,
1068
    ) -> GitlabResult<Note>
Ben Boeckel's avatar
Ben Boeckel committed
1069
1070
    where
        C: AsRef<str>,
1071
    {
1072
        let path = format!("projects/{}/issues/{}/notes", project, issue);
Ben Boeckel's avatar
Ben Boeckel committed
1073

1074
        self.post_with_param(path, &[("body", content.as_ref())])
Ben Boeckel's avatar
Ben Boeckel committed
1075
1076
    }

1077
    /// Create a note on a issue.
Ben Boeckel's avatar
Ben Boeckel committed
1078
1079
1080
1081
1082
    pub fn create_issue_note_by_name<P, C>(
        &self,
        project: P,
        issue: IssueInternalId,
        content: C,
1083
    ) -> GitlabResult<Note>
Ben Boeckel's avatar
Ben Boeckel committed
1084
1085
1086
    where
        P: AsRef<str>,
        C: AsRef<str>,
1087
    {
1088
        let path = format!(
Ben Boeckel's avatar
Ben Boeckel committed
1089
1090
1091
1092
            "projects/{}/issues/{}/notes",
            Self::url_name(project.as_ref()),
            issue,
        );
1093

1094
        self.post_with_param(path, &[("body", content.as_ref())])
1095
1096
    }

1097
    /// Get the merge requests for a project.
1098
1099
1100
1101
    pub fn merge_requests<I, K, V>(
        &self,
        project: ProjectId,
        params: I,
Ben Boeckel's avatar