AkurAI Build
Menu

AkurAI-Build

public

Latest change 30e6b8c347048d64c2c048dfd05a8ded219d37f6 - Host authenticated Git repositories over Smart HTTP with repo host/sync CLI by Ólafur Búi Ólafsson

use std::{
    fs,
    path::{Path, PathBuf},
    process::Command,
};

use anyhow::{Context, Result, ensure};

use crate::config;

pub fn repository_path(root: &Path, name: &str) -> Result<PathBuf> {
    config::validate_repo_name(name)?;
    Ok(root.join("hosted").join(format!("{name}.git")))
}

pub fn host(root: &Path, name: &str, source: &Path) -> Result<PathBuf> {
    let source = source
        .canonicalize()
        .with_context(|| format!("source repository does not exist: {}", source.display()))?;
    let destination = repository_path(root, name)?;
    ensure!(
        !destination.exists(),
        "hosted repository already exists: {name}"
    );
    let parent = destination
        .parent()
        .context("hosted repository has no parent")?;
    fs::create_dir_all(parent)?;
    run(
        Command::new("git")
            .args(["clone", "--mirror"])
            .arg(&source)
            .arg(&destination),
        "git mirror clone",
    )?;
    if let Err(error) = run(
        Command::new("git").arg("-C").arg(&destination).args([
            "config",
            "http.receivepack",
            "true",
        ]),
        "enable Git Smart HTTP writes",
    ) {
        let _ = fs::remove_dir_all(&destination);
        return Err(error);
    }
    Ok(destination)
}

pub fn sync(root: &Path, name: &str, source: &Path) -> Result<PathBuf> {
    let source = source
        .canonicalize()
        .with_context(|| format!("source repository does not exist: {}", source.display()))?;
    let destination = repository_path(root, name)?;
    ensure!(destination.is_dir(), "unknown hosted repository: {name}");
    run(
        Command::new("git")
            .arg("-C")
            .arg(&destination)
            .args(["fetch", "--prune"])
            .arg(&source)
            .arg("+refs/*:refs/*"),
        "sync Git mirror",
    )?;
    Ok(destination)
}

fn run(command: &mut Command, operation: &str) -> Result<()> {
    let output = command
        .output()
        .with_context(|| format!("failed to start {operation}"))?;
    ensure!(
        output.status.success(),
        "{operation} failed: {}",
        String::from_utf8_lossy(&output.stderr).trim()
    );
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn hosts_and_synchronizes_a_repository() -> Result<()> {
        let work = tempdir()?;
        let source = work.path().join("source");
        fs::create_dir(&source)?;
        run(
            Command::new("git")
                .arg("init")
                .args(["-b", "main"])
                .arg(&source),
            "initialize source",
        )?;
        fs::write(source.join("README.md"), "first\n")?;
        commit(&source, "first")?;

        let root = work.path().join("data");
        let hosted = host(&root, "app", &source)?;
        let first = head(&hosted)?;
        fs::write(source.join("README.md"), "second\n")?;
        commit(&source, "second")?;
        sync(&root, "app", &source)?;

        assert_ne!(first, head(&hosted)?);
        Ok(())
    }

    fn commit(repository: &Path, message: &str) -> Result<()> {
        run(
            Command::new("git")
                .arg("-C")
                .arg(repository)
                .args(["add", "."]),
            "stage fixture",
        )?;
        run(
            Command::new("git").arg("-C").arg(repository).args([
                "-c",
                "user.name=AkurAI Build",
                "-c",
                "user.email=build@localhost",
                "commit",
                "-m",
                message,
            ]),
            "commit fixture",
        )
    }

    fn head(repository: &Path) -> Result<String> {
        let output = Command::new("git")
            .arg("-C")
            .arg(repository)
            .args(["rev-parse", "refs/heads/main"])
            .output()?;
        ensure!(output.status.success(), "read hosted head failed");
        Ok(String::from_utf8(output.stdout)?.trim().to_owned())
    }
}