gitlab.rs 23.1 KB
Newer Older
1
2
3
4
5
6
7
8
// Copyright 2016 Kitware, Inc.
//
// 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.

9
10
11
12
use crates::itertools::Itertools;
use crates::reqwest::{Client, Method, RequestBuilder, Url};
use crates::serde::{Deserialize, Deserializer, Serializer};
use crates::serde::de::Error as SerdeError;
13
use crates::serde::de::{DeserializeOwned, Unexpected};
14
15
16
use crates::serde::ser::Serialize;
use crates::serde_json;
use crates::url::percent_encoding::{PATH_SEGMENT_ENCODE_SET, percent_encode};
17

18
19
use error::*;
use types::*;
20

Makoto Nakashima's avatar
Makoto Nakashima committed
21
use std::borrow::Borrow;
22
use std::fmt::{self, Display, Debug};
Ben Boeckel's avatar
Ben Boeckel committed
23

Ben Boeckel's avatar
Ben Boeckel committed
24
25
26
/// A representation of the Gitlab API for a single user.
///
/// Separate users should use separate instances of this.
27
pub struct Gitlab {
28
29
    /// The client to use for API calls.
    client: Client,
Ben Boeckel's avatar
Ben Boeckel committed
30
    /// The base URL to use for API calls.
31
    base_url: Url,
Ben Boeckel's avatar
Ben Boeckel committed
32
    /// The secret token to use when communicating with Gitlab.
33
34
35
    token: String,
}

Ben Boeckel's avatar
Ben Boeckel committed
36
impl Debug for Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
37
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
38
39
40
        f.debug_struct("Gitlab")
            .field("base_url", &self.base_url)
            .finish()
Ben Boeckel's avatar
Ben Boeckel committed
41
42
43
    }
}

Ben Boeckel's avatar
Ben Boeckel committed
44
// The header Gitlab uses to authenticate the user.
45
46
header!{ (GitlabPrivateToken, "PRIVATE-TOKEN") => [String] }

Ben Boeckel's avatar
Ben Boeckel committed
47
#[derive(Debug)]
48
49
50
51
52
53
54
55
56
57
58
59
/// 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>,
}

60
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61
62
63
64
65
66
67
68
69
/// 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,
}
70
71
72
73
74
75

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

77
impl Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
78
79
80
    /// Create a new Gitlab API representation.
    ///
    /// Errors out if `token` is invalid.
81
82
83
84
85
    pub fn new<H, T>(host: H, token: T) -> Result<Self>
        where H: AsRef<str>,
              T: ToString,
    {
        Self::new_impl("https", host.as_ref(), token.to_string())
86
87
88
89
90
    }

    /// Create a new non-SSL Gitlab API representation.
    ///
    /// Errors out if `token` is invalid.
91
92
93
94
95
    pub fn new_insecure<H, T>(host: H, token: T) -> Result<Self>
        where H: AsRef<str>,
              T: ToString,
    {
        Self::new_impl("http", host.as_ref(), token.to_string())
96
97
    }

Ben Boeckel's avatar
Ben Boeckel committed
98
    /// Internal method to create a new Gitlab client.
99
    fn new_impl(protocol: &str, host: &str, token: String) -> Result<Self> {
Brad King's avatar
Brad King committed
100
        let base_url = Url::parse(&format!("{}://{}/api/v4/", protocol, host))
101
            .chain_err(|| ErrorKind::UrlParse)?;
Ben Boeckel's avatar
Ben Boeckel committed
102

103
        let api = Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
104
            client: Client::new(),
105
            base_url: base_url,
106
            token: token,
107
        };
108

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

112
        Ok(api)
113
114
    }

115
116
117
118
119
120
121
122
    /// Create a new Gitlab API client builder.
    pub fn builder<H, T>(host: H, token: T) -> GitlabBuilder
        where H: ToString,
              T: ToString,
    {
        GitlabBuilder::new(host, token)
    }

Ben Boeckel's avatar
Ben Boeckel committed
123
    /// The user the API is acting as.
124
    pub fn current_user(&self) -> Result<UserPublic> {
125
        self.get("user")
126
127
    }

128
    /// Get all user accounts
Ben Boeckel's avatar
Ben Boeckel committed
129
130
131
    pub fn users<T>(&self) -> Result<Vec<T>>
        where T: UserResult,
    {
132
        self.get_paged("users")
133
134
135
    }

    /// Find a user by id.
Ben Boeckel's avatar
Ben Boeckel committed
136
137
138
    pub fn user<T>(&self, user: UserId) -> Result<T>
        where T: UserResult,
    {
139
        self.get(&format!("users/{}", user))
140
141
    }

Ben Boeckel's avatar
Ben Boeckel committed
142
    /// Find a user by username.
143
144
145
146
147
    pub fn user_by_name<T, N>(&self, name: N) -> Result<T>
        where T: UserResult,
              N: AsRef<str>,
    {
        let mut users = self.get_paged_with_param("users", &[("username", name.as_ref())])?;
148
        users.pop()
Ben Boeckel's avatar
Ben Boeckel committed
149
            .ok_or_else(|| Error::from_kind(ErrorKind::Gitlab("no such user".to_string())))
150
151
    }

152
    /// Get all accessible projects.
Ben Boeckel's avatar
Ben Boeckel committed
153
    pub fn projects(&self) -> Result<Vec<Project>> {
154
        self.get_paged("projects")
155
156
157
    }

    /// Get all owned projects.
Ben Boeckel's avatar
Ben Boeckel committed
158
    pub fn owned_projects(&self) -> Result<Vec<Project>> {
159
        self.get_paged_with_param("projects", &[("owned", "true")])
160
161
162
    }

    /// Find a project by id.
Ben Boeckel's avatar
Ben Boeckel committed
163
    pub fn project(&self, project: ProjectId) -> Result<Project> {
164
        self.get(&format!("projects/{}", project))
165
166
    }

Ben Boeckel's avatar
Ben Boeckel committed
167
    /// Find a project by name.
168
169
170
    pub fn project_by_name<N>(&self, name: N) -> Result<Project>
        where N: AsRef<str>,
    {
171
        self.get(&format!("projects/{}",
172
                          percent_encode(name.as_ref().as_bytes(), PATH_SEGMENT_ENCODE_SET)))
173
174
    }

175
    /// Get a project's hooks.
176
    pub fn hooks(&self, project: ProjectId) -> Result<Vec<ProjectHook>> {
177
        self.get_paged(&format!("projects/{}/hooks", project))
178
    }
179

180
    /// Get a project hook.
181
    pub fn hook(&self, project: ProjectId, hook: HookId) -> Result<ProjectHook> {
182
        self.get(&format!("projects/{}/hooks/{}", project, hook))
183
184
    }

Ben Boeckel's avatar
Ben Boeckel committed
185
    /// Convert a boolean parameter into an HTTP request value.
Ben Boeckel's avatar
Ben Boeckel committed
186
187
188
189
190
191
192
193
    fn bool_param_value(value: bool) -> &'static str {
        if value {
            "true"
        } else {
            "false"
        }
    }

Ben Boeckel's avatar
Ben Boeckel committed
194
    /// HTTP parameters required to register to a project.
Makoto Nakashima's avatar
Makoto Nakashima committed
195
    fn event_flags(events: WebhookEvents) -> Vec<(&'static str, &'static str)> {
Brad King's avatar
Brad King committed
196
        vec![("job_events", Self::bool_param_value(events.job())),
Makoto Nakashima's avatar
Makoto Nakashima committed
197
198
199
200
201
202
             ("issues_events", Self::bool_param_value(events.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
203
204
205
    }

    /// Add a project hook.
206
207
208
    pub fn add_hook<U>(&self, project: ProjectId, url: U, events: WebhookEvents) -> Result<ProjectHook>
        where U: AsRef<str>,
    {
Makoto Nakashima's avatar
Makoto Nakashima committed
209
        let mut flags = Self::event_flags(events);
210
        flags.push(("url", url.as_ref()));
Ben Boeckel's avatar
Ben Boeckel committed
211

212
        self.post_with_param(&format!("projects/{}/hooks", project), &flags)
Ben Boeckel's avatar
Ben Boeckel committed
213
214
    }

215
    /// Get the team members of a group.
Ben Boeckel's avatar
Ben Boeckel committed
216
    pub fn group_members(&self, group: GroupId) -> Result<Vec<Member>> {
217
        self.get_paged(&format!("groups/{}/members", group))
218
219
220
    }

    /// Get a team member of a group.
Ben Boeckel's avatar
Ben Boeckel committed
221
    pub fn group_member(&self, group: GroupId, user: UserId) -> Result<Member> {
222
        self.get(&format!("groups/{}/members/{}", group, user))
223
224
    }

225
    /// Get the team members of a project.
Ben Boeckel's avatar
Ben Boeckel committed
226
    pub fn project_members(&self, project: ProjectId) -> Result<Vec<Member>> {
227
        self.get_paged(&format!("projects/{}/members", project))
228
229
230
    }

    /// Get a team member of a project.
Ben Boeckel's avatar
Ben Boeckel committed
231
    pub fn project_member(&self, project: ProjectId, user: UserId) -> Result<Member> {
232
        self.get(&format!("projects/{}/members/{}", project, user))
233
234
    }

235
236
    /// Add a user to a project.
    pub fn add_user_to_project(&self, project: ProjectId, user: UserId, access: AccessLevel)
Ben Boeckel's avatar
Ben Boeckel committed
237
                               -> Result<Member> {
238
239
240
        let user_str = format!("{}", user);
        let access_str = format!("{}", access);

241
242
        self.post_with_param(&format!("projects/{}/members", project),
                             &[("user", &user_str), ("access", &access_str)])
243
244
245
    }

    /// Get branches for a project.
Ben Boeckel's avatar
Ben Boeckel committed
246
    pub fn branches(&self, project: ProjectId) -> Result<Vec<RepoBranch>> {
247
        self.get_paged(&format!("projects/{}/branches", project))
248
249
250
    }

    /// Get a branch.
251
252
253
    pub fn branch<B>(&self, project: ProjectId, branch: B) -> Result<RepoBranch>
        where B: AsRef<str>,
    {
254
255
        self.get(&format!("projects/{}/repository/branches/{}",
                          project,
256
                          percent_encode(branch.as_ref().as_bytes(), PATH_SEGMENT_ENCODE_SET)))
257
258
259
    }

    /// Get a commit.
260
261
262
263
    pub fn commit<C>(&self, project: ProjectId, commit: C) -> Result<RepoCommitDetail>
        where C: AsRef<str>,
    {
        self.get(&format!("projects/{}/repository/commits/{}", project, commit.as_ref()))
264
265
266
    }

    /// Get comments on a commit.
267
268
269
    pub fn commit_comments<C>(&self, project: ProjectId, commit: C) -> Result<Vec<CommitNote>>
        where C: AsRef<str>,
    {
270
271
        self.get_paged(&format!("projects/{}/repository/commits/{}/comments",
                                project,
272
                                commit.as_ref()))
273
274
275
    }

    /// Get comments on a commit.
276
277
278
279
280
    pub fn create_commit_comment<C, B>(&self, project: ProjectId, commit: C, body: B)
                                       -> Result<CommitNote>
        where C: AsRef<str>,
              B: AsRef<str>,
    {
281
282
        self.post_with_param(&format!("projects/{}/repository/commits/{}/comment",
                                      project,
283
284
                                      commit.as_ref()),
                             &[("note", body.as_ref())])
285
286
287
288
    }

    /// Get comments on a commit.
    pub fn create_commit_line_comment(&self, project: ProjectId, commit: &str, body: &str,
Ben Boeckel's avatar
Ben Boeckel committed
289
                                      path: &str, line: u64)
Ben Boeckel's avatar
Ben Boeckel committed
290
                                      -> Result<CommitNote> {
291
        let line_str = format!("{}", line);
Ben Boeckel's avatar
Ben Boeckel committed
292
        let line_type = LineType::New;
293

294
295
296
297
298
299
300
        self.post_with_param(&format!("projects/{}/repository/commits/{}/comment",
                                      project,
                                      commit),
                             &[("note", body),
                               ("path", path),
                               ("line", &line_str),
                               ("line_type", line_type.as_str())])
301
302
    }

303
    /// Get the latest statuses of a commit.
304
305
306
307
    pub fn commit_latest_statuses<C>(&self, project: ProjectId, commit: C)
                                     -> Result<Vec<CommitStatus>>
        where C: AsRef<str>,
    {
308
309
        self.get_paged(&format!("projects/{}/repository/commits/{}/statuses",
                                project,
310
                                commit.as_ref()))
311
312
    }

313
    /// Get the all statuses of a commit.
314
315
316
317
    pub fn commit_all_statuses<C>(&self, project: ProjectId, commit: C)
                                  -> Result<Vec<CommitStatus>>
        where C: AsRef<str>,
    {
318
319
        self.get_paged_with_param(&format!("projects/{}/repository/commits/{}/statuses",
                                           project,
320
                                           commit.as_ref()),
321
                                  &[("all", "true")])
322
323
    }

324
    /// Get the latest builds of a commit.
325
326
327
328
    pub fn commit_latest_builds<C>(&self, project: ProjectId, commit: C) -> Result<Vec<Job>>
        where C: AsRef<str>,
    {
        self.get_paged(&format!("projects/{}/repository/commits/{}/builds", project, commit.as_ref()))
Makoto Nakashima's avatar
Makoto Nakashima committed
329
330
    }

331
    /// Get the all builds of a commit.
332
333
334
    pub fn commit_all_builds<C>(&self, project: ProjectId, commit: C) -> Result<Vec<Job>>
        where C: AsRef<str>,
    {
335
336
        self.get_paged_with_param(&format!("projects/{}/repository/commits/{}/builds",
                                           project,
337
                                           commit.as_ref()),
338
                                  &[("all", "true")])
Makoto Nakashima's avatar
Makoto Nakashima committed
339
340
    }

Ben Boeckel's avatar
Ben Boeckel committed
341
    /// Create a status message for a commit.
342
343
344
345
346
347
    pub fn create_commit_status<S>(&self, project: ProjectId, sha: S, state: StatusState,
                                   info: &CommitStatusInfo)
                                   -> Result<CommitStatus>
        where S: AsRef<str>,
    {
        let path = &format!("projects/{}/statuses/{}", project, sha.as_ref());
348

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

Makoto Nakashima's avatar
Makoto Nakashima committed
351
352
353
354
        info.refname.map(|v| params.push(("ref", v)));
        info.name.map(|v| params.push(("name", v)));
        info.target_url.map(|v| params.push(("target_url", v)));
        info.description.map(|v| params.push(("description", v)));
355

356
        self.post_with_param(path, &params)
357
358
    }

Ben Boeckel's avatar
Ben Boeckel committed
359
    /// Get the issues for a project.
Ben Boeckel's avatar
Ben Boeckel committed
360
    pub fn issues(&self, project: ProjectId) -> Result<Vec<Issue>> {
361
        self.get_paged(&format!("projects/{}/issues", project))
Ben Boeckel's avatar
Ben Boeckel committed
362
363
364
    }

    /// Get issues.
Brad King's avatar
Brad King committed
365
    pub fn issue(&self, project: ProjectId, issue: IssueInternalId) -> Result<Issue> {
366
        self.get(&format!("projects/{}/issues/{}", project, issue))
Ben Boeckel's avatar
Ben Boeckel committed
367
368
369
    }

    /// Get the notes from a issue.
Brad King's avatar
Brad King committed
370
    pub fn issue_notes(&self, project: ProjectId, issue: IssueInternalId) -> Result<Vec<Note>> {
371
        self.get_paged(&format!("projects/{}/issues/{}/notes", project, issue))
Ben Boeckel's avatar
Ben Boeckel committed
372
373
374
    }

    /// Create a note on a issue.
375
376
377
378
    pub fn create_issue_note<C>(&self, project: ProjectId, issue: IssueInternalId, content: C)
                                -> Result<Note>
        where C: AsRef<str>,
    {
Ben Boeckel's avatar
Ben Boeckel committed
379
        let path = &format!("projects/{}/issues/{}/notes", project, issue);
Ben Boeckel's avatar
Ben Boeckel committed
380

381
        self.post_with_param(path, &[("body", content.as_ref())])
Ben Boeckel's avatar
Ben Boeckel committed
382
383
    }

384
    /// Get the merge requests for a project.
Ben Boeckel's avatar
Ben Boeckel committed
385
    pub fn merge_requests(&self, project: ProjectId) -> Result<Vec<MergeRequest>> {
386
        self.get_paged(&format!("projects/{}/merge_requests", project))
387
388
    }

389
    /// Get the merge requests with a given state.
Ben Boeckel's avatar
Ben Boeckel committed
390
    pub fn merge_requests_with_state(&self, project: ProjectId, state: MergeRequestStateFilter)
Ben Boeckel's avatar
Ben Boeckel committed
391
                                     -> Result<Vec<MergeRequest>> {
392
393
        self.get_paged_with_param(&format!("projects/{}/merge_requests", project),
                                  &[("state", state.as_str())])
394
395
    }

396
    /// Get merge requests.
Brad King's avatar
Brad King committed
397
    pub fn merge_request(&self, project: ProjectId, merge_request: MergeRequestInternalId)
Ben Boeckel's avatar
Ben Boeckel committed
398
                         -> Result<MergeRequest> {
399
        self.get(&format!("projects/{}/merge_requests/{}", project, merge_request))
400
401
    }

402
    /// Get the issues that will be closed when a merge request is merged.
Brad King's avatar
Brad King committed
403
    pub fn merge_request_closes_issues(&self, project: ProjectId, merge_request: MergeRequestInternalId)
Ben Boeckel's avatar
Ben Boeckel committed
404
                                       -> Result<Vec<IssueReference>> {
405
406
407
        self.get_paged(&format!("projects/{}/merge_requests/{}/closes_issues",
                                project,
                                merge_request))
408
409
    }

410
    /// Get the notes from a merge request.
Brad King's avatar
Brad King committed
411
    pub fn merge_request_notes(&self, project: ProjectId, merge_request: MergeRequestInternalId)
Ben Boeckel's avatar
Ben Boeckel committed
412
                               -> Result<Vec<Note>> {
413
414
415
        self.get_paged(&format!("projects/{}/merge_requests/{}/notes",
                                project,
                                merge_request))
416
417
    }

418
    /// Award a merge request note with an award.
Brad King's avatar
Brad King committed
419
    pub fn award_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestInternalId,
Ben Boeckel's avatar
Ben Boeckel committed
420
                                    note: NoteId, award: &str)
Ben Boeckel's avatar
Ben Boeckel committed
421
                                    -> Result<AwardEmoji> {
422
        let path = &format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
Ben Boeckel's avatar
Ben Boeckel committed
423
424
425
                            project,
                            merge_request,
                            note);
426
        self.post_with_param(path, &[("name", award)])
427
428
    }

429
    /// Get the awards for a merge request.
Brad King's avatar
Brad King committed
430
    pub fn merge_request_awards(&self, project: ProjectId, merge_request: MergeRequestInternalId)
Ben Boeckel's avatar
Ben Boeckel committed
431
                                -> Result<Vec<AwardEmoji>> {
432
        self.get_paged(&format!("projects/{}/merge_requests/{}/award_emoji",
Ben Boeckel's avatar
Ben Boeckel committed
433
434
                                project,
                                merge_request))
435
436
    }

437
    /// Get the awards for a merge request note.
Brad King's avatar
Brad King committed
438
    pub fn merge_request_note_awards(&self, project: ProjectId, merge_request: MergeRequestInternalId,
Ben Boeckel's avatar
Ben Boeckel committed
439
                                     note: NoteId)
Ben Boeckel's avatar
Ben Boeckel committed
440
                                     -> Result<Vec<AwardEmoji>> {
441
442
443
444
        self.get_paged(&format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
                                project,
                                merge_request,
                                note))
445
446
    }

447
    /// Create a note on a merge request.
Brad King's avatar
Brad King committed
448
    pub fn create_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestInternalId,
Ben Boeckel's avatar
Ben Boeckel committed
449
                                     content: &str)
Ben Boeckel's avatar
Ben Boeckel committed
450
                                     -> Result<Note> {
Ben Boeckel's avatar
Ben Boeckel committed
451
452
453
        let path = &format!("projects/{}/merge_requests/{}/notes",
                            project,
                            merge_request);
454
        self.post_with_param(path, &[("body", content)])
455
456
    }

457
458
    /// Get issues closed by a merge request.
    pub fn get_issues_closed_by_merge_request(&self, project: ProjectId,
Brad King's avatar
Brad King committed
459
                                              merge_request: MergeRequestInternalId)
460
461
462
463
                                              -> Result<Vec<Issue>> {
        let path = &format!("projects/{}/merge_requests/{}/closes_issues",
                            project,
                            merge_request);
464
        self.get_paged(path)
465
466
    }

467
    /// Set the labels on an issue.
Brad King's avatar
Brad King committed
468
    pub fn set_issue_labels<I, L>(&self, project: ProjectId, issue: IssueInternalId, labels: I)
469
470
471
472
473
474
475
                                  -> Result<Issue>
        where I: IntoIterator<Item = L>,
              L: Display,
    {
        let path = &format!("projects/{}/issues/{}",
                            project,
                            issue);
476
        self.put_with_param(path, &[("labels", labels.into_iter().join(","))])
477
478
    }

479
480
481
482
483
484
485
486
487
    /// Set the labels on a merge request.
    pub fn set_merge_request_labels<I, L>(&self, project: ProjectId, merge_request: MergeRequestInternalId, labels: I)
                                  -> Result<MergeRequest>
        where I: IntoIterator<Item = L>,
              L: Display,
    {
        let path = &format!("projects/{}/merge_requests/{}",
                            project,
                            merge_request);
488
        self.put_with_param(path, &[("labels", labels.into_iter().join(","))])
489
490
    }

Ben Boeckel's avatar
Ben Boeckel committed
491
    /// Create a URL to an API endpoint.
492
    fn create_url(&self, url: &str) -> Result<Url> {
493
        debug!(target: "gitlab", "api call {}", url);
494
        self.base_url.join(url).chain_err(|| ErrorKind::UrlParse)
495
496
    }

Ben Boeckel's avatar
Ben Boeckel committed
497
    /// Create a URL to an API endpoint with query parameters.
498
    fn create_url_with_param<I, K, V>(&self, url: &str, param: I) -> Result<Url>
Makoto Nakashima's avatar
Makoto Nakashima committed
499
500
501
502
503
        where I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
504
        let mut full_url = self.create_url(url)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
505
506
        full_url.query_pairs_mut().extend_pairs(param);
        Ok(full_url)
507
508
    }

Ben Boeckel's avatar
Ben Boeckel committed
509
    /// Refactored code which talks to Gitlab and transforms error messages properly.
510
    fn send<T>(&self, mut req: RequestBuilder) -> Result<T>
511
        where T: DeserializeOwned,
512
    {
513
        req.header(GitlabPrivateToken(self.token.to_string()));
514
        let rsp = req.send().chain_err(|| ErrorKind::Communication)?;
515
516
517
        let success = rsp.status().is_success();
        let v = serde_json::from_reader(rsp).chain_err(|| ErrorKind::Deserialize)?;
        if !success {
Makoto Nakashima's avatar
Makoto Nakashima committed
518
            return Err(Error::from_gitlab(v));
519
520
        }

Makoto Nakashima's avatar
Makoto Nakashima committed
521
522
523
        debug!(target: "gitlab",
               "received data: {:?}",
               v);
524
        serde_json::from_value::<T>(v).chain_err(|| ErrorKind::Deserialize)
525
526
    }

Ben Boeckel's avatar
Ben Boeckel committed
527
    /// Create a `GET` request to an API endpoint.
528
    fn get<T>(&self, url: &str) -> Result<T>
529
530
        where T: DeserializeOwned,
    {
Makoto Nakashima's avatar
Makoto Nakashima committed
531
        let param: &[(&str, &str)] = &[];
532
        self.get_with_param(url, param)
533
534
    }

Ben Boeckel's avatar
Ben Boeckel committed
535
    /// Create a `GET` request to an API endpoint with query parameters.
536
    fn get_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<T>
537
        where T: DeserializeOwned,
Makoto Nakashima's avatar
Makoto Nakashima committed
538
539
540
541
542
              I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
543
        let full_url = self.create_url_with_param(url, param)?;
Ben Boeckel's avatar
Ben Boeckel committed
544
        let req = self.client.get(full_url);
545
        self.send(req)
546
547
    }

Ben Boeckel's avatar
Ben Boeckel committed
548
    /// Create a `POST` request to an API endpoint with query parameters.
549
    fn post_with_param<T, U>(&self, url: &str, param: U) -> Result<T>
550
        where T: DeserializeOwned,
Makoto Nakashima's avatar
Makoto Nakashima committed
551
552
              U: Serialize,
    {
553
        let full_url = self.create_url(url)?;
Ben Boeckel's avatar
Ben Boeckel committed
554
555
        let mut req = self.client.post(full_url);
        req.form(&param);
556
        self.send(req)
557
558
    }

559
    /// Create a `PUT` request to an API endpoint with query parameters.
560
    fn put_with_param<T, U>(&self, url: &str, param: U) -> Result<T>
561
        where T: DeserializeOwned,
562
563
              U: Serialize,
    {
564
        let full_url = self.create_url(url)?;
Ben Boeckel's avatar
Ben Boeckel committed
565
566
        let mut req = self.client.request(Method::Put, full_url);
        req.form(&param);
567
        self.send(req)
568
569
    }

Ben Boeckel's avatar
Ben Boeckel committed
570
    /// Handle paginated queries. Returns all results.
571
    fn get_paged<T>(&self, url: &str) -> Result<Vec<T>>
572
573
        where T: DeserializeOwned,
    {
Makoto Nakashima's avatar
Makoto Nakashima committed
574
        let param: &[(&str, &str)] = &[];
575
        self.get_paged_with_param(url, param)
576
577
    }

Ben Boeckel's avatar
Ben Boeckel committed
578
    /// Handle paginated queries with query parameters. Returns all results.
579
    fn get_paged_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<Vec<T>>
580
        where T: DeserializeOwned,
Makoto Nakashima's avatar
Makoto Nakashima committed
581
582
583
584
585
              I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
586
        let mut page_num = 1;
587
588
589
        let per_page = 100;
        let per_page_str = &format!("{}", per_page);

590
        let full_url = self.create_url_with_param(url, param)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
591

592
        let mut results: Vec<T> = vec![];
593
594

        loop {
595
            let page_str = &format!("{}", page_num);
Makoto Nakashima's avatar
Makoto Nakashima committed
596
597
598
            let mut page_url = full_url.clone();
            page_url.query_pairs_mut()
                .extend_pairs(&[("page", page_str), ("per_page", per_page_str)]);
Ben Boeckel's avatar
Ben Boeckel committed
599
            let req = self.client.get(page_url);
600

601
            let page: Vec<T> = self.send(req)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
602
603
            let page_len = page.len();
            results.extend(page);
604

605
606
607
608
            // Gitlab used to have issues returning paginated results; these have been fixed since,
            // but if it is needed, the bug manifests as Gitlab returning *all* results instead of
            // just the requested results. This can cause an infinite loop here if the number of
            // total results is exactly equal to `per_page`.
609
            if page_len != per_page {
610
                break;
611
            }
612
            page_num += 1;
613
614
        }

615
        Ok(results)
616
617
    }
}
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648

pub struct GitlabBuilder {
    protocol: &'static str,
    host: String,
    token: String,
}

impl GitlabBuilder {
    /// Create a new Gitlab API client builder.
    pub fn new<H, T>(host: H, token: T) -> Self
        where H: ToString,
              T: ToString,
    {
        Self {
            protocol: "https",
            host: host.to_string(),
            token: token.to_string(),
        }
    }

    /// Switch to an insecure protocol (http instead of https).
    pub fn insecure(&mut self) -> &mut Self
    {
        self.protocol = "http";
        self
    }

    pub fn build(&self) -> Result<Gitlab> {
        Gitlab::new_impl(self.protocol, &self.host, self.token.clone())
    }
}