From cc7e1ad29e2bdd7952068068b3eb8a9defa73fe2 Mon Sep 17 00:00:00 2001 From: Matthew West Date: Mon, 25 May 2026 19:31:52 +0100 Subject: [PATCH] fix(parser): only block alias merge when HostName itself depends on %h/%n - Adds Host::hostname_depends_on_pattern() helper. - Fixes aliases with %h in ControlPath being split into separate rows. Likely also solves for PR #147. --- src/ssh_config/host.rs | 84 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/src/ssh_config/host.rs b/src/ssh_config/host.rs index 5a16d22..7d24f8e 100644 --- a/src/ssh_config/host.rs +++ b/src/ssh_config/host.rs @@ -40,6 +40,13 @@ impl Host { } } + /// Does HostName expand differently for each pattern? + fn hostname_depends_on_pattern(&self) -> bool { + self.entries + .get(&EntryType::Hostname) + .is_some_and(|hostname| hostname.contains("%h") || hostname.contains("%n")) + } + #[allow(clippy::must_use_candidate)] pub fn get_patterns(&self) -> &Vec { &self.patterns @@ -138,11 +145,7 @@ impl HostVecExt for Vec { continue; } - if current_host - .entries - .values() - .any(|value| value.contains("%h")) - { + if current_host.hostname_depends_on_pattern() { continue; } @@ -333,4 +336,75 @@ mod tests { assert_eq!(hosts[2].entries.len(), 1); assert_eq!(hosts[2].entries[&EntryType::Hostname], "%h.com"); } + + #[test] + fn test_merge_same_hosts_with_literal_hostname_and_control_path() { + let mut hosts = Vec::new(); + + let mut host = Host::new(vec!["example.com".to_string()]); + host.update((EntryType::Hostname, "example.com".to_string())); + host.update((EntryType::User, "user".to_string())); + host.update(( + EntryType::ControlPath, + "~/.ssh/control/%r@%h:%p".to_string(), + )); + hosts.push(host); + + let mut host = Host::new(vec!["example".to_string()]); + host.update((EntryType::Hostname, "example.com".to_string())); + host.update((EntryType::User, "user".to_string())); + host.update(( + EntryType::ControlPath, + "~/.ssh/control/%r@%h:%p".to_string(), + )); + hosts.push(host); + + let hosts = hosts.merge_same_hosts(); + + assert_eq!(hosts.len(), 1); + assert_eq!(hosts[0].patterns.len(), 2); + assert_eq!(hosts[0].patterns[0], "example.com"); + assert_eq!(hosts[0].patterns[1], "example"); + assert_eq!(hosts[0].entries.len(), 3); + assert_eq!(hosts[0].entries[&EntryType::Hostname], "example.com"); + assert_eq!(hosts[0].entries[&EntryType::User], "user"); + assert_eq!( + hosts[0].entries[&EntryType::ControlPath], + "~/.ssh/control/%r@%h:%p" + ); + } + + #[test] + fn test_merge_same_hosts_with_h_in_hostname_still_blocks() { + let mut hosts = Vec::new(); + + let mut host = Host::new(vec!["s1".to_string()]); + host.update((EntryType::Hostname, "%h.example.com".to_string())); + hosts.push(host); + + let mut host = Host::new(vec!["s2".to_string()]); + host.update((EntryType::Hostname, "%h.example.com".to_string())); + hosts.push(host); + + let hosts = hosts.merge_same_hosts(); + + assert_eq!(hosts.len(), 2); + } + + #[test] + fn test_merge_same_hosts_with_n_in_hostname_still_blocks() { + let mut hosts = Vec::new(); + + let mut host = Host::new(vec!["s1".to_string()]); + host.update((EntryType::Hostname, "%n.example.com".to_string())); + hosts.push(host); + + let mut host = Host::new(vec!["s2".to_string()]); + host.update((EntryType::Hostname, "%n.example.com".to_string())); + hosts.push(host); + + let hosts = hosts.merge_same_hosts(); + + assert_eq!(hosts.len(), 2); + } }