119 lines
3.4 KiB
Rust
119 lines
3.4 KiB
Rust
//! Blocking local git operations against GRASP servers.
|
|
//!
|
|
//! All functions may block; call them inside `cx.background_spawn`.
|
|
|
|
use std::io::Write;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Stdio};
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use git2::Repository;
|
|
use signed_core::RepoAddr;
|
|
|
|
/// On-disk cache of cloned repositories, keyed by owner pubkey / repo id.
|
|
#[derive(Debug, Clone)]
|
|
pub struct GitCache {
|
|
root: PathBuf,
|
|
}
|
|
|
|
impl GitCache {
|
|
pub fn new(root: PathBuf) -> Self {
|
|
Self { root }
|
|
}
|
|
|
|
/// Local path of the clone for a repository.
|
|
pub fn repo_path(&self, addr: &RepoAddr) -> PathBuf {
|
|
self.root
|
|
.join(addr.owner.to_hex())
|
|
.join(sanitize_path_component(&addr.id))
|
|
}
|
|
|
|
/// Open an existing clone.
|
|
pub fn open(&self, addr: &RepoAddr) -> Result<Option<Repository>> {
|
|
let path = self.repo_path(addr);
|
|
match Repository::open(&path) {
|
|
Ok(repo) => Ok(Some(repo)),
|
|
Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(None),
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|
|
|
|
/// Open the local clone if it exists (fetching first), otherwise clone
|
|
/// from the first working URL in `clone_urls` (the announcement's `clone` tag).
|
|
pub fn ensure_clone(&self, addr: &RepoAddr, clone_urls: &[String]) -> Result<Repository> {
|
|
let path = self.repo_path(addr);
|
|
|
|
if let Some(repo) = self.open(addr)? {
|
|
fetch_all(&repo).ok();
|
|
return Ok(repo);
|
|
}
|
|
|
|
if let Some(parent) = path.parent() {
|
|
std::fs::create_dir_all(parent)
|
|
.with_context(|| format!("failed to create {}", parent.display()))?;
|
|
}
|
|
|
|
let mut last_err: Option<git2::Error> = None;
|
|
for url in clone_urls {
|
|
match Repository::clone(url, &path) {
|
|
Ok(repo) => return Ok(repo),
|
|
Err(e) => last_err = Some(e),
|
|
}
|
|
}
|
|
|
|
match last_err {
|
|
Some(e) => Err(e).context("failed to clone from any mirror"),
|
|
None => bail!("no clone URLs provided"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Fetch all heads from `origin`.
|
|
pub fn fetch_all(repo: &Repository) -> Result<()> {
|
|
let mut remote = repo.find_remote("origin")?;
|
|
remote.fetch(
|
|
&["+refs/heads/*:refs/remotes/origin/*", "+refs/tags/*:refs/tags/*"],
|
|
None,
|
|
None,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Apply a `git format-patch` patch (or series) with `git am`.
|
|
///
|
|
/// Uses the git CLI because it handles the mbox format natively; can be
|
|
/// replaced with a pure-Rust implementation later without changing callers.
|
|
pub fn apply_patch(repo_path: &Path, patch: &str) -> Result<()> {
|
|
let mut child = Command::new("git")
|
|
.arg("am")
|
|
.current_dir(repo_path)
|
|
.stdin(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.context("failed to spawn `git am`")?;
|
|
|
|
child
|
|
.stdin
|
|
.as_mut()
|
|
.expect("stdin piped")
|
|
.write_all(patch.as_bytes())?;
|
|
|
|
let output = child.wait_with_output()?;
|
|
if !output.status.success() {
|
|
bail!("git am failed: {}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn sanitize_path_component(id: &str) -> String {
|
|
id.chars()
|
|
.map(|c| {
|
|
if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.') {
|
|
c
|
|
} else {
|
|
'_'
|
|
}
|
|
})
|
|
.collect()
|
|
}
|