From ab32e8826e781c662a507776b4a972d86f6fe9d1 Mon Sep 17 00:00:00 2001 From: zhenyi <434836402@qq.com> Date: Mon, 8 Jun 2026 21:46:31 +0800 Subject: [PATCH] feat(pack): add raw advertise refs and stateless protocol support - Add raw flag to AdvertiseRefsRequest to enable raw pkt-line output - Implement advertise_refs_raw function that calls git upload-pack/receive-pack with --advertise-refs - Add stateless flag to GitProtocolFeatures for HTTP smart protocol support - Modify upload_pack and receive_pack to accept stateless parameter - Update command construction to include --stateless-rpc flag when enabled - Add raw_data field to AdvertiseRefsResponse for raw output - Update pack cache key computation to include raw service differentiation - Initialize raw field to false in all request creation calls --- actor/sync.rs | 1 + pack/advertise_refs.rs | 61 ++++++++++++++++++++++++++++++++++++++++-- pack/receive_pack.rs | 17 ++++++++---- pack/upload_pack.rs | 17 ++++++++---- proto/pack.proto | 3 +++ server/pack.rs | 14 +++++++--- tests/archive_test.rs | 1 + tests/refs_test.rs | 1 + 8 files changed, 99 insertions(+), 16 deletions(-) diff --git a/actor/sync.rs b/actor/sync.rs index db5efc9..9a87461 100644 --- a/actor/sync.rs +++ b/actor/sync.rs @@ -163,6 +163,7 @@ fn sync_via_pack_service( repository: Some(header.clone()), protocol: None, service: "upload-pack".to_string(), + raw: false, }) .await .map_err(|e| format!("AdvertiseRefs: {e}"))?; diff --git a/pack/advertise_refs.rs b/pack/advertise_refs.rs index 98c23d6..ccc683a 100644 --- a/pack/advertise_refs.rs +++ b/pack/advertise_refs.rs @@ -1,12 +1,15 @@ use crate::bare::GitBare; -use crate::error::GitResult; +use crate::error::{GitError, GitResult}; use crate::pb::{AdvertiseRefsRequest, AdvertiseRefsResponse, ReferenceAdvertisement}; impl GitBare { pub fn advertise_refs( &self, - _request: AdvertiseRefsRequest, + request: AdvertiseRefsRequest, ) -> GitResult { + if request.raw { + return self.advertise_refs_raw(&request); + } let repo = self.gix_repo()?; let mut references = Vec::new(); for r in repo.references()?.all()? { @@ -52,6 +55,60 @@ impl GitBare { "multi_ack".into(), "symref=HEAD".into(), ], + raw_data: Vec::new(), + }) + } + + /// Return raw pkt-line output from git upload-pack/receive-pack --advertise-refs. + /// Used by transparent proxies (gitshell) that forward bytes verbatim to git clients. + fn advertise_refs_raw( + &self, + request: &AdvertiseRefsRequest, + ) -> GitResult { + let bare_dir_str = self.bare_dir.to_string_lossy().into_owned(); + let stateless = request.protocol.as_ref().is_some_and(|p| p.stateless); + + // Default to upload-pack if service is unspecified + let subcommand = if request.service == "git-receive-pack" { + "receive-pack" + } else { + "upload-pack" + }; + + let mut args: Vec = vec![ + "--git-dir".into(), + bare_dir_str, + subcommand.into(), + "--advertise-refs".into(), + ]; + if stateless { + args.push("--stateless-rpc".into()); + } + + let result = duct::cmd("git", &args) + .stdout_capture() + .stderr_capture() + .unchecked() + .run()?; + + if !result.status.success() { + return Err(GitError::CommandFailed { + status_code: result.status.code(), + stderr: String::from_utf8_lossy(&result.stderr).into_owned(), + }); + } + + tracing::debug!( + raw_len = result.stdout.len(), + service = %request.service, + stateless = stateless, + "advertise_refs raw output" + ); + + Ok(AdvertiseRefsResponse { + references: Vec::new(), + capabilities: Vec::new(), + raw_data: result.stdout, }) } } diff --git a/pack/receive_pack.rs b/pack/receive_pack.rs index 612ddc1..45c9b16 100644 --- a/pack/receive_pack.rs +++ b/pack/receive_pack.rs @@ -14,8 +14,12 @@ impl GitBare { /// Client-streaming input → server-streaming output. /// Stdin packets are forwarded to the child process as they arrive from the client, /// while stdout is concurrently read and streamed back via a channel. + /// + /// `stateless` enables `--stateless-rpc` for HTTP smart protocol. + /// Leave `false` for SSH (persistent connection). pub async fn receive_pack( &self, + stateless: bool, input: impl tokio_stream::Stream> + Send + 'static, @@ -23,6 +27,7 @@ impl GitBare { let bare_dir = self.bare_dir.to_string_lossy().into_owned(); tracing::info!( repo = %bare_dir, + stateless = stateless, "spawning git receive-pack subprocess" ); @@ -31,11 +36,13 @@ impl GitBare { let stream = Box::pin(input); tokio::spawn(async move { let stream = stream; - let mut child = match Command::new("git") - .arg("--git-dir") - .arg(&bare_dir) - .arg("receive-pack") - .arg(&bare_dir) + let mut cmd = Command::new("git"); + cmd.arg("--git-dir").arg(&bare_dir); + if stateless { + cmd.arg("--stateless-rpc"); + } + cmd.arg("receive-pack").arg(&bare_dir); + let mut child = match cmd .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) diff --git a/pack/upload_pack.rs b/pack/upload_pack.rs index c5d7097..ab1868c 100644 --- a/pack/upload_pack.rs +++ b/pack/upload_pack.rs @@ -14,8 +14,12 @@ impl GitBare { /// Client-streaming input → server-streaming output. /// Stdin packets are forwarded to the child process as they arrive from the client, /// while stdout is concurrently read and streamed back via a channel. + /// + /// `stateless` enables `--stateless-rpc` for HTTP smart protocol. + /// Leave `false` for SSH (persistent connection). pub async fn upload_pack( &self, + stateless: bool, input: impl tokio_stream::Stream> + Send + 'static, @@ -23,6 +27,7 @@ impl GitBare { let bare_dir = self.bare_dir.to_string_lossy().into_owned(); tracing::info!( repo = %bare_dir, + stateless = stateless, "spawning git upload-pack subprocess" ); @@ -32,11 +37,13 @@ impl GitBare { let stream = Box::pin(input); tokio::spawn(async move { let stream = stream; - let mut child = match Command::new("git") - .arg("--git-dir") - .arg(&bare_dir) - .arg("upload-pack") - .arg(&bare_dir) + let mut cmd = Command::new("git"); + cmd.arg("--git-dir").arg(&bare_dir); + if stateless { + cmd.arg("--stateless-rpc"); + } + cmd.arg("upload-pack").arg(&bare_dir); + let mut child = match cmd .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) diff --git a/proto/pack.proto b/proto/pack.proto index b3ee87b..6dd17ca 100644 --- a/proto/pack.proto +++ b/proto/pack.proto @@ -10,6 +10,7 @@ message GitProtocolFeatures { repeated string capabilities = 2; repeated string server_options = 3; repeated string agent = 4; + bool stateless = 5; } message ReferenceAdvertisement { @@ -24,11 +25,13 @@ message AdvertiseRefsRequest { RepositoryHeader repository = 1; GitProtocolFeatures protocol = 2; string service = 3; + bool raw = 4; } message AdvertiseRefsResponse { repeated ReferenceAdvertisement references = 1; repeated string capabilities = 2; + bytes raw_data = 3; } message UploadPackRequest { diff --git a/server/pack.rs b/server/pack.rs index 3ed18d9..72b8310 100644 --- a/server/pack.rs +++ b/server/pack.rs @@ -47,8 +47,12 @@ impl pack_service_server::PackService for GitksService { }; if let Some(ref pc) = self.pack_cache { - let protocol = inner.service.clone(); - if let Ok(digest) = pc.disk_cache().compute_info_refs_key(&repo, &protocol) { + let protocol_key = if inner.raw { + format!("{}:raw", inner.service) + } else { + inner.service.clone() + }; + if let Ok(digest) = pc.disk_cache().compute_info_refs_key(&repo, &protocol_key) { if let Some(cached) = pc.lookup_info_refs::(&digest) { tracing::info!(%repo, refs = cached.references.len(), "advertise_refs done (cached)"); m.record("ok"); @@ -119,6 +123,7 @@ impl pack_service_server::PackService for GitksService { } }; tracing::info!(%repo, "upload-pack streaming started"); + let stateless = first.protocol.as_ref().is_some_and(|p| p.stateless); let (tx, rx) = tokio::sync::mpsc::channel(16); tx.send(Ok(first)) @@ -132,7 +137,7 @@ impl pack_service_server::PackService for GitksService { } }); - let result = gb.upload_pack(ReceiverStream::new(rx)).await?; + let result = gb.upload_pack(stateless, ReceiverStream::new(rx)).await?; m.record("ok"); Ok(tonic::Response::new(result)) } @@ -188,6 +193,7 @@ impl pack_service_server::PackService for GitksService { } }; tracing::info!(%repo, "receive-pack streaming started"); + let stateless = first.protocol.as_ref().is_some_and(|p| p.stateless); let (tx, rx) = tokio::sync::mpsc::channel(16); tx.send(Ok(first)) @@ -201,7 +207,7 @@ impl pack_service_server::PackService for GitksService { } }); - let result = gb.receive_pack(ReceiverStream::new(rx)).await?; + let result = gb.receive_pack(stateless, ReceiverStream::new(rx)).await?; m.record("ok"); Ok(tonic::Response::new(result)) } diff --git a/tests/archive_test.rs b/tests/archive_test.rs index ebe5ef7..f4e249f 100644 --- a/tests/archive_test.rs +++ b/tests/archive_test.rs @@ -216,6 +216,7 @@ async fn test_advertise_refs() { repository: Some(hdr("test-repo")), protocol: None, service: String::new(), + raw: false, })) .await .unwrap() diff --git a/tests/refs_test.rs b/tests/refs_test.rs index 6c1f52b..fa133b3 100644 --- a/tests/refs_test.rs +++ b/tests/refs_test.rs @@ -20,6 +20,7 @@ async fn test_list_refs_via_service() { repository: Some(hdr()), protocol: None, service: String::new(), + raw: false, })) .await .unwrap()