extern crate itertools;
use self::itertools::Itertools;
extern crate reqwest;
use self::reqwest::{Client, Method, RequestBuilder, Url};
extern crate serde;
use self::serde::{Deserialize, Deserializer, Serializer};
use self::serde::de::Error as SerdeError;
use self::serde::de::Unexpected;
use self::serde::ser::Serialize;
extern crate serde_json;
extern crate url;
use self::url::percent_encoding::{PATH_SEGMENT_ENCODE_SET, percent_encode};
use error::*;
use types::*;
use std::borrow::Borrow;
use std::fmt::{self, Display, Debug};
pub struct Gitlab {
base_url: Url,
token: String,
}
impl Debug for Gitlab {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Gitlab {{ {} }}", self.base_url)
}
}
header!{ (GitlabPrivateToken, "PRIVATE-TOKEN") => [String] }
#[derive(Debug)]
pub struct CommitStatusInfo<'a> {
pub refname: Option<&'a str>,
pub name: Option<&'a str>,
pub target_url: Option<&'a str>,
pub description: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeRequestStateFilter {
Opened,
Closed,
Merged,
}
enum_serialize!(MergeRequestStateFilter -> "state",
Opened => "opened",
Closed => "closed",
Merged => "merged",
);
impl Gitlab {
pub fn new<T: ToString>(host: &str, token: T) -> Result<Self> {
Self::_new("https", host, token.to_string())
}
pub fn new_insecure<T: ToString>(host: &str, token: T) -> Result<Self> {
Self::_new("http", host, token.to_string())
}
fn _new(protocol: &str, host: &str, token: String) -> Result<Self> {
let base_url = Url::parse(&format!("{}://{}/api/v3/", protocol, host))
.chain_err(|| ErrorKind::UrlParse)?;
let api = Gitlab {
base_url: base_url,
token: token,
};
let _: UserPublic = api._get("user")?;
Ok(api)
}
pub fn current_user(&self) -> Result<UserPublic> {
self._get("user")
}
pub fn users<T: UserResult>(&self) -> Result<Vec<T>> {
self._get_paged("users")
}
pub fn user<T: UserResult>(&self, user: UserId) -> Result<T> {
self._get(&format!("users/{}", user))
}
pub fn user_by_name<T: UserResult>(&self, name: &str) -> Result<T> {
let mut users = self._get_paged_with_param("users", &[("username", name)])?;
users.pop()
.ok_or_else(|| Error::from_kind(ErrorKind::Gitlab("no such user".to_string())))
}
pub fn projects(&self) -> Result<Vec<Project>> {
self._get_paged("projects")
}
pub fn owned_projects(&self) -> Result<Vec<Project>> {
self._get_paged("projects/owned")
}
pub fn all_projects(&self) -> Result<Vec<Project>> {
self._get_paged("projects/all")
}
pub fn project(&self, project: ProjectId) -> Result<Project> {
self._get(&format!("projects/{}", project))
}
pub fn project_by_name(&self, name: &str) -> Result<Project> {
self._get(&format!("projects/{}",
percent_encode(name.as_bytes(), PATH_SEGMENT_ENCODE_SET)))
}
pub fn hooks(&self, project: ProjectId) -> Result<Vec<ProjectHook>> {
self._get_paged(&format!("projects/{}/hooks", project))
}
pub fn hook(&self, project: ProjectId, hook: HookId) -> Result<ProjectHook> {
self._get(&format!("projects/{}/hooks/{}", project, hook))
}
fn bool_param_value(value: bool) -> &'static str {
if value {
"true"
} else {
"false"
}
}
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()))]
}
pub fn add_hook(&self, project: ProjectId, url: &str, events: WebhookEvents) -> Result<ProjectHook> {
let mut flags = Self::event_flags(events);
flags.push(("url", url));
self._post_with_param(&format!("projects/{}/hooks", project), &flags)
}
pub fn group_members(&self, group: GroupId) -> Result<Vec<Member>> {
self._get_paged(&format!("groups/{}/members", group))
}
pub fn group_member(&self, group: GroupId, user: UserId) -> Result<Member> {
self._get(&format!("groups/{}/members/{}", group, user))
}
pub fn project_members(&self, project: ProjectId) -> Result<Vec<Member>> {
self._get_paged(&format!("projects/{}/members", project))
}
pub fn project_member(&self, project: ProjectId, user: UserId) -> Result<Member> {
self._get(&format!("projects/{}/members/{}", project, user))
}
pub fn add_user_to_project(&self, project: ProjectId, user: UserId, access: AccessLevel)
-> Result<Member> {
let user_str = format!("{}", user);
let access_str = format!("{}", access);
self._post_with_param(&format!("projects/{}/members", project),
&[("user", &user_str), ("access", &access_str)])
}
pub fn branches(&self, project: ProjectId) -> Result<Vec<RepoBranch>> {
self._get_paged(&format!("projects/{}/branches", project))
}
pub fn branch(&self, project: ProjectId, branch: &str) -> Result<RepoBranch> {
self._get(&format!("projects/{}/repository/branches/{}",
project,
percent_encode(branch.as_bytes(), PATH_SEGMENT_ENCODE_SET)))
}
pub fn commit(&self, project: ProjectId, commit: &str) -> Result<RepoCommitDetail> {
self._get(&format!("projects/{}/repository/commits/{}", project, commit))
}
pub fn commit_comments(&self, project: ProjectId, commit: &str) -> Result<Vec<CommitNote>> {
self._get_paged(&format!("projects/{}/repository/commits/{}/comments",
project,
commit))
}
pub fn create_commit_comment(&self, project: ProjectId, commit: &str, body: &str)
-> Result<CommitNote> {
self._post_with_param(&format!("projects/{}/repository/commits/{}/comment",
project,
commit),
&[("note", body)])
}
pub fn create_commit_line_comment(&self, project: ProjectId, commit: &str, body: &str,
path: &str, line: u64)
-> Result<CommitNote> {
let line_str = format!("{}", line);
let line_type = LineType::New;
self._post_with_param(&format!("projects/{}/repository/commits/{}/comment",
project,
commit),
&[("note", body),
("path", path),
("line", &line_str),
("line_type", line_type.as_str())])
}
pub fn commit_latest_statuses(&self, project: ProjectId, commit: &str)
-> Result<Vec<CommitStatus>> {
self._get_paged(&format!("projects/{}/repository/commits/{}/statuses",
project,
commit))
}
pub fn commit_all_statuses(&self, project: ProjectId, commit: &str)
-> Result<Vec<CommitStatus>> {
self._get_paged_with_param(&format!("projects/{}/repository/commits/{}/statuses",
project,
commit),
&[("all", "true")])
}
pub fn commit_latest_builds(&self, project: ProjectId, commit: &str) -> Result<Vec<Build>> {
self._get_paged(&format!("projects/{}/repository/commits/{}/builds", project, commit))
}
pub fn commit_all_builds(&self, project: ProjectId, commit: &str) -> Result<Vec<Build>> {
self._get_paged_with_param(&format!("projects/{}/repository/commits/{}/builds",
project,
commit),
&[("all", "true")])
}
pub fn create_commit_status(&self, project: ProjectId, sha: &str, state: StatusState,
info: &CommitStatusInfo)
-> Result<CommitStatus> {
let path = &format!("projects/{}/statuses/{}", project, sha);
let mut params = vec![("state", state.as_str())];
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)));
self._post_with_param(path, ¶ms)
}
pub fn issues(&self, project: ProjectId) -> Result<Vec<Issue>> {
self._get_paged(&format!("projects/{}/issues", project))
}
pub fn issue(&self, project: ProjectId, issue: IssueId) -> Result<Issue> {
self._get(&format!("projects/{}/issues/{}", project, issue))
}
pub fn issue_notes(&self, project: ProjectId, issue: IssueId) -> Result<Vec<Note>> {
self._get_paged(&format!("projects/{}/issues/{}/notes", project, issue))
}
pub fn create_issue_note(&self, project: ProjectId, issue: IssueId, content: &str)
-> Result<Note> {
let path = &format!("projects/{}/issues/{}/notes", project, issue);
self._post_with_param(path, &[("body", content)])
}
pub fn merge_requests(&self, project: ProjectId) -> Result<Vec<MergeRequest>> {
self._get_paged(&format!("projects/{}/merge_requests", project))
}
pub fn merge_requests_with_state(&self, project: ProjectId, state: MergeRequestStateFilter)
-> Result<Vec<MergeRequest>> {
self._get_paged_with_param(&format!("projects/{}/merge_requests", project),
&[("state", state.as_str())])
}
pub fn merge_request(&self, project: ProjectId, merge_request: MergeRequestId)
-> Result<MergeRequest> {
self._get(&format!("projects/{}/merge_requests/{}", project, merge_request))
}
pub fn merge_request_closes_issues(&self, project: ProjectId, merge_request: MergeRequestId)
-> Result<Vec<IssueReference>> {
self._get_paged(&format!("projects/{}/merge_requests/{}/closes_issues",
project,
merge_request))
}
pub fn merge_request_notes(&self, project: ProjectId, merge_request: MergeRequestId)
-> Result<Vec<Note>> {
self._get_paged(&format!("projects/{}/merge_requests/{}/notes",
project,
merge_request))
}
pub fn award_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestId,
note: NoteId, award: &str)
-> Result<AwardEmoji> {
let path = &format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
project,
merge_request,
note);
self._post_with_param(path, &[("name", award)])
}
pub fn merge_request_awards(&self, project: ProjectId, merge_request: MergeRequestId)
-> Result<Vec<AwardEmoji>> {
self._get_paged(&format!("projects/{}/merge_requests/{}/award_emoji",
project,
merge_request))
}
pub fn merge_request_note_awards(&self, project: ProjectId, merge_request: MergeRequestId,
note: NoteId)
-> Result<Vec<AwardEmoji>> {
self._get_paged(&format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
project,
merge_request,
note))
}
pub fn create_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestId,
content: &str)
-> Result<Note> {
let path = &format!("projects/{}/merge_requests/{}/notes",
project,
merge_request);
self._post_with_param(path, &[("body", content)])
}
pub fn get_issues_closed_by_merge_request(&self, project: ProjectId,
merge_request: MergeRequestId)
-> Result<Vec<Issue>> {
let path = &format!("projects/{}/merge_requests/{}/closes_issues",
project,
merge_request);
self._get_paged(path)
}
pub fn set_issue_labels<I, L>(&self, project: ProjectId, issue: IssueId, labels: I)
-> Result<Issue>
where I: IntoIterator<Item = L>,
L: Display,
{
let path = &format!("projects/{}/issues/{}",
project,
issue);
self._put_with_param(path, &[("labels", labels.into_iter().join(","))])
}
fn _mk_url(&self, url: &str) -> Result<Url> {
debug!(target: "gitlab", "api call {}", url);
Ok(self.base_url.join(url).chain_err(|| ErrorKind::UrlParse)?)
}
fn _mk_url_with_param<I, K, V>(&self, url: &str, param: I) -> Result<Url>
where I: IntoIterator,
I::Item: Borrow<(K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let mut full_url = self._mk_url(url)?;
full_url.query_pairs_mut().extend_pairs(param);
Ok(full_url)
}
fn _comm<T>(&self, req: RequestBuilder) -> Result<T>
where T: Deserialize,
{
let req = req.header(GitlabPrivateToken(self.token.to_string()));
let rsp = req.send().chain_err(|| ErrorKind::Communication)?;
if !rsp.status().is_success() {
let v = serde_json::from_reader(rsp).chain_err(|| ErrorKind::Deserialize)?;
return Err(Error::from_gitlab(v));
}
let v = serde_json::from_reader(rsp).chain_err(|| ErrorKind::Deserialize)?;
debug!(target: "gitlab",
"received data: {:?}",
v);
Ok(serde_json::from_value::<T>(v).chain_err(|| ErrorKind::Deserialize)?)
}
fn _get<T: Deserialize>(&self, url: &str) -> Result<T> {
let param: &[(&str, &str)] = &[];
self._get_with_param(url, param)
}
fn _get_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<T>
where T: Deserialize,
I: IntoIterator,
I::Item: Borrow<(K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let full_url = self._mk_url_with_param(url, param)?;
let req = Client::new().chain_err(|| ErrorKind::Communication)?.get(full_url);
self._comm(req)
}
fn _post<T: Deserialize>(&self, url: &str) -> Result<T> {
let param: &[(&str, &str)] = &[];
self._post_with_param(url, param)
}
fn _post_with_param<T, U>(&self, url: &str, param: U) -> Result<T>
where T: Deserialize,
U: Serialize,
{
let full_url = self._mk_url(url)?;
let req = Client::new().chain_err(|| ErrorKind::Communication)?.post(full_url).form(¶m);
self._comm(req)
}
fn _put<T: Deserialize>(&self, url: &str) -> Result<T> {
let param: &[(&str, &str)] = &[];
self._put_with_param(url, param)
}
fn _put_with_param<T, U>(&self, url: &str, param: U) -> Result<T>
where T: Deserialize,
U: Serialize,
{
let full_url = self._mk_url(url)?;
let req = Client::new().chain_err(|| ErrorKind::Communication)?.request(Method::Put, full_url).form(¶m);
self._comm(req)
}
fn _get_paged<T: Deserialize>(&self, url: &str) -> Result<Vec<T>> {
let param: &[(&str, &str)] = &[];
self._get_paged_with_param(url, param)
}
fn _get_paged_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<Vec<T>>
where T: Deserialize,
I: IntoIterator,
I::Item: Borrow<(K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let mut page_num = 1;
let per_page = 100;
let per_page_str = &format!("{}", per_page);
let full_url = self._mk_url_with_param(url, param)?;
let mut results: Vec<T> = vec![];
loop {
let page_str = &format!("{}", page_num);
let mut page_url = full_url.clone();
page_url.query_pairs_mut()
.extend_pairs(&[("page", page_str), ("per_page", per_page_str)]);
let req = Client::new().chain_err(|| ErrorKind::Communication)?.get(page_url);
let page: Vec<T> = self._comm(req)?;
let page_len = page.len();
results.extend(page);
if page_len != per_page {
break;
}
page_num += 1;
}
Ok(results)
}
}