1use reqwest::Client;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::bitbucket_data_center_auth::BitbucketDataCenterAuth;
6use crate::error::{Error, Result};
7
8#[derive(Debug, Deserialize, Serialize, Clone)]
9pub struct BitbucketDataCenterUser {
10 pub name: String,
11 #[serde(rename = "displayName")]
12 pub display_name: String,
13 #[serde(rename = "emailAddress")]
14 pub email_address: Option<String>,
15 pub id: u64,
16 pub slug: String,
17 #[serde(rename = "type")]
18 pub user_type: Option<String>,
19 pub active: Option<bool>,
20 pub links: Option<HashMap<String, serde_json::Value>>,
21}
22
23#[derive(Debug, Deserialize, Serialize, Clone)]
24pub struct BitbucketDataCenterAuthor {
25 pub user: BitbucketDataCenterUser,
26 pub role: String,
27 pub approved: bool,
28 pub status: String,
29}
30
31#[derive(Debug, Deserialize, Serialize, Clone)]
32pub struct BitbucketDataCenterProject {
33 pub key: String,
34 pub name: String,
35 pub id: u64,
36 pub description: Option<String>,
37 #[serde(rename = "public")]
38 pub is_public: Option<bool>,
39 #[serde(rename = "type")]
40 pub project_type: Option<String>,
41 pub links: Option<HashMap<String, serde_json::Value>>,
42}
43
44#[derive(Debug, Deserialize, Serialize, Clone)]
45pub struct BitbucketDataCenterRepository {
46 pub slug: String,
47 pub name: String,
48 pub id: u64,
49 pub project: BitbucketDataCenterProject,
50 pub description: Option<String>,
51 #[serde(rename = "hierarchyId")]
52 pub hierarchy_id: Option<String>,
53 #[serde(rename = "scmId")]
54 pub scm_id: Option<String>,
55 pub state: Option<String>,
56 #[serde(rename = "statusMessage")]
57 pub status_message: Option<String>,
58 pub forkable: Option<bool>,
59 #[serde(rename = "public")]
60 pub is_public: Option<bool>,
61 pub archived: Option<bool>,
62 pub links: Option<HashMap<String, serde_json::Value>>,
63}
64
65#[derive(Debug, Deserialize, Serialize, Clone)]
66pub struct BitbucketDataCenterBranch {
67 pub id: String,
68 #[serde(rename = "displayId")]
69 pub display_id: String,
70 pub repository: Option<BitbucketDataCenterRepository>,
71}
72
73#[derive(Debug, Deserialize, Serialize, Clone)]
74pub struct BitbucketDataCenterPullRequestRef {
75 pub id: String,
76 #[serde(rename = "displayId")]
77 pub display_id: String,
78 #[serde(rename = "latestCommit")]
79 pub latest_commit: String,
80 #[serde(rename = "type")]
81 pub ref_type: String,
82 pub repository: BitbucketDataCenterRepository,
83}
84
85#[derive(Debug, Deserialize, Serialize, Clone)]
86pub struct BitbucketDataCenterPullRequest {
87 pub id: u64,
88 pub version: u32,
89 pub title: String,
90 pub description: Option<String>,
91 pub state: String,
92 pub open: bool,
93 pub closed: bool,
94 pub draft: Option<bool>,
95 pub author: BitbucketDataCenterAuthor,
96 #[serde(rename = "fromRef")]
97 pub from_ref: BitbucketDataCenterPullRequestRef,
98 #[serde(rename = "toRef")]
99 pub to_ref: BitbucketDataCenterPullRequestRef,
100 #[serde(rename = "createdDate")]
101 pub created_date: u64,
102 #[serde(rename = "updatedDate")]
103 pub updated_date: u64,
104 pub locked: Option<bool>,
105 pub reviewers: Option<Vec<serde_json::Value>>,
106 pub participants: Option<Vec<serde_json::Value>>,
107 pub properties: Option<HashMap<String, serde_json::Value>>,
108 pub links: HashMap<String, serde_json::Value>,
109}
110
111#[derive(Debug, Deserialize)]
112pub struct BitbucketDataCenterPullRequestsResponse {
113 pub values: Vec<BitbucketDataCenterPullRequest>,
114 #[allow(dead_code)]
115 pub size: u32,
116 #[allow(dead_code)]
117 pub limit: u32,
118 #[serde(rename = "isLastPage")]
119 #[allow(dead_code)]
120 pub is_last_page: bool,
121 #[allow(dead_code)]
122 pub start: u32,
123}
124
125pub struct BitbucketDataCenterClient {
126 client: Client,
127 auth: BitbucketDataCenterAuth,
128 base_url: String,
129}
130
131impl BitbucketDataCenterClient {
132 pub fn new(auth: BitbucketDataCenterAuth, base_url: String) -> Self {
133 let client = Client::new();
134 BitbucketDataCenterClient { client, auth, base_url }
135 }
136
137 pub async fn get_pull_requests(
138 &self,
139 project_key: &str,
140 repo_slug: &str,
141 ) -> Result<Vec<BitbucketDataCenterPullRequest>> {
142 let token = self.auth.get_token()?;
143 let url = format!(
144 "{}/rest/api/1.0/projects/{}/repos/{}/pull-requests",
145 self.base_url.trim_end_matches('/'),
146 project_key,
147 repo_slug
148 );
149
150 let response = self
151 .client
152 .get(&url)
153 .bearer_auth(&token)
154 .header("Accept", "application/json")
155 .send()
156 .await
157 .map_err(|e| Error::network(format!("Failed to send request to Bitbucket Data Center API: {}", e)))?;
158
159 if response.status().is_client_error() {
160 let status = response.status();
161 let text = response.text().await.unwrap_or_default();
162
163 if status == 401 {
164 return Err(Error::auth(
165 "Authentication failed. Please check your Bitbucket Data Center access token and run 'gwt auth bitbucket-data-center' to update it."
166 ));
167 } else if status == 404 {
168 return Err(Error::provider(format!(
169 "Repository not found: {}/{}. Please check the project key and repository slug.",
170 project_key, repo_slug
171 )));
172 } else {
173 return Err(Error::provider(format!(
174 "API request failed with status {}: {}",
175 status, text
176 )));
177 }
178 }
179
180 let pr_response: BitbucketDataCenterPullRequestsResponse = response
181 .json()
182 .await
183 .map_err(|e| Error::provider(format!("Failed to parse Bitbucket Data Center API response: {}", e)))?;
184
185 Ok(pr_response.values)
186 }
187
188 pub async fn test_connection(&self) -> Result<()> {
189 let token = self.auth.get_token()?;
190 let url = format!("{}/rest/api/1.0/users", self.base_url.trim_end_matches('/'));
191
192 let response = self
193 .client
194 .get(&url)
195 .bearer_auth(&token)
196 .header("Accept", "application/json")
197 .send()
198 .await
199 .map_err(|e| Error::network(format!("Failed to test Bitbucket Data Center API connection: {}", e)))?;
200
201 if response.status().is_success() {
202 println!("✓ Bitbucket Data Center API connection successful");
203 Ok(())
204 } else {
205 let status = response.status();
206 if status == 401 {
207 Err(Error::auth(
208 "Authentication failed. Please check your Bitbucket Data Center access token.",
209 ))
210 } else {
211 Err(Error::provider(format!(
212 "API connection failed with status: {}",
213 status
214 )))
215 }
216 }
217 }
218}
219
220pub fn extract_bitbucket_data_center_info_from_url(url: &str) -> Option<(String, String, String)> {
221 if let Some(captures) = regex::Regex::new(r"([^/]+)/scm/([^/]+)/([^/\.]+)").ok()?.captures(url) {
228 let base_url = captures.get(1)?.as_str();
229 let project = captures.get(2)?.as_str();
230 let repo = captures.get(3)?.as_str();
231
232 let api_base_url = if base_url.starts_with("http") {
234 base_url.to_string()
235 } else {
236 format!("https://{}", base_url)
237 };
238
239 return Some((api_base_url, project.to_string(), repo.to_string()));
240 }
241
242 if let Some(captures) = regex::Regex::new(r"([^/]+)/projects/([^/]+)/repos/([^/\.]+)")
244 .ok()?
245 .captures(url)
246 {
247 let base_url = captures.get(1)?.as_str();
248 let project = captures.get(2)?.as_str();
249 let repo = captures.get(3)?.as_str();
250
251 let api_base_url = if base_url.starts_with("http") {
252 base_url.to_string()
253 } else {
254 format!("https://{}", base_url)
255 };
256
257 return Some((api_base_url, project.to_string(), repo.to_string()));
258 }
259
260 if let Some(captures) = regex::Regex::new(r"git@([^:]+):([^/]+)/([^/\.]+)").ok()?.captures(url) {
262 let host = captures.get(1)?.as_str();
263 let project = captures.get(2)?.as_str();
264 let repo = captures.get(3)?.as_str();
265
266 return Some((format!("https://{}", host), project.to_string(), repo.to_string()));
267 }
268
269 if let Some(captures) = regex::Regex::new(r"ssh://git@([^/]+)/([^/]+)/([^/\.]+)")
271 .ok()?
272 .captures(url)
273 {
274 let host = captures.get(1)?.as_str();
275 let project = captures.get(2)?.as_str();
276 let repo = captures.get(3)?.as_str();
277
278 return Some((format!("https://{}", host), project.to_string(), repo.to_string()));
279 }
280
281 None
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_extract_bitbucket_data_center_info_scm() {
290 let url = "https://git.acmeorg.com/scm/PROJ/repo";
291 let result = extract_bitbucket_data_center_info_from_url(url);
292 assert_eq!(
293 result,
294 Some((
295 "https://git.acmeorg.com".to_string(),
296 "PROJ".to_string(),
297 "repo".to_string()
298 ))
299 );
300 }
301
302 #[test]
303 fn test_extract_bitbucket_data_center_info_scm_git() {
304 let url = "https://git.acmeorg.com/scm/PROJ/repo.git";
305 let result = extract_bitbucket_data_center_info_from_url(url);
306 assert_eq!(
307 result,
308 Some((
309 "https://git.acmeorg.com".to_string(),
310 "PROJ".to_string(),
311 "repo".to_string()
312 ))
313 );
314 }
315
316 #[test]
317 fn test_extract_bitbucket_data_center_info_projects() {
318 let url = "https://git.acmeorg.com/projects/PROJ/repos/repo";
319 let result = extract_bitbucket_data_center_info_from_url(url);
320 assert_eq!(
321 result,
322 Some((
323 "https://git.acmeorg.com".to_string(),
324 "PROJ".to_string(),
325 "repo".to_string()
326 ))
327 );
328 }
329
330 #[test]
331 fn test_extract_bitbucket_data_center_info_ssh() {
332 let url = "git@git.acmeorg.com:PROJ/repo.git";
333 let result = extract_bitbucket_data_center_info_from_url(url);
334 assert_eq!(
335 result,
336 Some((
337 "https://git.acmeorg.com".to_string(),
338 "PROJ".to_string(),
339 "repo".to_string()
340 ))
341 );
342 }
343
344 #[test]
345 fn test_extract_bitbucket_data_center_info_ssh_protocol() {
346 let url = "ssh://git@git.acmeorg.com/PROJECT_ID/REPO_ID.git";
347 let result = extract_bitbucket_data_center_info_from_url(url);
348 assert_eq!(
349 result,
350 Some((
351 "https://git.acmeorg.com".to_string(),
352 "PROJECT_ID".to_string(),
353 "REPO_ID".to_string()
354 ))
355 );
356 }
357
358 #[test]
359 fn test_extract_bitbucket_data_center_info_invalid() {
360 let url = "https://github.com/user/repo";
361 let result = extract_bitbucket_data_center_info_from_url(url);
362 assert_eq!(result, None);
363 }
364}