use crate::Path;
use anyhow::Context;
use std::{fs, process::Command};
pub fn capture(mut cmd: Command) -> anyhow::Result<String> {
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let output = cmd
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_INDEX_FILE")
.env_remove("GIT_OBJECT_DIRECTORY")
.env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
.spawn()
.context("failed to spawn git")?
.wait_with_output()
.context("failed to wait on git output")?;
if output.status.success() {
String::from_utf8(output.stdout)
.or_else(|_err| Ok("git command succeeded but gave non-utf8 output".to_owned()))
} else {
let args: Vec<_> = cmd.get_args().collect();
match String::from_utf8(output.stderr) {
Ok(err) => {
anyhow::bail!("{args:?}\n{err}");
}
Err(_err) => {
anyhow::bail!("{args:?}\ngit command failed and gave non-utf8 output");
}
}
}
}
pub fn get_fetch_time(repo: &Path) -> anyhow::Result<jiff::Timestamp> {
let path = repo.join(".git");
let file_timestamp = |name: &str| -> anyhow::Result<jiff::Timestamp> {
let path = path.join(name);
let attr =
fs::metadata(path).with_context(|| format!("failed to get '{name}' metadata"))?;
attr.modified()
.with_context(|| format!("failed to get '{name}' modification time"))?
.try_into()
.with_context(|| format!("failed to convert file timestamp for '{name}'"))
};
let commit_timestamp = || -> anyhow::Result<jiff::Timestamp> {
let mut cmd = Command::new("git");
cmd.arg("-C")
.arg(repo)
.args(["show", "-s", "--format=%cI", "HEAD"]);
let ts = capture(cmd).context("failed to get HEAD timestamp")?;
ts.trim()
.parse()
.with_context(|| format!("failed to parse ISO-8601 timestamp '{}'", ts.trim()))
};
let timestamp = match file_timestamp("FETCH_HEAD") {
Ok(ts) => ts,
Err(fh_err) => {
match commit_timestamp() {
Ok(commit_ts) => {
let file_head_ts = file_timestamp("HEAD").unwrap_or_default();
std::cmp::max(commit_ts, file_head_ts)
}
Err(hc_err) => {
return Err(hc_err).context(fh_err);
}
}
}
};
Ok(timestamp)
}
pub enum FetchResult {
Fetched,
Cloned,
}
impl std::fmt::Display for FetchResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Fetched => f.write_str("fetched"),
Self::Cloned => f.write_str("cloned"),
}
}
}
pub fn fetch_repo(url: &str, repo_path: &Path, branch: &str) -> anyhow::Result<FetchResult> {
if let Some(parent) = repo_path.parent() {
if !parent.is_dir() {
fs::create_dir_all(parent).with_context(|| {
format!("failed to create advisory database directory {parent}")
})?;
}
} else {
anyhow::bail!("invalid directory: {repo_path}");
}
let run = |args: &[&str]| {
let mut cmd = Command::new("git");
cmd.arg("-C").arg(repo_path);
cmd.args(args);
capture(cmd)
};
if repo_path.exists() {
match run(&["reset", "--hard"]) {
Ok(_reset) => log::debug!("reset {url}"),
Err(err) => log::error!("failed to reset {url}: {err}"),
}
let rspec = format!("+{branch}:{branch}");
run(&["fetch", "--depth=1", "-u", "origin", &rspec])
.context("failed to fetch latest changes")?;
run(&["reset", "--hard", "FETCH_HEAD"]).context("failed to reset to FETCH_HEAD")?;
Ok(FetchResult::Fetched)
} else {
let mut cmd = Command::new("git");
cmd.args(["clone", "--depth=1", "--branch", branch])
.arg(url)
.arg(repo_path);
capture(cmd).context("failed to clone")?;
Ok(FetchResult::Cloned)
}
}