1use gix::bstr::BString;
2
3use crate::OutputFormat;
4
5pub struct Options {
6 pub format: OutputFormat,
7 pub dry_run: bool,
8 pub remote: Option<String>,
9 pub ref_specs: Vec<BString>,
11 pub shallow: gix::remote::fetch::Shallow,
12 pub handshake_info: bool,
13 pub negotiation_info: bool,
14 pub open_negotiation_graph: Option<std::path::PathBuf>,
15}
16
17pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;
18
19pub(crate) mod function {
20 use anyhow::bail;
21 use gix::{
22 prelude::ObjectIdExt,
23 refspec::match_group::validate::Fix,
24 remote::fetch::{Status, refs::update::TypeChange},
25 };
26 use layout::{
27 backends::svg::SVGWriter,
28 core::{base::Orientation, geometry::Point, style::StyleAttr},
29 std_shapes::shapes::{Arrow, Element, ShapeKind},
30 };
31
32 use super::Options;
33 use crate::OutputFormat;
34
35 pub fn fetch<P>(
36 repo: gix::Repository,
37 mut progress: P,
38 mut out: impl std::io::Write,
39 err: impl std::io::Write,
40 Options {
41 format,
42 dry_run,
43 remote,
44 handshake_info,
45 negotiation_info,
46 open_negotiation_graph,
47 shallow,
48 ref_specs,
49 }: Options,
50 ) -> anyhow::Result<()>
51 where
52 P: gix::NestedProgress,
53 P::SubProgress: 'static,
54 {
55 if format != OutputFormat::Human {
56 bail!("JSON output isn't yet supported for fetching.");
57 }
58
59 let mut remote = crate::repository::remote::by_name_or_url(&repo, remote.as_deref())?;
60 if !ref_specs.is_empty() {
61 remote.replace_refspecs(ref_specs.iter(), gix::remote::Direction::Fetch)?;
62 remote = remote.with_fetch_tags(gix::remote::fetch::Tags::None);
63 }
64 let res: gix::remote::fetch::Outcome = remote
65 .connect(gix::remote::Direction::Fetch)?
66 .prepare_fetch(&mut progress, Default::default())?
67 .with_dry_run(dry_run)
68 .with_shallow(shallow)
69 .receive(&mut progress, &gix::interrupt::IS_INTERRUPTED)?;
70
71 if handshake_info {
72 writeln!(out, "Handshake Information")?;
73 writeln!(out, "\t{:?}", res.handshake)?;
74 }
75
76 let ref_specs = remote.refspecs(gix::remote::Direction::Fetch);
77 match res.status {
78 Status::NoPackReceived {
79 update_refs,
80 negotiate,
81 dry_run: _,
82 } => {
83 let negotiate_default = Default::default();
84 print_updates(
85 &repo,
86 negotiate.as_ref().unwrap_or(&negotiate_default),
87 update_refs,
88 ref_specs,
89 res.ref_map,
90 &mut out,
91 err,
92 )?;
93 if negotiation_info {
94 print_negotiate_info(&mut out, negotiate.as_ref())?;
95 }
96 if let Some((negotiate, path)) =
97 open_negotiation_graph.and_then(|path| negotiate.as_ref().map(|n| (n, path)))
98 {
99 render_graph(&repo, &negotiate.graph, &path, progress)?;
100 }
101 Ok::<_, anyhow::Error>(())
102 }
103 Status::Change {
104 update_refs,
105 write_pack_bundle,
106 negotiate,
107 } => {
108 print_updates(&repo, &negotiate, update_refs, ref_specs, res.ref_map, &mut out, err)?;
109 if let Some(data_path) = write_pack_bundle.data_path {
110 writeln!(out, "pack file: \"{}\"", data_path.display()).ok();
111 }
112 if let Some(index_path) = write_pack_bundle.index_path {
113 writeln!(out, "index file: \"{}\"", index_path.display()).ok();
114 }
115 if negotiation_info {
116 print_negotiate_info(&mut out, Some(&negotiate))?;
117 }
118 if let Some(path) = open_negotiation_graph {
119 render_graph(&repo, &negotiate.graph, &path, progress)?;
120 }
121 Ok(())
122 }
123 }?;
124 if dry_run {
125 writeln!(out, "DRY-RUN: No ref was updated and no pack was received.").ok();
126 }
127 Ok(())
128 }
129
130 fn render_graph(
131 repo: &gix::Repository,
132 graph: &gix::negotiate::IdMap,
133 path: &std::path::Path,
134 mut progress: impl gix::Progress,
135 ) -> anyhow::Result<()> {
136 progress.init(Some(graph.len()), gix::progress::count("commits"));
137 progress.set_name("building graph".into());
138
139 let mut map = gix::hashtable::HashMap::default();
140 let mut vg = layout::topo::layout::VisualGraph::new(Orientation::TopToBottom);
141
142 for (id, commit) in graph.iter().inspect(|_| progress.inc()) {
143 let source = match map.get(id) {
144 Some(handle) => *handle,
145 None => {
146 let handle = vg.add_node(new_node(id.attach(repo), commit.data.flags));
147 map.insert(*id, handle);
148 handle
149 }
150 };
151
152 for parent_id in &commit.parents {
153 let dest = match map.get(parent_id) {
154 Some(handle) => *handle,
155 None => {
156 let flags = match graph.get(parent_id) {
157 Some(c) => c.data.flags,
158 None => continue,
159 };
160 let dest = vg.add_node(new_node(parent_id.attach(repo), flags));
161 map.insert(*parent_id, dest);
162 dest
163 }
164 };
165 let arrow = Arrow::simple("");
166 vg.add_edge(arrow, source, dest);
167 }
168 }
169
170 let start = std::time::Instant::now();
171 progress.set_name("layout graph".into());
172 progress.info(format!("writing {}…", path.display()));
173 let mut svg = SVGWriter::new();
174 vg.do_it(false, false, false, &mut svg);
175 std::fs::write(path, svg.finalize().as_bytes())?;
176 open::that(path)?;
177 progress.show_throughput(start);
178
179 return Ok(());
180
181 fn new_node(id: gix::Id<'_>, flags: gix::negotiate::Flags) -> Element {
182 let pt = Point::new(250., 50.);
183 let name = format!("{}\n\n{flags:?}", id.shorten_or_id());
184 let shape = ShapeKind::new_box(name.as_str());
185 let style = StyleAttr::simple();
186 Element::create(shape, style, Orientation::LeftToRight, pt)
187 }
188 }
189
190 fn print_negotiate_info(
191 mut out: impl std::io::Write,
192 negotiate: Option<&gix::remote::fetch::outcome::Negotiate>,
193 ) -> std::io::Result<()> {
194 writeln!(out, "Negotiation Phase Information")?;
195 match negotiate {
196 Some(negotiate) => {
197 writeln!(out, "\t{:?}", negotiate.rounds)?;
198 writeln!(out, "\tnum commits traversed in graph: {}", negotiate.graph.len())
199 }
200 None => writeln!(out, "\tno negotiation performed"),
201 }
202 }
203
204 pub(crate) fn print_updates(
205 repo: &gix::Repository,
206 negotiate: &gix::remote::fetch::outcome::Negotiate,
207 update_refs: gix::remote::fetch::refs::update::Outcome,
208 refspecs: &[gix::refspec::RefSpec],
209 mut map: gix::remote::fetch::RefMap,
210 mut out: impl std::io::Write,
211 mut err: impl std::io::Write,
212 ) -> anyhow::Result<()> {
213 let mut last_spec_index = gix::remote::fetch::refmap::SpecIndex::ExplicitInRemote(usize::MAX);
214 let mut updates = update_refs
215 .iter_mapping_updates(&map.mappings, refspecs, &map.extra_refspecs)
216 .filter_map(|(update, mapping, spec, edit)| spec.map(|spec| (update, mapping, spec, edit)))
217 .collect::<Vec<_>>();
218 updates.sort_by_key(|t| t.2);
219 let mut skipped_due_to_implicit_tag = None;
220 fn consume_skipped_tags(skipped: &mut Option<usize>, out: &mut impl std::io::Write) -> std::io::Result<()> {
221 match skipped.take() {
222 Some(skipped) if skipped != 0 => {
223 writeln!(
224 out,
225 "\tskipped {skipped} tags known to the remote without bearing on this commit-graph. Use `gix remote ref-map` to list them."
226 )?;
227 }
228 _ => {}
229 }
230 Ok(())
231 }
232 for (update, mapping, spec, edit) in updates {
233 if mapping.spec_index != last_spec_index {
234 last_spec_index = mapping.spec_index;
235 consume_skipped_tags(&mut skipped_due_to_implicit_tag, &mut out)?;
236 spec.to_ref().write_to(&mut out)?;
237 let is_implicit = mapping.spec_index.implicit_index().is_some();
238 if is_implicit {
239 write!(&mut out, " (implicit")?;
240 if spec.to_ref()
241 == gix::remote::fetch::Tags::Included
242 .to_refspec()
243 .expect("always yields refspec")
244 {
245 skipped_due_to_implicit_tag = Some(0);
246 write!(&mut out, ", due to auto-tag")?;
247 }
248 write!(&mut out, ")")?;
249 }
250 writeln!(out)?;
251 }
252
253 match skipped_due_to_implicit_tag.as_mut() {
254 Some(num_skipped) if matches!(update.mode, gix::remote::fetch::refs::update::Mode::NoChangeNeeded) => {
255 *num_skipped += 1;
256 continue;
257 }
258 _ => {}
259 }
260
261 write!(out, "\t")?;
262 match &mapping.remote {
263 gix::remote::fetch::refmap::Source::ObjectId(id) => {
264 write!(out, "{}", id.attach(repo).shorten_or_id())?;
265 }
266 gix::remote::fetch::refmap::Source::Ref(r) => {
267 crate::repository::remote::refs::print_ref(&mut out, r)?;
268 }
269 }
270 let mode_and_type = update.type_change.map_or_else(
271 || format!("{}", update.mode),
272 |type_change| {
273 format!(
274 "{} ({})",
275 update.mode,
276 match type_change {
277 TypeChange::DirectToSymbolic => {
278 "direct ref overwrites symbolic"
279 }
280 TypeChange::SymbolicToDirect => {
281 "symbolic ref overwrites direct"
282 }
283 }
284 )
285 },
286 );
287 match edit {
288 Some(edit) => {
289 writeln!(out, " -> {} [{mode_and_type}]", edit.name)
290 }
291 None => writeln!(out, " [{mode_and_type}]"),
292 }?;
293 }
294 consume_skipped_tags(&mut skipped_due_to_implicit_tag, &mut out)?;
295 if !map.fixes.is_empty() {
296 writeln!(
297 err,
298 "The following destination refs were removed as they didn't start with 'ref/'"
299 )?;
300 map.fixes.sort_by(|l, r| match (l, r) {
301 (
302 Fix::MappingWithPartialDestinationRemoved { spec: l, .. },
303 Fix::MappingWithPartialDestinationRemoved { spec: r, .. },
304 ) => l.cmp(r),
305 });
306 let mut prev_spec = None;
307 for fix in &map.fixes {
308 match fix {
309 Fix::MappingWithPartialDestinationRemoved { name, spec } => {
310 if prev_spec.is_some_and(|prev_spec| prev_spec != spec) {
311 prev_spec = spec.into();
312 spec.to_ref().write_to(&mut err)?;
313 writeln!(err)?;
314 }
315 writeln!(err, "\t{name}")?;
316 }
317 }
318 }
319 }
320 if map.remote_refs.len() - map.mappings.len() != 0 {
321 writeln!(
322 err,
323 "server sent {} tips, {} were filtered due to {} refspec(s).",
324 map.remote_refs.len(),
325 map.remote_refs.len().saturating_sub(map.mappings.len()),
326 refspecs.len()
327 )?;
328 }
329 match negotiate.rounds.len() {
330 0 => writeln!(err, "no negotiation was necessary")?,
331 1 => {}
332 rounds => writeln!(err, "needed {rounds} rounds of pack-negotiation")?,
333 }
334 Ok(())
335 }
336}