use crate::RemoteConnectionOptions; /// A normalized remote identity for matching live remote hosts against /// persisted remote metadata. /// /// This mirrors workspace persistence identity semantics rather than full /// `RemoteConnectionOptions` equality, so runtime-only fields like SSH /// nicknames or Docker environment overrides do not affect matching. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum RemoteConnectionIdentity { Ssh { host: String, username: Option, port: Option, }, Wsl { distro_name: String, user: Option, }, Docker { container_id: String, name: String, remote_user: String, }, #[cfg(any(test, feature = "test-support"))] Mock { id: u64 }, } impl RemoteConnectionIdentity { /// A stable string form of this identity, suitable for use in /// persistence keys (e.g. database keys scoped to a remote host). pub fn persistence_key(&self) -> String { match self { Self::Ssh { host, username, port, } => format!( "ssh:{}@{}:{}", username.as_deref().unwrap_or_default(), host, port.map(|port| port.to_string()).unwrap_or_default() ), Self::Wsl { distro_name, user } => format!( "wsl:{}@{}", user.as_deref().unwrap_or_default(), distro_name ), Self::Docker { container_id, name, remote_user, } => format!("docker:{remote_user}@{name}:{container_id}"), #[cfg(any(test, feature = "test-support"))] Self::Mock { id } => format!("mock:{id}"), } } } impl From<&RemoteConnectionOptions> for RemoteConnectionIdentity { fn from(options: &RemoteConnectionOptions) -> Self { match options { RemoteConnectionOptions::Ssh(options) => Self::Ssh { host: options.host.to_string(), username: options.username.clone(), port: options.port, }, RemoteConnectionOptions::Wsl(options) => Self::Wsl { distro_name: options.distro_name.clone(), user: options.user.clone(), }, RemoteConnectionOptions::Docker(options) => Self::Docker { container_id: options.container_id.clone(), name: options.name.clone(), remote_user: options.remote_user.clone(), }, #[cfg(any(test, feature = "test-support"))] RemoteConnectionOptions::Mock(options) => Self::Mock { id: options.id }, } } } pub fn remote_connection_identity(options: &RemoteConnectionOptions) -> RemoteConnectionIdentity { options.into() } pub fn same_remote_connection_identity( left: Option<&RemoteConnectionOptions>, right: Option<&RemoteConnectionOptions>, ) -> bool { match (left, right) { (Some(left), Some(right)) => { remote_connection_identity(left) == remote_connection_identity(right) } (None, None) => true, _ => false, } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use super::*; use crate::{DockerConnectionOptions, SshConnectionOptions, WslConnectionOptions}; #[test] fn ssh_identity_ignores_non_persisted_runtime_fields() { let left = RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "example.com".into(), username: Some("anth".to_string()), port: Some(2222), password: Some("secret".to_string()), args: Some(vec!["-v".to_string()]), connection_timeout: Some(30), nickname: Some("work".to_string()), upload_binary_over_ssh: true, ..Default::default() }); let right = RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "example.com".into(), username: Some("anth".to_string()), port: Some(2222), password: None, args: None, connection_timeout: None, nickname: None, upload_binary_over_ssh: false, ..Default::default() }); assert!(same_remote_connection_identity(Some(&left), Some(&right),)); } #[test] fn ssh_identity_distinguishes_persistence_key_fields() { let left = RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "example.com".into(), username: Some("anth".to_string()), port: Some(2222), ..Default::default() }); let right = RemoteConnectionOptions::Ssh(SshConnectionOptions { host: "example.com".into(), username: Some("anth".to_string()), port: Some(2223), ..Default::default() }); assert!(!same_remote_connection_identity(Some(&left), Some(&right),)); } #[test] fn wsl_identity_includes_user() { let left = RemoteConnectionOptions::Wsl(WslConnectionOptions { distro_name: "Ubuntu".to_string(), user: Some("anth".to_string()), }); let right = RemoteConnectionOptions::Wsl(WslConnectionOptions { distro_name: "Ubuntu".to_string(), user: Some("root".to_string()), }); assert!(!same_remote_connection_identity(Some(&left), Some(&right),)); } #[test] fn docker_identity_ignores_non_persisted_runtime_fields() { let left = RemoteConnectionOptions::Docker(DockerConnectionOptions { name: "zed-dev".to_string(), container_id: "container-123".to_string(), remote_user: "anth".to_string(), upload_binary_over_docker_exec: true, use_podman: true, remote_env: BTreeMap::from([("FOO".to_string(), "BAR".to_string())]), }); let right = RemoteConnectionOptions::Docker(DockerConnectionOptions { name: "zed-dev".to_string(), container_id: "container-123".to_string(), remote_user: "anth".to_string(), upload_binary_over_docker_exec: false, use_podman: false, remote_env: BTreeMap::new(), }); assert!(same_remote_connection_identity(Some(&left), Some(&right),)); } #[test] fn local_identity_matches_only_local_identity() { let remote = RemoteConnectionOptions::Wsl(WslConnectionOptions { distro_name: "Ubuntu".to_string(), user: Some("anth".to_string()), }); assert!(same_remote_connection_identity(None, None)); assert!(!same_remote_connection_identity(None, Some(&remote))); } }