use std::io::Write;
use std::path::{Path, PathBuf};
#[cfg(feature = "log")]
use crate::log::*;
#[derive(Copy, Clone)]
pub(crate) struct DefaultPrompter;
impl crate::Prompter for DefaultPrompter {
fn prompt_username_password(&mut self, url: &str, git_config: &git2::Config) -> Option<(String, String)> {
prompt_username_password(url, git_config)
.map_err(|e| log_error("username and password", &e))
.ok()
}
fn prompt_password(&mut self, username: &str, url: &str, git_config: &git2::Config) -> Option<String> {
prompt_password(username, url, git_config)
.map_err(|e| log_error("password", &e))
.ok()
}
fn prompt_ssh_key_passphrase(&mut self, private_key_path: &Path, git_config: &git2::Config) -> Option<String> {
prompt_ssh_key_passphrase(private_key_path, git_config)
.map_err(|e| log_error("SSH key passphrase", &e))
.ok()
}
}
fn log_error(kind: &str, error: &Error) {
warn!("Failed to prompt the user for {kind}: {error}");
if let Error::AskpassExitStatus(error) = error {
if let Some(extra_message) = error.extra_message() {
for line in extra_message.lines() {
warn!("askpass: {line}");
}
}
}
}
pub enum Error {
AskpassCommand(std::io::Error),
AskpassExitStatus(AskpassExitStatusError),
InvalidUtf8,
OpenTerminal(std::io::Error),
ReadWriteTerminal(std::io::Error),
}
pub struct AskpassExitStatusError {
pub status: std::process::ExitStatus,
pub stderr: Result<String, std::string::FromUtf8Error>,
}
impl AskpassExitStatusError {
pub fn extra_message(&self) -> Option<&str> {
self.stderr.as_deref().ok()
}
}
fn prompt_username_password(url: &str, git_config: &git2::Config) -> Result<(String, String), Error> {
if let Some(askpass) = askpass_command(git_config) {
let username = askpass_prompt(&askpass, &format!("Username for {url}"))?;
let password = askpass_prompt(&askpass, &format!("Password for {url}"))?;
Ok((username, password))
} else {
let mut terminal = terminal_prompt::Terminal::open()
.map_err(Error::OpenTerminal)?;
writeln!(terminal, "Authentication needed for {url}")
.map_err(Error::ReadWriteTerminal)?;
let username = terminal.prompt("Username: ")
.map_err(Error::ReadWriteTerminal)?;
let password = terminal.prompt_sensitive("Password: ")
.map_err(Error::ReadWriteTerminal)?;
Ok((username, password))
}
}
fn prompt_password(_username: &str, url: &str, git_config: &git2::Config) -> Result<String, Error> {
if let Some(askpass) = askpass_command(git_config) {
let password = askpass_prompt(&askpass, &format!("Password for {url}"))?;
Ok(password)
} else {
let mut terminal = terminal_prompt::Terminal::open()
.map_err(Error::OpenTerminal)?;
writeln!(terminal, "Authentication needed for {url}")
.map_err(Error::ReadWriteTerminal)?;
let password = terminal.prompt_sensitive("Password: ")
.map_err(Error::ReadWriteTerminal)?;
Ok(password)
}
}
fn prompt_ssh_key_passphrase(private_key_path: &Path, git_config: &git2::Config) -> Result<String, Error> {
if let Some(askpass) = askpass_command(git_config) {
askpass_prompt(&askpass, &format!("Password for {}", private_key_path.display()))
} else {
let mut terminal = terminal_prompt::Terminal::open()
.map_err(Error::OpenTerminal)?;
writeln!(terminal, "Password needed for {}", private_key_path.display())
.map_err(Error::ReadWriteTerminal)?;
terminal.prompt_sensitive("Password: ")
.map_err(Error::ReadWriteTerminal)
}
}
fn askpass_command(git_config: &git2::Config) -> Option<PathBuf> {
if let Some(command) = std::env::var_os("GIT_ASKPASS") {
Some(command.into())
} else if let Ok(command) = git_config.get_path("core.askPass") {
return Some(command)
} else if let Some(command) = std::env::var_os("SSH_ASKPASS") {
return Some(command.into());
} else {
None
}
}
fn askpass_prompt(program: &Path, prompt: &str) -> Result<String, Error> {
let output = std::process::Command::new(program)
.arg(prompt)
.output()
.map_err(Error::AskpassCommand)?;
if output.status.success() {
let password = String::from_utf8(output.stdout)
.map_err(|_| Error::InvalidUtf8)?;
Ok(password)
} else {
Err(Error::AskpassExitStatus(AskpassExitStatusError {
status: output.status,
stderr: String::from_utf8(output.stderr),
}))
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AskpassCommand(e) => write!(f, "Failed to run askpass command: {e}"),
Self::AskpassExitStatus(e) => write!(f, "{e}"),
Self::InvalidUtf8 => write!(f, "User response contains invalid UTF-8"),
Self::OpenTerminal(e) => write!(f, "Failed to open terminal: {e}"),
Self::ReadWriteTerminal(e) => write!(f, "Failed to read/write to terminal: {e}"),
}
}
}
impl std::fmt::Display for AskpassExitStatusError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Program exitted with {}", self.status)
}
}