diff --git a/ceres/src/merge_checker/mod.rs b/ceres/src/merge_checker/mod.rs index 99926e070..a91534b04 100644 --- a/ceres/src/merge_checker/mod.rs +++ b/ceres/src/merge_checker/mod.rs @@ -94,13 +94,16 @@ pub struct CheckResult { pub struct CheckerRegistry { checkers: HashMap>, storage: Arc, + #[allow(dead_code)] + username: String, } impl CheckerRegistry { - pub fn new(storage: Arc) -> Self { + pub fn new(storage: Arc, username: String) -> Self { let mut r = CheckerRegistry { checkers: HashMap::new(), storage: storage.clone(), + username, }; r.register(CheckType::MrSync, Box::new(MrSyncChecker { storage })); r diff --git a/ceres/src/pack/monorepo.rs b/ceres/src/pack/monorepo.rs index dc54f5e84..fabc4eb55 100644 --- a/ceres/src/pack/monorepo.rs +++ b/ceres/src/pack/monorepo.rs @@ -539,7 +539,7 @@ impl MonoRepo { .await? .expect("MR Not Found"); - let check_reg = CheckerRegistry::new(self.storage.clone().into()); + let check_reg = CheckerRegistry::new(self.storage.clone().into(), self.username()); check_reg.run_checks(mr_info.into()).await?; Ok(()) } diff --git a/jupiter/callisto/src/issue_mr_references.rs b/jupiter/callisto/src/issue_mr_references.rs new file mode 100644 index 000000000..c81c8999a --- /dev/null +++ b/jupiter/callisto/src/issue_mr_references.rs @@ -0,0 +1,22 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.15 + +use super::sea_orm_active_enums::ReferenceTypeEnum; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "issue_mr_references")] +pub struct Model { + pub created_at: DateTime, + pub updated_at: DateTime, + #[sea_orm(primary_key, auto_increment = false)] + pub source_id: String, + #[sea_orm(primary_key, auto_increment = false)] + pub target_id: String, + pub reference_type: ReferenceTypeEnum, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/jupiter/callisto/src/mod.rs b/jupiter/callisto/src/mod.rs index 671f46c57..e2e5e81ba 100644 --- a/jupiter/callisto/src/mod.rs +++ b/jupiter/callisto/src/mod.rs @@ -14,6 +14,7 @@ pub mod git_tag; pub mod git_tree; pub mod gpg_key; pub mod import_refs; +pub mod issue_mr_references; pub mod item_assignees; pub mod item_labels; pub mod label; diff --git a/jupiter/callisto/src/prelude.rs b/jupiter/callisto/src/prelude.rs index 7994f8b21..8885732c9 100644 --- a/jupiter/callisto/src/prelude.rs +++ b/jupiter/callisto/src/prelude.rs @@ -11,6 +11,7 @@ pub use super::git_tag::Entity as GitTag; pub use super::git_tree::Entity as GitTree; pub use super::gpg_key::Entity as GpgKey; pub use super::import_refs::Entity as ImportRefs; +pub use super::issue_mr_references::Entity as IssueMrReferences; pub use super::item_assignees::Entity as ItemAssignees; pub use super::item_labels::Entity as ItemLabels; pub use super::label::Entity as Label; diff --git a/jupiter/callisto/src/sea_orm_active_enums.rs b/jupiter/callisto/src/sea_orm_active_enums.rs index 81d65f28c..751ffe4d4 100644 --- a/jupiter/callisto/src/sea_orm_active_enums.rs +++ b/jupiter/callisto/src/sea_orm_active_enums.rs @@ -50,6 +50,8 @@ pub enum ConvTypeEnum { Label, #[sea_orm(string_value = "assignee")] Assignee, + #[sea_orm(string_value = "mention")] + Mention, } #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "merge_status_enum")] @@ -70,6 +72,20 @@ pub enum RefTypeEnum { Tag, } #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "reference_type_enum" +)] +pub enum ReferenceTypeEnum { + #[sea_orm(string_value = "mention")] + Mention, + #[sea_orm(string_value = "build_relates")] + BuildRelates, + #[sea_orm(string_value = "blocks")] + Blocks, +} +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "storage_type_enum")] pub enum StorageTypeEnum { #[sea_orm(string_value = "database")] diff --git a/jupiter/src/migration/m20250903_071928_add_issue_refs.rs b/jupiter/src/migration/m20250903_071928_add_issue_refs.rs new file mode 100644 index 000000000..3fcedf3aa --- /dev/null +++ b/jupiter/src/migration/m20250903_071928_add_issue_refs.rs @@ -0,0 +1,76 @@ +use sea_orm::{sea_query::extension::postgres::Type, DatabaseBackend, EnumIter, Iterable}; +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let backend = manager.get_database_backend(); + + match backend { + DatabaseBackend::Postgres => { + manager + .create_type( + Type::create() + .as_enum(ReferenceTypeEnum) + .values(ReferenceType::iter()) + .to_owned(), + ) + .await?; + + manager + .get_connection() + .execute_unprepared( + r#"ALTER TYPE conv_type_enum ADD VALUE IF NOT EXISTS 'mention';"#, + ) + .await?; + } + DatabaseBackend::MySql | DatabaseBackend::Sqlite => {} + } + + manager + .create_table( + table_auto(IssueMrReferences::Table) + .if_not_exists() + .col(string(IssueMrReferences::SourceId)) + .col(string(IssueMrReferences::TargetId)) + .col(enumeration( + IssueMrReferences::ReferenceType, + Alias::new("reference_type_enum"), + ReferenceType::iter(), + )) + .primary_key( + Index::create() + .col(IssueMrReferences::SourceId) + .col(IssueMrReferences::TargetId), + ) + .to_owned(), + ) + .await?; + Ok(()) + } + + async fn down(&self, _: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} + +#[derive(DeriveIden)] +enum IssueMrReferences { + Table, + SourceId, + TargetId, + ReferenceType, +} + +#[derive(DeriveIden)] +struct ReferenceTypeEnum; + +#[derive(Iden, EnumIter)] +pub enum ReferenceType { + Mention, + BuildRelates, + Blocks, +} diff --git a/jupiter/src/migration/mod.rs b/jupiter/src/migration/mod.rs index ec067e545..df4fa1a54 100644 --- a/jupiter/src/migration/mod.rs +++ b/jupiter/src/migration/mod.rs @@ -54,6 +54,7 @@ mod m20250821_083749_add_checks; mod m20250828_092459_remove_gpg_table; mod m20250828_092729_create_standalone_table; mod m20250903_013904_create_task_table; +mod m20250903_071928_add_issue_refs; /// Creates a primary key column definition with big integer type. /// /// # Arguments @@ -95,6 +96,7 @@ impl MigratorTrait for Migrator { Box::new(m20250828_092459_remove_gpg_table::Migration), Box::new(m20250828_092729_create_standalone_table::Migration), Box::new(m20250903_013904_create_task_table::Migration), + Box::new(m20250903_071928_add_issue_refs::Migration), ] } } diff --git a/jupiter/src/storage/issue_storage.rs b/jupiter/src/storage/issue_storage.rs index 836d09267..e9a931893 100644 --- a/jupiter/src/storage/issue_storage.rs +++ b/jupiter/src/storage/issue_storage.rs @@ -1,13 +1,14 @@ use std::collections::HashMap; use std::ops::Deref; +use callisto::sea_orm_active_enums::ReferenceTypeEnum; use sea_orm::prelude::Expr; use sea_orm::{ ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, JoinType, PaginatorTrait, QueryFilter, QuerySelect, RelationTrait, Set, TransactionTrait, }; -use callisto::{item_assignees, item_labels, label, mega_conversation, mega_issue}; +use callisto::{issue_mr_references, item_assignees, item_labels, label, mega_conversation, mega_issue}; use common::errors::MegaError; use common::model::Pagination; @@ -350,6 +351,28 @@ impl IssueStorage { Ok(()) } + + pub async fn add_reference( + &self, + source_id: &str, + target_id: &str, + reference_type: ReferenceTypeEnum, + ) -> Result { + let issue_ref = issue_mr_references::Model { + source_id: source_id.to_owned(), + target_id: target_id.to_owned(), + reference_type, + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + }; + + let res = issue_ref + .into_active_model() + .insert(self.get_connection()) + .await?; + + Ok(res) + } } fn filter_by_author(cond: Condition, author: Option) -> Condition { diff --git a/mono/src/api/api_common/comment.rs b/mono/src/api/api_common/comment.rs new file mode 100644 index 000000000..993c7486b --- /dev/null +++ b/mono/src/api/api_common/comment.rs @@ -0,0 +1,75 @@ +use std::collections::HashSet; + +use axum::{extract::State, Json}; +use regex::Regex; + +use callisto::sea_orm_active_enums::{ConvTypeEnum, ReferenceTypeEnum}; +use common::model::CommonResult; + +use crate::api::error::ApiError; +use crate::api::oauth::model::LoginUser; +use crate::api::MonoApiServiceState; + +pub fn parse_data_id(comment: &str) -> HashSet { + let data_id = Regex::new(r#"data-id="([A-Za-z0-9]+)""#).unwrap(); + let links: HashSet = data_id + .captures_iter(comment) + .map(|cap| cap[1].to_string()) + .collect(); + links +} + +pub async fn check_comment_ref( + user: LoginUser, + state: State, + comment: &str, + source_link: &str, +) -> Result>, ApiError> { + let links = parse_data_id(comment); + let username = user.username; + for ref_link in links { + state + .issue_stg() + .add_reference(source_link, &ref_link, ReferenceTypeEnum::Mention) + .await?; + state + .conv_stg() + .add_conversation( + &ref_link, + &username, + Some(format!("{username} mentioned this on")), + ConvTypeEnum::Mention, + ) + .await?; + } + + Ok(Json(CommonResult::success(None))) +} + +#[cfg(test)] +mod test { + use std::collections::HashSet; + + use crate::api::api_common::comment::parse_data_id; + + #[test] + pub fn test_parse_data_id_from_comment() { + let single_ref = r#"

$HDQL6ATY

"#; + let data_id = parse_data_id(single_ref); + assert_eq!( + data_id.iter().next(), + Some(String::from("HDQL6ATY")).as_ref() + ); + + let normal_comment = r#"

This is a normal comment without reference

"#; + let data_id = parse_data_id(normal_comment); + assert_eq!(data_id.iter().next(), None); + + let multi_referene = r#"

multireference?? $PIXLXMS9 $PIXLXMS9 $ZE710J7U

"#; + let data_id = parse_data_id(multi_referene); + assert_eq!( + data_id, + HashSet::from([String::from("PIXLXMS9"), String::from("ZE710J7U")]) + ); + } +} diff --git a/mono/src/api/api_common/mod.rs b/mono/src/api/api_common/mod.rs index 00bf63bfc..86b5274ab 100644 --- a/mono/src/api/api_common/mod.rs +++ b/mono/src/api/api_common/mod.rs @@ -1,2 +1,3 @@ +pub mod comment; pub mod label_assignee; pub mod model; diff --git a/mono/src/api/conversation/mod.rs b/mono/src/api/conversation/mod.rs index 19438d8b3..4ac0d1866 100644 --- a/mono/src/api/conversation/mod.rs +++ b/mono/src/api/conversation/mod.rs @@ -98,6 +98,7 @@ pub enum ConvType { Reopen, Label, Assignee, + Mention, } impl From for ConvType { @@ -116,6 +117,7 @@ impl From for ConvType { ConvTypeEnum::Reopen => ConvType::Reopen, ConvTypeEnum::Label => ConvType::Label, ConvTypeEnum::Assignee => ConvType::Assignee, + ConvTypeEnum::Mention => ConvType::Mention } } } diff --git a/mono/src/api/issue/issue_router.rs b/mono/src/api/issue/issue_router.rs index 59e5bde0e..74a93e05e 100644 --- a/mono/src/api/issue/issue_router.rs +++ b/mono/src/api/issue/issue_router.rs @@ -200,17 +200,17 @@ async fn save_comment( Path(link): Path, state: State, Json(payload): Json, -) -> Result>, ApiError> { +) -> Result>, ApiError> { state .conv_stg() .add_conversation( &link, &user.username, - Some(payload.content), + Some(payload.content.clone()), ConvTypeEnum::Comment, ) .await?; - Ok(Json(CommonResult::success(None))) + api_common::comment::check_comment_ref(user, state, &payload.content, &link).await } /// Update issue related labels diff --git a/mono/src/api/mr/mr_router.rs b/mono/src/api/mr/mr_router.rs index 8ffd139e1..66fac5ff5 100644 --- a/mono/src/api/mr/mr_router.rs +++ b/mono/src/api/mr/mr_router.rs @@ -418,7 +418,7 @@ async fn save_comment( Path(link): Path, state: State, Json(payload): Json, -) -> Result>, ApiError> { +) -> Result>, ApiError> { let res = state.mr_stg().get_mr(&link).await?; let model = res.ok_or(MegaError::with_message("Not Found"))?; state @@ -426,11 +426,11 @@ async fn save_comment( .add_conversation( &model.link, &user.username, - Some(payload.content), + Some(payload.content.clone()), ConvTypeEnum::Comment, ) .await?; - Ok(Json(CommonResult::success(None))) + api_common::comment::check_comment_ref(user, state, &payload.content, &link).await } /// Edit MR title