gitlab.rs 19.3 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.

Makoto Nakashima's avatar
Makoto Nakashima committed
9
10
extern crate reqwest;
use self::reqwest::{Client, RequestBuilder, Url};
11

12
extern crate serde;
13
14
use self::serde::{Deserialize, Deserializer, Serialize, Serializer};
use self::serde::de::Error as SerdeError;
15

16
17
18
19
20
extern crate serde_json;

extern crate url;
use self::url::percent_encoding::{PATH_SEGMENT_ENCODE_SET, percent_encode};

Ben Boeckel's avatar
Ben Boeckel committed
21
use super::error::*;
22
use super::types::*;
23

Makoto Nakashima's avatar
Makoto Nakashima committed
24
use std::borrow::Borrow;
Ben Boeckel's avatar
Ben Boeckel committed
25
26
use std::fmt::{self, Debug};

Ben Boeckel's avatar
Ben Boeckel committed
27
28
29
/// A representation of the Gitlab API for a single user.
///
/// Separate users should use separate instances of this.
30
31
32
33
34
pub struct Gitlab {
    base_url: Url,
    token: String,
}

Ben Boeckel's avatar
Ben Boeckel committed
35
impl Debug for Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
36
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Ben Boeckel's avatar
Ben Boeckel committed
37
38
39
40
        write!(f, "Gitlab {{ {} }}", self.base_url)
    }
}

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

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

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

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

74
impl Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
75
76
77
    /// Create a new Gitlab API representation.
    ///
    /// Errors out if `token` is invalid.
Ben Boeckel's avatar
Ben Boeckel committed
78
    pub fn new<T: ToString>(host: &str, token: T) -> Result<Self> {
Ben Boeckel's avatar
Ben Boeckel committed
79
        Self::_new("https", host, token.to_string())
80
81
82
83
84
    }

    /// Create a new non-SSL Gitlab API representation.
    ///
    /// Errors out if `token` is invalid.
Ben Boeckel's avatar
Ben Boeckel committed
85
    pub fn new_insecure<T: ToString>(host: &str, token: T) -> Result<Self> {
Ben Boeckel's avatar
Ben Boeckel committed
86
        Self::_new("http", host, token.to_string())
87
88
    }

Ben Boeckel's avatar
Ben Boeckel committed
89
90
91
    fn _new(protocol: &str, host: &str, token: String) -> Result<Self> {
        let base_url = try!(Url::parse(&format!("{}://{}/api/v3/", protocol, host))
            .chain_err(|| ErrorKind::UrlParse));
Ben Boeckel's avatar
Ben Boeckel committed
92

93
94
        let api = Gitlab {
            base_url: base_url,
95
            token: token,
96
        };
97

98
99
100
        // Ensure the API is working.
        let _: UserFull = try!(api._get("user"));

101
        Ok(api)
102
103
    }

Ben Boeckel's avatar
Ben Boeckel committed
104
    /// The user the API is acting as.
Ben Boeckel's avatar
Ben Boeckel committed
105
    pub fn current_user(&self) -> Result<UserFull> {
106
107
108
        self._get("user")
    }

109
    /// Get all user accounts
Ben Boeckel's avatar
Ben Boeckel committed
110
    pub fn users<T: UserResult>(&self) -> Result<Vec<T>> {
111
112
113
114
        self._get_paged("users")
    }

    /// Find a user by id.
Ben Boeckel's avatar
Ben Boeckel committed
115
    pub fn user<T: UserResult>(&self, user: UserId) -> Result<T> {
116
117
118
        self._get(&format!("users/{}", user))
    }

Ben Boeckel's avatar
Ben Boeckel committed
119
    /// Find a user by username.
Ben Boeckel's avatar
Ben Boeckel committed
120
    pub fn user_by_name<T: UserResult>(&self, name: &str) -> Result<T> {
Makoto Nakashima's avatar
Makoto Nakashima committed
121
        let mut users = try!(self._get_paged_with_param("users", &[("username", name)]));
122
        users.pop()
Ben Boeckel's avatar
Ben Boeckel committed
123
            .ok_or_else(|| Error::from_kind(ErrorKind::Gitlab("no such user".to_string())))
124
125
    }

126
    /// Get all accessible projects.
Ben Boeckel's avatar
Ben Boeckel committed
127
    pub fn projects(&self) -> Result<Vec<Project>> {
128
129
130
131
        self._get_paged("projects")
    }

    /// Get all owned projects.
Ben Boeckel's avatar
Ben Boeckel committed
132
    pub fn owned_projects(&self) -> Result<Vec<Project>> {
133
134
135
136
137
138
        self._get_paged("projects/owned")
    }

    /// Get all projects.
    ///
    /// Requires administrator privileges.
Ben Boeckel's avatar
Ben Boeckel committed
139
    pub fn all_projects(&self) -> Result<Vec<Project>> {
140
141
142
143
        self._get_paged("projects/all")
    }

    /// Find a project by id.
Ben Boeckel's avatar
Ben Boeckel committed
144
    pub fn project(&self, project: ProjectId) -> Result<Project> {
145
        self._get(&format!("projects/{}", project))
146
147
    }

Ben Boeckel's avatar
Ben Boeckel committed
148
    /// Find a project by name.
Ben Boeckel's avatar
Ben Boeckel committed
149
    pub fn project_by_name(&self, name: &str) -> Result<Project> {
150
151
        self._get(&format!("projects/{}",
                           percent_encode(name.as_bytes(), PATH_SEGMENT_ENCODE_SET)))
152
153
    }

154
    /// Get a project's hooks.
Ben Boeckel's avatar
Ben Boeckel committed
155
    pub fn hooks(&self, project: ProjectId) -> Result<Vec<Hook>> {
156
157
        self._get_paged(&format!("projects/{}/hooks", project))
    }
158

159
    /// Get a project hook.
Ben Boeckel's avatar
Ben Boeckel committed
160
    pub fn hook(&self, project: ProjectId, hook: HookId) -> Result<Hook> {
161
162
163
        self._get(&format!("projects/{}/hooks/{}", project, hook))
    }

Ben Boeckel's avatar
Ben Boeckel committed
164
165
166
167
168
169
170
171
    fn bool_param_value(value: bool) -> &'static str {
        if value {
            "true"
        } else {
            "false"
        }
    }

Makoto Nakashima's avatar
Makoto Nakashima committed
172
173
174
175
176
177
178
179
    fn event_flags(events: WebhookEvents) -> Vec<(&'static str, &'static str)> {
        vec![("build_events", Self::bool_param_value(events.build())),
             ("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
180
181
182
    }

    /// Add a project hook.
Ben Boeckel's avatar
Ben Boeckel committed
183
    pub fn add_hook(&self, project: ProjectId, url: &str, events: WebhookEvents)
Ben Boeckel's avatar
Ben Boeckel committed
184
                    -> Result<Hook> {
Makoto Nakashima's avatar
Makoto Nakashima committed
185
186
        let mut flags = Self::event_flags(events);
        flags.push(("url", url));
Ben Boeckel's avatar
Ben Boeckel committed
187

Makoto Nakashima's avatar
Makoto Nakashima committed
188
        self._post_with_param(&format!("projects/{}/hooks", project), &flags)
Ben Boeckel's avatar
Ben Boeckel committed
189
190
    }

191
    /// Get the team members of a group.
Ben Boeckel's avatar
Ben Boeckel committed
192
    pub fn group_members(&self, group: GroupId) -> Result<Vec<Member>> {
193
194
195
196
        self._get_paged(&format!("groups/{}/members", group))
    }

    /// Get a team member of a group.
Ben Boeckel's avatar
Ben Boeckel committed
197
    pub fn group_member(&self, group: GroupId, user: UserId) -> Result<Member> {
198
199
200
        self._get(&format!("groups/{}/members/{}", group, user))
    }

201
    /// Get the team members of a project.
Ben Boeckel's avatar
Ben Boeckel committed
202
    pub fn project_members(&self, project: ProjectId) -> Result<Vec<Member>> {
203
204
205
206
        self._get_paged(&format!("projects/{}/members", project))
    }

    /// Get a team member of a project.
Ben Boeckel's avatar
Ben Boeckel committed
207
    pub fn project_member(&self, project: ProjectId, user: UserId) -> Result<Member> {
208
209
210
        self._get(&format!("projects/{}/members/{}", project, user))
    }

211
212
    /// 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
213
                               -> Result<Member> {
214
215
216
        let user_str = format!("{}", user);
        let access_str = format!("{}", access);

Makoto Nakashima's avatar
Makoto Nakashima committed
217
218
        self._post_with_param(&format!("projects/{}/members", project),
                              &[("user", &user_str), ("access", &access_str)])
219
220
221
    }

    /// Get branches for a project.
Ben Boeckel's avatar
Ben Boeckel committed
222
    pub fn branches(&self, project: ProjectId) -> Result<Vec<RepoBranch>> {
223
224
225
226
        self._get_paged(&format!("projects/{}/branches", project))
    }

    /// Get a branch.
Ben Boeckel's avatar
Ben Boeckel committed
227
    pub fn branch(&self, project: ProjectId, branch: &str) -> Result<RepoBranch> {
228
229
230
        self._get(&format!("projects/{}/repository/branches/{}",
                           project,
                           percent_encode(branch.as_bytes(), PATH_SEGMENT_ENCODE_SET)))
231
232
233
    }

    /// Get a commit.
Ben Boeckel's avatar
Ben Boeckel committed
234
    pub fn commit(&self, project: ProjectId, commit: &str) -> Result<RepoCommitDetail> {
235
        self._get(&format!("projects/{}/repository/commits/{}", project, commit))
236
237
238
    }

    /// Get comments on a commit.
Ben Boeckel's avatar
Ben Boeckel committed
239
    pub fn commit_comments(&self, project: ProjectId, commit: &str)
Ben Boeckel's avatar
Ben Boeckel committed
240
                           -> Result<Vec<CommitNote>> {
Ben Boeckel's avatar
Ben Boeckel committed
241
242
243
        self._get_paged(&format!("projects/{}/repository/commits/{}/comments",
                                 project,
                                 commit))
244
245
246
    }

    /// Get comments on a commit.
Ben Boeckel's avatar
Ben Boeckel committed
247
    pub fn create_commit_comment(&self, project: ProjectId, commit: &str, body: &str)
Ben Boeckel's avatar
Ben Boeckel committed
248
                                 -> Result<CommitNote> {
Makoto Nakashima's avatar
Makoto Nakashima committed
249
250
251
252
        self._post_with_param(&format!("projects/{}/repository/commits/{}/comment",
                                       project,
                                       commit),
                              &[("note", body)])
253
254
255
256
    }

    /// 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
257
                                      path: &str, line: u64)
Ben Boeckel's avatar
Ben Boeckel committed
258
                                      -> Result<CommitNote> {
259
        let line_str = format!("{}", line);
Ben Boeckel's avatar
Ben Boeckel committed
260
        let line_type = LineType::New;
261

Makoto Nakashima's avatar
Makoto Nakashima committed
262
263
264
265
266
267
268
        self._post_with_param(&format!("projects/{}/repository/commits/{}/comment",
                                       project,
                                       commit),
                              &[("note", body),
                                ("path", path),
                                ("line", &line_str),
                                ("line_type", line_type.as_str())])
269
270
    }

271
272
    /// Get the latest statuses of a commit.
    pub fn commit_latest_statuses(&self, project: ProjectId, commit: &str)
Ben Boeckel's avatar
Ben Boeckel committed
273
                                  -> Result<Vec<CommitStatus>> {
Ben Boeckel's avatar
Ben Boeckel committed
274
275
276
        self._get_paged(&format!("projects/{}/repository/commits/{}/statuses",
                                 project,
                                 commit))
277
278
    }

279
    /// Get the all statuses of a commit.
Ben Boeckel's avatar
Ben Boeckel committed
280
    pub fn commit_all_statuses(&self, project: ProjectId, commit: &str)
Ben Boeckel's avatar
Ben Boeckel committed
281
                               -> Result<Vec<CommitStatus>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
282
283
284
285
        self._get_paged_with_param(&format!("projects/{}/repository/commits/{}/statuses",
                                            project,
                                            commit),
                                   &[("all", "true")])
286
287
    }

288
    /// Get the latest builds of a commit.
Ben Boeckel's avatar
Ben Boeckel committed
289
    pub fn commit_latest_builds(&self, project: ProjectId, commit: &str)
Ben Boeckel's avatar
Ben Boeckel committed
290
                                -> Result<Vec<Build>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
291
292
293
        self._get_paged(&format!("projects/{}/repository/commits/{}/builds", project, commit))
    }

294
    /// Get the all builds of a commit.
Ben Boeckel's avatar
Ben Boeckel committed
295
    pub fn commit_all_builds(&self, project: ProjectId, commit: &str) -> Result<Vec<Build>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
296
297
298
299
        self._get_paged_with_param(&format!("projects/{}/repository/commits/{}/builds",
                                            project,
                                            commit),
                                   &[("all", "true")])
Makoto Nakashima's avatar
Makoto Nakashima committed
300
301
    }

Ben Boeckel's avatar
Ben Boeckel committed
302
    /// Create a status message for a commit.
Ben Boeckel's avatar
Ben Boeckel committed
303
    pub fn create_commit_status(&self, project: ProjectId, sha: &str, state: StatusState,
Ben Boeckel's avatar
Ben Boeckel committed
304
                                info: &CommitStatusInfo)
Ben Boeckel's avatar
Ben Boeckel committed
305
                                -> Result<CommitStatus> {
306
        let path = &format!("projects/{}/statuses/{}", project, sha);
307

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

Makoto Nakashima's avatar
Makoto Nakashima committed
310
311
312
313
        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)));
314

Makoto Nakashima's avatar
Makoto Nakashima committed
315
        self._post_with_param(path, &params)
316
317
    }

Ben Boeckel's avatar
Ben Boeckel committed
318
    /// Get the issues for a project.
Ben Boeckel's avatar
Ben Boeckel committed
319
    pub fn issues(&self, project: ProjectId) -> Result<Vec<Issue>> {
Ben Boeckel's avatar
Ben Boeckel committed
320
321
322
323
        self._get_paged(&format!("projects/{}/issues", project))
    }

    /// Get issues.
Ben Boeckel's avatar
Ben Boeckel committed
324
    pub fn issue(&self, project: ProjectId, issue: IssueId) -> Result<Issue> {
Ben Boeckel's avatar
Ben Boeckel committed
325
326
327
328
        self._get(&format!("projects/{}/issues/{}", project, issue))
    }

    /// Get the notes from a issue.
Ben Boeckel's avatar
Ben Boeckel committed
329
    pub fn issue_notes(&self, project: ProjectId, issue: IssueId) -> Result<Vec<Note>> {
Ben Boeckel's avatar
Ben Boeckel committed
330
        self._get_paged(&format!("projects/{}/issues/{}/notes", project, issue))
Ben Boeckel's avatar
Ben Boeckel committed
331
332
333
    }

    /// Create a note on a issue.
Ben Boeckel's avatar
Ben Boeckel committed
334
    pub fn create_issue_note(&self, project: ProjectId, issue: IssueId, content: &str)
Ben Boeckel's avatar
Ben Boeckel committed
335
                             -> Result<Note> {
Ben Boeckel's avatar
Ben Boeckel committed
336
        let path = &format!("projects/{}/issues/{}/notes", project, issue);
Ben Boeckel's avatar
Ben Boeckel committed
337

Makoto Nakashima's avatar
Makoto Nakashima committed
338
        self._post_with_param(path, &[("body", content)])
Ben Boeckel's avatar
Ben Boeckel committed
339
340
    }

341
    /// Get the merge requests for a project.
Ben Boeckel's avatar
Ben Boeckel committed
342
    pub fn merge_requests(&self, project: ProjectId) -> Result<Vec<MergeRequest>> {
343
344
345
        self._get_paged(&format!("projects/{}/merge_requests", project))
    }

346
    /// Get the merge requests with a given state.
Ben Boeckel's avatar
Ben Boeckel committed
347
    pub fn merge_requests_with_state(&self, project: ProjectId, state: MergeRequestStateFilter)
Ben Boeckel's avatar
Ben Boeckel committed
348
                                     -> Result<Vec<MergeRequest>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
349
350
        self._get_paged_with_param(&format!("projects/{}/merge_requests", project),
                                   &[("state", state.as_str())])
351
352
    }

353
    /// Get merge requests.
Ben Boeckel's avatar
Ben Boeckel committed
354
    pub fn merge_request(&self, project: ProjectId, merge_request: MergeRequestId)
Ben Boeckel's avatar
Ben Boeckel committed
355
                         -> Result<MergeRequest> {
356
357
358
        self._get(&format!("projects/{}/merge_requests/{}", project, merge_request))
    }

359
360
    /// Get the issues that will be closed when a merge request is merged.
    pub fn merge_request_closes_issues(&self, project: ProjectId, merge_request: MergeRequestId)
Ben Boeckel's avatar
Ben Boeckel committed
361
                                       -> Result<Vec<IssueReference>> {
362
363
364
365
366
        self._get_paged(&format!("projects/{}/merge_requests/{}/closes_issues",
                                 project,
                                 merge_request))
    }

367
    /// Get the notes from a merge request.
Ben Boeckel's avatar
Ben Boeckel committed
368
    pub fn merge_request_notes(&self, project: ProjectId, merge_request: MergeRequestId)
Ben Boeckel's avatar
Ben Boeckel committed
369
                               -> Result<Vec<Note>> {
Ben Boeckel's avatar
Ben Boeckel committed
370
371
372
        self._get_paged(&format!("projects/{}/merge_requests/{}/notes",
                                 project,
                                 merge_request))
373
374
    }

375
376
    /// Award a merge request note with an award.
    pub fn award_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestId,
Ben Boeckel's avatar
Ben Boeckel committed
377
                                    note: NoteId, award: &str)
Ben Boeckel's avatar
Ben Boeckel committed
378
                                    -> Result<AwardEmoji> {
379
        let path = &format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
Ben Boeckel's avatar
Ben Boeckel committed
380
381
382
                            project,
                            merge_request,
                            note);
Makoto Nakashima's avatar
Makoto Nakashima committed
383
        self._post_with_param(path, &[("name", award)])
384
385
    }

386
387
    /// Get the awards for a merge request.
    pub fn merge_request_awards(&self, project: ProjectId, merge_request: MergeRequestId)
Ben Boeckel's avatar
Ben Boeckel committed
388
                                -> Result<Vec<AwardEmoji>> {
389
390
391
392
393
        self._get_paged(&format!("projects/{}/merge_requests/{}/award_emoji",
                                 project,
                                 merge_request))
    }

394
395
    /// Get the awards for a merge request note.
    pub fn merge_request_note_awards(&self, project: ProjectId, merge_request: MergeRequestId,
Ben Boeckel's avatar
Ben Boeckel committed
396
                                     note: NoteId)
Ben Boeckel's avatar
Ben Boeckel committed
397
                                     -> Result<Vec<AwardEmoji>> {
398
399
400
401
402
403
        self._get_paged(&format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
                                 project,
                                 merge_request,
                                 note))
    }

404
    /// Create a note on a merge request.
Ben Boeckel's avatar
Ben Boeckel committed
405
406
    pub fn create_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestId,
                                     content: &str)
Ben Boeckel's avatar
Ben Boeckel committed
407
                                     -> Result<Note> {
Ben Boeckel's avatar
Ben Boeckel committed
408
409
410
        let path = &format!("projects/{}/merge_requests/{}/notes",
                            project,
                            merge_request);
Makoto Nakashima's avatar
Makoto Nakashima committed
411
        self._post_with_param(path, &[("body", content)])
412
413
    }

Ben Boeckel's avatar
Ben Boeckel committed
414
    fn _mk_url(&self, url: &str) -> Result<Url> {
415
        debug!(target: "gitlab", "api call {}", url);
Ben Boeckel's avatar
Ben Boeckel committed
416
        Ok(try!(self.base_url.join(url).chain_err(|| ErrorKind::UrlParse)))
417
418
    }

Ben Boeckel's avatar
Ben Boeckel committed
419
    fn _mk_url_with_param<I, K, V>(&self, url: &str, param: I) -> Result<Url>
Makoto Nakashima's avatar
Makoto Nakashima committed
420
421
422
423
424
425
426
427
        where I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
        let mut full_url = try!(self._mk_url(url));
        full_url.query_pairs_mut().extend_pairs(param);
        Ok(full_url)
428
429
    }

Ben Boeckel's avatar
Ben Boeckel committed
430
    // Refactored code which talks to Gitlab and transforms error messages properly.
Ben Boeckel's avatar
Ben Boeckel committed
431
    fn _comm<T>(&self, req: RequestBuilder) -> Result<T>
Makoto Nakashima's avatar
Makoto Nakashima committed
432
        where T: Deserialize,
433
    {
Makoto Nakashima's avatar
Makoto Nakashima committed
434
        let req = req.header(GitlabPrivateToken(self.token.to_string()));
Ben Boeckel's avatar
Ben Boeckel committed
435
        let rsp = try!(req.send().chain_err(|| ErrorKind::Communication));
Makoto Nakashima's avatar
Makoto Nakashima committed
436
        if !rsp.status().is_success() {
Ben Boeckel's avatar
Ben Boeckel committed
437
            let v = try!(serde_json::from_reader(rsp).chain_err(|| ErrorKind::Deserialize));
Makoto Nakashima's avatar
Makoto Nakashima committed
438
            return Err(Error::from_gitlab(v));
439
440
        }

Ben Boeckel's avatar
Ben Boeckel committed
441
        let v = try!(serde_json::from_reader(rsp).chain_err(|| ErrorKind::Deserialize));
Makoto Nakashima's avatar
Makoto Nakashima committed
442
443
444
        debug!(target: "gitlab",
               "received data: {:?}",
               v);
Ben Boeckel's avatar
Ben Boeckel committed
445
        Ok(try!(serde_json::from_value::<T>(v).chain_err(|| ErrorKind::Deserialize)))
446
447
    }

Ben Boeckel's avatar
Ben Boeckel committed
448
    fn _get<T: Deserialize>(&self, url: &str) -> Result<T> {
Makoto Nakashima's avatar
Makoto Nakashima committed
449
450
        let param: &[(&str, &str)] = &[];
        self._get_with_param(url, param)
451
452
    }

Ben Boeckel's avatar
Ben Boeckel committed
453
    fn _get_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<T>
Makoto Nakashima's avatar
Makoto Nakashima committed
454
455
456
457
458
459
460
        where T: Deserialize,
              I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
        let full_url = try!(self._mk_url_with_param(url, param));
Ben Boeckel's avatar
Ben Boeckel committed
461
        let req = try!(Client::new().chain_err(|| ErrorKind::Communication)).get(full_url);
Makoto Nakashima's avatar
Makoto Nakashima committed
462
        self._comm(req)
463
464
    }

Ben Boeckel's avatar
Ben Boeckel committed
465
    fn _post<T: Deserialize>(&self, url: &str) -> Result<T> {
Makoto Nakashima's avatar
Makoto Nakashima committed
466
467
        let param: &[(&str, &str)] = &[];
        self._post_with_param(url, param)
468
469
    }

Ben Boeckel's avatar
Ben Boeckel committed
470
    fn _post_with_param<T, U>(&self, url: &str, param: U) -> Result<T>
Makoto Nakashima's avatar
Makoto Nakashima committed
471
472
473
474
        where T: Deserialize,
              U: Serialize,
    {
        let full_url = try!(self._mk_url(url));
Ben Boeckel's avatar
Ben Boeckel committed
475
        let req = try!(Client::new().chain_err(|| ErrorKind::Communication)).post(full_url).form(&param);
Makoto Nakashima's avatar
Makoto Nakashima committed
476
        self._comm(req)
477
478
    }

Makoto Nakashima's avatar
Makoto Nakashima committed
479
    // Handle paginated queries. Returns all results.
Ben Boeckel's avatar
Ben Boeckel committed
480
    fn _get_paged<T: Deserialize>(&self, url: &str) -> Result<Vec<T>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
481
482
        let param: &[(&str, &str)] = &[];
        self._get_paged_with_param(url, param)
483
484
    }

Ben Boeckel's avatar
Ben Boeckel committed
485
    fn _get_paged_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<Vec<T>>
Makoto Nakashima's avatar
Makoto Nakashima committed
486
487
488
489
490
491
        where T: Deserialize,
              I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
492
        let mut page_num = 1;
493
494
495
        let per_page = 100;
        let per_page_str = &format!("{}", per_page);

Makoto Nakashima's avatar
Makoto Nakashima committed
496
497
        let full_url = try!(self._mk_url_with_param(url, param));

498
        let mut results: Vec<T> = vec![];
499
500

        loop {
501
            let page_str = &format!("{}", page_num);
Makoto Nakashima's avatar
Makoto Nakashima committed
502
503
504
            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
505
            let req = try!(Client::new().chain_err(|| ErrorKind::Communication)).get(page_url);
506

Makoto Nakashima's avatar
Makoto Nakashima committed
507
508
509
            let page: Vec<T> = try!(self._comm(req));
            let page_len = page.len();
            results.extend(page);
510

511
512
513
514
            // 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`.
515
            if page_len != per_page {
516
                break;
517
            }
518
            page_num += 1;
519
520
        }

521
        Ok(results)
522
523
    }
}