git-credentials 0.9.0

A WIP crate of the gitoxide project to interact with git credentials helpers
Documentation
use bstr::BString;

use crate::helper;

/// The outcome of the credentials top-level functions to obtain a complete identity.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Outcome {
    /// The identity provide by the helper.
    pub identity: git_sec::identity::Account,
    /// A handle to the action to perform next in another call to [`helper::invoke()`][crate::helper::invoke()].
    pub next: helper::NextAction,
}

/// The Result type used in credentials top-level functions to obtain a complete identity.
pub type Result = std::result::Result<Option<Outcome>, Error>;

/// The error returned top-level credential functions.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
    #[error(transparent)]
    UrlParse(#[from] git_url::parse::Error),
    #[error("The 'url' field must be set when performing a 'get/fill' action")]
    UrlMissing,
    #[error(transparent)]
    ContextDecode(#[from] context::decode::Error),
    #[error(transparent)]
    InvokeHelper(#[from] helper::Error),
    #[error("Could not obtain identity for context: {}", { let mut buf = Vec::<u8>::new(); context.write_to(&mut buf).ok(); String::from_utf8_lossy(&buf).into_owned() })]
    IdentityMissing { context: Context },
    #[error("The handler asked to stop trying to obtain credentials")]
    Quit,
    #[error("Couldn't obtain {prompt}")]
    Prompt { prompt: String, source: git_prompt::Error },
}

/// Additional context to be passed to the credentials helper.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct Context {
    /// The protocol over which the credential will be used (e.g., https).
    pub protocol: Option<String>,
    /// The remote hostname for a network credential. This includes the port number if one was specified (e.g., "example.com:8088").
    pub host: Option<String>,
    /// The path with which the credential will be used. E.g., for accessing a remote https repository, this will be the repository’s path on the server.
    /// It can also be a path on the file system.
    pub path: Option<BString>,
    /// The credential’s username, if we already have one (e.g., from a URL, the configuration, the user, or from a previously run helper).
    pub username: Option<String>,
    /// The credential’s password, if we are asking it to be stored.
    pub password: Option<String>,
    /// When this special attribute is read by git credential, the value is parsed as a URL and treated as if its constituent
    /// parts were read (e.g., url=<https://example.com> would behave as if
    /// protocol=https and host=example.com had been provided). This can help callers avoid parsing URLs themselves.
    pub url: Option<BString>,
    /// If true, the caller should stop asking for credentials immediately without calling more credential helpers in the chain.
    pub quit: Option<bool>,
}

/// Convert the outcome of a helper invocation to a helper result, assuring that the identity is complete in the process.
#[allow(clippy::result_large_err)]
pub fn helper_outcome_to_result(outcome: Option<helper::Outcome>, action: helper::Action) -> Result {
    fn redact(mut ctx: Context) -> Context {
        if let Some(pw) = ctx.password.as_mut() {
            *pw = "<redacted>".into()
        }
        ctx
    }
    match (action, outcome) {
        (helper::Action::Get(ctx), None) => Err(Error::IdentityMissing { context: redact(ctx) }),
        (helper::Action::Get(ctx), Some(mut outcome)) => match outcome.consume_identity() {
            Some(identity) => Ok(Some(Outcome {
                identity,
                next: outcome.next,
            })),
            None => Err(if outcome.quit {
                Error::Quit
            } else {
                Error::IdentityMissing { context: redact(ctx) }
            }),
        },
        (helper::Action::Store(_) | helper::Action::Erase(_), _ignore) => Ok(None),
    }
}

///
pub mod context;