Menu
AkurAI-Build
publicLatest 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())
}
}