diff --git a/jupiter/callisto/src/item_labels.rs b/jupiter/callisto/src/item_labels.rs index ff764aca3..39b06e84b 100644 --- a/jupiter/callisto/src/item_labels.rs +++ b/jupiter/callisto/src/item_labels.rs @@ -15,7 +15,48 @@ pub struct Model { pub item_type: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + MegaIssue, + MegaMr, + Label +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::MegaIssue => Entity::belongs_to(super::mega_issue::Entity) + .from(Column::ItemId) + .to(super::mega_issue::Column::Id) + .into(), + Self::MegaMr => Entity::belongs_to(super::mega_mr::Entity) + .from(Column::ItemId) + .to(super::mega_mr::Column::Id) + .into(), + Self::Label => Entity::belongs_to(super::label::Entity) + .from(Column::LabelId) + .to(super::label::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Label.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaIssue.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::MegaMr.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/label.rs b/jupiter/callisto/src/label.rs index 7d7ffdf59..89e33202d 100644 --- a/jupiter/callisto/src/label.rs +++ b/jupiter/callisto/src/label.rs @@ -15,7 +15,17 @@ pub struct Model { pub description: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + ItemLabels, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::ItemLabels => Entity::has_many(super::item_labels::Entity).into(), + } + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/mega_issue.rs b/jupiter/callisto/src/mega_issue.rs index 6a18d537f..0e0a61214 100644 --- a/jupiter/callisto/src/mega_issue.rs +++ b/jupiter/callisto/src/mega_issue.rs @@ -18,7 +18,27 @@ pub struct Model { pub user_id: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Label, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Label => Entity::has_many(super::item_labels::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::item_labels::Relation::Label.def() + } + + fn via() -> Option { + Some(super::item_labels::Relation::MegaIssue.def().rev()) + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/mega_mr.rs b/jupiter/callisto/src/mega_mr.rs index 7ebf15901..9a1e50ca1 100644 --- a/jupiter/callisto/src/mega_mr.rs +++ b/jupiter/callisto/src/mega_mr.rs @@ -22,7 +22,27 @@ pub struct Model { pub updated_at: DateTime, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Label, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Label => Entity::has_many(super::item_labels::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + super::item_labels::Relation::Label.def() + } + + fn via() -> Option { + Some(super::item_labels::Relation::MegaMr.def().rev()) + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/src/storage/issue_storage.rs b/jupiter/src/storage/issue_storage.rs index bcdca6047..13780eab4 100644 --- a/jupiter/src/storage/issue_storage.rs +++ b/jupiter/src/storage/issue_storage.rs @@ -34,7 +34,7 @@ impl IssueStorage { &self, status: &str, page: Pagination, - ) -> Result<(Vec, u64), MegaError> { + ) -> Result<(Vec<(mega_issue::Model, Vec)>, u64), MegaError> { let paginator = mega_issue::Entity::find() .filter(mega_issue::Column::Status.eq(status)) .order_by_desc(mega_issue::Column::CreatedAt) @@ -45,14 +45,16 @@ impl IssueStorage { .await .map(|m| (m, num_pages))?; - // let issue_ids: Vec = issues.iter().map(|i| i.id).collect(); - // let item_label_with_labels = item_labels::Entity::find() - // .filter(item_labels::Column::ItemId.is_in(issue_ids.clone())) - // .find_also_related(label::Entity) - // .all(self.get_connection()) - // .await?; + let issues_with_label: Vec<(mega_issue::Model, Vec)> = + mega_issue::Entity::find() + .filter( + mega_issue::Column::Id.is_in(issues.iter().map(|i| i.id).collect::>()), + ) + .find_with_related(label::Entity) + .all(self.get_connection()) + .await?; - Ok((issues, page)) + Ok((issues_with_label, page)) } pub async fn get_issue(&self, link: &str) -> Result, MegaError> { diff --git a/jupiter/src/storage/mr_storage.rs b/jupiter/src/storage/mr_storage.rs index b8b3f5304..3856be1e9 100644 --- a/jupiter/src/storage/mr_storage.rs +++ b/jupiter/src/storage/mr_storage.rs @@ -1,11 +1,12 @@ use std::sync::Arc; +use common::model::Pagination; use sea_orm::{ ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, QueryOrder, Set, }; -use callisto::mega_mr; +use callisto::{label, mega_mr}; use callisto::sea_orm_active_enums::MergeStatusEnum; use common::errors::MegaError; use common::utils::generate_id; @@ -46,18 +47,28 @@ impl MrStorage { pub async fn get_mr_by_status( &self, status: Vec, - page: u64, - per_page: u64, - ) -> Result<(Vec, u64), MegaError> { + page: Pagination, + ) -> Result<(Vec<(mega_mr::Model, Vec)>, u64), MegaError> { let paginator = mega_mr::Entity::find() .filter(mega_mr::Column::Status.is_in(status)) .order_by_desc(mega_mr::Column::CreatedAt) - .paginate(self.get_connection(), per_page); + .paginate(self.get_connection(), page.per_page); let num_pages = paginator.num_items().await?; - Ok(paginator - .fetch_page(page - 1) + + let (mr_list, page) = paginator + .fetch_page(page.page - 1) .await - .map(|m| (m, num_pages))?) + .map(|m| (m, num_pages))?; + + let mr_with_label: Vec<(mega_mr::Model, Vec)> = + mega_mr::Entity::find() + .filter( + mega_mr::Column::Id.is_in(mr_list.iter().map(|i| i.id).collect::>()), + ) + .find_with_related(label::Entity) + .all(self.get_connection()) + .await?; + Ok((mr_with_label, page)) } pub async fn get_mr(&self, link: &str) -> Result, MegaError> { @@ -129,5 +140,4 @@ impl MrStorage { a_model.update(self.get_connection()).await.unwrap(); Ok(()) } - } diff --git a/mono/Cargo.toml b/mono/Cargo.toml index ef02a76d4..f5112f7c8 100644 --- a/mono/Cargo.toml +++ b/mono/Cargo.toml @@ -54,6 +54,8 @@ cedar-policy = { workspace = true } utoipa = { workspace = true, features = ["axum_extras"] } utoipa-axum = { workspace = true } utoipa-swagger-ui = { workspace = true, features = ["axum"] } +uuid = { workspace = true, features = ["v4"] } + [target.'cfg(not(windows))'.dependencies] jemallocator = { workspace = true } diff --git a/mono/src/api/issue/mod.rs b/mono/src/api/issue/mod.rs index eeb3320c4..7ebd068dd 100644 --- a/mono/src/api/issue/mod.rs +++ b/mono/src/api/issue/mod.rs @@ -1,4 +1,4 @@ -use callisto::mega_issue; +use callisto::{label, mega_issue}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -18,17 +18,17 @@ pub struct IssueItem { pub updated_at: i64, } -impl From for IssueItem { - fn from(value: mega_issue::Model) -> Self { +impl From<(mega_issue::Model, Vec)> for IssueItem { + fn from(value: (mega_issue::Model, Vec)) -> Self { Self { - link: value.link, - title: value.title, - status: value.status.to_string(), - user_id: value.user_id, - open_timestamp: value.created_at.and_utc().timestamp(), - closed_at: value.closed_at.map(|dt| dt.and_utc().timestamp()), - updated_at: value.updated_at.and_utc().timestamp(), - labels: vec![] + link: value.0.link, + title: value.0.title, + status: value.0.status.to_string(), + user_id: value.0.user_id, + open_timestamp: value.0.created_at.and_utc().timestamp(), + closed_at: value.0.closed_at.map(|dt| dt.and_utc().timestamp()), + updated_at: value.0.updated_at.and_utc().timestamp(), + labels: value.1.into_iter().map(|m| m.into()).collect() } } } diff --git a/mono/src/api/mr/mod.rs b/mono/src/api/mr/mod.rs index 8ef8a472d..6009670ab 100644 --- a/mono/src/api/mr/mod.rs +++ b/mono/src/api/mr/mod.rs @@ -1,10 +1,13 @@ use serde::{Deserialize, Serialize}; use callisto::{ - mega_conversation, mega_mr, + label, mega_conversation, mega_mr, sea_orm_active_enums::{ConvTypeEnum, MergeStatusEnum}, }; use utoipa::ToSchema; +use uuid::Uuid; + +use crate::api::label::LabelItem; pub mod mr_router; @@ -21,17 +24,19 @@ pub struct MrInfoItem { pub open_timestamp: i64, pub merge_timestamp: Option, pub updated_at: i64, + pub labels: Vec, } -impl From for MrInfoItem { - fn from(value: mega_mr::Model) -> Self { +impl From<(mega_mr::Model, Vec)> for MrInfoItem { + fn from(value: (mega_mr::Model, Vec)) -> Self { Self { - link: value.link, - title: value.title, - status: value.status, - open_timestamp: value.created_at.and_utc().timestamp(), - merge_timestamp: value.merge_date.map(|dt| dt.and_utc().timestamp()), - updated_at: value.updated_at.and_utc().timestamp(), + link: value.0.link, + title: value.0.title, + status: value.0.status, + open_timestamp: value.0.created_at.and_utc().timestamp(), + merge_timestamp: value.0.merge_date.map(|dt| dt.and_utc().timestamp()), + updated_at: value.0.updated_at.and_utc().timestamp(), + labels: value.1.into_iter().map(|m| m.into()).collect(), } } } @@ -84,18 +89,51 @@ impl From for MegaConversation { } } -#[derive(Serialize, Deserialize, ToSchema)] -pub struct FilesChangedItem { - pub path: String, - pub status: String, -} - -#[derive(Serialize, Deserialize, ToSchema)] +#[derive(Serialize, ToSchema)] pub struct FilesChangedList { - pub files: Vec, + pub mui_trees: Vec, pub content: String, } +#[derive(Serialize, Debug, ToSchema)] +pub struct MuiTreeNode { + id: String, + label: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(no_recursion)] + children: Option>, +} + +impl MuiTreeNode { + fn new(label: &str) -> Self { + Self { + id: Uuid::new_v4().to_string(), + label: label.to_string(), + children: None, + } + } + + fn insert_path(&mut self, parts: &[&str]) { + if parts.is_empty() { + return; + } + + if self.children.is_none() { + self.children = Some(Vec::new()); + } + + let children = self.children.as_mut().unwrap(); + + if let Some(existing) = children.iter_mut().find(|c| c.label == parts[0]) { + existing.insert_path(&parts[1..]); + } else { + let mut new_node = MuiTreeNode::new(parts[0]); + new_node.insert_path(&parts[1..]); + children.push(new_node); + } + } +} + #[derive(Deserialize, ToSchema)] pub struct SaveCommentRequest { pub content: String, diff --git a/mono/src/api/mr/mr_router.rs b/mono/src/api/mr/mr_router.rs index d816f4754..0135ef254 100644 --- a/mono/src/api/mr/mr_router.rs +++ b/mono/src/api/mr/mr_router.rs @@ -4,7 +4,6 @@ use axum::{ extract::{Path, State}, Json, }; -use saturn::ActionEnum; use utoipa_axum::{router::OpenApiRouter, routes}; use callisto::sea_orm_active_enums::{ConvTypeEnum, MergeStatusEnum}; @@ -12,12 +11,15 @@ use common::{ errors::MegaError, model::{CommonPage, CommonResult, PageParams}, }; +use saturn::ActionEnum; use crate::api::{ - issue::{issue_router::common_label_update, LabelUpdatePayload}, mr::{ - FilesChangedItem, FilesChangedList, MRDetail, MRStatusParams, MrInfoItem, + issue::{issue_router::common_label_update, LabelUpdatePayload}, + mr::{ + FilesChangedList, MRDetail, MRStatusParams, MrInfoItem, MuiTreeNode, SaveCommentRequest, - }, oauth::model::LoginUser + }, + oauth::model::LoginUser, }; use crate::api::{util, MonoApiServiceState}; use crate::{api::error::ApiError, server::https_server::MR_TAG}; @@ -31,7 +33,7 @@ pub fn routers() -> OpenApiRouter { .routes(routes!(merge)) .routes(routes!(close_mr)) .routes(routes!(reopen_mr)) - .routes(routes!(get_mr_files_changed)) + .routes(routes!(mr_files_changed)) .routes(routes!(save_comment)) .routes(routes!(delete_comment)) .routes(routes!(labels)), @@ -194,13 +196,13 @@ async fn fetch_mr_list( }; let (items, total) = state .mr_stg() - .get_mr_by_status(status, json.pagination.page, json.pagination.per_page) + .get_mr_by_status(status, json.pagination) .await?; - let res = CommonResult::success(Some(CommonPage { + let res = CommonPage { items: items.into_iter().map(|m| m.into()).collect(), total, - })); - Ok(Json(res)) + }; + Ok(Json(CommonResult::success(Some(res)))) } /// Get merge request details @@ -240,7 +242,7 @@ async fn mr_detail( ), tag = MR_TAG )] -async fn get_mr_files_changed( +async fn mr_files_changed( Path(link): Path, state: State, ) -> Result>, ApiError> { @@ -248,12 +250,13 @@ async fn get_mr_files_changed( let diff_res = state.monorepo().content_diff(&link, listen_addr).await?; let diff_files = extract_files_with_status(&diff_res); - let mut diff_list: Vec = vec![]; - for (path, status) in diff_files { - diff_list.push(FilesChangedItem { path, status }); + let mut paths = vec![]; + for (path, _) in diff_files { + paths.push(path); } + let mui_trees = build_forest(paths); let res = CommonResult::success(Some(FilesChangedList { - files: diff_list, + mui_trees, content: diff_res, })); Ok(Json(res)) @@ -350,11 +353,33 @@ async fn labels( common_label_update(user, state, payload).await } +fn build_forest(paths: Vec) -> Vec { + let mut roots: Vec = Vec::new(); + + for path in paths { + let parts: Vec<&str> = path.split('/').collect(); + if parts.is_empty() { + continue; + } + + let root_label = parts[0]; + if let Some(existing_root) = roots.iter_mut().find(|r| r.label == root_label) { + existing_root.insert_path(&parts[1..]); + } else { + let mut new_root = MuiTreeNode::new(root_label); + new_root.insert_path(&parts[1..]); + roots.push(new_root); + } + } + + roots +} + #[cfg(test)] mod test { use std::collections::HashMap; - use crate::api::mr::mr_router::extract_files_with_status; + use crate::api::mr::mr_router::{build_forest, extract_files_with_status}; #[test] fn test_parse_diff_result_to_filelist() { @@ -390,4 +415,17 @@ mod test { assert_eq!(files_with_status, expected); } + + #[test] + fn test_files_changed_tree() { + let paths = vec![ + String::from("crates-pro/crates_pro/src/bin/bin_analyze.rs"), + String::from("crates-pro/images/analysis-tool-worker.Dockerfile"), + String::from("crates-pro/images/crates-pro.Dockerfile"), + String::from("another-root/foo/bar.txt"), + ]; + + let forest = build_forest(paths); + println!("{}", serde_json::to_string_pretty(&forest).unwrap()); + } } diff --git a/moon/packages/types/generated.ts b/moon/packages/types/generated.ts index 3a0bfa907..31214be42 100644 --- a/moon/packages/types/generated.ts +++ b/moon/packages/types/generated.ts @@ -3080,6 +3080,7 @@ export type CommonResultCommonPageLabelItem = { export type CommonResultCommonPageMrInfoItem = { data?: { items: { + labels: LabelItem[] link: string /** @format int64 */ merge_timestamp?: number | null @@ -3103,7 +3104,7 @@ export type CommonResultCommonPageMrInfoItem = { export type CommonResultFilesChangedList = { data?: { content: string - files: FilesChangedItem[] + mui_trees: MuiTreeNode[] } err_message: string req_result: boolean @@ -3194,14 +3195,9 @@ export type CreateFileInfo = { path: string } -export type FilesChangedItem = { - path: string - status: string -} - export type FilesChangedList = { content: string - files: FilesChangedItem[] + mui_trees: MuiTreeNode[] } export type IssueItem = { @@ -3278,6 +3274,7 @@ export enum MergeStatusEnum { } export type MrInfoItem = { + labels: LabelItem[] link: string /** @format int64 */ merge_timestamp?: number | null @@ -3289,6 +3286,12 @@ export type MrInfoItem = { updated_at: number } +export type MuiTreeNode = { + children?: any[] | null + id: string + label: string +} + export type NewIssue = { description: string title: string