Skip to main content

bytes_radar/net/providers/
azure_devops.rs

1use crate::net::traits::{GitProvider, ParsedRepository, ProviderConfig};
2use async_trait::async_trait;
3use reqwest::Client;
4use std::collections::HashMap;
5
6pub struct AzureDevOpsProvider {
7    credentials: HashMap<String, String>,
8}
9
10impl AzureDevOpsProvider {
11    pub fn new() -> Self {
12        Self {
13            credentials: HashMap::new(),
14        }
15    }
16}
17
18#[async_trait]
19impl GitProvider for AzureDevOpsProvider {
20    fn name(&self) -> &'static str {
21        "azure_devops"
22    }
23
24    fn can_handle(&self, url: &str) -> bool {
25        url.contains("dev.azure.com") || url.contains("visualstudio.com") || url.contains("_git/")
26    }
27
28    fn parse_url(&self, url: &str) -> Option<ParsedRepository> {
29        if !self.can_handle(url) {
30            return None;
31        }
32
33        let url = url.trim_end_matches('/');
34
35        if url.contains("?version=GB") {
36            return self.parse_branch_url(url);
37        }
38
39        if url.contains("?version=GC") {
40            return self.parse_commit_url(url);
41        }
42
43        self.parse_basic_url(url)
44    }
45
46    fn build_download_urls(&self, parsed: &ParsedRepository) -> Vec<String> {
47        let mut urls = Vec::new();
48
49        if let Some(ref branch_or_commit) = parsed.branch_or_commit {
50            let host = parsed.host.as_deref().unwrap_or("dev.azure.com");
51
52            if parsed.is_commit {
53                urls.push(format!(
54                    "https://{}/{}/{}/_apis/git/repositories/{}/items?path=/&versionDescriptor.version={}&$format=zip",
55                    host, parsed.owner, parsed.repo.split('/').next().unwrap_or(&parsed.repo), parsed.repo, branch_or_commit
56                ));
57            } else {
58                urls.push(format!(
59                    "https://{}/{}/{}/_apis/git/repositories/{}/items?path=/&versionDescriptor.versionType=branch&versionDescriptor.version={}&$format=zip",
60                    host, parsed.owner, parsed.repo.split('/').next().unwrap_or(&parsed.repo), parsed.repo, branch_or_commit
61                ));
62            }
63        }
64
65        urls
66    }
67
68    async fn get_default_branch(
69        &self,
70        _client: &Client,
71        _parsed: &ParsedRepository,
72    ) -> Option<String> {
73        None
74    }
75
76    fn apply_config(&mut self, config: &ProviderConfig) {
77        self.credentials = config.credentials.clone();
78    }
79
80    fn get_project_name(&self, url: &str) -> String {
81        if let Some(parsed) = self.parse_url(url) {
82            return parsed.project_name;
83        }
84
85        if let Some(filename) = url.split('/').next_back() {
86            if filename.ends_with(".zip") {
87                return filename.trim_end_matches(".zip").to_string();
88            }
89            return filename.to_string();
90        }
91
92        "azure-devops-project".to_string()
93    }
94}
95
96impl AzureDevOpsProvider {
97    fn parse_commit_url(&self, url: &str) -> Option<ParsedRepository> {
98        if let Some(commit_start) = url.find("?version=GC") {
99            let base_url = &url[..commit_start];
100            let commit = &url[commit_start + "?version=GC".len()..];
101
102            if let Some(parsed_base) = self.parse_basic_url(base_url) {
103                return Some(parsed_base.with_commit(commit.to_string()));
104            }
105        }
106        None
107    }
108
109    fn parse_branch_url(&self, url: &str) -> Option<ParsedRepository> {
110        if let Some(branch_start) = url.find("?version=GB") {
111            let base_url = &url[..branch_start];
112            let branch = &url[branch_start + "?version=GB".len()..];
113
114            if let Some(parsed_base) = self.parse_basic_url(base_url) {
115                return Some(parsed_base.with_branch(branch.to_string()));
116            }
117        }
118        None
119    }
120
121    fn parse_basic_url(&self, url: &str) -> Option<ParsedRepository> {
122        if url.contains("dev.azure.com") {
123            let parts: Vec<&str> = url.split('/').collect();
124            if parts.len() >= 7 && parts.contains(&"_git") {
125                let host = parts[2].to_string();
126                let org = parts[3].to_string();
127                let project = parts[4].to_string();
128                let repo = parts[6].to_string();
129
130                return Some(
131                    ParsedRepository::new(format!("{}/{}", org, project), repo).with_host(host),
132                );
133            }
134        } else if url.contains("visualstudio.com") {
135            let parts: Vec<&str> = url.split('/').collect();
136            if parts.len() >= 6 && parts.contains(&"_git") {
137                let host = parts[2].to_string();
138                let org = parts[2].split('.').next().unwrap_or("").to_string();
139                let project = parts[4].to_string();
140                let repo = parts[6].to_string();
141
142                return Some(
143                    ParsedRepository::new(format!("{}/{}", org, project), repo).with_host(host),
144                );
145            }
146        }
147        None
148    }
149}
150
151impl Default for AzureDevOpsProvider {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_can_handle() {
163        let provider = AzureDevOpsProvider::new();
164        assert!(provider.can_handle("https://dev.azure.com/org/project/_git/repo"));
165        assert!(provider.can_handle("https://org.visualstudio.com/project/_git/repo"));
166        assert!(!provider.can_handle("https://github.com/user/repo"));
167    }
168
169    #[test]
170    fn test_parse_basic_url() {
171        let provider = AzureDevOpsProvider::new();
172
173        let parsed = provider
174            .parse_url("https://dev.azure.com/myorg/myproject/_git/myrepo")
175            .unwrap();
176        assert_eq!(parsed.owner, "myorg/myproject");
177        assert_eq!(parsed.repo, "myrepo");
178        assert_eq!(parsed.project_name, "myrepo@main");
179        assert_eq!(parsed.branch_or_commit, None);
180        assert!(!parsed.is_commit);
181        assert_eq!(parsed.host.as_ref().unwrap(), "dev.azure.com");
182    }
183
184    #[test]
185    fn test_parse_branch_url() {
186        let provider = AzureDevOpsProvider::new();
187
188        let parsed = provider
189            .parse_url("https://dev.azure.com/myorg/myproject/_git/myrepo?version=GBdevelop")
190            .unwrap();
191        assert_eq!(parsed.owner, "myorg/myproject");
192        assert_eq!(parsed.repo, "myrepo");
193        assert_eq!(parsed.project_name, "myrepo@develop");
194        assert_eq!(parsed.branch_or_commit, Some("develop".to_string()));
195        assert!(!parsed.is_commit);
196    }
197}