use std::backtrace::Backtrace;
use std::fmt;
#[derive(Debug)]
pub(crate) enum GitwayErrorKind {
Io(std::io::Error),
Ssh(russh::Error),
Keys(russh::keys::Error),
HostKeyMismatch { fingerprint: String },
AuthenticationFailed,
NoKeyFound,
InvalidConfig { message: String },
}
impl fmt::Display for GitwayErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::Ssh(e) => write!(f, "SSH protocol error: {e}"),
Self::Keys(e) => write!(f, "SSH key error: {e}"),
Self::HostKeyMismatch { fingerprint } => {
write!(
f,
"host key mismatch — received fingerprint {fingerprint} \
does not match any pinned fingerprint"
)
}
Self::AuthenticationFailed => write!(f, "public-key authentication failed"),
Self::NoKeyFound => {
write!(f, "no SSH identity key found on any search path or agent")
}
Self::InvalidConfig { message } => write!(f, "invalid configuration: {message}"),
}
}
}
#[derive(Debug)]
pub struct GitwayError {
kind: GitwayErrorKind,
backtrace: Backtrace,
}
impl GitwayError {
pub(crate) fn new(kind: GitwayErrorKind) -> Self {
Self {
kind,
backtrace: Backtrace::capture(),
}
}
pub fn host_key_mismatch(fingerprint: impl Into<String>) -> Self {
Self::new(GitwayErrorKind::HostKeyMismatch {
fingerprint: fingerprint.into(),
})
}
#[must_use]
pub fn authentication_failed() -> Self {
Self::new(GitwayErrorKind::AuthenticationFailed)
}
#[must_use]
pub fn no_key_found() -> Self {
Self::new(GitwayErrorKind::NoKeyFound)
}
pub fn invalid_config(message: impl Into<String>) -> Self {
Self::new(GitwayErrorKind::InvalidConfig {
message: message.into(),
})
}
#[must_use]
pub fn is_io(&self) -> bool {
matches!(self.kind, GitwayErrorKind::Io(_))
}
#[must_use]
pub fn is_host_key_mismatch(&self) -> bool {
matches!(self.kind, GitwayErrorKind::HostKeyMismatch { .. })
}
#[must_use]
pub fn is_authentication_failed(&self) -> bool {
matches!(self.kind, GitwayErrorKind::AuthenticationFailed)
}
#[must_use]
pub fn is_no_key_found(&self) -> bool {
matches!(self.kind, GitwayErrorKind::NoKeyFound)
}
#[must_use]
pub fn is_key_encrypted(&self) -> bool {
matches!(
self.kind,
GitwayErrorKind::Keys(russh::keys::Error::KeyIsEncrypted)
)
}
#[must_use]
pub fn fingerprint(&self) -> Option<&str> {
match &self.kind {
GitwayErrorKind::HostKeyMismatch { fingerprint } => Some(fingerprint),
_ => None,
}
}
#[must_use]
pub fn error_code(&self) -> &'static str {
match &self.kind {
GitwayErrorKind::InvalidConfig { .. } => "USAGE_ERROR",
GitwayErrorKind::NoKeyFound => "NOT_FOUND",
GitwayErrorKind::HostKeyMismatch { .. } | GitwayErrorKind::AuthenticationFailed => {
"PERMISSION_DENIED"
}
GitwayErrorKind::Io(_) | GitwayErrorKind::Ssh(_) | GitwayErrorKind::Keys(_) => {
"GENERAL_ERROR"
}
}
}
#[must_use]
pub fn exit_code(&self) -> u32 {
match &self.kind {
GitwayErrorKind::InvalidConfig { .. } => 2,
GitwayErrorKind::NoKeyFound => 3,
GitwayErrorKind::HostKeyMismatch { .. } | GitwayErrorKind::AuthenticationFailed => 4,
GitwayErrorKind::Io(_) | GitwayErrorKind::Ssh(_) | GitwayErrorKind::Keys(_) => 1,
}
}
#[must_use]
pub fn hint(&self) -> &'static str {
match &self.kind {
GitwayErrorKind::HostKeyMismatch { .. } => {
"Run 'gitway --test --verbose' to diagnose, \
or check ~/.config/gitway/known_hosts"
}
GitwayErrorKind::AuthenticationFailed => {
"Ensure your SSH public key is registered with the Git hosting service, \
or run 'ssh-add' to load a key into the agent"
}
GitwayErrorKind::NoKeyFound => {
"Run 'ssh-keygen -t ed25519' to generate a key, or use --identity to specify one"
}
GitwayErrorKind::InvalidConfig { .. } => {
"Run 'gitway --help' for usage information"
}
GitwayErrorKind::Io(_) | GitwayErrorKind::Ssh(_) | GitwayErrorKind::Keys(_) => {
"Run 'gitway --test --verbose' to diagnose the connection"
}
}
}
}
impl fmt::Display for GitwayError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
let bt = self.backtrace.to_string();
if !bt.is_empty() && bt != "disabled backtrace" {
write!(f, "\n\nstack backtrace:\n{bt}")?;
}
Ok(())
}
}
impl std::error::Error for GitwayError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.kind {
GitwayErrorKind::Io(e) => Some(e),
GitwayErrorKind::Ssh(e) => Some(e),
GitwayErrorKind::Keys(e) => Some(e),
_ => None,
}
}
}
impl From<russh::Error> for GitwayError {
fn from(e: russh::Error) -> Self {
Self::new(GitwayErrorKind::Ssh(e))
}
}
impl From<russh::keys::Error> for GitwayError {
fn from(e: russh::keys::Error) -> Self {
Self::new(GitwayErrorKind::Keys(e))
}
}
impl From<std::io::Error> for GitwayError {
fn from(e: std::io::Error) -> Self {
Self::new(GitwayErrorKind::Io(e))
}
}
impl From<russh::AgentAuthError> for GitwayError {
fn from(e: russh::AgentAuthError) -> Self {
match e {
russh::AgentAuthError::Send(_) => Self::new(GitwayErrorKind::Ssh(russh::Error::SendError)),
russh::AgentAuthError::Key(k) => Self::new(GitwayErrorKind::Keys(k)),
}
}
}