- Updating to support Bitbucket App Passwords

- Improved boundaries for several rules
- Added more rules
This commit is contained in:
Mick Grove 2025-11-20 16:33:28 -08:00
commit 17e0ca3594
11 changed files with 245 additions and 37 deletions

View file

@ -4,6 +4,7 @@ use std::{
};
use tracing::{debug, debug_span};
use url::Url;
use crate::{bitbucket::is_bitbucket_access_token, git_url::GitUrl};
@ -101,6 +102,8 @@ pub struct Git {
credentials: Vec<String>,
ignore_certs: bool,
bitbucket_access_token: Option<String>,
bitbucket_env: Vec<(String, String)>,
bitbucket_basic_auth: Option<(String, String)>,
}
impl Git {
@ -110,23 +113,60 @@ impl Git {
pub fn new(ignore_certs: bool) -> Self {
let mut credentials = Vec::new();
fn normalized_env_var(name: &str) -> Option<String> {
std::env::var(name)
.ok()
.map(|value| value.trim().to_owned())
.filter(|value| !value.is_empty())
}
let bitbucket_username = normalized_env_var("KF_BITBUCKET_USERNAME");
let bitbucket_app_password = normalized_env_var("KF_BITBUCKET_APP_PASSWORD");
let bitbucket_token = normalized_env_var("KF_BITBUCKET_TOKEN");
let bitbucket_password = normalized_env_var("KF_BITBUCKET_PASSWORD");
let bitbucket_oauth_token = normalized_env_var("KF_BITBUCKET_OAUTH_TOKEN");
let mut bitbucket_env = Vec::new();
for (key, value) in [
("KF_BITBUCKET_USERNAME", bitbucket_username.as_ref()),
("KF_BITBUCKET_APP_PASSWORD", bitbucket_app_password.as_ref()),
("KF_BITBUCKET_TOKEN", bitbucket_token.as_ref()),
("KF_BITBUCKET_PASSWORD", bitbucket_password.as_ref()),
("KF_BITBUCKET_OAUTH_TOKEN", bitbucket_oauth_token.as_ref()),
] {
if let Some(value) = value {
bitbucket_env.push((key.to_string(), value.to_string()));
}
}
let has_github_token =
matches!(std::env::var("KF_GITHUB_TOKEN"), Ok(token) if !token.is_empty());
let has_gitlab_token =
matches!(std::env::var("KF_GITLAB_TOKEN"), Ok(token) if !token.is_empty());
let has_gitea_token =
matches!(std::env::var("KF_GITEA_TOKEN"), Ok(token) if !token.is_empty());
let has_bitbucket_username =
matches!(std::env::var("KF_BITBUCKET_USERNAME"), Ok(value) if !value.is_empty());
let bitbucket_access_token = std::env::var("KF_BITBUCKET_TOKEN")
.ok()
.filter(|value| !value.is_empty() && is_bitbucket_access_token(value));
let has_bitbucket_password =
["KF_BITBUCKET_APP_PASSWORD", "KF_BITBUCKET_TOKEN", "KF_BITBUCKET_PASSWORD"]
.iter()
.any(|key| matches!(std::env::var(key), Ok(value) if !value.is_empty()));
let has_bitbucket_oauth_token =
matches!(std::env::var("KF_BITBUCKET_OAUTH_TOKEN"), Ok(value) if !value.is_empty());
let bitbucket_access_token =
bitbucket_token.as_ref().filter(|token| is_bitbucket_access_token(token)).cloned();
let bitbucket_basic_password = bitbucket_app_password
.clone()
.or(bitbucket_token.clone())
.or(bitbucket_password.clone());
let bitbucket_basic_auth = if let Some(token) = bitbucket_oauth_token.clone() {
Some(("x-token-auth".to_string(), token))
} else if let Some(token) = bitbucket_access_token.clone() {
Some(("x-token-auth".to_string(), token))
} else if let (Some(username), Some(password)) =
(bitbucket_username.clone(), bitbucket_basic_password)
{
Some((username, password))
} else {
None
};
let has_bitbucket_username = bitbucket_username.is_some();
let has_bitbucket_password = bitbucket_app_password.is_some()
|| bitbucket_token.is_some()
|| bitbucket_password.is_some();
let has_bitbucket_oauth_token = bitbucket_oauth_token.is_some();
let has_bitbucket_credentials = has_bitbucket_oauth_token
|| bitbucket_access_token.is_some()
|| (has_bitbucket_username && has_bitbucket_password);
@ -186,7 +226,13 @@ impl Git {
credentials.push(HUGGINGFACE_CREDENTIAL_HELPER.into());
}
Self { credentials, ignore_certs, bitbucket_access_token }
Self {
credentials,
ignore_certs,
bitbucket_access_token,
bitbucket_env,
bitbucket_basic_auth,
}
}
/// Create a basic `git` `Command` with environment variables set to
@ -201,6 +247,9 @@ impl Git {
if self.ignore_certs {
cmd.env("GIT_SSL_NO_VERIFY", "1");
}
for (key, value) in &self.bitbucket_env {
cmd.env(key, value);
}
if let Some(token) = &self.bitbucket_access_token {
cmd.env("KF_BITBUCKET_ACCESS_TOKEN", token);
}
@ -268,11 +317,31 @@ impl Git {
cmd.arg("--quiet");
cmd.arg("-c");
cmd.arg("remote.origin.fetch=+refs/*:refs/remotes/origin/*");
cmd.arg(repo_url.as_str());
cmd.arg(self.repo_arg_for_clone(repo_url));
cmd.arg(output_dir);
debug!("{cmd:#?}");
self.run_cmd(cmd)
}
fn repo_arg_for_clone(&self, repo_url: &GitUrl) -> String {
if let Some((username, password)) = &self.bitbucket_basic_auth {
if let Ok(mut url) = Url::parse(repo_url.as_str()) {
if url
.host_str()
.map(|host| host.eq_ignore_ascii_case("bitbucket.org"))
.unwrap_or(false)
{
if url.set_username(username).is_ok()
&& url.set_password(Some(password)).is_ok()
{
return url.into();
}
}
}
}
repo_url.as_str().to_string()
}
}
impl Default for Git {
@ -349,6 +418,61 @@ mod tests {
);
}
#[test]
fn test_repo_arg_for_clone_includes_bitbucket_app_password() {
let url =
GitUrl::try_from(url::Url::parse("https://bitbucket.org/workspace/demo.git").unwrap())
.unwrap();
temp_env::with_vars(
&[
("KF_BITBUCKET_USERNAME", Some("user")),
("KF_BITBUCKET_APP_PASSWORD", Some("secret")),
],
|| {
let git = Git::new(false);
assert_eq!(
git.repo_arg_for_clone(&url),
"https://user:secret@bitbucket.org/workspace/demo.git"
);
},
);
}
#[test]
fn test_repo_arg_for_clone_uses_token_auth_when_available() {
let url =
GitUrl::try_from(url::Url::parse("https://bitbucket.org/workspace/demo.git").unwrap())
.unwrap();
temp_env::with_vars(&[("KF_BITBUCKET_OAUTH_TOKEN", Some("token123"))], || {
let git = Git::new(false);
assert_eq!(
git.repo_arg_for_clone(&url),
"https://x-token-auth:token123@bitbucket.org/workspace/demo.git"
);
});
}
#[test]
fn test_repo_arg_for_clone_leaves_non_bitbucket_urls_untouched() {
let url = GitUrl::try_from(
url::Url::parse("https://github.com/octocat/Hello-World.git").unwrap(),
)
.unwrap();
temp_env::with_vars(
&[
("KF_BITBUCKET_USERNAME", Some("user")),
("KF_BITBUCKET_APP_PASSWORD", Some("secret")),
],
|| {
let git = Git::new(false);
assert_eq!(git.repo_arg_for_clone(&url), url.as_str());
},
);
}
#[test]
fn test_git_new_bitbucket_access_token() {
let token = "AT1234567890_ACCESS_TOKEN_EXAMPLE_WITH_UNDERSCORE";
@ -360,6 +484,30 @@ mod tests {
});
}
#[test]
fn test_git_new_bitbucket_trims_whitespace() {
let trimmed_token = "AT1234567890_ACCESS_TOKEN_EXAMPLE_WITH_UNDERSCORE";
let token = format!(" {trimmed_token} \n");
temp_env::with_vars(
&[("KF_BITBUCKET_USERNAME", Some(" user\n")), ("KF_BITBUCKET_TOKEN", Some(&token))],
|| {
let git = Git::new(false);
assert_eq!(
git.bitbucket_env,
vec![
("KF_BITBUCKET_USERNAME".to_string(), "user".to_string()),
("KF_BITBUCKET_TOKEN".to_string(), trimmed_token.to_string(),),
],
);
assert_eq!(git.credentials.len(), 4);
assert!(git.credentials.iter().any(|value| value == BITBUCKET_CREDENTIAL_HELPER));
assert_eq!(git.bitbucket_access_token.as_deref(), Some(trimmed_token));
},
);
}
#[test]
fn test_clone_mode_arg() {
assert_eq!(CloneMode::Bare.arg(), Some("--bare"));