use std::io;
pub trait GithubFacts {
fn resolve_ref(&self, owner_repo: &str, git_ref: &str) -> io::Result<Option<String>>;
fn commit_reachable(&self, _owner_repo: &str, _sha: &str) -> io::Result<Option<bool>> {
Ok(None)
}
fn ref_timestamp(&self, _owner_repo: &str, _git_ref: &str) -> io::Result<Option<i64>> {
Ok(None)
}
fn ref_count(&self, _owner_repo: &str) -> io::Result<Option<usize>> {
Ok(None)
}
}
pub struct GitRemote;
impl GithubFacts for GitRemote {
fn resolve_ref(&self, owner_repo: &str, git_ref: &str) -> io::Result<Option<String>> {
let url = format!("https://github.com/{owner_repo}.git");
let peeled = format!("{git_ref}^{{}}");
let out = std::process::Command::new("git")
.args(["ls-remote", &url, git_ref, &peeled])
.output()?;
if !out.status.success() {
return Err(io::Error::other(format!(
"git ls-remote 실패 ({owner_repo}): {}",
String::from_utf8_lossy(&out.stderr).trim()
)));
}
Ok(pick_best(&String::from_utf8_lossy(&out.stdout)))
}
fn commit_reachable(&self, owner_repo: &str, sha: &str) -> io::Result<Option<bool>> {
Self::commit_reachable_impl(owner_repo, sha)
}
fn ref_timestamp(&self, owner_repo: &str, git_ref: &str) -> io::Result<Option<i64>> {
Self::ref_timestamp_impl(owner_repo, git_ref)
}
fn ref_count(&self, owner_repo: &str) -> io::Result<Option<usize>> {
let url = format!("https://github.com/{owner_repo}.git");
let out = std::process::Command::new("git")
.args(["ls-remote", "--tags", &url])
.output()?;
if !out.status.success() {
return Ok(None);
}
Ok(Some(
String::from_utf8_lossy(&out.stdout)
.lines()
.filter(|l| !l.trim().is_empty())
.count(),
))
}
}
impl GitRemote {
fn shallow_fetch(
owner_repo: &str,
want: &str,
) -> io::Result<(bool, String, std::path::PathBuf)> {
let url = format!("https://github.com/{owner_repo}.git");
let key = want
.bytes()
.fold(0u64, |h, b| h.wrapping_mul(31).wrapping_add(b as u64));
let tmp =
std::env::temp_dir().join(format!("just-shield-fetch-{}-{key:x}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp)?;
let init = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(&tmp)
.output()?;
if !init.status.success() {
return Err(io::Error::other("git init 실패"));
}
let fetch = std::process::Command::new("git")
.args(["fetch", "--quiet", "--depth=1", &url, want])
.current_dir(&tmp)
.output()?;
let stderr = String::from_utf8_lossy(&fetch.stderr).to_lowercase();
Ok((fetch.status.success(), stderr, tmp))
}
fn commit_reachable_impl(owner_repo: &str, sha: &str) -> io::Result<Option<bool>> {
let (ok, stderr, tmp) = Self::shallow_fetch(owner_repo, sha)?;
let _ = std::fs::remove_dir_all(&tmp);
if ok {
return Ok(Some(true));
}
if stderr.contains("not our ref") || stderr.contains("unadvertised object") {
return Ok(Some(false));
}
Err(io::Error::other(format!(
"git fetch 실패 ({owner_repo}@{sha}): {}",
stderr.trim()
)))
}
fn ref_timestamp_impl(owner_repo: &str, git_ref: &str) -> io::Result<Option<i64>> {
let (ok, stderr, tmp) = Self::shallow_fetch(owner_repo, git_ref)?;
if !ok {
let _ = std::fs::remove_dir_all(&tmp);
if stderr.contains("couldn't find remote ref") {
return Ok(None);
}
return Err(io::Error::other(format!(
"git fetch 실패 ({owner_repo}@{git_ref}): {}",
stderr.trim()
)));
}
let log = std::process::Command::new("git")
.args(["log", "-1", "--format=%ct", "FETCH_HEAD"])
.current_dir(&tmp)
.output()?;
let _ = std::fs::remove_dir_all(&tmp);
if !log.status.success() {
return Ok(None);
}
Ok(String::from_utf8_lossy(&log.stdout).trim().parse().ok())
}
}
fn pick_best(output: &str) -> Option<String> {
let mut tag = None;
let mut other = None;
for line in output.lines() {
let Some((sha, name)) = line.split_once('\t') else {
continue;
};
if name.ends_with("^{}") {
return Some(sha.to_string());
}
if name.starts_with("refs/tags/") {
tag.get_or_insert_with(|| sha.to_string());
} else {
other.get_or_insert_with(|| sha.to_string());
}
}
tag.or(other)
}
#[cfg(test)]
mod tests {
use super::pick_best;
#[test]
fn prefers_peeled_then_tag_then_branch() {
let out = "aaa\trefs/heads/v4\nbbb\trefs/tags/v4\nccc\trefs/tags/v4^{}\n";
assert_eq!(pick_best(out).as_deref(), Some("ccc"));
let out = "aaa\trefs/heads/v4\nbbb\trefs/tags/v4\n";
assert_eq!(pick_best(out).as_deref(), Some("bbb"));
assert_eq!(pick_best(""), None);
}
}