libknox 0.4.0

secret vault encrypted with GPG
Documentation
use std::error::Error;

use git2::{
    Commit, Config, Cred, IndexAddOption, ObjectType, PushOptions, RemoteCallbacks, Repository,
    Signature,
};

use crate::{util::VaultError, VaultContext};

pub fn exists(vault: &VaultContext) -> bool {
    vault.has_pack(".git")
}

fn last_commit(repo: &Repository) -> Result<Commit, git2::Error> {
    let object = repo.head()?.resolve()?.peel(ObjectType::Commit)?;

    object
        .into_commit()
        .map_err(|_| git2::Error::from_str("could not find latest commit"))
}

pub(crate) fn init(vault: &VaultContext) -> Result<(), Box<dyn Error>> {
    match Repository::init(&vault.path) {
        Ok(_) => commit(&vault, "Initialized knox repository."),
        Err(_) => Err(VaultError::throw("could not init git repository")),
    }
}

pub(crate) fn commit(vault: &VaultContext, message: &str) -> Result<(), Box<dyn Error>> {
    if !exists(&vault) {
        return Ok(());
    }

    match Repository::open(&vault.path) {
        Ok(repo) => {
            let (name, email) = Config::open_default()?
                .snapshot()
                .map(|c| {
                    (
                        c.get_string("user.name")
                            .unwrap_or_else(|_| "Knox".to_string()),
                        c.get_string("user.email")
                            .unwrap_or_else(|_| "N/A".to_string()),
                    )
                })
                .unwrap_or(("Knox".to_string(), "N/A".to_string()));

            let last_commit = last_commit(&repo).ok();
            let parent = match last_commit {
                Some(ref commit) => vec![commit],
                None => vec![],
            };

            let mut index = repo.index()?;
            index.add_all(&["*"], IndexAddOption::DEFAULT, None)?;

            let tree = repo.find_tree(index.write_tree()?)?;

            let author = Signature::now(&name, &email)?;

            repo.commit(Some("HEAD"), &author, &author, message, &tree, &parent)?;
            index.write()?;
        }
        Err(_) => {
            return Err(VaultError::throw(
                "could not open git repository, data was still written locally",
            ));
        }
    }

    Ok(())
}

pub(crate) fn set_origin(vault: &VaultContext, origin: &str) -> Result<(), Box<dyn Error>> {
    if !exists(&vault) {
        return Err(VaultError::throw("local git repository does not exist"));
    }

    let repo = Repository::open(&vault.path)?;

    repo.remote_set_url("origin", origin)?;

    Ok(())
}

pub(crate) fn push(vault: &VaultContext) -> Result<(), Box<dyn Error>> {
    if !exists(&vault) {
        return Err(VaultError::throw("local git repository does not exist"));
    }

    let repo = Repository::open(&vault.path)?;
    let mut remote = repo.find_remote("origin")?;
    let mut retry = false;
    let mut callbacks = RemoteCallbacks::new();

    callbacks.credentials(|_, user, credentials| {
        if retry {
            return Err(git2::Error::from_str(
                "ssh agent did not provide valid public key",
            ));
        }

        if credentials.contains(git2::CredentialType::USERNAME) {
            return Cred::username(user.unwrap_or("git"));
        }

        retry = true;

        Cred::ssh_key_from_agent("git")
    });

    let mut options = PushOptions::new();
    options.remote_callbacks(callbacks);

    remote.push(&["refs/heads/master:refs/heads/master"], Some(&mut options))?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use git2::{Repository, Sort};

    use knox_testing::spec;

    use crate::*;

    #[test]
    fn init() {
        let tmp = spec::setup();
        let context = VaultContext {
            path: tmp.path().to_str().unwrap().to_string(),
            vault: Vault::new(),
        };

        git::init(&context).expect("could not initialize git repository");

        let repo = Repository::open(tmp.path()).expect("could not open repository");
        let mut revwalk = repo.revwalk().expect("could not get revwalk");
        revwalk.push_head().expect("could not find HEAD");
        revwalk
            .set_sorting(Sort::REVERSE)
            .expect("could not set sort");

        let log: Vec<_> = revwalk.collect();

        assert_eq!(log.len(), 1);

        for rev in log {
            let commit = repo
                .find_commit(rev.unwrap())
                .expect("could not find root commit");

            assert_eq!(
                commit.summary().unwrap_or(""),
                "Initialized knox repository."
            );
        }
    }

    #[test]
    fn commit() {
        let tmp = spec::setup();
        let mut context = crate::spec::get_test_vault(tmp.path()).expect("could not get vault");
        context
            .git_init()
            .expect("could not create local git repository");

        context
            .write_entry("a", &Entry::default())
            .expect("could not write entry");
        context.commit("abcdef").expect("could not commit changes");

        let repo = Repository::open(tmp.path()).expect("could not open repository");
        let mut revwalk = repo.revwalk().expect("could not get revwalk");
        revwalk.push_head().expect("could not find HEAD");
        revwalk
            .set_sorting(Sort::REVERSE)
            .expect("could not get GPG key");

        for (i, rev) in revwalk.enumerate() {
            if i == 1 {
                let commit = repo
                    .find_commit(rev.unwrap())
                    .expect("could not find root commit");

                assert_eq!(commit.summary().unwrap_or(""), "abcdef");
            }
        }
    }

    #[test]
    fn set_remote() {
        let tmp = spec::setup();
        let context = crate::spec::get_test_vault(tmp.path()).expect("could not get vault");
        context
            .git_init()
            .expect("could not create local git repository");

        let repo = Repository::open(tmp.path()).expect("could not open repository");
        repo.remote("testremote", "https://git.example.com")
            .expect("could not set remote URL");

        let remote = repo
            .find_remote("testremote")
            .expect("could not find created remote");

        assert_eq!(remote.url().unwrap_or(""), "https://git.example.com");
    }
}