gitoxide_core/repository/
remote.rs1#[cfg(any(feature = "blocking-client", feature = "async-client"))]
2mod refs_impl {
3 use anyhow::bail;
4 use gix::{
5 protocol::handshake,
6 refspec::{RefSpec, match_group::validate::Fix},
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!(
226 "Without refspecs there is nothing to show here. Add refspecs as arguments or configure them in .git/config."
227 )
228 }
229 Ok(())
230 }
231
232 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
233 pub enum JsonRef {
234 Peeled {
235 path: String,
236 tag: String,
237 object: String,
238 },
239 Direct {
240 path: String,
241 object: String,
242 },
243 Unborn {
244 path: String,
245 target: String,
246 },
247 Symbolic {
248 path: String,
249 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
250 tag: Option<String>,
251 target: String,
252 object: String,
253 },
254 }
255
256 impl From<handshake::Ref> for JsonRef {
257 fn from(value: handshake::Ref) -> Self {
258 match value {
259 handshake::Ref::Unborn { full_ref_name, target } => JsonRef::Unborn {
260 path: full_ref_name.to_string(),
261 target: target.to_string(),
262 },
263 handshake::Ref::Direct {
264 full_ref_name: path,
265 object,
266 } => JsonRef::Direct {
267 path: path.to_string(),
268 object: object.to_string(),
269 },
270 handshake::Ref::Symbolic {
271 full_ref_name: path,
272 tag,
273 target,
274 object,
275 } => JsonRef::Symbolic {
276 path: path.to_string(),
277 tag: tag.map(|t| t.to_string()),
278 target: target.to_string(),
279 object: object.to_string(),
280 },
281 handshake::Ref::Peeled {
282 full_ref_name: path,
283 tag,
284 object,
285 } => JsonRef::Peeled {
286 path: path.to_string(),
287 tag: tag.to_string(),
288 object: object.to_string(),
289 },
290 }
291 }
292 }
293
294 pub(crate) fn print_ref(mut out: impl std::io::Write, r: &handshake::Ref) -> std::io::Result<&gix::hash::oid> {
295 match r {
296 handshake::Ref::Direct {
297 full_ref_name: path,
298 object,
299 } => write!(&mut out, "{object} {path}").map(|_| object.as_ref()),
300 handshake::Ref::Peeled {
301 full_ref_name: path,
302 tag,
303 object,
304 } => write!(&mut out, "{tag} {path} object:{object}").map(|_| tag.as_ref()),
305 handshake::Ref::Symbolic {
306 full_ref_name: path,
307 tag,
308 target,
309 object,
310 } => match tag {
311 Some(tag) => {
312 write!(&mut out, "{tag} {path} symref-target:{target} peeled:{object}").map(|_| tag.as_ref())
313 }
314 None => write!(&mut out, "{object} {path} symref-target:{target}").map(|_| object.as_ref()),
315 },
316 handshake::Ref::Unborn { full_ref_name, target } => {
317 static NULL: gix::hash::ObjectId = gix::hash::ObjectId::null(gix::hash::Kind::Sha1);
318 write!(&mut out, "unborn {full_ref_name} symref-target:{target}").map(|_| NULL.as_ref())
319 }
320 }
321 }
322
323 pub(crate) fn print(mut out: impl std::io::Write, refs: &[handshake::Ref]) -> std::io::Result<()> {
324 for r in refs {
325 print_ref(&mut out, r)?;
326 writeln!(out)?;
327 }
328 Ok(())
329 }
330}
331#[cfg(any(feature = "blocking-client", feature = "async-client"))]
332pub use refs_impl::{JsonRef, refs, refs_fn as refs};
333
334#[cfg(any(feature = "blocking-client", feature = "async-client"))]
335pub(crate) fn by_name_or_url<'repo>(
336 repo: &'repo gix::Repository,
337 name_or_url: Option<&str>,
338) -> anyhow::Result<gix::Remote<'repo>> {
339 repo.find_fetch_remote(name_or_url.map(Into::into)).map_err(Into::into)
340}