Untitled

 avatar
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