Skip to main content

grm/provider/
gitlab.rs

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}