Skip to content

Commit

Permalink
git: prompt for credentials when needed
Browse files Browse the repository at this point in the history
  • Loading branch information
Ralith committed Nov 7, 2022
1 parent 740cacd commit 4f7b296
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 31 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
36 changes: 26 additions & 10 deletions lib/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -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 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);
}
}
}
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);
} 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);
}
}
}
Expand Down
88 changes: 84 additions & 4 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -4058,11 +4059,13 @@ fn do_git_clone(
}

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() {
let mut progress = Progress::new(Instant::now(), ui);
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);
progress.update(Instant::now(), x, &mut *ui.lock().unwrap());
});
}
let mut callbacks = git::RemoteCallbacks::default();
Expand All @@ -4071,9 +4074,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(&mut *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");
Expand Down
29 changes: 12 additions & 17 deletions src/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,32 @@ use jujutsu_lib::git;

use crate::ui::Ui;

pub struct Progress<'a> {
ui: &'a mut Ui,
pub struct Progress {
next_print: Instant,
rate: RateEstimate,
buffer: String,
}

impl<'a> Progress<'a> {
pub fn new(now: Instant, ui: &'a mut Ui) -> Self {
impl Progress {
pub fn new(now: Instant) -> Self {
Self {
ui,
next_print: now + INITIAL_DELAY,
rate: RateEstimate::new(),
buffer: String::new(),
}
}

pub fn update(&mut self, now: Instant, progress: &git::Progress) {
pub fn update(&mut self, now: Instant, progress: &git::Progress, ui: &mut Ui) {
use std::fmt::Write as _;

if progress.overall == 1.0 {
_ = write!(ui, "\r{}", Clear(ClearType::CurrentLine));
return;
}

let rate = progress
.bytes_downloaded
.and_then(|x| self.rate.update(now, x));

if now < self.next_print {
return;
}
Expand All @@ -43,8 +45,7 @@ impl<'a> Progress<'a> {
write!(self.buffer, " at {: >5.1} {}B/s ", scaled, prefix).unwrap();
}

let bar_width = self
.ui
let bar_width = ui
.size()
.map(|(cols, _rows)| usize::from(cols))
.unwrap_or(0)
Expand All @@ -53,14 +54,8 @@ impl<'a> Progress<'a> {
draw_progress(progress.overall, &mut self.buffer, bar_width);
self.buffer.push(']');

_ = write!(self.ui, "{}", self.buffer);
_ = self.ui.flush();
}
}

impl Drop for Progress<'_> {
fn drop(&mut self) {
_ = write!(self.ui, "\r{}", Clear(ClearType::CurrentLine));
_ = write!(ui, "{}", self.buffer);
_ = ui.flush();
}
}

Expand Down
38 changes: 38 additions & 0 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
"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,
"not connected to a terminal",
));
}
rpassword::prompt_password(&format!("{}: ", prompt))
}

pub fn size(&self) -> Option<(u16, u16)> {
crossterm::terminal::size().ok()
}
Expand All @@ -215,3 +239,17 @@ impl Ui {
enum UiOutputPair {
Terminal { stdout: Stdout, stderr: Stderr },
}

impl io::Write for Ui {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.output_pair {
UiOutputPair::Terminal { ref mut stdout, .. } => stdout.write(buf),
}
}

fn flush(&mut self) -> io::Result<()> {
match self.output_pair {
UiOutputPair::Terminal { ref mut stdout, .. } => stdout.flush(),
}
}
}

0 comments on commit 4f7b296

Please sign in to comment.