gitlab.rs 14.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
extern crate serde_json;
17
use self::serde_json::from_value;
18
19
20
21

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

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

25
26
27
// TODO: Add system hook APIs
// TODO: Add webhook APIs

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

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

Ben Boeckel's avatar
Ben Boeckel committed
40
/// A JSON value return from Gitlab.
41
pub type GitlabResult<T: Deserialize> = Result<T, Error>;
42

43
44
45
46
47
48
49
50
51
52
53
54
/// 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>,
}

55
impl Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
56
57
58
    /// Create a new Gitlab API representation.
    ///
    /// Errors out if `token` is invalid.
Ben Boeckel's avatar
Ben Boeckel committed
59
    pub fn new<T: ToString>(host: &str, token: T) -> GitlabResult<Self> {
60
        let base_url = try!(Url::parse(&format!("https://{}/api/v3/", host)));
61

62
63
        let api = Gitlab {
            base_url: base_url,
64
            token: token.to_string(),
65
        };
66

67
68
69
        // Ensure the API is working.
        let _: UserFull = try!(api._get("user"));

70
        Ok(api)
71
72
    }

Ben Boeckel's avatar
Ben Boeckel committed
73
    /// The user the API is acting as.
74
    pub fn current_user(&self) -> GitlabResult<UserFull> {
75
76
77
        self._get("user")
    }

78
79
80
81
82
83
84
85
86
87
    /// 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
88
    /// Find a user by username.
Ben Boeckel's avatar
Ben Boeckel committed
89
90
91
92
93
94
95
96
    pub fn user_by_name<T: UserResult>(&self, name: &str) -> GitlabResult<Option<T>> {
        let mut req = try!(self._mkrequest("users"));

        req.param("username", name);

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

        Ok(users.pop())
97
98
    }

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
    /// 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))
119
120
    }

Ben Boeckel's avatar
Ben Boeckel committed
121
    /// Find a project by name.
122
    pub fn project_by_name(&self, name: &str) -> GitlabResult<Project> {
123
124
        self._get(&format!("projects/{}",
                           percent_encode(name.as_bytes(), PATH_SEGMENT_ENCODE_SET)))
125
126
    }

127
128
129
130
    /// Get a project's hooks.
    pub fn hooks(&self, project: ProjectId) -> GitlabResult<Vec<Hook>> {
        self._get_paged(&format!("projects/{}/hooks", project))
    }
131

132
133
134
135
136
    /// Get a project hook.
    pub fn hook(&self, project: ProjectId, hook: HookId) -> GitlabResult<Hook> {
        self._get(&format!("projects/{}/hooks/{}", project, hook))
    }

137
138
139
140
141
142
    /// 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
143
    pub fn group_member(&self, group: GroupId, user: UserId) -> GitlabResult<Member> {
144
145
146
        self._get(&format!("groups/{}/members/{}", group, user))
    }

147
    /// Get the team members of a project.
148
    pub fn project_members(&self, project: ProjectId) -> GitlabResult<Vec<Member>> {
149
150
151
152
        self._get_paged(&format!("projects/{}/members", project))
    }

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

157
158
159
    /// Add a user to a project.
    pub fn add_user_to_project(&self, project: ProjectId, user: UserId, access: AccessLevel)
                               -> GitlabResult<Member> {
160
161
162
163
164
165
166
167
        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);

168
        Self::_post_req(req)
169
170
171
    }

    /// Get branches for a project.
Ben Boeckel's avatar
Ben Boeckel committed
172
    pub fn branches(&self, project: ProjectId) -> GitlabResult<Vec<RepoBranch>> {
173
174
175
176
        self._get_paged(&format!("projects/{}/branches", project))
    }

    /// Get a branch.
Ben Boeckel's avatar
Ben Boeckel committed
177
    pub fn branch(&self, project: ProjectId, branch: &str) -> GitlabResult<RepoBranch> {
178
179
180
        self._get(&format!("projects/{}/repository/branches/{}",
                           project,
                           percent_encode(branch.as_bytes(), PATH_SEGMENT_ENCODE_SET)))
181
182
183
184
185
186
187
188
    }

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

    /// Get comments on a commit.
Ben Boeckel's avatar
Ben Boeckel committed
189
190
    pub fn commit_comments(&self, project: ProjectId, commit: &str)
                           -> GitlabResult<Vec<CommitNote>> {
191
192
193
194
        self._get_paged(&format!("projects/{}/repository/commit/{}/comments", project, commit))
    }

    /// Get comments on a commit.
Ben Boeckel's avatar
Ben Boeckel committed
195
196
    pub fn create_commit_comment(&self, project: ProjectId, commit: &str, body: &str)
                                 -> GitlabResult<CommitNote> {
Ben Boeckel's avatar
Ben Boeckel committed
197
198
199
        let mut req = try!(self._mkrequest(&format!("projects/{}/repository/commit/{}/comment",
                                                    project,
                                                    commit)));
200
201
202

        req.param("note", body);

203
        Self::_post_req(req)
204
205
206
207
    }

    /// 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
208
209
                                      path: &str, line: u64)
                                      -> GitlabResult<CommitNote> {
210
        let line_str = format!("{}", line);
Ben Boeckel's avatar
Ben Boeckel committed
211
        let line_type = LineType::New;
212

Ben Boeckel's avatar
Ben Boeckel committed
213
214
215
        let mut req = try!(self._mkrequest(&format!("projects/{}/repository/commit/{}/comment",
                                                    project,
                                                    commit)));
216
217
218
219

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

222
        Self::_post_req(req)
223
224
225
    }

    /// Get the statuses of a commit.
Ben Boeckel's avatar
Ben Boeckel committed
226
227
    pub fn commit_statuses(&self, project: ProjectId, commit: &str)
                           -> GitlabResult<Vec<CommitStatus>> {
228
229
230
231
        self._get_paged(&format!("projects/{}/repository/commit/{}/statuses", project, commit))
    }

    /// Get the statuses of a commit.
Ben Boeckel's avatar
Ben Boeckel committed
232
233
    pub fn commit_all_statuses(&self, project: ProjectId, commit: &str)
                               -> GitlabResult<Vec<CommitStatus>> {
Ben Boeckel's avatar
Ben Boeckel committed
234
        let mut req = try!(self._mkrequest(&format!("projects/{}/repository/commit/{}/statuses",
Ben Boeckel's avatar
Ben Boeckel committed
235
236
                                                    project,
                                                    commit)));
237
238
239
240

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

        Self::_get_paged_req(req)
241
242
    }

Ben Boeckel's avatar
Ben Boeckel committed
243
    /// Create a status message for a commit.
Ben Boeckel's avatar
Ben Boeckel committed
244
    pub fn create_commit_status(&self, project: ProjectId, sha: &str, state: StatusState,
Ben Boeckel's avatar
Ben Boeckel committed
245
246
                                info: &CommitStatusInfo)
                                -> GitlabResult<CommitStatus> {
247
        let path = &format!("projects/{}/statuses/{}", project, sha);
248
249
        let mut req = try!(self._mkrequest(path));

250
        req.param("state", state.as_str());
251

252
253
254
255
256
        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));

257
        Self::_post_req(req)
258
259
    }

Ben Boeckel's avatar
Ben Boeckel committed
260
261
262
263
264
265
    /// 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
266
    pub fn issue(&self, project: ProjectId, issue: IssueId) -> GitlabResult<Issue> {
Ben Boeckel's avatar
Ben Boeckel committed
267
268
269
270
        self._get(&format!("projects/{}/issues/{}", project, issue))
    }

    /// Get the notes from a issue.
Ben Boeckel's avatar
Ben Boeckel committed
271
272
    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
273
274
275
    }

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

Ben Boeckel's avatar
Ben Boeckel committed
280
281
282
283
284
        let mut req = try!(self._mkrequest(path));

        req.param("body", content);

        Self::_post_req(req)
Ben Boeckel's avatar
Ben Boeckel committed
285
286
    }

287
288
289
290
291
292
    /// 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))
    }

    /// Get merge requests.
Ben Boeckel's avatar
Ben Boeckel committed
293
294
    pub fn merge_request(&self, project: ProjectId, merge_request: MergeRequestId)
                         -> GitlabResult<MergeRequest> {
295
296
297
        self._get(&format!("projects/{}/merge_requests/{}", project, merge_request))
    }

298
299
    /// 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)
300
                                       -> GitlabResult<Vec<IssueReference>> {
301
302
303
304
305
        self._get_paged(&format!("projects/{}/merge_requests/{}/closes_issues",
                                 project,
                                 merge_request))
    }

306
    /// Get the notes from a merge request.
Ben Boeckel's avatar
Ben Boeckel committed
307
308
309
310
311
    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))
312
313
314
    }

    /// Create a note on a merge request.
Ben Boeckel's avatar
Ben Boeckel committed
315
316
317
318
319
320
    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);
321

322
323
324
325
326
        let mut req = try!(self._mkrequest(path));

        req.param("body", content);

        Self::_post_req(req)
327
328
    }

Ben Boeckel's avatar
Ben Boeckel committed
329
    // Create a request with the proper common metadata for authentication.
330
331
332
333
334
335
    //
    // 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.
    fn _mkrequest1<'a>(base_url: &Url, token: &str, url: &str) -> GitlabResult<Request<'a>> {
        let full_url = try!(base_url.join(url));
336
337
        let mut req = Request::new(full_url);

338
339
        debug!(target: "gitlab", "api call {}", url);

340
        req.header(GitlabPrivateToken(token.to_string()));
341
342
343
344

        Ok(req)
    }

345
346
347
348
349
    // 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
350
    // Refactored code which talks to Gitlab and transforms error messages properly.
351
352
    fn _comm<F, T>(req: Request, f: F) -> GitlabResult<T>
        where F: FnOnce(Request) -> Result<Response, EaseError>,
353
              T: Deserialize,
354
    {
355
        match f(req) {
356
            Ok(rsp) => {
357
                let v = try!(rsp.from_json().map_err(Error::Ease));
358
359
360

                Ok(try!(from_value::<T>(v)))
            },
361
            Err(err) => {
362
363
364
                if let EaseError::UnsuccessfulResponse(rsp) = err {
                    Err(Error::from_gitlab(try!(rsp.from_json())))
                } else {
365
                    Err(Error::Ease(err))
366
367
368
369
370
                }
            },
        }
    }

371
372
    fn _get_req<T: Deserialize>(req: Request) -> GitlabResult<T> {
        Self::_comm(req, |mut req| req.get())
373
374
    }

375
    fn _get<T: Deserialize>(&self, url: &str) -> GitlabResult<T> {
376
        Self::_get_req(try!(self._mkrequest(url)))
377
378
    }

379
380
    fn _post_req<T: Deserialize>(req: Request) -> GitlabResult<T> {
        Self::_comm(req, |mut req| req.post())
381
382
    }

383
    fn _post<T: Deserialize>(&self, url: &str) -> GitlabResult<T> {
384
        Self::_post_req(try!(self._mkrequest(url)))
385
386
    }

Ben Boeckel's avatar
Ben Boeckel committed
387
388
    fn _put_req<T: Deserialize>(req: Request) -> GitlabResult<T> {
        Self::_comm(req, |mut req| req.put())
389
390
    }

391
    fn _put<T: Deserialize>(&self, url: &str) -> GitlabResult<T> {
Ben Boeckel's avatar
Ben Boeckel committed
392
        Self::_put_req(try!(self._mkrequest(url)))
393
394
    }

395
396
    fn _get_paged_req<T: Deserialize>(req: Request) -> GitlabResult<Vec<T>> {
        let mut page_num = 1;
397
398
399
        let per_page = 100;
        let per_page_str = &format!("{}", per_page);

400
        let mut results: Vec<T> = vec![];
401
402

        loop {
403
            let page_str = &format!("{}", page_num);
Ben Boeckel's avatar
Ben Boeckel committed
404
            let mut page_req = req.clone();
405
            page_req.param("page", page_str)
Ben Boeckel's avatar
Ben Boeckel committed
406
                .param("per_page", per_page_str);
407
            let page = try!(Self::_get_req::<Vec<T>>(page_req));
408
            let page_len = page.len();
409

410
            results.extend(page.into_iter());
411

412
413
414
415
            // 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`.
416
            if page_len != per_page {
417
                break;
418
            }
419
            page_num += 1;
420
421
        }

422
        Ok(results)
423
    }
Ben Boeckel's avatar
Ben Boeckel committed
424

Ben Boeckel's avatar
Ben Boeckel committed
425
    // Handle paginated queries. Returns all results.
426
    fn _get_paged<T: Deserialize>(&self, url: &str) -> GitlabResult<Vec<T>> {
Ben Boeckel's avatar
Ben Boeckel committed
427
428
        Self::_get_paged_req(try!(self._mkrequest(url)))
    }
429
}