Skip to main content

gitoxide_core/repository/
fetch.rs

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    /// If non-empty, override all ref-specs otherwise configured in the remote
10    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}