1pub 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}