git_worktree_cli/
bitbucket_auth.rs

1use 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        // Use workspace/repo as the key identifier for better isolation
18        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        // Check environment variable
26        if let Ok(token) = env::var(TOKEN_ENV_VAR) {
27            if !token.is_empty() {
28                return Ok(token);
29            }
30        }
31
32        // Then check keyring
33        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        // First check environment variable
44        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        // Check env var first
55        if let Ok(token) = env::var(TOKEN_ENV_VAR) {
56            if !token.is_empty() {
57                return true;
58            }
59        }
60
61        // Then check keyring
62        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        // Temporarily remove environment variable for isolated testing
104        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        // The auth should be created successfully
122        assert!(auth.email().is_none());
123    }
124}