use db::sqlx; use git::rpc::{proto as p, proto::init_service_client::InitServiceClient}; use model::repos::RepoModel; use session::Session; use crate::{ AppService, error::AppError, git::rpc_err, metrics::with_op_metric, session_user, }; #[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] pub struct CreateRepo { pub name: String, pub description: Option, pub default_branch: Option, pub visibility: Option, pub initialize_with_readme: Option, pub enable_lfs: Option, } #[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] pub struct CloneRepo { pub name: String, pub source_url: String, pub description: Option, pub visibility: Option, } impl AppService { #[tracing::instrument(skip(self, ctx), fields(workspace = %wk_name))] pub async fn git_init_bare( &self, ctx: &Session, wk_name: &str, params: CreateRepo, ) -> Result { with_op_metric(&self.metrics.repo_operations_total, &["create"], async { let user_uid = session_user(ctx)?; let wk = self.workspace_resolve(wk_name).await?; self.workspace_require_admin(wk.id, user_uid).await?; let name = params.name.trim(); if name.is_empty() { return Err(AppError::BadRequest( "repo name is required".to_string(), )); } let existing = sqlx::query_scalar::<_, bool>( "SELECT EXISTS(SELECT 1 FROM repo WHERE wk = $1 AND name = $2 AND deleted_at IS NULL)", ) .bind(wk.id) .bind(name) .fetch_one(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; if existing { return Err(AppError::RepoNameAlreadyExists); } let default_branch = params.default_branch.unwrap_or_else(|| "main".to_string()); let visibility = params.visibility.unwrap_or_else(|| "private".to_string()); let description = params.description.unwrap_or_default(); let now = chrono::Utc::now(); let repo_id = uuid::Uuid::now_v7(); let mut txn = self.db.begin().await.map_err(|_| AppError::TxnError)?; let _repo = sqlx::query_as::<_, RepoModel>( "INSERT INTO repo (id, wk, name, description, default_branch, visibility, size_bytes, \ is_archived, is_template, is_mirror, created_by, storage_path, created_at, updated_at) \ VALUES ($1, $2, $3, $4, $5, $6, 0, false, false, false, $7, '', $8, $8) \ RETURNING id, wk, name, description, default_branch, visibility, size_bytes, \ is_archived, is_template, is_mirror, created_by, storage_path, created_at, updated_at, deleted_at", ) .bind(repo_id) .bind(wk.id) .bind(name) .bind(&description) .bind(&default_branch) .bind(&visibility) .bind(user_uid) .bind(now) .fetch_one(&mut **txn.inner_mut()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let storage_root = self .config .repos_root() .map_err(|e| AppError::InternalServerError(e.to_string()))?; let mut client = InitServiceClient::new(self.git.clone()); let rpc_resp = client .init_bare(tonic::Request::new(p::InitBareRequest { storage_root, params: Some(p::InitRepoParams { namespace: wk.name.clone(), repo_name: name.to_string(), default_branch, description: Some(description), initialize_with_readme: params .initialize_with_readme .unwrap_or(false), enable_lfs: params.enable_lfs.unwrap_or(false), }), })) .await .map_err(rpc_err)? .into_inner(); let repo = sqlx::query_as::<_, RepoModel>( "UPDATE repo SET storage_path = $1 WHERE id = $2 \ RETURNING id, wk, name, description, default_branch, visibility, size_bytes, \ is_archived, is_template, is_mirror, created_by, storage_path, created_at, updated_at, deleted_at", ) .bind(&rpc_resp.storage_path) .bind(repo_id) .fetch_one(&mut **txn.inner_mut()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; txn.commit().await.map_err(|_| AppError::TxnError)?; self.queue_sync(repo_id).await; Ok(repo) }).await } pub async fn git_clone_bare( &self, ctx: &Session, wk_name: &str, params: CloneRepo, ) -> Result { let user_uid = session_user(ctx)?; let wk = self.workspace_resolve(wk_name).await?; self.workspace_require_admin(wk.id, user_uid).await?; let name = params.name.trim(); if name.is_empty() { return Err(AppError::BadRequest( "repo name is required".to_string(), )); } let source_url = params.source_url.trim(); if source_url.is_empty() { return Err(AppError::BadRequest( "source URL is required".to_string(), )); } let existing = sqlx::query_scalar::<_, bool>( "SELECT EXISTS(SELECT 1 FROM repo WHERE wk = $1 AND name = $2 AND deleted_at IS NULL)", ) .bind(wk.id) .bind(name) .fetch_one(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; if existing { return Err(AppError::RepoNameAlreadyExists); } let visibility = params.visibility.unwrap_or_else(|| "private".to_string()); let description = params.description.unwrap_or_default(); let now = chrono::Utc::now(); let repo_id = uuid::Uuid::now_v7(); let mut txn = self.db.begin().await.map_err(|_| AppError::TxnError)?; let _repo = sqlx::query_as::<_, RepoModel>( "INSERT INTO repo (id, wk, name, description, default_branch, visibility, size_bytes, \ is_archived, is_template, is_mirror, created_by, storage_path, created_at, updated_at) \ VALUES ($1, $2, $3, $4, '', $5, 0, false, false, true, $6, '', $7, $7) \ RETURNING id, wk, name, description, default_branch, visibility, size_bytes, \ is_archived, is_template, is_mirror, created_by, storage_path, created_at, updated_at, deleted_at", ) .bind(repo_id) .bind(wk.id) .bind(name) .bind(&description) .bind(&visibility) .bind(user_uid) .bind(now) .fetch_one(&mut **txn.inner_mut()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let storage_root = self .config .repos_root() .map_err(|e| AppError::InternalServerError(e.to_string()))?; let mut client = InitServiceClient::new(self.git.clone()); let rpc_resp = client .clone_bare(tonic::Request::new(p::CloneBareRequest { storage_root, source_url: source_url.to_string(), namespace: wk.name.clone(), repo_name: name.to_string(), })) .await .map_err(rpc_err)? .into_inner(); let repo = sqlx::query_as::<_, RepoModel>( "UPDATE repo SET storage_path = $1 WHERE id = $2 \ RETURNING id, wk, name, description, default_branch, visibility, size_bytes, \ is_archived, is_template, is_mirror, created_by, storage_path, created_at, updated_at, deleted_at", ) .bind(&rpc_resp.storage_path) .bind(repo_id) .fetch_one(&mut **txn.inner_mut()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; txn.commit().await.map_err(|_| AppError::TxnError)?; self.queue_sync(repo_id).await; Ok(repo) } }