//! Reference operations. use crate::commit::types::CommitOid; use crate::reference::types::RefInfo; use crate::{GitDomain, GitError, GitResult}; /// Specifies what the reference update should be based on. pub enum RefUpdateTarget { Oid(CommitOid), } /// Result of a reference update operation. pub struct RefUpdateResult { pub name: String, pub old_oid: Option, pub new_oid: Option, } impl GitDomain { /// List all references matching a pattern (e.g. "refs/heads/*"). pub fn ref_list(&self, pattern: Option<&str>) -> GitResult> { let mut refs = Vec::new(); let iter = self .repo() .references() .map_err(|e| GitError::Internal(e.to_string()))?; for result in iter { let r = result.map_err(|e| GitError::Internal(e.to_string()))?; let name = match r.name() { Some(n) => n.to_string(), None => continue, }; if let Some(pat) = pattern { if !name_match(&name, pat) { continue; } } let target = r.target().map(CommitOid::from_git2); let oid = r .peel_to_commit() .ok() .map(|c| CommitOid::from_git2(c.id())); let is_symbolic = r.kind() == Some(git2::ReferenceType::Symbolic); let is_branch = name.starts_with("refs/heads/"); let is_remote = name.starts_with("refs/remotes/"); let is_tag = name.starts_with("refs/tags/"); let is_note = name.starts_with("refs/notes/"); refs.push(RefInfo { name, oid, target, is_symbolic, is_branch, is_remote, is_tag, is_note, }); } Ok(refs) } pub fn ref_get(&self, name: &str) -> GitResult { let r = self .repo() .find_reference(name) .map_err(|_e| GitError::RefNotFound(name.to_string()))?; let target = r.target().map(CommitOid::from_git2); let oid = r .peel_to_commit() .ok() .map(|c| CommitOid::from_git2(c.id())); Ok(RefInfo { name: name.to_string(), oid, target, is_symbolic: r.kind() == Some(git2::ReferenceType::Symbolic), is_branch: name.starts_with("refs/heads/"), is_remote: name.starts_with("refs/remotes/"), is_tag: name.starts_with("refs/tags/"), is_note: name.starts_with("refs/notes/"), }) } pub fn ref_create( &self, name: &str, oid: CommitOid, force: bool, message: Option<&str>, ) -> GitResult { let git_oid = oid .to_oid() .map_err(|_| GitError::InvalidOid(oid.to_string()))?; let old = self.repo().find_reference(name).ok(); let old_oid = old .as_ref() .and_then(|r| r.target().map(CommitOid::from_git2)); self.repo() .reference(name, git_oid, force, message.unwrap_or("create ref")) .map_err(|e| { if !force && e.code() == git2::ErrorCode::Exists { GitError::BranchExists(name.to_string()) } else { GitError::Internal(e.to_string()) } })?; Ok(RefUpdateResult { name: name.to_string(), old_oid, new_oid: Some(oid), }) } pub fn ref_delete(&self, name: &str) -> GitResult { let mut r = self .repo() .find_reference(name) .map_err(|_e| GitError::RefNotFound(name.to_string()))?; let target = r .target() .map(CommitOid::from_git2) .ok_or_else(|| GitError::Internal("ref has no target".to_string()))?; r.delete().map_err(|e| GitError::Internal(e.to_string()))?; Ok(target) } /// Rename a reference. Fails if new name already exists unless `force` is true. pub fn ref_rename(&self, old_name: &str, new_name: &str, force: bool) -> GitResult { let mut r = self .repo() .find_reference(old_name) .map_err(|_e| GitError::RefNotFound(old_name.to_string()))?; let target = r.target().map(CommitOid::from_git2); let oid = r .peel_to_commit() .ok() .map(|c| CommitOid::from_git2(c.id())); let ref_kind = r.kind(); // Capture kind before rename if !force && self.repo().find_reference(new_name).is_ok() { return Err(GitError::BranchExists(new_name.to_string())); } r.rename(new_name, force, "rename ref") .map_err(|e| GitError::Internal(e.to_string()))?; Ok(RefInfo { name: new_name.to_string(), oid, target, is_symbolic: ref_kind == Some(git2::ReferenceType::Symbolic), is_branch: new_name.starts_with("refs/heads/"), is_remote: new_name.starts_with("refs/remotes/"), is_tag: new_name.starts_with("refs/tags/"), is_note: new_name.starts_with("refs/notes/"), }) } pub fn ref_update( &self, name: &str, new_oid: CommitOid, expected_oid: Option, message: Option<&str>, ) -> GitResult { let old = self .repo() .find_reference(name) .map_err(|_e| GitError::RefNotFound(name.to_string()))?; let old_oid = old.target().map(CommitOid::from_git2); // CAS check if let Some(expected) = expected_oid { let git_expected = expected .to_oid() .map_err(|_| GitError::InvalidOid(expected.to_string()))?; if old.target() != Some(git_expected) { return Err(GitError::Internal( "ref update failed: unexpected current value (CAS mismatch)".to_string(), )); } } let git_new_oid = new_oid .to_oid() .map_err(|_| GitError::InvalidOid(new_oid.to_string()))?; // Use reference_matching for CAS. Pass None as previous target only when // the ref has no target (symbolic ref with broken target) — fall back to // unconditional reference update in that case. match old.target() { Some(prev) => { self.repo() .reference_matching( name, git_new_oid, true, prev, message.unwrap_or("update ref"), ) .map_err(|e| GitError::Internal(e.to_string()))?; } None => { self.repo() .reference(name, git_new_oid, true, message.unwrap_or("update ref")) .map_err(|e| GitError::Internal(e.to_string()))?; } } Ok(RefUpdateResult { name: name.to_string(), old_oid, new_oid: Some(new_oid), }) } pub fn ref_exists(&self, name: &str) -> bool { self.repo().find_reference(name).is_ok() } /// Get the peeled (commit) OID of a reference. pub fn ref_target(&self, name: &str) -> GitResult> { let r = self .repo() .find_reference(name) .map_err(|_e| GitError::RefNotFound(name.to_string()))?; Ok(r.peel_to_commit() .ok() .map(|c| CommitOid::from_git2(c.id()))) } } fn name_match(name: &str, pattern: &str) -> bool { if let Some(stripped) = pattern.strip_suffix("/**") { name.starts_with(stripped) } else if let Some(stripped) = pattern.strip_suffix("/*") { name.starts_with(stripped) && !name[stripped.len()..].contains('/') } else { name == pattern } }