git_worktree_cli/
bitbucket_api.rs

1use reqwest::Client;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::bitbucket_auth::BitbucketAuth;
6use crate::error::{Error, Result};
7
8#[derive(Debug, Deserialize, Serialize, Clone)]
9pub struct BitbucketUser {
10    pub display_name: String,
11    pub uuid: String,
12    pub nickname: Option<String>,
13}
14
15#[derive(Debug, Deserialize, Serialize, Clone)]
16pub struct BitbucketRepository {
17    pub name: String,
18    pub full_name: String,
19    pub uuid: String,
20}
21
22#[derive(Debug, Deserialize, Serialize, Clone)]
23pub struct BitbucketBranch {
24    pub name: String,
25}
26
27#[derive(Debug, Deserialize, Serialize, Clone)]
28pub struct BitbucketSource {
29    pub branch: BitbucketBranch,
30    pub repository: BitbucketRepository,
31}
32
33#[derive(Debug, Deserialize, Serialize, Clone)]
34pub struct BitbucketDestination {
35    pub branch: BitbucketBranch,
36    pub repository: BitbucketRepository,
37}
38
39#[derive(Debug, Deserialize, Serialize, Clone)]
40pub struct BitbucketPullRequest {
41    pub id: u64,
42    pub title: String,
43    pub state: String,
44    pub author: BitbucketUser,
45    pub source: BitbucketSource,
46    pub destination: BitbucketDestination,
47    pub created_on: String,
48    pub updated_on: String,
49    pub links: HashMap<String, serde_json::Value>,
50}
51
52#[derive(Debug, Deserialize)]
53pub struct BitbucketPullRequestsResponse {
54    pub values: Vec<BitbucketPullRequest>,
55}
56
57pub struct BitbucketClient {
58    client: Client,
59    auth: BitbucketAuth,
60}
61
62impl BitbucketClient {
63    pub fn new(auth: BitbucketAuth) -> Self {
64        let client = Client::new();
65        BitbucketClient { client, auth }
66    }
67
68    fn get_email(&self) -> String {
69        // Use email from auth if available, otherwise use a placeholder
70        self.auth.email().unwrap_or_else(|| "user".to_string())
71    }
72
73    pub async fn get_pull_requests(&self, workspace: &str, repo_slug: &str) -> Result<Vec<BitbucketPullRequest>> {
74        let token = self.auth.get_token()?;
75        let url = format!(
76            "https://api.bitbucket.org/2.0/repositories/{}/{}/pullrequests",
77            workspace, repo_slug
78        );
79
80        let response = self
81            .client
82            .get(&url)
83            .basic_auth(self.get_email(), Some(&token))
84            .header("Accept", "application/json")
85            .send()
86            .await
87            .map_err(|e| Error::network(format!("Failed to send request to Bitbucket API: {}", e)))?;
88
89        if response.status().is_client_error() {
90            let status = response.status();
91            let text = response.text().await.unwrap_or_default();
92
93            if status == 401 {
94                return Err(Error::auth(
95                    "Authentication failed. Please check your Bitbucket credentials and run 'gwt auth bitbucket' to update them."
96                ));
97            } else if status == 404 {
98                return Err(Error::provider(format!(
99                    "Repository not found: {}/{}. Please check the workspace and repository name.",
100                    workspace, repo_slug
101                )));
102            } else {
103                return Err(Error::provider(format!(
104                    "API request failed with status {}: {}",
105                    status, text
106                )));
107            }
108        }
109
110        let pr_response: BitbucketPullRequestsResponse = response
111            .json()
112            .await
113            .map_err(|e| Error::provider(format!("Failed to parse Bitbucket API response: {}", e)))?;
114
115        Ok(pr_response.values)
116    }
117
118    pub async fn test_connection(&self) -> Result<()> {
119        let token = self.auth.get_token()?;
120        let url = "https://api.bitbucket.org/2.0/user";
121
122        let response = self
123            .client
124            .get(url)
125            .basic_auth(self.get_email(), Some(&token))
126            .header("Accept", "application/json")
127            .send()
128            .await
129            .map_err(|e| Error::network(format!("Failed to test Bitbucket API connection: {}", e)))?;
130
131        if response.status().is_success() {
132            println!("✓ Bitbucket API connection successful");
133            Ok(())
134        } else {
135            let status = response.status();
136            if status == 401 {
137                Err(Error::auth(
138                    "Authentication failed. Please check your Bitbucket credentials.",
139                ))
140            } else {
141                Err(Error::provider(format!(
142                    "API connection failed with status: {}",
143                    status
144                )))
145            }
146        }
147    }
148}
149
150pub fn extract_bitbucket_info_from_url(url: &str) -> Option<(String, String)> {
151    // Parse URLs like:
152    // https://bitbucket.org/workspace/repo
153    // git@bitbucket.org:workspace/repo.git
154    // https://bitbucket.org/workspace/repo.git
155
156    if url.contains("bitbucket.org") {
157        if let Some(captures) = regex::Regex::new(r"bitbucket\.org[:/]([^/]+)/([^/\.]+)")
158            .ok()?
159            .captures(url)
160        {
161            let workspace = captures.get(1)?.as_str();
162            let repo = captures.get(2)?.as_str();
163            return Some((workspace.to_string(), repo.to_string()));
164        }
165    }
166
167    None
168}
169
170pub fn is_bitbucket_repository(remote_url: &str) -> bool {
171    remote_url.contains("bitbucket.org")
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_extract_bitbucket_info_https() {
180        let url = "https://bitbucket.org/myworkspace/myrepo";
181        let result = extract_bitbucket_info_from_url(url);
182        assert_eq!(result, Some(("myworkspace".to_string(), "myrepo".to_string())));
183    }
184
185    #[test]
186    fn test_extract_bitbucket_info_https_git() {
187        let url = "https://bitbucket.org/myworkspace/myrepo.git";
188        let result = extract_bitbucket_info_from_url(url);
189        assert_eq!(result, Some(("myworkspace".to_string(), "myrepo".to_string())));
190    }
191
192    #[test]
193    fn test_extract_bitbucket_info_ssh() {
194        let url = "git@bitbucket.org:myworkspace/myrepo.git";
195        let result = extract_bitbucket_info_from_url(url);
196        assert_eq!(result, Some(("myworkspace".to_string(), "myrepo".to_string())));
197    }
198
199    #[test]
200    fn test_extract_bitbucket_info_invalid() {
201        let url = "https://github.com/user/repo";
202        let result = extract_bitbucket_info_from_url(url);
203        assert_eq!(result, None);
204    }
205
206    #[test]
207    fn test_is_bitbucket_repository() {
208        assert!(is_bitbucket_repository("https://bitbucket.org/workspace/repo"));
209        assert!(is_bitbucket_repository("git@bitbucket.org:workspace/repo.git"));
210        assert!(!is_bitbucket_repository("https://github.com/user/repo"));
211    }
212}