gitdataai/libs/git/branch/ops.rs
2026-04-14 19:02:01 +08:00

199 lines
6.1 KiB
Rust

//! Branch create/delete/rename operations.
use git2::BranchType;
use crate::branch::types::BranchInfo;
use crate::commit::types::CommitOid;
use crate::ref_utils::validate_ref_name;
use crate::{GitDomain, GitError, GitResult};
impl GitDomain {
pub fn branch_create(&self, name: &str, oid: &CommitOid, force: bool) -> GitResult<BranchInfo> {
validate_ref_name(name)?;
let target = oid
.to_oid()
.map_err(|_| GitError::InvalidOid(oid.to_string()))?;
let commit = self
.repo()
.find_commit(target)
.map_err(|e| GitError::Internal(e.to_string()))?;
let branch = self.repo.branch(name, &commit, force).map_err(|e| {
if e.code() == git2::ErrorCode::Exists && !force {
GitError::BranchExists(name.to_string())
} else {
GitError::Internal(e.to_string())
}
})?;
let full_name = branch.get().name().unwrap_or("").to_string();
Ok(BranchInfo {
name: full_name,
oid: CommitOid::from_git2(target),
is_head: false,
is_remote: false,
is_current: false,
upstream: None,
})
}
pub fn branch_create_from_head(&self, name: &str, force: bool) -> GitResult<BranchInfo> {
let head_oid = self
.repo()
.head()
.ok()
.and_then(|r| r.target())
.ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?;
self.branch_create(name, &CommitOid::from_git2(head_oid), force)
}
pub fn branch_delete(&self, name: &str) -> GitResult<()> {
let full_name = if name.starts_with("refs/heads/") {
name.to_string()
} else {
format!("refs/heads/{}", name)
};
let mut branch = self
.repo()
.find_branch(&full_name, BranchType::Local)
.map_err(|_e| GitError::RefNotFound(name.to_string()))?;
branch
.delete()
.map_err(|e| GitError::Internal(e.to_string()))
}
pub fn branch_delete_remote(&self, name: &str) -> GitResult<()> {
let full_name = format!("refs/remotes/{}", name);
let mut branch = self
.repo()
.find_branch(&full_name, BranchType::Remote)
.map_err(|_e| GitError::RefNotFound(name.to_string()))?;
branch
.delete()
.map_err(|e| GitError::Internal(e.to_string()))
}
pub fn branch_rename(&self, old_name: &str, new_name: &str) -> GitResult<BranchInfo> {
validate_ref_name(new_name)?;
let old_full = if old_name.starts_with("refs/heads/") {
old_name.to_string()
} else {
format!("refs/heads/{}", old_name)
};
let mut branch = self
.repo()
.find_branch(&old_full, BranchType::Local)
.map_err(|_e| GitError::RefNotFound(old_name.to_string()))?;
let target = branch
.get()
.target()
.ok_or_else(|| GitError::Internal("branch has no target".to_string()))?;
branch.rename(new_name, false).map_err(|e| {
if e.code() == git2::ErrorCode::Exists {
GitError::BranchExists(new_name.to_string())
} else {
GitError::Internal(e.to_string())
}
})?;
Ok(BranchInfo {
name: format!("refs/heads/{}", new_name),
oid: CommitOid::from_git2(target),
is_head: false,
is_remote: false,
is_current: false,
upstream: None,
})
}
pub fn branch_move(&self, name: &str, new_name: &str, force: bool) -> GitResult<BranchInfo> {
validate_ref_name(new_name)?;
let full_name = if name.starts_with("refs/heads/") {
name.to_string()
} else {
format!("refs/heads/{}", name)
};
let mut branch = self
.repo()
.find_branch(&full_name, BranchType::Local)
.map_err(|_e| GitError::RefNotFound(name.to_string()))?;
let target = branch
.get()
.target()
.ok_or_else(|| GitError::Internal("branch has no target".to_string()))?;
let commit = self
.repo()
.find_commit(target)
.map_err(|e| GitError::Internal(e.to_string()))?;
// Delete the old branch first. If deletion fails, we fail atomically.
branch
.delete()
.map_err(|e| GitError::Internal(e.to_string()))?;
// Create the new branch pointing to the same commit.
self.repo().branch(new_name, &commit, force).map_err(|e| {
if e.code() == git2::ErrorCode::Exists && !force {
GitError::BranchExists(new_name.to_string())
} else {
GitError::Internal(e.to_string())
}
})?;
Ok(BranchInfo {
name: format!("refs/heads/{}", new_name),
oid: CommitOid::from_git2(target),
is_head: false,
is_remote: false,
is_current: false,
upstream: None,
})
}
pub fn branch_set_upstream(&self, name: &str, upstream: Option<&str>) -> GitResult<()> {
let full_name = if name.starts_with("refs/heads/") {
name.to_string()
} else {
format!("refs/heads/{}", name)
};
let mut branch = self
.repo()
.find_branch(&full_name, BranchType::Local)
.map_err(|_e| GitError::RefNotFound(name.to_string()))?;
match upstream {
Some(u) => {
let upstream_name = if u.starts_with("refs/remotes/") || u.contains('/') {
u.to_string()
} else {
format!("refs/remotes/{}", u)
};
branch
.set_upstream(Some(&upstream_name))
.map_err(|e| GitError::Internal(e.to_string()))
}
None => branch
.set_upstream(None)
.map_err(|e| GitError::Internal(e.to_string())),
}
}
}