git-transport 0.25.5

Please use `gix-<thiscrate>` instead ('git' -> 'gix')
Documentation
use std::process::Stdio;

use crate::{client::blocking_io, Protocol};

/// The error used in [`connect()`].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
    #[error("The scheme in \"{}\" is not usable for an ssh connection", .0.to_bstring())]
    UnsupportedScheme(git_url::Url),
}

impl crate::IsSpuriousError for Error {}

/// The kind of SSH programs we have built-in support for.
///
/// Various different programs exists with different capabilities, and we have a few built in.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ProgramKind {
    /// The standard linux ssh program
    Ssh,
    /// The `(plink|putty).exe` binaries, typically only on windows.
    Plink,
    /// The `putty.exe` binary, typically only on windows.
    Putty,
    /// The `tortoiseplink.exe` binary, only on windows.
    TortoisePlink,
    /// A minimal ssh client that supports on options.
    Simple,
}

mod program_kind;

///
pub mod invocation {
    use std::ffi::OsString;

    /// The error returned when producing ssh invocation arguments based on a selected invocation kind.
    #[derive(Debug, thiserror::Error)]
    #[error("The 'Simple' ssh variant doesn't support {function}")]
    pub struct Error {
        /// The simple command that should have been invoked.
        pub command: OsString,
        /// The function that was unsupported
        pub function: &'static str,
    }
}

///
pub mod connect {
    use std::ffi::{OsStr, OsString};

    use crate::client::ssh::ProgramKind;

    /// The options for use when [connecting][super::connect()] via the `ssh` protocol.
    #[derive(Debug, Clone, Default)]
    pub struct Options {
        /// The program or script to use.
        /// If unset, it defaults to `ssh` or `ssh.exe`, or the program implied by `kind` if that one is set.
        pub command: Option<OsString>,
        /// If `true`, a shell must not be used to execute `command`.
        /// This defaults to `false`, and a shell can then be used if `command` seems to require it, but won't be
        /// used unnecessarily.
        pub disallow_shell: bool,
        /// The ssh variant further identifying `program`. This determines which arguments will be used
        /// when invoking the program.
        /// If unset, the `program` basename determines the variant, or an invocation of the `command` itself.
        pub kind: Option<ProgramKind>,
    }

    impl Options {
        /// Return the configured ssh command, defaulting to `ssh` if neither the `command` nor the `kind` fields are set.
        pub fn ssh_command(&self) -> &OsStr {
            self.command
                .as_deref()
                .or_else(|| self.kind.and_then(|kind| kind.exe()))
                .unwrap_or_else(|| OsStr::new("ssh"))
        }
    }
}

/// 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.
pub fn connect(
    url: git_url::Url,
    desired_version: Protocol,
    options: connect::Options,
) -> Result<blocking_io::file::SpawnProcessOnDemand, Error> {
    if url.scheme != git_url::Scheme::Ssh || url.host().is_none() {
        return Err(Error::UnsupportedScheme(url));
    }
    let ssh_cmd = options.ssh_command();
    let mut kind = options.kind.unwrap_or_else(|| ProgramKind::from(ssh_cmd));
    if options.kind.is_none() && kind == ProgramKind::Simple {
        kind = if std::process::Command::from(
            git_command::prepare(ssh_cmd)
                .stderr(Stdio::null())
                .stdout(Stdio::null())
                .stdin(Stdio::null())
                .with_shell()
                .arg("-G")
                .arg(url.host().expect("always set for ssh urls")),
        )
        .status()
        .ok()
        .map_or(false, |status| status.success())
        {
            ProgramKind::Ssh
        } else {
            ProgramKind::Simple
        };
    }

    let path = git_url::expand_path::for_shell(url.path.clone());
    Ok(blocking_io::file::SpawnProcessOnDemand::new_ssh(
        url,
        ssh_cmd,
        path,
        kind,
        options.disallow_shell,
        desired_version,
    ))
}

#[cfg(test)]
mod tests;