//! 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 { 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 { 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 { 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 { 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())), } } }