mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-11-01 01:10:51 +08:00
165 lines
4.3 KiB
Rust
165 lines
4.3 KiB
Rust
use std::fmt::Display;
|
|
|
|
use radicle_term as term;
|
|
|
|
use radicle::cob::issue::{Issue, IssueId};
|
|
use radicle::cob::patch::{Patch, PatchId};
|
|
use radicle::crypto::ssh::keystore::{Keystore, MemorySigner};
|
|
use radicle::crypto::Signer;
|
|
use radicle::prelude::{Id, Project};
|
|
use radicle::profile::env::RAD_PASSPHRASE;
|
|
use radicle::storage::git::Repository;
|
|
use radicle::storage::{ReadRepository, ReadStorage};
|
|
|
|
use radicle::Profile;
|
|
|
|
use term::{passphrase, spinner, Passphrase};
|
|
|
|
use inquire::validator;
|
|
|
|
/// Git revision parameter. Supports extended SHA-1 syntax.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Rev(String);
|
|
|
|
impl From<String> for Rev {
|
|
fn from(value: String) -> Self {
|
|
Rev(value)
|
|
}
|
|
}
|
|
|
|
impl Display for Rev {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
pub struct Context {
|
|
profile: Profile,
|
|
id: Id,
|
|
project: Project,
|
|
repository: Repository,
|
|
issues: Vec<(IssueId, Issue)>,
|
|
patches: Vec<(PatchId, Patch)>,
|
|
signer: Box<dyn Signer>,
|
|
}
|
|
|
|
impl Context {
|
|
pub fn new(id: Id) -> Result<Self, anyhow::Error> {
|
|
let profile = profile()?;
|
|
let signer = signer(&profile)?;
|
|
|
|
let repository = profile.storage.repository(id).unwrap();
|
|
let issues = crate::cob::issue::all(&repository).unwrap_or_default();
|
|
let patches = crate::cob::patch::all(&repository).unwrap_or_default();
|
|
|
|
let project = repository.identity_doc()?.project()?;
|
|
|
|
Ok(Self {
|
|
id,
|
|
profile,
|
|
project,
|
|
repository,
|
|
issues,
|
|
patches,
|
|
signer,
|
|
})
|
|
}
|
|
|
|
pub fn profile(&self) -> &Profile {
|
|
&self.profile
|
|
}
|
|
|
|
pub fn id(&self) -> &Id {
|
|
&self.id
|
|
}
|
|
|
|
pub fn project(&self) -> &Project {
|
|
&self.project
|
|
}
|
|
|
|
pub fn repository(&self) -> &Repository {
|
|
&self.repository
|
|
}
|
|
|
|
pub fn issues(&self) -> &Vec<(IssueId, Issue)> {
|
|
&self.issues
|
|
}
|
|
|
|
pub fn patches(&self) -> &Vec<(PatchId, Patch)> {
|
|
&self.patches
|
|
}
|
|
|
|
#[allow(clippy::borrowed_box)]
|
|
pub fn signer(&self) -> &Box<dyn Signer> {
|
|
&self.signer
|
|
}
|
|
|
|
pub fn reload(&mut self) {
|
|
self.issues = crate::cob::issue::all(&self.repository).unwrap_or_default();
|
|
self.patches = crate::cob::patch::all(&self.repository).unwrap_or_default();
|
|
}
|
|
}
|
|
|
|
/// Get the default profile. Fails if there is no profile.
|
|
fn profile() -> Result<Profile, anyhow::Error> {
|
|
match Profile::load() {
|
|
Ok(profile) => Ok(profile),
|
|
Err(_) => Err(anyhow::anyhow!(
|
|
"Could not load radicle profile. To setup your radicle profile, run `rad auth`."
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Validates secret key passphrases.
|
|
#[derive(Clone)]
|
|
pub struct PassphraseValidator {
|
|
keystore: Keystore,
|
|
}
|
|
|
|
impl PassphraseValidator {
|
|
/// Create a new validator.
|
|
pub fn new(keystore: Keystore) -> Self {
|
|
Self { keystore }
|
|
}
|
|
}
|
|
|
|
impl inquire::validator::StringValidator for PassphraseValidator {
|
|
fn validate(
|
|
&self,
|
|
input: &str,
|
|
) -> Result<validator::Validation, inquire::error::CustomUserError> {
|
|
let passphrase = Passphrase::from(input.to_owned());
|
|
if self.keystore.is_valid_passphrase(&passphrase)? {
|
|
Ok(validator::Validation::Valid)
|
|
} else {
|
|
Ok(validator::Validation::Invalid(
|
|
validator::ErrorMessage::from("Invalid passphrase, please try again"),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get the signer. First we try getting it from ssh-agent, otherwise we prompt the user,
|
|
/// if we're connected to a TTY.
|
|
pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
|
|
if let Ok(signer) = profile.signer() {
|
|
return Ok(signer);
|
|
}
|
|
let validator = PassphraseValidator::new(profile.keystore.clone());
|
|
let passphrase = match passphrase(validator) {
|
|
Ok(p) => p,
|
|
Err(inquire::InquireError::NotTTY) => {
|
|
return Err(anyhow::anyhow!(
|
|
"running in non-interactive mode, please set `{RAD_PASSPHRASE}` to unseal your key",
|
|
));
|
|
}
|
|
Err(e) => return Err(e.into()),
|
|
};
|
|
let spinner = spinner("Unsealing key...");
|
|
let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?;
|
|
|
|
spinner.finish();
|
|
|
|
Ok(signer.boxed())
|
|
}
|