338 lines
12 KiB
Rust
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(¶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,
|
|
})
|
|
}
|
|
}
|