1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
use crate::{client, Protocol};
use bstr::BString;
use quick_error::quick_error;
use std::borrow::Cow;

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<u16>,
) -> Result<client::file::SpawnProcessOnDemand, Error> {
    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<Cow<'_, str>>, Vec<EnvVar>)> = 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)) => client::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 => client::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 crate::{client::ssh::connect, Protocol};
    use bstr::ByteSlice;

    #[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"
            );
        }
    }
}