diff --git a/crates/sandcage/src/setup.rs b/crates/sandcage/src/setup.rs index 66d9bf7..9352fb3 100644 --- a/crates/sandcage/src/setup.rs +++ b/crates/sandcage/src/setup.rs @@ -823,7 +823,8 @@ pub fn build_ssh_discovery( git_remote_hosts: &[String], ssh_dir: &Path, ) -> Vec { - // Keyed by display_host for deduplication and sorted output. + // Keyed by Host alias (not resolved HostName) to preserve distinct blocks + // that point to the same server with different keys. let mut map: BTreeMap = BTreeMap::new(); // 1. Add SSH config blocks where user is git. @@ -831,19 +832,18 @@ pub fn build_ssh_discovery( if !block.is_git_user() { continue; } - // display_host = HostName directive, or first alias as fallback. + let alias = match block.aliases.first() { + Some(a) => a.clone(), + None => continue, + }; let display_host = block .hostname() - .unwrap_or_else(|| block.aliases.first().map(|s| s.as_str()).unwrap_or("")) + .unwrap_or(alias.as_str()) .to_string(); - if display_host.is_empty() { - continue; - } - let ssh_host_alias = block.aliases.first().cloned(); let identity_file = block.identity_file().map(|s| s.to_string()); - map.entry(display_host.clone()).or_insert_with(|| SshDiscoveryEntry { + map.entry(alias.clone()).or_insert_with(|| SshDiscoveryEntry { display_host, - ssh_host_alias, + ssh_host_alias: Some(alias), identity_file, host_block: Some(block.clone()), source: SshDiscoverySource::SshConfig, @@ -852,9 +852,13 @@ pub fn build_ssh_discovery( // 2. Process git remote hosts. for git_host in git_remote_hosts { - if let Some(entry) = map.get_mut(git_host.as_str()) { - // Hostname already present via SSH config — upgrade source. - entry.source = SshDiscoverySource::Both; + // Check if this hostname matches any existing entry (by alias or display_host). + let existing_key = map.iter() + .find(|(_, e)| e.display_host == *git_host || e.ssh_host_alias.as_deref() == Some(git_host)) + .map(|(k, _)| k.clone()); + + if let Some(key) = existing_key { + map.get_mut(&key).unwrap().source = SshDiscoverySource::Both; } else { // Check if any SSH block has this git_host as an alias. let alias_match = ssh_blocks.iter().find(|b| { @@ -862,24 +866,9 @@ pub fn build_ssh_discovery( }); if let Some(block) = alias_match { - // The display_host should already be in the map via step 1, but just in case - // the block wasn't added (e.g., HostName was set and we keyed on HostName), - // upgrade source on whichever entry owns this block's alias. - let display = block - .hostname() - .unwrap_or_else(|| block.aliases.first().map(|s| s.as_str()).unwrap_or("")) - .to_string(); - if let Some(existing) = map.get_mut(&display) { + let alias = block.aliases.first().cloned().unwrap_or_default(); + if let Some(existing) = map.get_mut(&alias) { existing.source = SshDiscoverySource::Both; - } else { - // Shouldn't normally happen, but handle gracefully. - map.insert(display.clone(), SshDiscoveryEntry { - display_host: display, - ssh_host_alias: block.aliases.first().cloned(), - identity_file: block.identity_file().map(|s| s.to_string()), - host_block: Some(block.clone()), - source: SshDiscoverySource::Both, - }); } } else { // No SSH config match — pure git remote entry. @@ -1310,6 +1299,37 @@ Host myserver assert!(gitlab.ssh_host_alias.is_none()); } + #[test] + fn discover_preserves_distinct_aliases_for_same_hostname() { + use crate::ssh_config::parse_ssh_config; + let ssh_config_text = "\ +Host sparkle.g4b.org + Hostname git.g4b.org + User git + IdentityFile ~/.ssh/id_rsa_sparkle + +Host git.g4b.org + User git + IdentityFile ~/.ssh/id_rsa +"; + let blocks = parse_ssh_config(ssh_config_text); + let git_remote_hosts = vec!["git.g4b.org".to_string()]; + let ssh_dir = Path::new("/nonexistent/.ssh"); + + let entries = build_ssh_discovery(&blocks, &git_remote_hosts, ssh_dir); + + assert_eq!(entries.len(), 2, "both aliases should be preserved"); + + let sparkle = entries.iter().find(|e| e.ssh_host_alias.as_deref() == Some("sparkle.g4b.org")).unwrap(); + assert_eq!(sparkle.display_host, "git.g4b.org"); + assert_eq!(sparkle.identity_file.as_deref(), Some("~/.ssh/id_rsa_sparkle")); + + let direct = entries.iter().find(|e| e.ssh_host_alias.as_deref() == Some("git.g4b.org")).unwrap(); + assert_eq!(direct.display_host, "git.g4b.org"); + assert_eq!(direct.identity_file.as_deref(), Some("~/.ssh/id_rsa")); + assert_eq!(direct.source, SshDiscoverySource::Both); + } + // -- has_legacy_ssh_mount tests -- #[test]