git_worktree_cli/
bitbucket_api.rs1use 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 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 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}