use std::path::Path;
use anyhow::{anyhow, Context, Result};
pub fn head_sha(root: &Path) -> Result<String> {
let repo = gix::open(root).with_context(|| format!("gix::open {}", root.display()))?;
let id = repo
.head()
.context("read HEAD")?
.id()
.ok_or_else(|| anyhow!("HEAD has no commit (unborn?) in {}", root.display()))?;
Ok(id.to_string())
}
pub fn head_branch(root: &Path) -> Result<String> {
let repo = gix::open(root).with_context(|| format!("gix::open {}", root.display()))?;
Ok(match repo.head_name().context("read HEAD name")? {
Some(name) => name.shorten().to_string(),
None => "(detached)".to_string(),
})
}
pub fn head_sha_and_branch(root: &Path) -> Result<(String, String)> {
let repo = gix::open(root).with_context(|| format!("gix::open {}", root.display()))?;
let sha = repo
.head()
.context("read HEAD")?
.id()
.ok_or_else(|| anyhow!("HEAD has no commit (unborn?) in {}", root.display()))?
.to_string();
let branch = match repo.head_name().context("read HEAD name")? {
Some(name) => name.shorten().to_string(),
None => "(detached)".to_string(),
};
Ok((sha, branch))
}
pub fn tag_commit_sha(root: &Path, tag: &str) -> Result<Option<String>> {
let repo = gix::open(root).with_context(|| format!("gix::open {}", root.display()))?;
let full = format!("refs/tags/{tag}");
let Some(reference) = repo
.try_find_reference(full.as_str())
.with_context(|| format!("look up {full}"))?
else {
return Ok(None);
};
let id = reference
.into_fully_peeled_id()
.with_context(|| format!("peel {full}"))?;
Ok(Some(id.to_string()))
}
pub fn tag_points_at_head(root: &Path, tag: &str) -> Result<bool> {
match tag_commit_sha(root, tag)? {
Some(tag_sha) => Ok(tag_sha == head_sha(root)?),
None => Ok(false),
}
}
const FIXTURE_NAME: &str = "Nornir Fixture";
const FIXTURE_EMAIL: &str = "fixtures@nornir.invalid";
const FIXTURE_TIME: &str = "1700000000 +0000";
pub fn init(root: &Path) -> Result<()> {
std::fs::create_dir_all(root).with_context(|| format!("mkdir {}", root.display()))?;
gix::init(root).with_context(|| format!("gix init {}", root.display()))?;
Ok(())
}
pub fn commit_all(root: &Path, message: &str) -> Result<String> {
let repo = gix::open(root).with_context(|| format!("gix::open {}", root.display()))?;
let empty = gix::ObjectId::empty_tree(repo.object_hash());
let mut editor = repo.edit_tree(empty).context("seed empty tree editor")?;
add_dir_recursive(&repo, &mut editor, root, root)?;
let tree = editor.write().context("write fixture tree")?.detach();
let parents: Vec<gix::ObjectId> = repo.head_commit().ok().map(|c| c.id).into_iter().collect();
let sig = gix::actor::SignatureRef {
name: gix::bstr::BStr::new(FIXTURE_NAME),
email: gix::bstr::BStr::new(FIXTURE_EMAIL),
time: FIXTURE_TIME,
};
let id = repo
.commit_as(sig, sig, "HEAD", message, tree, parents)
.context("create fixture commit")?;
let mut index = repo
.index_from_tree(&tree)
.context("rebuild index from fixture tree")?;
index
.write(gix::index::write::Options::default())
.context("persist fixture index")?;
Ok(id.to_string())
}
pub fn tag_lightweight(root: &Path, name: &str, target_sha: &str) -> Result<()> {
let repo = gix::open(root).with_context(|| format!("gix::open {}", root.display()))?;
let target = gix::ObjectId::from_hex(target_sha.as_bytes())
.with_context(|| format!("parse target sha `{target_sha}`"))?;
repo.tag_reference(name, target, gix::refs::transaction::PreviousValue::MustNotExist)
.with_context(|| format!("create tag `{name}` -> {target_sha}"))?;
Ok(())
}
fn add_dir_recursive(
repo: &gix::Repository,
editor: &mut gix::object::tree::Editor<'_>,
repo_root: &Path,
dir: &Path,
) -> Result<()> {
use gix::object::tree::EntryKind;
let mut entries: Vec<_> = std::fs::read_dir(dir)
.with_context(|| format!("read_dir {}", dir.display()))?
.collect::<std::io::Result<Vec<_>>>()
.with_context(|| format!("iterate {}", dir.display()))?;
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let path = entry.path();
let name = entry.file_name();
if name == ".git" {
continue;
}
let meta = std::fs::symlink_metadata(&path)
.with_context(|| format!("stat {}", path.display()))?;
let ft = meta.file_type();
if ft.is_dir() {
add_dir_recursive(repo, editor, repo_root, &path)?;
continue;
}
let rela = path
.strip_prefix(repo_root)
.expect("path is under repo_root");
let rela = gix::path::into_bstr(rela).into_owned();
let (bytes, kind): (Vec<u8>, EntryKind) = if ft.is_symlink() {
let target = std::fs::read_link(&path)
.with_context(|| format!("readlink {}", path.display()))?;
(gix::path::into_bstr(target).into_owned().into(), EntryKind::Link)
} else {
let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
#[cfg(unix)]
let kind = {
use std::os::unix::fs::PermissionsExt;
if meta.permissions().mode() & 0o111 != 0 {
EntryKind::BlobExecutable
} else {
EntryKind::Blob
}
};
#[cfg(not(unix))]
let kind = EntryKind::Blob;
(bytes, kind)
};
let blob = repo.write_blob(&bytes).context("write blob")?;
editor
.upsert(rela.as_ref() as &gix::bstr::BStr, kind, blob.detach())
.with_context(|| format!("tree upsert {rela}"))?;
}
Ok(())
}