use reqwest::Url;
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
use tracing::debug;
#[derive(Debug, Clone)]
pub enum GitCredential {
UsernamePassword { username: String, password: String },
Formatted {
auth_type: String,
credential: String,
},
}
impl GitCredential {
pub fn apply_to_request(
self,
request_builder: reqwest::RequestBuilder,
) -> reqwest::RequestBuilder {
match self {
GitCredential::UsernamePassword { username, password }
if username == "oauth2" || username == "access_token" =>
{
request_builder.header("Authorization", format!("Bearer {password}"))
}
GitCredential::UsernamePassword { username, password } => {
request_builder.basic_auth(username, Some(password))
}
GitCredential::Formatted {
auth_type,
credential,
} => request_builder.header("Authorization", format!("{} {}", auth_type, credential)),
}
}
}
async fn execute_git_credential_fill(
credential_input: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let mut child = tokio::process::Command::new("git")
.args(["credential", "fill"])
.env("GIT_TERMINAL_PROMPT", "0")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
if let Some(stdin) = child.stdin.as_mut() {
stdin.write_all(credential_input.as_bytes()).await?;
stdin.shutdown().await?;
}
let output = child.wait_with_output().await?;
if !output.status.success() {
return Ok(None);
}
Ok(Some(String::from_utf8_lossy(&output.stdout).to_string()))
}
fn create_git_credential_input(url: &Url) -> String {
let mut credential_input = String::new();
credential_input.push_str("capability[]=authtype\n");
credential_input.push_str(&format!("protocol={}\n", url.scheme()));
if let Some(host) = url.host_str() {
let host_with_port = if let Some(port) = url.port() {
format!("{}:{}", host, port)
} else {
host.to_string()
};
credential_input.push_str(&format!("host={}\n", host_with_port));
}
if !url.path().is_empty() && url.path() != "/" {
credential_input.push_str(&format!("path={}\n", url.path().trim_start_matches('/')));
}
credential_input.push('\n'); credential_input
}
fn parse_git_credential_output(output: &str) -> Option<GitCredential> {
let mut username = None;
let mut password = None;
let mut authtype = None;
let mut credential = None;
for line in output.lines() {
if line.is_empty() {
break;
}
if let Some((key, value)) = line.split_once('=') {
match key {
"username" => username = Some(value.to_string()),
"password" => password = Some(value.to_string()),
"authtype" => authtype = Some(value.to_string()),
"credential" => credential = Some(value.to_string()),
_ => {} }
}
}
if let (Some(authtype_str), Some(credential_str)) = (authtype, credential) {
Some(GitCredential::Formatted {
auth_type: authtype_str,
credential: credential_str,
})
} else if let (Some(username), Some(password)) = (username, password) {
Some(GitCredential::UsernamePassword { username, password })
} else {
None
}
}
pub async fn get_credentials_from_git(
url: &Url,
) -> Result<Option<GitCredential>, Box<dyn std::error::Error>> {
debug!("Getting credentials for URL: {}", url);
let credential_input = create_git_credential_input(url);
debug!("Credential input:\n{:?}", credential_input);
let credential_output = execute_git_credential_fill(&credential_input).await?;
debug!("Credential output:\n{:?}", credential_output);
let output = match credential_output {
Some(output_str) => Ok(parse_git_credential_output(&output_str)),
None => Ok(None),
};
debug!("Parsed credentials: {:?}", output);
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_git_credential_output_username_password() {
let output = "protocol=https\nhost=example.com\nusername=testuser\npassword=testpass\n\n";
let result = parse_git_credential_output(output);
match result {
Some(GitCredential::UsernamePassword { username, password }) => {
assert_eq!(username, "testuser");
assert_eq!(password, "testpass");
}
_ => panic!("Expected UsernamePassword credential"),
}
}
#[test]
fn test_parse_git_credential_output_formatted() {
let output = "capability[]=authtype\nprotocol=https\nhost=example.com\nauthtype=Bearer\ncredential=token123\n\n";
let result = parse_git_credential_output(output);
match result {
Some(GitCredential::Formatted {
auth_type,
credential,
}) => {
assert_eq!(auth_type, "Bearer");
assert_eq!(credential, "token123");
}
_ => panic!("Expected Formatted credential"),
}
}
#[test]
fn test_parse_git_credential_output_custom_authtype() {
let output =
"capability[]=authtype\nprotocol=https\nhost=example.com\nauthtype=CustomAuth\ncredential=custom123\n\n";
let result = parse_git_credential_output(output);
match result {
Some(GitCredential::Formatted {
auth_type,
credential,
}) => {
assert_eq!(auth_type, "CustomAuth");
assert_eq!(credential, "custom123");
}
_ => panic!("Expected Formatted credential with custom auth type"),
}
}
#[test]
fn test_parse_git_credential_output_empty() {
let output = "";
let result = parse_git_credential_output(output);
assert!(result.is_none());
}
#[test]
fn test_parse_git_credential_output_incomplete() {
let output = "protocol=https\nhost=example.com\nusername=testuser\n\n";
let result = parse_git_credential_output(output);
assert!(result.is_none());
}
#[test]
fn test_create_git_credential_input_basic() {
let url = Url::parse("https://example.com/").unwrap();
let input = create_git_credential_input(&url);
assert_eq!(
input,
"capability[]=authtype\nprotocol=https\nhost=example.com\n\n"
);
}
#[test]
fn test_create_git_credential_input_with_port() {
let url = Url::parse("https://example.com:8080/").unwrap();
let input = create_git_credential_input(&url);
assert_eq!(
input,
"capability[]=authtype\nprotocol=https\nhost=example.com:8080\n\n"
);
}
#[test]
fn test_create_git_credential_input_with_path() {
let url = Url::parse("https://example.com/path/to/resource").unwrap();
let input = create_git_credential_input(&url);
assert_eq!(
input,
"capability[]=authtype\nprotocol=https\nhost=example.com\npath=path/to/resource\n\n"
);
}
#[test]
fn test_create_git_credential_input_with_port_and_path() {
let url = Url::parse("https://example.com:8080/path/to/resource").unwrap();
let input = create_git_credential_input(&url);
assert_eq!(
input,
"capability[]=authtype\nprotocol=https\nhost=example.com:8080\npath=path/to/resource\n\n"
);
}
#[test]
fn test_create_git_credential_input_http() {
let url = Url::parse("http://example.com/path").unwrap();
let input = create_git_credential_input(&url);
assert_eq!(
input,
"capability[]=authtype\nprotocol=http\nhost=example.com\npath=path\n\n"
);
}
}