mirror of
https://github.com/martinvonz/jj.git
synced 2024-11-12 19:08:46 +03:00
git: prompt for credentials when needed
This commit is contained in:
parent
fd0a065801
commit
d69eb808df
@ -35,6 +35,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
* It is now possible to specity configuration options on the command line
|
||||
with he new `--config-toml` global option.
|
||||
|
||||
* (#469) `jj git` subcommands will prompt for credentials when
|
||||
required for HTTPS remotes rather than failing.
|
||||
|
||||
### Fixed bugs
|
||||
|
||||
* `jj edit root` now fails gracefully.
|
||||
|
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -730,6 +730,7 @@ dependencies = [
|
||||
"predicates",
|
||||
"rand",
|
||||
"regex",
|
||||
"rpassword",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
@ -1340,6 +1341,16 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "7.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20c9f5d2a0c3e2ea729ab3706d22217177770654c3ef5056b68b69d07332d3f5"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.11"
|
||||
|
@ -51,6 +51,7 @@ pest = "2.4.0"
|
||||
pest_derive = "2.4"
|
||||
rand = "0.8.5"
|
||||
regex = "1.6.0"
|
||||
rpassword = "7.1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.3.0"
|
||||
textwrap = "0.16.0"
|
||||
|
@ -483,6 +483,8 @@ fn push_refs(
|
||||
pub struct RemoteCallbacks<'a> {
|
||||
pub progress: Option<&'a mut dyn FnMut(&Progress)>,
|
||||
pub get_ssh_key: Option<&'a mut dyn FnMut(&str) -> Option<PathBuf>>,
|
||||
pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
|
||||
pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
|
||||
}
|
||||
|
||||
impl<'a> RemoteCallbacks<'a> {
|
||||
@ -504,17 +506,31 @@ impl<'a> RemoteCallbacks<'a> {
|
||||
}
|
||||
// TODO: We should expose the callbacks to the caller instead -- the library
|
||||
// crate shouldn't read environment variables.
|
||||
callbacks.credentials(move |_url, username_from_url, allowed_types| {
|
||||
if allowed_types.contains(git2::CredentialType::SSH_KEY) {
|
||||
if std::env::var("SSH_AUTH_SOCK").is_ok() || std::env::var("SSH_AGENT_PID").is_ok()
|
||||
{
|
||||
return git2::Cred::ssh_key_from_agent(username_from_url.unwrap());
|
||||
callbacks.credentials(move |url, username_from_url, allowed_types| {
|
||||
if let Some(username) = username_from_url {
|
||||
if allowed_types.contains(git2::CredentialType::SSH_KEY) {
|
||||
if std::env::var("SSH_AUTH_SOCK").is_ok()
|
||||
|| std::env::var("SSH_AGENT_PID").is_ok()
|
||||
{
|
||||
return git2::Cred::ssh_key_from_agent(username);
|
||||
}
|
||||
if let Some(ref mut cb) = self.get_ssh_key {
|
||||
if let Some(path) = cb(username) {
|
||||
return git2::Cred::ssh_key(username, None, &path, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let (&mut Some(ref mut cb), Some(username)) =
|
||||
(&mut self.get_ssh_key, username_from_url)
|
||||
{
|
||||
if let Some(path) = cb(username) {
|
||||
return git2::Cred::ssh_key(username, None, &path, None);
|
||||
if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
|
||||
if let Some(ref mut cb) = self.get_password {
|
||||
if let Some(pw) = cb(url, username) {
|
||||
return git2::Cred::userpass_plaintext(username, &pw);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
|
||||
if let Some(ref mut cb) = self.get_username_password {
|
||||
if let Some((username, pw)) = cb(url) {
|
||||
return git2::Cred::userpass_plaintext(&username, &pw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ use std::fs::OpenOptions;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use std::{fs, io};
|
||||
|
||||
@ -4095,12 +4096,15 @@ fn do_git_clone(
|
||||
Ok((workspace_command, maybe_default_branch))
|
||||
}
|
||||
|
||||
#[allow(clippy::explicit_auto_deref)] // https://github.com/rust-lang/rust-clippy/issues/9763
|
||||
fn with_remote_callbacks<T>(ui: &mut Ui, f: impl FnOnce(git::RemoteCallbacks<'_>) -> T) -> T {
|
||||
let mut ui = Mutex::new(ui);
|
||||
let mut callback = None;
|
||||
if ui.use_progress_indicator() {
|
||||
if ui.get_mut().unwrap().use_progress_indicator() {
|
||||
let mut progress = Progress::new(Instant::now());
|
||||
let ui = &ui;
|
||||
callback = Some(move |x: &git::Progress| {
|
||||
progress.update(Instant::now(), x, ui);
|
||||
progress.update(Instant::now(), x, *ui.lock().unwrap());
|
||||
});
|
||||
}
|
||||
let mut callbacks = git::RemoteCallbacks::default();
|
||||
@ -4109,9 +4113,86 @@ fn with_remote_callbacks<T>(ui: &mut Ui, f: impl FnOnce(git::RemoteCallbacks<'_>
|
||||
.map(|x| x as &mut dyn FnMut(&git::Progress));
|
||||
let mut get_ssh_key = get_ssh_key; // Coerce to unit fn type
|
||||
callbacks.get_ssh_key = Some(&mut get_ssh_key);
|
||||
let mut get_pw = |url: &str, _username: &str| {
|
||||
pinentry_get_pw(url).or_else(|| terminal_get_pw(*ui.lock().unwrap(), url))
|
||||
};
|
||||
callbacks.get_password = Some(&mut get_pw);
|
||||
let mut get_user_pw = |url: &str| {
|
||||
let ui = &mut *ui.lock().unwrap();
|
||||
Some((terminal_get_username(ui, url)?, terminal_get_pw(ui, url)?))
|
||||
};
|
||||
callbacks.get_username_password = Some(&mut get_user_pw);
|
||||
f(callbacks)
|
||||
}
|
||||
|
||||
fn terminal_get_username(ui: &mut Ui, url: &str) -> Option<String> {
|
||||
ui.prompt(&format!("Username for {}", url)).ok()
|
||||
}
|
||||
|
||||
fn terminal_get_pw(ui: &mut Ui, url: &str) -> Option<String> {
|
||||
ui.prompt_password(&format!("Passphrase for {}: ", url))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn pinentry_get_pw(url: &str) -> Option<String> {
|
||||
let mut pinentry = Command::new("pinentry")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.ok()?;
|
||||
#[rustfmt::skip]
|
||||
pinentry
|
||||
.stdin
|
||||
.take()
|
||||
.unwrap()
|
||||
.write_all(
|
||||
format!(
|
||||
"SETTITLE jj passphrase\n\
|
||||
SETDESC Enter passphrase for {url}\n\
|
||||
SETPROMPT Passphrase:\n\
|
||||
GETPIN\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.ok()?;
|
||||
let mut out = String::new();
|
||||
pinentry
|
||||
.stdout
|
||||
.take()
|
||||
.unwrap()
|
||||
.read_to_string(&mut out)
|
||||
.ok()?;
|
||||
_ = pinentry.wait();
|
||||
for line in out.split('\n') {
|
||||
if !line.starts_with("D ") {
|
||||
continue;
|
||||
}
|
||||
let (_, encoded) = line.split_at(2);
|
||||
return decode_assuan_data(encoded);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// https://www.gnupg.org/documentation/manuals/assuan/Server-responses.html#Server-responses
|
||||
fn decode_assuan_data(encoded: &str) -> Option<String> {
|
||||
let encoded = encoded.as_bytes();
|
||||
let mut decoded = Vec::with_capacity(encoded.len());
|
||||
let mut i = 0;
|
||||
while i < encoded.len() {
|
||||
if encoded[i] != b'%' {
|
||||
decoded.push(encoded[i]);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
let byte =
|
||||
u8::from_str_radix(std::str::from_utf8(encoded.get(i..i + 2)?).ok()?, 16).ok()?;
|
||||
decoded.push(byte);
|
||||
i += 2;
|
||||
}
|
||||
String::from_utf8(decoded).ok()
|
||||
}
|
||||
|
||||
fn get_ssh_key(_username: &str) -> Option<PathBuf> {
|
||||
let home_dir = std::env::var("HOME").ok()?;
|
||||
let key_path = std::path::Path::new(&home_dir).join(".ssh").join("id_rsa");
|
||||
|
24
src/ui.rs
24
src/ui.rs
@ -207,6 +207,30 @@ impl Ui {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prompt(&mut self, prompt: &str) -> io::Result<String> {
|
||||
if !atty::is(Stream::Stdout) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Cannot prompt for input since the output is not connected to a terminal",
|
||||
));
|
||||
}
|
||||
write!(self, "{}: ", prompt)?;
|
||||
self.flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn prompt_password(&mut self, prompt: &str) -> io::Result<String> {
|
||||
if !atty::is(Stream::Stdout) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Cannot prompt for input since the output is not connected to a terminal",
|
||||
));
|
||||
}
|
||||
rpassword::prompt_password(&format!("{}: ", prompt))
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Option<(u16, u16)> {
|
||||
crossterm::terminal::size().ok()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user