From e181cf75f366f66d4e6e460f1da7611db791d9ab Mon Sep 17 00:00:00 2001 From: allure <1550220889@qq.com> Date: Sat, 18 Oct 2025 13:04:39 +0800 Subject: [PATCH 1/3] feat: Abstract HTTP and SSH protocal from mega to git-internal Signed-off-by: allure <1550220889@qq.com> --- Cargo.toml | 13 +- docs/PROTOCOL_ABSTRACTION.md | 369 ++++++++++++++++++++ src/config.rs | 59 ++++ src/lib.rs | 11 +- src/protocol/core.rs | 317 +++++++++++++++++ src/protocol/http.rs | 186 ++++++++++ src/protocol/mod.rs | 16 + src/protocol/pack.rs | 510 ++++++++++++++++++++++++++++ src/protocol/smart.rs | 642 +++++++++++++++++++++++++++++++++++ src/protocol/ssh.rs | 87 +++++ src/protocol/types.rs | 239 +++++++++++++ src/protocol/utils.rs | 116 +++++++ 12 files changed, 2562 insertions(+), 3 deletions(-) create mode 100644 docs/PROTOCOL_ABSTRACTION.md create mode 100644 src/config.rs create mode 100644 src/protocol/core.rs create mode 100644 src/protocol/http.rs create mode 100644 src/protocol/mod.rs create mode 100644 src/protocol/pack.rs create mode 100644 src/protocol/smart.rs create mode 100644 src/protocol/ssh.rs create mode 100644 src/protocol/types.rs create mode 100644 src/protocol/utils.rs diff --git a/Cargo.toml b/Cargo.toml index c23cea5a..2fecffee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ serde_json = "1.0.145" ahash = "0.8.12" diffs = "0.5.1" libc = "0.2.177" -zstd-sys = { version = "2.0.16+zstd.1.5.7", features = ["experimental"] } +zstd-sys = { version = "2.0.16", features = ["experimental"] } sea-orm = { version = "1.1.17", features = ["sqlx-sqlite"] } flate2 = { version = "1.1.4", features = ["zlib"] } serde = { version = "1.0.228", features = ["derive"] } @@ -39,6 +39,17 @@ uuid = { version = "1.18.1", features = ["v4"] } tokio = { version = "1.47.1", features = ["fs"] } bincode = { version = "2.0.1", features = ["serde"] } axum = { version = "0.8.6", features = ["macros", "json"] } +async-trait = "0.1.83" +futures = "0.3.31" +tokio-stream = "0.1.17" +http = "1.2.0" +base64 = "0.22.1" +# SSH server dependencies +russh = "0.52.1" +russh-keys = "0.49.2" +hyper = "1.5.1" +async-stream = "0.3.6" +anyhow = "1.0.93" [dev-dependencies] diff --git a/docs/PROTOCOL_ABSTRACTION.md b/docs/PROTOCOL_ABSTRACTION.md new file mode 100644 index 00000000..3efe4f94 --- /dev/null +++ b/docs/PROTOCOL_ABSTRACTION.md @@ -0,0 +1,369 @@ +## Git Protocol Abstraction in git-internal + +The git-internal library provides a clean, transport-agnostic abstraction for Git smart protocol operations. This document outlines how the protocol abstraction works and how it separates concerns between transport layers, business logic, and Git protocol handling. + +### 1. Architecture Overview + +The git-internal library implements a layered architecture that cleanly separates different concerns: + + • **Transport Layer**: HTTP and SSH adapters that handle transport-specific details + • **Protocol Layer**: Core Git smart protocol implementation that is transport-agnostic + • **Business Logic Layer**: Trait-based abstractions for repository access and authentication + • **Storage Layer**: Internal Git object handling and pack file operations + +This separation allows the same Git protocol logic to work across different transports and integrate with any storage backend or business system. + +### 2. Core Abstractions + +#### (1) RepositoryAccess Trait + +The `RepositoryAccess` trait provides a clean interface for storage operations without exposing Git protocol details: + +```rust +#[async_trait] +pub trait RepositoryAccess: Send + Sync + Clone { + // Basic repository operations + async fn get_repository_refs(&self, repo_path: &str) -> Result, ProtocolError>; + async fn has_object(&self, repo_path: &str, object_hash: &str) -> Result; + async fn get_object(&self, repo_path: &str, object_hash: &str) -> Result, ProtocolError>; + async fn store_pack_data(&self, repo_path: &str, pack_data: &[u8]) -> Result<(), ProtocolError>; + + // Reference management + async fn update_reference(&self, repo_path: &str, ref_name: &str, old_hash: Option<&str>, new_hash: &str) -> Result<(), ProtocolError>; + + // Pack operations + async fn get_objects_for_pack(&self, repo_path: &str, wants: &[String], haves: &[String]) -> Result, ProtocolError>; + + // Repo-specific utilities + async fn has_default_branch(&self, repo_path: &str) -> Result; + async fn post_receive_hook(&self, repo_path: &str) -> Result<(), ProtocolError>; + + // Optional helpers with default impls (can be overridden): + // - get_blob/get_commit/get_tree + // - commit_exists + // - handle_pack_objects(repo_path, commits, trees, blobs) +} +``` + +This trait abstracts away all storage implementation details, allowing integration with any backend (filesystem, database, cloud storage, etc.). + +#### (2) AuthenticationService Trait + +The `AuthenticationService` trait handles authentication for both HTTP and SSH transports: + +```rust +#[async_trait] +pub trait AuthenticationService: Send + Sync { + /// Authenticate HTTP request using headers + async fn authenticate_http( + &self, + headers: &std::collections::HashMap + ) -> Result<(), ProtocolError>; + + /// Authenticate SSH connection using public key + async fn authenticate_ssh(&self, username: &str, public_key: &[u8]) -> Result<(), ProtocolError>; +} +``` + +This allows for flexible authentication strategies while keeping the protocol layer agnostic to authentication details. + +### 3. Transport Layer Abstraction + +#### (1) HTTP Transport Handler + +The `HttpGitHandler` provides HTTP-specific request handling: + +```rust +pub struct HttpGitHandler { + protocol: GitProtocol, +} + +impl HttpGitHandler { + // Handle HTTP info/refs requests + pub async fn handle_info_refs(&mut self, request_path: &str, query: &str) -> Result<(Vec, &'static str), ProtocolError>; + + // Handle HTTP upload-pack requests (clone/fetch) + pub async fn handle_upload_pack(&mut self, request_path: &str, request_body: &[u8]) -> Result<(Stream, &'static str), ProtocolError>; + + // Handle HTTP receive-pack requests (push) + pub async fn handle_receive_pack(&mut self, request_path: &str, request_stream: Stream) -> Result<(Stream, &'static str), ProtocolError>; + + // Authenticate HTTP request using headers (call before handle_*) + pub async fn authenticate_http(&self, headers: &std::collections::HashMap) -> Result<(), ProtocolError>; +} +``` + +The HTTP handler includes utility functions for: +- Extracting repository paths from URLs +- Parsing query parameters +- Setting appropriate content types +- Validating Git requests + +#### (2) SSH Transport Handler + +The `SshGitHandler` provides SSH-specific command handling: + +```rust +pub struct SshGitHandler { + protocol: GitProtocol, +} + +impl SshGitHandler { + // Handle git-upload-pack command + pub async fn handle_upload_pack(&mut self, repo_path: &str, request_data: &[u8]) -> Result; + + // Handle git-receive-pack command + pub async fn handle_receive_pack(&mut self, repo_path: &str, request_stream: Stream) -> Result; + + // Handle info/refs for SSH + pub async fn handle_info_refs(&mut self, repo_path: &str, service: &str) -> Result, ProtocolError>; + + // Authenticate SSH session (call once after handshake) + pub async fn authenticate_ssh(&self, username: &str, public_key: &[u8]) -> Result<(), ProtocolError>; +} +``` + +The SSH handler includes utility functions for: +- Parsing SSH command lines +- Validating Git SSH commands +- Extracting repository paths from arguments + +### 4. Protocol Layer Implementation + +#### (1) GitProtocol Core + +The `GitProtocol` struct provides the main transport-agnostic interface: + +```rust +pub struct GitProtocol { + smart_protocol: SmartProtocol, +} +``` + +This struct delegates to `SmartProtocol` for the actual Git protocol implementation while providing a clean public API. + +Authentication helpers are also exposed to keep adapters minimal: + +```rust +impl GitProtocol { + pub async fn authenticate_http(&self, headers: &std::collections::HashMap) -> Result<(), ProtocolError>; + pub async fn authenticate_ssh(&self, username: &str, public_key: &[u8]) -> Result<(), ProtocolError>; +} +``` + +#### (2) SmartProtocol Implementation + +The `SmartProtocol` handles all Git smart protocol details: + +- **info/refs**: Advertises repository capabilities and references +- **upload-pack**: Handles fetch/clone operations with pack generation +- **receive-pack**: Handles push operations with pack unpacking + +The protocol implementation is completely transport-agnostic and uses the trait abstractions for all external dependencies. + +### 5. Pack File Handling + +The library includes comprehensive pack file support: + +#### (1) PackGenerator + +Handles pack file generation for upload-pack operations: + +```rust +pub struct PackGenerator<'a, R: RepositoryAccess> { + repo_access: &'a R, +} +``` + +Features: +- Full pack generation for initial clones +- Incremental pack generation for fetches +- Efficient object traversal and collection +- Streaming pack output + +#### (2) Pack Decoding + +Built on the internal pack module for robust pack file handling: +- Delta object reconstruction +- Streaming pack decoding +- Memory-efficient processing +- Error recovery and validation + +### 6. Integration Benefits + +#### (1) Transport Independence + +The same Git protocol logic works across: +- HTTP/HTTPS web servers +- SSH servers +- Custom transport protocols +- Local file system access + +#### (2) Business Logic Separation + +The trait-based design allows: +- Integration with any storage backend +- Custom authentication strategies +- Flexible repository management +- Easy testing and mocking + +#### (3) Framework Agnostic + +The library doesn't depend on: +- Specific web frameworks (Axum, Warp, etc.) +- SSH libraries +- Database systems +- Authentication providers + +### 7. Usage Example + +```rust +use git_internal::{GitProtocol, RepositoryAccess, AuthenticationService}; + +// Implement traits for your specific backend +struct MyRepository; +struct MyAuth; + +#[async_trait] +impl RepositoryAccess for MyRepository { + // Implement storage operations... +} + +#[async_trait] +impl AuthenticationService for MyAuth { + // Implement authentication... +} + +// Use with HTTP +let http_handler = HttpGitHandler::new(MyRepository, MyAuth); +// Enforce auth at the entry (framework provides headers) +http_handler.authenticate_http(&headers).await?; +let response = http_handler.handle_info_refs("/repo.git/info/refs", "service=git-upload-pack").await?; + +// Use with SSH +let ssh_handler = SshGitHandler::new(MyRepository, MyAuth); +// Authenticate once after SSH handshake +ssh_handler.authenticate_ssh(username, public_key).await?; +let response = ssh_handler.handle_upload_pack("repo.git", &request_data).await?; + +// Error handling +// On authentication failure, methods return ProtocolError::Unauthorized("...") +``` + +### 8. Migration Benefits + +The abstraction provides significant benefits over monolithic implementations: + + • **Reduced Coupling**: Business logic is separated from Git protocol details + • **Improved Testability**: Each layer can be tested independently + • **Enhanced Reusability**: The same protocol logic works across different systems + • **Simplified Maintenance**: Changes to transport or storage don't affect protocol logic + • **Better Performance**: Optimized pack handling and streaming operations + +### 9. Design Notes + +- Removed the default `update_ref` method from `RepositoryAccess` to prevent misuse. Callers should always use `update_reference(repo_path, ...)` with an explicit repository path. +- Corrected examples to match the implementation: + - `AuthenticationService::{authenticate_http, authenticate_ssh}` return `Result<(), ProtocolError>`. + - `RepositoryAccess` includes `has_default_branch` and `post_receive_hook` utilities. + - Optional helpers exist with default implementations: `get_blob`, `get_commit`, `get_tree`, `commit_exists`, and `handle_pack_objects(repo_path, ...)`. + +### 10. Mega Mapping (for migration reference) + +The abstractions were derived from the original Mega codebase. This section summarizes the semantic mapping to ensure consistency: + +- Repository operations + - `get_repository_refs` → Mega: list refs (e.g., `ceres` storage API) + - `has_object` / `get_object` → Mega: object DB and pack lookups + - `store_pack_data` → Mega: pack ingestion pipeline +- Reference management + - `update_reference` → Mega: ref update with old/new hash checks (monorepo refs) + - `has_default_branch` → Mega: read default branch metadata + - `post_receive_hook` → Mega: post-receive runner dispatch +- Pack traversal + - `get_objects_for_pack` → Mega: traversal used by pack generation (fetch/clone) +- Authentication + - HTTP: header-based auth aligned with Mega `mono` HTTP auth + - SSH: key-based auth aligned with Mega `mono` SSH auth + +### 11. Non-Goals + +- No LFS, or other extensions are required if they are not present in the original Mega implementation. The library focuses strictly on smart protocol migration and abstraction. + +This design successfully achieves the goal of extracting Git protocol handling from the Mega monorepo while providing a clean, reusable abstraction for any Git-based system. + + +## 12. Capabilities & Feature Matrix + +This section summarizes supported capabilities and how they map to transport handlers: + +- Capability advertisement: `info/refs` exposes negotiated capability lists (`COMMON_CAP_LIST`, `UPLOAD_CAP_LIST`, `RECEIVE_CAP_LIST`). +- Side-band channels: multiplexed streams supported via `SmartProtocol::build_side_band_format`. +- Pack features: thin-pack, ofs-delta, shallow clones handled by pack encode/decode modules. +- Reference updates: atomic update semantics enforced via `update_reference(repo_path, old, new)`. +- Authentication gate: handlers call `authenticate_http` / `authenticate_ssh` prior to serving operations. + +## 13. Error Handling & Mapping + +Protocol errors are framework-agnostic and live under `protocol/types.rs`: + +- Errors represent Git protocol semantics (e.g., `InvalidService`, `RepositoryNotFound`, `InvalidRequest`). +- Storage and parsing failures are wrapped using helpers like `repository_error(...)`. +- Transport adapters are responsible for mapping errors to framework responses. + +Example adapter-side mapping (do not couple the library to HTTP frameworks): + +```rust +// In application layer (e.g. HTTP framework adapter) +fn map_protocol_error_to_http(e: ProtocolError) -> http::StatusCode { + use http::StatusCode; + match e { + ProtocolError::InvalidService(_) => StatusCode::BAD_REQUEST, + ProtocolError::RepositoryNotFound(_) => StatusCode::NOT_FOUND, + ProtocolError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + ProtocolError::RepositoryError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } +} +``` + +## 14. Security & Performance Considerations + +- Input validation: strict parsing for service/query/path; reject non-Git endpoints early. +- Resource limits: stream-based pack processing to avoid large memory spikes; consider timeouts and rate limiting at transport layer. +- DoS resilience: cap negotiation and bounded buffering to prevent unbounded allocations. +- Authentication: perform auth before any expensive operations; prefer fail-fast. +- Logging: use `tracing` for structured logs; avoid leaking sensitive data in errors. + +## 15. Testing Strategy & CI + +- Unit tests: cover pkt-line parsing, capability negotiation, ref command parsing. +- Property tests: object parsing and hash invariants using `quickcheck` where applicable. +- Integration tests: pack encode/decode streams on curated datasets (can be long-running). +- CI pipeline: GitHub Actions runs `cargo check`, `cargo clippy -- -D warnings`, and `cargo test`. Long-running tests may be split or marked for scheduled runs if needed. + +## 16. Adoption Guide (Trait-based Integration) + +Steps to integrate with any business system: + +- Implement `RepositoryAccess` for your storage backend: + - Provide refs/object IO, pack storage, ref updates, and optional helpers. +- Implement `AuthenticationService` for your auth strategy: + - Validate HTTP headers or SSH public keys. +- Wire transport adapters: + - HTTP: construct `HttpGitHandler` and route `info/refs`, `upload-pack`, `receive-pack`. + - SSH: construct `SshGitHandler` and dispatch `git-upload-pack` / `git-receive-pack`. +- Keep error mapping in the application layer to stay framework-agnostic. + +Example skeleton: + +```rust +let repo = MyRepoBackend::new(); +let auth = MyAuthService::new(); +let mut http = HttpGitHandler::new(repo.clone(), auth.clone()); +http.authenticate_http(&headers).await?; +let (bytes, content_type) = http.handle_info_refs("/repo.git/info/refs", "service=git-upload-pack").await?; + +let mut ssh = SshGitHandler::new(repo, auth); +ssh.authenticate_ssh(user, key).await?; +let stream = ssh.handle_upload_pack("repo.git", &request).await?; +``` \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..07b8cd52 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,59 @@ +use serde::{Deserialize, Deserializer, Serialize}; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PackConfig { + #[serde(deserialize_with = "string_or_usize")] + pub pack_decode_mem_size: String, + #[serde(deserialize_with = "string_or_usize")] + pub pack_decode_disk_size: String, + pub pack_decode_cache_path: PathBuf, + pub clean_cache_after_decode: bool, + pub channel_message_size: usize, +} + +impl Default for PackConfig { + fn default() -> Self { + Self { + pack_decode_mem_size: "4G".to_string(), + pack_decode_disk_size: "20%".to_string(), + pack_decode_cache_path: PathBuf::from("pack_decode_cache"), + clean_cache_after_decode: true, + channel_message_size: 1_000_000, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct LfsConfig { + pub enable: bool, + pub host: String, + pub port: u16, +} + +impl Default for LfsConfig { + fn default() -> Self { + Self { + enable: false, + host: "localhost".to_string(), + port: 8080, + } + } +} + +fn string_or_usize<'deserialize, D>(deserializer: D) -> Result +where + D: Deserializer<'deserialize>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrUSize { + String(String), + USize(usize), + } + + Ok(match StringOrUSize::deserialize(deserializer)? { + StringOrUSize::String(v) => v, + StringOrUSize::USize(v) => v.to_string(), + }) +} diff --git a/src/lib.rs b/src/lib.rs index a76bbaf9..3ba34e54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! Git-Internal: a high-performance Rust library for Git objects and pack files—encode/decode, delta (ref/offset/zstd), +//! Git-Internal: a high-performance Rust library for Git objects and pack files—encode/decode, delta (ref/offset/zstd), //! caching, streaming, and sync/async pipelines. //! //! Goals @@ -30,10 +30,17 @@ //! Test Data //! - Located under `tests/data/`, includes real pack files and object sets. +pub mod config; pub mod errors; pub mod hash; pub mod internal; +pub mod protocol; pub mod utils; +mod delta; mod zstdelta; -mod delta; \ No newline at end of file + +// Core traits and types that external users need to implement/use +pub use protocol::{ + AuthenticationService, GitProtocol, ProtocolError, RepositoryAccess, ServiceType, +}; diff --git a/src/protocol/core.rs b/src/protocol/core.rs new file mode 100644 index 00000000..33d04bc9 --- /dev/null +++ b/src/protocol/core.rs @@ -0,0 +1,317 @@ +//! Core Git protocol implementation +//! +//! This module provides the main `GitProtocol` struct and `RepositoryAccess` trait +//! that form the core interface of the git-internal library. + +use async_trait::async_trait; +use bytes::Bytes; +use futures::stream::{Stream, StreamExt}; +use std::collections::HashMap; +use std::pin::Pin; +use std::str::FromStr; + +use crate::hash::SHA1; +use crate::internal::object::ObjectTrait; + +use super::smart::SmartProtocol; +use super::types::{ProtocolError, ServiceType}; + +/// Repository access trait for storage operations +/// +/// This trait only handles storage-level operations, not Git protocol details. +/// The git-internal library handles all Git protocol formatting and parsing. +#[async_trait] +pub trait RepositoryAccess: Send + Sync + Clone { + /// Get repository references as raw (name, hash) pairs + async fn get_repository_refs( + &self, + repo_path: &str, + ) -> Result, ProtocolError>; + + /// Check if an object exists in the repository + async fn has_object(&self, repo_path: &str, object_hash: &str) -> Result; + + /// Get raw object data by hash + async fn get_object( + &self, + repo_path: &str, + object_hash: &str, + ) -> Result, ProtocolError>; + + /// Store pack data in the repository + async fn store_pack_data(&self, repo_path: &str, pack_data: &[u8]) + -> Result<(), ProtocolError>; + + /// Update a single reference + async fn update_reference( + &self, + repo_path: &str, + ref_name: &str, + old_hash: Option<&str>, + new_hash: &str, + ) -> Result<(), ProtocolError>; + + /// Get objects needed for pack generation + async fn get_objects_for_pack( + &self, + repo_path: &str, + wants: &[String], + haves: &[String], + ) -> Result, ProtocolError>; + + /// Check if repository has a default branch + async fn has_default_branch(&self, repo_path: &str) -> Result; + + /// Post-receive hook after successful push + async fn post_receive_hook(&self, repo_path: &str) -> Result<(), ProtocolError>; + + /// Get blob data by hash (with repo_path) + /// + /// Default implementation parses the object data using the internal object module. + /// Override this method if you need custom blob handling logic. + async fn get_blob( + &self, + repo_path: &str, + object_hash: &str, + ) -> Result { + let data = self.get_object(repo_path, object_hash).await?; + let hash = SHA1::from_str(object_hash) + .map_err(|e| ProtocolError::repository_error(format!("Invalid hash format: {}", e)))?; + + crate::internal::object::blob::Blob::from_bytes(&data, hash) + .map_err(|e| ProtocolError::repository_error(format!("Failed to parse blob: {}", e))) + } + + /// Get commit data by hash (with repo_path) + /// + /// Default implementation parses the object data using the internal object module. + /// Override this method if you need custom commit handling logic. + async fn get_commit( + &self, + repo_path: &str, + commit_hash: &str, + ) -> Result { + let data = self.get_object(repo_path, commit_hash).await?; + let hash = SHA1::from_str(commit_hash) + .map_err(|e| ProtocolError::repository_error(format!("Invalid hash format: {}", e)))?; + + crate::internal::object::commit::Commit::from_bytes(&data, hash) + .map_err(|e| ProtocolError::repository_error(format!("Failed to parse commit: {}", e))) + } + + /// Get tree data by hash (with repo_path) + /// + /// Default implementation parses the object data using the internal object module. + /// Override this method if you need custom tree handling logic. + async fn get_tree( + &self, + repo_path: &str, + tree_hash: &str, + ) -> Result { + let data = self.get_object(repo_path, tree_hash).await?; + let hash = SHA1::from_str(tree_hash) + .map_err(|e| ProtocolError::repository_error(format!("Invalid hash format: {}", e)))?; + + crate::internal::object::tree::Tree::from_bytes(&data, hash) + .map_err(|e| ProtocolError::repository_error(format!("Failed to parse tree: {}", e))) + } + + /// Check if a commit exists (with repo_path) + /// + /// Default implementation checks object existence and validates it's a commit. + /// Override this method if you have more efficient commit existence checking. + async fn commit_exists( + &self, + repo_path: &str, + commit_hash: &str, + ) -> Result { + match self.has_object(repo_path, commit_hash).await { + Ok(exists) => { + if !exists { + return Ok(false); + } + + // Verify it's actually a commit by trying to parse it + match self.get_commit(repo_path, commit_hash).await { + Ok(_) => Ok(true), + Err(_) => Ok(false), // Object exists but is not a valid commit + } + } + Err(e) => Err(e), + } + } + + /// Handle pack objects (commits, trees, blobs) after unpacking (with repo_path) + /// + /// Default implementation stores each object individually using store_pack_data. + /// Override this method if you need batch processing or custom storage logic. + async fn handle_pack_objects( + &self, + repo_path: &str, + commits: Vec, + trees: Vec, + blobs: Vec, + ) -> Result<(), ProtocolError> { + // Store blobs + for blob in blobs { + let data = blob.to_data().map_err(|e| { + ProtocolError::repository_error(format!("Failed to serialize blob: {}", e)) + })?; + self.store_pack_data(repo_path, &data).await.map_err(|e| { + ProtocolError::repository_error(format!("Failed to store blob {}: {}", blob.id, e)) + })?; + } + + // Store trees + for tree in trees { + let data = tree.to_data().map_err(|e| { + ProtocolError::repository_error(format!("Failed to serialize tree: {}", e)) + })?; + self.store_pack_data(repo_path, &data).await.map_err(|e| { + ProtocolError::repository_error(format!("Failed to store tree {}: {}", tree.id, e)) + })?; + } + + // Store commits + for commit in commits { + let data = commit.to_data().map_err(|e| { + ProtocolError::repository_error(format!("Failed to serialize commit: {}", e)) + })?; + self.store_pack_data(repo_path, &data).await.map_err(|e| { + ProtocolError::repository_error(format!( + "Failed to store commit {}: {}", + commit.id, e + )) + })?; + } + + Ok(()) + } +} + +/// Authentication service trait +#[async_trait] +pub trait AuthenticationService: Send + Sync { + /// Authenticate HTTP request + async fn authenticate_http( + &self, + headers: &std::collections::HashMap, + ) -> Result<(), ProtocolError>; + + /// Authenticate SSH public key + async fn authenticate_ssh( + &self, + username: &str, + public_key: &[u8], + ) -> Result<(), ProtocolError>; +} + +/// Transport-agnostic Git smart protocol handler +/// Main Git protocol handler +/// +/// This struct provides the core Git protocol implementation that works +/// across HTTP, SSH, and other transports. It uses SmartProtocol internally +/// to handle all Git protocol details. +pub struct GitProtocol { + smart_protocol: SmartProtocol, +} + +impl GitProtocol { + /// Create a new GitProtocol instance + pub fn new(repo_access: R, auth_service: A) -> Self { + Self { + smart_protocol: SmartProtocol::new( + super::types::TransportProtocol::Http, + repo_access, + auth_service, + ), + } + } + + /// Authenticate HTTP request before serving Git operations + pub async fn authenticate_http( + &self, + headers: &HashMap, + ) -> Result<(), ProtocolError> { + self.smart_protocol.authenticate_http(headers).await + } + + /// Authenticate SSH session before serving Git operations + pub async fn authenticate_ssh( + &self, + username: &str, + public_key: &[u8], + ) -> Result<(), ProtocolError> { + self.smart_protocol + .authenticate_ssh(username, public_key) + .await + } + + /// Set transport protocol (Http, Ssh, etc.) + pub fn set_transport(&mut self, protocol: super::types::TransportProtocol) { + self.smart_protocol.set_transport_protocol(protocol); + } + + /// Handle git info-refs request + pub async fn info_refs( + &mut self, + repo_path: &str, + service: &str, + ) -> Result, ProtocolError> { + let service_type = match service { + "git-upload-pack" => ServiceType::UploadPack, + "git-receive-pack" => ServiceType::ReceivePack, + _ => return Err(ProtocolError::InvalidService(service.to_string())), + }; + + self.smart_protocol.set_service_type(service_type); + self.smart_protocol + .set_repository_path(repo_path.to_string()); + + self.smart_protocol + .git_info_refs() + .await + .map(|bytes| bytes.to_vec()) + } + + /// Handle git-upload-pack request (for clone/fetch) + pub async fn upload_pack( + &mut self, + repo_path: &str, + request_data: &[u8], + ) -> Result> + Send>>, ProtocolError> + { + self.smart_protocol + .set_service_type(ServiceType::UploadPack); + self.smart_protocol + .set_repository_path(repo_path.to_string()); + + let mut request_bytes = bytes::Bytes::from(request_data.to_vec()); + let (stream, _) = self + .smart_protocol + .git_upload_pack(&mut request_bytes) + .await?; + Ok(Box::pin(stream.map(|data| Ok(Bytes::from(data))))) + } + + /// Handle git-receive-pack request (for push) + pub async fn receive_pack( + &mut self, + repo_path: &str, + request_stream: Pin> + Send>>, + ) -> Result> + Send>>, ProtocolError> + { + self.smart_protocol + .set_service_type(ServiceType::ReceivePack); + self.smart_protocol + .set_repository_path(repo_path.to_string()); + + let pack_config = crate::config::PackConfig::default(); + let result_bytes = self + .smart_protocol + .git_receive_pack_stream(request_stream, &pack_config) + .await?; + // Return the report status as a single-chunk stream + Ok(Box::pin(futures::stream::once(async { Ok(result_bytes) }))) + } +} diff --git a/src/protocol/http.rs b/src/protocol/http.rs new file mode 100644 index 00000000..8e5bf36d --- /dev/null +++ b/src/protocol/http.rs @@ -0,0 +1,186 @@ +use super::core::{AuthenticationService, GitProtocol, RepositoryAccess}; +use super::types::ProtocolError; +use bytes::Bytes; +use futures::stream::Stream; +/// HTTP transport adapter for Git protocol +/// +/// This module provides HTTP-specific handling for Git smart protocol operations. +/// It's a thin wrapper around the core GitProtocol that handles HTTP-specific +/// request/response formatting and uses the utility functions for proper HTTP handling. +use serde::Deserialize; +use std::collections::HashMap; +use std::pin::Pin; + +/// HTTP Git protocol handler +pub struct HttpGitHandler { + protocol: GitProtocol, +} + +impl HttpGitHandler { + /// Create a new HTTP Git handler + pub fn new(repo_access: R, auth_service: A) -> Self { + let mut protocol = GitProtocol::new(repo_access, auth_service); + protocol.set_transport(super::types::TransportProtocol::Http); + Self { protocol } + } + + /// Authenticate the HTTP request using provided headers + /// Call this before invoking handle_* methods if your server requires auth + pub async fn authenticate_http( + &self, + headers: &HashMap, + ) -> Result<(), ProtocolError> { + self.protocol.authenticate_http(headers).await + } + + /// Handle HTTP info/refs request + /// + /// Processes GET requests to /{repo}/info/refs?service=git-{service} + /// Uses extract_repo_path and get_service_from_query for proper parsing + pub async fn handle_info_refs( + &mut self, + request_path: &str, + query: &str, + ) -> Result<(Vec, &'static str), ProtocolError> { + // Extract repository path from request + let repo_path = extract_repo_path(request_path) + .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?; + + // Get service from query parameters + let service = get_service_from_query(query).ok_or_else(|| { + ProtocolError::InvalidRequest("Missing service parameter".to_string()) + })?; + + // Validate it's a Git request + if !is_git_request(request_path) { + return Err(ProtocolError::InvalidRequest( + "Not a Git request".to_string(), + )); + } + + let response_data = self.protocol.info_refs(repo_path, service).await?; + let content_type = get_advertisement_content_type(service); + + Ok((response_data, content_type)) + } + + /// Handle HTTP upload-pack request + /// + /// Processes POST requests to /{repo}/git-upload-pack + pub async fn handle_upload_pack( + &mut self, + request_path: &str, + request_body: &[u8], + ) -> Result< + ( + Pin> + Send>>, + &'static str, + ), + ProtocolError, + > { + // Extract repository path from request + let repo_path = extract_repo_path(request_path) + .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?; + + // Validate it's a Git request + if !is_git_request(request_path) { + return Err(ProtocolError::InvalidRequest( + "Not a Git request".to_string(), + )); + } + + let response_stream = self.protocol.upload_pack(repo_path, request_body).await?; + let content_type = get_content_type("git-upload-pack"); + + Ok((response_stream, content_type)) + } + + /// Handle HTTP receive-pack request + /// + /// Processes POST requests to /{repo}/git-receive-pack + pub async fn handle_receive_pack( + &mut self, + request_path: &str, + request_stream: Pin> + Send>>, + ) -> Result< + ( + Pin> + Send>>, + &'static str, + ), + ProtocolError, + > { + // Extract repository path from request + let repo_path = extract_repo_path(request_path) + .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?; + + // Validate it's a Git request + if !is_git_request(request_path) { + return Err(ProtocolError::InvalidRequest( + "Not a Git request".to_string(), + )); + } + + let response_stream = self + .protocol + .receive_pack(repo_path, request_stream) + .await?; + let content_type = get_content_type("git-receive-pack"); + + Ok((response_stream, content_type)) + } +} + +/// HTTP-specific utility functions +/// Get content type for Git HTTP responses +pub fn get_content_type(service: &str) -> &'static str { + match service { + "git-upload-pack" => "application/x-git-upload-pack-result", + "git-receive-pack" => "application/x-git-receive-pack-result", + _ => "application/x-git-upload-pack-advertisement", + } +} + +/// Get content type for Git HTTP info/refs advertisement +pub fn get_advertisement_content_type(service: &str) -> &'static str { + match service { + "git-upload-pack" => "application/x-git-upload-pack-advertisement", + "git-receive-pack" => "application/x-git-receive-pack-advertisement", + _ => "application/x-git-upload-pack-advertisement", + } +} + +/// Check if request is a Git smart protocol request +pub fn is_git_request(path: &str) -> bool { + path.ends_with("/info/refs") + || path.ends_with("/git-upload-pack") + || path.ends_with("/git-receive-pack") +} + +/// Extract repository path from HTTP request path +pub fn extract_repo_path(path: &str) -> Option<&str> { + if let Some(pos) = path.rfind("/info/refs") { + Some(&path[..pos]) + } else if let Some(pos) = path.rfind("/git-upload-pack") { + Some(&path[..pos]) + } else if let Some(pos) = path.rfind("/git-receive-pack") { + Some(&path[..pos]) + } else { + None + } +} + +/// Get Git service from query parameters +pub fn get_service_from_query(query: &str) -> Option<&str> { + for param in query.split('&') { + if let Some(("service", value)) = param.split_once('=') { + return Some(value); + } + } + None +} + +/// Parameters for git info-refs request +#[derive(Debug, Deserialize)] +pub struct InfoRefsParams { + pub service: String, +} diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs new file mode 100644 index 00000000..e5666295 --- /dev/null +++ b/src/protocol/mod.rs @@ -0,0 +1,16 @@ +/// Git Protocol Module +/// +/// This module provides a clean, minimal, and transport-agnostic Git smart protocol implementation. +/// It abstracts away the complexities of different transport layers (HTTP, SSH) and provides +/// a unified interface for Git operations. +pub mod core; +pub mod http; +pub mod pack; +pub mod smart; +pub mod ssh; +pub mod types; +pub mod utils; + +// Re-export main interfaces +pub use core::{AuthenticationService, GitProtocol, RepositoryAccess}; +pub use types::*; diff --git a/src/protocol/pack.rs b/src/protocol/pack.rs new file mode 100644 index 00000000..e9cc4503 --- /dev/null +++ b/src/protocol/pack.rs @@ -0,0 +1,510 @@ +use bytes::Bytes; +use std::collections::{HashSet, VecDeque}; +use std::io::Cursor; +use tokio; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; + +use super::core::RepositoryAccess; +use super::types::ProtocolError; +use crate::internal::object::types::ObjectType; +use crate::internal::object::{ObjectTrait, blob::Blob, commit::Commit, tree::Tree}; +use crate::internal::pack::{Pack, encode::PackEncoder, entry::Entry}; + +/// Pack generation service for Git protocol operations +/// +/// This handles the core Git pack generation logic internally within git-internal, +/// using the RepositoryAccess trait only for data access. +pub struct PackGenerator<'a, R> +where + R: RepositoryAccess, +{ + repo_access: &'a R, + repo_path: &'a str, +} + +impl<'a, R> PackGenerator<'a, R> +where + R: RepositoryAccess, +{ + pub fn new(repo_access: &'a R, repo_path: &'a str) -> Self { + Self { + repo_access, + repo_path, + } + } + + /// Generate a full pack containing all requested objects + pub async fn generate_full_pack( + &self, + want: Vec, + ) -> Result>, ProtocolError> { + let (tx, rx) = mpsc::channel(1024); + + // Collect all objects needed for the wanted commits + let all_objects = self.collect_all_objects(want).await?; + + // Generate pack data + tokio::spawn(async move { + if let Err(e) = Self::generate_pack_stream(all_objects, tx).await { + tracing::error!("Failed to generate pack stream: {}", e); + } + }); + + Ok(ReceiverStream::new(rx)) + } + + /// Generate an incremental pack containing only objects not in 'have' + pub async fn generate_incremental_pack( + &self, + want: Vec, + have: Vec, + ) -> Result>, ProtocolError> { + let (tx, rx) = mpsc::channel(1024); + + // Collect objects for wanted commits + let wanted_objects = self.collect_all_objects(want).await?; + + // Collect objects for have commits (to exclude) + let have_objects = self.collect_all_objects(have).await?; + + // Filter out objects that are already in 'have' + let incremental_objects = Self::filter_objects(wanted_objects, have_objects); + + // Generate pack data + tokio::spawn(async move { + if let Err(e) = Self::generate_pack_stream(incremental_objects, tx).await { + tracing::error!("Failed to generate incremental pack stream: {}", e); + } + }); + + Ok(ReceiverStream::new(rx)) + } + + /// Unpack incoming pack stream and extract objects + pub async fn unpack_stream( + &self, + pack_data: Bytes, + ) -> Result<(Vec, Vec, Vec), ProtocolError> { + use std::sync::{Arc, Mutex}; + + let commits = Arc::new(Mutex::new(Vec::new())); + let trees = Arc::new(Mutex::new(Vec::new())); + let blobs = Arc::new(Mutex::new(Vec::new())); + + let commits_clone = commits.clone(); + let trees_clone = trees.clone(); + let blobs_clone = blobs.clone(); + + // Create a Pack instance for decoding + let mut pack = Pack::new(None, None, None, true); + let mut cursor = Cursor::new(pack_data.to_vec()); + + // Decode the pack and collect entries + pack.decode( + &mut cursor, + move |entry: Entry, _offset: usize| match entry.obj_type { + ObjectType::Commit => { + if let Ok(commit) = Commit::from_bytes(&entry.data, entry.hash) { + commits_clone.lock().unwrap().push(commit); + } else { + tracing::warn!("Failed to parse commit from pack entry"); + } + } + ObjectType::Tree => { + if let Ok(tree) = Tree::from_bytes(&entry.data, entry.hash) { + trees_clone.lock().unwrap().push(tree); + } else { + tracing::warn!("Failed to parse tree from pack entry"); + } + } + ObjectType::Blob => { + if let Ok(blob) = Blob::from_bytes(&entry.data, entry.hash) { + blobs_clone.lock().unwrap().push(blob); + } else { + tracing::warn!("Failed to parse blob from pack entry"); + } + } + _ => { + tracing::warn!("Unknown object type in pack: {:?}", entry.obj_type); + } + }, + ) + .map_err(|e| ProtocolError::invalid_request(&format!("Failed to decode pack: {}", e)))?; + + // Extract the results + let commits_result = Arc::try_unwrap(commits).unwrap().into_inner().unwrap(); + let trees_result = Arc::try_unwrap(trees).unwrap().into_inner().unwrap(); + let blobs_result = Arc::try_unwrap(blobs).unwrap().into_inner().unwrap(); + + Ok((commits_result, trees_result, blobs_result)) + } + + /// Collect all objects reachable from the given commit hashes + async fn collect_all_objects( + &self, + commit_hashes: Vec, + ) -> Result<(Vec, Vec, Vec), ProtocolError> { + let mut commits = Vec::new(); + let mut trees = Vec::new(); + let mut blobs = Vec::new(); + + let mut visited_commits = HashSet::new(); + let mut visited_trees = HashSet::new(); + let mut visited_blobs = HashSet::new(); + + let mut commit_queue = VecDeque::from(commit_hashes); + + // BFS traversal of commit graph + while let Some(commit_hash) = commit_queue.pop_front() { + if visited_commits.contains(&commit_hash) { + continue; + } + visited_commits.insert(commit_hash.clone()); + + // Get commit object + let commit = self + .repo_access + .get_commit(self.repo_path, &commit_hash) + .await + .map_err(|e| { + ProtocolError::repository_error(format!( + "Failed to get commit {}: {}", + commit_hash, e + )) + })?; + + // Add parent commits to queue + for parent in &commit.parent_commit_ids { + let parent_str = parent.to_string(); + if !visited_commits.contains(&parent_str) { + commit_queue.push_back(parent_str); + } + } + + // Collect tree objects + Box::pin(self.collect_tree_objects( + &commit.tree_id.to_string(), + &mut trees, + &mut blobs, + &mut visited_trees, + &mut visited_blobs, + )) + .await?; + + commits.push(commit); + } + + Ok((commits, trees, blobs)) + } + + /// Recursively collect tree and blob objects + async fn collect_tree_objects( + &self, + tree_hash: &str, + trees: &mut Vec, + blobs: &mut Vec, + visited_trees: &mut HashSet, + visited_blobs: &mut HashSet, + ) -> Result<(), ProtocolError> { + if visited_trees.contains(tree_hash) { + return Ok(()); + } + visited_trees.insert(tree_hash.to_string()); + + let tree = self + .repo_access + .get_tree(self.repo_path, tree_hash) + .await + .map_err(|e| { + ProtocolError::repository_error(format!("Failed to get tree {}: {}", tree_hash, e)) + })?; + + for entry in &tree.tree_items { + let entry_hash = entry.id.to_string(); + match entry.mode { + crate::internal::object::tree::TreeItemMode::Tree => { + Box::pin(self.collect_tree_objects( + &entry_hash, + trees, + blobs, + visited_trees, + visited_blobs, + )) + .await?; + } + crate::internal::object::tree::TreeItemMode::Blob + | crate::internal::object::tree::TreeItemMode::BlobExecutable => { + if !visited_blobs.contains(&entry_hash) { + visited_blobs.insert(entry_hash.clone()); + let blob = self + .repo_access + .get_blob(self.repo_path, &entry_hash) + .await + .map_err(|e| { + ProtocolError::repository_error(format!( + "Failed to get blob {}: {}", + entry_hash, e + )) + })?; + blobs.push(blob); + } + } + _ => {} + } + } + + trees.push(tree); + Ok(()) + } + + /// Filter objects to exclude those already in 'have' + fn filter_objects( + wanted: (Vec, Vec, Vec), + have: (Vec, Vec, Vec), + ) -> (Vec, Vec, Vec) { + let (wanted_commits, wanted_trees, wanted_blobs) = wanted; + let (have_commits, have_trees, have_blobs) = have; + + // Create hash sets for efficient lookup + let have_commit_hashes: HashSet = + have_commits.iter().map(|c| c.id.to_string()).collect(); + let have_tree_hashes: HashSet = + have_trees.iter().map(|t| t.id.to_string()).collect(); + let have_blob_hashes: HashSet = + have_blobs.iter().map(|b| b.id.to_string()).collect(); + + // Filter out objects that are in 'have' + let filtered_commits: Vec = wanted_commits + .into_iter() + .filter(|c| !have_commit_hashes.contains(&c.id.to_string())) + .collect(); + + let filtered_trees: Vec = wanted_trees + .into_iter() + .filter(|t| !have_tree_hashes.contains(&t.id.to_string())) + .collect(); + + let filtered_blobs: Vec = wanted_blobs + .into_iter() + .filter(|b| !have_blob_hashes.contains(&b.id.to_string())) + .collect(); + + (filtered_commits, filtered_trees, filtered_blobs) + } + + /// Generate pack stream from objects + async fn generate_pack_stream( + objects: (Vec, Vec, Vec), + tx: mpsc::Sender>, + ) -> Result<(), ProtocolError> { + let (commits, trees, blobs) = objects; + + // Convert objects to entries + let mut entries = Vec::new(); + + for commit in commits { + entries.push(Entry::from(commit)); + } + + for tree in trees { + entries.push(Entry::from(tree)); + } + + for blob in blobs { + entries.push(Entry::from(blob)); + } + + // Create PackEncoder and encode entries + let (pack_tx, mut pack_rx) = mpsc::channel(1024); + let (entry_tx, entry_rx) = mpsc::channel(1024); + let mut encoder = PackEncoder::new(entries.len(), 10, pack_tx); // window_size = 10 + + // Spawn encoding task + tokio::spawn(async move { + if let Err(e) = encoder.encode(entry_rx).await { + tracing::error!("Failed to encode pack: {}", e); + } + }); + + // Send entries to encoder + tokio::spawn(async move { + for entry in entries { + if entry_tx.send(entry).await.is_err() { + break; // Receiver dropped + } + } + // Drop sender to signal end of entries + }); + + // Forward pack data to output channel + while let Some(chunk) = pack_rx.recv().await { + if tx.send(chunk).await.is_err() { + break; // Receiver dropped + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::internal::object::blob::Blob; + use crate::internal::object::commit::Commit; + use crate::internal::object::signature::{Signature, SignatureType}; + use crate::internal::object::tree::{Tree, TreeItem, TreeItemMode}; + use async_trait::async_trait; + use bytes::Bytes; + + #[derive(Clone)] + struct DummyRepoAccess; + + #[async_trait] + impl RepositoryAccess for DummyRepoAccess { + async fn get_repository_refs( + &self, + _repo_path: &str, + ) -> Result, ProtocolError> { + Ok(vec![]) + } + async fn has_object( + &self, + _repo_path: &str, + _object_hash: &str, + ) -> Result { + Ok(false) + } + async fn get_object( + &self, + _repo_path: &str, + _object_hash: &str, + ) -> Result, ProtocolError> { + Err(ProtocolError::repository_error( + "not implemented".to_string(), + )) + } + async fn store_pack_data( + &self, + _repo_path: &str, + _pack_data: &[u8], + ) -> Result<(), ProtocolError> { + Ok(()) + } + async fn update_reference( + &self, + _repo_path: &str, + _ref_name: &str, + _old_hash: Option<&str>, + _new_hash: &str, + ) -> Result<(), ProtocolError> { + Ok(()) + } + async fn get_objects_for_pack( + &self, + _repo_path: &str, + _wants: &[String], + _haves: &[String], + ) -> Result, ProtocolError> { + Ok(vec![]) + } + async fn has_default_branch(&self, _repo_path: &str) -> Result { + Ok(false) + } + async fn post_receive_hook(&self, _repo_path: &str) -> Result<(), ProtocolError> { + Ok(()) + } + } + + #[tokio::test] + async fn test_pack_roundtrip_encode_decode() { + // 构造两个 Blob + let blob1 = Blob::from_content("hello"); + let blob2 = Blob::from_content("world"); + + // 构造一个 Tree,包含两个文件项 + let item1 = TreeItem::new(TreeItemMode::Blob, blob1.id, "hello.txt".to_string()); + let item2 = TreeItem::new(TreeItemMode::Blob, blob2.id, "world.txt".to_string()); + let tree = Tree::from_tree_items(vec![item1, item2]).unwrap(); + + // 构造一个 Commit 指向该 Tree + let author = Signature::new( + SignatureType::Author, + "tester".to_string(), + "tester@example.com".to_string(), + ); + let committer = Signature::new( + SignatureType::Committer, + "tester".to_string(), + "tester@example.com".to_string(), + ); + let commit = Commit::new(author, committer, tree.id, vec![], "init commit"); + + // 生成 pack 流 + let (tx, mut rx) = mpsc::channel::>(64); + PackGenerator::::generate_pack_stream( + ( + vec![commit.clone()], + vec![tree.clone()], + vec![blob1.clone(), blob2.clone()], + ), + tx, + ) + .await + .unwrap(); + + let mut pack_bytes: Vec = Vec::new(); + while let Some(chunk) = rx.recv().await { + pack_bytes.extend_from_slice(&chunk); + } + println!("Encoded pack size: {} bytes", pack_bytes.len()); + + // 解包 pack 流 + let dummy = DummyRepoAccess; + let generator = PackGenerator::new(&dummy, "test-repo-path"); + let (decoded_commits, decoded_trees, decoded_blobs) = generator + .unpack_stream(Bytes::from(pack_bytes)) + .await + .unwrap(); + + println!( + "Decoded commits: {:?}", + decoded_commits + .iter() + .map(|c| c.id.to_string()) + .collect::>() + ); + println!( + "Decoded trees: {:?}", + decoded_trees + .iter() + .map(|t| t.id.to_string()) + .collect::>() + ); + println!( + "Decoded blobs: {:?}", + decoded_blobs + .iter() + .map(|b| b.id.to_string()) + .collect::>() + ); + + // 验证对象 ID 往返一致 + assert_eq!(decoded_commits.len(), 1); + assert_eq!(decoded_trees.len(), 1); + assert_eq!(decoded_blobs.len(), 2); + + assert_eq!(decoded_commits[0].id, commit.id); + assert_eq!(decoded_trees[0].id, tree.id); + + let mut orig_blob_ids = vec![blob1.id.to_string(), blob2.id.to_string()]; + orig_blob_ids.sort(); + let mut decoded_blob_ids = decoded_blobs + .iter() + .map(|b| b.id.to_string()) + .collect::>(); + decoded_blob_ids.sort(); + assert_eq!(orig_blob_ids, decoded_blob_ids); + } +} diff --git a/src/protocol/smart.rs b/src/protocol/smart.rs new file mode 100644 index 00000000..4748a778 --- /dev/null +++ b/src/protocol/smart.rs @@ -0,0 +1,642 @@ +use std::pin::Pin; + +use bytes::{BufMut, Bytes, BytesMut}; +use futures::Stream; +use std::collections::HashMap; +use tokio_stream::wrappers::ReceiverStream; + +use super::core::{AuthenticationService, RepositoryAccess}; +use super::pack::PackGenerator; +use super::types::ProtocolError; +use super::types::{ + COMMON_CAP_LIST, Capability, LF, NUL, PKT_LINE_END_MARKER, RECEIVE_CAP_LIST, RefCommand, + RefTypeEnum, SP, ServiceType, SideBind, TransportProtocol, UPLOAD_CAP_LIST, ZERO_ID, +}; +use super::utils::{add_pkt_line_string, build_smart_reply, read_pkt_line, read_until_white_space}; +use crate::config::PackConfig; + +/// Smart Git Protocol implementation +/// +/// This struct handles the Git smart protocol operations for both HTTP and SSH transports. +/// It uses trait abstractions to decouple from specific business logic implementations. +pub struct SmartProtocol +where + R: RepositoryAccess, + A: AuthenticationService, +{ + pub transport_protocol: TransportProtocol, + pub service_type: Option, + pub capabilities: Vec, + pub side_bind: Option, + pub command_list: Vec, + pub repo_path: Option, + + // Trait-based dependencies + repo_storage: R, + auth_service: A, +} + +impl SmartProtocol +where + R: RepositoryAccess, + A: AuthenticationService, +{ + /// Create a new SmartProtocol instance + pub fn new(transport_protocol: TransportProtocol, repo_storage: R, auth_service: A) -> Self { + Self { + transport_protocol, + service_type: None, + capabilities: Vec::new(), + side_bind: None, + command_list: Vec::new(), + repo_path: None, + repo_storage, + auth_service, + } + } + + /// Authenticate an HTTP request using the injected auth service + pub async fn authenticate_http( + &self, + headers: &HashMap, + ) -> Result<(), ProtocolError> { + // Authentication service now returns Result<(), ProtocolError>. + // Propagate Unauthorized or other errors directly. + self.auth_service.authenticate_http(headers).await?; + Ok(()) + } + + /// Authenticate an SSH session using username and public key + pub async fn authenticate_ssh( + &self, + username: &str, + public_key: &[u8], + ) -> Result<(), ProtocolError> { + // Authentication service now returns Result<(), ProtocolError>. + // Propagate Unauthorized or other errors directly. + self.auth_service + .authenticate_ssh(username, public_key) + .await?; + Ok(()) + } + + /// Set the service type for this protocol session + pub fn set_service_type(&mut self, service_type: ServiceType) { + self.service_type = Some(service_type); + } + + /// Get the current service type + pub fn get_service_type(&self) -> Option { + self.service_type + } + + /// Set the repository path + pub fn set_repository_path(&mut self, repo_path: String) { + self.repo_path = Some(repo_path); + } + + /// Set transport protocol (Http, Ssh, etc.) + pub fn set_transport_protocol(&mut self, protocol: TransportProtocol) { + self.transport_protocol = protocol; + } + + /// Get git info refs for the repository + pub async fn git_info_refs(&self) -> Result { + let service_type = self + .service_type + .ok_or_else(|| ProtocolError::repository_error("Service type not set".to_string()))?; + + let repo_path = self.repo_path.as_ref().ok_or_else(|| { + ProtocolError::repository_error("Repository path not set".to_string()) + })?; + + let refs = self + .repo_storage + .get_repository_refs(repo_path) + .await + .map_err(|e| ProtocolError::repository_error(format!("Failed to get refs: {}", e)))?; + + // Convert to the expected format (head_hash, git_refs) + let head_hash = refs + .iter() + .find(|(name, _)| { + name == "HEAD" || name.ends_with("/main") || name.ends_with("/master") + }) + .map(|(_, hash)| hash.clone()) + .unwrap_or_else(|| "0000000000000000000000000000000000000000".to_string()); + + let git_refs: Vec = refs + .into_iter() + .map(|(name, hash)| super::types::GitRef { name, hash }) + .collect(); + + // Determine capabilities based on service type + let cap_list = match service_type { + ServiceType::UploadPack => format!("{UPLOAD_CAP_LIST}{COMMON_CAP_LIST}"), + ServiceType::ReceivePack => format!("{RECEIVE_CAP_LIST}{COMMON_CAP_LIST}"), + }; + + // The stream MUST include capability declarations behind a NUL on the first ref. + let name = if head_hash == ZERO_ID { + "capabilities^{}" + } else { + "HEAD" + }; + let pkt_line = format!("{head_hash}{SP}{name}{NUL}{cap_list}{LF}"); + let mut ref_list = vec![pkt_line]; + + for git_ref in git_refs { + let pkt_line = format!("{}{}{}{}", git_ref.hash, SP, git_ref.name, LF); + ref_list.push(pkt_line); + } + + let pkt_line_stream = + build_smart_reply(self.transport_protocol, &ref_list, service_type.to_string()); + tracing::debug!("git_info_refs, return: --------> {:?}", pkt_line_stream); + Ok(pkt_line_stream) + } + + /// Handle git upload-pack operation (fetch/clone) + pub async fn git_upload_pack( + &mut self, + upload_request: &mut Bytes, + ) -> Result<(ReceiverStream>, BytesMut), ProtocolError> { + let mut want: Vec = Vec::new(); + let mut have: Vec = Vec::new(); + let mut last_common_commit = String::new(); + + let mut read_first_line = false; + loop { + let (bytes_take, pkt_line) = read_pkt_line(upload_request); + + if bytes_take == 0 { + break; + } + + if pkt_line.is_empty() { + break; + } + + let mut pkt_line = pkt_line; + let command = read_until_white_space(&mut pkt_line); + + match command.as_str() { + "want" => { + let hash = read_until_white_space(&mut pkt_line); + want.push(hash); + if !read_first_line { + let cap_str = String::from_utf8_lossy(&pkt_line).to_string(); + self.parse_capabilities(&cap_str); + read_first_line = true; + } + } + "have" => { + let hash = read_until_white_space(&mut pkt_line); + have.push(hash); + } + "done" => { + break; + } + _ => { + tracing::warn!("Unknown upload-pack command: {}", command); + } + } + } + + let mut protocol_buf = BytesMut::new(); + + // Create pack generator for this operation + let repo_path = self.repo_path.as_deref().unwrap_or(""); + let pack_generator = PackGenerator::new(&self.repo_storage, repo_path); + + if have.is_empty() { + // Full pack + add_pkt_line_string(&mut protocol_buf, String::from("NAK\n")); + let pack_stream = pack_generator.generate_full_pack(want).await?; + return Ok((pack_stream, protocol_buf)); + } + + // Check for common commits + for hash in &have { + let exists = self + .repo_storage + .commit_exists(repo_path, hash) + .await + .map_err(|e| { + ProtocolError::repository_error(format!( + "Failed to check commit existence: {}", + e + )) + })?; + if exists { + add_pkt_line_string(&mut protocol_buf, format!("ACK {hash} common\n")); + if last_common_commit.is_empty() { + last_common_commit = hash.clone(); + } + } + } + + if last_common_commit.is_empty() { + // No common commits found + add_pkt_line_string(&mut protocol_buf, String::from("NAK\n")); + let pack_stream = pack_generator.generate_full_pack(want).await?; + return Ok((pack_stream, protocol_buf)); + } + + // Generate incremental pack + add_pkt_line_string( + &mut protocol_buf, + format!("ACK {last_common_commit} ready\n"), + ); + protocol_buf.put(&PKT_LINE_END_MARKER[..]); + + add_pkt_line_string(&mut protocol_buf, format!("ACK {last_common_commit} \n")); + + let pack_stream = pack_generator.generate_incremental_pack(want, have).await?; + + Ok((pack_stream, protocol_buf)) + } + + /// Parse receive pack commands from protocol bytes + pub fn parse_receive_pack_commands(&mut self, mut protocol_bytes: Bytes) { + loop { + let (bytes_take, pkt_line) = read_pkt_line(&mut protocol_bytes); + + if bytes_take == 0 { + break; + } + + if pkt_line.is_empty() { + break; + } + + let ref_command = self.parse_ref_command(&mut pkt_line.clone()); + self.command_list.push(ref_command); + } + } + + /// Handle git receive-pack operation (push) + pub async fn git_receive_pack_stream( + &mut self, + data_stream: Pin> + Send>>, + _pack_config: &PackConfig, + ) -> Result { + // Collect all pack data from stream + let mut pack_data = BytesMut::new(); + let mut stream = data_stream; + + while let Some(chunk_result) = futures::StreamExt::next(&mut stream).await { + let chunk = chunk_result + .map_err(|e| ProtocolError::invalid_request(&format!("Stream error: {}", e)))?; + pack_data.extend_from_slice(&chunk); + } + + // Create pack generator for unpacking + let repo_path = self.repo_path.as_deref().unwrap_or(""); + let pack_generator = PackGenerator::new(&self.repo_storage, repo_path); + + // Unpack the received data + let (commits, trees, blobs) = pack_generator.unpack_stream(pack_data.freeze()).await?; + + // Store the unpacked objects via the repository access trait + self.repo_storage + .handle_pack_objects(repo_path, commits, trees, blobs) + .await + .map_err(|e| { + ProtocolError::repository_error(format!("Failed to store pack objects: {}", e)) + })?; + + // Build status report + let mut report_status = BytesMut::new(); + add_pkt_line_string(&mut report_status, "unpack ok\n".to_owned()); + + let repo_path = self.repo_path.as_deref().unwrap_or(""); + let mut default_exist = self + .repo_storage + .has_default_branch(repo_path) + .await + .map_err(|e| { + ProtocolError::repository_error(format!("Failed to check default branch: {}", e)) + })?; + + // Update refs with proper error handling + for command in &mut self.command_list { + if command.ref_type == RefTypeEnum::Tag { + // Just update if refs type is tag + // Convert ZERO_ID to None for old hash + let old_hash = if command.old_hash == ZERO_ID { + None + } else { + Some(command.old_hash.as_str()) + }; + if let Err(e) = self + .repo_storage + .update_reference(repo_path, &command.ref_name, old_hash, &command.new_hash) + .await + { + command.failed(e.to_string()); + } + } else { + // Handle default branch setting for the first branch + if !default_exist { + command.default_branch = true; + default_exist = true; + } + // Convert ZERO_ID to None for old hash + let old_hash = if command.old_hash == ZERO_ID { + None + } else { + Some(command.old_hash.as_str()) + }; + if let Err(e) = self + .repo_storage + .update_reference(repo_path, &command.ref_name, old_hash, &command.new_hash) + .await + { + command.failed(e.to_string()); + } + } + add_pkt_line_string(&mut report_status, command.get_status()); + } + + // Post-receive hook + let repo_path = self.repo_path.as_deref().unwrap_or(""); + self.repo_storage + .post_receive_hook(repo_path) + .await + .map_err(|e| { + ProtocolError::repository_error(format!("Post-receive hook failed: {}", e)) + })?; + + report_status.put(&PKT_LINE_END_MARKER[..]); + Ok(report_status.freeze()) + } + + /// Builds the packet data in the sideband format if the SideBand/64k capability is enabled. + pub fn build_side_band_format(&self, from_bytes: BytesMut, length: usize) -> BytesMut { + let mut to_bytes = BytesMut::new(); + if self.capabilities.contains(&Capability::SideBand) + || self.capabilities.contains(&Capability::SideBand64k) + { + let length = length + 5; + to_bytes.put(Bytes::from(format!("{length:04x}"))); + to_bytes.put_u8(SideBind::PackfileData.value()); + to_bytes.put(from_bytes); + } else { + to_bytes.put(from_bytes); + } + to_bytes + } + + /// Parse capabilities from capability string + pub fn parse_capabilities(&mut self, cap_str: &str) { + for cap in cap_str.split_whitespace() { + if let Ok(capability) = cap.parse::() { + self.capabilities.push(capability); + } + } + } + + /// Parse a reference command from packet line + pub fn parse_ref_command(&self, pkt_line: &mut Bytes) -> RefCommand { + let old_id = read_until_white_space(pkt_line); + let new_id = read_until_white_space(pkt_line); + let ref_name = read_until_white_space(pkt_line); + let _capabilities = String::from_utf8_lossy(&pkt_line[..]).to_string(); + + RefCommand::new(old_id, new_id, ref_name) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::internal::object::blob::Blob; + use crate::internal::object::commit::Commit; + use crate::internal::object::signature::{Signature, SignatureType}; + use crate::internal::object::tree::{Tree, TreeItem, TreeItemMode}; + use crate::internal::pack::{encode::PackEncoder, entry::Entry}; + use crate::protocol::types::{RefCommand, ZERO_ID}; // import sibling types + use crate::protocol::utils; // import sibling module + use async_trait::async_trait; + use bytes::Bytes; + use futures; + use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }; + use tokio::sync::mpsc; + + // Simplify complex type via aliases to satisfy clippy::type_complexity + type UpdateRecord = (String, Option, String); + type UpdateList = Vec; + type SharedUpdates = Arc>; + + #[derive(Clone)] + struct TestRepoAccess { + updates: SharedUpdates, + stored_count: Arc>, + default_branch_exists: Arc>, + post_called: Arc, + } + + impl TestRepoAccess { + fn new() -> Self { + Self { + updates: Arc::new(Mutex::new(vec![])), + stored_count: Arc::new(Mutex::new(0)), + default_branch_exists: Arc::new(Mutex::new(false)), + post_called: Arc::new(AtomicBool::new(false)), + } + } + + fn updates_len(&self) -> usize { + self.updates.lock().unwrap().len() + } + + fn post_hook_called(&self) -> bool { + self.post_called.load(Ordering::SeqCst) + } + } + + #[async_trait] + impl RepositoryAccess for TestRepoAccess { + async fn get_repository_refs( + &self, + _repo_path: &str, + ) -> Result, ProtocolError> { + Ok(vec![]) + } + + async fn has_object( + &self, + _repo_path: &str, + _object_hash: &str, + ) -> Result { + Ok(false) + } + + async fn get_object( + &self, + _repo_path: &str, + _object_hash: &str, + ) -> Result, ProtocolError> { + Err(ProtocolError::repository_error( + "get_object not implemented".to_string(), + )) + } + + async fn store_pack_data( + &self, + _repo_path: &str, + _pack_data: &[u8], + ) -> Result<(), ProtocolError> { + *self.stored_count.lock().unwrap() += 1; + Ok(()) + } + + async fn update_reference( + &self, + _repo_path: &str, + ref_name: &str, + old_hash: Option<&str>, + new_hash: &str, + ) -> Result<(), ProtocolError> { + self.updates.lock().unwrap().push(( + ref_name.to_string(), + old_hash.map(|s| s.to_string()), + new_hash.to_string(), + )); + Ok(()) + } + + async fn get_objects_for_pack( + &self, + _repo_path: &str, + _wants: &[String], + _haves: &[String], + ) -> Result, ProtocolError> { + Ok(vec![]) + } + + async fn has_default_branch(&self, _repo_path: &str) -> Result { + Ok(*self.default_branch_exists.lock().unwrap()) + } + + async fn post_receive_hook(&self, _repo_path: &str) -> Result<(), ProtocolError> { + self.post_called.store(true, Ordering::SeqCst); + Ok(()) + } + } + + struct TestAuth; + + #[async_trait] + impl AuthenticationService for TestAuth { + async fn authenticate_http( + &self, + _headers: &std::collections::HashMap, + ) -> Result<(), ProtocolError> { + Ok(()) + } + + async fn authenticate_ssh( + &self, + _username: &str, + _public_key: &[u8], + ) -> Result<(), ProtocolError> { + Ok(()) + } + } + + #[tokio::test] + async fn test_receive_pack_stream_status_report() { + // Build simple objects + let blob1 = Blob::from_content("hello"); + let blob2 = Blob::from_content("world"); + + let item1 = TreeItem::new(TreeItemMode::Blob, blob1.id, "hello.txt".to_string()); + let item2 = TreeItem::new(TreeItemMode::Blob, blob2.id, "world.txt".to_string()); + let tree = Tree::from_tree_items(vec![item1, item2]).unwrap(); + + let author = Signature::new( + SignatureType::Author, + "tester".to_string(), + "tester@example.com".to_string(), + ); + let committer = Signature::new( + SignatureType::Committer, + "tester".to_string(), + "tester@example.com".to_string(), + ); + let commit = Commit::new(author, committer, tree.id, vec![], "init commit"); + + // Encode pack bytes via PackEncoder + let (pack_tx, mut pack_rx) = mpsc::channel(1024); + let (entry_tx, entry_rx) = mpsc::channel(1024); + let mut encoder = PackEncoder::new(4, 10, pack_tx); + + tokio::spawn(async move { + if let Err(e) = encoder.encode(entry_rx).await { + panic!("Failed to encode pack: {}", e); + } + }); + + let commit_clone = commit.clone(); + let tree_clone = tree.clone(); + let blob1_clone = blob1.clone(); + let blob2_clone = blob2.clone(); + tokio::spawn(async move { + let _ = entry_tx.send(Entry::from(commit_clone)).await; + let _ = entry_tx.send(Entry::from(tree_clone)).await; + let _ = entry_tx.send(Entry::from(blob1_clone)).await; + let _ = entry_tx.send(Entry::from(blob2_clone)).await; + // sender drop indicates end + }); + + let mut pack_bytes: Vec = Vec::new(); + while let Some(chunk) = pack_rx.recv().await { + pack_bytes.extend_from_slice(&chunk); + } + + // Prepare protocol and command + let repo_access = TestRepoAccess::new(); + let auth = TestAuth; + let mut smart = SmartProtocol::new(TransportProtocol::Http, repo_access.clone(), auth); + smart.set_repository_path("test-repo-path".to_string()); + smart.command_list.push(RefCommand::new( + ZERO_ID.to_string(), + commit.id.to_string(), + "refs/heads/main".to_string(), + )); + + // Create request stream + let request_stream = Box::pin(futures::stream::once(async { Ok(Bytes::from(pack_bytes)) })); + + // Execute receive-pack + let pack_config = crate::config::PackConfig::default(); + let result_bytes = smart + .git_receive_pack_stream(request_stream, &pack_config) + .await + .expect("receive-pack should succeed"); + + // Verify pkt-lines + let mut out = result_bytes.clone(); + let (_c1, l1) = utils::read_pkt_line(&mut out); + assert_eq!(String::from_utf8(l1.to_vec()).unwrap(), "unpack ok\n"); + + let (_c2, l2) = utils::read_pkt_line(&mut out); + assert_eq!( + String::from_utf8(l2.to_vec()).unwrap(), + "ok refs/heads/main" + ); + + let (c3, l3) = utils::read_pkt_line(&mut out); + assert_eq!(c3, 4); + assert!(l3.is_empty()); + + // Verify side effects + assert_eq!(repo_access.updates_len(), 1); + assert!(repo_access.post_hook_called()); + } +} diff --git a/src/protocol/ssh.rs b/src/protocol/ssh.rs new file mode 100644 index 00000000..523b8e3a --- /dev/null +++ b/src/protocol/ssh.rs @@ -0,0 +1,87 @@ +/// SSH transport adapter for Git protocol +/// +/// This module provides SSH-specific handling for Git smart protocol operations. +/// It's a thin wrapper around the core GitProtocol that handles SSH command +/// execution and data streaming. +use super::core::{AuthenticationService, GitProtocol, RepositoryAccess}; +use super::types::ProtocolError; +use bytes::Bytes; +use futures::stream::Stream; +use std::pin::Pin; + +/// SSH Git protocol handler +pub struct SshGitHandler { + protocol: GitProtocol, +} + +impl SshGitHandler { + /// Create a new SSH Git handler + pub fn new(repo_access: R, auth_service: A) -> Self { + let mut protocol = GitProtocol::new(repo_access, auth_service); + protocol.set_transport(super::types::TransportProtocol::Ssh); + Self { protocol } + } + + /// Authenticate SSH session using username and public key + /// Call this once after SSH handshake, before running Git commands + pub async fn authenticate_ssh( + &self, + username: &str, + public_key: &[u8], + ) -> Result<(), ProtocolError> { + self.protocol.authenticate_ssh(username, public_key).await + } + + /// Handle git-upload-pack command (for clone/fetch) + pub async fn handle_upload_pack( + &mut self, + repo_path: &str, + request_data: &[u8], + ) -> Result> + Send>>, ProtocolError> + { + self.protocol.upload_pack(repo_path, request_data).await + } + + /// Handle git-receive-pack command (for push) + pub async fn handle_receive_pack( + &mut self, + repo_path: &str, + request_stream: Pin> + Send>>, + ) -> Result> + Send>>, ProtocolError> + { + self.protocol.receive_pack(repo_path, request_stream).await + } + + /// Handle info/refs request for SSH + pub async fn handle_info_refs( + &mut self, + repo_path: &str, + service: &str, + ) -> Result, ProtocolError> { + self.protocol.info_refs(repo_path, service).await + } +} + +/// SSH-specific utility functions +/// Parse SSH command line into command and arguments +pub fn parse_ssh_command(command_line: &str) -> Option<(String, Vec)> { + let parts: Vec<&str> = command_line.split_whitespace().collect(); + if parts.is_empty() { + return None; + } + + let command = parts[0].to_string(); + let args = parts[1..].iter().map(|s| s.to_string()).collect(); + + Some((command, args)) +} + +/// Check if command is a valid Git SSH command +pub fn is_git_ssh_command(command: &str) -> bool { + matches!(command, "git-upload-pack" | "git-receive-pack") +} + +/// Extract repository path from SSH command arguments +pub fn extract_repo_path_from_args(args: &[String]) -> Option<&str> { + args.first().map(|s| s.as_str()) +} diff --git a/src/protocol/types.rs b/src/protocol/types.rs new file mode 100644 index 00000000..54c1227d --- /dev/null +++ b/src/protocol/types.rs @@ -0,0 +1,239 @@ +use std::fmt; +use std::str::FromStr; + +/// Protocol error types +#[derive(Debug, thiserror::Error)] +pub enum ProtocolError { + #[error("Invalid service: {0}")] + InvalidService(String), + + #[error("Repository not found: {0}")] + RepositoryNotFound(String), + + #[error("Object not found: {0}")] + ObjectNotFound(String), + + #[error("Invalid request: {0}")] + InvalidRequest(String), + + #[error("Unauthorized: {0}")] + Unauthorized(String), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Pack error: {0}")] + Pack(String), + + #[error("Internal error: {0}")] + Internal(String), +} + +impl ProtocolError { + pub fn invalid_service(service: &str) -> Self { + ProtocolError::InvalidService(service.to_string()) + } + + pub fn repository_error(msg: String) -> Self { + ProtocolError::Internal(msg) + } + + pub fn invalid_request(msg: &str) -> Self { + ProtocolError::InvalidRequest(msg.to_string()) + } + + pub fn unauthorized(msg: &str) -> Self { + ProtocolError::Unauthorized(msg.to_string()) + } +} + +/// Git transport protocol types +#[derive(Debug, PartialEq, Clone, Copy, Default)] +pub enum TransportProtocol { + Local, + #[default] + Http, + Ssh, + Git, + P2p, +} + +/// Git service types for smart protocol +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum ServiceType { + UploadPack, + ReceivePack, +} + +impl fmt::Display for ServiceType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ServiceType::UploadPack => write!(f, "git-upload-pack"), + ServiceType::ReceivePack => write!(f, "git-receive-pack"), + } + } +} + +impl FromStr for ServiceType { + type Err = ProtocolError; + + fn from_str(s: &str) -> Result { + match s { + "git-upload-pack" => Ok(ServiceType::UploadPack), + "git-receive-pack" => Ok(ServiceType::ReceivePack), + _ => Err(ProtocolError::InvalidService(s.to_string())), + } + } +} + +/// Git protocol capabilities +#[derive(Debug, Clone, PartialEq)] +pub enum Capability { + MultiAck, + MultiAckDetailed, + NoDone, + SideBand, + SideBand64k, + ReportStatus, + ReportStatusv2, + OfsDelta, + DeepenSince, + DeepenNot, +} + +impl FromStr for Capability { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "multi_ack" => Ok(Capability::MultiAck), + "multi_ack_detailed" => Ok(Capability::MultiAckDetailed), + "no-done" => Ok(Capability::NoDone), + "side-band" => Ok(Capability::SideBand), + "side-band-64k" => Ok(Capability::SideBand64k), + "report-status" => Ok(Capability::ReportStatus), + "report-status-v2" => Ok(Capability::ReportStatusv2), + "ofs-delta" => Ok(Capability::OfsDelta), + "deepen-since" => Ok(Capability::DeepenSince), + "deepen-not" => Ok(Capability::DeepenNot), + _ => Err(()), + } + } +} + +/// Side-band types for multiplexed data streams +pub enum SideBind { + /// Sideband 1 contains packfile data + PackfileData, + /// Sideband 2 contains progress information + ProgressInfo, + /// Sideband 3 contains error information + Error, +} + +impl SideBind { + pub fn value(&self) -> u8 { + match self { + Self::PackfileData => b'\x01', + Self::ProgressInfo => b'\x02', + Self::Error => b'\x03', + } + } +} + +/// Reference types in Git +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum RefTypeEnum { + Branch, + Tag, +} + +/// Git reference information +#[derive(Clone, Debug)] +pub struct GitRef { + pub name: String, + pub hash: String, +} + +/// Reference command for push operations +#[derive(Debug, Clone)] +pub struct RefCommand { + pub old_hash: String, + pub new_hash: String, + pub ref_name: String, + pub ref_type: RefTypeEnum, + pub default_branch: bool, + pub status: CommandStatus, + pub error_message: Option, +} + +#[derive(Debug, Clone)] +pub enum CommandStatus { + Pending, + Success, + Failed, +} + +impl RefCommand { + pub fn new(old_hash: String, new_hash: String, ref_name: String) -> Self { + // Determine ref type based on ref name + let ref_type = if ref_name.starts_with("refs/tags/") { + RefTypeEnum::Tag + } else { + RefTypeEnum::Branch + }; + + Self { + old_hash, + new_hash, + ref_name, + ref_type, + default_branch: false, + status: CommandStatus::Pending, + error_message: None, + } + } + + pub fn failed(&mut self, error: String) { + self.status = CommandStatus::Failed; + self.error_message = Some(error); + } + + pub fn success(&mut self) { + self.status = CommandStatus::Success; + self.error_message = None; + } + + pub fn get_status(&self) -> String { + match &self.status { + CommandStatus::Success => format!("ok {}", self.ref_name), + CommandStatus::Failed => { + let error = self.error_message.as_deref().unwrap_or("unknown error"); + format!("ng {} {}", self.ref_name, error) + } + CommandStatus::Pending => format!("ok {}", self.ref_name), // Default to ok for pending + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum CommandType { + Create, + Update, + Delete, +} + +/// Zero object ID constant +pub const ZERO_ID: &str = "0000000000000000000000000000000000000000"; + +/// Protocol constants +pub const LF: char = '\n'; +pub const SP: char = ' '; +pub const NUL: char = '\0'; +pub const PKT_LINE_END_MARKER: &[u8; 4] = b"0000"; + +// Git protocol capability lists +pub const RECEIVE_CAP_LIST: &str = + "report-status report-status-v2 delete-refs quiet atomic no-thin "; +pub const COMMON_CAP_LIST: &str = "side-band-64k ofs-delta agent=git-internal/0.1.0"; +pub const UPLOAD_CAP_LIST: &str = "multi_ack_detailed no-done include-tag "; diff --git a/src/protocol/utils.rs b/src/protocol/utils.rs new file mode 100644 index 00000000..e511ab97 --- /dev/null +++ b/src/protocol/utils.rs @@ -0,0 +1,116 @@ +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +use super::types::{PKT_LINE_END_MARKER, TransportProtocol}; + +/// Read a packet line from the given bytes buffer +/// +/// Returns a tuple of (bytes_consumed, packet_data) +/// +/// This is the original simple implementation from ceres +pub fn read_pkt_line(bytes: &mut Bytes) -> (usize, Bytes) { + if bytes.is_empty() { + return (0, Bytes::new()); + } + + // Ensure we have at least 4 bytes for the length prefix + if bytes.len() < 4 { + return (0, Bytes::new()); + } + + let pkt_length = bytes.copy_to_bytes(4); + let pkt_length_str = match core::str::from_utf8(&pkt_length) { + Ok(s) => s, + Err(_) => { + tracing::warn!("Invalid UTF-8 in packet length: {:?}", pkt_length); + return (0, Bytes::new()); + } + }; + + let pkt_length = match usize::from_str_radix(pkt_length_str, 16) { + Ok(len) => len, + Err(_) => { + tracing::warn!("Invalid hex packet length: {:?}", pkt_length_str); + return (0, Bytes::new()); + } + }; + + if pkt_length == 0 { + return (4, Bytes::new()); // Consumed 4 bytes for the "0000" marker + } + + if pkt_length < 4 { + tracing::warn!("Invalid packet length: {} (must be >= 4)", pkt_length); + return (0, Bytes::new()); + } + + let data_length = pkt_length - 4; + if bytes.len() < data_length { + tracing::warn!( + "Insufficient data: need {} bytes, have {}", + data_length, + bytes.len() + ); + return (0, Bytes::new()); + } + + // this operation will change the original bytes + let pkt_line = bytes.copy_to_bytes(data_length); + tracing::debug!("pkt line: {:?}", pkt_line); + + (pkt_length, pkt_line) +} + +/// Add a packet line string to the buffer with proper length prefix +/// +/// This is the original simple implementation from ceres +pub fn add_pkt_line_string(pkt_line_stream: &mut BytesMut, buf_str: String) { + let buf_str_length = buf_str.len() + 4; + pkt_line_stream.put(Bytes::from(format!("{buf_str_length:04x}"))); + pkt_line_stream.put(buf_str.as_bytes()); +} + +/// Read until whitespace and return the extracted string +/// +/// This is the original implementation from ceres +pub fn read_until_white_space(bytes: &mut Bytes) -> String { + let mut buf = Vec::new(); + while bytes.has_remaining() { + let c = bytes.get_u8(); + if c.is_ascii_whitespace() || c == 0 { + break; + } + buf.push(c); + } + match String::from_utf8(buf) { + Ok(s) => s, + Err(e) => { + tracing::warn!("Invalid UTF-8 in protocol data: {}", e); + String::new() // Return empty string on invalid UTF-8 + } + } +} + +pub fn build_smart_reply( + transport_protocol: TransportProtocol, + ref_list: &Vec, + service: String, +) -> BytesMut { + let mut pkt_line_stream = BytesMut::new(); + if transport_protocol == TransportProtocol::Http { + add_pkt_line_string(&mut pkt_line_stream, format!("# service={service}\n")); + pkt_line_stream.put(&PKT_LINE_END_MARKER[..]); + } + + for ref_line in ref_list { + add_pkt_line_string(&mut pkt_line_stream, ref_line.to_string()); + } + pkt_line_stream.put(&PKT_LINE_END_MARKER[..]); + pkt_line_stream +} + +/// Search for a subsequence in a byte slice +pub fn search_subsequence(haystack: &[u8], needle: &[u8]) -> Option { + haystack + .windows(needle.len()) + .position(|window| window == needle) +} From ee6334c1508b782fb9ec199750d155f32ed45994 Mon Sep 17 00:00:00 2001 From: allure <1550220889@qq.com> Date: Mon, 20 Oct 2025 22:21:10 +0800 Subject: [PATCH 2/3] refactor: Address review feedback for protocol Signed-off-by: allure <1550220889@qq.com> --- Cargo.toml | 2 +- docs/PROTOCOL_ABSTRACTION.md | 133 ++++++++++++++++++++--------------- src/config.rs | 59 ---------------- src/hash.rs | 2 +- src/lib.rs | 1 - src/protocol/core.rs | 13 +--- src/protocol/smart.rs | 57 ++++++--------- src/protocol/types.rs | 92 +++++++++++++++++++++++- 8 files changed, 193 insertions(+), 166 deletions(-) delete mode 100644 src/config.rs diff --git a/Cargo.toml b/Cargo.toml index 2fecffee..22142bdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ tokio-stream = "0.1.17" http = "1.2.0" base64 = "0.22.1" # SSH server dependencies -russh = "0.52.1" +russh = "0.54.6" russh-keys = "0.49.2" hyper = "1.5.1" async-stream = "0.3.6" diff --git a/docs/PROTOCOL_ABSTRACTION.md b/docs/PROTOCOL_ABSTRACTION.md index 3efe4f94..2a6f040a 100644 --- a/docs/PROTOCOL_ABSTRACTION.md +++ b/docs/PROTOCOL_ABSTRACTION.md @@ -6,10 +6,10 @@ The git-internal library provides a clean, transport-agnostic abstraction for Gi The git-internal library implements a layered architecture that cleanly separates different concerns: - • **Transport Layer**: HTTP and SSH adapters that handle transport-specific details - • **Protocol Layer**: Core Git smart protocol implementation that is transport-agnostic - • **Business Logic Layer**: Trait-based abstractions for repository access and authentication - • **Storage Layer**: Internal Git object handling and pack file operations + • **Transport Layer**: HTTP and SSH adapters that handle transport-specific details + • **Protocol Layer**: Core Git smart protocol implementation that is transport-agnostic + • **Business Logic Layer**: Trait-based abstractions for repository access and authentication + • **Storage Layer**: Internal Git object handling and pack file operations This separation allows the same Git protocol logic to work across different transports and integrate with any storage backend or business system. @@ -27,10 +27,10 @@ pub trait RepositoryAccess: Send + Sync + Clone { async fn has_object(&self, repo_path: &str, object_hash: &str) -> Result; async fn get_object(&self, repo_path: &str, object_hash: &str) -> Result, ProtocolError>; async fn store_pack_data(&self, repo_path: &str, pack_data: &[u8]) -> Result<(), ProtocolError>; - + // Reference management async fn update_reference(&self, repo_path: &str, ref_name: &str, old_hash: Option<&str>, new_hash: &str) -> Result<(), ProtocolError>; - + // Pack operations async fn get_objects_for_pack(&self, repo_path: &str, wants: &[String], haves: &[String]) -> Result, ProtocolError>; @@ -59,7 +59,7 @@ pub trait AuthenticationService: Send + Sync { &self, headers: &std::collections::HashMap ) -> Result<(), ProtocolError>; - + /// Authenticate SSH connection using public key async fn authenticate_ssh(&self, username: &str, public_key: &[u8]) -> Result<(), ProtocolError>; } @@ -81,12 +81,32 @@ pub struct HttpGitHandler { impl HttpGitHandler { // Handle HTTP info/refs requests pub async fn handle_info_refs(&mut self, request_path: &str, query: &str) -> Result<(Vec, &'static str), ProtocolError>; - + // Handle HTTP upload-pack requests (clone/fetch) - pub async fn handle_upload_pack(&mut self, request_path: &str, request_body: &[u8]) -> Result<(Stream, &'static str), ProtocolError>; - + pub async fn handle_upload_pack( + &mut self, + request_path: &str, + request_body: &[u8], + ) -> Result< + ( + std::pin::Pin> + Send>>, + &'static str, + ), + ProtocolError, + >; + // Handle HTTP receive-pack requests (push) - pub async fn handle_receive_pack(&mut self, request_path: &str, request_stream: Stream) -> Result<(Stream, &'static str), ProtocolError>; + pub async fn handle_receive_pack( + &mut self, + request_path: &str, + request_stream: std::pin::Pin> + Send>>, + ) -> Result< + ( + std::pin::Pin> + Send>>, + &'static str, + ), + ProtocolError, + >; // Authenticate HTTP request using headers (call before handle_*) pub async fn authenticate_http(&self, headers: &std::collections::HashMap) -> Result<(), ProtocolError>; @@ -94,6 +114,7 @@ impl HttpGitHandler { ``` The HTTP handler includes utility functions for: + - Extracting repository paths from URLs - Parsing query parameters - Setting appropriate content types @@ -110,11 +131,15 @@ pub struct SshGitHandler { impl SshGitHandler { // Handle git-upload-pack command - pub async fn handle_upload_pack(&mut self, repo_path: &str, request_data: &[u8]) -> Result; - - // Handle git-receive-pack command - pub async fn handle_receive_pack(&mut self, repo_path: &str, request_stream: Stream) -> Result; - + pub async fn handle_upload_pack(&mut self, repo_path: &str, request_data: &[u8]) -> Result> + Send>>, ProtocolError>; + + // Handle git-receive-pack command + pub async fn handle_receive_pack( + &mut self, + repo_path: &str, + request_stream: std::pin::Pin> + Send>>, + ) -> Result> + Send>>, ProtocolError>; + // Handle info/refs for SSH pub async fn handle_info_refs(&mut self, repo_path: &str, service: &str) -> Result, ProtocolError>; @@ -124,6 +149,7 @@ impl SshGitHandler { ``` The SSH handler includes utility functions for: + - Parsing SSH command lines - Validating Git SSH commands - Extracting repository paths from arguments @@ -176,6 +202,7 @@ pub struct PackGenerator<'a, R: RepositoryAccess> { ``` Features: + - Full pack generation for initial clones - Incremental pack generation for fetches - Efficient object traversal and collection @@ -184,6 +211,7 @@ Features: #### (2) Pack Decoding Built on the internal pack module for robust pack file handling: + - Delta object reconstruction - Streaming pack decoding - Memory-efficient processing @@ -194,6 +222,7 @@ Built on the internal pack module for robust pack file handling: #### (1) Transport Independence The same Git protocol logic works across: + - HTTP/HTTPS web servers - SSH servers - Custom transport protocols @@ -202,6 +231,7 @@ The same Git protocol logic works across: #### (2) Business Logic Separation The trait-based design allows: + - Integration with any storage backend - Custom authentication strategies - Flexible repository management @@ -210,6 +240,7 @@ The trait-based design allows: #### (3) Framework Agnostic The library doesn't depend on: + - Specific web frameworks (Axum, Warp, etc.) - SSH libraries - Database systems @@ -229,7 +260,7 @@ impl RepositoryAccess for MyRepository { // Implement storage operations... } -#[async_trait] +#[async_trait] impl AuthenticationService for MyAuth { // Implement authentication... } @@ -240,7 +271,7 @@ let http_handler = HttpGitHandler::new(MyRepository, MyAuth); http_handler.authenticate_http(&headers).await?; let response = http_handler.handle_info_refs("/repo.git/info/refs", "service=git-upload-pack").await?; -// Use with SSH +// Use with SSH let ssh_handler = SshGitHandler::new(MyRepository, MyAuth); // Authenticate once after SSH handshake ssh_handler.authenticate_ssh(username, public_key).await?; @@ -250,15 +281,15 @@ let response = ssh_handler.handle_upload_pack("repo.git", &request_data).await?; // On authentication failure, methods return ProtocolError::Unauthorized("...") ``` -### 8. Migration Benefits +### 8. Advantages -The abstraction provides significant benefits over monolithic implementations: +The abstraction provides significant benefits: - • **Reduced Coupling**: Business logic is separated from Git protocol details - • **Improved Testability**: Each layer can be tested independently - • **Enhanced Reusability**: The same protocol logic works across different systems - • **Simplified Maintenance**: Changes to transport or storage don't affect protocol logic - • **Better Performance**: Optimized pack handling and streaming operations + • **Reduced Coupling**: Business logic is separated from Git protocol details + • **Improved Testability**: Each layer can be tested independently + • **Enhanced Reusability**: The same protocol logic works across different systems + • **Simplified Maintenance**: Changes to transport or storage don't affect protocol logic + • **Performance-Oriented**: Optimized pack handling and streaming operations ### 9. Design Notes @@ -268,42 +299,31 @@ The abstraction provides significant benefits over monolithic implementations: - `RepositoryAccess` includes `has_default_branch` and `post_receive_hook` utilities. - Optional helpers exist with default implementations: `get_blob`, `get_commit`, `get_tree`, `commit_exists`, and `handle_pack_objects(repo_path, ...)`. -### 10. Mega Mapping (for migration reference) - -The abstractions were derived from the original Mega codebase. This section summarizes the semantic mapping to ensure consistency: - -- Repository operations - - `get_repository_refs` → Mega: list refs (e.g., `ceres` storage API) - - `has_object` / `get_object` → Mega: object DB and pack lookups - - `store_pack_data` → Mega: pack ingestion pipeline -- Reference management - - `update_reference` → Mega: ref update with old/new hash checks (monorepo refs) - - `has_default_branch` → Mega: read default branch metadata - - `post_receive_hook` → Mega: post-receive runner dispatch -- Pack traversal - - `get_objects_for_pack` → Mega: traversal used by pack generation (fetch/clone) -- Authentication - - HTTP: header-based auth aligned with Mega `mono` HTTP auth - - SSH: key-based auth aligned with Mega `mono` SSH auth - -### 11. Non-Goals - -- No LFS, or other extensions are required if they are not present in the original Mega implementation. The library focuses strictly on smart protocol migration and abstraction. - -This design successfully achieves the goal of extracting Git protocol handling from the Mega monorepo while providing a clean, reusable abstraction for any Git-based system. - - -## 12. Capabilities & Feature Matrix +## 10. Capabilities & Feature Matrix This section summarizes supported capabilities and how they map to transport handlers: - Capability advertisement: `info/refs` exposes negotiated capability lists (`COMMON_CAP_LIST`, `UPLOAD_CAP_LIST`, `RECEIVE_CAP_LIST`). +- LFS advertisement: `COMMON_CAP_LIST` includes `lfs`; negotiation is supported, but object transfer/storage is application-layer (proxy/pass-through only). - Side-band channels: multiplexed streams supported via `SmartProtocol::build_side_band_format`. - Pack features: thin-pack, ofs-delta, shallow clones handled by pack encode/decode modules. - Reference updates: atomic update semantics enforced via `update_reference(repo_path, old, new)`. - Authentication gate: handlers call `authenticate_http` / `authenticate_ssh` prior to serving operations. -## 13. Error Handling & Mapping +Supported capability constants: + +```rust +pub const RECEIVE_CAP_LIST: &str = + "report-status report-status-v2 delete-refs quiet atomic no-thin "; +pub const COMMON_CAP_LIST: &str = + "side-band-64k ofs-delta lfs agent=git-internal/0.1.0"; +pub const UPLOAD_CAP_LIST: &str = + "multi_ack_detailed no-done include-tag "; +``` + +Note: trailing spaces in `RECEIVE_CAP_LIST` and `UPLOAD_CAP_LIST` are intentional; they concatenate with `COMMON_CAP_LIST` in `info/refs` advertisement. + +## 11. Error Handling & Mapping Protocol errors are framework-agnostic and live under `protocol/types.rs`: @@ -321,12 +341,13 @@ fn map_protocol_error_to_http(e: ProtocolError) -> http::StatusCode { ProtocolError::InvalidService(_) => StatusCode::BAD_REQUEST, ProtocolError::RepositoryNotFound(_) => StatusCode::NOT_FOUND, ProtocolError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + ProtocolError::Unauthorized(_) => StatusCode::UNAUTHORIZED, ProtocolError::RepositoryError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } ``` -## 14. Security & Performance Considerations +## 12. Security & Performance Considerations - Input validation: strict parsing for service/query/path; reject non-Git endpoints early. - Resource limits: stream-based pack processing to avoid large memory spikes; consider timeouts and rate limiting at transport layer. @@ -334,14 +355,14 @@ fn map_protocol_error_to_http(e: ProtocolError) -> http::StatusCode { - Authentication: perform auth before any expensive operations; prefer fail-fast. - Logging: use `tracing` for structured logs; avoid leaking sensitive data in errors. -## 15. Testing Strategy & CI +## 13. Testing Strategy & CI - Unit tests: cover pkt-line parsing, capability negotiation, ref command parsing. - Property tests: object parsing and hash invariants using `quickcheck` where applicable. - Integration tests: pack encode/decode streams on curated datasets (can be long-running). - CI pipeline: GitHub Actions runs `cargo check`, `cargo clippy -- -D warnings`, and `cargo test`. Long-running tests may be split or marked for scheduled runs if needed. -## 16. Adoption Guide (Trait-based Integration) +## 14. Adoption Guide (Trait-based Integration) Steps to integrate with any business system: @@ -366,4 +387,4 @@ let (bytes, content_type) = http.handle_info_refs("/repo.git/info/refs", "servic let mut ssh = SshGitHandler::new(repo, auth); ssh.authenticate_ssh(user, key).await?; let stream = ssh.handle_upload_pack("repo.git", &request).await?; -``` \ No newline at end of file +``` diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 07b8cd52..00000000 --- a/src/config.rs +++ /dev/null @@ -1,59 +0,0 @@ -use serde::{Deserialize, Deserializer, Serialize}; -use std::path::PathBuf; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct PackConfig { - #[serde(deserialize_with = "string_or_usize")] - pub pack_decode_mem_size: String, - #[serde(deserialize_with = "string_or_usize")] - pub pack_decode_disk_size: String, - pub pack_decode_cache_path: PathBuf, - pub clean_cache_after_decode: bool, - pub channel_message_size: usize, -} - -impl Default for PackConfig { - fn default() -> Self { - Self { - pack_decode_mem_size: "4G".to_string(), - pack_decode_disk_size: "20%".to_string(), - pack_decode_cache_path: PathBuf::from("pack_decode_cache"), - clean_cache_after_decode: true, - channel_message_size: 1_000_000, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct LfsConfig { - pub enable: bool, - pub host: String, - pub port: u16, -} - -impl Default for LfsConfig { - fn default() -> Self { - Self { - enable: false, - host: "localhost".to_string(), - port: 8080, - } - } -} - -fn string_or_usize<'deserialize, D>(deserializer: D) -> Result -where - D: Deserializer<'deserialize>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrUSize { - String(String), - USize(usize), - } - - Ok(match StringOrUSize::deserialize(deserializer)? { - StringOrUSize::String(v) => v, - StringOrUSize::USize(v) => v.to_string(), - }) -} diff --git a/src/hash.rs b/src/hash.rs index 85e5c898..9d22ca89 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -108,7 +108,7 @@ impl SHA1 { /// Calculate the SHA-1 hash of the byte slice, then create a Hash value pub fn new(data: &[u8]) -> SHA1 { let h = sha1::Sha1::digest(data); - SHA1::from_bytes(h.as_slice()) + SHA1::from_bytes(h.as_ref()) } /// Create a Hash from the object type and data /// This function is used to create a SHA1 hash from the object type and data. diff --git a/src/lib.rs b/src/lib.rs index 3ba34e54..0216f8a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,6 @@ //! Test Data //! - Located under `tests/data/`, includes real pack files and object sets. -pub mod config; pub mod errors; pub mod hash; pub mod internal; diff --git a/src/protocol/core.rs b/src/protocol/core.rs index 33d04bc9..fc4c24e1 100644 --- a/src/protocol/core.rs +++ b/src/protocol/core.rs @@ -265,11 +265,9 @@ impl GitProtocol { }; self.smart_protocol.set_service_type(service_type); - self.smart_protocol - .set_repository_path(repo_path.to_string()); self.smart_protocol - .git_info_refs() + .git_info_refs(repo_path) .await .map(|bytes| bytes.to_vec()) } @@ -283,13 +281,11 @@ impl GitProtocol { { self.smart_protocol .set_service_type(ServiceType::UploadPack); - self.smart_protocol - .set_repository_path(repo_path.to_string()); let mut request_bytes = bytes::Bytes::from(request_data.to_vec()); let (stream, _) = self .smart_protocol - .git_upload_pack(&mut request_bytes) + .git_upload_pack(repo_path, &mut request_bytes) .await?; Ok(Box::pin(stream.map(|data| Ok(Bytes::from(data))))) } @@ -303,13 +299,10 @@ impl GitProtocol { { self.smart_protocol .set_service_type(ServiceType::ReceivePack); - self.smart_protocol - .set_repository_path(repo_path.to_string()); - let pack_config = crate::config::PackConfig::default(); let result_bytes = self .smart_protocol - .git_receive_pack_stream(request_stream, &pack_config) + .git_receive_pack_stream(repo_path, request_stream) .await?; // Return the report status as a single-chunk stream Ok(Box::pin(futures::stream::once(async { Ok(result_bytes) }))) diff --git a/src/protocol/smart.rs b/src/protocol/smart.rs index 4748a778..3b538e2e 100644 --- a/src/protocol/smart.rs +++ b/src/protocol/smart.rs @@ -13,7 +13,6 @@ use super::types::{ RefTypeEnum, SP, ServiceType, SideBind, TransportProtocol, UPLOAD_CAP_LIST, ZERO_ID, }; use super::utils::{add_pkt_line_string, build_smart_reply, read_pkt_line, read_until_white_space}; -use crate::config::PackConfig; /// Smart Git Protocol implementation /// @@ -29,7 +28,6 @@ where pub capabilities: Vec, pub side_bind: Option, pub command_list: Vec, - pub repo_path: Option, // Trait-based dependencies repo_storage: R, @@ -49,7 +47,6 @@ where capabilities: Vec::new(), side_bind: None, command_list: Vec::new(), - repo_path: None, repo_storage, auth_service, } @@ -60,10 +57,7 @@ where &self, headers: &HashMap, ) -> Result<(), ProtocolError> { - // Authentication service now returns Result<(), ProtocolError>. - // Propagate Unauthorized or other errors directly. - self.auth_service.authenticate_http(headers).await?; - Ok(()) + self.auth_service.authenticate_http(headers).await } /// Authenticate an SSH session using username and public key @@ -72,12 +66,9 @@ where username: &str, public_key: &[u8], ) -> Result<(), ProtocolError> { - // Authentication service now returns Result<(), ProtocolError>. - // Propagate Unauthorized or other errors directly. self.auth_service .authenticate_ssh(username, public_key) - .await?; - Ok(()) + .await } /// Set the service type for this protocol session @@ -90,26 +81,17 @@ where self.service_type } - /// Set the repository path - pub fn set_repository_path(&mut self, repo_path: String) { - self.repo_path = Some(repo_path); - } - /// Set transport protocol (Http, Ssh, etc.) pub fn set_transport_protocol(&mut self, protocol: TransportProtocol) { self.transport_protocol = protocol; } /// Get git info refs for the repository - pub async fn git_info_refs(&self) -> Result { + pub async fn git_info_refs(&self, repo_path: &str) -> Result { let service_type = self .service_type .ok_or_else(|| ProtocolError::repository_error("Service type not set".to_string()))?; - let repo_path = self.repo_path.as_ref().ok_or_else(|| { - ProtocolError::repository_error("Repository path not set".to_string()) - })?; - let refs = self .repo_storage .get_repository_refs(repo_path) @@ -159,6 +141,7 @@ where /// Handle git upload-pack operation (fetch/clone) pub async fn git_upload_pack( &mut self, + repo_path: &str, upload_request: &mut Bytes, ) -> Result<(ReceiverStream>, BytesMut), ProtocolError> { let mut want: Vec = Vec::new(); @@ -206,7 +189,6 @@ where let mut protocol_buf = BytesMut::new(); // Create pack generator for this operation - let repo_path = self.repo_path.as_deref().unwrap_or(""); let pack_generator = PackGenerator::new(&self.repo_storage, repo_path); if have.is_empty() { @@ -278,8 +260,8 @@ where /// Handle git receive-pack operation (push) pub async fn git_receive_pack_stream( &mut self, + repo_path: &str, data_stream: Pin> + Send>>, - _pack_config: &PackConfig, ) -> Result { // Collect all pack data from stream let mut pack_data = BytesMut::new(); @@ -292,7 +274,6 @@ where } // Create pack generator for unpacking - let repo_path = self.repo_path.as_deref().unwrap_or(""); let pack_generator = PackGenerator::new(&self.repo_storage, repo_path); // Unpack the received data @@ -310,7 +291,6 @@ where let mut report_status = BytesMut::new(); add_pkt_line_string(&mut report_status, "unpack ok\n".to_owned()); - let repo_path = self.repo_path.as_deref().unwrap_or(""); let mut default_exist = self .repo_storage .has_default_branch(repo_path) @@ -360,7 +340,6 @@ where } // Post-receive hook - let repo_path = self.repo_path.as_deref().unwrap_or(""); self.repo_storage .post_receive_hook(repo_path) .await @@ -465,7 +444,16 @@ mod tests { &self, _repo_path: &str, ) -> Result, ProtocolError> { - Ok(vec![]) + Ok(vec![ + ( + "HEAD".to_string(), + "0000000000000000000000000000000000000000".to_string(), + ), + ( + "refs/heads/main".to_string(), + "1111111111111111111111111111111111111111".to_string(), + ), + ]) } async fn has_object( @@ -473,7 +461,7 @@ mod tests { _repo_path: &str, _object_hash: &str, ) -> Result { - Ok(false) + Ok(true) } async fn get_object( @@ -481,9 +469,7 @@ mod tests { _repo_path: &str, _object_hash: &str, ) -> Result, ProtocolError> { - Err(ProtocolError::repository_error( - "get_object not implemented".to_string(), - )) + Ok(vec![]) } async fn store_pack_data( @@ -520,7 +506,10 @@ mod tests { } async fn has_default_branch(&self, _repo_path: &str) -> Result { - Ok(*self.default_branch_exists.lock().unwrap()) + let mut exists = self.default_branch_exists.lock().unwrap(); + let current = *exists; + *exists = true; // flip to true after first check + Ok(current) } async fn post_receive_hook(&self, _repo_path: &str) -> Result<(), ProtocolError> { @@ -603,7 +592,6 @@ mod tests { let repo_access = TestRepoAccess::new(); let auth = TestAuth; let mut smart = SmartProtocol::new(TransportProtocol::Http, repo_access.clone(), auth); - smart.set_repository_path("test-repo-path".to_string()); smart.command_list.push(RefCommand::new( ZERO_ID.to_string(), commit.id.to_string(), @@ -614,9 +602,8 @@ mod tests { let request_stream = Box::pin(futures::stream::once(async { Ok(Bytes::from(pack_bytes)) })); // Execute receive-pack - let pack_config = crate::config::PackConfig::default(); let result_bytes = smart - .git_receive_pack_stream(request_stream, &pack_config) + .git_receive_pack_stream("test-repo-path", request_stream) .await .expect("receive-pack should succeed"); diff --git a/src/protocol/types.rs b/src/protocol/types.rs index 54c1227d..e2e3b43b 100644 --- a/src/protocol/types.rs +++ b/src/protocol/types.rs @@ -55,7 +55,6 @@ pub enum TransportProtocol { Http, Ssh, Git, - P2p, } /// Git service types for smart protocol @@ -99,12 +98,51 @@ pub enum Capability { OfsDelta, DeepenSince, DeepenNot, + ThinPack, + Shallow, + Deepen, + IncludeTag, + DeleteRefs, + Quiet, + Atomic, + NoThin, + AllowTipSha1InWant, + AllowReachableSha1InWant, + PushCert(String), + PushOptions, + ObjectFormat(String), + ServerOption(String), + SessionId(String), + PackfileUris(String), + Lfs, + Agent(String), + Unknown(String), } impl FromStr for Capability { type Err = (); fn from_str(s: &str) -> Result { + // Parameterized capabilities + if let Some(rest) = s.strip_prefix("agent=") { + return Ok(Capability::Agent(rest.to_string())); + } + if let Some(rest) = s.strip_prefix("server-option=") { + return Ok(Capability::ServerOption(rest.to_string())); + } + if let Some(rest) = s.strip_prefix("session-id=") { + return Ok(Capability::SessionId(rest.to_string())); + } + if let Some(rest) = s.strip_prefix("packfile-uris=") { + return Ok(Capability::PackfileUris(rest.to_string())); + } + if let Some(rest) = s.strip_prefix("push-cert=") { + return Ok(Capability::PushCert(rest.to_string())); + } + if let Some(rest) = s.strip_prefix("object-format=") { + return Ok(Capability::ObjectFormat(rest.to_string())); + } + match s { "multi_ack" => Ok(Capability::MultiAck), "multi_ack_detailed" => Ok(Capability::MultiAckDetailed), @@ -116,7 +154,55 @@ impl FromStr for Capability { "ofs-delta" => Ok(Capability::OfsDelta), "deepen-since" => Ok(Capability::DeepenSince), "deepen-not" => Ok(Capability::DeepenNot), - _ => Err(()), + "thin-pack" => Ok(Capability::ThinPack), + "shallow" => Ok(Capability::Shallow), + "deepen" => Ok(Capability::Deepen), + "include-tag" => Ok(Capability::IncludeTag), + "delete-refs" => Ok(Capability::DeleteRefs), + "quiet" => Ok(Capability::Quiet), + "atomic" => Ok(Capability::Atomic), + "no-thin" => Ok(Capability::NoThin), + "allow-tip-sha1-in-want" => Ok(Capability::AllowTipSha1InWant), + "allow-reachable-sha1-in-want" => Ok(Capability::AllowReachableSha1InWant), + "push-options" => Ok(Capability::PushOptions), + "lfs" => Ok(Capability::Lfs), + _ => Ok(Capability::Unknown(s.to_string())), + } + } +} + +impl std::fmt::Display for Capability { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Capability::MultiAck => write!(f, "multi_ack"), + Capability::MultiAckDetailed => write!(f, "multi_ack_detailed"), + Capability::NoDone => write!(f, "no-done"), + Capability::SideBand => write!(f, "side-band"), + Capability::SideBand64k => write!(f, "side-band-64k"), + Capability::ReportStatus => write!(f, "report-status"), + Capability::ReportStatusv2 => write!(f, "report-status-v2"), + Capability::OfsDelta => write!(f, "ofs-delta"), + Capability::DeepenSince => write!(f, "deepen-since"), + Capability::DeepenNot => write!(f, "deepen-not"), + Capability::ThinPack => write!(f, "thin-pack"), + Capability::Shallow => write!(f, "shallow"), + Capability::Deepen => write!(f, "deepen"), + Capability::IncludeTag => write!(f, "include-tag"), + Capability::DeleteRefs => write!(f, "delete-refs"), + Capability::Quiet => write!(f, "quiet"), + Capability::Atomic => write!(f, "atomic"), + Capability::NoThin => write!(f, "no-thin"), + Capability::AllowTipSha1InWant => write!(f, "allow-tip-sha1-in-want"), + Capability::AllowReachableSha1InWant => write!(f, "allow-reachable-sha1-in-want"), + Capability::PushCert(value) => write!(f, "push-cert={}", value), + Capability::PushOptions => write!(f, "push-options"), + Capability::ObjectFormat(format) => write!(f, "object-format={}", format), + Capability::ServerOption(option) => write!(f, "server-option={}", option), + Capability::SessionId(id) => write!(f, "session-id={}", id), + Capability::PackfileUris(uris) => write!(f, "packfile-uris={}", uris), + Capability::Lfs => write!(f, "lfs"), + Capability::Agent(agent) => write!(f, "agent={}", agent), + Capability::Unknown(s) => write!(f, "{}", s), } } } @@ -235,5 +321,5 @@ pub const PKT_LINE_END_MARKER: &[u8; 4] = b"0000"; // Git protocol capability lists pub const RECEIVE_CAP_LIST: &str = "report-status report-status-v2 delete-refs quiet atomic no-thin "; -pub const COMMON_CAP_LIST: &str = "side-band-64k ofs-delta agent=git-internal/0.1.0"; +pub const COMMON_CAP_LIST: &str = "side-band-64k ofs-delta lfs agent=git-internal/0.1.0"; pub const UPLOAD_CAP_LIST: &str = "multi_ack_detailed no-done include-tag "; From 9337d5bfb013fba88d5815f657c52152f7582bf6 Mon Sep 17 00:00:00 2001 From: allure <1550220889@qq.com> Date: Fri, 24 Oct 2025 08:40:20 +0800 Subject: [PATCH 3/3] fix: fix error about HTTP and SSH abstract Signed-off-by: allure <1550220889@qq.com> --- Cargo.toml | 2 +- src/protocol/core.rs | 104 +++++++++++---------------------- src/protocol/http.rs | 44 +++++--------- src/protocol/pack.rs | 79 ++++++++----------------- src/protocol/smart.rs | 130 +++++++++++++----------------------------- src/protocol/ssh.rs | 27 +++------ src/protocol/types.rs | 93 ++++++++++++++++++++++++------ src/protocol/utils.rs | 2 +- 8 files changed, 195 insertions(+), 286 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 22142bdb..a98adf7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ serde_json = "1.0.145" ahash = "0.8.12" diffs = "0.5.1" libc = "0.2.177" -zstd-sys = { version = "2.0.16", features = ["experimental"] } +zstd-sys = { version = "2.0.16+zstd.1.5.7", features = ["experimental"] } sea-orm = { version = "1.1.17", features = ["sqlx-sqlite"] } flate2 = { version = "1.1.4", features = ["zlib"] } serde = { version = "1.0.228", features = ["derive"] } diff --git a/src/protocol/core.rs b/src/protocol/core.rs index fc4c24e1..c99ef8c3 100644 --- a/src/protocol/core.rs +++ b/src/protocol/core.rs @@ -5,16 +5,15 @@ use async_trait::async_trait; use bytes::Bytes; -use futures::stream::{Stream, StreamExt}; +use futures::stream::StreamExt; use std::collections::HashMap; -use std::pin::Pin; use std::str::FromStr; use crate::hash::SHA1; use crate::internal::object::ObjectTrait; use super::smart::SmartProtocol; -use super::types::{ProtocolError, ServiceType}; +use super::types::{ProtocolError, ProtocolStream, ServiceType}; /// Repository access trait for storage operations /// @@ -23,29 +22,20 @@ use super::types::{ProtocolError, ServiceType}; #[async_trait] pub trait RepositoryAccess: Send + Sync + Clone { /// Get repository references as raw (name, hash) pairs - async fn get_repository_refs( - &self, - repo_path: &str, - ) -> Result, ProtocolError>; + async fn get_repository_refs(&self) -> Result, ProtocolError>; /// Check if an object exists in the repository - async fn has_object(&self, repo_path: &str, object_hash: &str) -> Result; + async fn has_object(&self, object_hash: &str) -> Result; /// Get raw object data by hash - async fn get_object( - &self, - repo_path: &str, - object_hash: &str, - ) -> Result, ProtocolError>; + async fn get_object(&self, object_hash: &str) -> Result, ProtocolError>; /// Store pack data in the repository - async fn store_pack_data(&self, repo_path: &str, pack_data: &[u8]) - -> Result<(), ProtocolError>; + async fn store_pack_data(&self, pack_data: &[u8]) -> Result<(), ProtocolError>; /// Update a single reference async fn update_reference( &self, - repo_path: &str, ref_name: &str, old_hash: Option<&str>, new_hash: &str, @@ -54,27 +44,25 @@ pub trait RepositoryAccess: Send + Sync + Clone { /// Get objects needed for pack generation async fn get_objects_for_pack( &self, - repo_path: &str, wants: &[String], haves: &[String], ) -> Result, ProtocolError>; /// Check if repository has a default branch - async fn has_default_branch(&self, repo_path: &str) -> Result; + async fn has_default_branch(&self) -> Result; /// Post-receive hook after successful push - async fn post_receive_hook(&self, repo_path: &str) -> Result<(), ProtocolError>; + async fn post_receive_hook(&self) -> Result<(), ProtocolError>; - /// Get blob data by hash (with repo_path) + /// Get blob data by hash /// /// Default implementation parses the object data using the internal object module. /// Override this method if you need custom blob handling logic. async fn get_blob( &self, - repo_path: &str, object_hash: &str, ) -> Result { - let data = self.get_object(repo_path, object_hash).await?; + let data = self.get_object(object_hash).await?; let hash = SHA1::from_str(object_hash) .map_err(|e| ProtocolError::repository_error(format!("Invalid hash format: {}", e)))?; @@ -82,16 +70,15 @@ pub trait RepositoryAccess: Send + Sync + Clone { .map_err(|e| ProtocolError::repository_error(format!("Failed to parse blob: {}", e))) } - /// Get commit data by hash (with repo_path) + /// Get commit data by hash /// /// Default implementation parses the object data using the internal object module. /// Override this method if you need custom commit handling logic. async fn get_commit( &self, - repo_path: &str, commit_hash: &str, ) -> Result { - let data = self.get_object(repo_path, commit_hash).await?; + let data = self.get_object(commit_hash).await?; let hash = SHA1::from_str(commit_hash) .map_err(|e| ProtocolError::repository_error(format!("Invalid hash format: {}", e)))?; @@ -99,16 +86,15 @@ pub trait RepositoryAccess: Send + Sync + Clone { .map_err(|e| ProtocolError::repository_error(format!("Failed to parse commit: {}", e))) } - /// Get tree data by hash (with repo_path) + /// Get tree data by hash /// /// Default implementation parses the object data using the internal object module. /// Override this method if you need custom tree handling logic. async fn get_tree( &self, - repo_path: &str, tree_hash: &str, ) -> Result { - let data = self.get_object(repo_path, tree_hash).await?; + let data = self.get_object(tree_hash).await?; let hash = SHA1::from_str(tree_hash) .map_err(|e| ProtocolError::repository_error(format!("Invalid hash format: {}", e)))?; @@ -116,23 +102,19 @@ pub trait RepositoryAccess: Send + Sync + Clone { .map_err(|e| ProtocolError::repository_error(format!("Failed to parse tree: {}", e))) } - /// Check if a commit exists (with repo_path) + /// Check if a commit exists /// /// Default implementation checks object existence and validates it's a commit. /// Override this method if you have more efficient commit existence checking. - async fn commit_exists( - &self, - repo_path: &str, - commit_hash: &str, - ) -> Result { - match self.has_object(repo_path, commit_hash).await { + async fn commit_exists(&self, commit_hash: &str) -> Result { + match self.has_object(commit_hash).await { Ok(exists) => { if !exists { return Ok(false); } // Verify it's actually a commit by trying to parse it - match self.get_commit(repo_path, commit_hash).await { + match self.get_commit(commit_hash).await { Ok(_) => Ok(true), Err(_) => Ok(false), // Object exists but is not a valid commit } @@ -141,13 +123,12 @@ pub trait RepositoryAccess: Send + Sync + Clone { } } - /// Handle pack objects (commits, trees, blobs) after unpacking (with repo_path) + /// Handle pack objects after unpacking /// /// Default implementation stores each object individually using store_pack_data. /// Override this method if you need batch processing or custom storage logic. async fn handle_pack_objects( &self, - repo_path: &str, commits: Vec, trees: Vec, blobs: Vec, @@ -157,7 +138,7 @@ pub trait RepositoryAccess: Send + Sync + Clone { let data = blob.to_data().map_err(|e| { ProtocolError::repository_error(format!("Failed to serialize blob: {}", e)) })?; - self.store_pack_data(repo_path, &data).await.map_err(|e| { + self.store_pack_data(&data).await.map_err(|e| { ProtocolError::repository_error(format!("Failed to store blob {}: {}", blob.id, e)) })?; } @@ -167,7 +148,7 @@ pub trait RepositoryAccess: Send + Sync + Clone { let data = tree.to_data().map_err(|e| { ProtocolError::repository_error(format!("Failed to serialize tree: {}", e)) })?; - self.store_pack_data(repo_path, &data).await.map_err(|e| { + self.store_pack_data(&data).await.map_err(|e| { ProtocolError::repository_error(format!("Failed to store tree {}: {}", tree.id, e)) })?; } @@ -177,7 +158,7 @@ pub trait RepositoryAccess: Send + Sync + Clone { let data = commit.to_data().map_err(|e| { ProtocolError::repository_error(format!("Failed to serialize commit: {}", e)) })?; - self.store_pack_data(repo_path, &data).await.map_err(|e| { + self.store_pack_data(&data).await.map_err(|e| { ProtocolError::repository_error(format!( "Failed to store commit {}: {}", commit.id, e @@ -253,56 +234,35 @@ impl GitProtocol { } /// Handle git info-refs request - pub async fn info_refs( - &mut self, - repo_path: &str, - service: &str, - ) -> Result, ProtocolError> { + pub async fn info_refs(&self, service: &str) -> Result, ProtocolError> { let service_type = match service { "git-upload-pack" => ServiceType::UploadPack, "git-receive-pack" => ServiceType::ReceivePack, - _ => return Err(ProtocolError::InvalidService(service.to_string())), + _ => return Err(ProtocolError::invalid_service(service)), }; - self.smart_protocol.set_service_type(service_type); - - self.smart_protocol - .git_info_refs(repo_path) - .await - .map(|bytes| bytes.to_vec()) + let bytes = self.smart_protocol.git_info_refs(service_type).await?; + Ok(bytes.to_vec()) } /// Handle git-upload-pack request (for clone/fetch) pub async fn upload_pack( &mut self, - repo_path: &str, request_data: &[u8], - ) -> Result> + Send>>, ProtocolError> - { - self.smart_protocol - .set_service_type(ServiceType::UploadPack); - - let mut request_bytes = bytes::Bytes::from(request_data.to_vec()); - let (stream, _) = self - .smart_protocol - .git_upload_pack(repo_path, &mut request_bytes) - .await?; + ) -> Result { + let request_bytes = bytes::Bytes::from(request_data.to_vec()); + let (stream, _) = self.smart_protocol.git_upload_pack(request_bytes).await?; Ok(Box::pin(stream.map(|data| Ok(Bytes::from(data))))) } /// Handle git-receive-pack request (for push) pub async fn receive_pack( &mut self, - repo_path: &str, - request_stream: Pin> + Send>>, - ) -> Result> + Send>>, ProtocolError> - { - self.smart_protocol - .set_service_type(ServiceType::ReceivePack); - + request_stream: ProtocolStream, + ) -> Result { let result_bytes = self .smart_protocol - .git_receive_pack_stream(repo_path, request_stream) + .git_receive_pack_stream(request_stream) .await?; // Return the report status as a single-chunk stream Ok(Box::pin(futures::stream::once(async { Ok(result_bytes) }))) diff --git a/src/protocol/http.rs b/src/protocol/http.rs index 8e5bf36d..3dab9c62 100644 --- a/src/protocol/http.rs +++ b/src/protocol/http.rs @@ -1,7 +1,5 @@ use super::core::{AuthenticationService, GitProtocol, RepositoryAccess}; -use super::types::ProtocolError; -use bytes::Bytes; -use futures::stream::Stream; +use super::types::{ProtocolError, ProtocolStream}; /// HTTP transport adapter for Git protocol /// /// This module provides HTTP-specific handling for Git smart protocol operations. @@ -9,7 +7,6 @@ use futures::stream::Stream; /// request/response formatting and uses the utility functions for proper HTTP handling. use serde::Deserialize; use std::collections::HashMap; -use std::pin::Pin; /// HTTP Git protocol handler pub struct HttpGitHandler { @@ -42,8 +39,8 @@ impl HttpGitHandler { request_path: &str, query: &str, ) -> Result<(Vec, &'static str), ProtocolError> { - // Extract repository path from request - let repo_path = extract_repo_path(request_path) + // Validate repository path exists in request + extract_repo_path(request_path) .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?; // Get service from query parameters @@ -58,7 +55,7 @@ impl HttpGitHandler { )); } - let response_data = self.protocol.info_refs(repo_path, service).await?; + let response_data = self.protocol.info_refs(service).await?; let content_type = get_advertisement_content_type(service); Ok((response_data, content_type)) @@ -71,15 +68,9 @@ impl HttpGitHandler { &mut self, request_path: &str, request_body: &[u8], - ) -> Result< - ( - Pin> + Send>>, - &'static str, - ), - ProtocolError, - > { - // Extract repository path from request - let repo_path = extract_repo_path(request_path) + ) -> Result<(ProtocolStream, &'static str), ProtocolError> { + // Validate repository path exists in request + extract_repo_path(request_path) .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?; // Validate it's a Git request @@ -89,7 +80,7 @@ impl HttpGitHandler { )); } - let response_stream = self.protocol.upload_pack(repo_path, request_body).await?; + let response_stream = self.protocol.upload_pack(request_body).await?; let content_type = get_content_type("git-upload-pack"); Ok((response_stream, content_type)) @@ -101,16 +92,10 @@ impl HttpGitHandler { pub async fn handle_receive_pack( &mut self, request_path: &str, - request_stream: Pin> + Send>>, - ) -> Result< - ( - Pin> + Send>>, - &'static str, - ), - ProtocolError, - > { - // Extract repository path from request - let repo_path = extract_repo_path(request_path) + request_stream: ProtocolStream, + ) -> Result<(ProtocolStream, &'static str), ProtocolError> { + // Validate repository path exists in request + extract_repo_path(request_path) .ok_or_else(|| ProtocolError::InvalidRequest("Invalid repository path".to_string()))?; // Validate it's a Git request @@ -120,10 +105,7 @@ impl HttpGitHandler { )); } - let response_stream = self - .protocol - .receive_pack(repo_path, request_stream) - .await?; + let response_stream = self.protocol.receive_pack(request_stream).await?; let content_type = get_content_type("git-receive-pack"); Ok((response_stream, content_type)) diff --git a/src/protocol/pack.rs b/src/protocol/pack.rs index e9cc4503..933cf4d6 100644 --- a/src/protocol/pack.rs +++ b/src/protocol/pack.rs @@ -20,18 +20,14 @@ where R: RepositoryAccess, { repo_access: &'a R, - repo_path: &'a str, } impl<'a, R> PackGenerator<'a, R> where R: RepositoryAccess, { - pub fn new(repo_access: &'a R, repo_path: &'a str) -> Self { - Self { - repo_access, - repo_path, - } + pub fn new(repo_access: &'a R) -> Self { + Self { repo_access } } /// Generate a full pack containing all requested objects @@ -165,7 +161,7 @@ where // Get commit object let commit = self .repo_access - .get_commit(self.repo_path, &commit_hash) + .get_commit(&commit_hash) .await .map_err(|e| { ProtocolError::repository_error(format!( @@ -212,13 +208,9 @@ where } visited_trees.insert(tree_hash.to_string()); - let tree = self - .repo_access - .get_tree(self.repo_path, tree_hash) - .await - .map_err(|e| { - ProtocolError::repository_error(format!("Failed to get tree {}: {}", tree_hash, e)) - })?; + let tree = self.repo_access.get_tree(tree_hash).await.map_err(|e| { + ProtocolError::repository_error(format!("Failed to get tree {}: {}", tree_hash, e)) + })?; for entry in &tree.tree_items { let entry_hash = entry.id.to_string(); @@ -237,16 +229,12 @@ where | crate::internal::object::tree::TreeItemMode::BlobExecutable => { if !visited_blobs.contains(&entry_hash) { visited_blobs.insert(entry_hash.clone()); - let blob = self - .repo_access - .get_blob(self.repo_path, &entry_hash) - .await - .map_err(|e| { - ProtocolError::repository_error(format!( - "Failed to get blob {}: {}", - entry_hash, e - )) - })?; + let blob = self.repo_access.get_blob(&entry_hash).await.map_err(|e| { + ProtocolError::repository_error(format!( + "Failed to get blob {}: {}", + entry_hash, e + )) + })?; blobs.push(blob); } } @@ -363,38 +351,22 @@ mod tests { #[async_trait] impl RepositoryAccess for DummyRepoAccess { - async fn get_repository_refs( - &self, - _repo_path: &str, - ) -> Result, ProtocolError> { + async fn get_repository_refs(&self) -> Result, ProtocolError> { Ok(vec![]) } - async fn has_object( - &self, - _repo_path: &str, - _object_hash: &str, - ) -> Result { + async fn has_object(&self, _object_hash: &str) -> Result { Ok(false) } - async fn get_object( - &self, - _repo_path: &str, - _object_hash: &str, - ) -> Result, ProtocolError> { + async fn get_object(&self, _object_hash: &str) -> Result, ProtocolError> { Err(ProtocolError::repository_error( "not implemented".to_string(), )) } - async fn store_pack_data( - &self, - _repo_path: &str, - _pack_data: &[u8], - ) -> Result<(), ProtocolError> { + async fn store_pack_data(&self, _pack_data: &[u8]) -> Result<(), ProtocolError> { Ok(()) } async fn update_reference( &self, - _repo_path: &str, _ref_name: &str, _old_hash: Option<&str>, _new_hash: &str, @@ -403,32 +375,31 @@ mod tests { } async fn get_objects_for_pack( &self, - _repo_path: &str, _wants: &[String], _haves: &[String], ) -> Result, ProtocolError> { Ok(vec![]) } - async fn has_default_branch(&self, _repo_path: &str) -> Result { + async fn has_default_branch(&self) -> Result { Ok(false) } - async fn post_receive_hook(&self, _repo_path: &str) -> Result<(), ProtocolError> { + async fn post_receive_hook(&self) -> Result<(), ProtocolError> { Ok(()) } } #[tokio::test] async fn test_pack_roundtrip_encode_decode() { - // 构造两个 Blob + // Create two Blob objects let blob1 = Blob::from_content("hello"); let blob2 = Blob::from_content("world"); - // 构造一个 Tree,包含两个文件项 + // Create a Tree containing two file items let item1 = TreeItem::new(TreeItemMode::Blob, blob1.id, "hello.txt".to_string()); let item2 = TreeItem::new(TreeItemMode::Blob, blob2.id, "world.txt".to_string()); let tree = Tree::from_tree_items(vec![item1, item2]).unwrap(); - // 构造一个 Commit 指向该 Tree + // Create a Commit pointing to the Tree let author = Signature::new( SignatureType::Author, "tester".to_string(), @@ -441,7 +412,7 @@ mod tests { ); let commit = Commit::new(author, committer, tree.id, vec![], "init commit"); - // 生成 pack 流 + // Generate pack stream let (tx, mut rx) = mpsc::channel::>(64); PackGenerator::::generate_pack_stream( ( @@ -460,9 +431,9 @@ mod tests { } println!("Encoded pack size: {} bytes", pack_bytes.len()); - // 解包 pack 流 + // Unpack the pack stream let dummy = DummyRepoAccess; - let generator = PackGenerator::new(&dummy, "test-repo-path"); + let generator = PackGenerator::new(&dummy); let (decoded_commits, decoded_trees, decoded_blobs) = generator .unpack_stream(Bytes::from(pack_bytes)) .await @@ -490,7 +461,7 @@ mod tests { .collect::>() ); - // 验证对象 ID 往返一致 + // Verify object ID roundtrip consistency assert_eq!(decoded_commits.len(), 1); assert_eq!(decoded_trees.len(), 1); assert_eq!(decoded_blobs.len(), 2); diff --git a/src/protocol/smart.rs b/src/protocol/smart.rs index 3b538e2e..7b71b93a 100644 --- a/src/protocol/smart.rs +++ b/src/protocol/smart.rs @@ -1,7 +1,4 @@ -use std::pin::Pin; - use bytes::{BufMut, Bytes, BytesMut}; -use futures::Stream; use std::collections::HashMap; use tokio_stream::wrappers::ReceiverStream; @@ -9,8 +6,9 @@ use super::core::{AuthenticationService, RepositoryAccess}; use super::pack::PackGenerator; use super::types::ProtocolError; use super::types::{ - COMMON_CAP_LIST, Capability, LF, NUL, PKT_LINE_END_MARKER, RECEIVE_CAP_LIST, RefCommand, - RefTypeEnum, SP, ServiceType, SideBind, TransportProtocol, UPLOAD_CAP_LIST, ZERO_ID, + COMMON_CAP_LIST, Capability, LF, NUL, PKT_LINE_END_MARKER, ProtocolStream, RECEIVE_CAP_LIST, + RefCommand, RefTypeEnum, SP, ServiceType, SideBand, TransportProtocol, UPLOAD_CAP_LIST, + ZERO_ID, }; use super::utils::{add_pkt_line_string, build_smart_reply, read_pkt_line, read_until_white_space}; @@ -24,9 +22,8 @@ where A: AuthenticationService, { pub transport_protocol: TransportProtocol, - pub service_type: Option, pub capabilities: Vec, - pub side_bind: Option, + pub side_band: Option, pub command_list: Vec, // Trait-based dependencies @@ -43,9 +40,8 @@ where pub fn new(transport_protocol: TransportProtocol, repo_storage: R, auth_service: A) -> Self { Self { transport_protocol, - service_type: None, capabilities: Vec::new(), - side_bind: None, + side_band: None, command_list: Vec::new(), repo_storage, auth_service, @@ -71,32 +67,20 @@ where .await } - /// Set the service type for this protocol session - pub fn set_service_type(&mut self, service_type: ServiceType) { - self.service_type = Some(service_type); - } - - /// Get the current service type - pub fn get_service_type(&self) -> Option { - self.service_type - } - /// Set transport protocol (Http, Ssh, etc.) pub fn set_transport_protocol(&mut self, protocol: TransportProtocol) { self.transport_protocol = protocol; } - /// Get git info refs for the repository - pub async fn git_info_refs(&self, repo_path: &str) -> Result { - let service_type = self - .service_type - .ok_or_else(|| ProtocolError::repository_error("Service type not set".to_string()))?; - - let refs = self - .repo_storage - .get_repository_refs(repo_path) - .await - .map_err(|e| ProtocolError::repository_error(format!("Failed to get refs: {}", e)))?; + /// Get git info refs for the repository, with explicit service type + pub async fn git_info_refs( + &self, + service_type: ServiceType, + ) -> Result { + let refs = + self.repo_storage.get_repository_refs().await.map_err(|e| { + ProtocolError::repository_error(format!("Failed to get refs: {}", e)) + })?; // Convert to the expected format (head_hash, git_refs) let head_hash = refs @@ -138,19 +122,19 @@ where Ok(pkt_line_stream) } - /// Handle git upload-pack operation (fetch/clone) + /// Handle git-upload-pack request pub async fn git_upload_pack( &mut self, - repo_path: &str, - upload_request: &mut Bytes, + upload_request: Bytes, ) -> Result<(ReceiverStream>, BytesMut), ProtocolError> { + let mut upload_request = upload_request; let mut want: Vec = Vec::new(); let mut have: Vec = Vec::new(); let mut last_common_commit = String::new(); let mut read_first_line = false; loop { - let (bytes_take, pkt_line) = read_pkt_line(upload_request); + let (bytes_take, pkt_line) = read_pkt_line(&mut upload_request); if bytes_take == 0 { break; @@ -189,7 +173,7 @@ where let mut protocol_buf = BytesMut::new(); // Create pack generator for this operation - let pack_generator = PackGenerator::new(&self.repo_storage, repo_path); + let pack_generator = PackGenerator::new(&self.repo_storage); if have.is_empty() { // Full pack @@ -200,16 +184,9 @@ where // Check for common commits for hash in &have { - let exists = self - .repo_storage - .commit_exists(repo_path, hash) - .await - .map_err(|e| { - ProtocolError::repository_error(format!( - "Failed to check commit existence: {}", - e - )) - })?; + let exists = self.repo_storage.commit_exists(hash).await.map_err(|e| { + ProtocolError::repository_error(format!("Failed to check commit existence: {}", e)) + })?; if exists { add_pkt_line_string(&mut protocol_buf, format!("ACK {hash} common\n")); if last_common_commit.is_empty() { @@ -260,8 +237,7 @@ where /// Handle git receive-pack operation (push) pub async fn git_receive_pack_stream( &mut self, - repo_path: &str, - data_stream: Pin> + Send>>, + data_stream: ProtocolStream, ) -> Result { // Collect all pack data from stream let mut pack_data = BytesMut::new(); @@ -274,14 +250,14 @@ where } // Create pack generator for unpacking - let pack_generator = PackGenerator::new(&self.repo_storage, repo_path); + let pack_generator = PackGenerator::new(&self.repo_storage); // Unpack the received data let (commits, trees, blobs) = pack_generator.unpack_stream(pack_data.freeze()).await?; // Store the unpacked objects via the repository access trait self.repo_storage - .handle_pack_objects(repo_path, commits, trees, blobs) + .handle_pack_objects(commits, trees, blobs) .await .map_err(|e| { ProtocolError::repository_error(format!("Failed to store pack objects: {}", e)) @@ -291,13 +267,9 @@ where let mut report_status = BytesMut::new(); add_pkt_line_string(&mut report_status, "unpack ok\n".to_owned()); - let mut default_exist = self - .repo_storage - .has_default_branch(repo_path) - .await - .map_err(|e| { - ProtocolError::repository_error(format!("Failed to check default branch: {}", e)) - })?; + let mut default_exist = self.repo_storage.has_default_branch().await.map_err(|e| { + ProtocolError::repository_error(format!("Failed to check default branch: {}", e)) + })?; // Update refs with proper error handling for command in &mut self.command_list { @@ -311,7 +283,7 @@ where }; if let Err(e) = self .repo_storage - .update_reference(repo_path, &command.ref_name, old_hash, &command.new_hash) + .update_reference(&command.ref_name, old_hash, &command.new_hash) .await { command.failed(e.to_string()); @@ -330,7 +302,7 @@ where }; if let Err(e) = self .repo_storage - .update_reference(repo_path, &command.ref_name, old_hash, &command.new_hash) + .update_reference(&command.ref_name, old_hash, &command.new_hash) .await { command.failed(e.to_string()); @@ -340,12 +312,9 @@ where } // Post-receive hook - self.repo_storage - .post_receive_hook(repo_path) - .await - .map_err(|e| { - ProtocolError::repository_error(format!("Post-receive hook failed: {}", e)) - })?; + self.repo_storage.post_receive_hook().await.map_err(|e| { + ProtocolError::repository_error(format!("Post-receive hook failed: {}", e)) + })?; report_status.put(&PKT_LINE_END_MARKER[..]); Ok(report_status.freeze()) @@ -359,7 +328,7 @@ where { let length = length + 5; to_bytes.put(Bytes::from(format!("{length:04x}"))); - to_bytes.put_u8(SideBind::PackfileData.value()); + to_bytes.put_u8(SideBand::PackfileData.value()); to_bytes.put(from_bytes); } else { to_bytes.put(from_bytes); @@ -440,10 +409,7 @@ mod tests { #[async_trait] impl RepositoryAccess for TestRepoAccess { - async fn get_repository_refs( - &self, - _repo_path: &str, - ) -> Result, ProtocolError> { + async fn get_repository_refs(&self) -> Result, ProtocolError> { Ok(vec![ ( "HEAD".to_string(), @@ -456,34 +422,21 @@ mod tests { ]) } - async fn has_object( - &self, - _repo_path: &str, - _object_hash: &str, - ) -> Result { + async fn has_object(&self, _object_hash: &str) -> Result { Ok(true) } - async fn get_object( - &self, - _repo_path: &str, - _object_hash: &str, - ) -> Result, ProtocolError> { + async fn get_object(&self, _object_hash: &str) -> Result, ProtocolError> { Ok(vec![]) } - async fn store_pack_data( - &self, - _repo_path: &str, - _pack_data: &[u8], - ) -> Result<(), ProtocolError> { + async fn store_pack_data(&self, _pack_data: &[u8]) -> Result<(), ProtocolError> { *self.stored_count.lock().unwrap() += 1; Ok(()) } async fn update_reference( &self, - _repo_path: &str, ref_name: &str, old_hash: Option<&str>, new_hash: &str, @@ -498,21 +451,20 @@ mod tests { async fn get_objects_for_pack( &self, - _repo_path: &str, _wants: &[String], _haves: &[String], ) -> Result, ProtocolError> { Ok(vec![]) } - async fn has_default_branch(&self, _repo_path: &str) -> Result { + async fn has_default_branch(&self) -> Result { let mut exists = self.default_branch_exists.lock().unwrap(); let current = *exists; *exists = true; // flip to true after first check Ok(current) } - async fn post_receive_hook(&self, _repo_path: &str) -> Result<(), ProtocolError> { + async fn post_receive_hook(&self) -> Result<(), ProtocolError> { self.post_called.store(true, Ordering::SeqCst); Ok(()) } @@ -603,7 +555,7 @@ mod tests { // Execute receive-pack let result_bytes = smart - .git_receive_pack_stream("test-repo-path", request_stream) + .git_receive_pack_stream(request_stream) .await .expect("receive-pack should succeed"); diff --git a/src/protocol/ssh.rs b/src/protocol/ssh.rs index 523b8e3a..20d94830 100644 --- a/src/protocol/ssh.rs +++ b/src/protocol/ssh.rs @@ -4,10 +4,7 @@ /// It's a thin wrapper around the core GitProtocol that handles SSH command /// execution and data streaming. use super::core::{AuthenticationService, GitProtocol, RepositoryAccess}; -use super::types::ProtocolError; -use bytes::Bytes; -use futures::stream::Stream; -use std::pin::Pin; +use super::types::{ProtocolError, ProtocolStream}; /// SSH Git protocol handler pub struct SshGitHandler { @@ -35,30 +32,22 @@ impl SshGitHandler { /// Handle git-upload-pack command (for clone/fetch) pub async fn handle_upload_pack( &mut self, - repo_path: &str, request_data: &[u8], - ) -> Result> + Send>>, ProtocolError> - { - self.protocol.upload_pack(repo_path, request_data).await + ) -> Result { + self.protocol.upload_pack(request_data).await } /// Handle git-receive-pack command (for push) pub async fn handle_receive_pack( &mut self, - repo_path: &str, - request_stream: Pin> + Send>>, - ) -> Result> + Send>>, ProtocolError> - { - self.protocol.receive_pack(repo_path, request_stream).await + request_stream: ProtocolStream, + ) -> Result { + self.protocol.receive_pack(request_stream).await } /// Handle info/refs request for SSH - pub async fn handle_info_refs( - &mut self, - repo_path: &str, - service: &str, - ) -> Result, ProtocolError> { - self.protocol.info_refs(repo_path, service).await + pub async fn handle_info_refs(&mut self, service: &str) -> Result, ProtocolError> { + self.protocol.info_refs(service).await } } diff --git a/src/protocol/types.rs b/src/protocol/types.rs index e2e3b43b..14a8f5b1 100644 --- a/src/protocol/types.rs +++ b/src/protocol/types.rs @@ -1,6 +1,12 @@ +use bytes::Bytes; +use futures::stream::Stream; use std::fmt; +use std::pin::Pin; use std::str::FromStr; +/// Type alias for protocol data streams to reduce nesting +pub type ProtocolStream = Pin> + Send>>; + /// Protocol error types #[derive(Debug, thiserror::Error)] pub enum ProtocolError { @@ -86,36 +92,85 @@ impl FromStr for ServiceType { } /// Git protocol capabilities +/// +/// ## Implementation Status Overview +/// +/// ### Implemented capabilities: +/// - **Data transmission**: SideBand, SideBand64k - Multiplexed data streams via side-band formatter +/// - **Status reporting**: ReportStatus, ReportStatusv2 - Push status feedback via protocol handlers +/// - **Pack optimization**: OfsDelta, ThinPack, NoThin - Delta compression and efficient transmission +/// - **Protocol control**: MultiAckDetailed, NoDone - ACK mechanism optimization for upload-pack +/// - **Push control**: Atomic, DeleteRefs, Quiet - Atomic operations and reference management +/// - **Tag handling**: IncludeTag - Automatic tag inclusion for upload-pack +/// - **Client identification**: Agent - Client/server identification in capability negotiation +/// +/// ### Not yet implemented capabilities: +/// - **Basic protocol**: MultiAck - Basic multi-ack support (only detailed version implemented) +/// - **Shallow cloning**: Shallow, DeepenSince, DeepenNot, DeepenRelative - Depth control for shallow clones +/// - **Progress control**: NoProgress - Progress output suppression +/// - **Special fetch**: AllowTipSha1InWant, AllowReachableSha1InWant - SHA1 validation in want processing +/// - **Security**: PushCert - Push certificate verification mechanism +/// - **Extensions**: PushOptions, Filter, Symref - Extended parameter handling +/// - **Session management**: SessionId, ObjectFormat - Session and format negotiation #[derive(Debug, Clone, PartialEq)] pub enum Capability { + /// Multi-ack capability for upload-pack protocol MultiAck, + /// Multi-ack-detailed capability for more granular acknowledgment MultiAckDetailed, + /// No-done capability to optimize upload-pack protocol NoDone, + /// Side-band capability for multiplexing data streams SideBand, + /// Side-band-64k capability for larger side-band packets SideBand64k, + /// Report-status capability for push status reporting ReportStatus, + /// Report-status-v2 capability for enhanced push status reporting ReportStatusv2, + /// OFS-delta capability for offset-based delta compression OfsDelta, + /// Deepen-since capability for shallow clone with time-based depth DeepenSince, + /// Deepen-not capability for shallow clone exclusions DeepenNot, + /// Deepen-relative capability for relative depth specification + DeepenRelative, + /// Thin-pack capability for efficient pack transmission ThinPack, + /// Shallow capability for shallow clone support Shallow, - Deepen, + /// Include-tag capability for automatic tag inclusion IncludeTag, + /// Delete-refs capability for reference deletion DeleteRefs, + /// Quiet capability to suppress output Quiet, + /// Atomic capability for atomic push operations Atomic, + /// No-thin capability to disable thin pack NoThin, + /// No-progress capability to disable progress reporting + NoProgress, + /// Allow-tip-sha1-in-want capability for fetching specific commits AllowTipSha1InWant, + /// Allow-reachable-sha1-in-want capability for fetching reachable commits AllowReachableSha1InWant, + /// Push-cert capability for signed push certificates PushCert(String), + /// Push-options capability for additional push metadata PushOptions, + /// Object-format capability for specifying hash algorithm ObjectFormat(String), - ServerOption(String), + /// Session-id capability for session tracking SessionId(String), - PackfileUris(String), - Lfs, + /// Filter capability for partial clone support + Filter(String), + /// Symref capability for symbolic reference information + Symref(String), + /// Agent capability for client/server identification Agent(String), + /// Unknown capability for forward compatibility Unknown(String), } @@ -127,21 +182,21 @@ impl FromStr for Capability { if let Some(rest) = s.strip_prefix("agent=") { return Ok(Capability::Agent(rest.to_string())); } - if let Some(rest) = s.strip_prefix("server-option=") { - return Ok(Capability::ServerOption(rest.to_string())); - } if let Some(rest) = s.strip_prefix("session-id=") { return Ok(Capability::SessionId(rest.to_string())); } - if let Some(rest) = s.strip_prefix("packfile-uris=") { - return Ok(Capability::PackfileUris(rest.to_string())); - } if let Some(rest) = s.strip_prefix("push-cert=") { return Ok(Capability::PushCert(rest.to_string())); } if let Some(rest) = s.strip_prefix("object-format=") { return Ok(Capability::ObjectFormat(rest.to_string())); } + if let Some(rest) = s.strip_prefix("filter=") { + return Ok(Capability::Filter(rest.to_string())); + } + if let Some(rest) = s.strip_prefix("symref=") { + return Ok(Capability::Symref(rest.to_string())); + } match s { "multi_ack" => Ok(Capability::MultiAck), @@ -154,18 +209,18 @@ impl FromStr for Capability { "ofs-delta" => Ok(Capability::OfsDelta), "deepen-since" => Ok(Capability::DeepenSince), "deepen-not" => Ok(Capability::DeepenNot), + "deepen-relative" => Ok(Capability::DeepenRelative), "thin-pack" => Ok(Capability::ThinPack), "shallow" => Ok(Capability::Shallow), - "deepen" => Ok(Capability::Deepen), "include-tag" => Ok(Capability::IncludeTag), "delete-refs" => Ok(Capability::DeleteRefs), "quiet" => Ok(Capability::Quiet), "atomic" => Ok(Capability::Atomic), "no-thin" => Ok(Capability::NoThin), + "no-progress" => Ok(Capability::NoProgress), "allow-tip-sha1-in-want" => Ok(Capability::AllowTipSha1InWant), "allow-reachable-sha1-in-want" => Ok(Capability::AllowReachableSha1InWant), "push-options" => Ok(Capability::PushOptions), - "lfs" => Ok(Capability::Lfs), _ => Ok(Capability::Unknown(s.to_string())), } } @@ -184,23 +239,23 @@ impl std::fmt::Display for Capability { Capability::OfsDelta => write!(f, "ofs-delta"), Capability::DeepenSince => write!(f, "deepen-since"), Capability::DeepenNot => write!(f, "deepen-not"), + Capability::DeepenRelative => write!(f, "deepen-relative"), Capability::ThinPack => write!(f, "thin-pack"), Capability::Shallow => write!(f, "shallow"), - Capability::Deepen => write!(f, "deepen"), Capability::IncludeTag => write!(f, "include-tag"), Capability::DeleteRefs => write!(f, "delete-refs"), Capability::Quiet => write!(f, "quiet"), Capability::Atomic => write!(f, "atomic"), Capability::NoThin => write!(f, "no-thin"), + Capability::NoProgress => write!(f, "no-progress"), Capability::AllowTipSha1InWant => write!(f, "allow-tip-sha1-in-want"), Capability::AllowReachableSha1InWant => write!(f, "allow-reachable-sha1-in-want"), Capability::PushCert(value) => write!(f, "push-cert={}", value), Capability::PushOptions => write!(f, "push-options"), Capability::ObjectFormat(format) => write!(f, "object-format={}", format), - Capability::ServerOption(option) => write!(f, "server-option={}", option), Capability::SessionId(id) => write!(f, "session-id={}", id), - Capability::PackfileUris(uris) => write!(f, "packfile-uris={}", uris), - Capability::Lfs => write!(f, "lfs"), + Capability::Filter(filter) => write!(f, "filter={}", filter), + Capability::Symref(symref) => write!(f, "symref={}", symref), Capability::Agent(agent) => write!(f, "agent={}", agent), Capability::Unknown(s) => write!(f, "{}", s), } @@ -208,7 +263,7 @@ impl std::fmt::Display for Capability { } /// Side-band types for multiplexed data streams -pub enum SideBind { +pub enum SideBand { /// Sideband 1 contains packfile data PackfileData, /// Sideband 2 contains progress information @@ -217,7 +272,7 @@ pub enum SideBind { Error, } -impl SideBind { +impl SideBand { pub fn value(&self) -> u8 { match self { Self::PackfileData => b'\x01', @@ -321,5 +376,5 @@ pub const PKT_LINE_END_MARKER: &[u8; 4] = b"0000"; // Git protocol capability lists pub const RECEIVE_CAP_LIST: &str = "report-status report-status-v2 delete-refs quiet atomic no-thin "; -pub const COMMON_CAP_LIST: &str = "side-band-64k ofs-delta lfs agent=git-internal/0.1.0"; +pub const COMMON_CAP_LIST: &str = "side-band-64k ofs-delta agent=git-internal/0.1.0"; pub const UPLOAD_CAP_LIST: &str = "multi_ack_detailed no-done include-tag "; diff --git a/src/protocol/utils.rs b/src/protocol/utils.rs index e511ab97..366f53fb 100644 --- a/src/protocol/utils.rs +++ b/src/protocol/utils.rs @@ -92,7 +92,7 @@ pub fn read_until_white_space(bytes: &mut Bytes) -> String { pub fn build_smart_reply( transport_protocol: TransportProtocol, - ref_list: &Vec, + ref_list: &[String], service: String, ) -> BytesMut { let mut pkt_line_stream = BytesMut::new();