gitlab.rs 22.2 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
    }

Ben Boeckel's avatar
Ben Boeckel committed
115
    /// The user the API is acting as.
116
    pub fn current_user(&self) -> Result<UserPublic> {
117
        self.get("user")
118
119
    }

120
    /// Get all user accounts
Ben Boeckel's avatar
Ben Boeckel committed
121
122
123
    pub fn users<T>(&self) -> Result<Vec<T>>
        where T: UserResult,
    {
124
        self.get_paged("users")
125
126
127
    }

    /// Find a user by id.
Ben Boeckel's avatar
Ben Boeckel committed
128
129
130
    pub fn user<T>(&self, user: UserId) -> Result<T>
        where T: UserResult,
    {
131
        self.get(&format!("users/{}", user))
132
133
    }

Ben Boeckel's avatar
Ben Boeckel committed
134
    /// Find a user by username.
135
136
137
138
139
    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())])?;
140
        users.pop()
Ben Boeckel's avatar
Ben Boeckel committed
141
            .ok_or_else(|| Error::from_kind(ErrorKind::Gitlab("no such user".to_string())))
142
143
    }

144
    /// Get all accessible projects.
Ben Boeckel's avatar
Ben Boeckel committed
145
    pub fn projects(&self) -> Result<Vec<Project>> {
146
        self.get_paged("projects")
147
148
149
    }

    /// Get all owned projects.
Ben Boeckel's avatar
Ben Boeckel committed
150
    pub fn owned_projects(&self) -> Result<Vec<Project>> {
151
        self.get_paged_with_param("projects", &[("owned", "true")])
152
153
154
    }

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

Ben Boeckel's avatar
Ben Boeckel committed
159
    /// Find a project by name.
160
161
162
    pub fn project_by_name<N>(&self, name: N) -> Result<Project>
        where N: AsRef<str>,
    {
163
        self.get(&format!("projects/{}",
164
                          percent_encode(name.as_ref().as_bytes(), PATH_SEGMENT_ENCODE_SET)))
165
166
    }

167
    /// Get a project's hooks.
168
    pub fn hooks(&self, project: ProjectId) -> Result<Vec<ProjectHook>> {
169
        self.get_paged(&format!("projects/{}/hooks", project))
170
    }
171

172
    /// Get a project hook.
173
    pub fn hook(&self, project: ProjectId, hook: HookId) -> Result<ProjectHook> {
174
        self.get(&format!("projects/{}/hooks/{}", project, hook))
175
176
    }

Ben Boeckel's avatar
Ben Boeckel committed
177
    /// Convert a boolean parameter into an HTTP request value.
Ben Boeckel's avatar
Ben Boeckel committed
178
179
180
181
182
183
184
185
    fn bool_param_value(value: bool) -> &'static str {
        if value {
            "true"
        } else {
            "false"
        }
    }

Ben Boeckel's avatar
Ben Boeckel committed
186
    /// HTTP parameters required to register to a project.
Makoto Nakashima's avatar
Makoto Nakashima committed
187
    fn event_flags(events: WebhookEvents) -> Vec<(&'static str, &'static str)> {
Brad King's avatar
Brad King committed
188
        vec![("job_events", Self::bool_param_value(events.job())),
Makoto Nakashima's avatar
Makoto Nakashima committed
189
190
191
192
193
194
             ("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
195
196
197
    }

    /// Add a project hook.
198
199
200
    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
201
        let mut flags = Self::event_flags(events);
202
        flags.push(("url", url.as_ref()));
Ben Boeckel's avatar
Ben Boeckel committed
203

204
        self.post_with_param(&format!("projects/{}/hooks", project), &flags)
Ben Boeckel's avatar
Ben Boeckel committed
205
206
    }

207
    /// Get the team members of a group.
Ben Boeckel's avatar
Ben Boeckel committed
208
    pub fn group_members(&self, group: GroupId) -> Result<Vec<Member>> {
209
        self.get_paged(&format!("groups/{}/members", group))
210
211
212
    }

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

217
    /// Get the team members of a project.
Ben Boeckel's avatar
Ben Boeckel committed
218
    pub fn project_members(&self, project: ProjectId) -> Result<Vec<Member>> {
219
        self.get_paged(&format!("projects/{}/members", project))
220
221
222
    }

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

227
228
    /// 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
229
                               -> Result<Member> {
230
231
232
        let user_str = format!("{}", user);
        let access_str = format!("{}", access);

233
234
        self.post_with_param(&format!("projects/{}/members", project),
                             &[("user", &user_str), ("access", &access_str)])
235
236
237
    }

    /// Get branches for a project.
Ben Boeckel's avatar
Ben Boeckel committed
238
    pub fn branches(&self, project: ProjectId) -> Result<Vec<RepoBranch>> {
239
        self.get_paged(&format!("projects/{}/branches", project))
240
241
242
    }

    /// Get a branch.
243
244
245
    pub fn branch<B>(&self, project: ProjectId, branch: B) -> Result<RepoBranch>
        where B: AsRef<str>,
    {
246
247
        self.get(&format!("projects/{}/repository/branches/{}",
                          project,
248
                          percent_encode(branch.as_ref().as_bytes(), PATH_SEGMENT_ENCODE_SET)))
249
250
251
    }

    /// Get a commit.
252
253
254
255
    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()))
256
257
258
    }

    /// Get comments on a commit.
259
260
261
    pub fn commit_comments<C>(&self, project: ProjectId, commit: C) -> Result<Vec<CommitNote>>
        where C: AsRef<str>,
    {
262
263
        self.get_paged(&format!("projects/{}/repository/commits/{}/comments",
                                project,
264
                                commit.as_ref()))
265
266
267
    }

    /// Get comments on a commit.
268
269
270
271
272
    pub fn create_commit_comment<C, B>(&self, project: ProjectId, commit: C, body: B)
                                       -> Result<CommitNote>
        where C: AsRef<str>,
              B: AsRef<str>,
    {
273
274
        self.post_with_param(&format!("projects/{}/repository/commits/{}/comment",
                                      project,
275
276
                                      commit.as_ref()),
                             &[("note", body.as_ref())])
277
278
279
280
    }

    /// 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
281
                                      path: &str, line: u64)
Ben Boeckel's avatar
Ben Boeckel committed
282
                                      -> Result<CommitNote> {
283
        let line_str = format!("{}", line);
Ben Boeckel's avatar
Ben Boeckel committed
284
        let line_type = LineType::New;
285

286
287
288
289
290
291
292
        self.post_with_param(&format!("projects/{}/repository/commits/{}/comment",
                                      project,
                                      commit),
                             &[("note", body),
                               ("path", path),
                               ("line", &line_str),
                               ("line_type", line_type.as_str())])
293
294
    }

295
    /// Get the latest statuses of a commit.
296
297
298
299
    pub fn commit_latest_statuses<C>(&self, project: ProjectId, commit: C)
                                     -> Result<Vec<CommitStatus>>
        where C: AsRef<str>,
    {
300
301
        self.get_paged(&format!("projects/{}/repository/commits/{}/statuses",
                                project,
302
                                commit.as_ref()))
303
304
    }

305
    /// Get the all statuses of a commit.
306
307
308
309
    pub fn commit_all_statuses<C>(&self, project: ProjectId, commit: C)
                                  -> Result<Vec<CommitStatus>>
        where C: AsRef<str>,
    {
310
311
        self.get_paged_with_param(&format!("projects/{}/repository/commits/{}/statuses",
                                           project,
312
                                           commit.as_ref()),
313
                                  &[("all", "true")])
314
315
    }

316
    /// Get the latest builds of a commit.
317
318
319
320
    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
321
322
    }

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

Ben Boeckel's avatar
Ben Boeckel committed
333
    /// Create a status message for a commit.
334
335
336
337
338
339
    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());
340

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

Makoto Nakashima's avatar
Makoto Nakashima committed
343
344
345
346
        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)));
347

348
        self.post_with_param(path, &params)
349
350
    }

Ben Boeckel's avatar
Ben Boeckel committed
351
    /// Get the issues for a project.
Ben Boeckel's avatar
Ben Boeckel committed
352
    pub fn issues(&self, project: ProjectId) -> Result<Vec<Issue>> {
353
        self.get_paged(&format!("projects/{}/issues", project))
Ben Boeckel's avatar
Ben Boeckel committed
354
355
356
    }

    /// Get issues.
Brad King's avatar
Brad King committed
357
    pub fn issue(&self, project: ProjectId, issue: IssueInternalId) -> Result<Issue> {
358
        self.get(&format!("projects/{}/issues/{}", project, issue))
Ben Boeckel's avatar
Ben Boeckel committed
359
360
361
    }

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

    /// Create a note on a issue.
367
368
369
370
    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
371
        let path = &format!("projects/{}/issues/{}/notes", project, issue);
Ben Boeckel's avatar
Ben Boeckel committed
372

373
        self.post_with_param(path, &[("body", content.as_ref())])
Ben Boeckel's avatar
Ben Boeckel committed
374
375
    }

376
    /// Get the merge requests for a project.
Ben Boeckel's avatar
Ben Boeckel committed
377
    pub fn merge_requests(&self, project: ProjectId) -> Result<Vec<MergeRequest>> {
378
        self.get_paged(&format!("projects/{}/merge_requests", project))
379
380
    }

381
    /// Get the merge requests with a given state.
Ben Boeckel's avatar
Ben Boeckel committed
382
    pub fn merge_requests_with_state(&self, project: ProjectId, state: MergeRequestStateFilter)
Ben Boeckel's avatar
Ben Boeckel committed
383
                                     -> Result<Vec<MergeRequest>> {
384
385
        self.get_paged_with_param(&format!("projects/{}/merge_requests", project),
                                  &[("state", state.as_str())])
386
387
    }

388
    /// Get merge requests.
Brad King's avatar
Brad King committed
389
    pub fn merge_request(&self, project: ProjectId, merge_request: MergeRequestInternalId)
Ben Boeckel's avatar
Ben Boeckel committed
390
                         -> Result<MergeRequest> {
391
        self.get(&format!("projects/{}/merge_requests/{}", project, merge_request))
392
393
    }

394
    /// Get the issues that will be closed when a merge request is merged.
Brad King's avatar
Brad King committed
395
    pub fn merge_request_closes_issues(&self, project: ProjectId, merge_request: MergeRequestInternalId)
Ben Boeckel's avatar
Ben Boeckel committed
396
                                       -> Result<Vec<IssueReference>> {
397
398
399
        self.get_paged(&format!("projects/{}/merge_requests/{}/closes_issues",
                                project,
                                merge_request))
400
401
    }

402
    /// Get the notes from a merge request.
Brad King's avatar
Brad King committed
403
    pub fn merge_request_notes(&self, project: ProjectId, merge_request: MergeRequestInternalId)
Ben Boeckel's avatar
Ben Boeckel committed
404
                               -> Result<Vec<Note>> {
405
406
407
        self.get_paged(&format!("projects/{}/merge_requests/{}/notes",
                                project,
                                merge_request))
408
409
    }

410
    /// Award a merge request note with an award.
Brad King's avatar
Brad King committed
411
    pub fn award_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestInternalId,
Ben Boeckel's avatar
Ben Boeckel committed
412
                                    note: NoteId, award: &str)
Ben Boeckel's avatar
Ben Boeckel committed
413
                                    -> Result<AwardEmoji> {
414
        let path = &format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
Ben Boeckel's avatar
Ben Boeckel committed
415
416
417
                            project,
                            merge_request,
                            note);
418
        self.post_with_param(path, &[("name", award)])
419
420
    }

421
    /// Get the awards for a merge request.
Brad King's avatar
Brad King committed
422
    pub fn merge_request_awards(&self, project: ProjectId, merge_request: MergeRequestInternalId)
Ben Boeckel's avatar
Ben Boeckel committed
423
                                -> Result<Vec<AwardEmoji>> {
424
        self.get_paged(&format!("projects/{}/merge_requests/{}/award_emoji",
Ben Boeckel's avatar
Ben Boeckel committed
425
426
                                project,
                                merge_request))
427
428
    }

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

439
    /// Create a note on a merge request.
Brad King's avatar
Brad King committed
440
    pub fn create_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestInternalId,
Ben Boeckel's avatar
Ben Boeckel committed
441
                                     content: &str)
Ben Boeckel's avatar
Ben Boeckel committed
442
                                     -> Result<Note> {
Ben Boeckel's avatar
Ben Boeckel committed
443
444
445
        let path = &format!("projects/{}/merge_requests/{}/notes",
                            project,
                            merge_request);
446
        self.post_with_param(path, &[("body", content)])
447
448
    }

449
450
    /// 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
451
                                              merge_request: MergeRequestInternalId)
452
453
454
455
                                              -> Result<Vec<Issue>> {
        let path = &format!("projects/{}/merge_requests/{}/closes_issues",
                            project,
                            merge_request);
456
        self.get_paged(path)
457
458
    }

459
    /// Set the labels on an issue.
Brad King's avatar
Brad King committed
460
    pub fn set_issue_labels<I, L>(&self, project: ProjectId, issue: IssueInternalId, labels: I)
461
462
463
464
465
466
467
                                  -> Result<Issue>
        where I: IntoIterator<Item = L>,
              L: Display,
    {
        let path = &format!("projects/{}/issues/{}",
                            project,
                            issue);
468
        self.put_with_param(path, &[("labels", labels.into_iter().join(","))])
469
470
    }

471
472
473
474
475
476
477
478
479
    /// 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);
480
        self.put_with_param(path, &[("labels", labels.into_iter().join(","))])
481
482
    }

Ben Boeckel's avatar
Ben Boeckel committed
483
    /// Create a URL to an API endpoint.
484
    fn create_url(&self, url: &str) -> Result<Url> {
485
        debug!(target: "gitlab", "api call {}", url);
486
        self.base_url.join(url).chain_err(|| ErrorKind::UrlParse)
487
488
    }

Ben Boeckel's avatar
Ben Boeckel committed
489
    /// Create a URL to an API endpoint with query parameters.
490
    fn create_url_with_param<I, K, V>(&self, url: &str, param: I) -> Result<Url>
Makoto Nakashima's avatar
Makoto Nakashima committed
491
492
493
494
495
        where I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
496
        let mut full_url = self.create_url(url)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
497
498
        full_url.query_pairs_mut().extend_pairs(param);
        Ok(full_url)
499
500
    }

Ben Boeckel's avatar
Ben Boeckel committed
501
    /// Refactored code which talks to Gitlab and transforms error messages properly.
502
    fn send<T>(&self, mut req: RequestBuilder) -> Result<T>
503
        where T: DeserializeOwned,
504
    {
505
        req.header(GitlabPrivateToken(self.token.to_string()));
506
        let rsp = req.send().chain_err(|| ErrorKind::Communication)?;
507
508
509
        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
510
            return Err(Error::from_gitlab(v));
511
512
        }

Makoto Nakashima's avatar
Makoto Nakashima committed
513
514
515
        debug!(target: "gitlab",
               "received data: {:?}",
               v);
516
        serde_json::from_value::<T>(v).chain_err(|| ErrorKind::Deserialize)
517
518
    }

Ben Boeckel's avatar
Ben Boeckel committed
519
    /// Create a `GET` request to an API endpoint.
520
    fn get<T>(&self, url: &str) -> Result<T>
521
522
        where T: DeserializeOwned,
    {
Makoto Nakashima's avatar
Makoto Nakashima committed
523
        let param: &[(&str, &str)] = &[];
524
        self.get_with_param(url, param)
525
526
    }

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

Ben Boeckel's avatar
Ben Boeckel committed
540
    /// Create a `POST` request to an API endpoint with query parameters.
541
    fn post_with_param<T, U>(&self, url: &str, param: U) -> Result<T>
542
        where T: DeserializeOwned,
Makoto Nakashima's avatar
Makoto Nakashima committed
543
544
              U: Serialize,
    {
545
        let full_url = self.create_url(url)?;
Ben Boeckel's avatar
Ben Boeckel committed
546
547
        let mut req = self.client.post(full_url);
        req.form(&param);
548
        self.send(req)
549
550
    }

551
    /// Create a `PUT` request to an API endpoint with query parameters.
552
    fn put_with_param<T, U>(&self, url: &str, param: U) -> Result<T>
553
        where T: DeserializeOwned,
554
555
              U: Serialize,
    {
556
        let full_url = self.create_url(url)?;
Ben Boeckel's avatar
Ben Boeckel committed
557
558
        let mut req = self.client.request(Method::Put, full_url);
        req.form(&param);
559
        self.send(req)
560
561
    }

Ben Boeckel's avatar
Ben Boeckel committed
562
    /// Handle paginated queries. Returns all results.
563
    fn get_paged<T>(&self, url: &str) -> Result<Vec<T>>
564
565
        where T: DeserializeOwned,
    {
Makoto Nakashima's avatar
Makoto Nakashima committed
566
        let param: &[(&str, &str)] = &[];
567
        self.get_paged_with_param(url, param)
568
569
    }

Ben Boeckel's avatar
Ben Boeckel committed
570
    /// Handle paginated queries with query parameters. Returns all results.
571
    fn get_paged_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<Vec<T>>
572
        where T: DeserializeOwned,
Makoto Nakashima's avatar
Makoto Nakashima committed
573
574
575
576
577
              I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
578
        let mut page_num = 1;
579
580
581
        let per_page = 100;
        let per_page_str = &format!("{}", per_page);

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

584
        let mut results: Vec<T> = vec![];
585
586

        loop {
587
            let page_str = &format!("{}", page_num);
Makoto Nakashima's avatar
Makoto Nakashima committed
588
589
590
            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
591
            let req = self.client.get(page_url);
592

593
            let page: Vec<T> = self.send(req)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
594
595
            let page_len = page.len();
            results.extend(page);
596

597
598
599
600
            // 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`.
601
            if page_len != per_page {
602
                break;
603
            }
604
            page_num += 1;
605
606
        }

607
        Ok(results)
608
609
    }
}