gitoxide_core/repository/
remote.rs

1#[cfg(any(feature = "blocking-client", feature = "async-client"))]
2mod refs_impl {
3    use anyhow::bail;
4    use gix::{
5        protocol::handshake,
6        refspec::{match_group::validate::Fix, RefSpec},
7        remote::fetch::refmap::Source,
8    };
9
10    use super::by_name_or_url;
11    use crate::OutputFormat;
12
13    pub mod refs {
14        use gix::bstr::BString;
15
16        use crate::OutputFormat;
17
18        pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=2;
19
20        pub enum Kind {
21            Remote,
22            Tracking {
23                ref_specs: Vec<BString>,
24                show_unmapped_remote_refs: bool,
25            },
26        }
27
28        pub struct Options {
29            pub format: OutputFormat,
30            pub name_or_url: Option<String>,
31            pub handshake_info: bool,
32        }
33
34        pub(crate) use super::{print, print_ref};
35    }
36
37    #[gix::protocol::maybe_async::maybe_async]
38    pub async fn refs_fn(
39        repo: gix::Repository,
40        kind: refs::Kind,
41        mut progress: impl gix::Progress,
42        mut out: impl std::io::Write,
43        err: impl std::io::Write,
44        refs::Options {
45            format,
46            name_or_url,
47            handshake_info,
48        }: refs::Options,
49    ) -> anyhow::Result<()> {
50        use anyhow::Context;
51        let mut remote = by_name_or_url(&repo, name_or_url.as_deref())?;
52        let show_unmapped = if let refs::Kind::Tracking {
53            ref_specs,
54            show_unmapped_remote_refs,
55        } = &kind
56        {
57            if format != OutputFormat::Human {
58                bail!("JSON output isn't yet supported for listing ref-mappings.");
59            }
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            *show_unmapped_remote_refs
65        } else {
66            false
67        };
68        progress.info(format!(
69            "Connecting to {:?}",
70            remote
71                .url(gix::remote::Direction::Fetch)
72                .context("Remote didn't have a URL to connect to")?
73                .to_bstring()
74        ));
75        let (map, handshake) = remote
76            .connect(gix::remote::Direction::Fetch)
77            .await?
78            .ref_map(
79                &mut progress,
80                gix::remote::ref_map::Options {
81                    prefix_from_spec_as_filter_on_remote: !matches!(kind, refs::Kind::Remote),
82                    ..Default::default()
83                },
84            )
85            .await?;
86
87        if handshake_info {
88            writeln!(out, "Handshake Information")?;
89            writeln!(out, "\t{handshake:?}")?;
90        }
91        match kind {
92            refs::Kind::Tracking { .. } => print_refmap(
93                &repo,
94                remote.refspecs(gix::remote::Direction::Fetch),
95                map,
96                show_unmapped,
97                out,
98                err,
99            ),
100            refs::Kind::Remote => {
101                match format {
102                    OutputFormat::Human => drop(print(out, &map.remote_refs)),
103                    #[cfg(feature = "serde")]
104                    OutputFormat::Json => serde_json::to_writer_pretty(
105                        out,
106                        &map.remote_refs.into_iter().map(JsonRef::from).collect::<Vec<_>>(),
107                    )?,
108                }
109                Ok(())
110            }
111        }
112    }
113
114    pub(crate) fn print_refmap(
115        repo: &gix::Repository,
116        refspecs: &[RefSpec],
117        mut map: gix::remote::fetch::RefMap,
118        show_unmapped_remotes: bool,
119        mut out: impl std::io::Write,
120        mut err: impl std::io::Write,
121    ) -> anyhow::Result<()> {
122        let mut last_spec_index = gix::remote::fetch::refmap::SpecIndex::ExplicitInRemote(usize::MAX);
123        map.mappings.sort_by_key(|m| m.spec_index);
124        for mapping in &map.mappings {
125            if mapping.spec_index != last_spec_index {
126                last_spec_index = mapping.spec_index;
127                let spec = mapping
128                    .spec_index
129                    .get(refspecs, &map.extra_refspecs)
130                    .expect("refspecs here are the ones used for mapping");
131                spec.to_ref().write_to(&mut out)?;
132                let is_implicit = mapping.spec_index.implicit_index().is_some();
133                if is_implicit {
134                    write!(&mut out, " (implicit")?;
135                    if spec.to_ref()
136                        == gix::remote::fetch::Tags::Included
137                            .to_refspec()
138                            .expect("always yields refspec")
139                    {
140                        write!(&mut out, ", due to auto-tag")?;
141                    }
142                    write!(&mut out, ")")?;
143                }
144                writeln!(out)?;
145            }
146
147            write!(out, "\t")?;
148            let target_id = match &mapping.remote {
149                gix::remote::fetch::refmap::Source::ObjectId(id) => {
150                    write!(out, "{id}")?;
151                    id
152                }
153                gix::remote::fetch::refmap::Source::Ref(r) => print_ref(&mut out, r)?,
154            };
155            match &mapping.local {
156                Some(local) => {
157                    write!(out, " -> {local} ")?;
158                    match repo.try_find_reference(local)? {
159                        Some(tracking) => {
160                            let msg = match tracking.try_id() {
161                                Some(id) => {
162                                    if id.as_ref() == target_id {
163                                        "[up-to-date]"
164                                    } else {
165                                        "[changed]"
166                                    }
167                                }
168                                None => "[skipped]",
169                            };
170                            writeln!(out, "{msg}")
171                        }
172                        None => writeln!(out, "[new]"),
173                    }
174                }
175                None => writeln!(out, " (fetch only)"),
176            }?;
177        }
178        if !map.fixes.is_empty() {
179            writeln!(
180                err,
181                "The following destination refs were removed as they didn't start with 'ref/'"
182            )?;
183            map.fixes.sort_by(|l, r| match (l, r) {
184                (
185                    Fix::MappingWithPartialDestinationRemoved { spec: l, .. },
186                    Fix::MappingWithPartialDestinationRemoved { spec: r, .. },
187                ) => l.cmp(r),
188            });
189            let mut prev_spec = None;
190            for fix in &map.fixes {
191                match fix {
192                    Fix::MappingWithPartialDestinationRemoved { name, spec } => {
193                        if prev_spec.is_some_and(|prev_spec| prev_spec != spec) {
194                            prev_spec = spec.into();
195                            spec.to_ref().write_to(&mut err)?;
196                            writeln!(err)?;
197                        }
198                        writeln!(err, "\t{name}")?;
199                    }
200                }
201            }
202        }
203        if map.remote_refs.len() - map.mappings.len() != 0 {
204            writeln!(
205                err,
206                "server sent {} tips, {} were filtered due to {} refspec(s).",
207                map.remote_refs.len(),
208                map.remote_refs.len() - map.mappings.len(),
209                refspecs.len()
210            )?;
211            if show_unmapped_remotes {
212                writeln!(&mut out, "\nFiltered: ")?;
213                for remote_ref in map.remote_refs.iter().filter(|r| {
214                    !map.mappings.iter().any(|m| match &m.remote {
215                        Source::Ref(other) => other == *r,
216                        Source::ObjectId(_) => false,
217                    })
218                }) {
219                    print_ref(&mut out, remote_ref)?;
220                    writeln!(&mut out)?;
221                }
222            }
223        }
224        if refspecs.is_empty() {
225            bail!("Without refspecs there is nothing to show here. Add refspecs as arguments or configure them in .git/config.")
226        }
227        Ok(())
228    }
229
230    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
231    pub enum JsonRef {
232        Peeled {
233            path: String,
234            tag: String,
235            object: String,
236        },
237        Direct {
238            path: String,
239            object: String,
240        },
241        Unborn {
242            path: String,
243            target: String,
244        },
245        Symbolic {
246            path: String,
247            #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
248            tag: Option<String>,
249            target: String,
250            object: String,
251        },
252    }
253
254    impl From<handshake::Ref> for JsonRef {
255        fn from(value: handshake::Ref) -> Self {
256            match value {
257                handshake::Ref::Unborn { full_ref_name, target } => JsonRef::Unborn {
258                    path: full_ref_name.to_string(),
259                    target: target.to_string(),
260                },
261                handshake::Ref::Direct {
262                    full_ref_name: path,
263                    object,
264                } => JsonRef::Direct {
265                    path: path.to_string(),
266                    object: object.to_string(),
267                },
268                handshake::Ref::Symbolic {
269                    full_ref_name: path,
270                    tag,
271                    target,
272                    object,
273                } => JsonRef::Symbolic {
274                    path: path.to_string(),
275                    tag: tag.map(|t| t.to_string()),
276                    target: target.to_string(),
277                    object: object.to_string(),
278                },
279                handshake::Ref::Peeled {
280                    full_ref_name: path,
281                    tag,
282                    object,
283                } => JsonRef::Peeled {
284                    path: path.to_string(),
285                    tag: tag.to_string(),
286                    object: object.to_string(),
287                },
288            }
289        }
290    }
291
292    pub(crate) fn print_ref(mut out: impl std::io::Write, r: &handshake::Ref) -> std::io::Result<&gix::hash::oid> {
293        match r {
294            handshake::Ref::Direct {
295                full_ref_name: path,
296                object,
297            } => write!(&mut out, "{object} {path}").map(|_| object.as_ref()),
298            handshake::Ref::Peeled {
299                full_ref_name: path,
300                tag,
301                object,
302            } => write!(&mut out, "{tag} {path} object:{object}").map(|_| tag.as_ref()),
303            handshake::Ref::Symbolic {
304                full_ref_name: path,
305                tag,
306                target,
307                object,
308            } => match tag {
309                Some(tag) => {
310                    write!(&mut out, "{tag} {path} symref-target:{target} peeled:{object}").map(|_| tag.as_ref())
311                }
312                None => write!(&mut out, "{object} {path} symref-target:{target}").map(|_| object.as_ref()),
313            },
314            handshake::Ref::Unborn { full_ref_name, target } => {
315                static NULL: gix::hash::ObjectId = gix::hash::ObjectId::null(gix::hash::Kind::Sha1);
316                write!(&mut out, "unborn {full_ref_name} symref-target:{target}").map(|_| NULL.as_ref())
317            }
318        }
319    }
320
321    pub(crate) fn print(mut out: impl std::io::Write, refs: &[handshake::Ref]) -> std::io::Result<()> {
322        for r in refs {
323            print_ref(&mut out, r)?;
324            writeln!(out)?;
325        }
326        Ok(())
327    }
328}
329#[cfg(any(feature = "blocking-client", feature = "async-client"))]
330pub use refs_impl::{refs, refs_fn as refs, JsonRef};
331
332#[cfg(any(feature = "blocking-client", feature = "async-client"))]
333pub(crate) fn by_name_or_url<'repo>(
334    repo: &'repo gix::Repository,
335    name_or_url: Option<&str>,
336) -> anyhow::Result<gix::Remote<'repo>> {
337    repo.find_fetch_remote(name_or_url.map(Into::into)).map_err(Into::into)
338}