use db::conn::sea_orm::*; use db::git::git_branches as branch_entity; use time::OffsetDateTime; use uuid::Uuid; use crate::error::{RepoError, RepoResult}; use crate::features::RepoService; use crate::path::validate_branch_name; use super::types::{ BranchPage, BranchView, CreateBranchOption, ListBranchQuery, MergeUpstreamRequest, MergeUpstreamResponse, RenameBranchOption, UpdateBranchOption, }; fn require_authenticated(actor: Uuid) -> RepoResult<()> { if actor.is_nil() { return Err(RepoError::Unauthorized("authentication required".into())); } Ok(()) } async fn load_branch( svc: &RepoService, repo_id: Uuid, name: &str, ) -> RepoResult { let db = svc.db.reader(); let branch = branch_entity::Entity::find() .filter(branch_entity::Column::RepoId.eq(repo_id)) .filter(branch_entity::Column::Name.eq(name)) .one(&db) .await? .ok_or_else(|| RepoError::NotFound(format!("branch {name}")))?; Ok(branch) } pub async fn list_branches( svc: &RepoService, actor: Uuid, repo_id: Uuid, q: ListBranchQuery, ) -> RepoResult { let _ = actor; let limit = q.limit.unwrap_or(svc.config.default_pagination); let offset = q.page.unwrap_or(0).saturating_mul(limit); let limit = std::cmp::min(limit, svc.config.max_pagination); let db = svc.db.reader(); let mut query = branch_entity::Entity::find() .filter(branch_entity::Column::RepoId.eq(repo_id)) .filter(branch_entity::Column::IsDeleted.eq(false)); if let Some(kw) = q.keyword.as_deref() { if !kw.is_empty() { query = query.filter(branch_entity::Column::Name.contains(kw)); } } let total = query.clone().count(&db).await?; let items = query .order_by_asc(branch_entity::Column::Name) .limit(Some(limit)) .offset(Some(offset)) .all(&db) .await? .into_iter() .map(BranchView::from) .collect(); Ok(BranchPage { items, total: total as i64, }) } pub async fn get_branch( svc: &RepoService, actor: Uuid, repo_id: Uuid, name: &str, ) -> RepoResult { let _ = actor; let branch = load_branch(svc, repo_id, name).await?; Ok(BranchView::from(branch)) } pub async fn create_branch( svc: &RepoService, actor: Uuid, repo_id: Uuid, input: CreateBranchOption, ) -> RepoResult { require_authenticated(actor)?; let new_name = validate_branch_name(&input.new_branch_name, svc.config.max_branch_name_len)?; let source_name = input .old_ref_name .as_deref() .or(input.old_branch_name.as_deref()) .unwrap_or("HEAD"); let source = load_branch(svc, repo_id, source_name).await?; let db = svc.db.writer(); let now = OffsetDateTime::now_utc(); let active: branch_entity::ActiveModel = branch_entity::ActiveModel { id: Set(Uuid::new_v4()), repo_id: Set(repo_id), name: Set(new_name), commit_sha: Set(source.commit_sha), is_deleted: Set(false), is_protected: Set(false), deleted_at: Set(None), created_at: Set(now), updated_at: Set(now), }; let saved = active.insert(&db).await?; Ok(BranchView::from(saved)) } pub async fn update_branch( svc: &RepoService, actor: Uuid, repo_id: Uuid, name: &str, input: UpdateBranchOption, ) -> RepoResult { require_authenticated(actor)?; let branch = load_branch(svc, repo_id, name).await?; if let Some(ref old) = input.old_commit_id { if !old.is_empty() && branch.commit_sha != *old { return Err(RepoError::Conflict( "branch tip has moved; refresh and retry".into(), )); } } let db = svc.db.writer(); let mut active: branch_entity::ActiveModel = branch.into(); active.commit_sha = Set(input.new_commit_id); active.updated_at = Set(OffsetDateTime::now_utc()); let saved = active.update(&db).await?; Ok(BranchView::from(saved)) } pub async fn delete_branch( svc: &RepoService, actor: Uuid, repo_id: Uuid, name: &str, ) -> RepoResult<()> { require_authenticated(actor)?; let branch = load_branch(svc, repo_id, name).await?; if branch.is_protected { return Err(RepoError::Forbidden(format!("branch {name} is protected"))); } let db = svc.db.writer(); let now = OffsetDateTime::now_utc(); let mut active: branch_entity::ActiveModel = branch.into(); active.is_deleted = Set(true); active.deleted_at = Set(Some(now)); active.updated_at = Set(now); let _ = active.update(&db).await?; Ok(()) } pub async fn rename_branch( svc: &RepoService, actor: Uuid, repo_id: Uuid, old_name: &str, input: RenameBranchOption, ) -> RepoResult { require_authenticated(actor)?; let new_name = validate_branch_name(&input.new_name, svc.config.max_branch_name_len)?; let branch = load_branch(svc, repo_id, old_name).await?; if branch.is_protected { return Err(RepoError::Forbidden(format!( "branch {old_name} is protected" ))); } let db = svc.db.writer(); let mut active: branch_entity::ActiveModel = branch.into(); active.name = Set(new_name); active.updated_at = Set(OffsetDateTime::now_utc()); let saved = active.update(&db).await?; Ok(BranchView::from(saved)) } pub async fn merge_upstream( _svc: &RepoService, actor: Uuid, repo_id: Uuid, input: MergeUpstreamRequest, ) -> RepoResult { require_authenticated(actor)?; let _ = repo_id; let _ = input.branch; let merge_type = if input.ff_only.unwrap_or(false) { "fast-forward".to_string() } else { "merge".to_string() }; let _ = actor; Ok(MergeUpstreamResponse { merge_type }) }