7edcc57
//! refs usecase: implements RefsOps for GitRepository on top of gix.
use gix::hash::ObjectId;
use gix::refs::transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog};
use gix::refs::{Category, FullName, Target};
use crate::GitRepository;
use crate::object::types::ObjectType;
use crate::refs::impls::RefsOps;
use crate::refs::types::{BranchInfo, HeadInfo, RefInfo, RefType, UpdateRefResult};
fn classify_name(name: &str) -> RefType {
let full = match FullName::try_from(name) {
Ok(n) => n,
Err(_) => return RefType::Other,
};
match full.category() {
Some(Category::LocalBranch) => RefType::Branch,
Some(Category::Tag) => RefType::Tag,
Some(Category::RemoteBranch) => RefType::Remote,
_ => RefType::Other,
}
}
fn ref_to_info(name: &str, object: ObjectId) -> RefInfo {
RefInfo {
name: name.to_string(),
object,
r#type: classify_name(name),
}
}
/// Extract the short branch name from a `refs/heads/{short}` name, if any.
fn branch_short(name: &str) -> Option<&str> {
name.strip_prefix("refs/heads/")
}
/// Return an error if the given ref is a protected branch.
fn guard_protected_ref(
policy: &dyn crate::policy::PolicyProvider,
name: &str,
) -> Result<(), crate::Error> {
if let Some(short) = branch_short(name)
&& policy.is_branch_protected(short)
{
return Err(crate::Error::Other(format!("branch is protected: {short}")));
}
Ok(())
}
impl RefsOps for GitRepository {
fn refs_list(&self) -> Result<Vec<RefInfo>, crate::Error> {
let tip = self.tip_id()?;
if let Some(hit) = self.cache_get_rev::<Vec<RefInfo>>("refs", "list", "all", tip) {
return Ok(hit);
}
let repo = self.repo()?;
let platform = repo
.references()
.map_err(|e| crate::Error::Ref(e.to_string()))?;
let iter = platform
.all()
.map_err(|e| crate::Error::Ref(e.to_string()))?;
let mut out = Vec::new();
for r in iter {
let r = r.map_err(|e| crate::Error::Ref(e.to_string()))?;
let oid = r.target().try_id().map(|o| o.to_owned()).ok_or_else(|| {
crate::Error::Ref(format!("non-object ref {}", r.name().as_bstr()))
})?;
out.push(ref_to_info(&r.name().as_bstr().to_string(), oid));
}
self.cache_put_rev("refs", "list", "all", tip, &out);
Ok(out)
}
fn refs_list_filtered(&self, pattern: &str) -> Result<Vec<RefInfo>, crate::Error> {
let tip = self.tip_id()?;
if let Some(hit) = self.cache_get_rev::<Vec<RefInfo>>("refs", "list_filtered", pattern, tip)
{
return Ok(hit);
}
let repo = self.repo()?;
let platform = repo
.references()
.map_err(|e| crate::Error::Ref(e.to_string()))?;
let iter = platform
.prefixed(pattern)
.map_err(|e| crate::Error::Ref(e.to_string()))?;
let mut out = Vec::new();
for r in iter {
let r = r.map_err(|e| crate::Error::Ref(e.to_string()))?;
let oid = r.target().try_id().map(|o| o.to_owned()).ok_or_else(|| {
crate::Error::Ref(format!("non-object ref {}", r.name().as_bstr()))
})?;
out.push(ref_to_info(&r.name().as_bstr().to_string(), oid));
}
self.cache_put_rev("refs", "list_filtered", pattern, tip, &out);
Ok(out)
}
fn refs_get(&self, name: &str) -> Result<RefInfo, crate::Error> {
let tip = self.tip_id()?;
if let Some(hit) = self.cache_get_rev::<RefInfo>("refs", "get", name, tip) {
return Ok(hit);
}
let repo = self.repo()?;
let r = repo
.find_reference(name)
.map_err(|_| crate::Error::NotFound {
name: name.to_string(),
})?;
let oid = r
.target()
.try_id()
.map(|o| o.to_owned())
.ok_or_else(|| crate::Error::Ref(format!("non-object ref {name}")))?;
let v = ref_to_info(&r.name().as_bstr().to_string(), oid);
self.cache_put_rev("refs", "get", name, tip, &v);
Ok(v)
}
fn refs_resolve(&self, name: &str) -> Result<ObjectId, crate::Error> {
let tip = self.tip_id()?;
if let Some(hit) = self.cache_get_rev::<ObjectId>("refs", "resolve", name, tip) {
return Ok(hit);
}
let repo = self.repo()?;
let r = repo
.find_reference(name)
.map_err(|_| crate::Error::NotFound {
name: name.to_string(),
})?;
let oid = r
.target()
.try_id()
.map(|o| o.to_owned())
.ok_or_else(|| crate::Error::Ref(format!("non-object ref {name}")))?;
self.cache_put_rev("refs", "resolve", name, tip, &oid);
Ok(oid)
}
fn refs_update(
&self,
name: &str,
new: ObjectId,
expected: Option<ObjectId>,
) -> Result<UpdateRefResult, crate::Error> {
guard_protected_ref(self.policy.as_ref(), name)?;
let repo = self.repo()?;
let old_oid = repo
.find_reference(name)
.ok()
.and_then(|r| r.target().try_id().map(|o| o.to_owned()));
let previous = match expected.or(old_oid) {
Some(e) => PreviousValue::MustExistAndMatch(Target::Object(e)),
None => PreviousValue::Any,
};
let edit = RefEdit {
change: Change::Update {
log: LogChange::default(),
expected: previous,
new: Target::Object(new),
},
name: FullName::try_from(name)
.map_err(|e| crate::Error::InvalidRefName(e.to_string()))?,
deref: false,
};
repo.edit_reference(edit)
.map_err(|e| crate::Error::Ref(e.to_string()))?;
tracing::info!(ref = name, new = ?new, "ref updated");
Ok(UpdateRefResult {
name: name.to_string(),
old_oid,
new_oid: new,
})
}
fn refs_delete(&self, name: &str) -> Result<(), crate::Error> {
guard_protected_ref(self.policy.as_ref(), name)?;
let repo = self.repo()?;
let edit = RefEdit {
change: Change::Delete {
expected: PreviousValue::MustExist,
log: RefLog::AndReference,
},
name: FullName::try_from(name)
.map_err(|e| crate::Error::InvalidRefName(e.to_string()))?,
deref: false,
};
repo.edit_reference(edit)
.map_err(|e| crate::Error::Ref(e.to_string()))?;
tracing::info!(ref = name, "ref deleted");
Ok(())
}
fn refs_validate_name(&self, name: &str) -> Result<(), crate::Error> {
FullName::try_from(name)
.map(|_| ())
.map_err(|e| crate::Error::InvalidRefName(e.to_string()))
}
fn refs_is_branch_protected(&self, branch: &str) -> Result<bool, crate::Error> {
Ok(self.policy.is_branch_protected(branch))
}
fn refs_head(&self) -> Result<HeadInfo, crate::Error> {
let tip = self.tip_id()?;
if let Some(hit) = self.cache_get_rev::<HeadInfo>("refs", "head", "head", tip) {
return Ok(hit);
}
let repo = self.repo()?;
let head = repo.head().map_err(|e| crate::Error::Ref(e.to_string()))?;
let is_detached = head.is_detached();
let name = head
.referent_name()
.map(|n| n.as_bstr().to_string())
.unwrap_or_else(|| "HEAD".to_string());
let object = head
.id()
.map(|i| i.detach())
.ok_or_else(|| crate::Error::Ref("unborn HEAD".to_string()))?;
let obj = repo
.find_object(object)
.map_err(|e| crate::Error::Find(e.to_string()))?;
let v = HeadInfo {
name,
object,
object_type: ObjectType::from(obj.kind),
is_detached,
};
self.cache_put_rev("refs", "head", "head", tip, &v);
Ok(v)
}
fn refs_branches(&self, skip: u64, limit: u64) -> Result<Vec<BranchInfo>, crate::Error> {
let tip = self.tip_id()?;
let key = format!("{skip}.{limit}");
if let Some(hit) = self.cache_get_rev::<Vec<BranchInfo>>("refs", "branches", &key, tip) {
return Ok(hit);
}
let repo = self.repo()?;
let platform = repo
.references()
.map_err(|e| crate::Error::Ref(e.to_string()))?;
let iter = platform
.prefixed("refs/heads/")
.map_err(|e| crate::Error::Ref(e.to_string()))?;
let mut out = Vec::new();
let mut skipped = 0u64;
for r in iter {
let r = r.map_err(|e| crate::Error::Ref(e.to_string()))?;
let oid = r.target().try_id().map(|o| o.to_owned()).ok_or_else(|| {
crate::Error::Ref(format!("non-object ref {}", r.name().as_bstr()))
})?;
if skipped < skip {
skipped += 1;
continue;
}
if out.len() as u64 >= limit {
break;
}
let short = r.name().as_bstr().to_string();
let short = short
.strip_prefix("refs/heads/")
.unwrap_or(&short)
.to_string();
let message = repo
.find_commit(oid)
.ok()
.and_then(|c| c.decode().ok().and_then(|d| d.into_owned().ok()))
.map(|c| c.message.to_string());
out.push(BranchInfo {
name: short.clone(),
commit_sha: oid,
commit_message: message,
is_protected: self.policy.is_branch_protected(&short),
});
}
self.cache_put_rev("refs", "branches", &key, tip, &out);
Ok(out)
}
fn refs_is_object_in_ref(
&self,
object: &ObjectId,
refname: &str,
) -> Result<bool, crate::Error> {
let head_tip = self.tip_id()?;
let key = format!("{refname}.{object}");
if let Some(hit) = self.cache_get_rev::<bool>("refs", "in_ref", &key, head_tip) {
return Ok(hit);
}
let repo = self.repo()?;
let ref_tip = repo
.find_reference(refname)
.map_err(|_| crate::Error::NotFound {
name: refname.to_string(),
})?
.target()
.try_id()
.map(|o| o.to_owned())
.ok_or_else(|| crate::Error::Ref(format!("non-object ref {refname}")))?;
let walk = repo
.rev_walk([ref_tip])
.all()
.map_err(|e| crate::Error::RevParse(e.to_string()))?;
let mut found = false;
for info in walk {
let info = info.map_err(|e| crate::Error::Find(e.to_string()))?;
if info.id().detach() == *object {
found = true;
break;
}
}
self.cache_put_rev("refs", "in_ref", &key, head_tip, &found);
Ok(found)
}
fn refs_classify(&self, name: &str) -> RefType {
classify_name(name)
}
}