git_worktree_cli/
bitbucket_data_center_api.rs

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    // Parse URLs like:
222    // https://git.acmeorg.com/scm/PROJECT/repository.git
223    // https://git.acmeorg.com/projects/PROJECT/repos/repository
224    // git@git.acmeorg.com:PROJECT/repository.git
225
226    // Pattern for Data Center URLs with /scm/ path
227    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        // Reconstruct the base URL for API calls
233        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    // Pattern for Data Center URLs with /projects/ path
243    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    // Pattern for SSH URLs: git@host:project/repo.git
261    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    // Pattern for SSH URLs with protocol: ssh://git@host/project/repo.git
270    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}