use crate::config;
use anyhow::{anyhow, Result};
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Scheme {
Auto,
Ssh,
Https,
}
pub(crate) fn scheme_from(ssh: bool, https: bool) -> Scheme {
if ssh {
Scheme::Ssh
} else if https {
Scheme::Https
} else {
Scheme::Auto
}
}
pub(crate) struct Remote {
pub(crate) raw: String,
pub(crate) https: Option<String>,
pub(crate) ssh: Option<String>,
pub(crate) shorthand: Option<String>,
}
pub(crate) fn parse_remote(input: &str) -> Remote {
let raw = input.to_string();
if let Some(rest) = input.strip_prefix("git@") {
if let Some((host, path)) = rest.split_once(':') {
return gh_remote(raw, host, path.trim_end_matches(".git"));
}
}
if let Some((_scheme, after)) = input.split_once("://") {
let after = after.rsplit_once('@').map_or(after, |(_, h)| h); if let Some((host, path)) = after.split_once('/') {
return gh_remote(
raw,
host,
path.trim_end_matches('/').trim_end_matches(".git"),
);
}
}
if !input.contains("://")
&& !input.contains(':')
&& input.matches('/').count() == 1
&& !input.starts_with(['/', '.', '~'])
{
return gh_remote(raw, "github.com", input.trim_end_matches(".git"));
}
Remote {
raw,
https: None,
ssh: None,
shorthand: None,
}
}
fn gh_remote(raw: String, host: &str, path: &str) -> Remote {
Remote {
raw,
https: Some(format!("https://{host}/{path}.git")),
ssh: Some(format!("git@{host}:{path}.git")),
shorthand: (host == "github.com").then(|| path.to_string()),
}
}
fn prefer_ssh() -> bool {
match std::env::var("CONFER_SCHEME").ok().as_deref() {
Some("ssh") => return true,
Some("https") => return false,
_ => {}
}
if let Ok(home) = config::home() {
let sshdir = home.join(".ssh");
if sshdir.join("config").exists() {
return true;
}
if let Ok(rd) = std::fs::read_dir(&sshdir) {
for e in rd.flatten() {
let n = e.file_name();
let n = n.to_string_lossy();
if n.starts_with("id_") && !n.ends_with(".pub") {
return true;
}
}
}
}
std::process::Command::new("ssh-add")
.arg("-l")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub(crate) fn clone_url_candidates(url: &str, remote: &Remote, scheme: Scheme) -> Vec<String> {
if scheme != Scheme::Auto {
return clone_candidates(remote, scheme);
}
if url.starts_with("https://") || url.starts_with("http://") {
clone_candidates(remote, Scheme::Https)
.into_iter()
.chain(remote.ssh.clone())
.collect()
} else if url.starts_with("git@") || url.starts_with("ssh://") {
clone_candidates(remote, Scheme::Ssh)
.into_iter()
.chain(remote.https.clone())
.collect()
} else {
clone_candidates(remote, Scheme::Auto)
}
}
pub(crate) fn clone_candidates(r: &Remote, scheme: Scheme) -> Vec<String> {
match (scheme, &r.ssh, &r.https) {
(Scheme::Ssh, Some(s), _) => vec![s.clone()],
(Scheme::Https, _, Some(h)) => vec![h.clone()],
(Scheme::Auto, Some(s), Some(h)) => {
if prefer_ssh() {
vec![s.clone(), h.clone()]
} else {
vec![h.clone(), s.clone()]
}
}
_ => vec![r.raw.clone()],
}
}
fn expand_key_path(path: &str) -> std::path::PathBuf {
if path == "~" {
config::home().unwrap_or_else(|_| std::path::PathBuf::from(path))
} else if let Some(rest) = path.strip_prefix("~/") {
config::home()
.map(|h| h.join(rest))
.unwrap_or_else(|_| std::path::PathBuf::from(path))
} else {
std::path::PathBuf::from(path)
}
}
pub(crate) fn git_ssh_command(key: &str) -> String {
let expanded = expand_key_path(key);
format!(
"ssh -i '{}' -o IdentitiesOnly=yes -o IdentityAgent=none -o BatchMode=yes -o ConnectTimeout=30",
expanded.display()
)
}
pub(crate) fn validate_transport_key(path: &str) -> Result<()> {
let expanded = expand_key_path(path);
let s = expanded.to_string_lossy();
if s.contains('\'') || s.chars().any(|c| c.is_control()) {
return Err(anyhow!(
"--ssh-key path (expanded: {s}) contains a single-quote or control character — use a plain filesystem path"
));
}
if !expanded.is_file() {
return Err(anyhow!("--ssh-key {s}: not a readable key file"));
}
Ok(())
}