Skip to main content

gitoxide_core/repository/
remote.rs

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