use std::collections::HashMap; use crate::rpcgit::branch::ListBranchesOptions; use models::git::git_branches; use models::repo::repos; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, Unchanged}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use crate::context::facade::GitContext; use crate::service::GitService; use crate::share::errors::{GitError, map_baby}; use crate::share::policy::load_branch_policy; use crate::share::validate; #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct BranchDto { pub name: String, pub commit_sha: String, pub is_protected: bool, pub is_deleted: bool, pub deleted_at: Option, pub created_at: OffsetDateTime, pub updated_at: OffsetDateTime, } impl BranchDto { fn from_row(row: &git_branches::Model, sha: String) -> Self { Self { name: row.name.clone(), commit_sha: sha, is_protected: row.is_protected, is_deleted: row.is_deleted, deleted_at: row.deleted_at, created_at: row.created_at, updated_at: row.updated_at, } } } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct CreateBranchCmd { pub name: String, #[serde(default)] pub start_point: Option, } impl GitService { async fn branch_rows( &self, ctx: &GitContext, ) -> Result, GitError> { let rows = git_branches::Entity::find() .filter(git_branches::Column::RepoId.eq(ctx.model.id)) .all(self.db_read()) .await?; Ok(rows.into_iter().map(|r| (r.name.clone(), r)).collect()) } pub async fn list_branches(&self, ctx: &GitContext) -> Result, GitError> { if ctx.repo_uninitialized() { return Ok(Vec::new()); } let baby = self.baby(&ctx); let branches = baby .list_branches(ListBranchesOptions::default()) .await .map_err(map_baby)?; let mut rows = self.branch_rows(&ctx).await?; let policy = load_branch_policy(self.database.as_ref(), ctx.model.id).await?; for b in &branches { if !rows.contains_key(&b.name) && !b.name.is_empty() { let now = OffsetDateTime::now_utc(); let row = git_branches::ActiveModel { id: Set(uuid::Uuid::now_v7()), repo_id: Set(ctx.model.id), name: Set(b.name.clone()), commit_sha: Set(b.sha.to_string()), is_deleted: Set(false), is_protected: Set(policy.is_match(&b.name)), deleted_at: Set(None), created_at: Set(now), updated_at: Set(now), }; let inserted = row.insert(self.db_write()).await?; rows.insert(inserted.name.clone(), inserted); } else if let Some(row) = rows.get(&b.name) { let protected = policy.is_match(&b.name); if row.is_protected != protected { let am = git_branches::ActiveModel { id: Unchanged(row.id), is_protected: Set(protected), updated_at: Set(OffsetDateTime::now_utc()), ..Default::default() }; if let Ok(updated) = am.update(self.db_write()).await { rows.insert(updated.name.clone(), updated); } } } } let mut out: Vec = branches .iter() .filter_map(|b| { rows.get(&b.name) .map(|row| BranchDto::from_row(row, b.sha.to_string())) }) .collect(); out.sort_by(|a, b| a.name.cmp(&b.name)); Ok(out) } pub async fn get_branch(&self, ctx: &GitContext, name: String) -> Result { validate::validate_branch_name(&name)?; let baby = self.baby(&ctx); let info = baby.find_branch(&name).await.map_err(map_baby)?; let rows = self.branch_rows(&ctx).await?; match rows.get(&name) { Some(row) => Ok(BranchDto::from_row(row, info.sha.to_string())), None => Ok(BranchDto { name: info.name, commit_sha: info.sha.to_string(), is_protected: false, is_deleted: false, deleted_at: None, created_at: OffsetDateTime::now_utc(), updated_at: OffsetDateTime::now_utc(), }), } } pub async fn create_branch( &self, ctx: &GitContext, cmd: CreateBranchCmd, ) -> Result { let name = validate::validate_branch_name(&cmd.name)?; if let Some(sp) = &cmd.start_point && (sp.trim().is_empty() || sp.starts_with('-')) { return Err(GitError::InvalidArgument(format!( "invalid start point `{sp}`" ))); } let baby = self.baby(&ctx); baby.create_branch(&name, cmd.start_point.as_deref()) .await .map_err(map_baby)?; let info = baby.find_branch(&name).await.map_err(map_baby)?; let rows = self.branch_rows(&ctx).await?; let now = OffsetDateTime::now_utc(); let row = match rows.get(&name) { Some(existing) => { let am = git_branches::ActiveModel { id: Unchanged(existing.id), commit_sha: Set(info.sha.to_string()), is_deleted: Set(false), deleted_at: Set(None), updated_at: Set(now), ..Default::default() }; am.update(self.db_write()).await? } None => { let am = git_branches::ActiveModel { id: Set(uuid::Uuid::now_v7()), repo_id: Set(ctx.model.id), name: Set(name.clone()), commit_sha: Set(info.sha.to_string()), is_deleted: Set(false), is_protected: Set(false), deleted_at: Set(None), created_at: Set(now), updated_at: Set(now), }; am.insert(self.db_write()).await? } }; self.sync_repo_stats(&ctx).await?; Ok(BranchDto::from_row(&row, info.sha.to_string())) } pub async fn rename_branch( &self, ctx: &GitContext, from: String, to: String, ) -> Result<(), GitError> { validate::validate_branch_name(&from)?; let to = validate::validate_branch_name(&to)?; let baby = self.baby(&ctx); baby.rename_branch(&from, &to).await.map_err(map_baby)?; let product = git_branches::Entity::find() .filter(git_branches::Column::RepoId.eq(ctx.model.id)) .filter(git_branches::Column::Name.eq(&from)) .one(self.db_write()) .await?; if let Some(row) = product { let am = git_branches::ActiveModel { id: Unchanged(row.id), name: Set(to), updated_at: Set(OffsetDateTime::now_utc()), ..Default::default() }; am.update(self.db_write()).await?; } self.sync_repo_stats(&ctx).await?; Ok(()) } pub async fn delete_branch( &self, ctx: &GitContext, name: String, force: bool, ) -> Result<(), GitError> { let name = validate::validate_branch_name(&name)?; let policy = load_branch_policy(self.database.as_ref(), ctx.model.id).await?; if policy.is_match(&name) { return Err(GitError::Forbidden); } let baby = self.baby(&ctx); let info = baby.find_branch(&name).await.map_err(map_baby)?; baby.delete_branch(&name, force).await.map_err(map_baby)?; let now = OffsetDateTime::now_utc(); let rows = self.branch_rows(&ctx).await?; match rows.get(&name) { Some(existing) => { let am = git_branches::ActiveModel { id: Unchanged(existing.id), is_deleted: Set(true), deleted_at: Set(Some(now)), updated_at: Set(now), ..Default::default() }; am.update(self.db_write()).await?; } None => { let am = git_branches::ActiveModel { id: Set(uuid::Uuid::now_v7()), repo_id: Set(ctx.model.id), name: Set(name.clone()), commit_sha: Set(info.sha.to_string()), is_deleted: Set(true), is_protected: Set(false), deleted_at: Set(Some(now)), created_at: Set(now), updated_at: Set(now), }; am.insert(self.db_write()).await?; } } self.sync_repo_stats(&ctx).await?; Ok(()) } pub async fn set_default_branch(&self, ctx: &GitContext, name: String) -> Result<(), GitError> { let name = validate::validate_branch_name(&name)?; let baby = self.baby(&ctx); baby.set_default_branch(&name).await.map_err(map_baby)?; let am = repos::ActiveModel { id: Unchanged(ctx.model.id), default_branch: Set(name), updated_at: Set(OffsetDateTime::now_utc()), ..Default::default() }; am.update(self.db_write()).await?; Ok(()) } }