use anyhow::Context;
use base64::Engine;
use std::path::Path;
use tokio::process::Command;
use super::{build_clone_args, CloneOpts};
pub async fn clone_ssh(opts: &CloneOpts, dest: &Path) -> anyhow::Result<()> {
let crate::git::types::CredentialKind::Ssh { key } = &opts
.credentials
.as_ref()
.context("missing credentials")?
.kind
else {
anyhow::bail!("expected SSH credentials");
};
let key_data = base64::engine::general_purpose::STANDARD
.decode(key)
.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 repo_url = normalize_ssh_url(&opts.repo_url);
let args = build_clone_args(opts, &repo_url, dest, &[]);
let git_ssh_command = setup_ssh_env(key_file.path());
let status = Command::new("git")
.args(&args)
.env("GIT_SSH_COMMAND", git_ssh_command)
.status()
.await
.context("running git clone (SSH)")?;
if !status.success() {
anyhow::bail!("git clone (SSH) failed with status {status}");
}
Ok(())
}
pub fn setup_ssh_env(key_file_path: &Path) -> String {
format!(
"ssh -i {} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -o HostkeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa",
key_file_path.display()
)
}
pub fn normalize_ssh_url(raw_url: &str) -> String {
if raw_url.contains("source.developers.google") || raw_url.starts_with("ssh://FLUID") {
return raw_url.to_owned();
}
let with_scheme = if raw_url.starts_with("ssh://") {
raw_url.to_owned()
} else if let Some(colon_idx) = raw_url.find(':') {
let before_colon = &raw_url[..colon_idx];
if !before_colon.contains('/') && !raw_url.contains("://") {
format!(
"ssh://{}/{}",
before_colon,
&raw_url[colon_idx.saturating_add(1)..]
)
} else {
raw_url.to_owned()
}
} else {
raw_url.to_owned()
};
if with_scheme.starts_with("ssh://") && !url_has_port(&with_scheme) {
if let [scheme, host, path] = with_scheme.splitn(3, ':').collect::<Vec<_>>().as_slice() {
if !path.starts_with('/') {
return format!("{scheme}:{host}:/{path}");
}
}
}
with_scheme
}
pub fn url_has_port(url: &str) -> bool {
url::Url::parse(url).ok().and_then(|u| u.port()).is_some()
}