use std::borrow::Cow; use bstr::BString; use quick_error::quick_error; use crate::{client::blocking_io, Protocol}; quick_error! { /// The error used in [`connect()`]. #[derive(Debug)] #[allow(missing_docs)] pub enum Error { UnsupportedSshCommand(command: String) { display("The ssh command '{}' is not currently supported", command) } } } /// Connect to `host` using the ssh program to obtain data from the repository at `path` on the remote. /// /// The optional `user` identifies the user's account to which to connect, while `port` allows to specify non-standard /// ssh ports. /// /// The `desired_version` is the preferred protocol version when establishing the connection, but note that it can be /// downgraded by servers not supporting it. /// /// # Environment Variables /// /// Use `GIT_SSH_COMMAND` to override the `ssh` program to execute. This can be a script dealing with using the correct /// ssh key, for example. pub fn connect( host: &str, path: BString, desired_version: crate::Protocol, user: Option<&str>, port: Option, ) -> Result { let ssh_cmd_line = std::env::var("GIT_SSH_COMMAND").unwrap_or_else(|_| "ssh".into()); let mut ssh_cmd_line = ssh_cmd_line.split(' '); let ssh_cmd = ssh_cmd_line.next().expect("there is always a single item"); type EnvVar = (&'static str, String); let args_and_env: Option<(Vec>, Vec)> = match ssh_cmd { "ssh" | "ssh.exe" => { if desired_version != Protocol::V1 { let mut args = vec![Cow::from("-o"), "SendEnv=GIT_PROTOCOL".into()]; if let Some(port) = port { args.push(format!("-p={}", port).into()); } Some(( args, vec![("GIT_PROTOCOL", format!("version={}", desired_version as usize))], )) } else { None } } _ => return Err(Error::UnsupportedSshCommand(ssh_cmd.into())), }; let host = match user.as_ref() { Some(user) => format!("{}@{}", user, host), None => host.into(), }; let path = git_url::expand_path::for_shell(path); let url = git_url::Url { scheme: git_url::Scheme::Ssh, user: user.map(Into::into), host: Some(host.clone()), port, path: path.clone(), }; Ok(match args_and_env { Some((args, envs)) => blocking_io::file::SpawnProcessOnDemand::new_ssh( url, ssh_cmd.into(), ssh_cmd_line.map(Cow::from).chain(args).chain(Some(host.into())), envs, path, desired_version, ), None => blocking_io::file::SpawnProcessOnDemand::new_ssh( url, ssh_cmd.into(), ssh_cmd_line.chain(Some(host.as_str())), None::<(&str, String)>, path, desired_version, ), }) } #[cfg(test)] mod tests { use bstr::ByteSlice; use crate::{client::blocking_io::ssh::connect, Protocol}; #[test] fn connect_with_tilde_in_path() { for (url, expected) in &[ ("ssh://host.xy/~/repo", "~/repo"), ("ssh://host.xy/~username/repo", "~username/repo"), ] { let url = git_url::parse(url.as_bytes()).expect("valid url"); let cmd = connect("host", url.path, Protocol::V1, None, None).expect("parse success"); assert_eq!( cmd.path, expected.as_bytes().as_bstr(), "the path is prepared to be substituted by the remote shell" ); } } }