bytes_radar/net/providers/
azure_devops.rs1use 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}