gitlab.rs 19.9 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
extern crate ease;
use self::ease::Error as EaseError;
use self::ease::{Request, Response, Url};

13
14
15
extern crate serde;
use self::serde::Deserialize;

16
17
18
19
20
extern crate serde_json;

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

21
use super::error::Error;
22
use super::types::*;
23

Ben Boeckel's avatar
Ben Boeckel committed
24
25
use std::fmt::{self, Debug};

Ben Boeckel's avatar
Ben Boeckel committed
26
#[derive(Clone)]
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
36
37
38
39
40
impl Debug for Gitlab {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        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
/// A JSON value return from Gitlab.
45
pub type GitlabResult<T> = Result<T, Error>;
46

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
impl Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
61
62
63
    /// Create a new Gitlab API representation.
    ///
    /// Errors out if `token` is invalid.
Ben Boeckel's avatar
Ben Boeckel committed
64
    pub fn new<T: ToString>(host: &str, token: T) -> GitlabResult<Self> {
Ben Boeckel's avatar
Ben Boeckel committed
65
        Self::_new("https", host, token.to_string())
66
67
68
69
70
71
    }

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

Ben Boeckel's avatar
Ben Boeckel committed
75
76
77
    fn _new(protocol: &str, host: &str, token: String) -> GitlabResult<Self> {
        let base_url = try!(Url::parse(&format!("{}://{}/api/v3/", protocol, host)));

78
79
        let api = Gitlab {
            base_url: base_url,
80
            token: token,
81
        };
82

83
84
85
        // Ensure the API is working.
        let _: UserFull = try!(api._get("user"));

86
        Ok(api)
87
88
    }

Ben Boeckel's avatar
Ben Boeckel committed
89
    /// The user the API is acting as.
90
    pub fn current_user(&self) -> GitlabResult<UserFull> {
91
92
93
        self._get("user")
    }

94
95
96
97
98
99
100
101
102
103
    /// Get all user accounts
    pub fn users<T: UserResult>(&self) -> GitlabResult<Vec<T>> {
        self._get_paged("users")
    }

    /// Find a user by id.
    pub fn user<T: UserResult>(&self, user: UserId) -> GitlabResult<T> {
        self._get(&format!("users/{}", user))
    }

Ben Boeckel's avatar
Ben Boeckel committed
104
    /// Find a user by username.
105
    pub fn user_by_name<T: UserResult>(&self, name: &str) -> GitlabResult<T> {
Ben Boeckel's avatar
Ben Boeckel committed
106
107
108
109
110
111
        let mut req = try!(self._mkrequest("users"));

        req.param("username", name);

        let mut users = try!(Self::_get_paged_req(req));

112
        users.pop()
113
            .ok_or_else(|| Error::Gitlab("no such user".to_string()))
114
115
    }

116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
    /// Get all accessible projects.
    pub fn projects(&self) -> GitlabResult<Vec<Project>> {
        self._get_paged("projects")
    }

    /// Get all owned projects.
    pub fn owned_projects(&self) -> GitlabResult<Vec<Project>> {
        self._get_paged("projects/owned")
    }

    /// Get all projects.
    ///
    /// Requires administrator privileges.
    pub fn all_projects(&self) -> GitlabResult<Vec<Project>> {
        self._get_paged("projects/all")
    }

    /// Find a project by id.
    pub fn project(&self, project: ProjectId) -> GitlabResult<Project> {
        self._get(&format!("projects/{}", project))
136
137
    }

Ben Boeckel's avatar
Ben Boeckel committed
138
    /// Find a project by name.
139
    pub fn project_by_name(&self, name: &str) -> GitlabResult<Project> {
140
141
        self._get(&format!("projects/{}",
                           percent_encode(name.as_bytes(), PATH_SEGMENT_ENCODE_SET)))
142
143
    }

144
145
146
147
    /// Get a project's hooks.
    pub fn hooks(&self, project: ProjectId) -> GitlabResult<Vec<Hook>> {
        self._get_paged(&format!("projects/{}/hooks", project))
    }
148

149
150
151
152
153
    /// Get a project hook.
    pub fn hook(&self, project: ProjectId, hook: HookId) -> GitlabResult<Hook> {
        self._get(&format!("projects/{}/hooks/{}", project, hook))
    }

Ben Boeckel's avatar
Ben Boeckel committed
154
155
156
157
158
159
160
161
162
163
164
165
    fn bool_param_value(value: bool) -> &'static str {
        if value {
            "true"
        } else {
            "false"
        }
    }

    fn set_event_flags(request: &mut Request, events: WebhookEvents) {
        request
            .param("build_events", Self::bool_param_value(events.build()))
            .param("issues_events", Self::bool_param_value(events.issues()))
Ben Boeckel's avatar
Ben Boeckel committed
166
167
            .param("merge_requests_events",
                   Self::bool_param_value(events.merge_requests()))
Ben Boeckel's avatar
Ben Boeckel committed
168
169
170
            .param("note_events", Self::bool_param_value(events.note()))
            .param("pipeline_events", Self::bool_param_value(events.pipeline()))
            .param("push_events", Self::bool_param_value(events.push()))
Ben Boeckel's avatar
Ben Boeckel committed
171
172
            .param("wiki_page_events",
                   Self::bool_param_value(events.wiki_page()));
Ben Boeckel's avatar
Ben Boeckel committed
173
174
175
    }

    /// Add a project hook.
Ben Boeckel's avatar
Ben Boeckel committed
176
177
    pub fn add_hook(&self, project: ProjectId, url: &str, events: WebhookEvents)
                    -> GitlabResult<Hook> {
Ben Boeckel's avatar
Ben Boeckel committed
178
179
180
        let mut req = try!(self._mkrequest(&format!("projects/{}/hooks", project)));
        Self::set_event_flags(&mut req, events);

181
182
        req.param("url", url);

183
        Self::_post_req(req)
Ben Boeckel's avatar
Ben Boeckel committed
184
185
    }

186
187
188
189
190
191
    /// Get the team members of a group.
    pub fn group_members(&self, group: GroupId) -> GitlabResult<Vec<Member>> {
        self._get_paged(&format!("groups/{}/members", group))
    }

    /// Get a team member of a group.
Ben Boeckel's avatar
Ben Boeckel committed
192
    pub fn group_member(&self, group: GroupId, user: UserId) -> GitlabResult<Member> {
193
194
195
        self._get(&format!("groups/{}/members/{}", group, user))
    }

196
    /// Get the team members of a project.
197
    pub fn project_members(&self, project: ProjectId) -> GitlabResult<Vec<Member>> {
198
199
200
201
        self._get_paged(&format!("projects/{}/members", project))
    }

    /// Get a team member of a project.
Ben Boeckel's avatar
Ben Boeckel committed
202
    pub fn project_member(&self, project: ProjectId, user: UserId) -> GitlabResult<Member> {
203
204
205
        self._get(&format!("projects/{}/members/{}", project, user))
    }

206
207
208
    /// Add a user to a project.
    pub fn add_user_to_project(&self, project: ProjectId, user: UserId, access: AccessLevel)
                               -> GitlabResult<Member> {
209
210
211
212
213
214
215
216
        let user_str = format!("{}", user);
        let access_str = format!("{}", access);

        let mut req = try!(self._mkrequest(&format!("projects/{}/members", project)));

        req.param("user", &user_str)
            .param("access", &access_str);

217
        Self::_post_req(req)
218
219
220
    }

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

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

    /// Get a commit.
    pub fn commit(&self, project: ProjectId, commit: &str) -> GitlabResult<RepoCommitDetail> {
234
        self._get(&format!("projects/{}/repository/commits/{}", project, commit))
235
236
237
    }

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

    /// Get comments on a commit.
Ben Boeckel's avatar
Ben Boeckel committed
246
247
    pub fn create_commit_comment(&self, project: ProjectId, commit: &str, body: &str)
                                 -> GitlabResult<CommitNote> {
248
        let mut req = try!(self._mkrequest(&format!("projects/{}/repository/commits/{}/comment",
Ben Boeckel's avatar
Ben Boeckel committed
249
250
                                                    project,
                                                    commit)));
251
252
253

        req.param("note", body);

254
        Self::_post_req(req)
255
256
257
258
    }

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

264
        let mut req = try!(self._mkrequest(&format!("projects/{}/repository/commits/{}/comment",
Ben Boeckel's avatar
Ben Boeckel committed
265
266
                                                    project,
                                                    commit)));
267
268
269
270

        req.param("note", body)
            .param("path", path)
            .param("line", &line_str)
271
            .param("line_type", line_type.as_str());
272

273
        Self::_post_req(req)
274
275
    }

276
277
    /// Get the latest statuses of a commit.
    pub fn commit_latest_statuses(&self, project: ProjectId, commit: &str)
Ben Boeckel's avatar
Ben Boeckel committed
278
                           -> GitlabResult<Vec<CommitStatus>> {
Ben Boeckel's avatar
Ben Boeckel committed
279
280
281
        self._get_paged(&format!("projects/{}/repository/commits/{}/statuses",
                                 project,
                                 commit))
282
283
    }

284
    /// Get the all statuses of a commit.
Ben Boeckel's avatar
Ben Boeckel committed
285
286
    pub fn commit_all_statuses(&self, project: ProjectId, commit: &str)
                               -> GitlabResult<Vec<CommitStatus>> {
287
        let mut req = try!(self._mkrequest(&format!("projects/{}/repository/commits/{}/statuses",
Ben Boeckel's avatar
Ben Boeckel committed
288
289
                                                    project,
                                                    commit)));
290
291
292
293

        req.param("all", "true");

        Self::_get_paged_req(req)
294
295
    }

296
297
    /// Get the latest builds of a commit.
    pub fn commit_latest_builds(&self, project: ProjectId, commit: &str) -> GitlabResult<Vec<Build>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
298
299
300
        self._get_paged(&format!("projects/{}/repository/commits/{}/builds", project, commit))
    }

301
    /// Get the all builds of a commit.
Makoto Nakashima's avatar
Makoto Nakashima committed
302
303
304
305
306
307
308
309
310
311
    pub fn commit_all_builds(&self, project: ProjectId, commit: &str) -> GitlabResult<Vec<Build>> {
        let mut req = try!(self._mkrequest(&format!("projects/{}/repository/commits/{}/builds",
                                                    project,
                                                    commit)));

        req.param("all", "true");

        Self::_get_paged_req(req)
    }

Ben Boeckel's avatar
Ben Boeckel committed
312
    /// Create a status message for a commit.
Ben Boeckel's avatar
Ben Boeckel committed
313
    pub fn create_commit_status(&self, project: ProjectId, sha: &str, state: StatusState,
Ben Boeckel's avatar
Ben Boeckel committed
314
315
                                info: &CommitStatusInfo)
                                -> GitlabResult<CommitStatus> {
316
        let path = &format!("projects/{}/statuses/{}", project, sha);
317
318
        let mut req = try!(self._mkrequest(path));

319
        req.param("state", state.as_str());
320

321
322
323
324
325
        info.refname.map(|v| req.param("ref", v));
        info.name.map(|v| req.param("name", v));
        info.target_url.map(|v| req.param("target_url", v));
        info.description.map(|v| req.param("description", v));

326
        Self::_post_req(req)
327
328
    }

Ben Boeckel's avatar
Ben Boeckel committed
329
330
331
332
333
334
    /// Get the issues for a project.
    pub fn issues(&self, project: ProjectId) -> GitlabResult<Vec<Issue>> {
        self._get_paged(&format!("projects/{}/issues", project))
    }

    /// Get issues.
Ben Boeckel's avatar
Ben Boeckel committed
335
    pub fn issue(&self, project: ProjectId, issue: IssueId) -> GitlabResult<Issue> {
Ben Boeckel's avatar
Ben Boeckel committed
336
337
338
339
        self._get(&format!("projects/{}/issues/{}", project, issue))
    }

    /// Get the notes from a issue.
Ben Boeckel's avatar
Ben Boeckel committed
340
341
    pub fn issue_notes(&self, project: ProjectId, issue: IssueId) -> GitlabResult<Vec<Note>> {
        self._get_paged(&format!("projects/{}/issues/{}/notes", project, issue))
Ben Boeckel's avatar
Ben Boeckel committed
342
343
344
    }

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

Ben Boeckel's avatar
Ben Boeckel committed
349
350
351
352
353
        let mut req = try!(self._mkrequest(path));

        req.param("body", content);

        Self::_post_req(req)
Ben Boeckel's avatar
Ben Boeckel committed
354
355
    }

356
357
358
359
360
    /// Get the merge requests for a project.
    pub fn merge_requests(&self, project: ProjectId) -> GitlabResult<Vec<MergeRequest>> {
        self._get_paged(&format!("projects/{}/merge_requests", project))
    }

361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
    /// Get the opened/reopened merge requests for a project.
    pub fn opened_merge_requests(&self, project: ProjectId) -> GitlabResult<Vec<MergeRequest>> {
        let mut req = try!(self._mkrequest(&format!("projects/{}/merge_requests", project)));

        req.param("state", "opened");

        Self::_get_paged_req(req)
    }

    /// Get the closed merge requests for a project.
    pub fn closed_merge_requests(&self, project: ProjectId) -> GitlabResult<Vec<MergeRequest>> {
        let mut req = try!(self._mkrequest(&format!("projects/{}/merge_requests", project)));

        req.param("state", "closed");

        Self::_get_paged_req(req)
    }

    /// Get the merged merge requests for a project.
    pub fn merged_merge_requests(&self, project: ProjectId) -> GitlabResult<Vec<MergeRequest>> {
        let mut req = try!(self._mkrequest(&format!("projects/{}/merge_requests", project)));

        req.param("state", "merged");

        Self::_get_paged_req(req)
    }

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

394
395
    /// 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)
396
                                       -> GitlabResult<Vec<IssueReference>> {
397
398
399
400
401
        self._get_paged(&format!("projects/{}/merge_requests/{}/closes_issues",
                                 project,
                                 merge_request))
    }

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

410
411
    /// 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
412
413
                                    note: NoteId, award: &str)
                                    -> GitlabResult<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
419
420
421
422
423
424
        let mut req = try!(self._mkrequest(path));

        req.param("name", award);

        Self::_post_req(req)
    }

425
426
427
428
429
430
431
432
    /// Get the awards for a merge request.
    pub fn merge_request_awards(&self, project: ProjectId, merge_request: MergeRequestId)
                                -> GitlabResult<Vec<AwardEmoji>> {
        self._get_paged(&format!("projects/{}/merge_requests/{}/award_emoji",
                                 project,
                                 merge_request))
    }

433
434
    /// 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
435
436
                                     note: NoteId)
                                     -> GitlabResult<Vec<AwardEmoji>> {
437
438
439
440
441
442
        self._get_paged(&format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
                                 project,
                                 merge_request,
                                 note))
    }

443
    /// Create a note on a merge request.
Ben Boeckel's avatar
Ben Boeckel committed
444
445
446
447
448
449
    pub fn create_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestId,
                                     content: &str)
                                     -> GitlabResult<Note> {
        let path = &format!("projects/{}/merge_requests/{}/notes",
                            project,
                            merge_request);
450

451
452
453
454
455
        let mut req = try!(self._mkrequest(path));

        req.param("body", content);

        Self::_post_req(req)
456
457
    }

Ben Boeckel's avatar
Ben Boeckel committed
458
    // Create a request with the proper common metadata for authentication.
459
460
461
462
    //
    // This method exists because we want to store the current user in the structure, but we don't
    // have a `self` before we create the structure. Making it `Option<>` is a little silly and
    // refactoring this out is worth the cleaner API.
Ben Boeckel's avatar
Ben Boeckel committed
463
    fn _mkrequest1(base_url: &Url, token: &str, url: &str) -> GitlabResult<Request> {
464
        let full_url = try!(base_url.join(url));
465
466
        let mut req = Request::new(full_url);

467
468
        debug!(target: "gitlab", "api call {}", url);

469
        req.header(GitlabPrivateToken(token.to_string()));
470
471
472
473

        Ok(req)
    }

474
475
476
477
478
    // Create a request with the proper common metadata for authentication.
    fn _mkrequest(&self, url: &str) -> GitlabResult<Request> {
        Self::_mkrequest1(&self.base_url, &self.token, url)
    }

Ben Boeckel's avatar
Ben Boeckel committed
479
    // Refactored code which talks to Gitlab and transforms error messages properly.
480
481
    fn _comm<F, T>(req: Request, f: F) -> GitlabResult<T>
        where F: FnOnce(Request) -> Result<Response, EaseError>,
482
              T: Deserialize,
483
    {
484
        match f(req) {
485
            Ok(rsp) => {
486
                let v = try!(rsp.from_json().map_err(Error::Ease));
487

488
489
490
491
                debug!(target: "gitlab",
                       "received data: {}",
                       v);

Ben Boeckel's avatar
Ben Boeckel committed
492
                Ok(try!(serde_json::from_value::<T>(v)))
493
            },
494
            Err(err) => {
495
496
497
                if let EaseError::UnsuccessfulResponse(rsp) = err {
                    Err(Error::from_gitlab(try!(rsp.from_json())))
                } else {
498
                    Err(Error::Ease(err))
499
500
501
502
503
                }
            },
        }
    }

504
505
    fn _get_req<T: Deserialize>(req: Request) -> GitlabResult<T> {
        Self::_comm(req, |mut req| req.get())
506
507
    }

508
    fn _get<T: Deserialize>(&self, url: &str) -> GitlabResult<T> {
509
        Self::_get_req(try!(self._mkrequest(url)))
510
511
    }

512
513
    fn _post_req<T: Deserialize>(req: Request) -> GitlabResult<T> {
        Self::_comm(req, |mut req| req.post())
514
515
    }

516
    fn _post<T: Deserialize>(&self, url: &str) -> GitlabResult<T> {
517
        Self::_post_req(try!(self._mkrequest(url)))
518
519
    }

Ben Boeckel's avatar
Ben Boeckel committed
520
521
    fn _put_req<T: Deserialize>(req: Request) -> GitlabResult<T> {
        Self::_comm(req, |mut req| req.put())
522
523
    }

524
    fn _put<T: Deserialize>(&self, url: &str) -> GitlabResult<T> {
Ben Boeckel's avatar
Ben Boeckel committed
525
        Self::_put_req(try!(self._mkrequest(url)))
526
527
    }

528
529
    fn _get_paged_req<T: Deserialize>(req: Request) -> GitlabResult<Vec<T>> {
        let mut page_num = 1;
530
531
532
        let per_page = 100;
        let per_page_str = &format!("{}", per_page);

533
        let mut results: Vec<T> = vec![];
534
535

        loop {
536
            let page_str = &format!("{}", page_num);
Ben Boeckel's avatar
Ben Boeckel committed
537
            let mut page_req = req.clone();
538
            page_req.param("page", page_str)
Ben Boeckel's avatar
Ben Boeckel committed
539
                .param("per_page", per_page_str);
540
            let page = try!(Self::_get_req::<Vec<T>>(page_req));
541
            let page_len = page.len();
542

543
            results.extend(page.into_iter());
544

545
546
547
548
            // 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`.
549
            if page_len != per_page {
550
                break;
551
            }
552
            page_num += 1;
553
554
        }

555
        Ok(results)
556
    }
Ben Boeckel's avatar
Ben Boeckel committed
557

Ben Boeckel's avatar
Ben Boeckel committed
558
    // Handle paginated queries. Returns all results.
559
    fn _get_paged<T: Deserialize>(&self, url: &str) -> GitlabResult<Vec<T>> {
Ben Boeckel's avatar
Ben Boeckel committed
560
561
        Self::_get_paged_req(try!(self._mkrequest(url)))
    }
562
}