use gix::bstr::BString; use crate::OutputFormat; pub struct Options { pub format: OutputFormat, pub dry_run: bool, pub remote: Option, /// If non-empty, override all ref-specs otherwise configured in the remote pub ref_specs: Vec, pub shallow: gix::remote::fetch::Shallow, pub handshake_info: bool, pub negotiation_info: bool, pub open_negotiation_graph: Option, } pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=3; pub(crate) mod function { use anyhow::bail; use gix::{ prelude::ObjectIdExt, refspec::match_group::validate::Fix, remote::fetch::{Status, refs::update::TypeChange}, }; use layout::{ backends::svg::SVGWriter, core::{base::Orientation, geometry::Point, style::StyleAttr}, std_shapes::shapes::{Arrow, Element, ShapeKind}, }; use super::Options; use crate::OutputFormat; pub fn fetch

( repo: gix::Repository, mut progress: P, mut out: impl std::io::Write, err: impl std::io::Write, Options { format, dry_run, remote, handshake_info, negotiation_info, open_negotiation_graph, shallow, ref_specs, }: Options, ) -> anyhow::Result<()> where P: gix::NestedProgress, P::SubProgress: 'static, { if format != OutputFormat::Human { bail!("JSON output isn't yet supported for fetching."); } let mut remote = crate::repository::remote::by_name_or_url(&repo, remote.as_deref())?; if !ref_specs.is_empty() { remote.replace_refspecs(ref_specs.iter(), gix::remote::Direction::Fetch)?; remote = remote.with_fetch_tags(gix::remote::fetch::Tags::None); } let res: gix::remote::fetch::Outcome = remote .connect(gix::remote::Direction::Fetch)? .prepare_fetch(&mut progress, Default::default())? .with_dry_run(dry_run) .with_shallow(shallow) .receive(&mut progress, &gix::interrupt::IS_INTERRUPTED)?; if handshake_info { writeln!(out, "Handshake Information")?; writeln!(out, "\t{:?}", res.handshake)?; } let ref_specs = remote.refspecs(gix::remote::Direction::Fetch); match res.status { Status::NoPackReceived { update_refs, negotiate, dry_run: _, } => { let negotiate_default = Default::default(); print_updates( &repo, negotiate.as_ref().unwrap_or(&negotiate_default), update_refs, ref_specs, res.ref_map, &mut out, err, )?; if negotiation_info { print_negotiate_info(&mut out, negotiate.as_ref())?; } if let Some((negotiate, path)) = negotiate.as_ref().zip(open_negotiation_graph) { render_graph(&repo, &negotiate.graph, &path, progress)?; } Ok::<_, anyhow::Error>(()) } Status::Change { update_refs, write_pack_bundle, negotiate, } => { print_updates(&repo, &negotiate, update_refs, ref_specs, res.ref_map, &mut out, err)?; if let Some(data_path) = write_pack_bundle.data_path { writeln!(out, "pack file: \"{}\"", data_path.display()).ok(); } if let Some(index_path) = write_pack_bundle.index_path { writeln!(out, "index file: \"{}\"", index_path.display()).ok(); } if negotiation_info { print_negotiate_info(&mut out, Some(&negotiate))?; } if let Some(path) = open_negotiation_graph { render_graph(&repo, &negotiate.graph, &path, progress)?; } Ok(()) } }?; if dry_run { writeln!(out, "DRY-RUN: No ref was updated and no pack was received.").ok(); } Ok(()) } fn render_graph( repo: &gix::Repository, graph: &gix::negotiate::IdMap, path: &std::path::Path, mut progress: impl gix::Progress, ) -> anyhow::Result<()> { progress.init(Some(graph.len()), gix::progress::count("commits")); progress.set_name("building graph".into()); let mut map = gix::hashtable::HashMap::default(); let mut vg = layout::topo::layout::VisualGraph::new(Orientation::TopToBottom); for (id, commit) in graph.iter().inspect(|_| progress.inc()) { let source = match map.get(id) { Some(handle) => *handle, None => { let handle = vg.add_node(new_node(id.attach(repo), commit.data.flags)); map.insert(*id, handle); handle } }; for parent_id in &commit.parents { let dest = match map.get(parent_id) { Some(handle) => *handle, None => { let flags = match graph.get(parent_id) { Some(c) => c.data.flags, None => continue, }; let dest = vg.add_node(new_node(parent_id.attach(repo), flags)); map.insert(*parent_id, dest); dest } }; let arrow = Arrow::simple(""); vg.add_edge(arrow, source, dest); } } let start = std::time::Instant::now(); progress.set_name("layout graph".into()); progress.info(format!("writing {}…", path.display())); let mut svg = SVGWriter::new(); vg.do_it(false, false, false, &mut svg); std::fs::write(path, svg.finalize().as_bytes())?; open::that(path)?; progress.show_throughput(start); return Ok(()); fn new_node(id: gix::Id<'_>, flags: gix::negotiate::Flags) -> Element { let pt = Point::new(250., 50.); let name = format!("{}\n\n{flags:?}", id.shorten_or_id()); let shape = ShapeKind::new_box(name.as_str()); let style = StyleAttr::simple(); Element::create(shape, style, Orientation::LeftToRight, pt) } } fn print_negotiate_info( mut out: impl std::io::Write, negotiate: Option<&gix::remote::fetch::outcome::Negotiate>, ) -> std::io::Result<()> { writeln!(out, "Negotiation Phase Information")?; match negotiate { Some(negotiate) => { writeln!(out, "\t{:?}", negotiate.rounds)?; writeln!(out, "\tnum commits traversed in graph: {}", negotiate.graph.len()) } None => writeln!(out, "\tno negotiation performed"), } } pub(crate) fn print_updates( repo: &gix::Repository, negotiate: &gix::remote::fetch::outcome::Negotiate, update_refs: gix::remote::fetch::refs::update::Outcome, refspecs: &[gix::refspec::RefSpec], mut map: gix::remote::fetch::RefMap, mut out: impl std::io::Write, mut err: impl std::io::Write, ) -> anyhow::Result<()> { let mut last_spec_index = gix::remote::fetch::refmap::SpecIndex::ExplicitInRemote(usize::MAX); let mut updates = update_refs .iter_mapping_updates(&map.mappings, refspecs, &map.extra_refspecs) .filter_map(|(update, mapping, spec, edit)| spec.map(|spec| (update, mapping, spec, edit))) .collect::>(); updates.sort_by_key(|t| t.2); let mut skipped_due_to_implicit_tag = None; fn consume_skipped_tags(skipped: &mut Option, out: &mut impl std::io::Write) -> std::io::Result<()> { match skipped.take() { Some(skipped) if skipped != 0 => { writeln!( out, "\tskipped {skipped} tags known to the remote without bearing on this commit-graph. Use `gix remote ref-map` to list them." )?; } _ => {} } Ok(()) } for (update, mapping, spec, edit) in updates { if mapping.spec_index != last_spec_index { last_spec_index = mapping.spec_index; consume_skipped_tags(&mut skipped_due_to_implicit_tag, &mut out)?; spec.to_ref().write_to(&mut out)?; let is_implicit = mapping.spec_index.implicit_index().is_some(); if is_implicit { write!(&mut out, " (implicit")?; if spec.to_ref() == gix::remote::fetch::Tags::Included .to_refspec() .expect("always yields refspec") { skipped_due_to_implicit_tag = Some(0); write!(&mut out, ", due to auto-tag")?; } write!(&mut out, ")")?; } writeln!(out)?; } match skipped_due_to_implicit_tag.as_mut() { Some(num_skipped) if matches!(update.mode, gix::remote::fetch::refs::update::Mode::NoChangeNeeded) => { *num_skipped += 1; continue; } _ => {} } write!(out, "\t")?; match &mapping.remote { gix::remote::fetch::refmap::Source::ObjectId(id) => { write!(out, "{}", id.attach(repo).shorten_or_id())?; } gix::remote::fetch::refmap::Source::Ref(r) => { crate::repository::remote::refs::print_ref(&mut out, r)?; } } let mode_and_type = update.type_change.map_or_else( || format!("{}", update.mode), |type_change| { format!( "{} ({})", update.mode, match type_change { TypeChange::DirectToSymbolic => { "direct ref overwrites symbolic" } TypeChange::SymbolicToDirect => { "symbolic ref overwrites direct" } } ) }, ); match edit { Some(edit) => { writeln!(out, " -> {} [{mode_and_type}]", edit.name) } None => writeln!(out, " [{mode_and_type}]"), }?; } consume_skipped_tags(&mut skipped_due_to_implicit_tag, &mut out)?; if !map.fixes.is_empty() { writeln!( err, "The following destination refs were removed as they didn't start with 'ref/'" )?; map.fixes.sort_by(|l, r| match (l, r) { ( Fix::MappingWithPartialDestinationRemoved { spec: l, .. }, Fix::MappingWithPartialDestinationRemoved { spec: r, .. }, ) => l.cmp(r), }); let mut prev_spec = None; for fix in &map.fixes { match fix { Fix::MappingWithPartialDestinationRemoved { name, spec } => { if prev_spec.is_some_and(|prev_spec| prev_spec != spec) { prev_spec = spec.into(); spec.to_ref().write_to(&mut err)?; writeln!(err)?; } writeln!(err, "\t{name}")?; } } } } if map.remote_refs.len() - map.mappings.len() != 0 { writeln!( err, "server sent {} tips, {} were filtered due to {} refspec(s).", map.remote_refs.len(), map.remote_refs.len().saturating_sub(map.mappings.len()), refspecs.len() )?; } match negotiate.rounds.len() { 0 => writeln!(err, "no negotiation was necessary")?, 1 => {} rounds => writeln!(err, "needed {rounds} rounds of pack-negotiation")?, } Ok(()) } }