gitdataai/libs/git/reference/ops.rs
2026-04-15 09:08:09 +08:00

258 lines
8.1 KiB
Rust

//! 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<CommitOid>,
pub new_oid: Option<CommitOid>,
}
impl GitDomain {
/// List all references matching a pattern (e.g. "refs/heads/*").
pub fn ref_list(&self, pattern: Option<&str>) -> GitResult<Vec<RefInfo>> {
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<RefInfo> {
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<RefUpdateResult> {
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<CommitOid> {
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<RefInfo> {
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<CommitOid>,
message: Option<&str>,
) -> GitResult<RefUpdateResult> {
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<Option<CommitOid>> {
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
}
}