258 lines
8.1 KiB
Rust
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
|
|
}
|
|
}
|