Untitled
unknown
rust
9 days ago
6.1 kB
10
Indexable
use gix_protocol::{
handshake,
handshake::Ref,
indicate_end_of_interaction,
};
use gix_transport::{
client::blocking_io::connect,
Protocol, Service,
};
use gix_features::progress::prodash::progress::Discard;
fn ls_remote_tags(url: &str) -> Result<Vec<Ref>, Box<dyn std::error::Error>> {
// 1. Open a transport — pure network, zero disk I/O.
let transport = connect(
url,
connect::Options {
version: Protocol::V2,
..Default::default()
},
)?;
// 2. Handshake (the git protocol upload-pack greeting).
let mut outcome = handshake(
transport,
Service::UploadPack,
|_| { unreachable!("no auth configured") },
vec![],
&mut Discard,
)?;
// 3. V2: refs aren't included in the handshake — we must
// issue an explicit ls-refs command.
// V1: refs already arrived with the handshake.
let refs = if outcome.server_protocol_version == Protocol::V2 {
// Build an LsRefsCommand that filters to refs/tags/*.
// Passing `None` for prefix_refspecs means "give me everything."
// To filter to tags only, we build a refspec for "refs/tags/*".
let tag_spec = gix_refspec::parse(
"refs/tags/*".into(),
gix_refspec::parse::Operation::Fetch,
)?.to_owned();
let cmd = gix_protocol::LsRefsCommand::new(
Some(&[tag_spec]), // server-side ref-prefix filter
&outcome.capabilities,
gix_protocol::agent("my-app"),
);
let transport = outcome
.refs
.take() // not used in V2, but reclaim the transport
.ok_or("expected transport")?; // (won't actually fire for V2)
// Actually: the transport is what we need to keep hold of.
// Let me correct — after handshake the transport is returned
// *inside* the Handshake struct only for V1 refs. For V2 we
// need the original transport back. `handshake()` consumes it
// and the only way to keep issuing commands is through the
// transport that comes back embedded in the Handshake.
// Let me restructure properly ↓
todo!()
} else {
// V1: refs came with the handshake.
outcome.refs.unwrap_or_default()
};
Ok(refs)
}
OK — the sticking point everyone hits is that handshake() consumes the transport, and you need it back to call LsRefsCommand::invoke_blocking. The trick is that handshake() doesn't actually swallow it — it returns it inside outcome wrapped in a SendFlushOnDrop. But the public API doesn't expose that directly. The real way is to not use gix_protocol::handshake at all, and instead use the Transport trait's own handshake() method, which keeps ownership with you. Here's the corrected, complete version:
use gix_features::progress::prodash::progress::Discard;
use gix_protocol::{handshake::Ref, indicate_end_of_interaction, LsRefsCommand};
use gix_transport::{
client::blocking_io::{connect, Transport},
Protocol, Service,
};
fn ls_remote_tags(url: &str) -> Result<Vec<Ref>, Box<dyn std::error::Error>> {
// 1. Connect — network only, no disk.
let mut transport = connect(
url,
connect::Options {
version: Protocol::V2,
..Default::default()
},
)?;
// 2. Handshake via the Transport trait method.
// This borrows &mut, so we keep ownership.
let outcome = gix_protocol::handshake(
&mut transport,
Service::UploadPack,
|_| Err(std::io::Error::new(std::io::ErrorKind::Other, "no auth").into()),
vec![],
&mut Discard,
)?;
let refs = match outcome.server_protocol_version {
Protocol::V2 => {
// Build a refspec to filter to tags only on the server side.
let tag_spec = gix_refspec::parse(
"refs/tags/*".into(),
gix_refspec::parse::Operation::Fetch,
)?
.to_owned();
LsRefsCommand::new(
Some(&[tag_spec]),
&outcome.capabilities,
gix_protocol::agent("my-app"),
)
.invoke_blocking(&mut transport, &mut Discard, false)?
}
_ => {
// V1/V0: refs already came with the handshake.
// Filter to tags client-side.
outcome
.refs
.unwrap_or_default()
.into_iter()
.filter(|r| {
let name = match r {
Ref::Direct { full_ref_name, .. }
| Ref::Peeled { full_ref_name, .. }
| Ref::Symbolic { full_ref_name, .. }
| Ref::Unborn { full_ref_name, .. } => full_ref_name,
};
name.starts_with(b"refs/tags/")
})
.collect()
}
};
// 3. Graceful shutdown.
indicate_end_of_interaction(transport, false)?;
Ok(refs)
}
The key insight that trips everyone up: handshake() is generic over T: Transport, and &mut Box<dyn Transport> also implements Transport. So you pass &mut transport to handshake(), retain ownership, and then pass &mut transport again to invoke_blocking(). The borrow from the handshake is released by the time you call ls-refs.
The Cargo.toml dependencies you need:
[dependencies]
gix-transport = { version = "0.53", features = ["blocking-client", "http-client-reqwest"] }
gix-protocol = { version = "0.56", features = ["blocking-client", "handshake"] }
gix-refspec = "0.28"
gix-features = { version = "0.41", features = ["progress"] }
Swap http-client-reqwest for http-client-curl if you prefer, or drop it entirely if you only need git:// / ssh:// transports. The point is: zero disk I/O — no repo init, no clone, no tmpdir. It's purely transport + protocol over the wire.Editor is loading...
Leave a Comment