Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion ceres/src/application/api_service/mono/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ impl MonoApiService {
let expected_tree = root_ref.ref_tree_hash.clone();
let root_ref_id = root_ref.id;

let save_trees = tree_ops::search_and_create_tree(self, &path_buf).await?;
let (save_trees, gitkeep_blob) =
tree_ops::search_and_create_tree(self, &path_buf).await?;
let leaf_tree = save_trees
.back()
.ok_or_else(|| MegaError::Other("no tree generated".to_string()))?;
Expand All @@ -60,6 +61,13 @@ impl MonoApiService {
&commit_msg,
);

// Persist .gitkeep before attaching trees. Previously only trees/commits
// were saved, leaving object storage without the blob and breaking clone.
self.storage()
.mono_service
.save_blobs(&new_commit.id.to_string(), vec![gitkeep_blob])
.await?;

let txn = self.storage().begin_db_transaction().await?;
match storage
.attach_to_monorepo_parent_in_txn(
Expand Down
11 changes: 8 additions & 3 deletions ceres/src/application/api_service/tree_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use git_internal::{
errors::GitError,
internal::object::{
ObjectTrait,
blob::Blob,
tree::{Tree, TreeItem, TreeItemMode},
},
};
Expand Down Expand Up @@ -112,11 +113,15 @@ pub async fn search_tree_by_path<T: ApiHandler + ?Sized>(
///
/// # Errors
///
/// Returns a `GitError` if an error occurs during the search or tree creation process.
/// Returns a `MegaError` if an error occurs during the search or tree creation process.
///
/// The returned [`Blob`] is the placeholder `.gitkeep` referenced by the new leaf tree.
/// Callers **must** persist it via object storage (`save_blobs` / `put_objects`) before
/// or with the trees; otherwise clone/fetch will 404 on that object.
pub async fn search_and_create_tree<T: ApiHandler + ?Sized>(
handler: &T,
path: &Path,
) -> Result<VecDeque<Tree>, MegaError> {
) -> Result<(VecDeque<Tree>, Blob), MegaError> {
let relative_path = handler.strip_relative(path)?;
let root_tree = handler.get_root_tree(None).await?;
let mut search_tree = root_tree.clone();
Expand Down Expand Up @@ -190,7 +195,7 @@ pub async fn search_and_create_tree<T: ApiHandler + ?Sized>(
}
}

Ok(saving_trees)
Ok((saving_trees, blob))
}

/// return the dir's hash only
Expand Down
9 changes: 8 additions & 1 deletion ceres/src/application/code_edit/post_receive/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ pub async fn dispatch_import_receive_pack_finalized(
let expected_tree = root_ref.ref_tree_hash.clone();
let root_ref_id = root_ref.id;

let save_trees = tree_ops::search_and_create_tree(mono_api_service, &repo_path).await?;
let (save_trees, gitkeep_blob) =
tree_ops::search_and_create_tree(mono_api_service, &repo_path).await?;

let new_commit = Commit::from_tree_id(
save_trees
Expand All @@ -74,6 +75,12 @@ pub async fn dispatch_import_receive_pack_finalized(
&format!("\n{commit_msg}"),
);

// Persist placeholder .gitkeep referenced by newly created path trees.
storage
.mono_service
.save_blobs(&new_commit.id.to_string(), vec![gitkeep_blob])
.await?;

let txn = storage.begin_db_transaction().await?;
let git_db = storage.git_db_storage();
for cmd in &commands {
Expand Down
77 changes: 41 additions & 36 deletions ceres/src/transport/protocol/smart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
use anyhow::Result;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use callisto::sea_orm_active_enums::RefTypeEnum;
use common::errors::{ProtocolError, mega_to_protocol_error};
use common::errors::{ProtocolError, git_to_protocol_error, mega_to_protocol_error};
use tokio_stream::wrappers::ReceiverStream;

use crate::{
Expand Down Expand Up @@ -153,48 +153,53 @@ impl SmartSession {
let have: Vec<String> = have.into_iter().collect();

if have.is_empty() {
pack_data = repo_handler.full_pack(want).await.unwrap();
pack_data = repo_handler.full_pack(want).await.map_err(|e| {
tracing::error!(error = %e, "git upload-pack full_pack failed");
git_to_protocol_error(e)
})?;
add_pkt_line_string(&mut protocol_buf, String::from("NAK\n"));
} else {
if self.capabilities.contains(&Capability::MultiAckDetailed) {
// multi_ack_detailed mode, the server will differentiate the ACKs where it is signaling that
// it is ready to send data with ACK obj-id ready lines,
// and signals the identified common commits with ACK obj-id common lines

for hash in &have {
if repo_handler.check_commit_exist(hash).await {
add_pkt_line_string(&mut protocol_buf, format!("ACK {hash} common\n"));
if last_common_commit.is_empty() {
last_common_commit = hash.to_string();
}
} else if self.capabilities.contains(&Capability::MultiAckDetailed) {
// multi_ack_detailed mode, the server will differentiate the ACKs where it is signaling that
// it is ready to send data with ACK obj-id ready lines,
// and signals the identified common commits with ACK obj-id common lines

for hash in &have {
if repo_handler.check_commit_exist(hash).await {
add_pkt_line_string(&mut protocol_buf, format!("ACK {hash} common\n"));
if last_common_commit.is_empty() {
last_common_commit = hash.to_string();
}
}
pack_data = repo_handler
.incremental_pack(want.clone(), have)
.await
.unwrap();

if last_common_commit.is_empty() {
//send NAK if missing common commit
add_pkt_line_string(&mut protocol_buf, String::from("NAK\n"));
// need to handle rebase option, still need pack data when has no common commit
return Ok((pack_data, protocol_buf));
}
}
pack_data = repo_handler
.incremental_pack(want.clone(), have)
.await
.map_err(|e| {
tracing::error!(error = %e, "git upload-pack incremental_pack failed");
git_to_protocol_error(e)
})?;

if last_common_commit.is_empty() {
//send NAK if missing common commit
add_pkt_line_string(&mut protocol_buf, String::from("NAK\n"));
// need to handle rebase option, still need pack data when has no common commit
return Ok((pack_data, protocol_buf));
}

for hash in want {
if self.capabilities.contains(&Capability::NoDone) {
// If multi_ack_detailed and no-done are both present, then the sender is free to immediately send a pack
// following its first "ACK obj-id ready" message.
add_pkt_line_string(&mut protocol_buf, format!("ACK {hash} ready\n"));
}
for hash in want {
if self.capabilities.contains(&Capability::NoDone) {
// If multi_ack_detailed and no-done are both present, then the sender is free to immediately send a pack
// following its first "ACK obj-id ready" message.
add_pkt_line_string(&mut protocol_buf, format!("ACK {hash} ready\n"));
}
} else {
tracing::error!("capability unsupported");
// init a empty receiverstream
let (_, rx) = tokio::sync::mpsc::channel::<Vec<u8>>(1);
pack_data = ReceiverStream::new(rx);
}
add_pkt_line_string(&mut protocol_buf, format!("ACK {last_common_commit} \n"));
} else {
tracing::error!("capability unsupported");
// init a empty receiverstream
let (_, rx) = tokio::sync::mpsc::channel::<Vec<u8>>(1);
pack_data = ReceiverStream::new(rx);
add_pkt_line_string(&mut protocol_buf, format!("ACK {last_common_commit} \n"));
}
Ok((pack_data, protocol_buf))
}
Expand Down
36 changes: 36 additions & 0 deletions common/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,16 +352,36 @@ pub fn protocol_error_is_client_safe(err: &ProtocolError) -> bool {
pub fn mega_to_protocol_error(err: MegaError) -> ProtocolError {
match err {
MegaError::NotFound(msg) => ProtocolError::NotFound(msg),
MegaError::ObjStorageNotFound(msg) => {
// Missing blob/object content: return a typed client error instead of
// panicking in upload-pack (which surfaces to git as HTTP 502).
ProtocolError::NotFound(format!("git object missing from object storage: {msg}"))
}
MegaError::ObjStorageInconsistent(msg) => {
ProtocolError::InvalidInput(format!("git object storage inconsistent: {msg}"))
}
MegaError::BadRequest(msg) => ProtocolError::InvalidInput(msg),
MegaError::Unauthorized(msg) => ProtocolError::Deny(msg),
MegaError::Forbidden(msg) => ProtocolError::InvalidInput(msg),
MegaError::Unavailable(msg) => ProtocolError::InvalidInput(msg),
MegaError::Conflict(msg) => ProtocolError::InvalidInput(msg),
MegaError::Git(e) => git_to_protocol_error(e),
MegaError::Io(e) => ProtocolError::IO(e),
other => ProtocolError::InvalidInput(other.to_string()),
}
}

/// Map [`GitError`] into a protocol-layer error for upload/receive-pack handlers.
pub fn git_to_protocol_error(err: GitError) -> ProtocolError {
let msg = err.to_string();
match git_error_http_status(&err) {
404 => ProtocolError::NotFound(msg),
401 => ProtocolError::Deny(msg),
400 | 403 | 409 | 413 => ProtocolError::InvalidInput(msg),
_ => ProtocolError::IO(std::io::Error::other(msg)),
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -414,4 +434,20 @@ mod tests {
let err = mega_to_protocol_error(MegaError::NotFound("repo".into()));
assert!(matches!(err, ProtocolError::NotFound(_)));
}

#[test]
fn mega_to_protocol_error_maps_objstorage_not_found() {
let err = mega_to_protocol_error(MegaError::ObjStorageNotFound("missing key".into()));
assert!(matches!(err, ProtocolError::NotFound(_)));
assert_eq!(protocol_error_http_status(&err), 404);
}

#[test]
fn git_to_protocol_error_maps_objstorage_custom_error() {
let err = git_to_protocol_error(GitError::CustomError(
"ObjStorage not found: Object at location git/ab/cd/ef/123 not found".into(),
));
assert!(matches!(err, ProtocolError::NotFound(_)));
assert_eq!(protocol_error_http_status(&err), 404);
}
}
2 changes: 1 addition & 1 deletion orion/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "orion"
version = "0.1.2"
version = "0.1.3"
edition = "2024"

[[bin]]
Expand Down
Loading
Loading