diff --git a/ceres/src/api_service/mod.rs b/ceres/src/api_service/mod.rs index 58b0238b3..d0de94aa8 100644 --- a/ceres/src/api_service/mod.rs +++ b/ceres/src/api_service/mod.rs @@ -206,33 +206,37 @@ pub trait ApiHandler: Send + Sync { /// # Errors /// /// Returns a `GitError` if the path does not exist. - async fn search_tree_for_update(&self, path: &Path) -> Result<(Vec, Tree), GitError> { + async fn search_tree_for_update(&self, path: &Path) -> Result>, GitError> { + // strip repo root prefix let relative_path = self.strip_relative(path)?; let root_tree = self.get_root_tree().await; - let mut search_tree = root_tree.clone(); - let mut update_tree = vec![root_tree]; + + // init state + let mut current_tree = Arc::new(root_tree.clone()); + let mut update_chain = vec![Arc::new(root_tree)]; for component in relative_path.components() { // root tree already found if component != Component::RootDir { let target_name = component.as_os_str().to_str().unwrap(); - let search_res = search_tree + + // lookup child + let search_res = current_tree .tree_items .iter() - .find(|x| x.name == target_name); - - if let Some(search_res) = search_res { - let res = self.get_tree_by_hash(&search_res.id.to_string()).await; - search_tree = res.clone(); - update_tree.push(res); - } else { - return Err(GitError::CustomError( - "Path not exist, please create path first!".to_string(), - )); - } + .find(|x| x.name == target_name) + .ok_or_else(|| { + GitError::CustomError(format!( + "Path '{}' not exist, please create path first!", + target_name + )) + })?; + // fetch next tree + current_tree = Arc::new(self.get_tree_by_hash(&search_res.id.to_string()).await); + update_chain.push(current_tree.clone()); } } - Ok((update_tree, search_tree)) + Ok(update_chain) } /// Searches for a tree by a given path. diff --git a/ceres/src/api_service/mono_api_service.rs b/ceres/src/api_service/mono_api_service.rs index 25c4d7646..b7b1a3ed4 100644 --- a/ceres/src/api_service/mono_api_service.rs +++ b/ceres/src/api_service/mono_api_service.rs @@ -38,13 +38,14 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::sync::Arc; use crate::api_service::ApiHandler; use crate::model::git::CreateFileInfo; use crate::model::mr::MrDiffFile; use async_trait::async_trait; use callisto::sea_orm_active_enums::ConvTypeEnum; -use callisto::{mega_blob, mega_mr, mega_tree, raw_blob}; +use callisto::{mega_mr, mega_tree}; use common::errors::MegaError; use common::model::Pagination; use jupiter::storage::base_storage::StorageConnector; @@ -63,6 +64,16 @@ pub struct MonoApiService { pub storage: Storage, } +pub struct TreeUpdateResult { + pub updated_trees: Vec, + pub ref_updates: Vec, +} + +pub enum RefUpdate { + Update { path: String, tree_id: SHA1 }, + Delete { path: String }, +} + #[async_trait] impl ApiHandler for MonoApiService { fn get_context(&self) -> Storage { @@ -80,16 +91,15 @@ impl ApiHandler for MonoApiService { /// Returns `Ok(())` on success, or a `GitError` on failure. async fn create_monorepo_file(&self, file_info: CreateFileInfo) -> Result<(), GitError> { let storage = self.storage.mono_storage(); - let path = PathBuf::from(file_info.path); + let path = PathBuf::from(&file_info.path); let mut save_trees = vec![]; - // Search for the tree to update and get its tree items - let (update_trees, search_tree) = self.search_tree_for_update(&path).await?; - let mut t_items = search_tree.tree_items; + let mut update_chain = self.search_tree_for_update(&path).await?; + let mut target_items = update_chain.pop().unwrap().tree_items.clone(); // Create a new tree item based on whether it's a directory or file - let new_item = if file_info.is_directory { - if t_items + let (new_item, blob) = if file_info.is_directory { + if target_items .iter() .any(|x| x.mode == TreeItemMode::Tree && x.name == file_info.name) { @@ -101,53 +111,51 @@ impl ApiHandler for MonoApiService { id: blob.id, name: String::from(".gitkeep"), }; - let child_tree = Tree::from_tree_items(vec![tree_item]).unwrap(); - save_trees.push(child_tree.clone()); - TreeItem { - mode: TreeItemMode::Tree, - id: child_tree.id, - name: file_info.name.clone(), - } + let new_dir_tree = Tree::from_tree_items(vec![tree_item]).unwrap(); + save_trees.push(new_dir_tree.clone()); + ( + TreeItem { + mode: TreeItemMode::Tree, + id: new_dir_tree.id, + name: file_info.name.clone(), + }, + blob, + ) } else { - let content = file_info.content.unwrap(); - let blob = Blob::from_content(&content); - let mega_blob: mega_blob::ActiveModel = Into::::into(&blob).into(); - let raw_blob: raw_blob::ActiveModel = - Into::::into(blob.clone()).into(); - - storage.batch_save_model(vec![mega_blob]).await.unwrap(); - storage.batch_save_model(vec![raw_blob]).await.unwrap(); - TreeItem { - mode: TreeItemMode::Blob, - id: blob.id, - name: file_info.name.clone(), - } + let blob = Blob::from_content(&file_info.content.clone().unwrap()); + ( + TreeItem { + mode: TreeItemMode::Blob, + id: blob.id, + name: file_info.name.clone(), + }, + blob, + ) }; - // Add the new item to the tree items and create a new tree - t_items.push(new_item); - let p_tree = Tree::from_tree_items(t_items).unwrap(); - // Create a commit for the new tree - let refs = storage.get_ref("/").await.unwrap().unwrap(); - let commit = Commit::from_tree_id( - p_tree.id, - vec![SHA1::from_str(&refs.ref_commit_hash).unwrap()], - &format!("\ncreate file {} commit", file_info.name), - ); + // Add the new item to the tree items and create a new tree + target_items.push(new_item); + let target_tree = Tree::from_tree_items(target_items).unwrap(); // Update the parent tree with the new commit - let commit_id = self.update_parent_tree(path, update_trees, commit).await?; - save_trees.push(p_tree); + let update_result = self.build_result_by_chain(path, update_chain, target_tree.id)?; + save_trees.push(target_tree); + + let new_commit_id = self + .apply_update_result(&update_result, &file_info.commit_msg()) + .await?; + + storage.save_mega_blobs(vec![&blob], &new_commit_id).await?; let save_trees: Vec = save_trees .into_iter() .map(|save_t| { let mut tree_model: mega_tree::Model = save_t.into(); - tree_model.commit_id.clone_from(&commit_id); + tree_model.commit_id.clone_from(&new_commit_id); tree_model.into() }) .collect(); - storage.batch_save_model(save_trees).await.unwrap(); + storage.batch_save_model(save_trees).await?; Ok(()) } @@ -269,7 +277,7 @@ impl ApiHandler for MonoApiService { } impl MonoApiService { - pub async fn merge_mr(&self, username: &str, mr: mega_mr::Model) -> Result<(), MegaError> { + pub async fn merge_mr(&self, username: &str, mr: mega_mr::Model) -> Result<(), GitError> { let storage = self.storage.mono_storage(); let refs = storage.get_ref(&mr.path).await.unwrap().unwrap(); @@ -284,13 +292,10 @@ impl MonoApiService { if mr.path != "/" { let path = PathBuf::from(mr.path.clone()); // because only parent tree is needed so we skip current directory - let (tree_vec, _) = self - .search_tree_for_update(path.parent().unwrap()) - .await - .unwrap(); - self.update_parent_tree(path, tree_vec, commit) - .await - .unwrap(); + let update_chain = self.search_tree_for_update(path.parent().unwrap()).await?; + let result = self.build_result_by_chain(path, update_chain, commit.tree_id)?; + self.apply_update_result(&result, "mr merge generated commit") + .await?; // remove refs start with path except mr type storage.remove_none_mr_refs(&mr.path).await.unwrap(); // TODO: self.clean_dangling_commits().await; @@ -308,88 +313,113 @@ impl MonoApiService { .await .unwrap(); } else { - return Err(MegaError::with_message("ref hash conflict")); + return Err(GitError::CustomError("ref hash conflict".to_owned())); } Ok(()) } - async fn update_parent_tree( + /// Traverse parent trees and update them with the new commit's tree hash. + /// This function only prepares updated trees and optionally a new parent commit. + pub fn build_result_by_chain( &self, mut path: PathBuf, - mut tree_vec: Vec, - commit: Commit, - ) -> Result { - let storage = self.storage.mono_storage(); - let mut save_trees = Vec::new(); - let mut p_commit_id = String::new(); + mut update_chain: Vec>, + mut updated_tree_hash: SHA1, + ) -> Result { + let mut updated_trees = Vec::new(); + let mut ref_updates = Vec::new(); - let mut target_hash = commit.tree_id; - - while let Some(mut tree) = tree_vec.pop() { + while let Some(tree) = update_chain.pop() { let cloned_path = path.clone(); - let name = cloned_path.file_name().unwrap().to_str().unwrap(); + let name = cloned_path + .file_name() + .and_then(|n| n.to_str()) + .ok_or_else(|| GitError::CustomError("Invalid path".into()))?; path.pop(); - let index = tree.tree_items.iter().position(|x| x.name == name).unwrap(); - tree.tree_items[index].id = target_hash; - let new_tree = Tree::from_tree_items(tree.tree_items).unwrap(); - target_hash = new_tree.id; - - let model: mega_tree::Model = new_tree.into(); - save_trees.push(model); - - let p_ref = storage.get_ref(path.to_str().unwrap()).await.unwrap(); - if let Some(mut p_ref) = p_ref { - if path == Path::new("/") { - let p_commit = Commit::new( - commit.author.clone(), - commit.committer.clone(), - target_hash, - vec![SHA1::from_str(&p_ref.ref_commit_hash).unwrap()], - &commit.message, - ); - p_commit_id = p_commit.id.to_string(); - // update p_ref - p_ref.ref_commit_hash = p_commit.id.to_string(); - p_ref.ref_tree_hash = target_hash.to_string(); - storage.update_ref(p_ref).await.unwrap(); - storage.save_mega_commits(vec![p_commit]).await.unwrap(); - } else { - storage.remove_ref(p_ref).await.unwrap(); + let new_tree = self.update_tree_hash(tree, name, updated_tree_hash)?; + updated_tree_hash = new_tree.id; + updated_trees.push(new_tree); + + let path_str = path.to_string_lossy().to_string(); + if path == Path::new("/") { + ref_updates.push(RefUpdate::Update { + path: path_str, + tree_id: updated_tree_hash, + }); + } else { + ref_updates.push(RefUpdate::Delete { path: path_str }); + } + } + + Ok(TreeUpdateResult { + updated_trees, + ref_updates, + }) + } + + pub async fn apply_update_result( + &self, + result: &TreeUpdateResult, + commit_msg: &str, + ) -> Result { + let storage = self.storage.mono_storage(); + let mut new_commit_id = String::new(); + + for update in &result.ref_updates { + match update { + RefUpdate::Update { path, tree_id } => { + // update can only be root path + if let Some(mut p_ref) = storage.get_ref(path).await? { + let commit = Commit::from_tree_id( + *tree_id, + vec![SHA1::from_str(&p_ref.ref_commit_hash).unwrap()], + commit_msg, + ); + new_commit_id = commit.id.to_string(); + p_ref.ref_commit_hash = new_commit_id.clone(); + p_ref.ref_tree_hash = tree_id.to_string(); + storage.update_ref(p_ref).await?; + storage.save_mega_commits(vec![commit]).await?; + } + } + RefUpdate::Delete { path } => { + if let Some(p_ref) = storage.get_ref(path).await? { + storage.remove_ref(p_ref).await?; + } } } } - let save_trees: Vec = save_trees + + let save_trees: Vec = result + .updated_trees + .clone() .into_iter() - .map(|mut x| { - p_commit_id.clone_into(&mut x.commit_id); - x.into() + .map(|save_t| { + let mut tree_model: mega_tree::Model = save_t.into(); + tree_model.commit_id.clone_from(&new_commit_id); + tree_model.into() }) .collect(); + storage.batch_save_model(save_trees).await?; - storage.batch_save_model(save_trees).await.unwrap(); - Ok(p_commit_id) + Ok(new_commit_id) } - pub async fn content_diff(&self, mr_link: &str) -> Result, GitError> { - let stg = self.storage.mr_storage(); - let mr = - stg.get_mr(mr_link).await.unwrap().ok_or_else(|| { - GitError::CustomError(format!("Merge request not found: {mr_link}")) - })?; - - let old_blobs = self - .get_commit_blobs(&mr.from_hash) - .await - .map_err(|e| GitError::CustomError(format!("Failed to get old commit blobs: {e}")))?; - let new_blobs = self - .get_commit_blobs(&mr.to_hash) - .await - .map_err(|e| GitError::CustomError(format!("Failed to get new commit blobs: {e}")))?; - - let diff_output = self.get_diff_by_blobs(old_blobs, new_blobs).await?; - - Ok(diff_output) + fn update_tree_hash( + &self, + tree: Arc, + name: &str, + target_hash: SHA1, + ) -> Result { + let index = tree + .tree_items + .iter() + .position(|item| item.name == name) + .ok_or_else(|| GitError::CustomError(format!("Tree item '{}' not found", name)))?; + let mut items = tree.tree_items.clone(); + items[index].id = target_hash; + Tree::from_tree_items(items).map_err(|_| GitError::CustomError("Invalid tree".to_string())) } /// Fetches the content difference for a merge request, paginated by page_id and page_size. diff --git a/ceres/src/model/git.rs b/ceres/src/model/git.rs index 643280938..3d4fe6ce7 100644 --- a/ceres/src/model/git.rs +++ b/ceres/src/model/git.rs @@ -8,7 +8,7 @@ use mercury::internal::object::{ tree::{TreeItem, TreeItemMode}, }; -#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize, ToSchema)] +#[derive(PartialEq, Eq, Debug, Clone, Deserialize, ToSchema)] pub struct CreateFileInfo { /// can be a file or directory pub is_directory: bool, @@ -19,6 +19,22 @@ pub struct CreateFileInfo { pub content: Option, } +impl CreateFileInfo { + pub fn commit_msg(&self) -> String { + if self.is_directory { + format!("\n create new directory {}", self.name) + } else { + format!("\n create new file {}", self.name) + } + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize, ToSchema, Clone)] +pub enum ContentType { + File, + Directory, +} + #[derive(Debug, Deserialize, IntoParams)] pub struct CodePreviewQuery { #[serde(default)] diff --git a/jupiter/src/storage/mono_storage.rs b/jupiter/src/storage/mono_storage.rs index 696dbd994..cf63d25c1 100644 --- a/jupiter/src/storage/mono_storage.rs +++ b/jupiter/src/storage/mono_storage.rs @@ -12,7 +12,7 @@ use common::config::MonoConfig; use common::errors::MegaError; use common::utils::{generate_id, MEGA_BRANCH_NAME}; use mercury::internal::object::{MegaObjectModel, ObjectTrait}; -use mercury::internal::{object::commit::Commit, pack::entry::Entry}; +use mercury::internal::{object::blob::Blob, object::commit::Commit, pack::entry::Entry}; use crate::storage::base_storage::{BaseStorage, StorageConnector}; use crate::storage::commit_binding_storage::CommitBindingStorage; @@ -175,9 +175,17 @@ impl MonoStorage { match model { MegaObjectModel::Commit(commit) => { // Store for binding processing - if let Ok(commit_obj) = mercury::internal::object::commit::Commit::from_bytes(&entry.data, entry.hash) { + if let Ok(commit_obj) = + mercury::internal::object::commit::Commit::from_bytes( + &entry.data, + entry.hash, + ) + { let mut commits = commits_to_process.lock().unwrap(); - commits.push((commit_obj.id.to_string(), commit_obj.author.email.clone())); + commits.push(( + commit_obj.id.to_string(), + commit_obj.author.email.clone(), + )); } git_objects.commits.push(commit.into_active_model()) } @@ -212,16 +220,21 @@ impl MonoStorage { .expect("Failed to unwrap Arc") .into_inner() .unwrap(); - + if !commits_to_process.is_empty() { - self.process_commit_bindings(&commits_to_process, authenticated_username.as_deref()).await?; + self.process_commit_bindings(&commits_to_process, authenticated_username.as_deref()) + .await?; } Ok(()) } /// Process commit author bindings - async fn process_commit_bindings(&self, commits: &[(String, String)], authenticated_username: Option<&str>) -> Result<(), MegaError> { + async fn process_commit_bindings( + &self, + commits: &[(String, String)], + authenticated_username: Option<&str>, + ) -> Result<(), MegaError> { let user_storage = self.user_storage(); let commit_binding_storage = self.commit_binding_storage(); @@ -235,7 +248,10 @@ impl MonoStorage { Some(username.to_string()) } Ok(None) => { - tracing::warn!("Authenticated username {} not found in user table", username); + tracing::warn!( + "Authenticated username {} not found in user table", + username + ); None } Err(e) => { @@ -245,10 +261,13 @@ impl MonoStorage { } } else { // No authenticated username, commit will be anonymous - tracing::info!("No authenticated username available for commit {}", commit_sha); + tracing::info!( + "No authenticated username available for commit {}", + commit_sha + ); None }; - + let is_anonymous = matched_username.is_none(); // Save or update binding @@ -259,7 +278,12 @@ impl MonoStorage { tracing::error!("Failed to save commit binding for {}: {}", commit_sha, e); // Continue processing other commits even if one fails } else { - tracing::info!("Processed binding for commit {} with email {} (anonymous: {})", commit_sha, author_email, is_anonymous); + tracing::info!( + "Processed binding for commit {} with email {} (anonymous: {})", + commit_sha, + author_email, + is_anonymous + ); } } Ok(()) @@ -290,16 +314,41 @@ impl MonoStorage { } pub async fn save_mega_commits(&self, commits: Vec) -> Result<(), MegaError> { - let mega_commits: Vec = - commits.into_iter().map(mega_commit::Model::from).collect(); - let mut save_models = Vec::new(); - for mega_commit in mega_commits { - save_models.push(mega_commit.into_active_model()); - } + let save_models: Vec = commits + .into_iter() + .map(mega_commit::Model::from) + .map(|m| m.into_active_model()) + .collect(); self.batch_save_model(save_models).await.unwrap(); Ok(()) } + pub async fn save_mega_blobs( + &self, + blobs: Vec<&Blob>, + commit_id: &str, + ) -> Result<(), MegaError> { + let mega_blobs: Vec = blobs + .clone() + .into_iter() + .map(mega_blob::Model::from) + .map(|mut m| { + m.commit_id = commit_id.to_owned(); + m.into_active_model() + }) + .collect(); + self.batch_save_model(mega_blobs).await.unwrap(); + + let raw_blobs: Vec = blobs + .into_iter() + .map(raw_blob::Model::from) + .map(|m| m.into_active_model()) + .collect(); + self.batch_save_model(raw_blobs).await.unwrap(); + + Ok(()) + } + pub async fn get_commit_by_hash( &self, hash: &str, diff --git a/jupiter/src/utils/converter.rs b/jupiter/src/utils/converter.rs index 716fcf25d..340bcf0ad 100644 --- a/jupiter/src/utils/converter.rs +++ b/jupiter/src/utils/converter.rs @@ -93,7 +93,7 @@ impl MegaModelConverter { self.mega_blobs .borrow_mut() .insert(blob.id, mega_blob.clone().into()); - let raw_blob: raw_blob::Model = blob.to_owned().into(); + let raw_blob: raw_blob::Model = blob.into(); self.raw_blobs.borrow_mut().insert(blob.id, raw_blob.into()); } } diff --git a/mercury/src/internal/model/blob.rs b/mercury/src/internal/model/blob.rs index 211e8ab1b..a78ea0421 100644 --- a/mercury/src/internal/model/blob.rs +++ b/mercury/src/internal/model/blob.rs @@ -31,13 +31,13 @@ impl From<&Blob> for git_blob::Model { } } -impl From for raw_blob::Model { - fn from(value: Blob) -> Self { +impl From<&Blob> for raw_blob::Model { + fn from(value: &Blob) -> Self { raw_blob::Model { id: generate_id(), sha1: value.id.to_string(), storage_type: StorageTypeEnum::Database, - data: Some(value.data), + data: Some(value.data.clone()), content: None, file_type: None, local_path: None, diff --git a/mercury/src/internal/object/mod.rs b/mercury/src/internal/object/mod.rs index 4eac9a06b..f10557889 100644 --- a/mercury/src/internal/object/mod.rs +++ b/mercury/src/internal/object/mod.rs @@ -79,7 +79,7 @@ impl GitObject { match self { GitObject::Commit(commit) => MegaObjectModel::Commit(commit.into()), GitObject::Tree(tree) => MegaObjectModel::Tree(tree.into()), - GitObject::Blob(blob) => MegaObjectModel::Blob((&blob).into(), blob.into()), + GitObject::Blob(blob) => MegaObjectModel::Blob((&blob).into(), (&blob).into()), GitObject::Tag(tag) => MegaObjectModel::Tag(tag.into()), } } @@ -88,7 +88,7 @@ impl GitObject { match self { GitObject::Commit(commit) => GitObjectModel::Commit(commit.into()), GitObject::Tree(tree) => GitObjectModel::Tree(tree.into()), - GitObject::Blob(blob) => GitObjectModel::Blob((&blob).into(), blob.into()), + GitObject::Blob(blob) => GitObjectModel::Blob((&blob).into(), (&blob).into()), GitObject::Tag(tag) => GitObjectModel::Tag(tag.into()), } } diff --git a/mono/src/api/mr/mr_router.rs b/mono/src/api/mr/mr_router.rs index 613285c96..0eb667369 100644 --- a/mono/src/api/mr/mr_router.rs +++ b/mono/src/api/mr/mr_router.rs @@ -277,7 +277,6 @@ async fn mr_detail( ), tag = MR_TAG )] -#[axum::debug_handler] async fn mr_files_changed_by_page( Path(link): Path, state: State, @@ -347,7 +346,6 @@ async fn mr_files_list( ), tag = MR_TAG )] -#[axum::debug_handler] async fn merge_box( Path(link): Path, state: State,