Repository: web3infra-foundation/mega (language: rust)

Issue:
[Moon UI] Code Preview Page Optimization
The font size on the Code Preview page should be consistent with the overall UI design. 
Additionally, there are some layout issues such as UI misalignment and incorrect formatting. 


Discussion / hints:

Fix patch:
diff --git a/common/Cargo.toml b/common/Cargo.toml
index b72dcae6..a3593310 100644
--- a/common/Cargo.toml
+++ b/common/Cargo.toml
@@ -10,6 +10,7 @@ name = "common"
 path = "src/lib.rs"
 
 [dependencies]
+axum = { workspace = true }
 anyhow = { workspace = true }
 sea-orm = { workspace = true }
 thiserror = { workspace = true }
diff --git a/common/src/errors.rs b/common/src/errors.rs
index 82ab537e..be3366af 100644
--- a/common/src/errors.rs
+++ b/common/src/errors.rs
@@ -1,6 +1,12 @@
+use anyhow::Result;
+use axum::{
+    http::StatusCode,
+    response::{IntoResponse, Response},
+    Json,
+};
 use thiserror::Error;
 
-use anyhow::Result;
+use crate::model::CommonResult;
 
 pub type MegaResult = Result<(), MegaError>;
 
@@ -75,7 +81,6 @@ pub enum GitLFSError {
     GeneralError(String),
 }
 
-
 #[derive(Debug, Error)]
 pub enum ProtocolError {
     #[error("{0}")]
@@ -84,6 +89,36 @@ pub enum ProtocolError {
     Deny(String),
     #[error("Repository not found: {0}")]
     NotFound(String),
+    #[error("Invalid Input: {0}")]
+    InvalidInput(String),
+    #[error("HTTP Push Has Been Disabled")]
+    Disabled,
+}
+
+impl IntoResponse for ProtocolError {
+    fn into_response(self) -> Response {
+        let (status, message) = match self {
+            ProtocolError::Deny(err) => {
+                // This error is caused by bad user input so don't log it
+                (StatusCode::UNAUTHORIZED, err)
+            }
+            ProtocolError::NotFound(err) => {
+                // Because `TraceLayer` wraps each request in a span that contains the request
+                // method, uri, etc we don't need to include those details here
+                // tracing::error!(%err, "error");
+
+                // Don't expose any details about the error to the client
+                (StatusCode::NOT_FOUND, err)
+            }
+            ProtocolError::InvalidInput(err) => (StatusCode::BAD_REQUEST, err),
+            _ => (
+                StatusCode::INTERNAL_SERVER_ERROR,
+                "Something went wrong".to_owned(),
+            ),
+        };
+
+        (status, Json(CommonResult::<String>::failed(&message))).into_response()
+    }
 }
 
 #[cfg(test)]
diff --git a/mono/src/api/api_router.rs b/mono/src/api/api_router.rs
index 8fde2a8f..8f8fbc54 100644
--- a/mono/src/api/api_router.rs
+++ b/mono/src/api/api_router.rs
@@ -14,7 +14,7 @@ use ceres::{
         tree::{LatestCommitInfo, TreeBriefItem, TreeCommitItem},
     },
 };
-use common::model::CommonResult;
+use common::{errors::ProtocolError, model::CommonResult};
 use http::StatusCode;
 use taurus::event::api_request::{ApiRequestEvent, ApiType};
 
@@ -32,8 +32,8 @@ pub fn routers() -> Router<MonoApiServiceState> {
         .route("/tree", get(get_tree_info))
         .route("/blob", get(get_blob_string))
         .route("/file/blob/:object_id", get(get_blob_file))
-        .route("/file/tree", get(get_tree_file));
-
+        .route("/file/tree", get(get_tree_file))
+        .route("/path-can-clone", get(path_can_be_cloned));
     Router::new()
         .merge(router)
         .merge(mr_router::routers())
@@ -47,7 +47,7 @@ async fn get_blob_string(
     ApiRequestEvent::notify(ApiType::Blob, &state.0.context.config);
     let res = state
         .api_handler(query.path.clone().into())
-        .await
+        .await?
         .get_blob_as_string(query.path.into())
         .await;
 
@@ -69,7 +69,7 @@ async fn create_file(
     ApiRequestEvent::notify(ApiType::CreateFile, &state.0.context.config);
     let res = state
         .api_handler(json.path.clone().into())
-        .await
+        .await?
         .create_monorepo_file(json.clone())
         .await;
     let res = match res {
@@ -86,7 +86,7 @@ async fn get_latest_commit(
     ApiRequestEvent::notify(ApiType::LastestCommit, &state.0.context.config);
     let res = state
         .api_handler(query.path.clone().into())
-        .await
+        .await?
         .get_latest_commit(query.path.into())
         .await?;
     Ok(Json(res))
@@ -99,7 +99,7 @@ async fn get_tree_info(
     ApiRequestEvent::notify(ApiType::TreeInfo, &state.0.context.config);
     let res = state
         .api_handler(query.path.clone().into())
-        .await
+        .await?
         .get_tree_info(query.path.into())
         .await;
     let res = match res {
@@ -112,11 +112,11 @@ async fn get_tree_info(
 async fn get_tree_commit_info(
     Query(query): Query<CodePreviewQuery>,
     state: State<MonoApiServiceState>,
-) -> Result<Json<CommonResult<Vec<TreeCommitItem>>>, ApiError> {
+) -> Result<Json<CommonResult<Vec<TreeCommitItem>>>, ProtocolError> {
     ApiRequestEvent::notify(ApiType::CommitInfo, &state.0.context.config);
     let res = state
         .api_handler(query.path.clone().into())
-        .await
+        .await?
         .get_tree_commit_info(query.path.into())
         .await;
     let res = match res {
@@ -155,7 +155,7 @@ pub async fn get_tree_file(
 ) -> Result<Response, ApiError> {
     let res = state
         .api_handler(query.path.clone().into())
-        .await
+        .await?
         .get_tree_as_data(std::path::Path::new(&query.path))
         .await;
 
@@ -174,3 +174,11 @@ pub async fn get_tree_file(
         }),
     }
 }
+
+async fn path_can_be_cloned(
+    Query(query): Query<BlobContentQuery>,
+    state: State<MonoApiServiceState>,
+) -> Result<Json<CommonResult<bool>>, ApiError> {
+    let res = state.api_handler(query.path.clone().into()).await.is_ok();
+    Ok(Json(CommonResult::success(Some(res))))
+}
diff --git a/mono/src/api/mod.rs b/mono/src/api/mod.rs
index a80fb629..cf9d5097 100644
--- a/mono/src/api/mod.rs
+++ b/mono/src/api/mod.rs
@@ -10,7 +10,7 @@ use ceres::{
     },
     protocol::repo::Repo,
 };
-use common::model::CommonOptions;
+use common::{errors::ProtocolError, model::CommonOptions};
 use jupiter::{context::Context, storage::user_storage::UserStorage};
 
 pub mod api_router;
@@ -54,7 +54,7 @@ impl MonoApiServiceState {
         }
     }
 
-    async fn api_handler(&self, path: PathBuf) -> Box<dyn ApiHandler> {
+    async fn api_handler(&self, path: PathBuf) -> Result<Box<dyn ApiHandler>, ProtocolError> {
         let import_dir = self.context.config.monorepo.import_dir.clone();
         if path.starts_with(&import_dir) && path != import_dir {
             if let Some(model) = self
@@ -66,14 +66,18 @@ impl MonoApiServiceState {
                 .unwrap()
             {
                 let repo: Repo = model.into();
-                return Box::new(ImportApiService {
+                return Ok(Box::new(ImportApiService {
                     context: self.context.clone(),
                     repo,
-                });
+                }));
             }
+            return Err(ProtocolError::InvalidInput(format!(
+                "Invalid Path{}",
+                path.to_str().unwrap()
+            )));
         }
-        Box::new(MonoApiService {
+        Ok(Box::new(MonoApiService {
             context: self.context.clone(),
-        })
+        }))
     }
 }
diff --git a/mono/src/git_protocol/http.rs b/mono/src/git_protocol/http.rs
index b8cec502..967c2fd9 100644
--- a/mono/src/git_protocol/http.rs
+++ b/mono/src/git_protocol/http.rs
@@ -2,7 +2,7 @@ use std::convert::Infallible;
 
 use anyhow::Result;
 use axum::body::Body;
-use axum::http::{HeaderValue, Request, Response, StatusCode};
+use axum::http::{HeaderValue, Request, Response};
 use bytes::{Bytes, BytesMut};
 use futures::{stream, TryStreamExt};
 use tokio::io::AsyncReadExt;
@@ -21,19 +21,19 @@ use common::model::InfoRefsParams;
 pub async fn git_info_refs(
     params: InfoRefsParams,
     mut pack_protocol: SmartProtocol,
-) -> Result<Response<Body>, (StatusCode, String)> {
+) -> Result<Response<Body>, ProtocolError> {
     let service_name = params.service.unwrap();
     pack_protocol.service_type = Some(service_name.parse::<ServiceType>().unwrap());
 
-    let response = match pack_protocol.git_info_refs().await {
-        Ok(pkt_line_stream) => Response::builder()
-            .body(Body::from(pkt_line_stream.freeze()))
-            .unwrap(),
-        Err(err) => return Ok(handler_err(err)),
-    };
+    let pkt_line_stream = pack_protocol.git_info_refs().await?;
 
     let content_type = format!("application/x-{}-advertisement", service_name);
-    let response = add_default_header(content_type, response);
+    let response = add_default_header(
+        content_type,
+        Response::builder()
+            .body(Body::from(pkt_line_stream.freeze()))
+            .unwrap(),
+    );
     Ok(response)
 }
 
@@ -60,7 +60,7 @@ pub async fn git_info_refs(
 pub async fn git_upload_pack(
     req: Request<Body>,
     mut pack_protocol: SmartProtocol,
-) -> Result<Response<Body>, (StatusCode, String)> {
+) -> Result<Response<Body>, ProtocolError> {
     let upload_request: BytesMut = req
         .into_body()
         .into_data_stream()
@@ -71,42 +71,37 @@ pub async fn git_upload_pack(
         .await
         .unwrap();
     tracing::debug!("bytes from client: {:?}", upload_request);
-    let response = match pack_protocol
+    let (mut send_pack_data, protocol_buf) = pack_protocol
         .git_upload_pack(&mut upload_request.freeze())
-        .await
-    {
-        Ok((mut send_pack_data, protocol_buf)) => {
-            let body_stream = async_stream::stream! {
-                tracing::info!("send ack/nak message buf: {:?}", &protocol_buf);
-                yield Ok::<_, Infallible>(Bytes::copy_from_slice(&protocol_buf));
-                // send packdata with sideband64k
-                while let Some(chunk) = send_pack_data.next().await {
-                    let mut reader = chunk.as_slice();
-                    loop {
-                        let mut temp = BytesMut::new();
-                        temp.reserve(65500);
-                        let length = reader.read_buf(&mut temp).await.unwrap();
-                        if length == 0 {
-                            break;
-                        }
-                        let bytes_out = pack_protocol.build_side_band_format(temp, length);
-                        // tracing::info!("send pack file: length: {:?}", bytes_out.len());
-                        yield Ok::<_, Infallible>(bytes_out.freeze());
-                    }
+        .await?;
+
+    let body_stream = async_stream::stream! {
+        tracing::info!("send ack/nak message buf: {:?}", &protocol_buf);
+        yield Ok::<_, Infallible>(Bytes::copy_from_slice(&protocol_buf));
+        // send packdata with sideband64k
+        while let Some(chunk) = send_pack_data.next().await {
+            let mut reader = chunk.as_slice();
+            loop {
+                let mut temp = BytesMut::new();
+                temp.reserve(65500);
+                let length = reader.read_buf(&mut temp).await.unwrap();
+                if length == 0 {
+                    break;
                 }
-                let bytes_out = Bytes::from_static(smart::PKT_LINE_END_MARKER);
-                tracing::info!("send back pkt-flush line '0000', actually: {:?}", bytes_out);
-                yield Ok::<_, Infallible>(bytes_out);
-            };
-            Response::builder()
-                .body(Body::from_stream(body_stream))
-                .unwrap()
+                let bytes_out = pack_protocol.build_side_band_format(temp, length);
+                // tracing::info!("send pack file: length: {:?}", bytes_out.len());
+                yield Ok::<_, Infallible>(bytes_out.freeze());
+            }
         }
-        Err(err) => return Ok(handler_err(err)),
+        let bytes_out = Bytes::from_static(smart::PKT_LINE_END_MARKER);
+        tracing::info!("send back pkt-flush line '0000', actually: {:?}", bytes_out);
+        yield Ok::<_, Infallible>(bytes_out);
     };
     let response = add_default_header(
         String::from("application/x-git-upload-pack-result"),
-        response,
+        Response::builder()
+            .body(Body::from_stream(body_stream))
+            .unwrap(),
     );
     Ok(response)
 }
@@ -129,7 +124,7 @@ pub async fn git_upload_pack(
 pub async fn git_receive_pack(
     req: Request<Body>,
     mut pack_protocol: SmartProtocol,
-) -> Result<Response<Body>, (StatusCode, String)> {
+) -> Result<Response<Body>, ProtocolError> {
     // Convert the request body into a data stream.
     let mut data_stream = req.into_body().into_data_stream();
     let mut report_status = Bytes::new();
@@ -179,22 +174,5 @@ fn add_default_header<T>(content_type: String, mut response: Response<T>) -> Res
     response
 }
 
-fn handler_err(err: ProtocolError) -> Response<Body> {
-    match err {
-        ProtocolError::NotFound(err) => Response::builder()
-            .status(StatusCode::NOT_FOUND)
-            .body(Body::from(err.to_string()))
-            .unwrap(),
-        ProtocolError::Deny(err) => Response::builder()
-            .status(StatusCode::UNAUTHORIZED)
-            .body(Body::from(err.to_string()))
-            .unwrap(),
-        _ => Response::builder()
-            .status(StatusCode::INTERNAL_SERVER_ERROR)
-            .body(Body::empty())
-            .unwrap(),
-    }
-}
-
 #[cfg(test)]
 mod tests {}
diff --git a/mono/src/server/https_server.rs b/mono/src/server/https_server.rs
index 164ed0a2..cd480bd9 100644
--- a/mono/src/server/https_server.rs
+++ b/mono/src/server/https_server.rs
@@ -6,7 +6,7 @@ use anyhow::Result;
 use async_session::MemoryStore;
 use axum::body::Body;
 use axum::extract::{Query, State};
-use axum::http::{self, Request, StatusCode, Uri};
+use axum::http::{self, Request, Uri};
 use axum::response::Response;
 use axum::routing::get;
 use axum::Router;
@@ -21,6 +21,7 @@ use tower_http::trace::TraceLayer;
 
 use ceres::protocol::{ServiceType, SmartProtocol, TransportProtocol};
 use common::config::Config;
+use common::errors::ProtocolError;
 use common::model::{CommonOptions, InfoRefsParams};
 use jupiter::context::Context;
 
@@ -155,7 +156,7 @@ pub async fn get_method_router(
     state: State<AppState>,
     Query(params): Query<InfoRefsParams>,
     uri: Uri,
-) -> Result<Response<Body>, (StatusCode, String)> {
+) -> Result<Response<Body>, ProtocolError> {
     if INFO_REFS_REGEX.is_match(uri.path()) {
         let pack_protocol = SmartProtocol::new(
             remove_git_suffix(uri, "/info/refs"),
@@ -164,9 +165,8 @@ pub async fn get_method_router(
         );
         crate::git_protocol::http::git_info_refs(params, pack_protocol).await
     } else {
-        Err((
-            StatusCode::NOT_FOUND,
-            String::from("Operation not supported\n"),
+        Err(ProtocolError::NotFound(
+            "Operation not supported".to_owned(),
         ))
     }
 }
@@ -175,7 +175,7 @@ pub async fn post_method_router(
     state: State<AppState>,
     uri: Uri,
     req: Request<Body>,
-) -> Result<Response, (StatusCode, String)> {
+) -> Result<Response, ProtocolError> {
     if REGEX_GIT_UPLOAD_PACK.is_match(uri.path()) {
         let mut pack_protocol = SmartProtocol::new(
             remove_git_suffix(uri.clone(), "/git-upload-pack"),
@@ -186,10 +186,7 @@ pub async fn post_method_router(
         crate::git_protocol::http::git_upload_pack(req, pack_protocol).await
     } else if REGEX_GIT_RECEIVE_PACK.is_match(uri.path()) {
         if state.context.config.monorepo.disable_http_push {
-            return Err((
-                StatusCode::FORBIDDEN,
-                String::from("HTTP Push Has Been Disabled"),
-            ))
+            return Err(ProtocolError::Disabled);
         }
         let mut pack_protocol = SmartProtocol::new(
             remove_git_suffix(uri.clone(), "/git-receive-pack"),
@@ -199,10 +196,9 @@ pub async fn post_method_router(
         pack_protocol.service_type = Some(ServiceType::ReceivePack);
         crate::git_protocol::http::git_receive_pack(req, pack_protocol).await
     } else {
-        Err((
-            StatusCode::NOT_FOUND,
-            String::from("Operation not supported"),
-        ))
+        return Err(ProtocolError::NotFound(
+            "Operation not supported".to_owned(),
+        ));
     }
 }
 
diff --git a/moon/src/app/(dashboard)/blob/[...path]/page.tsx b/moon/src/app/(dashboard)/blob/[...path]/page.tsx
index 840ffe81..81494329 100644
--- a/moon/src/app/(dashboard)/blob/[...path]/page.tsx
+++ b/moon/src/app/(dashboard)/blob/[...path]/page.tsx
@@ -2,15 +2,16 @@
 import CodeContent from '@/components/CodeContent';
 import Bread from '@/components/BreadCrumb';
 import { useEffect, useState } from 'react';
+import { Flex, Layout } from "antd/lib";
 
 export default function BlobPage({ params }: { params: { path: string[] } }) {
     let path = '/' + params.path.join('/');
-    const [readmeContent, setReadmeContent] = useState("");
+    const [fileContent, setFileContent] = useState("");
     useEffect(() => {
         const fetchData = async () => {
             try {
-                let readmeContent = await getReadmeContent(path);
-                setReadmeContent(readmeContent);
+                let fileContent = await getFileContent(path);
+                setFileContent(fileContent);
             } catch (error) {
                 console.error('Error fetching data:', error);
             }
@@ -18,15 +19,47 @@ export default function BlobPage({ params }: { params: { path: string[] } }) {
         fetchData();
     }, [path]);
 
+    const treeStyle = {
+        borderRadius: 8,
+        overflow: 'hidden',
+        width: 'calc(15% - 8px)',
+        maxWidth: 'calc(15% - 8px)',
+        background: '#fff',
+    };
+
+    const codeStyle = {
+        borderRadius: 8,
+        overflow: 'hidden',
+        width: 'calc(85% - 8px)',
+        background: '#fff',
+    };
+
+    const breadStyle = {
+        minHeight: 30,
+        borderRadius: 8,
+        overflow: 'hidden',
+        width: 'calc(100% - 8px)',
+        background: '#fff',
+    };
+
     return (
         <div>
-            <Bread path={params.path} />
-            <CodeContent fileContent={readmeContent} />
+            <Flex gap="middle" wrap>
+                <Layout style={breadStyle}>
+                    <Bread path={params.path} />
+                </Layout>
+                <Layout style={treeStyle}>
+                    {/* <RepoTree directory={directory} /> */}
+                </Layout>
+                <Layout style={codeStyle}>
+                    <CodeContent fileContent={fileContent} />
+                </Layout>
+            </Flex>
         </div>
     )
 }
 
-async function getReadmeContent(pathname: string) {
+async function getFileContent(pathname: string) {
     const res = await fetch(`/api/blob?path=${pathname}`);
     const response = await res.json();
     const directory = response.data.data;
diff --git a/moon/src/components/CodeContent.module.css b/moon/src/components/CodeContent.module.css
index 7762baab..9469cdf7 100644
--- a/moon/src/components/CodeContent.module.css
+++ b/moon/src/components/CodeContent.module.css
@@ -1,7 +1,3 @@
-.codeShowContainer {
-    border-radius: 1rem;
-    padding-top: 70px;
-}
 
 .codeLineNumber {
     margin-left: 15px;
@@ -12,7 +8,6 @@
     background-color: rgba(53, 53, 53, 0.103);
     display: flex;
     position: absolute;
-    width: 80%;
     height: 50px;
     border-top-left-radius: 1rem;
     border-top-right-radius: 1rem;
@@ -33,8 +28,6 @@
 }
 
 .fileCodeContainer {
-    width: 80%;
-    margin-left: 18%;
     border-radius: 0.5rem;
     margin-top: 10px;
 
diff --git a/moon/src/components/CodeContent.tsx b/moon/src/components/CodeContent.tsx
index d3cc85a0..81b7fe98 100644
--- a/moon/src/components/CodeContent.tsx
+++ b/moon/src/components/CodeContent.tsx
@@ -1,13 +1,20 @@
 import Editor from './editor/Editor'
 import 'github-markdown-css/github-markdown-light.css'
 import { Highlight, themes } from "prism-react-renderer"
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
 import { createRoot } from 'react-dom/client'
 import styles from './CodeContent.module.css'
 
 const CodeContent = ({ fileContent }) => {
 
     const [showEditor, setShowEditor] = useState(false);
+    const [lfs, setLfs] = useState(false);
+
+    useEffect(() => {
+        if (isLfsContent(fileContent)) {
+            setLfs(true);
+        }
+    }, [fileContent])
 
     const handleLineNumberClick = (lineIndex) => {
         setShowEditor(!showEditor);
@@ -33,9 +40,28 @@ const CodeContent = ({ fileContent }) => {
 
     };
 
+    function isLfsContent(content: string): boolean {
+        const lines = content.split('\n');
+        let foundVersion = false;
+        let foundOid = false;
+        let foundSize = false;
+        for (const line of lines) {
+            if (line.startsWith('version ')) {
+                foundVersion = true;
+            } else if (line.startsWith('oid sha256:')) {
+                foundOid = true;
+            } else if (line.startsWith('size ')) {
+                foundSize = true;
+            }
+            if (foundVersion && foundOid && foundSize) {
+                return true;
+            }
+        }
+        return false;
+    }
 
     return (
-        <div className={styles.fileCodeContainer} >
+        <div  >
             <div className={styles.viewChangeTab}>
                 <button className={styles.viewChangeTabButton}>
                     Code
@@ -51,16 +77,29 @@ const CodeContent = ({ fileContent }) => {
                 language="rust"
             >
                 {({ className, style, tokens, getLineProps, getTokenProps }) => (
-                    <pre style={style} className={styles.codeShowContainer}>
-                        {tokens.map((line, i) => (
-                            <div key={i} {...getLineProps({ line })}>
-                                <button onClick={(event) => handleLineNumberClick(i)} className={styles.lineNumberButton} style={{ marginLeft: '8px', backgroundColor: 'rgb(247, 237, 224, 0.7)', width: '25px', height: '17px', lineHeight: '17px', borderRadius: '3px', marginTop: '5px', border: 'none' }}>+</button>
-                                <span className={styles.codeLineNumber}>{i + 1}</span>
-                                {line.map((token, key) => (
-                                    <span key={key} {...getTokenProps({ token })} />
-                                ))}
-                            </div>
-                        ))}
+                    <pre
+                        style={{
+                            ...style,
+                            padding: '16px',
+                            paddingTop: '70px',
+                        }}
+                        className="overflow-x-auto whitespace-pre p-4 bg-gray-100 rounded-lg text-sm" 
+                    >
+                        {
+                            !lfs &&
+                            tokens.map((line, i) => (
+                                <div key={i} {...getLineProps({ line })}>
+                                    <button onClick={(event) => handleLineNumberClick(i)} className={styles.lineNumberButton} style={{ marginLeft: '8px', backgroundColor: 'rgb(247, 237, 224, 0.7)', width: '25px', height: '17px', lineHeight: '17px', borderRadius: '3px', marginTop: '5px', border: 'none' }}>+</button>
+                                    <span className={styles.codeLineNumber}>{i + 1}</span>
+                                    {line.map((token, key) => (
+                                        <span key={key} {...getTokenProps({ token })} />
+                                    ))}
+                                </div>
+                            ))
+                        }
+                        {
+                            lfs && <span>(Sorry about that, but we can’t show files that are this big right now.)</span>
+                        }
                     </pre>
                 )}
             </Highlight>


Test patch: