use anyhow::{Context, Result};
use base64::Engine;
use std::time::Duration;
use tokio::process::Command;
use super::types::{CredentialKind, Credentials, LsRemoteResult};
use super::aws;
use crate::git::clone::https::embed_credentials_in_url;
use crate::git::clone::ssh::{normalize_ssh_url, setup_ssh_env};
pub async fn ls_remote(
url: &str,
branch: &str,
credentials: Option<&Credentials>,
follow_redirects: bool,
) -> Result<LsRemoteResult> {
match credentials.map(|c| &c.kind) {
Some(CredentialKind::Ssh { key }) => {
ls_remote_ssh(url, branch, key, follow_redirects).await
}
Some(CredentialKind::AwsRole { arn, external_id }) => {
ls_remote_codecommit(url, branch, arn, external_id).await
}
Some(CredentialKind::Https { user, password }) => {
let repo_url = embed_credentials_in_url(url, user, password)?;
ls_remote_https(&repo_url, branch, follow_redirects, &[]).await
}
Some(CredentialKind::Token {
token,
is_pat,
oauth_type: _,
}) => {
if *is_pat {
let encoded = base64::engine::general_purpose::STANDARD.encode(format!(":{token}"));
let extra_config = vec![
"-c".to_string(),
format!("http.extraHeader=Authorization: Basic {encoded}"),
];
ls_remote_https(url, branch, follow_redirects, &extra_config).await
} else {
let user = if url.to_lowercase().contains("bitbucket") {
"x-token-auth"
} else {
"oauth2"
};
let repo_url = embed_credentials_in_url(url, user, token)?;
ls_remote_https(&repo_url, branch, follow_redirects, &[]).await
}
}
None => ls_remote_https(url, branch, follow_redirects, &[]).await,
}
}
async fn ls_remote_ssh(
url: &str,
branch: &str,
key_b64: &str,
follow_redirects: bool,
) -> Result<LsRemoteResult> {
let key_data = base64::engine::general_purpose::STANDARD
.decode(key_b64)
.context("decoding SSH key")?;
let key_file = tempfile::Builder::new()
.prefix("melts-ssh-key-")
.tempfile()
.context("creating SSH key file")?;
std::fs::write(key_file.path(), &key_data).context("writing SSH key")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(key_file.path(), std::fs::Permissions::from_mode(0o400))
.context("setting SSH key permissions")?;
}
let git_ssh_command = setup_ssh_env(key_file.path());
let normalized_url = normalize_ssh_url(url);
let mut args: Vec<String> = Vec::new();
if follow_redirects {
args.extend(["-c".to_string(), "http.followRedirects=true".to_string()]);
}
args.extend([
"ls-remote".to_string(),
"--".to_string(),
normalized_url,
branch.to_string(),
]);
let output = tokio::time::timeout(
Duration::from_secs(20),
Command::new("git")
.args(&args)
.env("GIT_SSH_COMMAND", &git_ssh_command)
.output(),
)
.await
.context("git ls-remote timed out")?
.context("running git ls-remote (SSH)")?;
parse_ls_remote_output(&output)
}
async fn ls_remote_https(
url: &str,
branch: &str,
follow_redirects: bool,
extra_config: &[String],
) -> Result<LsRemoteResult> {
let mut args = vec!["-c".to_string(), "http.sslVerify=false".to_string()];
if follow_redirects {
args.extend(["-c".to_string(), "http.followRedirects=true".to_string()]);
}
args.extend(extra_config.iter().cloned());
args.extend([
"ls-remote".to_string(),
"--".to_string(),
url.to_string(),
branch.to_string(),
]);
let output = tokio::time::timeout(
Duration::from_secs(20),
Command::new("git").args(&args).output(),
)
.await
.context("git ls-remote timed out")?
.context("running git ls-remote (HTTPS)")?;
parse_ls_remote_output(&output)
}
async fn ls_remote_codecommit(
url: &str,
branch: &str,
arn: &str,
external_id: &str,
) -> Result<LsRemoteResult> {
let creds = aws::assume_role(arn, external_id, url).await?;
let args = vec![
"-c".to_string(),
"http.sslVerify=false".to_string(),
"ls-remote".to_string(),
"--".to_string(),
url.to_string(),
branch.to_string(),
];
let output = tokio::time::timeout(
Duration::from_secs(20),
Command::new("git")
.args(&args)
.env("AWS_ACCESS_KEY_ID", &creds.access_key)
.env("AWS_SECRET_ACCESS_KEY", &creds.secret_key)
.env("AWS_SESSION_TOKEN", &creds.session_token)
.env("AWS_DEFAULT_REGION", &creds.region)
.output(),
)
.await
.context("git ls-remote timed out")?
.context("running git ls-remote (CodeCommit)")?;
parse_ls_remote_output(&output)
}
fn parse_ls_remote_output(output: &std::process::Output) -> Result<LsRemoteResult> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Ok(LsRemoteResult {
commit: None,
error: Some(stderr),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
let commit = stdout
.lines()
.next()
.and_then(|line| line.split('\t').next())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
Ok(LsRemoteResult {
commit,
error: None,
})
}