git_worktree_cli/
bitbucket_auth.rs1use keyring::Entry;
2use std::env;
3
4use crate::error::{Error, Result};
5
6const SERVICE_NAME: &str = "git-worktree-cli-bitbucket";
7const EMAIL_ENV_VAR: &str = "BITBUCKET_CLOUD_EMAIL";
8const TOKEN_ENV_VAR: &str = "BITBUCKET_CLOUD_API_TOKEN";
9
10pub struct BitbucketAuth {
11 email: Option<String>,
12 token_entry: Entry,
13}
14
15impl BitbucketAuth {
16 pub fn new(workspace: String, repo: String, email: Option<String>) -> Result<Self> {
17 let key_id = format!("{}/{}", workspace, repo);
19 let token_entry = Entry::new(SERVICE_NAME, &key_id)?;
20
21 Ok(BitbucketAuth { email, token_entry })
22 }
23
24 pub fn get_token(&self) -> Result<String> {
25 if let Ok(token) = env::var(TOKEN_ENV_VAR) {
27 if !token.is_empty() {
28 return Ok(token);
29 }
30 }
31
32 self.token_entry.get_password().map_err(|_| {
34 Error::auth(format!(
35 "No Bitbucket Cloud API token found. Please set the {} and {} environment variables.\n\
36 Run 'gwt auth bitbucket-cloud setup' for instructions.",
37 EMAIL_ENV_VAR, TOKEN_ENV_VAR
38 ))
39 })
40 }
41
42 pub fn email(&self) -> Option<String> {
43 if let Ok(email) = env::var(EMAIL_ENV_VAR) {
45 if !email.is_empty() {
46 return Some(email);
47 }
48 }
49
50 self.email.clone()
51 }
52
53 pub fn has_stored_token(&self) -> bool {
54 if let Ok(token) = env::var(TOKEN_ENV_VAR) {
56 if !token.is_empty() {
57 return true;
58 }
59 }
60
61 self.token_entry.get_password().is_ok()
63 }
64}
65
66pub fn get_auth_from_config() -> Result<(String, String, Option<String>)> {
67 use crate::bitbucket_api::extract_bitbucket_info_from_url;
68 use crate::config::GitWorktreeConfig;
69
70 let (_, config) =
71 GitWorktreeConfig::find_config()?.ok_or_else(|| Error::config("No git-worktree-config.jsonc found"))?;
72
73 if !config.repository_url.contains("bitbucket.org") {
74 return Err(Error::provider("This is not a Bitbucket repository"));
75 }
76
77 let (workspace, repo) = extract_bitbucket_info_from_url(&config.repository_url)
78 .ok_or_else(|| Error::provider("Failed to parse Bitbucket repository URL"))?;
79
80 Ok((workspace, repo, config.bitbucket_email))
81}
82
83pub fn display_setup_instructions() {
84 println!("Setting up Bitbucket Cloud authentication\n");
85 println!("1. Create an API token (App Password) at:");
86 println!(" https://bitbucket.org/account/settings/app-passwords/\n");
87 println!("2. Required permissions for the token:");
88 println!(" - Repositories: Read");
89 println!(" - Pull requests: Read\n");
90 println!("3. Copy the generated token\n");
91 println!("4. Set environment variables:");
92 println!(" export {}=your-email@example.com", EMAIL_ENV_VAR);
93 println!(" export {}=YOUR_TOKEN", TOKEN_ENV_VAR);
94 println!("\nNote: The email should match your Bitbucket account email.");
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn test_bitbucket_auth_creation() {
103 env::remove_var(EMAIL_ENV_VAR);
105
106 let auth = BitbucketAuth::new(
107 "myworkspace".to_string(),
108 "myrepo".to_string(),
109 Some("test@example.com".to_string()),
110 );
111 assert!(auth.is_ok());
112
113 let auth = auth.unwrap();
114 assert_eq!(auth.email(), Some("test@example.com".to_string()));
115 }
116
117 #[test]
118 fn test_workspace_repo_key() {
119 let auth = BitbucketAuth::new("workspace".to_string(), "repo".to_string(), None).unwrap();
120
121 assert!(auth.email().is_none());
123 }
124}