From df3ead94fe02eef6ac4c0f5b72492720ec51be97 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Wed, 27 Aug 2025 18:24:13 +0800 Subject: [PATCH] feat:(UI) added merge box API and update generated.ts --- ceres/src/merge_checker/mod.rs | 26 ++++ jupiter/src/storage/mr_storage.rs | 11 ++ mono/src/api/mr/mod.rs | 68 +++++++++- mono/src/api/mr/mr_router.rs | 46 ++++++- mono/src/server/http_server.rs | 2 +- moon/packages/types/generated.ts | 216 ++++++++++++++++++++++++++++++ 6 files changed, 362 insertions(+), 7 deletions(-) diff --git a/ceres/src/merge_checker/mod.rs b/ceres/src/merge_checker/mod.rs index 40e629e4f..99926e070 100644 --- a/ceres/src/merge_checker/mod.rs +++ b/ceres/src/merge_checker/mod.rs @@ -30,6 +30,32 @@ pub enum CheckType { CodeReview, } +impl CheckType { + pub fn display_name(&self) -> &'static str { + match self { + CheckType::GpgSignature => "Gpg signature", + CheckType::BranchProtection => "Branch protection", + CheckType::CommitMessage => "Commit message", + CheckType::MrSync => "Mr sync", + CheckType::MergeConflict => "Merge conflict", + CheckType::CiStatus => "Ci status", + CheckType::CodeReview => "Code review", + } + } + + pub fn description(&self) -> &'static str { + match self { + CheckType::GpgSignature => "Verify whether the commit has a valid GPG signature and the key is trusted", + CheckType::BranchProtection => "Ensure the merge target complies with branch protection policies, such as no direct merges to main and requiring squash or rebase", + CheckType::CommitMessage => "Verify whether the commit message follows Conventional Commits or the internal agreed-upon format", + CheckType::MrSync => "Ensure the MR is based on the latest commit of the target branch and determine whether a rebase is required", + CheckType::MergeConflict => "The pull request must not have any unresolved merge conflicts", + CheckType::CiStatus => "Verify that all required continuous integration pipelines have passed", + CheckType::CodeReview => "Ensure the required reviewers have approved the merge request", + } + } +} + impl From for CheckType { fn from(value: CheckTypeEnum) -> Self { match value { diff --git a/jupiter/src/storage/mr_storage.rs b/jupiter/src/storage/mr_storage.rs index f1138c587..0875ede8b 100644 --- a/jupiter/src/storage/mr_storage.rs +++ b/jupiter/src/storage/mr_storage.rs @@ -298,4 +298,15 @@ impl MrStorage { .await?; Ok(()) } + + pub async fn get_check_result( + &self, + mr_link: &str, + ) -> Result, MegaError> { + let models = check_result::Entity::find() + .filter(check_result::Column::MrLink.eq(mr_link)) + .all(self.get_connection()) + .await?; + Ok(models) + } } diff --git a/mono/src/api/mr/mod.rs b/mono/src/api/mr/mod.rs index 3ba8ecd0c..d5a9a38c9 100644 --- a/mono/src/api/mr/mod.rs +++ b/mono/src/api/mr/mod.rs @@ -1,13 +1,13 @@ use std::str::FromStr; -use ceres::model::mr::MrDiffFile; +use ceres::{merge_checker::CheckType, model::mr::MrDiffFile}; use jupiter::model::mr_dto::MRDetails; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use utoipa::ToSchema; use uuid::Uuid; use crate::api::{conversation::ConversationItem, label::LabelItem}; -use callisto::sea_orm_active_enums::MergeStatusEnum; +use callisto::{check_result, sea_orm_active_enums::MergeStatusEnum}; use common::model::CommonPage; use neptune::model::diff_model::DiffItem; @@ -131,7 +131,7 @@ impl From for MrFilesRes { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)] pub enum MergeStatus { Open, Merged, @@ -157,3 +157,63 @@ impl From for MergeStatusEnum { } } } + +#[derive(Serialize, ToSchema)] +pub struct MergeBoxRes { + pub merge_requirements: Option, +} + +impl MergeBoxRes { + pub fn from_condition(conditions: Vec) -> Self { + let mut state = RequirementsState::MERGEABLE; + for cond in &conditions { + if cond.result == ConditionResult::FAILED { + state = RequirementsState::UNMERGEABLE + } + } + MergeBoxRes { + merge_requirements: Some(MergeRequirements { conditions, state }), + } + } +} + +#[derive(Serialize, ToSchema)] +pub struct MergeRequirements { + pub conditions: Vec, + pub state: RequirementsState, +} + +#[derive(Serialize, ToSchema)] +pub struct Condition { + #[serde(rename = "type")] + pub condition_type: CheckType, + pub display_name: String, + pub description: String, + pub message: String, + pub result: ConditionResult, +} + +impl From for Condition { + fn from(value: check_result::Model) -> Self { + let check_type: CheckType = value.check_type_code.into(); + Self { + condition_type: check_type.clone(), + display_name: check_type.clone().display_name().to_string(), + description: check_type.description().to_string(), + message: value.message, + result: ConditionResult::PASSED, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)] +pub enum RequirementsState { + UNMERGEABLE, + MERGEABLE, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)] +pub enum ConditionResult { + FAILED, + PASSED, +} diff --git a/mono/src/api/mr/mr_router.rs b/mono/src/api/mr/mr_router.rs index c64375830..bcc3ce9dc 100644 --- a/mono/src/api/mr/mr_router.rs +++ b/mono/src/api/mr/mr_router.rs @@ -5,7 +5,6 @@ use axum::{ extract::{Path, State}, Json, }; -use jupiter::service::mr_service::MRService; use utoipa_axum::{router::OpenApiRouter, routes}; use callisto::sea_orm_active_enums::{ConvTypeEnum, MergeStatusEnum}; @@ -13,6 +12,7 @@ use common::{ errors::MegaError, model::{CommonPage, CommonResult, PageParams}, }; +use jupiter::service::mr_service::MRService; use crate::api::{ api_common::{ @@ -22,7 +22,7 @@ use crate::api::{ conversation::ContentPayload, issue::ItemRes, label::LabelUpdatePayload, - mr::{FilesChangedList, MRDetailRes, MrFilesRes, MuiTreeNode}, + mr::{Condition, FilesChangedList, MRDetailRes, MergeBoxRes, MrFilesRes, MuiTreeNode}, oauth::model::LoginUser, }; use crate::api::{mr::FilesChangedPage, MonoApiServiceState}; @@ -35,6 +35,7 @@ pub fn routers() -> OpenApiRouter { .routes(routes!(fetch_mr_list)) .routes(routes!(mr_detail)) .routes(routes!(merge)) + .routes(routes!(merge_box)) .routes(routes!(merge_no_auth)) .routes(routes!(close_mr)) .routes(routes!(reopen_mr)) @@ -359,6 +360,47 @@ async fn mr_files_list( Ok(Json(CommonResult::success(Some(res)))) } +/// Get Merge Box to check merge status +#[utoipa::path( + get, + params( + ("link", description = "MR link"), + ), + path = "/{link}/merge-box", + responses( + (status = 200, body = CommonResult, content_type = "application/json") + ), + tag = MR_TAG +)] +#[axum::debug_handler] +async fn merge_box( + Path(link): Path, + state: State, +) -> Result>, ApiError> { + let mr = state + .mr_stg() + .get_mr(&link) + .await? + .ok_or(MegaError::with_message("MR Not Found"))?; + + let res = match mr.status { + MergeStatusEnum::Open => { + let check_res: Vec = state + .mr_stg() + .get_check_result(&link) + .await? + .into_iter() + .map(|m| m.into()) + .collect(); + MergeBoxRes::from_condition(check_res) + } + MergeStatusEnum::Merged | MergeStatusEnum::Closed => MergeBoxRes { + merge_requirements: None, + }, + }; + Ok(Json(CommonResult::success(Some(res)))) +} + /// Add new comment on Merge Request #[utoipa::path( post, diff --git a/mono/src/server/http_server.rs b/mono/src/server/http_server.rs index f3ca7f842..54b227189 100644 --- a/mono/src/server/http_server.rs +++ b/mono/src/server/http_server.rs @@ -109,7 +109,7 @@ pub async fn app(storage: Storage, host: String, port: u16) -> Router { let origins: Vec = oauth_config .allowed_cors_origins .into_iter() - .map(|x| x.parse::().unwrap()) + .map(|x| x.trim().parse::().unwrap()) .collect(); // add RequestDecompressionLayer for handle gzip encode diff --git a/moon/packages/types/generated.ts b/moon/packages/types/generated.ts index f9629e39f..c26ec2737 100644 --- a/moon/packages/types/generated.ts +++ b/moon/packages/types/generated.ts @@ -3045,6 +3045,16 @@ export type AssigneeUpdatePayload = { link: string } +export enum CheckType { + GpgSignature = 'GpgSignature', + BranchProtection = 'BranchProtection', + CommitMessage = 'CommitMessage', + MrSync = 'MrSync', + MergeConflict = 'MergeConflict', + CiStatus = 'CiStatus', + CodeReview = 'CodeReview' +} + export type CommonPageDiffItem = { items: { data: string @@ -3126,6 +3136,12 @@ export type CommonResultFilesChangedPage = { req_result: boolean } +export type CommonResultHashMapStringBool = { + data?: Record + err_message: string + req_result: boolean +} + export type CommonResultIssueDetailRes = { data?: { assignees: string[] @@ -3174,6 +3190,14 @@ export type CommonResultMRDetailRes = { req_result: boolean } +export type CommonResultMergeBoxRes = { + data?: { + merge_requirements?: null | MergeRequirements + } + err_message: string + req_result: boolean +} + export type CommonResultString = { data?: string err_message: string @@ -3189,6 +3213,21 @@ export type CommonResultTreeResponse = { req_result: boolean } +export type CommonResultVecGpgKey = { + data?: { + /** @format date-time */ + created_at: string + /** @format date-time */ + expires_at?: string | null + fingerprint: string + key_id: string + /** @format int64 */ + user_id: number + }[] + err_message: string + req_result: boolean +} + export type CommonResultVecIssueSuggestions = { data?: { /** @format int64 */ @@ -3265,6 +3304,19 @@ export type CommonResultBool = { req_result: boolean } +export type Condition = { + description: string + display_name: string + message: string + result: ConditionResult + type: CheckType +} + +export enum ConditionResult { + FAILED = 'FAILED', + PASSED = 'PASSED' +} + export type ContentPayload = { content: string } @@ -3328,6 +3380,17 @@ export type FilesChangedPage = { page: CommonPageDiffItem } +export type GpgKey = { + /** @format date-time */ + created_at: string + /** @format date-time */ + expires_at?: string | null + fingerprint: string + key_id: string + /** @format int64 */ + user_id: number +} + export type IssueDetailRes = { assignees: string[] conversations: ConversationItem[] @@ -3436,6 +3499,15 @@ export type MRDetailRes = { title: string } +export type MergeBoxRes = { + merge_requirements?: null | MergeRequirements +} + +export type MergeRequirements = { + conditions: Condition[] + state: RequirementsState +} + export enum MergeStatus { Open = 'Open', Merged = 'Merged', @@ -3454,6 +3526,14 @@ export type MuiTreeNode = { label: string } +export type NewGpgRequest = { + /** @format int32 */ + expires_days?: number | null + gpg_content: string + /** @format int64 */ + user_id: number +} + export type NewIssue = { description: string title: string @@ -3509,6 +3589,17 @@ export type ReactionRequest = { content: string } +export type RemoveGpgRequest = { + key_id: string + /** @format int64 */ + user_id: number +} + +export enum RequirementsState { + UNMERGEABLE = 'UNMERGEABLE', + MERGEABLE = 'MERGEABLE' +} + export type ShowResponse = { description_html: string /** @format int32 */ @@ -4703,6 +4794,12 @@ export type PostApiConversationReactionsData = CommonResultString export type PostApiCreateFileData = CommonResultString +export type PostApiGpgAddData = CommonResultString + +export type GetApiGpgListByIdData = CommonResultVecGpgKey + +export type DeleteApiGpgRemoveData = CommonResultString + export type PostApiIssueAssigneesData = CommonResultString export type GetApiIssueIssueSuggesterParams = { @@ -4760,12 +4857,16 @@ export type GetApiMrFilesListData = CommonResultVecMrFilesRes export type PostApiMrMergeData = CommonResultString +export type GetApiMrMergeBoxData = CommonResultMergeBoxRes + export type PostApiMrMergeNoAuthData = CommonResultString export type PostApiMrReopenData = CommonResultString export type PostApiMrTitleData = CommonResultString +export type GetApiMrVerifySignatureData = CommonResultHashMapStringBool + export type GetApiOrganizationsNotesSyncStateData = ShowResponse export type PatchApiOrganizationsNotesSyncStateData = any @@ -13242,6 +13343,76 @@ export class Api extends HttpClient { + const base = 'POST:/api/v1/gpg/add' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: () => dataTaggedQueryKey([base]), + request: (data: NewGpgRequest, params: RequestParams = {}) => + this.request({ + path: `/api/v1/gpg/add`, + method: 'POST', + body: data, + type: ContentType.Json, + ...params + }) + } + }, + + /** + * No description + * + * @tags gpg-key + * @name GetApiGpgListById + * @request GET:/api/v1/gpg/list/{id} + */ + getApiGpgListById: () => { + const base = 'GET:/api/v1/gpg/list/{id}' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: (id: number) => dataTaggedQueryKey([base, id]), + request: (id: number, params: RequestParams = {}) => + this.request({ + path: `/api/v1/gpg/list/${id}`, + method: 'GET', + ...params + }) + } + }, + + /** + * No description + * + * @tags gpg-key + * @name DeleteApiGpgRemove + * @request DELETE:/api/v1/gpg/remove + */ + deleteApiGpgRemove: () => { + const base = 'DELETE:/api/v1/gpg/remove' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: () => dataTaggedQueryKey([base]), + request: (data: RemoveGpgRequest, params: RequestParams = {}) => + this.request({ + path: `/api/v1/gpg/remove`, + method: 'DELETE', + body: data, + type: ContentType.Json, + ...params + }) + } + }, + /** * No description * @@ -13823,6 +13994,29 @@ export class Api extends HttpClient { + const base = 'GET:/api/v1/mr/{link}/merge-box' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: (link: string) => dataTaggedQueryKey([base, link]), + request: (link: string, params: RequestParams = {}) => + this.request({ + path: `/api/v1/mr/${link}/merge-box`, + method: 'GET', + ...params + }) + } + }, + /** * No description * @@ -13895,6 +14089,28 @@ It's for local testing purposes. } }, + /** + * No description + * + * @tags merge_request + * @name GetApiMrVerifySignature + * @request GET:/api/v1/mr/{link}/verify-signature + */ + getApiMrVerifySignature: () => { + const base = 'GET:/api/v1/mr/{link}/verify-signature' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: (link: string) => dataTaggedQueryKey([base, link]), + request: (link: string, params: RequestParams = {}) => + this.request({ + path: `/api/v1/mr/${link}/verify-signature`, + method: 'GET', + ...params + }) + } + }, + /** * No description *