use std::path::Path;
use std::sync::LazyLock as Lazy;
use anyhow::Context as ResultExt;
use git2::{
BranchType, Cred, CredentialType, Error, FetchOptions, Oid, RemoteCallbacks, Repository,
ResetType, SubmoduleUpdateOptions,
};
use url::Url;
fn with_fetch_options<T, F>(f: F) -> anyhow::Result<T>
where
F: FnOnce(FetchOptions<'_>) -> anyhow::Result<T>,
{
let mut rcb = RemoteCallbacks::new();
rcb.credentials(|_, username, allowed| {
if allowed.contains(CredentialType::SSH_KEY) {
if let Some(username) = username {
return Cred::ssh_key_from_agent(username);
}
}
if allowed.contains(CredentialType::DEFAULT) {
return Cred::default();
}
Err(Error::from_str(
"remote authentication required but none available",
))
});
let mut proxy_opts = git2::ProxyOptions::new();
proxy_opts.auto();
let mut opts = FetchOptions::new();
opts.remote_callbacks(rcb);
opts.proxy_options(proxy_opts);
f(opts)
}
pub fn open(dir: &Path) -> anyhow::Result<Repository> {
let repo = Repository::open(dir)
.with_context(|| format!("failed to open repository at `{}`", dir.display()))?;
Ok(repo)
}
static DEFAULT_REFSPECS: Lazy<Vec<String>> = Lazy::new(|| {
vec_into![
"+refs/heads/*:refs/remotes/origin/*",
"+HEAD:refs/remotes/origin/HEAD"
]
});
pub fn clone(url: &Url, dir: &Path) -> anyhow::Result<Repository> {
with_fetch_options(|mut opts| {
let repo = Repository::init(dir)?;
repo.remote("origin", url.as_str())?
.fetch(&DEFAULT_REFSPECS, Some(&mut opts), None)?;
Ok(repo)
})
.with_context(|| format!("failed to git clone `{url}`"))
}
pub fn fetch(repo: &Repository) -> anyhow::Result<()> {
with_fetch_options(|mut opts| {
repo.find_remote("origin")
.context("failed to find remote `origin`")?
.fetch(&DEFAULT_REFSPECS, Some(&mut opts), None)?;
Ok(())
})
.context("failed to git fetch")
}
pub fn checkout(repo: &Repository, oid: Oid) -> anyhow::Result<()> {
let obj = repo
.find_object(oid, None)
.with_context(|| format!("failed to find `{oid}`"))?;
repo.reset(&obj, ResetType::Hard, None)
.with_context(|| format!("failed to set HEAD to `{oid}`"))?;
repo.checkout_tree(&obj, None)
.with_context(|| format!("failed to checkout `{oid}`"))
}
pub fn submodule_update(repo: &Repository) -> anyhow::Result<()> {
fn _submodule_update(
repo: &Repository,
todo: &mut Vec<Repository>,
opts: &mut SubmoduleUpdateOptions<'_>,
) -> anyhow::Result<()> {
for mut submodule in repo.submodules()? {
submodule.update(true, Some(opts))?;
todo.push(submodule.open()?);
}
Ok(())
}
with_fetch_options(|fetch_opts| {
let mut opts = SubmoduleUpdateOptions::new();
let opts = opts.fetch(fetch_opts);
let mut repos = Vec::new();
_submodule_update(repo, &mut repos, opts)?;
while let Some(repo) = repos.pop() {
_submodule_update(&repo, &mut repos, opts)?;
}
Ok(())
})
}
fn resolve_refname(repo: &Repository, refname: &str) -> Result<Oid, Error> {
let ref_id = repo.refname_to_id(refname)?;
let obj = repo.find_object(ref_id, None)?;
let obj = obj.peel(git2::ObjectType::Commit)?;
Ok(obj.id())
}
pub fn resolve_head(repo: &Repository) -> anyhow::Result<Oid> {
resolve_refname(repo, "refs/remotes/origin/HEAD").context("failed to find remote HEAD")
}
pub fn resolve_branch(repo: &Repository, branch: &str) -> anyhow::Result<Oid> {
repo.find_branch(&format!("origin/{branch}"), BranchType::Remote)
.with_context(|| format!("failed to find branch `{branch}`"))?
.get()
.target()
.with_context(|| format!("branch `{branch}` does not have a target"))
}
pub fn resolve_rev(repo: &Repository, rev: &str) -> anyhow::Result<Oid> {
let obj = repo
.revparse_single(rev)
.with_context(|| format!("failed to find revision `{rev}`"))?;
Ok(match obj.as_tag() {
Some(tag) => tag.target_id(),
None => obj.id(),
})
}
pub fn resolve_tag(repo: &Repository, tag: &str) -> anyhow::Result<Oid> {
fn _resolve_tag(repo: &Repository, tag: &str) -> Result<Oid, Error> {
let id = repo.refname_to_id(&format!("refs/tags/{tag}"))?;
let obj = repo.find_object(id, None)?;
let obj = obj.peel(git2::ObjectType::Commit)?;
Ok(obj.id())
}
_resolve_tag(repo, tag).with_context(|| format!("failed to find tag `{tag}`"))
}