use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::error::AppError; use crate::models::common::Role; use crate::models::repos::RepoBranch; use crate::service::RepoService; use crate::session::Session; use super::util::{clamp_limit_offset, ensure_affected, required_text, set_local_user_id}; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct CreateBranchParams { pub name: String, pub commit_sha: String, } impl RepoService { pub async fn repo_branches( &self, ctx: &Session, wk_name: &str, repo_name: &str, limit: i64, offset: i64, ) -> Result, AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let repo = self.resolve_repo(wk_name, repo_name).await?; let repo_id = repo.id; self.ensure_repo_readable(user_uid, &repo).await?; let (limit, offset) = clamp_limit_offset(limit, offset); sqlx::query_as::<_, RepoBranch>( "SELECT id, repo_id, name, commit_sha, protected, default_branch, created_by, last_push_id, last_push_at, created_at, updated_at FROM repo_branch WHERE repo_id = $1 ORDER BY default_branch DESC, name ASC LIMIT $2 OFFSET $3", ) .bind(repo_id) .bind(limit) .bind(offset) .fetch_all(self.ctx.db.reader()) .await .map_err(AppError::Database) } pub async fn repo_create_branch( &self, ctx: &Session, wk_name: &str, repo_name: &str, params: CreateBranchParams, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let repo = self.resolve_repo(wk_name, repo_name).await?; let repo_id = repo.id; self.ensure_repo_role_at_least(user_uid, &repo, Role::Member) .await?; let name = required_text(params.name, "name")?; let existing = sqlx::query_scalar::<_, bool>( "SELECT EXISTS(SELECT 1 FROM repo_branch WHERE repo_id = $1 AND name = $2)", ) .bind(repo_id) .bind(&name) .fetch_one(self.ctx.db.reader()) .await .map_err(AppError::Database)?; if existing { return Err(AppError::Conflict("branch already exists".into())); } let now = chrono::Utc::now(); let mut txn = self .ctx .db .writer() .begin() .await .map_err(|_| AppError::TxnError)?; sqlx::query(set_local_user_id(user_uid)) .execute(&mut *txn) .await .map_err(AppError::Database)?; let branch = sqlx::query_as::<_, RepoBranch>( "INSERT INTO repo_branch (id, repo_id, name, commit_sha, protected, default_branch, created_by, created_at, updated_at) \ VALUES ($1, $2, $3, $4, false, false, $5, $6, $6) RETURNING id, repo_id, name, commit_sha, protected, default_branch, created_by, last_push_id, last_push_at, created_at, updated_at", ) .bind(Uuid::now_v7()) .bind(repo_id) .bind(&name) .bind(¶ms.commit_sha) .bind(user_uid) .bind(now) .fetch_one(&mut *txn) .await .map_err(AppError::Database)?; sqlx::query( "UPDATE repo_stats SET branches_count = branches_count + 1, updated_at = $1 WHERE repo_id = $2", ) .bind(now) .bind(repo_id) .execute(&mut *txn) .await .map_err(AppError::Database)?; txn.commit().await.map_err(|_| AppError::TxnError)?; Ok(branch) } pub async fn repo_set_default_branch( &self, ctx: &Session, wk_name: &str, repo_name: &str, branch_id: Uuid, ) -> Result<(), AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let repo = self.resolve_repo(wk_name, repo_name).await?; let repo_id = repo.id; self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin) .await?; let branch = sqlx::query_as::<_, RepoBranch>( "SELECT id, repo_id, name, commit_sha, protected, default_branch, created_by, last_push_id, last_push_at, created_at, updated_at FROM repo_branch WHERE id = $1 AND repo_id = $2", ) .bind(branch_id) .bind(repo_id) .fetch_optional(self.ctx.db.reader()) .await .map_err(AppError::Database)? .ok_or(AppError::NotFound("branch not found".into()))?; let now = chrono::Utc::now(); let mut txn = self .ctx .db .writer() .begin() .await .map_err(|_| AppError::TxnError)?; sqlx::query(set_local_user_id(user_uid)) .execute(&mut *txn) .await .map_err(AppError::Database)?; sqlx::query("UPDATE repo_branch SET default_branch = false, updated_at = $1 WHERE repo_id = $2 AND default_branch = true") .bind(now) .bind(repo_id) .execute(&mut *txn) .await .map_err(AppError::Database)?; sqlx::query("UPDATE repo_branch SET default_branch = true, updated_at = $1 WHERE id = $2") .bind(now) .bind(branch_id) .execute(&mut *txn) .await .map_err(AppError::Database)?; sqlx::query("UPDATE repo SET default_branch = $1, updated_at = $2 WHERE id = $3 AND deleted_at IS NULL") .bind(&branch.name) .bind(now) .bind(repo_id) .execute(&mut *txn) .await .map_err(AppError::Database)?; txn.commit().await.map_err(|_| AppError::TxnError)?; Ok(()) } pub async fn repo_set_default_branch_by_name( &self, ctx: &Session, wk_name: &str, repo_name: &str, branch_name: &str, ) -> Result<(), AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let repo = self.resolve_repo(wk_name, repo_name).await?; let repo_id = repo.id; self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin) .await?; let now = chrono::Utc::now(); let mut txn = self .ctx .db .writer() .begin() .await .map_err(|_| AppError::TxnError)?; sqlx::query(set_local_user_id(user_uid)) .execute(&mut *txn) .await .map_err(AppError::Database)?; sqlx::query("UPDATE repo_branch SET default_branch = false, updated_at = $1 WHERE repo_id = $2 AND default_branch = true") .bind(now).bind(repo_id).execute(&mut *txn).await.map_err(AppError::Database)?; sqlx::query("UPDATE repo_branch SET default_branch = true, updated_at = $1 WHERE repo_id = $2 AND name = $3") .bind(now).bind(repo_id).bind(branch_name).execute(&mut *txn).await.map_err(AppError::Database)?; sqlx::query("UPDATE repo SET default_branch = $1, updated_at = $2 WHERE id = $3 AND deleted_at IS NULL") .bind(branch_name).bind(now).bind(repo_id).execute(&mut *txn).await.map_err(AppError::Database)?; txn.commit().await.map_err(|_| AppError::TxnError)?; Ok(()) } pub async fn repo_set_branch_protection( &self, ctx: &Session, wk_name: &str, repo_name: &str, branch_id: Uuid, protected: bool, ) -> Result<(), AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let repo = self.resolve_repo(wk_name, repo_name).await?; let repo_id = repo.id; self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin) .await?; let now = chrono::Utc::now(); let mut txn = self .ctx .db .writer() .begin() .await .map_err(|_| AppError::TxnError)?; sqlx::query(set_local_user_id(user_uid)) .execute(&mut *txn) .await .map_err(AppError::Database)?; let result = sqlx::query( "UPDATE repo_branch SET protected = $1, updated_at = $2 WHERE id = $3 AND repo_id = $4", ) .bind(protected) .bind(now) .bind(branch_id) .bind(repo_id) .execute(&mut *txn) .await .map_err(AppError::Database)?; ensure_affected(result.rows_affected(), "branch not found")?; txn.commit().await.map_err(|_| AppError::TxnError)?; Ok(()) } pub async fn repo_set_branch_protection_by_name( &self, ctx: &Session, wk_name: &str, repo_name: &str, branch_name: &str, protected: bool, ) -> Result<(), AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let repo = self.resolve_repo(wk_name, repo_name).await?; let repo_id = repo.id; self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin) .await?; let now = chrono::Utc::now(); let mut txn = self .ctx .db .writer() .begin() .await .map_err(|_| AppError::TxnError)?; sqlx::query(set_local_user_id(user_uid)) .execute(&mut *txn) .await .map_err(AppError::Database)?; let result = sqlx::query( "UPDATE repo_branch SET protected = $1, updated_at = $2 WHERE name = $3 AND repo_id = $4", ) .bind(protected).bind(now).bind(branch_name).bind(repo_id) .execute(&mut *txn).await.map_err(AppError::Database)?; ensure_affected(result.rows_affected(), "branch not found")?; txn.commit().await.map_err(|_| AppError::TxnError)?; Ok(()) } pub async fn repo_delete_branch( &self, ctx: &Session, wk_name: &str, repo_name: &str, branch_id: Uuid, ) -> Result<(), AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let repo = self.resolve_repo(wk_name, repo_name).await?; let repo_id = repo.id; self.ensure_repo_role_at_least(user_uid, &repo, Role::Admin) .await?; let is_default = sqlx::query_scalar::<_, bool>( "SELECT default_branch FROM repo_branch WHERE id = $1 AND repo_id = $2", ) .bind(branch_id) .bind(repo_id) .fetch_optional(self.ctx.db.reader()) .await .map_err(AppError::Database)? .ok_or(AppError::NotFound("branch not found".into()))?; if is_default { return Err(AppError::BadRequest("cannot delete default branch".into())); } let mut txn = self .ctx .db .writer() .begin() .await .map_err(|_| AppError::TxnError)?; sqlx::query(set_local_user_id(user_uid)) .execute(&mut *txn) .await .map_err(AppError::Database)?; let result = sqlx::query("DELETE FROM repo_branch WHERE id = $1 AND repo_id = $2") .bind(branch_id) .bind(repo_id) .execute(&mut *txn) .await .map_err(AppError::Database)?; ensure_affected(result.rows_affected(), "branch not found")?; sqlx::query( "UPDATE repo_stats SET branches_count = GREATEST(branches_count - 1, 0), updated_at = $1 WHERE repo_id = $2", ) .bind(chrono::Utc::now()) .bind(repo_id) .execute(&mut *txn) .await .map_err(AppError::Database)?; txn.commit().await.map_err(|_| AppError::TxnError)?; Ok(()) } }