codebase 0.11.0

Manage your codebase like a boss!
Documentation
use std::collections::BTreeMap;
use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::{git_util, hook};

/// Represent a project as stored in the manifest
#[derive(Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct Project {
    pub remote: String,
    pub branch: String,
    pub configuration: BTreeMap<String, String>,
    pub hooks: BTreeMap<String, String>,
}

pub struct ProjectOpts {
    pub branch: Option<String>,
    pub configuration: BTreeMap<String, String>,
    pub hook: Option<String>,
}

impl Project {
    pub fn new(
        remote: &str,
        branch: &str,
        configuration: BTreeMap<String, String>,
        hooks: BTreeMap<String, String>,
    ) -> Self {
        Project {
            remote: remote.to_string(),
            branch: branch.to_string(),
            configuration,
            hooks,
        }
    }

    /// Create an empty project only with remote set
    ///
    /// # Arguments
    /// * `remote` - the Git remote
    pub fn empty(remote: &str) -> Self {
        Project::new(remote, "", BTreeMap::new(), BTreeMap::new())
    }

    /// Configure given Git repository using given config
    ///
    /// # Arguments
    /// * `repo` - the Git repository
    /// * `options` - the configuration options
    /// * `config_dir` - path to the .codebase-config directory
    pub fn configure<P: AsRef<Path>>(
        &mut self,
        repo: &git2::Repository,
        options: &ProjectOpts,
        config_dir: P,
    ) -> anyhow::Result<()> {
        // Change branch if needed
        if let Some(branch) = &options.branch {
            if *branch != self.branch {
                repo.set_head(&format!("refs/heads/{}", branch))?;
                self.branch = branch.clone();
            }
        }

        // Apply git configuration
        if !options.configuration.is_empty() {
            self.configuration.clear();
            let mut repo_config = repo.config()?;
            for (key, value) in &options.configuration {
                repo_config.set_str(key.as_str(), value.as_str())?;
                self.configuration.insert(key.clone(), value.clone());
            }
        }

        // Apply git hooks
        // TODO handle multiples hooks
        if let Some(hook) = &options.hook {
            hook::install(hook, repo.path(), config_dir)?;
            self.hooks.insert("pre-commit".to_string(), hook.clone());
        }

        Ok(())
    }
}

impl From<git2::Repository> for Project {
    fn from(repo: git2::Repository) -> Self {
        let mut project = Project::default();

        // Set remote
        if let Ok(remote) = repo.find_remote("origin") {
            project.remote = remote.name().unwrap().to_string();
        }

        // Set branch
        if let Some(branch) = git_util::get_active_branch(&repo).unwrap() {
            project.branch = branch;
        }

        // Set configuration
        let config = repo.config().unwrap().snapshot().unwrap();
        for entry in &config.entries(None).unwrap() {
            let entry = entry.unwrap();
            project.configuration.insert(
                entry.name().unwrap().to_string(),
                entry.value().unwrap().to_string(),
            );
        }

        project
    }
}

impl ProjectOpts {
    pub fn new(
        branch: Option<&str>,
        configuration: BTreeMap<String, String>,
        hook: Option<&str>,
    ) -> Self {
        ProjectOpts {
            branch: branch.map(String::from),
            configuration,
            hook: hook.map(String::from),
        }
    }
}

impl From<Project> for ProjectOpts {
    fn from(p: Project) -> Self {
        ProjectOpts {
            branch: Some(p.branch),
            configuration: p.configuration,
            hook: p.hooks.get("pre-commit").cloned(),
        }
    }
}