diff --git a/.gitignore b/.gitignore index ce26899..31e7785 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,6 @@ result # Added by cargo - +.idea/ +.zed/ /target diff --git a/src/ssh.rs b/src/ssh.rs index cc79745..4d93f01 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -66,34 +66,96 @@ impl From for ParseConfigError { } } -/// # Errors -/// -/// Will return `Err` if the SSH configuration file cannot be parsed. -pub fn parse_config(raw_path: &String) -> Result, ParseConfigError> { - let normalized_path = shellexpand::tilde(&raw_path).to_string(); - let path = std::fs::canonicalize(normalized_path)?; - - let hosts = ssh_config::Parser::new() - .parse_file(path)? - .apply_patterns() +/// Process raw host configurations, apply empty hostname logic and convert to Host structs +/// +/// # Arguments +/// * `raw_hosts` - List of raw host configurations parsed from SSH config file +/// +/// # Returns +/// List of processed Host structs +pub fn process_hosts(raw_hosts: Vec) -> Vec { + // Apply configuration processing in optimal order and convert to Host structs + raw_hosts .apply_name_to_empty_hostname() + .apply_patterns() .merge_same_hosts() .iter() .map(|host| Host { - name: host - .get_patterns() - .first() - .unwrap_or(&String::new()) - .clone(), + name: host.get_patterns().first().unwrap_or(&String::new()).clone(), aliases: host.get_patterns().iter().skip(1).join(", "), user: host.get(&ssh_config::EntryType::User), - destination: host - .get(&ssh_config::EntryType::Hostname) - .unwrap_or_default(), + destination: host.get(&ssh_config::EntryType::Hostname).unwrap_or_default(), port: host.get(&ssh_config::EntryType::Port), proxy_command: host.get(&ssh_config::EntryType::ProxyCommand), }) - .collect(); + .collect() +} + +/// # Errors +/// +/// Will return `Err` if the SSH configuration file cannot be parsed. +pub fn parse_config(raw_path: &String) -> Result, ParseConfigError> { + let normalized_path = shellexpand::tilde(&raw_path).to_string(); + let path = std::fs::canonicalize(normalized_path)?; + + // Parse the raw configuration file + let raw_hosts = ssh_config::Parser::new().parse_file(path)?; + + // Call the extracted processing logic + let hosts = process_hosts(raw_hosts); Ok(hosts) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ssh_config::{EntryType, Host}; + + #[test] + fn test_process_hosts_with_aliases() { + // 直接创建包含多个模式的Host对象 + let mut ssh_config_host = Host::new(vec![ + "server1".to_string(), + "server2".to_string(), + "dev-server".to_string() + ]); + + // 添加配置项 + ssh_config_host.update((EntryType::Hostname, "example.com".to_string())); + ssh_config_host.update((EntryType::User, "testuser".to_string())); + ssh_config_host.update((EntryType::Port, "2222".to_string())); + + // 创建原始主机列表 + let raw_hosts = vec![ssh_config_host]; + + // 调用process_hosts函数 + let hosts = process_hosts(raw_hosts); + + // 验证结果 + assert_eq!(hosts.len(), 1, "Should have one host entry"); + assert_eq!(hosts[0].name, "server1", "First pattern should be the name"); + assert_eq!(hosts[0].aliases, "server2, dev-server", "Remaining patterns should be aliases"); + assert_eq!(hosts[0].destination, "example.com", "Hostname should be correct"); + assert_eq!(hosts[0].user, Some("testuser".to_string()), "User should be correct"); + assert_eq!(hosts[0].port, Some("2222".to_string()), "Port should be correct"); + } + + #[test] + fn test_process_hosts_with_empty_hostname() { + // 测试没有设置Hostname的情况 + let mut ssh_config_host = Host::new(vec!["server1".to_string(), "server2".to_string()]); + + // 不设置Hostname,这样会应用第一个模式作为Hostname + ssh_config_host.update((EntryType::User, "testuser".to_string())); + + let raw_hosts = vec![ssh_config_host]; + let hosts = process_hosts(raw_hosts); + + // 验证结果 - Hostname应该被设置为第一个模式 + assert_eq!(hosts.len(), 1); + assert_eq!(hosts[0].name, "server1"); + assert_eq!(hosts[0].aliases, "server2"); + assert_eq!(hosts[0].destination, "server1", "Destination should be set to first pattern"); + } +} diff --git a/src/ssh_config/host.rs b/src/ssh_config/host.rs index 5a16d22..914d80d 100644 --- a/src/ssh_config/host.rs +++ b/src/ssh_config/host.rs @@ -177,46 +177,57 @@ impl HostVecExt for Vec { } /// Apply patterns entries to non-pattern hosts and remove the pattern hosts. + /// + /// Only copies UI-relevant parameters (User, Hostname, Port, ProxyCommand) from pattern hosts + /// to matching non-pattern hosts when the parameter doesn't already exist. /// /// You might want to call [`HostVecExt::merge_same_hosts`] after this. fn apply_patterns(&self) -> Self { - let mut hosts = self.spread(); - let mut pattern_indexes = Vec::new(); - - for i in 0..hosts.len() { - let matching_pattern_regexes = hosts[i].matching_pattern_regexes(); - if matching_pattern_regexes.is_empty() { - continue; - } - - pattern_indexes.push(i); - - for j in 0..hosts.len() { - if i == j { - continue; - } - - if !hosts[j].matching_pattern_regexes().is_empty() { - continue; - } - - for (regex, is_negated) in &matching_pattern_regexes { - if regex.is_match(&hosts[j].patterns[0]) == *is_negated { - continue; - } - - let host = hosts[i].clone(); - hosts[j].extend_if_not_contained(&host); - break; - } - } - } - - for i in pattern_indexes.into_iter().rev() { - hosts.remove(i); - } - - hosts + // UI展示关心的参数列表 + const UI_RELEVANT_ENTRY_TYPES: [EntryType; 4] = [ + EntryType::User, + EntryType::Hostname, + EntryType::Port, + EntryType::ProxyCommand, + ]; + + let spread_hosts = self.spread(); + + // 分离模式主机和非模式主机 + let (pattern_hosts, non_pattern_hosts): (Vec<_>, Vec<_>) = + spread_hosts.into_iter().partition(|host| !host.matching_pattern_regexes().is_empty()); + + // 对每个非模式主机应用匹配的模式主机参数 + non_pattern_hosts + .into_iter() + .map(|mut host| { + // 先计算匹配的模式主机,避免同时借用host + let host_name = host.patterns[0].clone(); + + // 找出所有匹配的模式主机 + let matching_patterns: Vec<_> = pattern_hosts + .iter() + .filter(|pattern_host| { + pattern_host.matching_pattern_regexes().iter().any(|(regex, is_negated)| { + regex.is_match(&host_name) != *is_negated + }) + }) + .collect(); + + // 应用参数 + matching_patterns.iter().for_each(|pattern_host| { + UI_RELEVANT_ENTRY_TYPES.iter().for_each(|entry_type| { + if host.get(entry_type).is_none() { + if let Some(value) = pattern_host.get(entry_type) { + host.update((entry_type.clone(), value)); + } + } + }); + }); + + host + }) + .collect() } }