Skip to content

Commit 124263c

Browse files
authored
feat(mono): Support automatically creating a CL and triggering the build process after editing files. (#1892)
Signed-off-by: Chen-Rong-Zi <1398881912@qq.com>
1 parent 692541c commit 124263c

10 files changed

Lines changed: 327 additions & 3 deletions

File tree

ceres/src/api_service/commit_ops.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ pub async fn traverse_history_commits<T: ApiHandler + ?Sized>(
454454

455455
// Final sort: by committer timestamp descending
456456
let mut result = matched_by_author;
457-
result.sort_by(|a, b| b.committer.timestamp.cmp(&a.committer.timestamp));
457+
result.sort_by_key(|b| std::cmp::Reverse(b.committer.timestamp));
458458
Ok(result)
459459
}
460460

ceres/src/api_service/import_api_service.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ impl ApiHandler for ImportApiService {
469469
commit_id: new_commit_id,
470470
new_oid: new_blob.id.to_string(),
471471
path: payload.path,
472+
cl_link: None,
472473
})
473474
}
474475
}

ceres/src/api_service/mono_api_service.rs

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ use std::{
4545

4646
use api_model::common::Pagination;
4747
use async_trait::async_trait;
48+
use bellatrix::Bellatrix;
4849
use bytes::Bytes;
4950
use callisto::{
51+
entity_ext::generate_link,
5052
mega_cl, mega_refs, mega_tag, mega_tree,
5153
sea_orm_active_enums::{ConvTypeEnum, MergeStatusEnum, QueueFailureTypeEnum, QueueStatusEnum},
5254
};
@@ -89,13 +91,14 @@ use crate::{
8991
ApiHandler, buck_tree_builder::BuckCommitBuilder, cache::GitObjectCache,
9092
state::ProtocolApiState, tree_ops,
9193
},
94+
build_trigger::BuildTriggerService,
9295
model::{
9396
buck::{
9497
CompletePayload, CompleteResponse, DEFAULT_MODE, FileChange,
9598
FileToUpload as ApiFileToUpload, ManifestPayload, ManifestResponse,
9699
},
97100
change_list::{ClDiffFile, UpdateBranchStatusRes},
98-
git::{CreateEntryInfo, EditFilePayload, EditFileResult},
101+
git::{CreateEntryInfo, EditCLMode, EditFilePayload, EditFileResult},
99102
tag::TagInfo,
100103
third_party::{ThirdPartyClient, ThirdPartyRepoTrait},
101104
},
@@ -398,15 +401,34 @@ impl ApiHandler for MonoApiService {
398401
.apply_update_result(&result, &payload.commit_message, None)
399402
.await?;
400403

404+
let username = payload
405+
.author_username
406+
.clone()
407+
.unwrap_or("Anonymous".to_string());
408+
409+
let cl = self
410+
.find_or_create_cl_for_edit(
411+
payload.mode,
412+
&new_commit_id,
413+
&payload.commit_message,
414+
&payload.path,
415+
&username,
416+
)
417+
.await?;
401418
self.storage
402419
.mono_service
403420
.save_blobs(&new_commit_id, vec![new_blob.clone()])
404421
.await?;
405422

423+
if !payload.skip_build {
424+
self.trigger_build_for_cl(&cl.link).await?;
425+
}
426+
406427
Ok(EditFileResult {
407428
commit_id: new_commit_id,
408429
new_oid: new_blob.id.to_string(),
409430
path: payload.path,
431+
cl_link: Some(cl.link),
410432
})
411433
}
412434

@@ -1027,6 +1049,145 @@ impl MonoApiService {
10271049
}
10281050
}
10291051

1052+
async fn create_new_cl(
1053+
&self,
1054+
repo_path: &str,
1055+
commit_message: &str,
1056+
to_hash: &str,
1057+
username: &str,
1058+
) -> Result<mega_cl::Model, GitError> {
1059+
let cl_link = generate_link();
1060+
1061+
let from_hash = self
1062+
.storage
1063+
.mono_storage()
1064+
.get_main_ref(repo_path)
1065+
.await?
1066+
.ok_or_else(|| MegaError::Other("Main ref not found".to_string()))?
1067+
.ref_commit_hash;
1068+
1069+
// Create and return a new CL model
1070+
let cl = self
1071+
.storage
1072+
.cl_storage()
1073+
.new_cl_model(
1074+
repo_path,
1075+
&cl_link,
1076+
commit_message,
1077+
&from_hash,
1078+
to_hash,
1079+
username,
1080+
)
1081+
.await
1082+
.map_err(|e| GitError::CustomError(format!("Failed to create CL: {}", e)))?;
1083+
1084+
Ok(cl)
1085+
}
1086+
1087+
/// Finds or creates a Change List (CL) for file edits.
1088+
/// This method determines whether to create a new CL or reuse an existing one based on the
1089+
/// [`EditCLMode`] passed in `mode`. For example, `EditCLMode::ForceCreate` will always create
1090+
/// a new CL, while `EditCLMode::TryReuse` will attempt to find an open CL for the given user
1091+
/// and path and only create a new one if none exists.
1092+
///
1093+
/// # Arguments
1094+
/// * `mode` - Controls whether to force creation of a new CL (`ForceCreate`) or try to reuse an
1095+
/// existing open CL for the user and path when possible (`TryReuse`).
1096+
/// * `to_hash` - The commit hash representing the new state after the edit.
1097+
/// * `commit_message` - The message describing the changes made in the CL.
1098+
/// * `file_path` - The path of the file being edited.
1099+
/// * `username` - The username of the user performing the edit.
1100+
///
1101+
/// # Returns
1102+
/// A `Result` containing the `mega_cl::Model` representing the found or created CL on success,
1103+
/// or a `GitError` on failure.
1104+
async fn find_or_create_cl_for_edit(
1105+
&self,
1106+
mode: EditCLMode,
1107+
to_hash: &str,
1108+
commit_message: &str,
1109+
file_path: &str,
1110+
username: &str,
1111+
) -> Result<mega_cl::Model, GitError> {
1112+
let path = PathBuf::from(file_path);
1113+
let repo_path = path
1114+
.parent()
1115+
.map(|p| p.to_string_lossy().to_string())
1116+
.unwrap_or_else(|| "/".to_string());
1117+
1118+
let storage = self.storage.cl_storage();
1119+
match mode {
1120+
EditCLMode::ForceCreate => {
1121+
self.create_new_cl(&repo_path, commit_message, to_hash, username)
1122+
.await
1123+
}
1124+
EditCLMode::TryReuse(None) => {
1125+
if let Some(existing_cl) =
1126+
storage
1127+
.get_open_cl_by_path(&repo_path, username)
1128+
.await
1129+
.map_err(|e| GitError::CustomError(format!("Failed to fetch CL: {}", e)))?
1130+
{
1131+
storage
1132+
.update_cl_to_hash(existing_cl.clone(), to_hash)
1133+
.await?;
1134+
Ok(existing_cl)
1135+
} else {
1136+
self.create_new_cl(&repo_path, commit_message, to_hash, username)
1137+
.await
1138+
}
1139+
}
1140+
EditCLMode::TryReuse(Some(link)) => {
1141+
match self.storage.cl_storage().get_cl(&link).await {
1142+
Ok(Some(model)) => {
1143+
storage.update_cl_to_hash(model.clone(), to_hash).await?;
1144+
Ok(model)
1145+
}
1146+
_ => {
1147+
self.create_new_cl(&repo_path, commit_message, to_hash, username)
1148+
.await
1149+
}
1150+
}
1151+
}
1152+
}
1153+
}
1154+
1155+
/// Triggers a build for the specified Change List (CL).
1156+
/// It spawns a background task to handle the build process, ensuring that the main application flow remains responsive.
1157+
/// # Arguments
1158+
/// * `cl_link` - The unique identifier (link) of the CL for which the build is to be triggered.
1159+
///
1160+
/// # Returns
1161+
/// A `Result` indicating success or failure. Returns `Ok(())` if the build was successfully triggered,
1162+
/// or a `GitError` if an error occurred during the process.
1163+
async fn trigger_build_for_cl(&self, cl_link: &str) -> Result<(), GitError> {
1164+
let config = self.storage.config();
1165+
let bellatrix = Bellatrix::new(config.build.clone());
1166+
let cl = self
1167+
.storage
1168+
.cl_storage()
1169+
.get_cl(cl_link)
1170+
.await
1171+
.map_err(|e| GitError::CustomError(format!("Failed to get CL: {}", e)))?
1172+
.ok_or_else(|| GitError::CustomError("CL not found".to_string()))?;
1173+
1174+
let storage = self.storage.clone();
1175+
let git_cache = self.git_object_cache.clone();
1176+
1177+
// Spawn a background task to handle the build process
1178+
tokio::spawn(async move {
1179+
let _ = BuildTriggerService::build_by_context(
1180+
storage,
1181+
git_cache,
1182+
Arc::new(bellatrix),
1183+
cl.into(),
1184+
)
1185+
.await;
1186+
});
1187+
1188+
Ok(())
1189+
}
1190+
10301191
async fn create_annotated_tag_mono(
10311192
&self,
10321193
repo_path: Option<String>,

ceres/src/build_trigger/dispatcher.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ impl BuildDispatcher {
5555
BuildTriggerPayload::Retry(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id),
5656
BuildTriggerPayload::Webhook(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id),
5757
BuildTriggerPayload::Schedule(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id),
58+
BuildTriggerPayload::WebEdit(p) => (&p.cl_link, &p.repo, &p.builds, p.cl_id),
5859
};
5960

6061
let builds: Vec<SerializableBuildInfo> = serde_json::from_value(builds_json.clone())

ceres/src/build_trigger/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod manual_handler;
1414
mod model;
1515
mod ref_resolver;
1616
mod retry_handler;
17+
mod web_edit_handler;
1718

1819
// Export all models from the single model file
1920
pub use model::*;
@@ -24,6 +25,7 @@ use git_push_handler::GitPushHandler;
2425
use manual_handler::ManualHandler;
2526
use retry_handler::RetryHandler;
2627
pub use service::BuildTriggerService;
28+
use web_edit_handler::WebEditHandler;
2729

2830
/// Trait for handling different types of build triggers.
2931
#[async_trait]
@@ -66,6 +68,10 @@ impl TriggerRegistry {
6668
storage.clone(),
6769
git_object_cache.clone(),
6870
)));
71+
registry.register(Box::new(WebEditHandler::new(
72+
storage.clone(),
73+
git_object_cache.clone(),
74+
)));
6975

7076
// Note: Webhook and Schedule handlers are reserved for future implementation
7177
// but not registered yet as they are not part of the current requirements

ceres/src/build_trigger/model.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{collections::HashMap, fmt};
22

3+
use callisto::mega_cl;
34
use chrono::{DateTime, Utc};
45
use serde::{Deserialize, Serialize};
56
use utoipa::ToSchema;
@@ -12,6 +13,7 @@ pub enum BuildTriggerType {
1213
Retry,
1314
Webhook,
1415
Schedule,
16+
WebEdit,
1517
}
1618

1719
impl fmt::Display for BuildTriggerType {
@@ -22,6 +24,7 @@ impl fmt::Display for BuildTriggerType {
2224
BuildTriggerType::Retry => "retry",
2325
BuildTriggerType::Webhook => "webhook",
2426
BuildTriggerType::Schedule => "schedule",
27+
BuildTriggerType::WebEdit => "webedit",
2528
};
2629
write!(f, "{}", s)
2730
}
@@ -144,6 +147,19 @@ pub struct SchedulePayload {
144147
pub cl_id: Option<i64>,
145148
}
146149

150+
#[derive(Debug, Clone, Serialize, Deserialize)]
151+
pub struct WebEditPayload {
152+
pub repo: String,
153+
pub from_hash: String,
154+
pub commit_hash: String,
155+
pub cl_link: String,
156+
#[serde(skip_serializing_if = "Option::is_none")]
157+
pub cl_id: Option<i64>,
158+
pub builds: serde_json::Value,
159+
#[serde(skip_serializing_if = "Option::is_none")]
160+
pub triggered_by: Option<String>,
161+
}
162+
147163
/// Trigger payload - stores context specific to each trigger type
148164
/// This enum is serialized to JSON and stored in database's trigger_payload column
149165
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -154,6 +170,7 @@ pub enum BuildTriggerPayload {
154170
Retry(RetryPayload),
155171
Webhook(WebhookPayload),
156172
Schedule(SchedulePayload),
173+
WebEdit(WebEditPayload),
157174
}
158175

159176
impl BuildTriggerPayload {
@@ -164,6 +181,7 @@ impl BuildTriggerPayload {
164181
BuildTriggerPayload::Retry(p) => &p.repo,
165182
BuildTriggerPayload::Webhook(p) => &p.repo,
166183
BuildTriggerPayload::Schedule(p) => &p.repo,
184+
BuildTriggerPayload::WebEdit(p) => &p.repo,
167185
}
168186
}
169187

@@ -174,6 +192,7 @@ impl BuildTriggerPayload {
174192
BuildTriggerPayload::Retry(p) => &p.commit_hash,
175193
BuildTriggerPayload::Webhook(p) => &p.commit_hash,
176194
BuildTriggerPayload::Schedule(p) => &p.commit_hash,
195+
BuildTriggerPayload::WebEdit(p) => &p.commit_hash,
177196
}
178197
}
179198

@@ -184,6 +203,7 @@ impl BuildTriggerPayload {
184203
BuildTriggerPayload::Retry(p) => &p.cl_link,
185204
BuildTriggerPayload::Webhook(p) => &p.cl_link,
186205
BuildTriggerPayload::Schedule(p) => &p.cl_link,
206+
BuildTriggerPayload::WebEdit(p) => &p.cl_link,
187207
}
188208
}
189209

@@ -194,6 +214,7 @@ impl BuildTriggerPayload {
194214
BuildTriggerPayload::Retry(p) => p.cl_id,
195215
BuildTriggerPayload::Webhook(p) => p.cl_id,
196216
BuildTriggerPayload::Schedule(p) => p.cl_id,
217+
BuildTriggerPayload::WebEdit(p) => p.cl_id,
197218
}
198219
}
199220

@@ -204,6 +225,7 @@ impl BuildTriggerPayload {
204225
BuildTriggerPayload::Retry(p) => Some(&p.triggered_by),
205226
BuildTriggerPayload::Webhook(_) => None,
206227
BuildTriggerPayload::Schedule(_) => None,
228+
BuildTriggerPayload::WebEdit(p) => p.triggered_by.as_deref(),
207229
}
208230
}
209231

@@ -214,6 +236,7 @@ impl BuildTriggerPayload {
214236
BuildTriggerPayload::Retry(p) => &p.from_hash,
215237
BuildTriggerPayload::Webhook(_) => "",
216238
BuildTriggerPayload::Schedule(_) => "",
239+
BuildTriggerPayload::WebEdit(p) => &p.from_hash,
217240
}
218241
}
219242
}
@@ -352,6 +375,25 @@ impl TriggerContext {
352375
}
353376
}
354377

378+
impl From<mega_cl::Model> for TriggerContext {
379+
fn from(cl: mega_cl::Model) -> Self {
380+
TriggerContext {
381+
trigger_type: BuildTriggerType::WebEdit,
382+
trigger_source: TriggerSource::User,
383+
triggered_by: Some(cl.username),
384+
repo_path: cl.path,
385+
from_hash: cl.from_hash,
386+
commit_hash: cl.to_hash,
387+
cl_link: Some(cl.link),
388+
cl_id: Some(cl.id),
389+
params: None,
390+
original_trigger_id: None,
391+
ref_name: None,
392+
ref_type: None,
393+
}
394+
}
395+
}
396+
355397
/// Create trigger request (new RESTful API)
356398
#[derive(Debug, Deserialize, ToSchema)]
357399
pub struct CreateTriggerRequest {

0 commit comments

Comments
 (0)