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::{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}