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