//! 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, crate::Error> { let tip = self.tip_id()?; if let Some(hit) = self.cache_get_rev::>("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, crate::Error> { let tip = self.tip_id()?; if let Some(hit) = self.cache_get_rev::>("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 { let tip = self.tip_id()?; if let Some(hit) = self.cache_get_rev::("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 { let tip = self.tip_id()?; if let Some(hit) = self.cache_get_rev::("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, ) -> Result { 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 { Ok(self.policy.is_branch_protected(branch)) } fn refs_head(&self) -> Result { let tip = self.tip_id()?; if let Some(hit) = self.cache_get_rev::("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, crate::Error> { let tip = self.tip_id()?; let key = format!("{skip}.{limit}"); if let Some(hit) = self.cache_get_rev::>("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 { let head_tip = self.tip_id()?; let key = format!("{refname}.{object}"); if let Some(hit) = self.cache_get_rev::("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) } }