1use serde::Deserialize;
2
3use super::{
4 ApiError, Error, Filter, JsonError, Project, ProjectName, ProjectNamespace, Provider,
5 RemoteUrl, Url, auth, escape,
6};
7
8const ACCEPT_HEADER_JSON: &str = "application/json";
9const GITLAB_API_BASEURL: &str = match option_env!("GITLAB_API_BASEURL") {
10 Some(url) => url,
11 None => "https://gitlab.com",
12};
13
14#[derive(Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum GitlabVisibility {
17 Private,
18 Internal,
19 Public,
20}
21
22#[derive(Deserialize)]
23pub struct GitlabProject {
24 #[serde(rename = "path")]
25 pub name: String,
26 pub path_with_namespace: String,
27 pub http_url_to_repo: String,
28 pub ssh_url_to_repo: String,
29 pub visibility: GitlabVisibility,
30}
31
32#[derive(Deserialize)]
33struct GitlabUser {
34 pub username: String,
35}
36
37impl Project for GitlabProject {
38 fn name(&self) -> ProjectName {
39 ProjectName::new(self.name.clone())
40 }
41
42 fn namespace(&self) -> Option<ProjectNamespace> {
43 if let Some((namespace, _name)) = self.path_with_namespace.rsplit_once('/') {
44 Some(ProjectNamespace::new(namespace.to_owned()))
45 } else {
46 None
47 }
48 }
49
50 fn ssh_url(&self) -> RemoteUrl {
51 RemoteUrl::new(self.ssh_url_to_repo.clone())
52 }
53
54 fn http_url(&self) -> RemoteUrl {
55 RemoteUrl::new(self.http_url_to_repo.clone())
56 }
57
58 fn private(&self) -> bool {
59 !matches!(self.visibility, GitlabVisibility::Public)
60 }
61}
62
63#[derive(Deserialize)]
64pub struct GitlabApiErrorResponse {
65 #[serde(alias = "error_description", alias = "error")]
66 pub message: String,
67}
68
69impl JsonError for GitlabApiErrorResponse {
70 fn to_string(self) -> String {
71 self.message
72 }
73}
74
75pub struct Gitlab {
76 filter: Filter,
77 secret_token: auth::AuthToken,
78 api_url_override: Option<Url>,
79}
80
81impl Gitlab {
82 fn api_url(&self) -> Url {
83 Url::new(
84 self.api_url_override
85 .as_ref()
86 .map(Url::as_str)
87 .unwrap_or(GITLAB_API_BASEURL)
88 .trim_end_matches('/')
89 .to_owned(),
90 )
91 }
92}
93
94impl Provider for Gitlab {
95 type Error = GitlabApiErrorResponse;
96 type Project = GitlabProject;
97
98 fn new(
99 filter: Filter,
100 secret_token: auth::AuthToken,
101 api_url_override: Option<Url>,
102 ) -> Result<Self, Error> {
103 Ok(Self {
104 filter,
105 secret_token,
106 api_url_override,
107 })
108 }
109
110 fn filter(&self) -> &Filter {
111 &self.filter
112 }
113
114 fn secret_token(&self) -> &auth::AuthToken {
115 &self.secret_token
116 }
117
118 fn auth_header_key() -> &'static str {
119 "bearer"
120 }
121
122 fn get_user_projects(
123 &self,
124 user: &super::User,
125 ) -> Result<Vec<GitlabProject>, ApiError<GitlabApiErrorResponse>> {
126 self.call_list(
127 &Url::new(format!(
128 "{}/api/v4/users/{}/projects",
129 self.api_url().as_str(),
130 escape(&user.0)
131 )),
132 Some(ACCEPT_HEADER_JSON),
133 )
134 }
135
136 fn get_group_projects(
137 &self,
138 group: &super::Group,
139 ) -> Result<Vec<GitlabProject>, ApiError<GitlabApiErrorResponse>> {
140 self.call_list(
141 &Url::new(format!(
142 "{}/api/v4/groups/{}/projects?include_subgroups=true&archived=false",
143 self.api_url().as_str(),
144 escape(&group.0),
145 )),
146 Some(ACCEPT_HEADER_JSON),
147 )
148 }
149
150 fn get_accessible_projects(
151 &self,
152 ) -> Result<Vec<GitlabProject>, ApiError<GitlabApiErrorResponse>> {
153 self.call_list(
154 &Url::new(format!("{}/api/v4/projects", self.api_url().as_str())),
155 Some(ACCEPT_HEADER_JSON),
156 )
157 }
158
159 fn get_current_user(&self) -> Result<super::User, ApiError<GitlabApiErrorResponse>> {
160 Ok(super::User(
161 super::call::<GitlabUser, GitlabApiErrorResponse>(
162 &format!("{}/api/v4/user", self.api_url().as_str()),
163 Self::auth_header_key(),
164 self.secret_token(),
165 Some(ACCEPT_HEADER_JSON),
166 )?
167 .username,
168 ))
169 }
170}