mirror of
https://github.com/martinvonz/jj.git
synced 2025-05-18 13:44:26 +00: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
|
* It is now possible to specity configuration options on the command line
|
||||||
with he new `--config-toml` global option.
|
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
|
### Fixed bugs
|
||||||
|
|
||||||
* `jj edit root` now fails gracefully.
|
* `jj edit root` now fails gracefully.
|
||||||
|
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -730,6 +730,7 @@ dependencies = [
|
|||||||
"predicates",
|
"predicates",
|
||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
|
"rpassword",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"test-case",
|
"test-case",
|
||||||
@ -1340,6 +1341,16 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
|
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]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
|
@ -51,6 +51,7 @@ pest = "2.4.0"
|
|||||||
pest_derive = "2.4"
|
pest_derive = "2.4"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
regex = "1.6.0"
|
regex = "1.6.0"
|
||||||
|
rpassword = "7.1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tempfile = "3.3.0"
|
tempfile = "3.3.0"
|
||||||
textwrap = "0.16.0"
|
textwrap = "0.16.0"
|
||||||
|
@ -483,6 +483,8 @@ fn push_refs(
|
|||||||
pub struct RemoteCallbacks<'a> {
|
pub struct RemoteCallbacks<'a> {
|
||||||
pub progress: Option<&'a mut dyn FnMut(&Progress)>,
|
pub progress: Option<&'a mut dyn FnMut(&Progress)>,
|
||||||
pub get_ssh_key: Option<&'a mut dyn FnMut(&str) -> Option<PathBuf>>,
|
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> {
|
impl<'a> RemoteCallbacks<'a> {
|
||||||
@ -504,17 +506,31 @@ impl<'a> RemoteCallbacks<'a> {
|
|||||||
}
|
}
|
||||||
// TODO: We should expose the callbacks to the caller instead -- the library
|
// TODO: We should expose the callbacks to the caller instead -- the library
|
||||||
// crate shouldn't read environment variables.
|
// crate shouldn't read environment variables.
|
||||||
callbacks.credentials(move |_url, username_from_url, allowed_types| {
|
callbacks.credentials(move |url, username_from_url, allowed_types| {
|
||||||
if allowed_types.contains(git2::CredentialType::SSH_KEY) {
|
if let Some(username) = username_from_url {
|
||||||
if std::env::var("SSH_AUTH_SOCK").is_ok() || std::env::var("SSH_AGENT_PID").is_ok()
|
if allowed_types.contains(git2::CredentialType::SSH_KEY) {
|
||||||
{
|
if std::env::var("SSH_AUTH_SOCK").is_ok()
|
||||||
return git2::Cred::ssh_key_from_agent(username_from_url.unwrap());
|
|| 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)) =
|
if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
|
||||||
(&mut self.get_ssh_key, username_from_url)
|
if let Some(ref mut cb) = self.get_password {
|
||||||
{
|
if let Some(pw) = cb(url, username) {
|
||||||
if let Some(path) = cb(username) {
|
return git2::Cred::userpass_plaintext(username, &pw);
|
||||||
return git2::Cred::ssh_key(username, None, &path, None);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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::io::{Read, Seek, SeekFrom, Write};
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::{Path, PathBuf};
|
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::time::Instant;
|
||||||
use std::{fs, io};
|
use std::{fs, io};
|
||||||
|
|
||||||
@ -4095,12 +4096,15 @@ fn do_git_clone(
|
|||||||
Ok((workspace_command, maybe_default_branch))
|
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 {
|
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;
|
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 mut progress = Progress::new(Instant::now());
|
||||||
|
let ui = &ui;
|
||||||
callback = Some(move |x: &git::Progress| {
|
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();
|
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));
|
.map(|x| x as &mut dyn FnMut(&git::Progress));
|
||||||
let mut get_ssh_key = get_ssh_key; // Coerce to unit fn type
|
let mut get_ssh_key = get_ssh_key; // Coerce to unit fn type
|
||||||
callbacks.get_ssh_key = Some(&mut get_ssh_key);
|
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)
|
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> {
|
fn get_ssh_key(_username: &str) -> Option<PathBuf> {
|
||||||
let home_dir = std::env::var("HOME").ok()?;
|
let home_dir = std::env::var("HOME").ok()?;
|
||||||
let key_path = std::path::Path::new(&home_dir).join(".ssh").join("id_rsa");
|
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)> {
|
pub fn size(&self) -> Option<(u16, u16)> {
|
||||||
crossterm::terminal::size().ok()
|
crossterm::terminal::size().ok()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user