use anyhow::Result; use tempfile::TempDir; use std::{ env, io, io::BufRead, path::PathBuf, sync::{atomic::AtomicBool, Arc}, }; use git_repository::{ hash::ObjectId, objs::bstr::{BString, ByteSlice}, odb::pack, protocol::{ credentials, fetch, fetch::{Action, Arguments, Delegate, DelegateBlocking, LsRefsAction, Ref, Response}, transport, transport::client::Capabilities, FetchConnection, }, Progress, }; use quick_error::quick_error; quick_error! { #[derive(Debug)] enum Error { DecodeObject(err: git_repository::objs::decode::Error) { display("Could not decode object") source(err) from() } } } pub struct Context { pub thread_limit: Option, pub should_interrupt: Arc, pub out: W, pub object_hash: git_repository::hash::Kind, } struct FetchDelegate { ctx: Context, directory: Option, ref_filter: Option<&'static [&'static str]>, wanted_refs: Vec, detected_version: Option, fetched_packfile: Option, } static FILTER: &[&str] = &[ "HEAD", "refs/tags", "refs/heads", "refs/pull", "refs/merge-requests", ]; fn remote_supports_ref_in_want(server: &Capabilities) -> bool { server .capability("fetch") .and_then(|cap| cap.supports("ref-in-want")) .unwrap_or(false) } impl DelegateBlocking for FetchDelegate { fn handshake_extra_parameters(&self) -> Vec<(String, Option)> { Vec::new() } fn prepare_ls_refs( &mut self, server: &Capabilities, arguments: &mut Vec, _features: &mut Vec<(&str, Option<&str>)>, ) -> io::Result { if server.contains("ls-refs") { arguments.extend(FILTER.iter().map(|r| format!("ref-prefix {}", r).into())); } Ok(if self.wanted_refs.is_empty() { LsRefsAction::Continue } else { LsRefsAction::Skip }) } fn prepare_fetch( &mut self, version: transport::Protocol, server: &Capabilities, _features: &mut Vec<(&str, Option<&str>)>, _refs: &[Ref], ) -> io::Result { if !self.wanted_refs.is_empty() && !remote_supports_ref_in_want(server) { return Err(io::Error::new( io::ErrorKind::Other, "Want to get specific refs, but remote doesn't support this capability", )); } if version == transport::Protocol::V1 { self.ref_filter = Some(FILTER); } self.detected_version = Some(version); Ok(Action::Continue) } fn negotiate( &mut self, refs: &[Ref], arguments: &mut Arguments, _previous_response: Option<&Response>, ) -> io::Result { if self.detected_version == Some(transport::Protocol::V2) { if arguments.can_use_filter() { arguments.filter("tree:0") } } if self.wanted_refs.is_empty() { for r in refs { let (path, id) = r.unpack(); match self.ref_filter { Some(ref_prefixes) => { if ref_prefixes .iter() .any(|prefix| path.starts_with_str(prefix)) { arguments.want(id); } } None => arguments.want(id), } } } else { for r in &self.wanted_refs { arguments.want_ref(r.as_ref()) } } Ok(Action::Cancel) } } impl Delegate for FetchDelegate { fn receive_pack( &mut self, input: impl BufRead, progress: impl Progress, refs: &[Ref], _previous_response: &Response, ) -> io::Result<()> { let options = pack::bundle::write::Options { thread_limit: self.ctx.thread_limit, index_kind: pack::index::Version::V2, iteration_mode: pack::data::input::Mode::Verify, object_hash: self.ctx.object_hash, }; let outcome = pack::Bundle::write_to_directory( input, self.directory.take(), progress, &self.ctx.should_interrupt, None, options, ) .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; self.fetched_packfile = outcome.data_path; Ok(()) } } fn print_hash_and_path( out: &mut impl io::Write, name: &str, id: ObjectId, path: Option, ) -> io::Result<()> { match path { Some(path) => writeln!(out, "{}: {} ({})", name, id, path.display()), None => writeln!(out, "{}: {}", name, id), } } fn print( out: &mut impl io::Write, res: pack::bundle::write::Outcome, refs: &[Ref], ) -> io::Result<()> { print_hash_and_path(out, "index", res.index.index_hash, res.index_path)?; print_hash_and_path(out, "pack", res.index.data_hash, res.data_path)?; writeln!(out)?; for r in refs { match r { Ref::Direct { path, object } => writeln!(out, "{} {}", object.to_hex(), path), Ref::Peeled { path, object, tag } => { writeln!(out, "{} {} tag:{}", object.to_hex(), path, tag) } Ref::Symbolic { path, target, object, } => { writeln!(out, "{} {} symref-target:{}", object.to_hex(), path, target) } }?; } Ok(()) } fn main() -> Result<()> { for argument in env::args().skip(1) { let url = argument.as_bytes(); let client = transport::connect(url, transport::Protocol::V2)?; println!( "Connected with transport {:?}", client.supported_protocol_versions() ); let tmp_dir = TempDir::new()?; let mut delegate = FetchDelegate { ctx: Context { should_interrupt: Arc::new(AtomicBool::new(false)), out: std::io::stderr(), thread_limit: None, object_hash: git_repository::hash::Kind::Sha1, }, directory: Some(tmp_dir.path().into()), ref_filter: None, wanted_refs: vec![], detected_version: None, fetched_packfile: None, }; fetch( client, &mut delegate, credentials::helper, git_repository::progress::Discard, FetchConnection::AllowReuse, )?; let pack_path = delegate.fetched_packfile.expect("fetched a packfile"); let bundle = git_pack::Bundle::at(pack_path, delegate.ctx.object_hash)?; let _result: Result<_, git_pack::index::traverse::Error> = bundle.index.traverse( &bundle.pack, git_repository::progress::Discard, &delegate.ctx.should_interrupt, move || { move |object_kind, buf, index_entry, progress| { let obj = git_repository::objs::Data::new(object_kind, buf) .decode() .map_err(|e| Error::DecodeObject(e))?; println!("{:#?}", obj); Ok(()) } }, git_pack::index::traverse::Options { traversal: git_pack::index::traverse::Algorithm::Lookup, thread_limit: delegate.ctx.thread_limit, check: pack::index::traverse::SafetyCheck::All, make_pack_lookup_cache: git_pack::cache::lru::StaticLinkedList::<64>::default, }, ); } Ok(()) } // let capabilities = res.capabilities.clone(); // match res.actual_protocol { // Protocol::V2 => { // drop(res); // let base_client_options = [ // ("agent", Some("swh.loader.git.rs/0.0.0devel")), // ("object-format", Some("sha1")), // ]; // let ls_refs = client.invoke( // "ls-refs", // base_client_options.iter().cloned(), // Some( // [ // "peel", // "symrefs", // "ref-prefix HEAD", // "ref-prefix refs/heads", // "ref-prefix refs/tags", // ] // .iter() // .map(|s| s.as_bytes().as_bstr().to_owned()), // ), // )?; // let listed_refs: HashMap)> = ls_refs // .lines() // .flat_map(|l| { // l.map(|s| { // s.trim() // .split_once(' ') // .map(|(head, tail)| match tail.split_once(' ') { // None => (tail.to_string(), (head.to_string(), None)), // Some((refname, opts)) => ( // refname.to_string(), // (head.to_string(), Some(opts.to_string())), // ), // }) // .unwrap() // }) // }) // .collect(); // println!("{:#?}\n\n", listed_refs); // let mut fetch_arguments = vec![]; // if capabilities // .capability("fetch") // .as_ref() // .map(|cap| cap.supports("filter").unwrap_or(false)) // .unwrap_or(false) // { // fetch_arguments.push("filter tree:0".to_string()); // } // fetch_arguments.extend( // listed_refs // .iter() // .map(|(_refname, (head, opts))| match opts { // None => Some(head.as_str()), // Some(ref opts) => opts.strip_prefix("peeled:"), // }) // .flatten() // .unique() // .map(|wanted| format!("want {}", wanted)), // ); // fetch_arguments.push("done".to_string()); // eprintln!("{:#?}", fetch_arguments); // let mut fetcher = client.invoke( // "fetch", // base_client_options.iter().cloned(), // Some( // fetch_arguments // .iter() // .map(|s| s.as_bytes().as_bstr().to_owned()), // ), // )?; // let mut buf = String::new(); // fetcher.read_line(&mut buf)?; // if buf != "packfile\n" { // panic!("Unexpected read: {}", buf) // } // fetcher.set_progress_handler(Some(Box::new(|_is_err, data| { // eprintln!("{}", std::str::from_utf8(data).expect("valid utf8")) // }))); // let tmp_dir = TempDir::new()?; // let bundle = git_pack::Bundle::write_to_directory( // fetcher, // Some(tmp_dir.path()), // git_features::progress::Discard, // &AtomicBool::new(false), // None, // git_pack::bundle::write::Options::default(), // ) // .map(|r| r.to_bundle())?; // } // Protocol::V1 => {} // }; // }