remote_identity.rs

  1use crate::RemoteConnectionOptions;
  2
  3/// A normalized remote identity for matching live remote hosts against
  4/// persisted remote metadata.
  5///
  6/// This mirrors workspace persistence identity semantics rather than full
  7/// `RemoteConnectionOptions` equality, so runtime-only fields like SSH
  8/// nicknames or Docker environment overrides do not affect matching.
  9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 10pub enum RemoteConnectionIdentity {
 11    Ssh {
 12        host: String,
 13        username: Option<String>,
 14        port: Option<u16>,
 15    },
 16    Wsl {
 17        distro_name: String,
 18        user: Option<String>,
 19    },
 20    Docker {
 21        container_id: String,
 22        name: String,
 23        remote_user: String,
 24    },
 25    #[cfg(any(test, feature = "test-support"))]
 26    Mock { id: u64 },
 27}
 28
 29impl From<&RemoteConnectionOptions> for RemoteConnectionIdentity {
 30    fn from(options: &RemoteConnectionOptions) -> Self {
 31        match options {
 32            RemoteConnectionOptions::Ssh(options) => Self::Ssh {
 33                host: options.host.to_string(),
 34                username: options.username.clone(),
 35                port: options.port,
 36            },
 37            RemoteConnectionOptions::Wsl(options) => Self::Wsl {
 38                distro_name: options.distro_name.clone(),
 39                user: options.user.clone(),
 40            },
 41            RemoteConnectionOptions::Docker(options) => Self::Docker {
 42                container_id: options.container_id.clone(),
 43                name: options.name.clone(),
 44                remote_user: options.remote_user.clone(),
 45            },
 46            #[cfg(any(test, feature = "test-support"))]
 47            RemoteConnectionOptions::Mock(options) => Self::Mock { id: options.id },
 48        }
 49    }
 50}
 51
 52pub fn remote_connection_identity(options: &RemoteConnectionOptions) -> RemoteConnectionIdentity {
 53    options.into()
 54}
 55
 56pub fn same_remote_connection_identity(
 57    left: Option<&RemoteConnectionOptions>,
 58    right: Option<&RemoteConnectionOptions>,
 59) -> bool {
 60    match (left, right) {
 61        (Some(left), Some(right)) => {
 62            remote_connection_identity(left) == remote_connection_identity(right)
 63        }
 64        (None, None) => true,
 65        _ => false,
 66    }
 67}
 68
 69#[cfg(test)]
 70mod tests {
 71    use std::collections::BTreeMap;
 72
 73    use super::*;
 74    use crate::{DockerConnectionOptions, SshConnectionOptions, WslConnectionOptions};
 75
 76    #[test]
 77    fn ssh_identity_ignores_non_persisted_runtime_fields() {
 78        let left = RemoteConnectionOptions::Ssh(SshConnectionOptions {
 79            host: "example.com".into(),
 80            username: Some("anth".to_string()),
 81            port: Some(2222),
 82            password: Some("secret".to_string()),
 83            args: Some(vec!["-v".to_string()]),
 84            connection_timeout: Some(30),
 85            nickname: Some("work".to_string()),
 86            upload_binary_over_ssh: true,
 87            ..Default::default()
 88        });
 89        let right = RemoteConnectionOptions::Ssh(SshConnectionOptions {
 90            host: "example.com".into(),
 91            username: Some("anth".to_string()),
 92            port: Some(2222),
 93            password: None,
 94            args: None,
 95            connection_timeout: None,
 96            nickname: None,
 97            upload_binary_over_ssh: false,
 98            ..Default::default()
 99        });
100
101        assert!(same_remote_connection_identity(Some(&left), Some(&right),));
102    }
103
104    #[test]
105    fn ssh_identity_distinguishes_persistence_key_fields() {
106        let left = RemoteConnectionOptions::Ssh(SshConnectionOptions {
107            host: "example.com".into(),
108            username: Some("anth".to_string()),
109            port: Some(2222),
110            ..Default::default()
111        });
112        let right = RemoteConnectionOptions::Ssh(SshConnectionOptions {
113            host: "example.com".into(),
114            username: Some("anth".to_string()),
115            port: Some(2223),
116            ..Default::default()
117        });
118
119        assert!(!same_remote_connection_identity(Some(&left), Some(&right),));
120    }
121
122    #[test]
123    fn wsl_identity_includes_user() {
124        let left = RemoteConnectionOptions::Wsl(WslConnectionOptions {
125            distro_name: "Ubuntu".to_string(),
126            user: Some("anth".to_string()),
127        });
128        let right = RemoteConnectionOptions::Wsl(WslConnectionOptions {
129            distro_name: "Ubuntu".to_string(),
130            user: Some("root".to_string()),
131        });
132
133        assert!(!same_remote_connection_identity(Some(&left), Some(&right),));
134    }
135
136    #[test]
137    fn docker_identity_ignores_non_persisted_runtime_fields() {
138        let left = RemoteConnectionOptions::Docker(DockerConnectionOptions {
139            name: "zed-dev".to_string(),
140            container_id: "container-123".to_string(),
141            remote_user: "anth".to_string(),
142            upload_binary_over_docker_exec: true,
143            use_podman: true,
144            remote_env: BTreeMap::from([("FOO".to_string(), "BAR".to_string())]),
145        });
146        let right = RemoteConnectionOptions::Docker(DockerConnectionOptions {
147            name: "zed-dev".to_string(),
148            container_id: "container-123".to_string(),
149            remote_user: "anth".to_string(),
150            upload_binary_over_docker_exec: false,
151            use_podman: false,
152            remote_env: BTreeMap::new(),
153        });
154
155        assert!(same_remote_connection_identity(Some(&left), Some(&right),));
156    }
157
158    #[test]
159    fn local_identity_matches_only_local_identity() {
160        let remote = RemoteConnectionOptions::Wsl(WslConnectionOptions {
161            distro_name: "Ubuntu".to_string(),
162            user: Some("anth".to_string()),
163        });
164
165        assert!(same_remote_connection_identity(None, None));
166        assert!(!same_remote_connection_identity(None, Some(&remote)));
167    }
168}