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
This commit is contained in:
@@ -163,6 +163,7 @@ fn sync_via_pack_service(
|
|||||||
repository: Some(header.clone()),
|
repository: Some(header.clone()),
|
||||||
protocol: None,
|
protocol: None,
|
||||||
service: "upload-pack".to_string(),
|
service: "upload-pack".to_string(),
|
||||||
|
raw: false,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("AdvertiseRefs: {e}"))?;
|
.map_err(|e| format!("AdvertiseRefs: {e}"))?;
|
||||||
|
|||||||
+59
-2
@@ -1,12 +1,15 @@
|
|||||||
use crate::bare::GitBare;
|
use crate::bare::GitBare;
|
||||||
use crate::error::GitResult;
|
use crate::error::{GitError, GitResult};
|
||||||
use crate::pb::{AdvertiseRefsRequest, AdvertiseRefsResponse, ReferenceAdvertisement};
|
use crate::pb::{AdvertiseRefsRequest, AdvertiseRefsResponse, ReferenceAdvertisement};
|
||||||
|
|
||||||
impl GitBare {
|
impl GitBare {
|
||||||
pub fn advertise_refs(
|
pub fn advertise_refs(
|
||||||
&self,
|
&self,
|
||||||
_request: AdvertiseRefsRequest,
|
request: AdvertiseRefsRequest,
|
||||||
) -> GitResult<AdvertiseRefsResponse> {
|
) -> GitResult<AdvertiseRefsResponse> {
|
||||||
|
if request.raw {
|
||||||
|
return self.advertise_refs_raw(&request);
|
||||||
|
}
|
||||||
let repo = self.gix_repo()?;
|
let repo = self.gix_repo()?;
|
||||||
let mut references = Vec::new();
|
let mut references = Vec::new();
|
||||||
for r in repo.references()?.all()? {
|
for r in repo.references()?.all()? {
|
||||||
@@ -52,6 +55,60 @@ impl GitBare {
|
|||||||
"multi_ack".into(),
|
"multi_ack".into(),
|
||||||
"symref=HEAD".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<AdvertiseRefsResponse> {
|
||||||
|
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<String> = 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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-5
@@ -14,8 +14,12 @@ impl GitBare {
|
|||||||
/// Client-streaming input → server-streaming output.
|
/// Client-streaming input → server-streaming output.
|
||||||
/// Stdin packets are forwarded to the child process as they arrive from the client,
|
/// 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.
|
/// 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(
|
pub async fn receive_pack(
|
||||||
&self,
|
&self,
|
||||||
|
stateless: bool,
|
||||||
input: impl tokio_stream::Stream<Item = Result<crate::pb::ReceivePackRequest, tonic::Status>>
|
input: impl tokio_stream::Stream<Item = Result<crate::pb::ReceivePackRequest, tonic::Status>>
|
||||||
+ Send
|
+ Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
@@ -23,6 +27,7 @@ impl GitBare {
|
|||||||
let bare_dir = self.bare_dir.to_string_lossy().into_owned();
|
let bare_dir = self.bare_dir.to_string_lossy().into_owned();
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
repo = %bare_dir,
|
repo = %bare_dir,
|
||||||
|
stateless = stateless,
|
||||||
"spawning git receive-pack subprocess"
|
"spawning git receive-pack subprocess"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -31,11 +36,13 @@ impl GitBare {
|
|||||||
let stream = Box::pin(input);
|
let stream = Box::pin(input);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let stream = stream;
|
let stream = stream;
|
||||||
let mut child = match Command::new("git")
|
let mut cmd = Command::new("git");
|
||||||
.arg("--git-dir")
|
cmd.arg("--git-dir").arg(&bare_dir);
|
||||||
.arg(&bare_dir)
|
if stateless {
|
||||||
.arg("receive-pack")
|
cmd.arg("--stateless-rpc");
|
||||||
.arg(&bare_dir)
|
}
|
||||||
|
cmd.arg("receive-pack").arg(&bare_dir);
|
||||||
|
let mut child = match cmd
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
|
|||||||
+12
-5
@@ -14,8 +14,12 @@ impl GitBare {
|
|||||||
/// Client-streaming input → server-streaming output.
|
/// Client-streaming input → server-streaming output.
|
||||||
/// Stdin packets are forwarded to the child process as they arrive from the client,
|
/// 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.
|
/// 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(
|
pub async fn upload_pack(
|
||||||
&self,
|
&self,
|
||||||
|
stateless: bool,
|
||||||
input: impl tokio_stream::Stream<Item = Result<crate::pb::UploadPackRequest, tonic::Status>>
|
input: impl tokio_stream::Stream<Item = Result<crate::pb::UploadPackRequest, tonic::Status>>
|
||||||
+ Send
|
+ Send
|
||||||
+ 'static,
|
+ 'static,
|
||||||
@@ -23,6 +27,7 @@ impl GitBare {
|
|||||||
let bare_dir = self.bare_dir.to_string_lossy().into_owned();
|
let bare_dir = self.bare_dir.to_string_lossy().into_owned();
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
repo = %bare_dir,
|
repo = %bare_dir,
|
||||||
|
stateless = stateless,
|
||||||
"spawning git upload-pack subprocess"
|
"spawning git upload-pack subprocess"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -32,11 +37,13 @@ impl GitBare {
|
|||||||
let stream = Box::pin(input);
|
let stream = Box::pin(input);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let stream = stream;
|
let stream = stream;
|
||||||
let mut child = match Command::new("git")
|
let mut cmd = Command::new("git");
|
||||||
.arg("--git-dir")
|
cmd.arg("--git-dir").arg(&bare_dir);
|
||||||
.arg(&bare_dir)
|
if stateless {
|
||||||
.arg("upload-pack")
|
cmd.arg("--stateless-rpc");
|
||||||
.arg(&bare_dir)
|
}
|
||||||
|
cmd.arg("upload-pack").arg(&bare_dir);
|
||||||
|
let mut child = match cmd
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ message GitProtocolFeatures {
|
|||||||
repeated string capabilities = 2;
|
repeated string capabilities = 2;
|
||||||
repeated string server_options = 3;
|
repeated string server_options = 3;
|
||||||
repeated string agent = 4;
|
repeated string agent = 4;
|
||||||
|
bool stateless = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ReferenceAdvertisement {
|
message ReferenceAdvertisement {
|
||||||
@@ -24,11 +25,13 @@ message AdvertiseRefsRequest {
|
|||||||
RepositoryHeader repository = 1;
|
RepositoryHeader repository = 1;
|
||||||
GitProtocolFeatures protocol = 2;
|
GitProtocolFeatures protocol = 2;
|
||||||
string service = 3;
|
string service = 3;
|
||||||
|
bool raw = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdvertiseRefsResponse {
|
message AdvertiseRefsResponse {
|
||||||
repeated ReferenceAdvertisement references = 1;
|
repeated ReferenceAdvertisement references = 1;
|
||||||
repeated string capabilities = 2;
|
repeated string capabilities = 2;
|
||||||
|
bytes raw_data = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UploadPackRequest {
|
message UploadPackRequest {
|
||||||
|
|||||||
+10
-4
@@ -47,8 +47,12 @@ impl pack_service_server::PackService for GitksService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref pc) = self.pack_cache {
|
if let Some(ref pc) = self.pack_cache {
|
||||||
let protocol = inner.service.clone();
|
let protocol_key = if inner.raw {
|
||||||
if let Ok(digest) = pc.disk_cache().compute_info_refs_key(&repo, &protocol) {
|
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::<AdvertiseRefsResponse>(&digest) {
|
if let Some(cached) = pc.lookup_info_refs::<AdvertiseRefsResponse>(&digest) {
|
||||||
tracing::info!(%repo, refs = cached.references.len(), "advertise_refs done (cached)");
|
tracing::info!(%repo, refs = cached.references.len(), "advertise_refs done (cached)");
|
||||||
m.record("ok");
|
m.record("ok");
|
||||||
@@ -119,6 +123,7 @@ impl pack_service_server::PackService for GitksService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
tracing::info!(%repo, "upload-pack streaming started");
|
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);
|
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||||
tx.send(Ok(first))
|
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");
|
m.record("ok");
|
||||||
Ok(tonic::Response::new(result))
|
Ok(tonic::Response::new(result))
|
||||||
}
|
}
|
||||||
@@ -188,6 +193,7 @@ impl pack_service_server::PackService for GitksService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
tracing::info!(%repo, "receive-pack streaming started");
|
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);
|
let (tx, rx) = tokio::sync::mpsc::channel(16);
|
||||||
tx.send(Ok(first))
|
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");
|
m.record("ok");
|
||||||
Ok(tonic::Response::new(result))
|
Ok(tonic::Response::new(result))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ async fn test_advertise_refs() {
|
|||||||
repository: Some(hdr("test-repo")),
|
repository: Some(hdr("test-repo")),
|
||||||
protocol: None,
|
protocol: None,
|
||||||
service: String::new(),
|
service: String::new(),
|
||||||
|
raw: false,
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ async fn test_list_refs_via_service() {
|
|||||||
repository: Some(hdr()),
|
repository: Some(hdr()),
|
||||||
protocol: None,
|
protocol: None,
|
||||||
service: String::new(),
|
service: String::new(),
|
||||||
|
raw: false,
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
Reference in New Issue
Block a user