gled-core 2.31.2

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
mod credentials;
mod remote;

pub(crate) use credentials::GitCredentials;

use super::STORAGE_DIR;
use git2::{Error, ErrorCode, Reference, Repository, Signature};
use mkdirp::mkdirp;
use std::{io::Write, path::Path};

pub(crate) struct Git {
    url: String,
    repository: Option<Repository>,
}

impl Git {
    pub(crate) fn open(url: String) -> Result<Self, Error> {
        let mut db = Self {
            url,
            repository: None,
        };
        db.init()?;
        Ok(db)
    }

    pub(crate) fn branches(&self) -> Result<Vec<String>, Error> {
        let repository = self
            .repository
            .as_ref()
            .ok_or(Error::from_str("No repository set"))?;

        repository
            .branches(Some(git2::BranchType::Remote))?
            .map(|branch| {
                Ok(branch?
                    .0
                    .name()?
                    .and_then(|name| name.strip_prefix("origin/"))
                    .map(|name| name.to_owned()))
            })
            .filter_map(|name| name.transpose())
            .filter(|name| name.as_deref() != Ok("HEAD"))
            .collect()
    }

    pub(crate) fn current_branch(&self) -> Result<String, Error> {
        Ok(self.branch_shorthand())
    }

    pub(crate) fn switch_branch(&self, name: &str) -> Result<(), Error> {
        let repository = self
            .repository
            .as_ref()
            .ok_or(Error::from_str("No repository set"))?;

        if repository
            .find_branch(name, git2::BranchType::Local)
            .is_err()
        {
            let remote_branch =
                repository.find_branch(&format!("origin/{name}"), git2::BranchType::Remote)?;
            let commit = remote_branch.into_reference().peel_to_commit()?;
            repository.branch(name, &commit, false)?;
        }

        let (object, reference) = repository.revparse_ext(name)?;
        repository.checkout_tree(&object, None)?;

        repository.set_head(
            reference
                .ok_or_else(|| Error::from_str("Reference is empty"))?
                .name()?,
        )?;

        Ok(())
    }

    fn branch_reference(&'_ self) -> Option<Reference<'_>> {
        self.repository
            .as_ref()
            .and_then(|repository| repository.head().ok())
    }

    fn branch_name(&self) -> String {
        self.branch_reference()
            .and_then(|h| h.name().ok().map(|name| name.to_owned()))
            .unwrap_or_else(|| "main".to_owned())
    }

    fn branch_shorthand(&self) -> String {
        self.branch_reference()
            .and_then(|h| h.shorthand().ok().map(|name| name.to_owned()))
            .unwrap_or_else(|| "main".to_owned())
    }

    pub(crate) fn init(&mut self) -> Result<(), Error> {
        tracing::debug!("Loading repository at {}", STORAGE_DIR.display());
        match Repository::open(&*STORAGE_DIR) {
            Ok(repository) => {
                tracing::debug!("Repository loaded!");
                self.repository = Some(repository);
                Ok(())
            }
            Err(err) => {
                tracing::warn!("Could not open repository: {err:?}");
                self.clone()
            }
        }
    }

    pub(crate) fn add(&self, file: &Path) -> Result<(), Error> {
        tracing::info!("Adding file: {}", file.display());

        let repository = self
            .repository
            .as_ref()
            .ok_or(Error::from_str("No repository set"))?;
        let file = file
            .strip_prefix(&*STORAGE_DIR)
            .map_err(|_err| Error::from_str("Could not make path relative"))?;

        let mut index = repository.index()?;
        index.add_path(file)?;
        index.write()?;

        Ok(())
    }

    fn delete(&self, file: &Path) -> Result<(), Error> {
        tracing::info!("Deleting file: {}", file.display());

        let repository = self
            .repository
            .as_ref()
            .ok_or(Error::from_str("No repository set"))?;

        let root = repository
            .path()
            .parent()
            .ok_or_else(|| Error::from_str("No parent"))?;
        let file = file
            .strip_prefix(root)
            .map_err(|_err| Error::from_str("Could not make path relative"))?;

        let mut index = repository.index()?;
        index.remove_path(file)?;
        index.write()?;

        Ok(())
    }

    pub(crate) fn commit(&self, msg: &str) -> Result<(), Error> {
        tracing::info!("Committing..");

        let repository = self
            .repository
            .as_ref()
            .ok_or(Error::from_str("No repository set"))?;

        let config = git2::Config::open_default()?;
        let name = config.get_string("user.name")?;
        let email = config.get_string("user.email")?;

        let mut index = repository.index()?;
        let tree_oid = index.write_tree()?;
        let tree = repository.find_tree(tree_oid)?;

        let parent_commit = match repository.revparse_single("HEAD") {
            Ok(obj) => Some(obj.into_commit().expect("HEAD must point to a commit")),
            // First commit so no parent commit
            Err(e) if e.code() == ErrorCode::NotFound => None,
            Err(e) => return Err(e),
        };

        let mut parents = Vec::new();
        if let Some(parent_commit) = parent_commit.as_ref() {
            parents.push(parent_commit);
        }

        let signature = Signature::now(&name, &email)?;
        repository.commit(
            Some("HEAD"),
            &signature,
            &signature,
            msg,
            &tree,
            &parents[..],
        )?;

        Ok(())
    }

    pub(crate) fn count_staged_files(&self) -> Result<usize, Error> {
        let repository = self
            .repository
            .as_ref()
            .ok_or(Error::from_str("No repository set"))?;

        Ok(repository.statuses(None)?.len())
    }

    pub(crate) fn delete_asset(&mut self, path: &Path) -> Result<(), String> {
        if let Err(err) = std::fs::remove_file(path) {
            return Err(format!("Could not remove file: {err:?}"));
        }
        if let Err(err) = self.delete(path) {
            return Err(format!("Could not delete file from git: {err:?}"));
        }

        Ok(())
    }

    pub(crate) fn write_asset(
        &mut self,
        file: &Path,
        contents: impl AsRef<[u8]>,
    ) -> Result<(), String> {
        let parent = file.parent().expect("Could not get parent of asset path");
        if let Err(err) = mkdirp(parent) {
            return Err(format!(
                "Could not create basedir of {}: {err:?}",
                parent.display()
            ));
        }
        {
            let mut file = match std::fs::File::create(file) {
                Ok(file) => file,
                Err(err) => {
                    return Err(format!("Could not open file {}: {err:?}", file.display()));
                }
            };
            if let Err(err) = file.write_all(contents.as_ref()) {
                return Err(format!("Could not write asset: {err:?}"));
            }
        }
        if let Err(err) = self.add(file) {
            return Err(format!("Could not add file to git: {err:?}"));
        }

        Ok(())
    }
}