1use std::io::{Cursor, Read};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use thiserror::Error;
12
13use crate::config::ConfigSet;
14#[cfg(feature = "http-ureq")]
15use crate::credentials::{
16 askpass_program, Credential, CredentialProvider, HelperCredentialProvider,
17};
18#[cfg(feature = "http-ureq")]
19use crate::environment::Environment;
20use crate::error::Error;
21use crate::fetch::{fetch_remote_into, Progress};
22use crate::objects::ObjectId;
23use crate::odb::Odb;
24use crate::push::push_remote_from;
25use crate::refs::listing::{
26 compare_version_names, patterns_match_ls_remote, RefQueryOptions, RefSort, SortKey,
27};
28use crate::repo::Repository;
29use crate::transfer::{
30 fetch_local_into, push_local_from, FetchOptions, FetchOutcome, PrePushGate, PushOptions,
31 PushOutcome, PushRefSpec,
32};
33use crate::transport::http::{HttpClient, SmartHttpTransport};
34use crate::transport::{ConnectOptions, GitDaemonTransport, Service, SshTransport, Transport};
35use crate::transport_path::{
36 is_local_path_remote_url, resolve_local_remote_git_dir, url_is_local_not_ssh,
37};
38use crate::url_rewrite;
39
40#[cfg(feature = "http-ureq")]
41use crate::transport::http::ureq_client::UreqHttpClient;
42
43#[cfg(feature = "http-ureq")]
45pub mod http_client {
46 pub use crate::transport::http::ureq_client::UreqHttpClient;
47 pub use crate::transport::http::HttpClient;
48}
49
50pub type RemoteResult<T> = std::result::Result<T, RemoteError>;
52
53pub const DEFAULT_REMOTE: &str = "origin";
55
56#[must_use]
58pub(crate) fn is_valid_remote_name(name: &str) -> bool {
59 use crate::check_ref_format::{check_refname_format, RefNameOptions};
60 !name.is_empty()
61 && check_refname_format(
62 &format!("refs/remotes/{name}/test"),
63 &RefNameOptions::default(),
64 )
65 .is_ok()
66}
67
68#[derive(Debug, Error)]
70pub enum RemoteError {
71 #[error("{}", match remote {
73 Some(name) => format!("remote '{name}' has no URL configured"),
74 None => "remote URL is empty".to_owned(),
75 })]
76 NoUrl {
77 remote: Option<String>,
79 },
80 #[error("unsupported remote URL scheme")]
82 UnsupportedScheme,
83 #[error("{0}")]
85 InvalidUrl(String),
86 #[error("HTTP remotes require the http-ureq feature or an HttpClientFactory")]
88 HttpUnavailable,
89 #[error("unknown remote '{0}'")]
91 UnknownRemote(String),
92 #[error("remote '{0}' already exists")]
95 RemoteExists(String),
96 #[error("'{0}' is not a valid remote name")]
98 InvalidRemoteName(String),
99 #[error(transparent)]
101 Library(#[from] Error),
102}
103
104impl RemoteError {
105 #[must_use]
107 pub fn is_remote_unreadable(&self) -> bool {
108 matches!(self, Self::Library(e) if e.is_remote_unreadable())
109 }
110
111 #[must_use]
113 pub fn is_cancelled(&self) -> bool {
114 matches!(self, Self::Library(e) if e.is_cancelled())
115 }
116}
117
118impl From<RemoteError> for Error {
119 fn from(e: RemoteError) -> Self {
120 match e {
121 RemoteError::Library(err) => err,
122 other => Error::Message(other.to_string()),
123 }
124 }
125}
126
127#[derive(Clone, Debug, PartialEq, Eq)]
129pub enum RemoteUrl {
130 Local(PathBuf),
132 File(PathBuf),
134 Git(String),
136 Ssh(String),
138 Http(String),
140 Https(String),
142 Bundle(PathBuf),
144}
145
146impl RemoteUrl {
147 #[must_use]
149 pub fn is_local(&self) -> bool {
150 matches!(self, Self::Local(_) | Self::File(_))
151 }
152
153 #[must_use]
155 pub fn as_wire_url(&self) -> Option<&str> {
156 match self {
157 Self::Git(s) | Self::Ssh(s) | Self::Http(s) | Self::Https(s) => Some(s.as_str()),
158 Self::Local(_) | Self::File(_) | Self::Bundle(_) => None,
159 }
160 }
161
162 #[must_use]
164 pub fn to_url_string(&self) -> String {
165 match self {
166 Self::Local(p) | Self::File(p) | Self::Bundle(p) => p.to_string_lossy().into_owned(),
167 Self::Git(s) | Self::Ssh(s) | Self::Http(s) | Self::Https(s) => s.clone(),
168 }
169 }
170}
171
172impl TryFrom<&str> for RemoteUrl {
173 type Error = RemoteError;
174
175 fn try_from(url: &str) -> std::result::Result<Self, Self::Error> {
176 parse_remote_url(url)
177 }
178}
179
180pub fn parse_remote_url(url: &str) -> RemoteResult<RemoteUrl> {
182 let url = url.trim();
183 if url.is_empty() {
184 return Err(RemoteError::NoUrl { remote: None });
185 }
186 if url.starts_with("file://") {
187 let path = crate::transport_path::file_url_to_local_path(url)
188 .map_err(|e| RemoteError::InvalidUrl(e.to_string()))?;
189 let path = PathBuf::from(path);
190 if crate::bundle_remote::is_bundle_path(&path) {
191 return Ok(RemoteUrl::Bundle(path));
192 }
193 return Ok(RemoteUrl::File(path));
194 }
195 if url.starts_with("git://") {
196 crate::transport::parse_git_url(url).map_err(|e| RemoteError::InvalidUrl(e.to_string()))?;
197 return Ok(RemoteUrl::Git(url.to_owned()));
198 }
199 if url.starts_with("http://") {
200 return Ok(RemoteUrl::Http(url.to_owned()));
201 }
202 if url.starts_with("https://") {
203 return Ok(RemoteUrl::Https(url.to_owned()));
204 }
205 if crate::transport::is_ssh_url(url) {
206 crate::transport::parse_ssh_url(url).map_err(|e| RemoteError::InvalidUrl(e.to_string()))?;
207 return Ok(RemoteUrl::Ssh(url.to_owned()));
208 }
209 if is_local_path_remote_url(url) && url_is_local_not_ssh(url) {
210 let path = PathBuf::from(url);
211 if crate::bundle_remote::is_bundle_path(&path) {
212 return Ok(RemoteUrl::Bundle(path));
213 }
214 return Ok(RemoteUrl::Local(path));
215 }
216 Err(RemoteError::UnsupportedScheme)
217}
218
219#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct RemoteRef {
222 pub name: String,
224 pub oid: ObjectId,
226 pub symref_target: Option<String>,
228}
229
230impl RemoteRef {
231 #[must_use]
234 pub fn peeled_tag(&self) -> Option<&str> {
235 self.name.strip_suffix("^{}")
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct PeeledRemoteRef {
242 pub name: String,
244 pub oid: ObjectId,
246 pub peeled: Option<ObjectId>,
248 pub symref_target: Option<String>,
250}
251
252#[must_use]
256pub fn fold_peeled_refs(refs: &[RemoteRef]) -> Vec<PeeledRemoteRef> {
257 let peeled: std::collections::HashMap<&str, ObjectId> = refs
258 .iter()
259 .filter_map(|r| Some((r.peeled_tag()?, r.oid)))
260 .collect();
261 refs.iter()
262 .filter(|r| r.peeled_tag().is_none())
263 .map(|r| PeeledRemoteRef {
264 name: r.name.clone(),
265 oid: r.oid,
266 peeled: peeled.get(r.name.as_str()).copied(),
267 symref_target: r.symref_target.clone(),
268 })
269 .collect()
270}
271
272#[derive(Debug, Default, Clone)]
274pub struct ListRefsOptions {
275 pub prefixes: Vec<String>,
277 pub heads: bool,
279 pub tags: bool,
281 pub symrefs: bool,
283 pub peel: bool,
285 pub protocol_version: Option<u8>,
287 pub sort: RefSort,
289 pub limit: Option<usize>,
291 pub versionsort_suffixes: Vec<String>,
293}
294
295pub trait HttpClientFactory: Send + Sync {
297 fn create(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>>;
303
304 fn create_for_probe(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>>;
319}
320
321pub(crate) struct ProbeHttpClientFactory<'a> {
324 inner: Option<&'a dyn HttpClientFactory>,
325 #[cfg(feature = "http-ureq")]
326 default: DefaultHttpClientFactory,
327}
328
329impl<'a> ProbeHttpClientFactory<'a> {
330 pub(crate) fn new(inner: Option<&'a dyn HttpClientFactory>, repo: &Repository) -> Self {
332 #[cfg(not(feature = "http-ureq"))]
333 let _ = repo;
334 Self {
335 inner,
336 #[cfg(feature = "http-ureq")]
337 default: DefaultHttpClientFactory::with_environment(repo.environment().clone()),
338 }
339 }
340}
341
342impl HttpClientFactory for ProbeHttpClientFactory<'_> {
343 fn create(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>> {
344 if let Some(inner) = self.inner {
345 return inner.create_for_probe(config);
346 }
347 #[cfg(feature = "http-ureq")]
348 {
349 self.default.create_for_probe(config)
350 }
351 #[cfg(not(feature = "http-ureq"))]
352 {
353 let _ = config;
354 Err(RemoteError::HttpUnavailable)
355 }
356 }
357
358 fn create_for_probe(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>> {
359 self.create(config)
360 }
361}
362
363#[cfg(feature = "http-ureq")]
365struct KeepRejectedCredentials<P>(P);
366
367#[cfg(feature = "http-ureq")]
368impl<P: CredentialProvider> CredentialProvider for KeepRejectedCredentials<P> {
369 fn fill(&self, input: &Credential) -> crate::error::Result<Credential> {
370 self.0.fill(input)
371 }
372
373 fn approve(&self, cred: &Credential) -> crate::error::Result<()> {
374 self.0.approve(cred)
375 }
376
377 fn reject(&self, _cred: &Credential) -> crate::error::Result<()> {
378 Ok(())
379 }
380}
381
382#[cfg(feature = "http-ureq")]
390#[derive(Debug, Default, Clone)]
391pub struct DefaultHttpClientFactory {
392 environment: Option<Environment>,
393}
394
395#[cfg(feature = "http-ureq")]
396impl DefaultHttpClientFactory {
397 #[must_use]
400 pub const fn new() -> Self {
401 Self { environment: None }
402 }
403
404 #[must_use]
407 pub fn with_environment(environment: Environment) -> Self {
408 Self {
409 environment: Some(environment),
410 }
411 }
412
413 fn credential_provider(&self, config: &ConfigSet) -> HelperCredentialProvider {
414 let askpass = match &self.environment {
415 Some(env) => askpass_program(env, config),
416 None => askpass_program(&Environment::empty(), config),
417 };
418 HelperCredentialProvider::new(config.clone()).with_askpass(askpass)
419 }
420
421 fn client(
422 config: &ConfigSet,
423 provider: Box<dyn CredentialProvider + Send + Sync>,
424 ) -> RemoteResult<Box<dyn HttpClient>> {
425 let client = UreqHttpClient::from_config(config)
426 .map_err(|e| RemoteError::InvalidUrl(e.to_string()))?
427 .with_credential_provider(provider);
428 Ok(Box::new(client))
429 }
430}
431
432#[cfg(feature = "http-ureq")]
433impl HttpClientFactory for DefaultHttpClientFactory {
434 fn create(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>> {
435 Self::client(config, Box::new(self.credential_provider(config)))
436 }
437
438 fn create_for_probe(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>> {
439 Self::client(
440 config,
441 Box::new(KeepRejectedCredentials(self.credential_provider(config))),
442 )
443 }
444}
445
446#[derive(Clone, Debug)]
448pub struct Remote {
449 pub name: Option<String>,
451 fetch_url: RemoteUrl,
452 push_url: Option<RemoteUrl>,
453 pub fetch_refspecs: Vec<String>,
455}
456
457impl Remote {
458 pub fn from_config(config: &ConfigSet, name: &str) -> RemoteResult<Self> {
466 let raw_url = config
467 .get(&format!("remote.{name}.url"))
468 .filter(|u| !u.trim().is_empty())
469 .ok_or_else(|| RemoteError::NoUrl {
470 remote: Some(name.to_owned()),
471 })?;
472 let fetch_url = url_rewrite::rewrite_fetch_url(config, &raw_url);
473 let push_source = config
474 .get(&format!("remote.{name}.pushurl"))
475 .filter(|u| !u.trim().is_empty())
476 .unwrap_or_else(|| raw_url.clone());
477 let push_url = url_rewrite::rewrite_push_url(config, push_source.as_str());
478 let mut fetch_refspecs = config.get_all(&format!("remote.{name}.fetch"));
479 fetch_refspecs.retain(|s| !s.trim().is_empty());
480 if fetch_refspecs.is_empty() {
481 fetch_refspecs = vec![format!("+refs/heads/*:refs/remotes/{name}/*")];
482 }
483 Ok(Self {
484 name: Some(name.to_owned()),
485 fetch_url: RemoteUrl::try_from(fetch_url.as_str())?,
486 push_url: Some(RemoteUrl::try_from(push_url.as_str())?),
487 fetch_refspecs,
488 })
489 }
490
491 pub const ANONYMOUS_FETCH_REFSPEC: &'static str = "refs/heads/*:";
494
495 pub fn from_url(url: &str) -> RemoteResult<Self> {
504 Ok(Self {
505 name: None,
506 fetch_url: RemoteUrl::try_from(url)?,
507 push_url: None,
508 fetch_refspecs: vec![Self::ANONYMOUS_FETCH_REFSPEC.to_owned()],
509 })
510 }
511
512 #[must_use]
514 pub fn fetch_url(&self) -> &RemoteUrl {
515 &self.fetch_url
516 }
517
518 #[must_use]
520 pub fn push_url(&self) -> &RemoteUrl {
521 self.push_url.as_ref().unwrap_or(&self.fetch_url)
522 }
523
524 pub fn fetch(
535 &self,
536 repo: &Repository,
537 mut opts: FetchOptions,
538 progress: &mut dyn Progress,
539 cancel: &dyn crate::progress::Cancel,
540 http_factory: Option<&dyn HttpClientFactory>,
541 ) -> RemoteResult<FetchOutcome> {
542 if opts.configured_refspecs.is_empty() {
543 opts.configured_refspecs = self.fetch_refspecs.clone();
544 }
545 if opts.refspecs.is_empty() {
546 opts.refspecs = self.fetch_refspecs.clone();
547 }
548 if opts.remote_name.is_none() {
549 opts.remote_name = self.name.clone();
550 }
551 if opts.fetch_source.is_none() {
552 opts.fetch_source = Some(self.fetch_url.to_url_string());
553 }
554 if opts.diagnostics.is_none() {
555 opts.diagnostics = Some(repo.diagnostics());
556 opts.network_trace = repo.network_trace_enabled();
557 }
558 let connect = ConnectOptions {
559 protocol_version: 2,
560 diagnostics: opts.diagnostics.clone(),
561 network_trace: opts.network_trace,
562 ..Default::default()
563 };
564 self.dispatch_fetch(
565 repo,
566 &self.fetch_url,
567 &mut opts,
568 progress,
569 cancel,
570 http_factory,
571 connect,
572 )
573 }
574
575 pub fn push(
591 &self,
592 repo: &Repository,
593 refs: &[PushRefSpec],
594 mut opts: PushOptions,
595 progress: &mut dyn Progress,
596 cancel: &dyn crate::progress::Cancel,
597 http_factory: Option<&dyn HttpClientFactory>,
598 ) -> RemoteResult<PushOutcome> {
599 if opts.tracking_remote.is_none() {
600 opts.tracking_remote = self.name.clone();
601 }
602 if opts.resumable_threshold.is_none() {
603 opts.resumable_threshold = repo
604 .config()
605 .ok()
606 .and_then(|c| c.get_i64("push.bundleThreshold").and_then(|r| r.ok()))
607 .and_then(|v| u64::try_from(v).ok());
608 }
609 let url = pre_push_hook_url(self.push_url());
610 let gate = PrePushGate {
611 repo,
612 remote: self.name.as_deref().unwrap_or(&url),
613 url: &url,
614 };
615 self.dispatch_push(
616 repo,
617 self.push_url(),
618 refs,
619 &opts,
620 (!opts.no_verify).then_some(&gate),
621 progress,
622 cancel,
623 http_factory,
624 ConnectOptions {
625 protocol_version: 0,
626 diagnostics: Some(repo.diagnostics()),
627 network_trace: repo.network_trace_enabled(),
628 ..Default::default()
629 },
630 )
631 }
632
633 #[must_use]
652 pub fn seed_from_bundle_uri(
653 &self,
654 repo: &Repository,
655 cache_dir: &Path,
656 progress: &mut dyn Progress,
657 cancel: &dyn crate::progress::Cancel,
658 http_factory: Option<&dyn HttpClientFactory>,
659 ) -> crate::bundle_uri::BundleSeed {
660 use crate::bundle_uri::BundleSeed;
661 let (RemoteUrl::Http(url) | RemoteUrl::Https(url)) = &self.fetch_url else {
662 return BundleSeed::NotOffered;
663 };
664 let enabled = repo
665 .config()
666 .ok()
667 .and_then(|c| c.get_bool("transfer.bundleURI").and_then(|r| r.ok()))
668 .unwrap_or(true);
669 if !enabled {
670 return BundleSeed::NotOffered;
671 }
672 let probe = ProbeHttpClientFactory::new(http_factory, repo);
673 let client = match http_client(Some(&probe), repo) {
674 Ok(c) => c,
675 Err(e) => {
676 return BundleSeed::Failed {
677 uri: None,
678 reason: e.to_string(),
679 }
680 }
681 };
682 crate::bundle_uri::seed_from_bundle_uri(
683 repo,
684 client.as_ref(),
685 url,
686 cache_dir,
687 progress,
688 cancel,
689 )
690 }
691
692 pub fn list_refs(
700 &self,
701 repo: Option<&Repository>,
702 opts: &ListRefsOptions,
703 http_factory: Option<&dyn HttpClientFactory>,
704 ) -> RemoteResult<Vec<RemoteRef>> {
705 let (git_dir, work_tree, odb) = repo_context(repo);
706 match &self.fetch_url {
707 RemoteUrl::Bundle(path) => list_refs_from_bundle(path, opts),
708 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
709 let remote_git = local_git_dir_from_url(&self.fetch_url, git_dir, work_tree)?;
710 list_refs_from_git_dir(&remote_git, None, opts)
711 }
712 wire => self.list_refs_wire(wire, git_dir, odb.as_ref(), opts, http_factory, repo),
713 }
714 }
715
716 pub fn list_push_refs(
727 &self,
728 repo: &Repository,
729 http_factory: Option<&dyn HttpClientFactory>,
730 ) -> RemoteResult<Vec<RemoteRef>> {
731 let url = self.push_url();
732 match url {
733 RemoteUrl::Bundle(_) => Err(RemoteError::UnsupportedScheme),
734 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
735 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
736 let refs =
737 crate::refs::list_refs(&remote_git, "refs/").map_err(RemoteError::Library)?;
738 Ok(refs
739 .into_iter()
740 .map(|(name, oid)| RemoteRef {
741 name,
742 oid,
743 symref_target: None,
744 })
745 .collect())
746 }
747 wire => self.list_refs_wire(
748 wire,
749 repo.git_dir(),
750 Some(repo.odb()),
751 &ListRefsOptions::default(),
752 http_factory,
753 Some(repo),
754 ),
755 }
756 }
757
758 #[allow(clippy::too_many_arguments)]
759 fn dispatch_fetch(
760 &self,
761 repo: &Repository,
762 url: &RemoteUrl,
763 opts: &mut FetchOptions,
764 progress: &mut dyn Progress,
765 cancel: &dyn crate::progress::Cancel,
766 http_factory: Option<&dyn HttpClientFactory>,
767 connect: ConnectOptions,
768 ) -> RemoteResult<FetchOutcome> {
769 match url {
770 RemoteUrl::Bundle(path) => Ok(crate::bundle_remote::fetch_from_bundle_into(
771 repo, path, opts, progress, cancel,
772 )?),
773 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
774 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
775 let cfg = ConfigSet::load(&opts.environment, Some(repo.git_dir()), true)
776 .map_err(RemoteError::Library)?;
777 if opts.filter.is_none() {
778 if let Some(name) = opts.remote_name.as_deref().or(self.name.as_deref()) {
779 opts.filter = crate::partial_clone::stored_remote_filter(&cfg, name);
780 }
781 }
782 if opts.auto_filter
788 || crate::partial_clone::fetch_requires_upload_pack(
789 opts.filter.as_ref(),
790 &cfg,
791 opts.remote_name.as_deref().or(self.name.as_deref()),
792 )
793 || local_source_has_promisor_remotes(&remote_git)
794 {
795 let client_env = if opts.environment.is_fetch_options_default() {
796 repo.environment()
797 } else {
798 &opts.environment
799 };
800 let mut conn = crate::transport::connect_local_upload_pack(
801 &remote_git,
802 Service::UploadPack,
803 &connect,
804 client_env,
805 repo.caches().promisor_fetcher(),
806 )
807 .map_err(RemoteError::Library)?;
808 let fetched = fetch_remote_into(repo, &mut *conn, opts, progress, cancel);
809 conn.finish().map_err(RemoteError::Library)?;
813 return Ok(fetched?);
814 }
815 Ok(fetch_local_into(repo, &remote_git, opts, progress, cancel)?)
816 }
817 RemoteUrl::Http(u) | RemoteUrl::Https(u) => {
818 let client: Arc<dyn HttpClient> = Arc::from(http_client(http_factory, repo)?);
819 let transport = SmartHttpTransport::new(client);
820 let mut conn = transport.connect(u, Service::UploadPack, &connect)?;
821 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
822 }
823 RemoteUrl::Git(u) => {
824 let mut conn =
825 GitDaemonTransport::new().connect(u, Service::UploadPack, &connect)?;
826 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
827 }
828 RemoteUrl::Ssh(u) => {
829 let transport = ssh_transport(Some(repo), Some(&opts.environment));
830 let mut conn = transport.connect(u, Service::UploadPack, &connect)?;
831 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
832 }
833 }
834 }
835
836 #[allow(clippy::too_many_arguments)]
837 fn dispatch_push(
838 &self,
839 repo: &Repository,
840 url: &RemoteUrl,
841 refs: &[PushRefSpec],
842 opts: &PushOptions,
843 pre_push: Option<&PrePushGate<'_>>,
844 progress: &mut dyn Progress,
845 cancel: &dyn crate::progress::Cancel,
846 http_factory: Option<&dyn HttpClientFactory>,
847 connect: ConnectOptions,
848 ) -> RemoteResult<PushOutcome> {
849 let mut over_wire = |conn: &mut dyn crate::transport::Connection| {
850 push_remote_from(repo, conn, refs, opts, progress, cancel, pre_push)
851 };
852 match url {
853 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
854 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
855 Ok(push_local_from(
856 repo,
857 &remote_git,
858 refs,
859 opts,
860 progress,
861 cancel,
862 pre_push,
863 )?)
864 }
865 RemoteUrl::Http(u) | RemoteUrl::Https(u) => {
866 let client: Arc<dyn HttpClient> = Arc::from(http_client(http_factory, repo)?);
867 let transport = SmartHttpTransport::new(client.clone());
868 let mut conn = transport.connect(u, Service::ReceivePack, &connect)?;
869 Ok(crate::push::push_remote_with(
870 repo,
871 &mut *conn,
872 refs,
873 opts,
874 progress,
875 cancel,
876 Some(crate::push::BundlePushTarget {
877 client: client.as_ref(),
878 repo_url: u,
879 }),
880 pre_push,
881 )?)
882 }
883 RemoteUrl::Git(u) => {
884 let mut conn =
885 GitDaemonTransport::new().connect(u, Service::ReceivePack, &connect)?;
886 Ok(over_wire(&mut *conn)?)
887 }
888 RemoteUrl::Ssh(u) => {
889 let transport = ssh_transport(Some(repo), None);
890 let mut conn = transport.connect(u, Service::ReceivePack, &connect)?;
891 Ok(over_wire(&mut *conn)?)
892 }
893 RemoteUrl::Bundle(_) => Err(RemoteError::UnsupportedScheme),
894 }
895 }
896
897 fn list_refs_wire(
898 &self,
899 url: &RemoteUrl,
900 local_git_dir: &Path,
901 odb: Option<&Odb>,
902 opts: &ListRefsOptions,
903 http_factory: Option<&dyn HttpClientFactory>,
904 repo: Option<&Repository>,
905 ) -> RemoteResult<Vec<RemoteRef>> {
906 let wire = url.as_wire_url().ok_or(RemoteError::UnsupportedScheme)?;
907 let requested = opts.protocol_version.unwrap_or(2);
908 let connect_opts = ConnectOptions {
909 protocol_version: requested,
910 ..Default::default()
911 };
912 match url {
913 RemoteUrl::Http(_) | RemoteUrl::Https(_) => {
914 list_refs_http(wire, local_git_dir, odb, opts, http_factory, repo)
915 }
916 RemoteUrl::Git(_) => {
917 let mut conn =
918 GitDaemonTransport::new().connect(wire, Service::UploadPack, &connect_opts)?;
919 list_refs_from_connection(&mut *conn, local_git_dir, odb, opts)
920 }
921 RemoteUrl::Ssh(_) => {
922 let transport = ssh_transport(repo, None);
923 let mut conn = transport.connect(wire, Service::UploadPack, &connect_opts)?;
924 list_refs_from_connection(&mut *conn, local_git_dir, odb, opts)
925 }
926 RemoteUrl::Local(_) | RemoteUrl::File(_) | RemoteUrl::Bundle(_) => unreachable!(),
927 }
928 }
929}
930
931fn pre_push_hook_url(url: &RemoteUrl) -> String {
933 match url {
934 RemoteUrl::File(path) => format!("file://{}", path.to_string_lossy()),
935 other => other.to_url_string(),
936 }
937}
938
939fn repo_context(repo: Option<&Repository>) -> (&Path, Option<&Path>, Option<Odb>) {
940 match repo {
941 Some(r) => (r.git_dir(), r.work_tree(), Some(r.odb().clone())),
942 None => (Path::new("."), None, None),
943 }
944}
945
946fn local_source_has_promisor_remotes(git_dir: &Path) -> bool {
949 ConfigSet::load_repo_local_only(git_dir)
950 .is_ok_and(|cfg| crate::promisor::repo_treats_promisor_packs(git_dir, &cfg))
951}
952
953fn local_git_dir_from_url(
954 url: &RemoteUrl,
955 git_dir: &Path,
956 work_tree: Option<&Path>,
957) -> RemoteResult<PathBuf> {
958 let s = url.to_url_string();
959 let remote_git = resolve_local_remote_git_dir(&s, git_dir, work_tree);
960 crate::transfer::ensure_local_remote_repository(&remote_git)?;
961 Ok(remote_git)
962}
963
964fn ssh_transport(
965 repo: Option<&Repository>,
966 fetch_env: Option<&crate::environment::Environment>,
967) -> SshTransport {
968 let mut transport = SshTransport::new();
969 if let Some(r) = repo {
970 transport = transport.with_command_runner(r.command_runner());
971 }
972 let env = ssh_wire_environment(repo, fetch_env);
973 transport = transport.with_ssh_environment(std::sync::Arc::new(env.clone()));
974 if let Some(r) = repo {
975 if let Ok(config) = r.config() {
976 transport = transport.with_ssh_config(config);
977 }
978 } else if let Ok(config) = ConfigSet::load(&env, None, true) {
979 transport = transport.with_ssh_config(Arc::new(config));
980 }
981 transport
982}
983
984fn ssh_wire_environment(
987 repo: Option<&Repository>,
988 fetch_env: Option<&crate::environment::Environment>,
989) -> crate::environment::Environment {
990 if let Some(fe) = fetch_env {
991 if !fe.is_fetch_options_default() {
992 return fe.clone();
993 }
994 }
995 repo.map(|r| r.environment().clone())
996 .unwrap_or_else(crate::environment::Environment::capture_process)
997}
998
999fn http_client(
1000 factory: Option<&dyn HttpClientFactory>,
1001 repo: &Repository,
1002) -> RemoteResult<Box<dyn HttpClient>> {
1003 let config = repo.config().map_err(RemoteError::Library)?;
1004 if let Some(f) = factory {
1005 return f.create(config.as_ref());
1006 }
1007 #[cfg(feature = "http-ureq")]
1008 {
1009 DefaultHttpClientFactory::with_environment(repo.environment().clone())
1010 .create(config.as_ref())
1011 }
1012 #[cfg(not(feature = "http-ureq"))]
1013 {
1014 let _ = config;
1015 Err(RemoteError::HttpUnavailable)
1016 }
1017}
1018
1019fn list_refs_http(
1020 repo_url: &str,
1021 local_git_dir: &Path,
1022 odb: Option<&Odb>,
1023 opts: &ListRefsOptions,
1024 http_factory: Option<&dyn HttpClientFactory>,
1025 repo: Option<&Repository>,
1026) -> RemoteResult<Vec<RemoteRef>> {
1027 let config: Arc<ConfigSet> = if let Some(r) = repo {
1028 r.config().map_err(RemoteError::Library)?
1029 } else {
1030 Arc::new(ConfigSet::new())
1031 };
1032 let client: Box<dyn HttpClient> = match http_factory {
1033 Some(f) => f.create(config.as_ref())?,
1034 None => {
1035 #[cfg(feature = "http-ureq")]
1036 {
1037 repo.map_or_else(DefaultHttpClientFactory::default, |r| {
1038 DefaultHttpClientFactory::with_environment(r.environment().clone())
1039 })
1040 .create(config.as_ref())?
1041 }
1042 #[cfg(not(feature = "http-ureq"))]
1043 {
1044 return Err(RemoteError::HttpUnavailable);
1045 }
1046 }
1047 };
1048
1049 let protocol_header_value = opts
1052 .protocol_version
1053 .and_then(crate::protocol::client_git_protocol_header_value);
1054 let git_protocol: Option<&str> = match opts.protocol_version {
1055 None => None,
1056 Some(0) => Some(""),
1057 Some(_) => protocol_header_value.as_deref(),
1058 };
1059
1060 let info_url = crate::transport::http::smart_info_refs_discovery_url(repo_url);
1061 let want_v2 = opts.protocol_version.unwrap_or(2) >= 2;
1062 let (body, final_url) = client
1063 .get_with_final_url_exact(&info_url, git_protocol)
1064 .map_err(RemoteError::Library)?;
1065 let effective_base =
1066 crate::transport::http::rebased_base_from_redirect(repo_url, final_url.as_deref())
1067 .unwrap_or_else(|| repo_url.to_owned());
1068 let (protocol_version, refs, caps, head_symref) =
1069 if effective_base.trim_end_matches('/') != repo_url.trim_end_matches('/') {
1070 client.reset_auth_after_redirect_rebase();
1071 crate::transport::http::discover_upload_pack_for_list_refs(
1072 client.as_ref(),
1073 &effective_base,
1074 git_protocol,
1075 )
1076 .map_err(RemoteError::Library)?
1077 } else {
1078 crate::transport::http::discover_upload_pack_for_list_refs_from_body(&body)
1079 .map_err(RemoteError::Library)?
1080 };
1081
1082 if protocol_version >= 2 && want_v2 {
1083 let local_odb = odb.cloned().unwrap_or_else(|| open_odb(local_git_dir));
1084 let req = build_list_refs_v2_request(&caps, &local_odb, opts)?;
1085 let post_url = format!("{}/git-upload-pack", effective_base.trim_end_matches('/'));
1086 let body = client
1087 .as_ref()
1088 .post(
1089 &post_url,
1090 "application/x-git-upload-pack-request",
1091 "application/x-git-upload-pack-result",
1092 &req,
1093 git_protocol,
1094 )
1095 .map_err(RemoteError::Library)?;
1096 let mut cur = Cursor::new(body);
1097 parse_list_refs_v2_response(&mut cur, opts, head_symref)
1098 } else {
1099 list_refs_v0_advertisement(&refs, head_symref.as_deref(), odb, opts)
1100 }
1101}
1102
1103fn list_refs_from_connection(
1104 conn: &mut dyn crate::transport::Connection,
1105 local_git_dir: &Path,
1106 odb: Option<&Odb>,
1107 opts: &ListRefsOptions,
1108) -> RemoteResult<Vec<RemoteRef>> {
1109 let want_v2 = opts.protocol_version.unwrap_or(2) >= 2;
1110 if conn.protocol_version() >= 2 && want_v2 {
1111 list_refs_v2_connection(conn, local_git_dir, odb, opts)
1112 } else {
1113 let out =
1114 list_refs_v0_advertisement(conn.advertised_refs(), conn.head_symref(), odb, opts)?;
1115 crate::transport::send_client_flush(conn).map_err(RemoteError::Library)?;
1116 Ok(out)
1117 }
1118}
1119
1120fn open_odb(git_dir: &Path) -> Odb {
1121 crate::transfer::open_odb(git_dir)
1122}
1123
1124fn list_refs_v2_connection(
1125 conn: &mut dyn crate::transport::Connection,
1126 local_git_dir: &Path,
1127 odb: Option<&Odb>,
1128 opts: &ListRefsOptions,
1129) -> RemoteResult<Vec<RemoteRef>> {
1130 let local_odb = odb.cloned().unwrap_or_else(|| open_odb(local_git_dir));
1131 let req = build_list_refs_v2_request(conn.capabilities(), &local_odb, opts)?;
1132 conn.writer().write_all(&req).map_err(Error::Io)?;
1133 conn.writer().flush().map_err(Error::Io)?;
1134 let discovery_head = conn.head_symref().map(str::to_owned);
1135 let out = parse_list_refs_v2_response(conn.reader(), opts, discovery_head)?;
1136 conn.finish_send();
1137 Ok(out)
1138}
1139
1140fn build_list_refs_v2_request(
1141 server_caps: &[String],
1142 local_odb: &Odb,
1143 opts: &ListRefsOptions,
1144) -> RemoteResult<Vec<u8>> {
1145 use crate::fetch::v2_object_format;
1146 use crate::pkt_line;
1147 use crate::protocol_v2;
1148
1149 let object_format = v2_object_format(server_caps, local_odb);
1150 let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
1151 let mut req: Vec<u8> = Vec::new();
1152 pkt_line::write_line(&mut req, "command=ls-refs").map_err(Error::Io)?;
1153 if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
1154 for line in &cap_echo {
1155 pkt_line::write_line(&mut req, line).map_err(Error::Io)?;
1156 }
1157 } else {
1158 for line in &cap_echo {
1159 pkt_line::write_line(&mut req, line).map_err(Error::Io)?;
1160 }
1161 pkt_line::write_line(&mut req, &format!("object-format={object_format}"))
1162 .map_err(Error::Io)?;
1163 }
1164 pkt_line::write_delim(&mut req).map_err(Error::Io)?;
1165 if opts.symrefs {
1166 pkt_line::write_line(&mut req, "symrefs").map_err(Error::Io)?;
1167 if crate::protocol_v2::server_advertises_ls_refs_unborn(server_caps) {
1168 pkt_line::write_line(&mut req, "unborn").map_err(Error::Io)?;
1169 }
1170 }
1171 if opts.peel {
1172 pkt_line::write_line(&mut req, "peel").map_err(Error::Io)?;
1173 }
1174 if list_refs_includes_head(opts) {
1175 pkt_line::write_line(&mut req, "ref-prefix HEAD").map_err(Error::Io)?;
1176 }
1177 for prefix in list_ref_prefixes(opts) {
1178 pkt_line::write_line(&mut req, &format!("ref-prefix {prefix}")).map_err(Error::Io)?;
1179 }
1180 pkt_line::write_flush(&mut req).map_err(Error::Io)?;
1181 Ok(req)
1182}
1183
1184fn list_refs_includes_head(opts: &ListRefsOptions) -> bool {
1185 !opts.heads && !opts.tags && opts.prefixes.is_empty()
1186}
1187
1188fn list_ref_prefixes(opts: &ListRefsOptions) -> Vec<String> {
1189 if !opts.prefixes.is_empty() {
1190 if opts.heads && !opts.tags {
1191 return vec!["refs/heads/".to_owned()];
1192 }
1193 if opts.tags && !opts.heads {
1194 return vec!["refs/tags/".to_owned()];
1195 }
1196 return vec!["refs/heads/".to_owned(), "refs/tags/".to_owned()];
1197 }
1198 let mut out = Vec::new();
1199 let all = !opts.heads && !opts.tags;
1200 if all || opts.heads {
1201 out.push("refs/heads/".to_owned());
1202 }
1203 if all || opts.tags {
1204 out.push("refs/tags/".to_owned());
1205 }
1206 out
1207}
1208
1209fn parse_list_refs_v2_response(
1210 reader: &mut dyn Read,
1211 opts: &ListRefsOptions,
1212 mut head_symref: Option<String>,
1213) -> RemoteResult<Vec<RemoteRef>> {
1214 use crate::fetch::parse_ls_refs_v2_line;
1215 use crate::pkt_line;
1216
1217 let mut entries: Vec<RemoteRef> = Vec::new();
1218 let mut peel_map: std::collections::HashMap<String, ObjectId> =
1219 std::collections::HashMap::new();
1220 let mut reader = reader;
1221 loop {
1222 match pkt_line::read_packet(&mut reader).map_err(Error::Io)? {
1223 None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => break,
1224 Some(pkt_line::Packet::ResponseEnd) => break,
1225 Some(pkt_line::Packet::Data(line)) => {
1226 let line = line.trim_end_matches('\n');
1227 if let Some(msg) = line.strip_prefix("ERR ") {
1228 return Err(RemoteError::Library(Error::Message(format!(
1229 "remote error: {}",
1230 msg.trim_end()
1231 ))));
1232 }
1233 if let Some(t) = crate::fetch::parse_v2_ls_refs_unborn_head_symref(line) {
1234 head_symref = Some(t);
1235 continue;
1236 }
1237 let Some((name, oid, symref_target, peel)) = parse_ls_refs_v2_line(line) else {
1238 continue;
1239 };
1240 if name.ends_with("^{}") {
1241 continue;
1242 }
1243 if name == "HEAD" {
1244 let sym =
1245 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t));
1246 if let Some(t) = sym.clone() {
1247 head_symref = Some(t);
1248 }
1249 if list_refs_includes_head(opts) && ref_matches_list_opts("HEAD", opts) {
1250 entries.push(RemoteRef {
1251 name: "HEAD".to_owned(),
1252 oid,
1253 symref_target: if opts.symrefs { sym } else { None },
1254 });
1255 }
1256 continue;
1257 }
1258 if !crate::refs::is_valid_fetch_advertised_ref(&name) {
1259 continue;
1260 }
1261 if let Some(p) = peel {
1262 peel_map.insert(name.clone(), p);
1263 }
1264 let sym =
1265 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t));
1266 entries.push(RemoteRef {
1267 name,
1268 oid,
1269 symref_target: sym,
1270 });
1271 }
1272 }
1273 }
1274 finalize_list_refs_output(entries, peel_map, opts, head_symref, None)
1275}
1276
1277fn list_refs_v0_advertisement(
1278 advertised: &[(String, ObjectId)],
1279 head_symref: Option<&str>,
1280 odb: Option<&Odb>,
1281 opts: &ListRefsOptions,
1282) -> RemoteResult<Vec<RemoteRef>> {
1283 let mut entries: Vec<RemoteRef> = Vec::new();
1284 let mut peel_map: std::collections::HashMap<String, ObjectId> =
1285 std::collections::HashMap::new();
1286 for (name, oid) in advertised {
1287 if name.ends_with("^{}") {
1288 if let Some(base) = name.strip_suffix("^{}") {
1289 peel_map.insert(base.to_owned(), *oid);
1290 }
1291 continue;
1292 }
1293 entries.push(RemoteRef {
1294 name: name.clone(),
1295 oid: *oid,
1296 symref_target: None,
1297 });
1298 }
1299 finalize_list_refs_output(entries, peel_map, opts, head_symref.map(str::to_owned), odb)
1300}
1301
1302fn finalize_list_refs_output(
1303 mut entries: Vec<RemoteRef>,
1304 peel_map: std::collections::HashMap<String, ObjectId>,
1305 opts: &ListRefsOptions,
1306 head_symref: Option<String>,
1307 odb: Option<&Odb>,
1308) -> RemoteResult<Vec<RemoteRef>> {
1309 entries.retain(|e| remote_ref_matches_namespace(&e.name, opts));
1310 entries.retain(|e| patterns_match_ls_remote(&e.name, &opts.prefixes));
1311 if opts.peel {
1312 let mut peel_lines: Vec<RemoteRef> = Vec::new();
1313 for e in &entries {
1314 if !e.name.starts_with("refs/tags/") {
1315 continue;
1316 }
1317 let peeled = peel_map
1318 .get(&e.name)
1319 .copied()
1320 .or_else(|| odb.and_then(|o| o.peel_tag_target(e.oid).ok().flatten()));
1321 if let Some(p) = peeled {
1322 let peel_name = format!("{}^{{}}", e.name);
1323 if should_emit_peel_line(&peel_name, &opts.prefixes) {
1324 peel_lines.push(RemoteRef {
1325 name: peel_name,
1326 oid: p,
1327 symref_target: None,
1328 });
1329 }
1330 }
1331 }
1332 entries.extend(peel_lines);
1333 }
1334 if list_refs_includes_head(opts) {
1335 if let Some(sym) = head_symref {
1336 if let Some(oid) = entries.iter().find(|e| e.name == sym).map(|e| e.oid) {
1337 entries.push(RemoteRef {
1338 name: "HEAD".to_owned(),
1339 oid,
1340 symref_target: if opts.symrefs { Some(sym) } else { None },
1341 });
1342 }
1343 }
1344 }
1345 let query_options = RefQueryOptions {
1346 versionsort_suffixes: opts.versionsort_suffixes.clone(),
1347 ..RefQueryOptions::default()
1348 };
1349 sort_remote_refs(
1350 &mut entries,
1351 &opts.sort,
1352 &query_options.versionsort_suffixes,
1353 );
1354 if let Some(limit) = opts.limit {
1355 entries.truncate(limit);
1356 }
1357 Ok(entries)
1358}
1359
1360#[must_use]
1368pub fn query_remote_refs_in_memory(
1369 mut entries: Vec<RemoteRef>,
1370 opts: &ListRefsOptions,
1371 query_options: &RefQueryOptions,
1372) -> Vec<RemoteRef> {
1373 entries.retain(|e| remote_ref_matches_namespace(&e.name, opts));
1374 entries.retain(|e| patterns_match_ls_remote(&e.name, &opts.prefixes));
1375 sort_remote_refs(
1376 &mut entries,
1377 &opts.sort,
1378 &query_options.versionsort_suffixes,
1379 );
1380 if let Some(limit) = opts.limit {
1381 entries.truncate(limit);
1382 }
1383 entries
1384}
1385
1386fn should_emit_peel_line(peel_name: &str, patterns: &[String]) -> bool {
1387 if patterns.is_empty() {
1388 return true;
1389 }
1390 patterns_match_ls_remote(peel_name, patterns)
1391}
1392
1393fn sort_remote_refs(entries: &mut [RemoteRef], sort: &RefSort, versionsort_suffixes: &[String]) {
1394 entries.sort_by(|a, b| {
1395 let primary = match sort.key {
1396 SortKey::Name => a.name.cmp(&b.name),
1397 SortKey::Version => compare_version_names(&a.name, &b.name, versionsort_suffixes),
1398 SortKey::Date => a.name.cmp(&b.name),
1399 };
1400 let primary = if sort.reverse {
1401 primary.reverse()
1402 } else {
1403 primary
1404 };
1405 if primary != std::cmp::Ordering::Equal {
1406 return primary;
1407 }
1408 a.name.cmp(&b.name)
1409 });
1410}
1411
1412fn remote_ref_matches_namespace(refname: &str, opts: &ListRefsOptions) -> bool {
1413 if opts.heads || opts.tags {
1414 let is_branch = opts.heads && refname.starts_with("refs/heads/");
1415 let is_tag = opts.tags && refname.starts_with("refs/tags/");
1416 if refname == "HEAD" {
1417 return false;
1418 }
1419 if refname.ends_with("^{}") {
1420 return is_tag;
1421 }
1422 if !is_branch && !is_tag {
1423 return false;
1424 }
1425 }
1426 true
1427}
1428
1429fn ref_matches_list_opts(refname: &str, opts: &ListRefsOptions) -> bool {
1430 remote_ref_matches_namespace(refname, opts) && patterns_match_ls_remote(refname, &opts.prefixes)
1431}
1432
1433#[must_use]
1435pub fn ref_matches_ls_remote_patterns(refname: &str, patterns: &[String]) -> bool {
1436 patterns_match_ls_remote(refname, patterns)
1437}
1438
1439fn list_refs_from_bundle(path: &Path, opts: &ListRefsOptions) -> RemoteResult<Vec<RemoteRef>> {
1443 let bundle = crate::bundle::Bundle::open(path)
1444 .map_err(|e| RemoteError::Library(Error::Message(e.to_string())))?;
1445 let mut out = Vec::new();
1446 for (name, oid) in &bundle.header().refs {
1447 if !ref_matches_list_opts(name, opts) {
1448 continue;
1449 }
1450 if name != "HEAD" && !crate::refs::is_valid_fetch_advertised_ref(name) {
1451 continue;
1452 }
1453 out.push(RemoteRef {
1454 name: name.clone(),
1455 oid: *oid,
1456 symref_target: None,
1457 });
1458 }
1459 sort_remote_refs(&mut out, &opts.sort, &opts.versionsort_suffixes);
1460 if let Some(limit) = opts.limit {
1461 out.truncate(limit);
1462 }
1463 Ok(out)
1464}
1465
1466pub(crate) fn list_refs_from_git_dir_for_fetch(
1468 git_dir: &Path,
1469 odb: Option<&Odb>,
1470 refspecs: &[String],
1471) -> RemoteResult<Vec<RemoteRef>> {
1472 let prefixes = crate::fetch::fetch_ref_list_prefixes(refspecs);
1473 let opts = ListRefsOptions {
1474 symrefs: true,
1475 ..Default::default()
1476 };
1477 list_refs_from_git_dir_with_scan_prefixes(git_dir, odb, &opts, &prefixes)
1478}
1479
1480pub fn list_refs_from_git_dir(
1483 git_dir: &Path,
1484 odb: Option<&Odb>,
1485 opts: &ListRefsOptions,
1486) -> RemoteResult<Vec<RemoteRef>> {
1487 list_refs_from_git_dir_with_scan_prefixes(git_dir, odb, opts, &list_ref_prefixes(opts))
1488}
1489
1490fn list_refs_from_git_dir_with_scan_prefixes(
1491 git_dir: &Path,
1492 odb: Option<&Odb>,
1493 opts: &ListRefsOptions,
1494 scan_prefixes: &[String],
1495) -> RemoteResult<Vec<RemoteRef>> {
1496 let mut lazy_odb: Option<Odb> = None;
1497
1498 let mut entries = Vec::new();
1499 if list_refs_includes_head(opts) && ref_matches_list_opts("HEAD", opts) {
1500 let symref_target = if opts.symrefs {
1501 crate::refs::read_symbolic_ref(git_dir, "HEAD")
1502 .ok()
1503 .flatten()
1504 } else {
1505 None
1506 };
1507 match crate::refs::resolve_ref(git_dir, "HEAD") {
1508 Ok(head_oid) => {
1509 entries.push(RemoteRef {
1510 name: "HEAD".to_owned(),
1511 oid: head_oid,
1512 symref_target,
1513 });
1514 }
1515 Err(_) if symref_target.is_some() => {
1516 entries.push(RemoteRef {
1517 name: "HEAD".to_owned(),
1518 oid: ObjectId::zero(),
1519 symref_target,
1520 });
1521 }
1522 Err(_) => {}
1523 }
1524 }
1525
1526 for prefix in scan_prefixes {
1527 let listed = crate::refs::list_refs(git_dir, prefix).map_err(RemoteError::Library)?;
1528 for (name, oid) in listed {
1529 if let Some(branch_tail) = name.strip_prefix("refs/heads/") {
1530 if branch_tail.starts_with("refs/") {
1531 continue;
1532 }
1533 }
1534 if !ref_matches_list_opts(&name, opts) {
1535 continue;
1536 }
1537 entries.push(RemoteRef {
1538 name: name.clone(),
1539 oid,
1540 symref_target: None,
1541 });
1542 if opts.peel && name.starts_with("refs/tags/") {
1543 let peel_name = format!("{name}^{{}}");
1544 if should_emit_peel_line(&peel_name, &opts.prefixes) {
1545 let peel_odb = match odb {
1546 Some(o) => o,
1547 None => lazy_odb.get_or_insert_with(|| {
1548 Odb::new(&git_dir.join("objects"))
1549 .with_config_git_dir(git_dir.to_path_buf())
1550 }),
1551 };
1552 if let Ok(Some(peeled)) = peel_odb.peel_tag_target(oid) {
1553 entries.push(RemoteRef {
1554 name: peel_name,
1555 oid: peeled,
1556 symref_target: None,
1557 });
1558 }
1559 }
1560 }
1561 }
1562 }
1563
1564 sort_remote_refs(&mut entries, &opts.sort, &opts.versionsort_suffixes);
1565 if let Some(limit) = opts.limit {
1566 entries.truncate(limit);
1567 }
1568 Ok(entries)
1569}
1570
1571#[cfg(test)]
1572mod tests {
1573 use super::*;
1574
1575 fn remote_ref(name: &str, hex_digit: char) -> RemoteRef {
1576 RemoteRef {
1577 name: name.to_owned(),
1578 oid: ObjectId::from_hex(&hex_digit.to_string().repeat(40)).unwrap(),
1579 symref_target: None,
1580 }
1581 }
1582
1583 #[test]
1584 fn missing_urls_name_the_remote() {
1585 let err = parse_remote_url(" ").expect_err("empty");
1586 assert!(matches!(err, RemoteError::NoUrl { remote: None }));
1587 assert_eq!(err.to_string(), "remote URL is empty");
1588
1589 let config = ConfigSet::new();
1590 let err = Remote::from_config(&config, "upstream").expect_err("no url");
1591 assert!(matches!(&err, RemoteError::NoUrl { remote: Some(name) } if name == "upstream"));
1592 assert_eq!(err.to_string(), "remote 'upstream' has no URL configured");
1593 }
1594
1595 #[test]
1596 fn fold_peeled_refs_attaches_peeled_lines_and_keeps_order() {
1597 let mut head = remote_ref("HEAD", '1');
1598 head.symref_target = Some("refs/heads/main".to_owned());
1599 let refs = vec![
1600 head,
1601 remote_ref("refs/heads/main", '1'),
1602 remote_ref("refs/tags/light", '1'),
1603 remote_ref("refs/tags/v1", '2'),
1604 remote_ref("refs/tags/v1^{}", '1'),
1605 ];
1606 let folded = fold_peeled_refs(&refs);
1607 let names: Vec<&str> = folded.iter().map(|r| r.name.as_str()).collect();
1608 assert_eq!(
1609 names,
1610 ["HEAD", "refs/heads/main", "refs/tags/light", "refs/tags/v1"]
1611 );
1612 assert_eq!(folded[0].symref_target.as_deref(), Some("refs/heads/main"));
1613 assert_eq!(folded[2].peeled, None);
1614 assert_eq!(folded[3].oid, refs[3].oid);
1615 assert_eq!(folded[3].peeled, Some(refs[4].oid));
1616 }
1617
1618 #[test]
1619 fn peeled_tag_only_matches_peel_suffix() {
1620 assert_eq!(
1621 remote_ref("refs/tags/v1^{}", '1').peeled_tag(),
1622 Some("refs/tags/v1")
1623 );
1624 assert_eq!(remote_ref("refs/tags/v1", '1').peeled_tag(), None);
1625 }
1626
1627 #[test]
1628 fn remote_url_parse_table() {
1629 assert!(matches!(
1630 RemoteUrl::try_from("/abs/path").unwrap(),
1631 RemoteUrl::Local(_)
1632 ));
1633 assert!(matches!(
1634 RemoteUrl::try_from("file:///tmp/r.git").unwrap(),
1635 RemoteUrl::File(_)
1636 ));
1637 assert!(matches!(
1638 RemoteUrl::try_from("git://host/repo").unwrap(),
1639 RemoteUrl::Git(_)
1640 ));
1641 assert!(matches!(
1642 RemoteUrl::try_from("git@host:repo.git").unwrap(),
1643 RemoteUrl::Ssh(_)
1644 ));
1645 assert!(matches!(
1646 RemoteUrl::try_from("ssh://git@host/repo").unwrap(),
1647 RemoteUrl::Ssh(_)
1648 ));
1649 assert!(matches!(
1650 RemoteUrl::try_from("https://host/r.git").unwrap(),
1651 RemoteUrl::Https(_)
1652 ));
1653 assert!(matches!(
1654 RemoteUrl::try_from("ftp://x"),
1655 Err(RemoteError::UnsupportedScheme)
1656 ));
1657 }
1658
1659 #[test]
1660 fn pattern_matches_empty_allows_all() {
1661 assert!(ref_matches_ls_remote_patterns("refs/heads/main", &[]));
1662 assert!(ref_matches_ls_remote_patterns("HEAD", &[]));
1663 }
1664
1665 #[test]
1666 fn pattern_matches_exact() {
1667 let pats = vec!["HEAD".to_owned()];
1668 assert!(ref_matches_ls_remote_patterns("HEAD", &pats));
1669 assert!(!ref_matches_ls_remote_patterns("refs/heads/main", &pats));
1670 }
1671
1672 #[test]
1673 fn pattern_matches_suffix_component() {
1674 let pats = vec!["main".to_owned()];
1675 assert!(ref_matches_ls_remote_patterns("refs/heads/main", &pats));
1676 assert!(!ref_matches_ls_remote_patterns("refs/heads/notmain", &pats));
1677 assert!(!ref_matches_ls_remote_patterns("main-branch", &pats));
1678 }
1679
1680 #[cfg_attr(
1681 not(feature = "full-tests"),
1682 ignore = "starts system git or is heavy; run with --features full-tests"
1683 )]
1684 #[test]
1685 fn query_remote_refs_in_memory_empty_pattern_matches_nothing() {
1686 let entries = vec![
1687 RemoteRef {
1688 name: "refs/heads/main".into(),
1689 oid: ObjectId::from_hex("1".repeat(40).as_str()).unwrap(),
1690 symref_target: None,
1691 },
1692 RemoteRef {
1693 name: "refs/tags/v1".into(),
1694 oid: ObjectId::from_hex("2".repeat(40).as_str()).unwrap(),
1695 symref_target: None,
1696 },
1697 ];
1698 let opts = ListRefsOptions {
1699 prefixes: vec![String::new()],
1700 ..Default::default()
1701 };
1702 let out = query_remote_refs_in_memory(entries, &opts, &RefQueryOptions::default());
1703 assert!(out.is_empty());
1704 }
1705
1706 #[cfg_attr(
1707 not(feature = "full-tests"),
1708 ignore = "starts system git or is heavy; run with --features full-tests"
1709 )]
1710 #[test]
1711 fn query_remote_refs_in_memory_version_sort_and_limit() {
1712 let entries = vec![
1713 RemoteRef {
1714 name: "refs/tags/v1.10".into(),
1715 oid: ObjectId::from_hex("1".repeat(40).as_str()).unwrap(),
1716 symref_target: None,
1717 },
1718 RemoteRef {
1719 name: "refs/tags/v1.2".into(),
1720 oid: ObjectId::from_hex("2".repeat(40).as_str()).unwrap(),
1721 symref_target: None,
1722 },
1723 RemoteRef {
1724 name: "refs/tags/v1.1".into(),
1725 oid: ObjectId::from_hex("3".repeat(40).as_str()).unwrap(),
1726 symref_target: None,
1727 },
1728 ];
1729 let opts = ListRefsOptions {
1730 sort: RefSort {
1731 key: SortKey::Version,
1732 reverse: false,
1733 },
1734 limit: Some(2),
1735 ..Default::default()
1736 };
1737 let out = query_remote_refs_in_memory(entries, &opts, &RefQueryOptions::default());
1738 assert_eq!(
1739 out.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(),
1740 vec!["refs/tags/v1.1", "refs/tags/v1.2"]
1741 );
1742 }
1743
1744 #[test]
1745 fn list_opts_heads_excludes_head() {
1746 let heads_only = super::ListRefsOptions {
1747 heads: true,
1748 ..Default::default()
1749 };
1750 assert!(!super::ref_matches_list_opts("HEAD", &heads_only));
1751 assert!(super::ref_matches_list_opts("refs/heads/main", &heads_only));
1752 assert!(!super::ref_matches_list_opts("refs/tags/v1", &heads_only));
1753 }
1754
1755 #[test]
1756 fn insteadof_rewrite_applied_in_from_config() {
1757 let mut cfg = ConfigSet::new();
1758 cfg.add_command_override("url.https://github.com/.insteadOf", "gh:")
1759 .unwrap();
1760 cfg.add_command_override("remote.origin.url", "gh:org/repo.git")
1761 .unwrap();
1762 let remote = Remote::from_config(&cfg, "origin").unwrap();
1763 assert!(matches!(remote.fetch_url(), RemoteUrl::Https(_)));
1764 }
1765
1766 #[test]
1767 fn push_insteadof_applied_when_pushurl_absent() {
1768 let mut cfg = ConfigSet::new();
1769 cfg.add_command_override("url.ssh://push.example/.pushInsteadOf", "short:")
1770 .unwrap();
1771 cfg.add_command_override("remote.origin.url", "short:org/repo.git")
1772 .unwrap();
1773 let remote = Remote::from_config(&cfg, "origin").unwrap();
1774 assert_eq!(
1775 remote.push_url().to_url_string(),
1776 "ssh://push.example/org/repo.git"
1777 );
1778 assert_eq!(remote.fetch_url().to_url_string(), "short:org/repo.git");
1779 }
1780
1781 #[test]
1782 fn list_ref_prefixes_broad_for_ls_remote_patterns() {
1783 let opts = super::ListRefsOptions {
1784 prefixes: vec!["main".to_owned()],
1785 ..Default::default()
1786 };
1787 let prefixes = super::list_ref_prefixes(&opts);
1788 assert_eq!(
1789 prefixes,
1790 vec!["refs/heads/".to_owned(), "refs/tags/".to_owned()]
1791 );
1792 assert!(super::ref_matches_list_opts("refs/heads/main", &opts));
1793 assert!(!super::ref_matches_list_opts("refs/heads/topic", &opts));
1794 }
1795}