use std::process::Command;
use crate::config::{self, Config};
use crate::matcher;
pub fn status(config: &Config) {
let global_cmd = Command::new("git")
.args(["config", "--global", "core.sshCommand"])
.output();
let is_active = global_cmd.as_ref().is_ok_and(|o| {
o.status.success() && String::from_utf8_lossy(&o.stdout).trim().contains("pickey")
});
if is_active {
println!("pickey: active ({} rules)", config.rules.len());
} else {
println!("pickey: not active");
println!(" Run `pickey init` to set up");
return;
}
let in_repo = Command::new("git")
.args(["rev-parse", "--git-dir"])
.output()
.is_ok_and(|o| o.status.success());
if !in_repo {
return;
}
if let Some(remote_url) = get_first_remote_url() {
if let Some((host, path)) = parse_remote_url(&remote_url) {
println!("\nThis repo: {}:{}", host, path);
match matcher::find_match(&config.rules, &host, &path) {
Some(m) => {
let key_name = m.rule.key.rsplit('/').next().unwrap_or(&m.rule.key);
print!("SSH key: {} (rule #{})", key_name, m.rule_index + 1);
if let Some(port) = m.rule.port {
print!(", port {}", port);
}
println!();
if let Some(email) = &m.rule.email {
println!(
"Commits: {} <{}>",
m.rule.name.as_deref().unwrap_or(""),
email
);
}
}
None => {
println!("SSH key: no matching rule (will use default)");
}
}
}
} else {
println!("\nThis repo: no remotes configured");
}
}
fn get_first_remote_url() -> Option<String> {
let output = Command::new("git")
.args(["remote", "get-url", "origin"])
.output();
if let Ok(o) = output {
if o.status.success() {
let url = String::from_utf8_lossy(&o.stdout).trim().to_string();
if !url.is_empty() {
return Some(url);
}
}
}
let output = Command::new("git").args(["remote"]).output().ok()?;
if !output.status.success() {
return None;
}
let first = String::from_utf8_lossy(&output.stdout)
.lines()
.next()?
.to_string();
if first.is_empty() {
return None;
}
let output = Command::new("git")
.args(["remote", "get-url", &first])
.output()
.ok()?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
None
}
}
pub fn check(config: &Config, url: &str) {
let (host, path) = match parse_remote_url(url) {
Some(v) => v,
None => {
match url.split_once(':') {
Some((h, p)) => {
let p = p.strip_suffix(".git").unwrap_or(p);
(h.to_string(), p.to_string())
}
None => {
eprintln!("Could not parse URL: {}", url);
return;
}
}
}
};
println!("Host: {}", host);
println!("Path: {}", path);
match matcher::find_match(&config.rules, &host, &path) {
Some(m) => {
let key_path = m.rule.expanded_key();
println!("Rule: #{}", m.rule_index + 1);
if let Some(pat) = &m.rule.match_pattern {
println!("Match: {}", pat);
}
println!("Key: {}", m.rule.key);
println!(
"File: {} {}",
key_path.display(),
if key_path.exists() { "✓" } else { "✗" }
);
if let Some(port) = m.rule.port {
println!("Port: {}", port);
}
if let Some(email) = &m.rule.email {
println!("Email: {}", email);
}
if let Some(name) = &m.rule.name {
println!("Name: {}", name);
}
}
None => {
println!("No matching rule. SSH will use default key selection.");
}
}
}
pub fn list(config: &Config) {
if config.rules.is_empty() {
println!("No rules configured.");
println!("Config: {}", config::default_config_path().display());
return;
}
for (i, rule) in config.rules.iter().enumerate() {
let key_path = rule.expanded_key();
let exists = key_path.exists();
println!(
"#{} {}{}",
i + 1,
rule.host,
rule.match_pattern
.as_ref()
.map(|p| format!("/{}", p))
.unwrap_or_default()
);
println!(
" Key: {} {}",
rule.key,
if exists { "✓" } else { "✗ (not found)" },
);
if let Some(email) = &rule.email {
println!(" Email: {}", email);
}
if let Some(name) = &rule.name {
println!(" Name: {}", name);
}
println!();
}
}
pub fn test(config: &Config) {
let output = Command::new("git")
.args(["remote", "get-url", "origin"])
.output();
let remote_url = match output {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
_ => {
eprintln!("Not in a git repo or no 'origin' remote found.");
return;
}
};
let (host, path) = match parse_remote_url(&remote_url) {
Some(v) => v,
None => {
eprintln!("Could not parse remote URL: {}", remote_url);
return;
}
};
let m = match matcher::find_match(&config.rules, &host, &path) {
Some(m) => m,
None => {
eprintln!("No matching rule for {}", remote_url);
eprintln!("Testing with default SSH key...");
let result = Command::new("ssh")
.args(["-T", &format!("git@{}", host)])
.output();
if let Ok(o) = result {
let out = String::from_utf8_lossy(&o.stderr);
println!("{}", out.trim());
}
return;
}
};
let key_path = m.rule.expanded_key();
println!("Testing SSH to {} with key {}", host, m.rule.key);
let result = Command::new("ssh")
.args([
"-T",
"-i",
&key_path.to_string_lossy(),
"-o",
"IdentitiesOnly=yes",
&format!("git@{}", host),
])
.output();
match result {
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
let stdout = String::from_utf8_lossy(&o.stdout);
if !stderr.is_empty() {
println!("{}", stderr.trim());
}
if !stdout.is_empty() {
println!("{}", stdout.trim());
}
}
Err(e) => {
eprintln!("SSH failed: {}", e);
}
}
}
pub fn parse_remote_url(url: &str) -> Option<(String, String)> {
if let Some(rest) = url.strip_prefix("ssh://") {
let rest = rest.split_once('@').map(|(_, r)| r).unwrap_or(rest);
let (host, path) = if let Some((before_slash, after_slash)) = rest.split_once('/') {
let host = before_slash.split(':').next().unwrap_or(before_slash);
(host.to_string(), after_slash.to_string())
} else {
return None;
};
let path = path.strip_suffix(".git").unwrap_or(&path).to_string();
return Some((host, path));
}
if let Some(at_pos) = url.find('@') {
let after_at = &url[at_pos + 1..];
if let Some(colon_pos) = after_at.find(':') {
let host = &after_at[..colon_pos];
let path = &after_at[colon_pos + 1..];
let path = path.strip_suffix(".git").unwrap_or(path);
let path = path.strip_prefix('/').unwrap_or(path);
return Some((host.to_string(), path.to_string()));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_scp_style() {
let (host, path) = parse_remote_url("git@github.com:VolvoGroup-Internal/repo.git").unwrap();
assert_eq!(host, "github.com");
assert_eq!(path, "VolvoGroup-Internal/repo");
}
#[test]
fn parse_ssh_url() {
let (host, path) = parse_remote_url("ssh://git@github.com/Org/repo.git").unwrap();
assert_eq!(host, "github.com");
assert_eq!(path, "Org/repo");
}
#[test]
fn parse_ssh_url_with_port() {
let (host, path) = parse_remote_url("ssh://git@github.com:22/Org/repo.git").unwrap();
assert_eq!(host, "github.com");
assert_eq!(path, "Org/repo");
}
#[test]
fn parse_no_dot_git() {
let (host, path) = parse_remote_url("git@github.com:Org/repo").unwrap();
assert_eq!(host, "github.com");
assert_eq!(path, "Org/repo");
}
#[test]
fn parse_azure_devops() {
let (host, path) =
parse_remote_url("git@ssh.dev.azure.com:v3/ClientX/Project/Repo").unwrap();
assert_eq!(host, "ssh.dev.azure.com");
assert_eq!(path, "v3/ClientX/Project/Repo");
}
}