199 lines
6.1 KiB
Rust
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())),
|
|
}
|
|
}
|
|
}
|