gitbackup 0.2.6

Backup all your git repositories with a single command
use crate::types::GenericRepository;
use crate::types::Provider;
use anyhow::{Context, Result};
use git2::build::CheckoutBuilder;
use git2::{Repository as Git, Repository};
use std::path::{Path, PathBuf};
use std::time::Duration;

pub fn sync_repositories(
    backup_path: &Path,
    provider: &Provider,
    repositories: &[impl GenericRepository],
) {
    println!("Found {:#?} repositories", &repositories.len());

    for r in repositories {
        if let Err(e) = sync_repository(backup_path, provider, r) {
            eprintln!(
                "Unable to sync repository {} with error {e:?}",
                &r.full_name()
            );
        }

        std::thread::sleep(Duration::from_millis(1000));
    }
}

fn sync_repository(
    backup_path: &Path,
    provider: &Provider,
    repository: &impl GenericRepository,
) -> Result<()> {
    let repository_directory = format!(
        "{}-{}",
        provider.host,
        &repository.full_name().replace('/', "-")
    );
    let path = backup_path.join(repository_directory);

    if let Ok(git) = Git::open(&path) {
        pull(&git, &path, provider, repository)?;
    } else {
        let git_url = format!(
            "https://{}:{}@{}/{}.git",
            &provider.username,
            &provider.token,
            &provider.host,
            &repository.full_name()
        );

        clone(&git_url, &path, provider, repository)?;
    };

    Ok(())
}

fn pull(
    git: &Repository,
    path: &Path,
    provider: &Provider,
    repository: &impl GenericRepository,
) -> Result<()> {
    println!(
        "Syncing {}/{} to {}",
        &provider.host,
        &repository.full_name(),
        &path.display()
    );

    if git.is_empty()? {
        return Ok(());
    }

    git.find_remote("origin")?
        .fetch(&[&repository.default_branch()], None, None)?;

    let fetch_head = git.find_reference("FETCH_HEAD")?;
    let fetch_commit = git.reference_to_annotated_commit(&fetch_head)?;
    let analysis = git.merge_analysis(&[&fetch_commit])?;

    if analysis.0.is_fast_forward() {
        let refname = format!("refs/heads/{}", &repository.default_branch());
        let mut reference = git.find_reference(&refname)?;
        reference.set_target(fetch_commit.id(), "Fast-Forward")?;
        git.set_head(&refname)?;
        git.checkout_head(Some(CheckoutBuilder::default().force()))?;
    };

    Ok(())
}

fn clone(
    url: &str,
    path: &PathBuf,
    provider: &Provider,
    repository: &impl GenericRepository,
) -> Result<()> {
    println!(
        "Cloning {}/{} to {}",
        &provider.host,
        &repository.full_name(),
        &path.display()
    );

    Git::clone(url, path).with_context(|| {
        format!(
            "Unable to clone repository {} with url {}",
            &repository.full_name(),
            &url
        )
    })?;

    Ok(())
}