gled-core 2.34.0

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
//! Talking to the remote repository: cloning, pulling and pushing with the
//! user's SSH key.

use super::Git;
use crate::{runtime::persistent_state::PersistentState, storage::STORAGE_DIR};
use git2::{
    Cred, Error, FetchOptions, PushOptions, RemoteCallbacks, Repository, build::RepoBuilder,
};

impl Git {
    fn push_options(&self) -> PushOptions<'static> {
        let mut push_options = PushOptions::new();
        push_options.remote_callbacks(self.remote_callbacks());
        push_options
    }

    fn fetch_options(&self) -> FetchOptions<'static> {
        let mut fetch_options = FetchOptions::new();
        fetch_options.remote_callbacks(self.remote_callbacks());
        fetch_options
    }

    fn remote_callbacks(&self) -> RemoteCallbacks<'static> {
        let mut callbacks = RemoteCallbacks::new();
        callbacks.credentials(move |_url, username_from_url, _allowed_types| {
            let username = username_from_url
                .map(|username| username.to_owned())
                .unwrap_or(whoami::username().expect("Could not get username"));

            let credentials = PersistentState::default().git_credentials();
            if let Some(private_key) = credentials
                .private_key_path()
                .and_then(|path| std::fs::read_to_string(path).ok())
            {
                Cred::ssh_key_from_memory(
                    &username,
                    None,
                    &private_key,
                    credentials.passphrase().as_deref(),
                )
            } else {
                Err(Error::from_str("No private key set"))
            }
        });
        callbacks
    }

    pub(crate) fn pull(&mut self) -> Result<(), Error> {
        self.repository = Some(Repository::open(&*STORAGE_DIR)?);
        let branch = self.branch_shorthand();

        let mut fetch_options = self.fetch_options();
        let repository = self
            .repository
            .as_mut()
            .ok_or_else(|| Error::from_str("no repository set"))?;

        let remote = &mut repository.find_remote("origin")?;

        remote.fetch::<&str>(&[], Some(&mut fetch_options), None)?;

        let fetch_head = repository.find_reference("FETCH_HEAD")?;
        let fetch_commit = repository.reference_to_annotated_commit(&fetch_head)?;

        let analysis = repository.merge_analysis(&[&fetch_commit])?;
        if analysis.0.is_fast_forward() {
            let refname = format!("refs/heads/{branch}");

            match repository.find_reference(&refname) {
                Ok(mut r) => {
                    let name = match r.name() {
                        Ok(s) => s.to_string(),
                        Err(_) => String::from_utf8_lossy(r.name_bytes()).to_string(),
                    };
                    let msg = format!(
                        "Fast-Forward: Setting {} to id: {}",
                        name,
                        fetch_commit.id()
                    );
                    r.set_target(fetch_commit.id(), &msg)?;
                    repository.set_head(&name)?;
                    repository
                        .checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?;
                }
                Err(_) => {
                    repository.reference(
                        &refname,
                        fetch_commit.id(),
                        true,
                        &format!("Setting {branch} to {}", fetch_commit.id()),
                    )?;
                    repository.set_head(&refname)?;
                    repository.checkout_head(Some(
                        git2::build::CheckoutBuilder::default()
                            .allow_conflicts(true)
                            .conflict_style_merge(true)
                            .force(),
                    ))?;
                }
            };
        } else if analysis.0.is_normal() {
            let head_commit = repository.reference_to_annotated_commit(&repository.head()?)?;

            let local_tree = repository.find_commit(head_commit.id())?.tree()?;
            let remote_tree = repository.find_commit(fetch_commit.id())?.tree()?;
            let ancestor = repository
                .find_commit(repository.merge_base(head_commit.id(), fetch_commit.id())?)?
                .tree()?;
            let mut idx = repository.merge_trees(&ancestor, &local_tree, &remote_tree, None)?;

            if idx.has_conflicts() {
                tracing::warn!("Merge conflicts detected");
                repository.checkout_index(Some(&mut idx), None)?;
                return Ok(());
            }
            let result_tree = repository.find_tree(idx.write_tree_to(repository)?)?;
            // now create the merge commit
            let msg = format!("Merge: {} into {}", fetch_commit.id(), head_commit.id());
            let sig = repository.signature()?;
            let local_commit = repository.find_commit(head_commit.id())?;
            let remote_commit = repository.find_commit(fetch_commit.id())?;
            // Do our merge commit and set current branch head to that commit.
            let _merge_commit = repository.commit(
                Some("HEAD"),
                &sig,
                &sig,
                &msg,
                &result_tree,
                &[&local_commit, &remote_commit],
            )?;
            repository.checkout_head(None)?;
        }

        Ok(())
    }

    pub(super) fn clone(&mut self) -> Result<(), Error> {
        let _ = std::fs::remove_dir_all(&*STORAGE_DIR);

        self.repository = Some(
            RepoBuilder::new()
                .fetch_options(self.fetch_options())
                .clone(&self.url, &STORAGE_DIR)?,
        );

        Ok(())
    }

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

        let remote = &mut repository.find_remote("origin")?;

        remote.push(&[name], Some(&mut self.push_options()))?;

        Ok(())
    }
}