use std::path::Path; use std::process::Command; use anyhow::{Context as _, Result, bail}; /// Runs a Git command, returning its stdout. /// /// Stderr is inherited so that Git's progress output is visible to the user. fn git(args: &[&str]) -> Result { let output = Command::new("git") .args(args) .stderr(std::process::Stdio::inherit()) .output() .context("failed to run `git`. Is Git installed?")?; if !output.status.success() { bail!("`git {}` exited with {}", args.join(" "), output.status); } String::from_utf8(output.stdout).context("git output was not valid UTF-8") } /// A local bare mirror of a repository, used as the staging area for pushes /// and bundles. pub struct MirrorRepo<'a> { path: &'a Path, } impl<'a> MirrorRepo<'a> { /// Clones or updates a bare mirror of `source` at `path`. pub fn clone_or_update(source: &str, path: &'a Path) -> Result { if path.join("HEAD").exists() { let path_str = path_str(path)?; // Keep the remote URL in sync in case the source changed in the config. git(&["-C", path_str, "remote", "set-url", "origin", source])?; git(&["-C", path_str, "remote", "update", "--prune"])?; } else { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).with_context(|| { format!("failed to create cache directory: {}", parent.display()) })?; } git(&["clone", "--mirror", source, path_str(path)?])?; } Ok(Self { path }) } /// Mirrors branches and tags to the given remote URL, force-updating and /// pruning refs that no longer exist in the source. /// /// We intentionally push only `refs/heads/*` and `refs/tags/*` rather than /// using `git push --mirror`, because mirrors of forges like GitHub /// contain read-only hidden refs (e.g. `refs/pull/*`) that destinations /// will reject. pub fn push_mirror(&self, url: &str) -> Result<()> { git(&[ "-C", path_str(self.path)?, "push", "--prune", "--force", url, "+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*", ])?; Ok(()) } /// Returns the output of `git for-each-ref`, which uniquely identifies /// the current state of all refs. pub fn refs_snapshot(&self) -> Result { git(&["-C", path_str(self.path)?, "for-each-ref"]) } /// Creates a bundle containing all refs at the given path. pub fn create_bundle(&self, bundle_path: &Path) -> Result<()> { git(&[ "-C", path_str(self.path)?, "bundle", "create", path_str(bundle_path)?, "--all", ])?; Ok(()) } } fn path_str(path: &Path) -> Result<&str> { path.to_str() .with_context(|| format!("path is not valid UTF-8: {}", path.display())) }