use std::path::{Path, PathBuf}; #[derive(Copy, Clone)] struct YadPrompter; impl auth_git2::Prompter for YadPrompter { fn prompt_username_password(&mut self, url: &str, _git_config: &git2::Config) -> Option<(String, String)> { let mut items = yad_prompt( "Git authentication", &format!("Authentication required for {url}"), &["Username", "Password:H"], ).ok()?.into_iter(); let username = items.next()?; let password = items.next()?; Some((username, password)) } fn prompt_password(&mut self, username: &str, url: &str, _git_config: &git2::Config) -> Option { let mut items = yad_prompt( "Git authentication", &format!("Authentication required for {url}"), &[&format!("Username: {username}:LBL"), "Password:H"], ).ok()?.into_iter(); let password = items.next()?; Some(password) } fn prompt_ssh_key_passphrase(&mut self, private_key_path: &std::path::Path, _git_config: &git2::Config) -> Option { let mut items = yad_prompt( "Git authentication", &format!("Passphrase required for {}", private_key_path.display()), &["Passphrase:H"], ).ok()?.into_iter(); let passphrase = items.next()?; Some(passphrase) } } fn yad_prompt(title: &str, text: &str, fields: &[&str]) -> Result, ()> { let mut command = std::process::Command::new("yad"); command .arg("--title") .arg(title) .arg("--text") .arg(text) .arg("--form") .arg("--separator=\n"); for field in fields { command.arg("--field"); command.arg(field); } let output = command .stderr(std::process::Stdio::inherit()) .output() .map_err(|e| log::error!("Failed to run `yad`: {e}"))?; if !output.status.success() { log::debug!("yad exited with {}", output.status); return Err(()); } let output = String::from_utf8(output.stdout) .map_err(|_| log::warn!("Invalid UTF-8 in response from yad"))?; let mut items: Vec<_> = output.splitn(fields.len() + 1, '\n') .take(fields.len()) .map(|x| x.to_owned()) .collect(); if let Some(last) = items.pop() { if !last.is_empty() { items.push(last) } } if items.len() != fields.len() { log::error!("asked yad for {} values but got only {}", fields.len(), items.len()); Err(()) } else { Ok(items) } } #[derive(clap::Parser)] struct Options { /// Show more verbose statement. #[clap(long, short)] #[clap(global = true)] #[clap(action = clap::ArgAction::Count)] verbose: u8, /// The URL of the repository to clone. #[clap(value_name = "URL")] repo: String, /// The path where to clone the repository. #[clap(value_name = "PATH")] local_path: Option, } fn main() { if let Err(()) = do_main(clap::Parser::parse()) { std::process::exit(1); } } fn log_level(verbose: u8) -> log::LevelFilter { match verbose { 0 => log::LevelFilter::Info, 1 => log::LevelFilter::Debug, 2.. => log::LevelFilter::Trace, } } fn do_main(options: Options) -> Result<(), ()> { let log_level = log_level(options.verbose); env_logger::builder() .parse_default_env() .filter_module(module_path!(), log_level) .filter_module("auth_git2", log_level) .init(); let local_path = options.local_path.as_deref() .unwrap_or_else(|| Path::new(repo_name_from_url(&options.repo))); log::info!("Cloning {} into {}", options.repo, local_path.display()); let auth = auth_git2::GitAuthenticator::default() .set_prompter(YadPrompter); auth.clone_repo(&options.repo, local_path) .map_err(|e| log::error!("Failed to clone {}: {}", options.repo, e))?; Ok(()) } fn repo_name_from_url(url: &str) -> &str { url.rsplit_once('/') .map(|(_head, tail)| tail) .unwrap_or(url) }