use crate::AppService; use crate::error::AppError; use chrono::{DateTime, Utc}; use models::projects::{Project, project_members}; use models::repos::repo::{ ActiveModel as RepoActiveModel, Column as RepoColumn, Entity as RepoEntity, }; use models::repos::{Repo, RepoBranch, RepoCommit, RepoStar, RepoTag, RepoWatch}; use sea_orm::*; use serde::{Deserialize, Serialize}; use session::Session; use std::collections::HashMap; use std::path::PathBuf; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema, IntoParams)] pub struct ProjectRepositoryQuery { pub limit: Option, pub cursor: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] pub struct ProjectRepositoryItem { pub uid: Uuid, pub repo_name: String, pub description: Option, pub default_branch: String, pub project_name: String, pub is_private: bool, pub commit_count: i64, pub branch_count: i64, pub tag_count: i64, pub star_count: i64, pub watch_count: i64, pub last_commit_at: Option>, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] pub struct ProjectRepositoryPagination { pub items: Vec, pub cursor: Option, pub total: u64, } #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct ProjectRepoCreateParams { pub repo_name: String, pub description: Option, /// Default: true. When false, skips bare git init and leaves default_branch empty; /// the branch will be auto-detected and set on first push. #[serde(default = "default_true")] pub init_repo: bool, /// Only used when init_repo is true. #[serde(default = "default_branch_name")] pub default_branch: String, #[serde(default)] pub is_private: bool, } fn default_true() -> bool { true } fn default_branch_name() -> String { "main".to_string() } #[derive(Debug, Clone, Serialize, ToSchema)] pub struct ProjectRepoCreateResponse { pub uid: Uuid, pub repo_name: String, pub description: Option, pub default_branch: String, pub project_name: String, pub is_private: bool, pub storage_path: String, pub created_at: DateTime, } impl AppService { pub async fn project_repo( &self, _ctx: &Session, project_name: String, query: ProjectRepositoryQuery, ) -> Result { let limit = query.limit.unwrap_or(10); let project = Project::find() .filter(models::projects::project::Column::Name.eq(&project_name)) .one(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .ok_or(AppError::ProjectNotFound)?; let repo_list = Repo::find() .filter(models::repos::repo::Column::Project.eq(project.id)) .order_by_desc(models::repos::repo::Column::UpdatedAt) .limit(limit) .all(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let repo_total = Repo::find() .filter(models::repos::repo::Column::Project.eq(project.id)) .count(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; if repo_list.is_empty() { return Ok(ProjectRepositoryPagination { items: vec![], cursor: None, total: repo_total, }); } let repo_ids: Vec = repo_list.iter().map(|r| r.id).collect(); let commit_counts: HashMap = RepoCommit::find() .select_only() .column(models::repos::repo_commit::Column::Repo) .column_as(models::repos::repo_commit::Column::Id.count(), "count") .filter(models::repos::repo_commit::Column::Repo.is_in(repo_ids.clone())) .group_by(models::repos::repo_commit::Column::Repo) .into_tuple::<(Uuid, i64)>() .all(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .into_iter() .collect(); let branch_counts: HashMap = RepoBranch::find() .select_only() .column(models::repos::repo_branch::Column::Repo) .column_as(models::repos::repo_branch::Column::Repo.count(), "count") .filter(models::repos::repo_branch::Column::Repo.is_in(repo_ids.clone())) .group_by(models::repos::repo_branch::Column::Repo) .into_tuple::<(Uuid, i64)>() .all(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .into_iter() .collect(); let tag_counts: HashMap = RepoTag::find() .select_only() .column(models::repos::repo_tag::Column::Repo) .column_as(models::repos::repo_tag::Column::Repo.count(), "count") .filter(models::repos::repo_tag::Column::Repo.is_in(repo_ids.clone())) .group_by(models::repos::repo_tag::Column::Repo) .into_tuple::<(Uuid, i64)>() .all(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .into_iter() .collect(); let star_counts: HashMap = RepoStar::find() .select_only() .column(models::repos::repo_star::Column::Repo) .column_as(models::repos::repo_star::Column::User.count(), "count") .filter(models::repos::repo_star::Column::Repo.is_in(repo_ids.clone())) .group_by(models::repos::repo_star::Column::Repo) .into_tuple::<(Uuid, i64)>() .all(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .into_iter() .collect(); let watch_counts: HashMap = RepoWatch::find() .select_only() .column(models::repos::repo_watch::Column::Repo) .column_as(models::repos::repo_watch::Column::User.count(), "count") .filter(models::repos::repo_watch::Column::Repo.is_in(repo_ids.clone())) .group_by(models::repos::repo_watch::Column::Repo) .into_tuple::<(Uuid, i64)>() .all(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .into_iter() .collect(); let last_commit_times: HashMap>> = { let mut map: HashMap>> = HashMap::new(); for repo_id in &repo_ids { let last_commit: Option = RepoCommit::find() .filter(models::repos::repo_commit::Column::Repo.eq(*repo_id)) .order_by_desc(models::repos::repo_commit::Column::CreatedAt) .one(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let time = last_commit.map(|c| c.created_at); map.insert(*repo_id, time); } map }; let items: Vec = repo_list .into_iter() .map(|r| ProjectRepositoryItem { uid: r.id, repo_name: r.repo_name, description: r.description, default_branch: r.default_branch, project_name: project.name.clone(), is_private: r.is_private, commit_count: *commit_counts.get(&r.id).unwrap_or(&0), branch_count: *branch_counts.get(&r.id).unwrap_or(&0), tag_count: *tag_counts.get(&r.id).unwrap_or(&0), star_count: *star_counts.get(&r.id).unwrap_or(&0), watch_count: *watch_counts.get(&r.id).unwrap_or(&0), last_commit_at: last_commit_times.get(&r.id).and_then(|t| *t), }) .collect(); Ok(ProjectRepositoryPagination { items, cursor: None, total: repo_total, }) } pub async fn project_repo_create( &self, ctx: &Session, project_name: String, params: ProjectRepoCreateParams, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; // Find project and verify membership let project = self .utils_find_project_by_name(project_name.clone()) .await?; let _member = project_members::Entity::find() .filter(project_members::Column::Project.eq(project.id)) .filter(project_members::Column::User.eq(user_uid)) .one(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .ok_or(AppError::NoPower)?; // Check repo name uniqueness within project let existing = RepoEntity::find() .filter(RepoColumn::Project.eq(project.id)) .filter(RepoColumn::RepoName.eq(¶ms.repo_name)) .one(&self.db) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; if existing.is_some() { return Err(AppError::RepoNameAlreadyExists); } // Build storage path let repos_root = self .config .repos_root() .map_err(|e| AppError::InternalServerError(e.to_string()))?; let project_dir: PathBuf = [&repos_root, &project.name].iter().collect(); let repo_dir: PathBuf = project_dir.join(format!("{}.git", params.repo_name)); // Only initialize bare git repo if requested if params.init_repo { crate::git::GitDomain::init_bare(&repo_dir).map_err(AppError::from)?; } // Insert DB record let repo_id = Uuid::now_v7(); let now = Utc::now(); // default_branch is only set when init_repo is true; otherwise it stays empty // and will be detected on first push via the sync hook let default_branch = if params.init_repo { params.default_branch.clone() } else { String::new() }; let default_branch_for_log = default_branch.clone(); let repo = RepoActiveModel { id: Set(repo_id), repo_name: Set(params.repo_name.clone()), project: Set(project.id), description: Set(params.description.clone()), default_branch: Set(default_branch), is_private: Set(params.is_private), storage_path: Set(repo_dir.to_string_lossy().to_string()), created_by: Set(user_uid), created_at: Set(now), updated_at: Set(now), ai_code_review_enabled: Set(false), }; let repo = repo.insert(&self.db).await?; let _ = self .project_log_activity( project.id, Some(repo.id), user_uid, super::activity::ActivityLogParams { event_type: "repo_create".to_string(), title: format!("{} created repository '{}'", user_uid, params.repo_name), repo_id: Some(repo.id), content: params.description.clone(), event_id: Some(repo.id), event_sub_id: None, metadata: Some(serde_json::json!({ "repo_name": params.repo_name, "default_branch": default_branch_for_log, "is_private": params.is_private, "init_repo": params.init_repo, })), is_private: false, }, ) .await; Ok(ProjectRepoCreateResponse { uid: repo.id, repo_name: repo.repo_name, description: repo.description, default_branch: repo.default_branch, project_name: project.name, is_private: repo.is_private, storage_path: repo.storage_path, created_at: repo.created_at, }) } }