gitdataai/libs/service/project/repo.rs
2026-04-15 09:08:09 +08:00

338 lines
12 KiB
Rust

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<u64>,
pub cursor: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct ProjectRepositoryItem {
pub uid: Uuid,
pub repo_name: String,
pub description: Option<String>,
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<DateTime<Utc>>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct ProjectRepositoryPagination {
pub items: Vec<ProjectRepositoryItem>,
pub cursor: Option<String>,
pub total: u64,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct ProjectRepoCreateParams {
pub repo_name: String,
pub description: Option<String>,
/// 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<String>,
pub default_branch: String,
pub project_name: String,
pub is_private: bool,
pub storage_path: String,
pub created_at: DateTime<Utc>,
}
impl AppService {
pub async fn project_repo(
&self,
_ctx: &Session,
project_name: String,
query: ProjectRepositoryQuery,
) -> Result<ProjectRepositoryPagination, AppError> {
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<Uuid> = repo_list.iter().map(|r| r.id).collect();
let commit_counts: HashMap<Uuid, i64> = 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<Uuid, i64> = 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<Uuid, i64> = 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<Uuid, i64> = 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<Uuid, i64> = 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<Uuid, Option<DateTime<Utc>>> = {
let mut map: HashMap<Uuid, Option<DateTime<Utc>>> = HashMap::new();
for repo_id in &repo_ids {
let last_commit: Option<models::repos::repo_commit::Model> = 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<ProjectRepositoryItem> = 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<ProjectRepoCreateResponse, AppError> {
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(&params.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,
})
}
}