Menu
AkurAI-Build
publicLatest change da796b936c05c2d842fce086d0072fb5c6f9da58 - pr: enforce branch protection at HTTP push and hosted_git::sync bypass 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",
) {
if let Err(cleanup_error) = fs::remove_dir_all(&destination) {
return Err(error.context(format!(
"also failed to clean up {}: {cleanup_error}",
destination.display()
)));
}
return Err(error);
}
Ok(destination)
}
/// Synchronize the hosted mirror from `source`. `protected_refs` are branch
/// names excluded from the fetch's `refs/*` wildcard (via a negative
/// refspec), so a protected branch can only advance through the merge queue,
/// never through a direct `akurai_repo_sync` mirror refresh.
pub fn sync(root: &Path, name: &str, source: &Path, protected_refs: &[String]) -> 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}");
let mut command = Command::new("git");
command
.arg("-C")
.arg(&destination)
.args(["fetch", "--prune"])
.arg(&source)
.arg("+refs/*:refs/*");
for branch in protected_refs {
command.arg(format!("^refs/heads/{branch}"));
}
run(&mut command, "sync Git mirror")?;
Ok(destination)
}
/// Delete a hosted bare mirror if it exists. Returns the removed path, or
/// `None` when there was no local mirror (e.g. a repository registered by URL).
pub fn remove(root: &Path, name: &str) -> Result<Option<PathBuf>> {
let destination = repository_path(root, name)?;
if destination.is_dir() {
fs::remove_dir_all(&destination).with_context(|| {
format!("failed to delete hosted mirror: {}", destination.display())
})?;
Ok(Some(destination))
} else {
Ok(None)
}
}
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;
fn init_source(work: &Path) -> Result<PathBuf> {
let source = work.join("source");
fs::create_dir(&source)?;
git(&source, &["init", "-b", "main"])?;
git(&source, &["config", "user.name", "AkurAI Build"])?;
git(&source, &["config", "user.email", "build@localhost"])?;
Ok(source)
}
fn commit(repository: &Path, message: &str) -> Result<()> {
fs::write(repository.join("README.md"), format!("{message}\n"))?;
git(repository, &["add", "."])?;
git(repository, &["commit", "-m", message])
}
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())
}
fn git(repository: &Path, arguments: &[&str]) -> Result<()> {
run(
Command::new("git")
.arg("-C")
.arg(repository)
.args(arguments),
&format!("git {}", arguments.join(" ")),
)
}
fn has_config(repository: &Path, key: &str, expected: &str) -> bool {
let output = Command::new("git")
.arg("-C")
.arg(repository)
.args(["config", key])
.output();
output
.ok()
.filter(|out| out.status.success())
.and_then(|out| String::from_utf8(out.stdout).ok())
.is_some_and(|v| v.trim() == expected)
}
#[test]
fn host_creates_bare_mirror_with_matching_head() -> Result<()> {
let work = tempdir()?;
let source = init_source(work.path())?;
commit(&source, "initial")?;
let source_head = head(&source)?;
let root = work.path().join("data");
let hosted = host(&root, "app", &source)?;
let hosted_head = head(&hosted)?;
assert_eq!(
source_head, hosted_head,
"mirror HEAD must equal source HEAD"
);
assert!(
hosted.join("HEAD").is_file(),
"bare mirror must have a HEAD file"
);
assert!(
!hosted.join("index").exists(),
"bare mirror should not have a working-tree index"
);
assert!(
has_config(&hosted, "http.receivepack", "true"),
"mirror must have Smart-HTTP receive enabled"
);
Ok(())
}
#[test]
fn sync_propagates_new_commits_branches_and_tags() -> Result<()> {
let work = tempdir()?;
let source = init_source(work.path())?;
commit(&source, "first")?;
git(&source, &["tag", "-a", "v1.0", "-m", "first release"])?;
let root = work.path().join("data");
let hosted = host(&root, "app", &source)?;
commit(&source, "second")?;
git(&source, &["branch", "feature"])?;
git(&source, &["tag", "-a", "v2.0", "-m", "second release"])?;
sync(&root, "app", &source, &[])?;
let mirror_head = head(&hosted)?;
let source_head = head(&source)?;
assert_eq!(
mirror_head, source_head,
"mirror HEAD should reflect new commits"
);
let branches = Command::new("git")
.arg("-C")
.arg(&hosted)
.args(["branch", "--list", "feature"])
.output()?;
assert!(
branches.status.success()
&& !String::from_utf8_lossy(&branches.stdout).trim().is_empty(),
"feature branch must be present in mirror"
);
let tags = Command::new("git")
.arg("-C")
.arg(&hosted)
.args(["tag", "--list"])
.output()?;
let tag_list = String::from_utf8_lossy(&tags.stdout);
assert!(
tag_list.contains("v1.0") && tag_list.contains("v2.0"),
"both tags must be present in mirror: {tag_list}"
);
Ok(())
}
#[test]
fn repository_name_rejects_traversal() {
for name in ["", "../escape", "repo/hack", "has space", "\x00null"] {
let result = repository_path(Path::new("root"), name);
assert!(
result.is_err(),
"repository name {name:?} should be rejected"
);
}
}
#[test]
fn repository_name_accepts_valid() -> Result<()> {
for name in ["app", "my-repo", "repo_v2", "test.repo", "a"] {
let path = repository_path(Path::new("root"), name)?;
assert!(
path.ends_with(format!("{name}.git")),
"valid name {name:?} should produce {name}.git suffix"
);
}
Ok(())
}
#[test]
fn sync_unknown_repository_errors() -> Result<()> {
let work = tempdir()?;
let source = init_source(work.path())?;
commit(&source, "first")?;
let root = work.path().join("data");
let result = sync(&root, "nonexistent", &source, &[]);
assert!(result.is_err(), "sync of unknown repo should error");
Ok(())
}
#[test]
fn host_missing_source_errors() {
let work = tempdir().expect("create temp dir");
let missing = work.path().join("does-not-exist");
let root = work.path().join("data");
let result = host(&root, "app", &missing);
assert!(result.is_err(), "host of missing source should error");
}
#[test]
fn rehost_existing_name_errors() -> Result<()> {
let work = tempdir()?;
let source = init_source(work.path())?;
commit(&source, "first")?;
let root = work.path().join("data");
host(&root, "app", &source)?;
let result = host(&root, "app", &source);
assert!(result.is_err(), "re-hosting existing name should error");
Ok(())
}
#[test]
fn host_mirror_is_bare() -> Result<()> {
let work = tempdir()?;
let source = init_source(work.path())?;
commit(&source, "first")?;
let root = work.path().join("data");
let hosted = host(&root, "app", &source)?;
let is_bare = Command::new("git")
.arg("-C")
.arg(&hosted)
.args(["rev-parse", "--is-bare-repository"])
.output()
.map(|out| String::from_utf8_lossy(&out.stdout).trim() == "true")
.unwrap_or(false);
assert!(is_bare, "hosted mirror must be a bare repository");
Ok(())
}
#[test]
fn remove_deletes_hosted_mirror() -> Result<()> {
let work = tempdir()?;
let source = init_source(work.path())?;
commit(&source, "first")?;
let root = work.path().join("data");
let hosted = host(&root, "app", &source)?;
assert!(hosted.is_dir());
let deleted = remove(&root, "app")?;
assert_eq!(deleted.as_deref(), Some(hosted.as_path()));
assert!(!hosted.exists(), "mirror dir must be gone after remove");
Ok(())
}
#[test]
fn remove_absent_mirror_returns_none() -> Result<()> {
let work = tempdir()?;
let root = work.path().join("data");
assert!(remove(&root, "nope")?.is_none());
Ok(())
}
#[test]
fn sync_excludes_protected_branch_from_wildcard_fetch() -> Result<()> {
let work = tempdir()?;
let source = init_source(work.path())?;
commit(&source, "initial")?;
let root = work.path().join("data");
let hosted = host(&root, "app", &source)?;
let protected_head = head(&hosted)?;
// New commits land on both the protected `main` and an unprotected
// `feature` branch.
commit(&source, "protected-branch-advance")?;
git(&source, &["checkout", "-b", "feature"])?;
commit(&source, "feature-advance")?;
git(&source, &["checkout", "main"])?;
sync(&root, "app", &source, &["main".to_owned()])?;
assert_eq!(
head(&hosted)?,
protected_head,
"protected branch must not advance through sync"
);
let feature_output = Command::new("git")
.arg("-C")
.arg(&hosted)
.args(["rev-parse", "refs/heads/feature"])
.output()?;
assert!(
feature_output.status.success(),
"unprotected feature branch must still sync"
);
Ok(())
}
}