Skip to content

Commit 9fc0d41

Browse files
authored
feat: impl preview-diff and edit code api (#1568)
Signed-off-by: Ruizhi Huang <[email protected]> fix problems
1 parent 798802d commit 9fc0d41

File tree

5 files changed

+317
-13
lines changed

5 files changed

+317
-13
lines changed

ceres/src/api_service/import_api_service.rs

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ use git_internal::errors::GitError;
1010
use git_internal::hash::SHA1;
1111
use git_internal::internal::object::commit::Commit;
1212
use git_internal::internal::object::tree::{Tree, TreeItem, TreeItemMode};
13+
use git_internal::internal::pack::entry::Entry;
1314

1415
use common::errors::MegaError;
1516
use common::model::TagInfo;
1617
use jupiter::storage::Storage;
1718

1819
use crate::api_service::{ApiHandler, GitObjectCache};
1920
use crate::model::blame::{BlameQuery, BlameResult};
20-
use crate::model::git::CreateEntryInfo;
21+
use crate::model::git::{CreateEntryInfo, EditFilePayload, EditFileResult};
2122
use crate::protocol::repo::Repo;
2223
use callisto::{git_tag, import_refs};
2324
use jupiter::utils::converter::FromGitModel;
@@ -248,14 +249,15 @@ impl ApiHandler for ImportApiService {
248249
if result.iter().any(|t| t.name == tag_name) {
249250
continue;
250251
}
252+
let created_at = r.created_at.and_utc().to_rfc3339();
251253
lightweight_refs.push(TagInfo {
252254
name: tag_name.clone(),
253255
tag_id: r.ref_git_id.clone(),
254256
object_id: r.ref_git_id.clone(),
255257
object_type: "commit".to_string(),
256258
tagger: "".to_string(),
257259
message: "".to_string(),
258-
created_at: chrono::Utc::now().to_rfc3339(),
260+
created_at,
259261
});
260262
}
261263
}
@@ -313,14 +315,15 @@ impl ApiHandler for ImportApiService {
313315
if let Ok(refs) = git_storage.get_ref(self.repo.repo_id).await {
314316
for r in refs {
315317
if r.ref_name == full_ref {
318+
let created_at = r.created_at.and_utc().to_rfc3339();
316319
return Ok(Some(TagInfo {
317320
name: name.clone(),
318321
tag_id: r.ref_git_id.clone(),
319322
object_id: r.ref_git_id.clone(),
320323
object_type: "commit".to_string(),
321324
tagger: "".to_string(),
322325
message: "".to_string(),
323-
created_at: chrono::Utc::now().to_rfc3339(),
326+
created_at,
324327
}));
325328
}
326329
}
@@ -387,6 +390,84 @@ impl ApiHandler for ImportApiService {
387390
"Import directory does not support blame functionality".to_string(),
388391
))
389392
}
393+
394+
/// Save file edit for import repo path
395+
async fn save_file_edit(&self, payload: EditFilePayload) -> Result<EditFileResult, GitError> {
396+
use git_internal::internal::object::blob::Blob;
397+
use git_internal::internal::object::tree::TreeItemMode;
398+
399+
let path = PathBuf::from(&payload.path);
400+
let parent = path
401+
.parent()
402+
.ok_or_else(|| GitError::CustomError("Invalid file path".to_string()))?;
403+
let update_chain = self
404+
.search_tree_for_update(parent)
405+
.await
406+
.map_err(|e| GitError::CustomError(e.to_string()))?;
407+
let parent_tree = update_chain
408+
.last()
409+
.cloned()
410+
.ok_or_else(|| GitError::CustomError("Parent tree not found".to_string()))?;
411+
let name = path
412+
.file_name()
413+
.and_then(|n| n.to_str())
414+
.ok_or_else(|| GitError::CustomError("Invalid file name".to_string()))?;
415+
let _current = parent_tree
416+
.tree_items
417+
.iter()
418+
.find(|x| x.name == name && x.mode == TreeItemMode::Blob)
419+
.ok_or_else(|| GitError::CustomError("[code:404] File not found".to_string()))?;
420+
421+
// Create new blob and rebuild tree up to root
422+
let new_blob = Blob::from_content(&payload.content);
423+
let (updated_trees, new_root_id) =
424+
self.build_updated_trees(path.clone(), update_chain, new_blob.id)?;
425+
426+
// Save commit and objects under import repo tables
427+
let git_storage = self.storage.git_db_storage();
428+
let new_commit_id = {
429+
// Update default branch ref commit with parent = current default commit
430+
let default_ref = git_storage
431+
.get_default_ref(self.repo.repo_id)
432+
.await
433+
.map_err(|e| GitError::CustomError(e.to_string()))?
434+
.ok_or_else(|| GitError::CustomError("Default ref not found".to_string()))?;
435+
let current_commit = git_storage
436+
.get_commit_by_hash(self.repo.repo_id, &default_ref.ref_git_id)
437+
.await
438+
.map_err(|e| GitError::CustomError(e.to_string()))?
439+
.ok_or(GitError::InvalidCommitObject)?;
440+
let parent_id = SHA1::from_str(&current_commit.commit_id).unwrap();
441+
442+
let new_commit =
443+
Commit::from_tree_id(new_root_id, vec![parent_id], &payload.commit_message);
444+
let new_commit_id = new_commit.id.to_string();
445+
446+
let mut entries: Vec<Entry> = Vec::new();
447+
for t in updated_trees.iter().cloned() {
448+
entries.push(Entry::from(t));
449+
}
450+
entries.push(Entry::from(new_blob.clone()));
451+
entries.push(Entry::from(new_commit.clone()));
452+
git_storage
453+
.save_entry(self.repo.repo_id, entries)
454+
.await
455+
.map_err(|e| GitError::CustomError(e.to_string()))?;
456+
457+
// Update ref to new commit id
458+
git_storage
459+
.update_ref(self.repo.repo_id, &default_ref.ref_name, &new_commit_id)
460+
.await
461+
.map_err(|e| GitError::CustomError(e.to_string()))?;
462+
new_commit_id
463+
};
464+
465+
Ok(EditFileResult {
466+
commit_id: new_commit_id,
467+
new_oid: new_blob.id.to_string(),
468+
path: payload.path,
469+
})
470+
}
390471
}
391472

392473
impl ImportApiService {
@@ -462,6 +543,8 @@ impl ImportApiService {
462543
created_at: chrono::Utc::now().naive_utc(),
463544
updated_at: chrono::Utc::now().naive_utc(),
464545
};
546+
// Use ref creation time as lightweight tag created_at (capture before move)
547+
let created_at = import_ref.created_at.and_utc().to_rfc3339();
465548
git_storage
466549
.save_ref(self.repo.repo_id, import_ref)
467550
.await
@@ -476,7 +559,7 @@ impl ImportApiService {
476559
object_type: "commit".to_string(),
477560
tagger: tagger_info.clone(),
478561
message: String::new(),
479-
created_at: chrono::Utc::now().to_rfc3339(),
562+
created_at,
480563
})
481564
}
482565
/// Traverses the commit history starting from a given commit, looking for the earliest commit
@@ -638,9 +721,7 @@ impl ImportApiService {
638721
.unwrap(),
639722
)
640723
}
641-
}
642724

643-
impl ImportApiService {
644725
async fn validate_target_commit(&self, target: Option<&String>) -> Result<(), GitError> {
645726
if let Some(ref t) = target {
646727
let git_storage = self.storage.git_db_storage();
@@ -743,4 +824,43 @@ impl ImportApiService {
743824
}
744825
Ok(())
745826
}
827+
828+
fn update_tree_hash(
829+
&self,
830+
tree: Arc<Tree>,
831+
name: &str,
832+
target_hash: SHA1,
833+
) -> Result<Tree, GitError> {
834+
let index = tree
835+
.tree_items
836+
.iter()
837+
.position(|item| item.name == name)
838+
.ok_or_else(|| GitError::CustomError(format!("Tree item '{}' not found", name)))?;
839+
let mut items = tree.tree_items.clone();
840+
items[index].id = target_hash;
841+
Tree::from_tree_items(items).map_err(|_| GitError::CustomError("Invalid tree".to_string()))
842+
}
843+
844+
/// Build updated trees chain and return (updated_trees, new_root_tree_id)
845+
fn build_updated_trees(
846+
&self,
847+
mut path: PathBuf,
848+
mut update_chain: Vec<Arc<Tree>>,
849+
mut updated_tree_hash: SHA1,
850+
) -> Result<(Vec<Tree>, SHA1), GitError> {
851+
let mut updated_trees = Vec::new();
852+
while let Some(tree) = update_chain.pop() {
853+
let cloned_path = path.clone();
854+
let name = cloned_path
855+
.file_name()
856+
.and_then(|n| n.to_str())
857+
.ok_or_else(|| GitError::CustomError("Invalid path".into()))?;
858+
path.pop();
859+
860+
let new_tree = self.update_tree_hash(tree, name, updated_tree_hash)?;
861+
updated_tree_hash = new_tree.id;
862+
updated_trees.push(new_tree);
863+
}
864+
Ok((updated_trees, updated_tree_hash))
865+
}
746866
}

ceres/src/api_service/mod.rs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ use tokio::sync::Mutex;
2222

2323
use crate::model::blame::{BlameQuery, BlameResult};
2424
use crate::model::git::{
25-
CommitBindingInfo, CreateEntryInfo, LatestCommitInfo, TreeBriefItem, TreeCommitItem,
26-
TreeHashItem,
25+
CommitBindingInfo, CreateEntryInfo, DiffPreviewPayload, EditFilePayload, EditFileResult,
26+
LatestCommitInfo, TreeBriefItem, TreeCommitItem, TreeHashItem,
2727
};
2828
use common::model::{Pagination, TagInfo};
2929

@@ -308,6 +308,62 @@ pub trait ApiHandler: Send + Sync {
308308
query: BlameQuery,
309309
) -> Result<BlameResult, GitError>;
310310

311+
/// Convenience: get file blob oid at HEAD (or provided refs) by path
312+
async fn get_file_blob_id(
313+
&self,
314+
path: &Path,
315+
refs: Option<&str>,
316+
) -> Result<Option<SHA1>, GitError> {
317+
let parent = path.parent().unwrap_or(Path::new("/"));
318+
if let Some(tree) = self.search_tree_by_path_with_refs(parent, refs).await? {
319+
let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
320+
if let Some(item) = tree.tree_items.into_iter().find(|x| x.name == name)
321+
&& item.mode == TreeItemMode::Blob
322+
{
323+
return Ok(Some(item.id));
324+
}
325+
}
326+
Ok(None)
327+
}
328+
329+
/// Preview unified diff for a single file change
330+
async fn preview_file_diff(
331+
&self,
332+
payload: DiffPreviewPayload,
333+
) -> Result<Option<neptune::model::diff_model::DiffItem>, GitError> {
334+
use neptune::neptune_engine::Diff;
335+
let path = PathBuf::from(&payload.path);
336+
// old oid and content
337+
let old_oid_opt = self
338+
.get_file_blob_id(&path, Some(payload.refs.as_str()))
339+
.await?;
340+
let old_entry = if let Some(oid) = old_oid_opt {
341+
vec![(path.clone(), oid)]
342+
} else {
343+
Vec::new()
344+
};
345+
let new_blob = git_internal::internal::object::blob::Blob::from_content(&payload.content);
346+
let new_entry = vec![(path.clone(), new_blob.id)];
347+
348+
// local content reader: use DB for old oid and memory for new
349+
let mut cache: std::collections::HashMap<SHA1, Vec<u8>> = std::collections::HashMap::new();
350+
if let Some(oid) = old_oid_opt
351+
&& let Some(model) = self.get_raw_blob_by_hash(&oid.to_string()).await?
352+
{
353+
cache.insert(oid, model.data.unwrap_or_default());
354+
}
355+
cache.insert(new_blob.id, payload.content.into_bytes());
356+
357+
let read =
358+
|_: &PathBuf, oid: &SHA1| -> Vec<u8> { cache.get(oid).cloned().unwrap_or_default() };
359+
let mut items =
360+
Diff::diff(old_entry, new_entry, "histogram".into(), Vec::new(), read).await;
361+
Ok(items.pop())
362+
}
363+
364+
/// Save file edit with conflict detection and commit creation.
365+
async fn save_file_edit(&self, payload: EditFilePayload) -> Result<EditFileResult, GitError>;
366+
311367
/// the dir's hash as same as old,file's hash is the content hash
312368
/// may think about change dir'hash as the content
313369
/// for now,only change the file's hash

ceres/src/api_service/mono_api_service.rs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ use crate::api_service::ApiHandler;
4444
use crate::model::blame::{BlameQuery, BlameResult};
4545
use crate::model::change_list::ClDiffFile;
4646
use crate::model::git::CreateEntryInfo;
47+
use crate::model::git::{EditFilePayload, EditFileResult};
4748
use crate::model::third_party::{ThirdPartyClient, ThirdPartyRepoTrait};
4849
use crate::protocol::{SmartProtocol, TransportProtocol};
4950
use async_trait::async_trait;
@@ -89,6 +90,53 @@ impl ApiHandler for MonoApiService {
8990
self.storage.clone()
9091
}
9192

93+
/// Save file edit in monorepo with optimistic concurrency check
94+
async fn save_file_edit(&self, payload: EditFilePayload) -> Result<EditFileResult, GitError> {
95+
let storage = self.storage.mono_storage();
96+
let file_path = PathBuf::from(&payload.path);
97+
let parent_path = file_path
98+
.parent()
99+
.ok_or_else(|| GitError::CustomError("Invalid file path".to_string()))?;
100+
101+
// Build update chain to parent directory and determine current blob id
102+
let update_chain = self.search_tree_for_update(parent_path).await?;
103+
let parent_tree = update_chain
104+
.last()
105+
.ok_or_else(|| GitError::CustomError("Parent tree not found".to_string()))?
106+
.clone();
107+
let file_name = file_path
108+
.file_name()
109+
.and_then(|n| n.to_str())
110+
.ok_or_else(|| GitError::CustomError("Invalid file name".to_string()))?;
111+
112+
let _current_item = parent_tree
113+
.tree_items
114+
.iter()
115+
.find(|x| x.name == file_name && x.mode == TreeItemMode::Blob)
116+
.ok_or_else(|| GitError::CustomError("[code:404] File not found".to_string()))?;
117+
118+
// Create new blob and build update result up to root
119+
let new_blob = Blob::from_content(&payload.content);
120+
let result = self
121+
.build_result_by_chain(file_path.clone(), update_chain, new_blob.id)
122+
.map_err(|e| GitError::CustomError(e.to_string()))?;
123+
124+
// Apply and save
125+
let new_commit_id = self
126+
.apply_update_result(&result, &payload.commit_message)
127+
.await?;
128+
storage
129+
.save_mega_blobs(vec![&new_blob], &new_commit_id)
130+
.await
131+
.map_err(|e| GitError::CustomError(e.to_string()))?;
132+
133+
Ok(EditFileResult {
134+
commit_id: new_commit_id,
135+
new_oid: new_blob.id.to_string(),
136+
path: payload.path,
137+
})
138+
}
139+
92140
/// Creates a new file or directory in the monorepo based on the provided file information.
93141
///
94142
/// # Arguments
@@ -579,7 +627,7 @@ impl ApiHandler for MonoApiService {
579627
object_type: "commit".to_string(),
580628
tagger: "".to_string(),
581629
message: "".to_string(),
582-
created_at: chrono::Utc::now().to_rfc3339(),
630+
created_at: r.created_at.and_utc().to_rfc3339(),
583631
});
584632
}
585633
}
@@ -627,7 +675,7 @@ impl ApiHandler for MonoApiService {
627675
object_type: "commit".to_string(),
628676
tagger: "".to_string(),
629677
message: "".to_string(),
630-
created_at: chrono::Utc::now().to_rfc3339(),
678+
created_at: r.created_at.and_utc().to_rfc3339(),
631679
}));
632680
}
633681
Ok(None)
@@ -875,6 +923,12 @@ impl MonoApiService {
875923
tracing::error!("Failed to write lightweight tag ref: {}", e);
876924
GitError::CustomError("[code:500] Failed to write lightweight tag ref".to_string())
877925
})?;
926+
// Fetch saved ref to use its creation time
927+
let saved_ref = mono_storage
928+
.get_ref_by_name(&full_ref)
929+
.await
930+
.map_err(|e| GitError::CustomError(e.to_string()))?
931+
.ok_or_else(|| GitError::CustomError("Ref not found after creation".to_string()))?;
878932

879933
Ok(TagInfo {
880934
name: name.clone(),
@@ -883,7 +937,7 @@ impl MonoApiService {
883937
object_type: "commit".to_string(),
884938
tagger: tagger_info.clone(),
885939
message: String::new(),
886-
created_at: chrono::Utc::now().to_rfc3339(),
940+
created_at: saved_ref.created_at.and_utc().to_rfc3339(),
887941
})
888942
}
889943
async fn validate_target_commit_mono(&self, target: Option<&String>) -> Result<(), GitError> {

0 commit comments

Comments
 (0)