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)) = negotiate.as_ref().zip(open_negotiation_graph) {
97 render_graph(&repo, &negotiate.graph, &path, progress)?;
98 }
99 Ok::<_, anyhow::Error>(())
100 }
101 Status::Change {
102 update_refs,
103 write_pack_bundle,
104 negotiate,
105 } => {
106 print_updates(&repo, &negotiate, update_refs, ref_specs, res.ref_map, &mut out, err)?;
107 if let Some(data_path) = write_pack_bundle.data_path {
108 writeln!(out, "pack file: \"{}\"", data_path.display()).ok();
109 }
110 if let Some(index_path) = write_pack_bundle.index_path {
111 writeln!(out, "index file: \"{}\"", index_path.display()).ok();
112 }
113 if negotiation_info {
114 print_negotiate_info(&mut out, Some(&negotiate))?;
115 }
116 if let Some(path) = open_negotiation_graph {
117 render_graph(&repo, &negotiate.graph, &path, progress)?;
118 }
119 Ok(())
120 }
121 }?;
122 if dry_run {
123 writeln!(out, "DRY-RUN: No ref was updated and no pack was received.").ok();
124 }
125 Ok(())
126 }
127
128 fn render_graph(
129 repo: &gix::Repository,
130 graph: &gix::negotiate::IdMap,
131 path: &std::path::Path,
132 mut progress: impl gix::Progress,
133 ) -> anyhow::Result<()> {
134 progress.init(Some(graph.len()), gix::progress::count("commits"));
135 progress.set_name("building graph".into());
136
137 let mut map = gix::hashtable::HashMap::default();
138 let mut vg = layout::topo::layout::VisualGraph::new(Orientation::TopToBottom);
139
140 for (id, commit) in graph.iter().inspect(|_| progress.inc()) {
141 let source = match map.get(id) {
142 Some(handle) => *handle,
143 None => {
144 let handle = vg.add_node(new_node(id.attach(repo), commit.data.flags));
145 map.insert(*id, handle);
146 handle
147 }
148 };
149
150 for parent_id in &commit.parents {
151 let dest = match map.get(parent_id) {
152 Some(handle) => *handle,
153 None => {
154 let flags = match graph.get(parent_id) {
155 Some(c) => c.data.flags,
156 None => continue,
157 };
158 let dest = vg.add_node(new_node(parent_id.attach(repo), flags));
159 map.insert(*parent_id, dest);
160 dest
161 }
162 };
163 let arrow = Arrow::simple("");
164 vg.add_edge(arrow, source, dest);
165 }
166 }
167
168 let start = std::time::Instant::now();
169 progress.set_name("layout graph".into());
170 progress.info(format!("writing {}…", path.display()));
171 let mut svg = SVGWriter::new();
172 vg.do_it(false, false, false, &mut svg);
173 std::fs::write(path, svg.finalize().as_bytes())?;
174 open::that(path)?;
175 progress.show_throughput(start);
176
177 return Ok(());
178
179 fn new_node(id: gix::Id<'_>, flags: gix::negotiate::Flags) -> Element {
180 let pt = Point::new(250., 50.);
181 let name = format!("{}\n\n{flags:?}", id.shorten_or_id());
182 let shape = ShapeKind::new_box(name.as_str());
183 let style = StyleAttr::simple();
184 Element::create(shape, style, Orientation::LeftToRight, pt)
185 }
186 }
187
188 fn print_negotiate_info(
189 mut out: impl std::io::Write,
190 negotiate: Option<&gix::remote::fetch::outcome::Negotiate>,
191 ) -> std::io::Result<()> {
192 writeln!(out, "Negotiation Phase Information")?;
193 match negotiate {
194 Some(negotiate) => {
195 writeln!(out, "\t{:?}", negotiate.rounds)?;
196 writeln!(out, "\tnum commits traversed in graph: {}", negotiate.graph.len())
197 }
198 None => writeln!(out, "\tno negotiation performed"),
199 }
200 }
201
202 pub(crate) fn print_updates(
203 repo: &gix::Repository,
204 negotiate: &gix::remote::fetch::outcome::Negotiate,
205 update_refs: gix::remote::fetch::refs::update::Outcome,
206 refspecs: &[gix::refspec::RefSpec],
207 mut map: gix::remote::fetch::RefMap,
208 mut out: impl std::io::Write,
209 mut err: impl std::io::Write,
210 ) -> anyhow::Result<()> {
211 let mut last_spec_index = gix::remote::fetch::refmap::SpecIndex::ExplicitInRemote(usize::MAX);
212 let mut updates = update_refs
213 .iter_mapping_updates(&map.mappings, refspecs, &map.extra_refspecs)
214 .filter_map(|(update, mapping, spec, edit)| spec.map(|spec| (update, mapping, spec, edit)))
215 .collect::<Vec<_>>();
216 updates.sort_by_key(|t| t.2);
217 let mut skipped_due_to_implicit_tag = None;
218 fn consume_skipped_tags(skipped: &mut Option<usize>, out: &mut impl std::io::Write) -> std::io::Result<()> {
219 match skipped.take() {
220 Some(skipped) if skipped != 0 => {
221 writeln!(
222 out,
223 "\tskipped {skipped} tags known to the remote without bearing on this commit-graph. Use `gix remote ref-map` to list them."
224 )?;
225 }
226 _ => {}
227 }
228 Ok(())
229 }
230 for (update, mapping, spec, edit) in updates {
231 if mapping.spec_index != last_spec_index {
232 last_spec_index = mapping.spec_index;
233 consume_skipped_tags(&mut skipped_due_to_implicit_tag, &mut out)?;
234 spec.to_ref().write_to(&mut out)?;
235 let is_implicit = mapping.spec_index.implicit_index().is_some();
236 if is_implicit {
237 write!(&mut out, " (implicit")?;
238 if spec.to_ref()
239 == gix::remote::fetch::Tags::Included
240 .to_refspec()
241 .expect("always yields refspec")
242 {
243 skipped_due_to_implicit_tag = Some(0);
244 write!(&mut out, ", due to auto-tag")?;
245 }
246 write!(&mut out, ")")?;
247 }
248 writeln!(out)?;
249 }
250
251 match skipped_due_to_implicit_tag.as_mut() {
252 Some(num_skipped) if matches!(update.mode, gix::remote::fetch::refs::update::Mode::NoChangeNeeded) => {
253 *num_skipped += 1;
254 continue;
255 }
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().saturating_sub(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}