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::{refs::update::TypeChange, Status},
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 if let Some(skipped) = skipped.take() {
222 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 Ok(())
230 }
231 for (update, mapping, spec, edit) in updates {
232 if mapping.spec_index != last_spec_index {
233 last_spec_index = mapping.spec_index;
234 consume_skipped_tags(&mut skipped_due_to_implicit_tag, &mut out)?;
235 spec.to_ref().write_to(&mut out)?;
236 let is_implicit = mapping.spec_index.implicit_index().is_some();
237 if is_implicit {
238 write!(&mut out, " (implicit")?;
239 if spec.to_ref()
240 == gix::remote::fetch::Tags::Included
241 .to_refspec()
242 .expect("always yields refspec")
243 {
244 skipped_due_to_implicit_tag = Some(0);
245 write!(&mut out, ", due to auto-tag")?;
246 }
247 write!(&mut out, ")")?;
248 }
249 writeln!(out)?;
250 }
251
252 if let Some(num_skipped) = skipped_due_to_implicit_tag.as_mut() {
253 if matches!(update.mode, gix::remote::fetch::refs::update::Mode::NoChangeNeeded) {
254 *num_skipped += 1;
255 continue;
256 }
257 }
258
259 write!(out, "\t")?;
260 match &mapping.remote {
261 gix::remote::fetch::refmap::Source::ObjectId(id) => {
262 write!(out, "{}", id.attach(repo).shorten_or_id())?;
263 }
264 gix::remote::fetch::refmap::Source::Ref(r) => {
265 crate::repository::remote::refs::print_ref(&mut out, r)?;
266 }
267 }
268 let mode_and_type = update.type_change.map_or_else(
269 || format!("{}", update.mode),
270 |type_change| {
271 format!(
272 "{} ({})",
273 update.mode,
274 match type_change {
275 TypeChange::DirectToSymbolic => {
276 "direct ref overwrites symbolic"
277 }
278 TypeChange::SymbolicToDirect => {
279 "symbolic ref overwrites direct"
280 }
281 }
282 )
283 },
284 );
285 match edit {
286 Some(edit) => {
287 writeln!(out, " -> {} [{mode_and_type}]", edit.name)
288 }
289 None => writeln!(out, " [{mode_and_type}]"),
290 }?;
291 }
292 consume_skipped_tags(&mut skipped_due_to_implicit_tag, &mut out)?;
293 if !map.fixes.is_empty() {
294 writeln!(
295 err,
296 "The following destination refs were removed as they didn't start with 'ref/'"
297 )?;
298 map.fixes.sort_by(|l, r| match (l, r) {
299 (
300 Fix::MappingWithPartialDestinationRemoved { spec: l, .. },
301 Fix::MappingWithPartialDestinationRemoved { spec: r, .. },
302 ) => l.cmp(r),
303 });
304 let mut prev_spec = None;
305 for fix in &map.fixes {
306 match fix {
307 Fix::MappingWithPartialDestinationRemoved { name, spec } => {
308 if prev_spec.is_some_and(|prev_spec| prev_spec != spec) {
309 prev_spec = spec.into();
310 spec.to_ref().write_to(&mut err)?;
311 writeln!(err)?;
312 }
313 writeln!(err, "\t{name}")?;
314 }
315 }
316 }
317 }
318 if map.remote_refs.len() - map.mappings.len() != 0 {
319 writeln!(
320 err,
321 "server sent {} tips, {} were filtered due to {} refspec(s).",
322 map.remote_refs.len(),
323 map.remote_refs.len() - map.mappings.len(),
324 refspecs.len()
325 )?;
326 }
327 match negotiate.rounds.len() {
328 0 => writeln!(err, "no negotiation was necessary")?,
329 1 => {}
330 rounds => writeln!(err, "needed {rounds} rounds of pack-negotiation")?,
331 }
332 Ok(())
333 }
334}