Skip to main content

bytes_radar/net/providers/
gitlab.rs

1use crate::net::traits::{GitProvider, ParsedRepository, ProviderConfig};
2use async_trait::async_trait;
3use reqwest::Client;
4
5pub struct GitLabProvider {
6    token: Option<String>,
7}
8
9impl GitLabProvider {
10    pub fn new() -> Self {
11        Self { token: None }
12    }
13}
14
15#[async_trait]
16impl GitProvider for GitLabProvider {
17    fn name(&self) -> &'static str {
18        "gitlab"
19    }
20
21    fn can_handle(&self, url: &str) -> bool {
22        url.contains("gitlab.com") || url.contains("gitlab.")
23    }
24
25    fn parse_url(&self, url: &str) -> Option<ParsedRepository> {
26        if !self.can_handle(url) {
27            return None;
28        }
29
30        let url = url.trim_end_matches('/');
31
32        if url.contains("/-/tree/") {
33            return self.parse_tree_url(url);
34        }
35
36        if url.contains("/-/commit/") {
37            return self.parse_commit_url(url);
38        }
39
40        self.parse_basic_url(url)
41    }
42
43    fn build_download_urls(&self, parsed: &ParsedRepository) -> Vec<String> {
44        let mut urls = Vec::new();
45
46        if let Some(ref branch_or_commit) = parsed.branch_or_commit {
47            let host = parsed.host.as_deref().unwrap_or("gitlab.com");
48
49            if parsed.is_commit {
50                urls.push(format!(
51                    "https://{}/{}/-/archive/{}/{}-{}.tar.gz",
52                    host,
53                    self.build_project_path(&parsed.owner, &parsed.repo),
54                    branch_or_commit,
55                    parsed.repo,
56                    branch_or_commit
57                ));
58            } else {
59                urls.push(format!(
60                    "https://{}/{}/-/archive/{}/{}-{}.tar.gz",
61                    host,
62                    self.build_project_path(&parsed.owner, &parsed.repo),
63                    branch_or_commit,
64                    parsed.repo,
65                    branch_or_commit
66                ));
67            }
68        }
69
70        urls
71    }
72
73    async fn get_default_branch(
74        &self,
75        _client: &Client,
76        _parsed: &ParsedRepository,
77    ) -> Option<String> {
78        None
79    }
80
81    fn apply_config(&mut self, config: &ProviderConfig) {
82        self.token = config
83            .credentials
84            .get("token")
85            .cloned()
86            .or_else(|| config.credentials.get("private_token").cloned());
87    }
88
89    fn get_project_name(&self, url: &str) -> String {
90        if let Some(parsed) = self.parse_url(url) {
91            return parsed.project_name;
92        }
93
94        if let Some(filename) = url.split('/').next_back() {
95            if filename.ends_with(".tar.gz") {
96                return filename.trim_end_matches(".tar.gz").to_string();
97            }
98            if filename.ends_with(".tgz") {
99                return filename.trim_end_matches(".tgz").to_string();
100            }
101            return filename.to_string();
102        }
103
104        "gitlab-project".to_string()
105    }
106}
107
108impl GitLabProvider {
109    fn parse_tree_url(&self, url: &str) -> Option<ParsedRepository> {
110        let parts: Vec<&str> = url.split('/').collect();
111        if let Some(tree_pos) = parts.iter().position(|&x| x == "tree") {
112            if tree_pos + 1 < parts.len() && tree_pos >= 3 {
113                let gitlab_pos = parts.iter().position(|&x| x.contains("gitlab"))?;
114                let host = parts[gitlab_pos].to_string();
115                let owner = parts[gitlab_pos + 1].to_string();
116                let repo = parts[gitlab_pos + 2].to_string();
117                let branch = parts[tree_pos + 1].to_string();
118
119                return Some(
120                    ParsedRepository::new(owner, repo)
121                        .with_branch(branch)
122                        .with_host(host),
123                );
124            }
125        }
126        None
127    }
128
129    fn parse_commit_url(&self, url: &str) -> Option<ParsedRepository> {
130        let parts: Vec<&str> = url.split('/').collect();
131        if let Some(commit_pos) = parts.iter().position(|&x| x == "commit") {
132            if commit_pos + 1 < parts.len() && commit_pos >= 3 {
133                let gitlab_pos = parts.iter().position(|&x| x.contains("gitlab"))?;
134                let host = parts[gitlab_pos].to_string();
135                let owner = parts[gitlab_pos + 1].to_string();
136                let repo = parts[gitlab_pos + 2].to_string();
137                let commit = parts[commit_pos + 1].to_string();
138
139                return Some(
140                    ParsedRepository::new(owner, repo)
141                        .with_commit(commit)
142                        .with_host(host),
143                );
144            }
145        }
146        None
147    }
148
149    fn parse_basic_url(&self, url: &str) -> Option<ParsedRepository> {
150        let parts: Vec<&str> = url.split('/').collect();
151        if let Some(gitlab_pos) = parts.iter().position(|&x| x.contains("gitlab")) {
152            if gitlab_pos + 2 < parts.len() {
153                let host = parts[gitlab_pos].to_string();
154                let owner = parts[gitlab_pos + 1].to_string();
155                let repo = parts[gitlab_pos + 2].to_string();
156
157                return Some(ParsedRepository::new(owner, repo).with_host(host));
158            }
159        }
160
161        None
162    }
163
164    fn build_project_path(&self, owner: &str, repo: &str) -> String {
165        format!("{}/{}", owner, repo)
166    }
167}
168
169impl Default for GitLabProvider {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_can_handle() {
181        let provider = GitLabProvider::new();
182        assert!(provider.can_handle("https://gitlab.com/user/repo"));
183        assert!(provider.can_handle("https://gitlab.example.com/user/repo"));
184        assert!(!provider.can_handle("https://github.com/user/repo"));
185    }
186
187    #[test]
188    fn test_parse_basic_url() {
189        let provider = GitLabProvider::new();
190
191        let parsed = provider.parse_url("https://gitlab.com/user/repo").unwrap();
192        assert_eq!(parsed.owner, "user");
193        assert_eq!(parsed.repo, "repo");
194        assert_eq!(parsed.project_name, "repo@main");
195        assert_eq!(parsed.branch_or_commit, None);
196        assert!(!parsed.is_commit);
197        assert_eq!(parsed.host.as_ref().unwrap(), "gitlab.com");
198    }
199
200    #[test]
201    fn test_parse_tree_url() {
202        let provider = GitLabProvider::new();
203
204        let parsed = provider
205            .parse_url("https://gitlab.com/user/repo/-/tree/develop")
206            .unwrap();
207        assert_eq!(parsed.owner, "user");
208        assert_eq!(parsed.repo, "repo");
209        assert_eq!(parsed.project_name, "repo@develop");
210        assert_eq!(parsed.branch_or_commit, Some("develop".to_string()));
211        assert!(!parsed.is_commit);
212    }
213
214    #[test]
215    fn test_build_download_urls() {
216        let provider = GitLabProvider::new();
217
218        let parsed = ParsedRepository::new("user".to_string(), "repo".to_string())
219            .with_branch("main".to_string())
220            .with_host("gitlab.com".to_string());
221
222        let urls = provider.build_download_urls(&parsed);
223        assert!(urls
224            .contains(&"https://gitlab.com/user/repo/-/archive/main/repo-main.tar.gz".to_string()));
225    }
226
227    #[test]
228    fn test_self_hosted_gitlab() {
229        let provider = GitLabProvider::new();
230
231        let parsed = provider
232            .parse_url("https://gitlab.company.com/team/project")
233            .unwrap();
234        assert_eq!(parsed.owner, "team");
235        assert_eq!(parsed.repo, "project");
236        assert_eq!(parsed.host.as_ref().unwrap(), "gitlab.company.com");
237    }
238}