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 protocol_version = upload_pack_protocol_version(opts.http_protocol_version, repo)?;
559 if opts.http_protocol_version.is_none() {
560 opts.http_protocol_version = Some(protocol_version);
561 }
562 let connect = ConnectOptions {
563 protocol_version,
564 diagnostics: opts.diagnostics.clone(),
565 network_trace: opts.network_trace,
566 ..Default::default()
567 };
568 self.dispatch_fetch(
569 repo,
570 &self.fetch_url,
571 &mut opts,
572 progress,
573 cancel,
574 http_factory,
575 connect,
576 )
577 }
578
579 pub fn push(
595 &self,
596 repo: &Repository,
597 refs: &[PushRefSpec],
598 mut opts: PushOptions,
599 progress: &mut dyn Progress,
600 cancel: &dyn crate::progress::Cancel,
601 http_factory: Option<&dyn HttpClientFactory>,
602 ) -> RemoteResult<PushOutcome> {
603 if opts.tracking_remote.is_none() {
604 opts.tracking_remote = self.name.clone();
605 }
606 if opts.resumable_threshold.is_none() {
607 opts.resumable_threshold = repo
608 .config()
609 .ok()
610 .and_then(|c| c.get_i64("push.bundleThreshold").and_then(|r| r.ok()))
611 .and_then(|v| u64::try_from(v).ok());
612 }
613 let url = pre_push_hook_url(self.push_url());
614 let gate = PrePushGate {
615 repo,
616 remote: self.name.as_deref().unwrap_or(&url),
617 url: &url,
618 };
619 self.dispatch_push(
620 repo,
621 self.push_url(),
622 refs,
623 &opts,
624 (!opts.no_verify).then_some(&gate),
625 progress,
626 cancel,
627 http_factory,
628 ConnectOptions {
629 protocol_version: 0,
630 diagnostics: Some(repo.diagnostics()),
631 network_trace: repo.network_trace_enabled(),
632 ..Default::default()
633 },
634 )
635 }
636
637 #[must_use]
656 pub fn seed_from_bundle_uri(
657 &self,
658 repo: &Repository,
659 cache_dir: &Path,
660 progress: &mut dyn Progress,
661 cancel: &dyn crate::progress::Cancel,
662 http_factory: Option<&dyn HttpClientFactory>,
663 ) -> crate::bundle_uri::BundleSeed {
664 use crate::bundle_uri::BundleSeed;
665 let (RemoteUrl::Http(url) | RemoteUrl::Https(url)) = &self.fetch_url else {
666 return BundleSeed::NotOffered;
667 };
668 let enabled = repo
669 .config()
670 .ok()
671 .and_then(|c| c.get_bool("transfer.bundleURI").and_then(|r| r.ok()))
672 .unwrap_or(true);
673 if !enabled {
674 return BundleSeed::NotOffered;
675 }
676 let probe = ProbeHttpClientFactory::new(http_factory, repo);
677 let client = match http_client(Some(&probe), repo) {
678 Ok(c) => c,
679 Err(e) => {
680 return BundleSeed::Failed {
681 uri: None,
682 reason: e.to_string(),
683 }
684 }
685 };
686 crate::bundle_uri::seed_from_bundle_uri(
687 repo,
688 client.as_ref(),
689 url,
690 cache_dir,
691 progress,
692 cancel,
693 )
694 }
695
696 pub fn list_refs(
704 &self,
705 repo: Option<&Repository>,
706 opts: &ListRefsOptions,
707 http_factory: Option<&dyn HttpClientFactory>,
708 ) -> RemoteResult<Vec<RemoteRef>> {
709 let (git_dir, work_tree, odb) = repo_context(repo);
710 match &self.fetch_url {
711 RemoteUrl::Bundle(path) => list_refs_from_bundle(path, opts),
712 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
713 let remote_git = local_git_dir_from_url(&self.fetch_url, git_dir, work_tree)?;
714 list_refs_from_git_dir(&remote_git, None, opts)
715 }
716 wire => self.list_refs_wire(wire, git_dir, odb.as_ref(), opts, http_factory, repo),
717 }
718 }
719
720 pub fn list_push_refs(
731 &self,
732 repo: &Repository,
733 http_factory: Option<&dyn HttpClientFactory>,
734 ) -> RemoteResult<Vec<RemoteRef>> {
735 let url = self.push_url();
736 match url {
737 RemoteUrl::Bundle(_) => Err(RemoteError::UnsupportedScheme),
738 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
739 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
740 let refs =
741 crate::refs::list_refs(&remote_git, "refs/").map_err(RemoteError::Library)?;
742 Ok(refs
743 .into_iter()
744 .map(|(name, oid)| RemoteRef {
745 name,
746 oid,
747 symref_target: None,
748 })
749 .collect())
750 }
751 wire => self.list_refs_wire(
752 wire,
753 repo.git_dir(),
754 Some(repo.odb()),
755 &ListRefsOptions::default(),
756 http_factory,
757 Some(repo),
758 ),
759 }
760 }
761
762 #[allow(clippy::too_many_arguments)]
763 fn dispatch_fetch(
764 &self,
765 repo: &Repository,
766 url: &RemoteUrl,
767 opts: &mut FetchOptions,
768 progress: &mut dyn Progress,
769 cancel: &dyn crate::progress::Cancel,
770 http_factory: Option<&dyn HttpClientFactory>,
771 connect: ConnectOptions,
772 ) -> RemoteResult<FetchOutcome> {
773 match url {
774 RemoteUrl::Bundle(path) => Ok(crate::bundle_remote::fetch_from_bundle_into(
775 repo, path, opts, progress, cancel,
776 )?),
777 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
778 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
779 let cfg = ConfigSet::load(&opts.environment, Some(repo.git_dir()), true)
780 .map_err(RemoteError::Library)?;
781 if opts.filter.is_none() {
782 if let Some(name) = opts.remote_name.as_deref().or(self.name.as_deref()) {
783 opts.filter = crate::partial_clone::stored_remote_filter(&cfg, name);
784 }
785 }
786 if opts.auto_filter
792 || crate::partial_clone::fetch_requires_upload_pack(
793 opts.filter.as_ref(),
794 &cfg,
795 opts.remote_name.as_deref().or(self.name.as_deref()),
796 )
797 || local_source_has_promisor_remotes(&remote_git)
798 {
799 let client_env = if opts.environment.is_fetch_options_default() {
800 repo.environment()
801 } else {
802 &opts.environment
803 };
804 let mut conn = crate::transport::connect_local_upload_pack(
805 &remote_git,
806 Service::UploadPack,
807 &connect,
808 client_env,
809 repo.caches().promisor_fetcher(),
810 )
811 .map_err(RemoteError::Library)?;
812 let fetched = fetch_remote_into(repo, &mut *conn, opts, progress, cancel);
813 conn.finish().map_err(RemoteError::Library)?;
817 return Ok(fetched?);
818 }
819 Ok(fetch_local_into(repo, &remote_git, opts, progress, cancel)?)
820 }
821 RemoteUrl::Http(u) | RemoteUrl::Https(u) => {
822 let client: Arc<dyn HttpClient> = Arc::from(http_client(http_factory, repo)?);
823 let transport = SmartHttpTransport::new(client);
824 let mut conn = transport.connect(u, Service::UploadPack, &connect)?;
825 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
826 }
827 RemoteUrl::Git(u) => {
828 let mut conn =
829 GitDaemonTransport::new().connect(u, Service::UploadPack, &connect)?;
830 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
831 }
832 RemoteUrl::Ssh(u) => {
833 let transport = ssh_transport(Some(repo), Some(&opts.environment));
834 let mut conn = transport.connect(u, Service::UploadPack, &connect)?;
835 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
836 }
837 }
838 }
839
840 #[allow(clippy::too_many_arguments)]
841 fn dispatch_push(
842 &self,
843 repo: &Repository,
844 url: &RemoteUrl,
845 refs: &[PushRefSpec],
846 opts: &PushOptions,
847 pre_push: Option<&PrePushGate<'_>>,
848 progress: &mut dyn Progress,
849 cancel: &dyn crate::progress::Cancel,
850 http_factory: Option<&dyn HttpClientFactory>,
851 connect: ConnectOptions,
852 ) -> RemoteResult<PushOutcome> {
853 let mut over_wire = |conn: &mut dyn crate::transport::Connection| {
854 push_remote_from(repo, conn, refs, opts, progress, cancel, pre_push)
855 };
856 match url {
857 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
858 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
859 Ok(push_local_from(
860 repo,
861 &remote_git,
862 refs,
863 opts,
864 progress,
865 cancel,
866 pre_push,
867 )?)
868 }
869 RemoteUrl::Http(u) | RemoteUrl::Https(u) => {
870 let client: Arc<dyn HttpClient> = Arc::from(http_client(http_factory, repo)?);
871 let transport = SmartHttpTransport::new(client.clone());
872 let mut conn = transport.connect(u, Service::ReceivePack, &connect)?;
873 Ok(crate::push::push_remote_with(
874 repo,
875 &mut *conn,
876 refs,
877 opts,
878 progress,
879 cancel,
880 Some(crate::push::BundlePushTarget {
881 client: client.as_ref(),
882 repo_url: u,
883 }),
884 pre_push,
885 )?)
886 }
887 RemoteUrl::Git(u) => {
888 let mut conn =
889 GitDaemonTransport::new().connect(u, Service::ReceivePack, &connect)?;
890 Ok(over_wire(&mut *conn)?)
891 }
892 RemoteUrl::Ssh(u) => {
893 let transport = ssh_transport(Some(repo), None);
894 let mut conn = transport.connect(u, Service::ReceivePack, &connect)?;
895 Ok(over_wire(&mut *conn)?)
896 }
897 RemoteUrl::Bundle(_) => Err(RemoteError::UnsupportedScheme),
898 }
899 }
900
901 fn list_refs_wire(
902 &self,
903 url: &RemoteUrl,
904 local_git_dir: &Path,
905 odb: Option<&Odb>,
906 opts: &ListRefsOptions,
907 http_factory: Option<&dyn HttpClientFactory>,
908 repo: Option<&Repository>,
909 ) -> RemoteResult<Vec<RemoteRef>> {
910 let wire = url.as_wire_url().ok_or(RemoteError::UnsupportedScheme)?;
911 let mut opts = opts.clone();
912 if opts.protocol_version.is_none() {
913 opts.protocol_version = Some(list_refs_protocol_version(None, repo)?);
914 }
915 let requested = opts.protocol_version.unwrap_or(2);
916 let connect_opts = ConnectOptions {
917 protocol_version: requested,
918 ..Default::default()
919 };
920 match url {
921 RemoteUrl::Http(_) | RemoteUrl::Https(_) => {
922 list_refs_http(wire, local_git_dir, odb, &opts, http_factory, repo)
923 }
924 RemoteUrl::Git(_) => {
925 let mut conn =
926 GitDaemonTransport::new().connect(wire, Service::UploadPack, &connect_opts)?;
927 list_refs_from_connection(&mut *conn, local_git_dir, odb, &opts)
928 }
929 RemoteUrl::Ssh(_) => {
930 let transport = ssh_transport(repo, None);
931 let mut conn = transport.connect(wire, Service::UploadPack, &connect_opts)?;
932 list_refs_from_connection(&mut *conn, local_git_dir, odb, &opts)
933 }
934 RemoteUrl::Local(_) | RemoteUrl::File(_) | RemoteUrl::Bundle(_) => unreachable!(),
935 }
936 }
937}
938
939fn pre_push_hook_url(url: &RemoteUrl) -> String {
941 match url {
942 RemoteUrl::File(path) => format!("file://{}", path.to_string_lossy()),
943 other => other.to_url_string(),
944 }
945}
946
947fn repo_context(repo: Option<&Repository>) -> (&Path, Option<&Path>, Option<Odb>) {
948 match repo {
949 Some(r) => (r.git_dir(), r.work_tree(), Some(r.odb().clone())),
950 None => (Path::new("."), None, None),
951 }
952}
953
954fn upload_pack_protocol_version(explicit: Option<u8>, repo: &Repository) -> RemoteResult<u8> {
955 if let Some(v) = explicit {
956 return Ok(v);
957 }
958 crate::protocol::try_effective_client_protocol_version_from_config(
959 repo.config().map_err(RemoteError::Library)?.as_ref(),
960 )
961 .map_err(RemoteError::Library)
962}
963
964fn list_refs_protocol_version(explicit: Option<u8>, repo: Option<&Repository>) -> RemoteResult<u8> {
965 if let Some(v) = explicit {
966 return Ok(v);
967 }
968 if let Some(repo) = repo {
969 return upload_pack_protocol_version(None, repo);
970 }
971 Ok(2)
972}
973
974fn local_source_has_promisor_remotes(git_dir: &Path) -> bool {
977 ConfigSet::load_repo_local_only(git_dir)
978 .is_ok_and(|cfg| crate::promisor::repo_treats_promisor_packs(git_dir, &cfg))
979}
980
981fn local_git_dir_from_url(
982 url: &RemoteUrl,
983 git_dir: &Path,
984 work_tree: Option<&Path>,
985) -> RemoteResult<PathBuf> {
986 let s = url.to_url_string();
987 let remote_git = resolve_local_remote_git_dir(&s, git_dir, work_tree);
988 crate::transfer::ensure_local_remote_repository(&remote_git)?;
989 Ok(remote_git)
990}
991
992fn ssh_transport(
993 repo: Option<&Repository>,
994 fetch_env: Option<&crate::environment::Environment>,
995) -> SshTransport {
996 let mut transport = SshTransport::new();
997 if let Some(r) = repo {
998 transport = transport.with_command_runner(r.command_runner());
999 }
1000 let env = ssh_wire_environment(repo, fetch_env);
1001 transport = transport.with_ssh_environment(std::sync::Arc::new(env.clone()));
1002 if let Some(r) = repo {
1003 if let Ok(config) = r.config() {
1004 transport = transport.with_ssh_config(config);
1005 }
1006 } else if let Ok(config) = ConfigSet::load(&env, None, true) {
1007 transport = transport.with_ssh_config(Arc::new(config));
1008 }
1009 transport
1010}
1011
1012fn ssh_wire_environment(
1015 repo: Option<&Repository>,
1016 fetch_env: Option<&crate::environment::Environment>,
1017) -> crate::environment::Environment {
1018 if let Some(fe) = fetch_env {
1019 if !fe.is_fetch_options_default() {
1020 return fe.clone();
1021 }
1022 }
1023 repo.map(|r| r.environment().clone())
1024 .unwrap_or_else(crate::environment::Environment::capture_process)
1025}
1026
1027fn http_client(
1028 factory: Option<&dyn HttpClientFactory>,
1029 repo: &Repository,
1030) -> RemoteResult<Box<dyn HttpClient>> {
1031 let config = repo.config().map_err(RemoteError::Library)?;
1032 if let Some(f) = factory {
1033 return f.create(config.as_ref());
1034 }
1035 #[cfg(feature = "http-ureq")]
1036 {
1037 DefaultHttpClientFactory::with_environment(repo.environment().clone())
1038 .create(config.as_ref())
1039 }
1040 #[cfg(not(feature = "http-ureq"))]
1041 {
1042 let _ = config;
1043 Err(RemoteError::HttpUnavailable)
1044 }
1045}
1046
1047fn list_refs_http(
1048 repo_url: &str,
1049 local_git_dir: &Path,
1050 odb: Option<&Odb>,
1051 opts: &ListRefsOptions,
1052 http_factory: Option<&dyn HttpClientFactory>,
1053 repo: Option<&Repository>,
1054) -> RemoteResult<Vec<RemoteRef>> {
1055 let config: Arc<ConfigSet> = if let Some(r) = repo {
1056 r.config().map_err(RemoteError::Library)?
1057 } else {
1058 Arc::new(ConfigSet::new())
1059 };
1060 let client: Box<dyn HttpClient> = match http_factory {
1061 Some(f) => f.create(config.as_ref())?,
1062 None => {
1063 #[cfg(feature = "http-ureq")]
1064 {
1065 repo.map_or_else(DefaultHttpClientFactory::default, |r| {
1066 DefaultHttpClientFactory::with_environment(r.environment().clone())
1067 })
1068 .create(config.as_ref())?
1069 }
1070 #[cfg(not(feature = "http-ureq"))]
1071 {
1072 return Err(RemoteError::HttpUnavailable);
1073 }
1074 }
1075 };
1076
1077 let protocol_header_value = opts
1080 .protocol_version
1081 .and_then(crate::protocol::client_git_protocol_header_value);
1082 let git_protocol: Option<&str> = match opts.protocol_version {
1083 None => None,
1084 Some(0) => Some(""),
1085 Some(_) => protocol_header_value.as_deref(),
1086 };
1087
1088 let info_url = crate::transport::http::smart_info_refs_discovery_url(repo_url);
1089 let want_v2 = opts.protocol_version.unwrap_or(2) >= 2;
1090 let (body, final_url) = client
1091 .get_with_final_url_exact(&info_url, git_protocol)
1092 .map_err(RemoteError::Library)?;
1093 let effective_base =
1094 crate::transport::http::rebased_base_from_redirect(repo_url, final_url.as_deref())
1095 .unwrap_or_else(|| repo_url.to_owned());
1096 let (protocol_version, refs, caps, head_symref) =
1097 if effective_base.trim_end_matches('/') != repo_url.trim_end_matches('/') {
1098 client.reset_auth_after_redirect_rebase();
1099 crate::transport::http::discover_upload_pack_for_list_refs(
1100 client.as_ref(),
1101 &effective_base,
1102 git_protocol,
1103 )
1104 .map_err(RemoteError::Library)?
1105 } else {
1106 crate::transport::http::discover_upload_pack_for_list_refs_from_body(&body)
1107 .map_err(RemoteError::Library)?
1108 };
1109
1110 if protocol_version >= 2 && want_v2 {
1111 let local_odb = odb.cloned().unwrap_or_else(|| open_odb(local_git_dir));
1112 let req = build_list_refs_v2_request(&caps, &local_odb, opts)?;
1113 let post_url = format!("{}/git-upload-pack", effective_base.trim_end_matches('/'));
1114 let body = client
1115 .as_ref()
1116 .post(
1117 &post_url,
1118 "application/x-git-upload-pack-request",
1119 "application/x-git-upload-pack-result",
1120 &req,
1121 git_protocol,
1122 )
1123 .map_err(RemoteError::Library)?;
1124 let mut cur = Cursor::new(body);
1125 parse_list_refs_v2_response(&mut cur, opts, head_symref)
1126 } else {
1127 list_refs_v0_advertisement(&refs, head_symref.as_deref(), odb, opts)
1128 }
1129}
1130
1131fn list_refs_from_connection(
1132 conn: &mut dyn crate::transport::Connection,
1133 local_git_dir: &Path,
1134 odb: Option<&Odb>,
1135 opts: &ListRefsOptions,
1136) -> RemoteResult<Vec<RemoteRef>> {
1137 let want_v2 = opts.protocol_version.unwrap_or(2) >= 2;
1138 if conn.protocol_version() >= 2 && want_v2 {
1139 list_refs_v2_connection(conn, local_git_dir, odb, opts)
1140 } else {
1141 let out =
1142 list_refs_v0_advertisement(conn.advertised_refs(), conn.head_symref(), odb, opts)?;
1143 crate::transport::send_client_flush(conn).map_err(RemoteError::Library)?;
1144 Ok(out)
1145 }
1146}
1147
1148fn open_odb(git_dir: &Path) -> Odb {
1149 crate::transfer::open_odb(git_dir)
1150}
1151
1152fn list_refs_v2_connection(
1153 conn: &mut dyn crate::transport::Connection,
1154 local_git_dir: &Path,
1155 odb: Option<&Odb>,
1156 opts: &ListRefsOptions,
1157) -> RemoteResult<Vec<RemoteRef>> {
1158 let local_odb = odb.cloned().unwrap_or_else(|| open_odb(local_git_dir));
1159 let req = build_list_refs_v2_request(conn.capabilities(), &local_odb, opts)?;
1160 conn.writer().write_all(&req).map_err(Error::Io)?;
1161 conn.writer().flush().map_err(Error::Io)?;
1162 let discovery_head = conn.head_symref().map(str::to_owned);
1163 let out = parse_list_refs_v2_response(conn.reader(), opts, discovery_head)?;
1164 conn.finish_send();
1165 Ok(out)
1166}
1167
1168fn build_list_refs_v2_request(
1169 server_caps: &[String],
1170 local_odb: &Odb,
1171 opts: &ListRefsOptions,
1172) -> RemoteResult<Vec<u8>> {
1173 use crate::fetch::v2_object_format;
1174 use crate::pkt_line;
1175 use crate::protocol_v2;
1176
1177 let object_format = v2_object_format(server_caps, local_odb);
1178 let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
1179 let mut req: Vec<u8> = Vec::new();
1180 pkt_line::write_line(&mut req, "command=ls-refs").map_err(Error::Io)?;
1181 if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
1182 for line in &cap_echo {
1183 pkt_line::write_line(&mut req, line).map_err(Error::Io)?;
1184 }
1185 } else {
1186 for line in &cap_echo {
1187 pkt_line::write_line(&mut req, line).map_err(Error::Io)?;
1188 }
1189 pkt_line::write_line(&mut req, &format!("object-format={object_format}"))
1190 .map_err(Error::Io)?;
1191 }
1192 pkt_line::write_delim(&mut req).map_err(Error::Io)?;
1193 if opts.symrefs {
1194 pkt_line::write_line(&mut req, "symrefs").map_err(Error::Io)?;
1195 if crate::protocol_v2::server_advertises_ls_refs_unborn(server_caps) {
1196 pkt_line::write_line(&mut req, "unborn").map_err(Error::Io)?;
1197 }
1198 }
1199 if opts.peel {
1200 pkt_line::write_line(&mut req, "peel").map_err(Error::Io)?;
1201 }
1202 if list_refs_includes_head(opts) {
1203 pkt_line::write_line(&mut req, "ref-prefix HEAD").map_err(Error::Io)?;
1204 }
1205 for prefix in list_ref_prefixes(opts) {
1206 pkt_line::write_line(&mut req, &format!("ref-prefix {prefix}")).map_err(Error::Io)?;
1207 }
1208 pkt_line::write_flush(&mut req).map_err(Error::Io)?;
1209 Ok(req)
1210}
1211
1212fn list_refs_includes_head(opts: &ListRefsOptions) -> bool {
1213 !opts.heads && !opts.tags && opts.prefixes.is_empty()
1214}
1215
1216fn list_ref_prefixes(opts: &ListRefsOptions) -> Vec<String> {
1217 if !opts.prefixes.is_empty() {
1218 if opts.heads && !opts.tags {
1219 return vec!["refs/heads/".to_owned()];
1220 }
1221 if opts.tags && !opts.heads {
1222 return vec!["refs/tags/".to_owned()];
1223 }
1224 return vec!["refs/heads/".to_owned(), "refs/tags/".to_owned()];
1225 }
1226 let mut out = Vec::new();
1227 let all = !opts.heads && !opts.tags;
1228 if all || opts.heads {
1229 out.push("refs/heads/".to_owned());
1230 }
1231 if all || opts.tags {
1232 out.push("refs/tags/".to_owned());
1233 }
1234 out
1235}
1236
1237fn parse_list_refs_v2_response(
1238 reader: &mut dyn Read,
1239 opts: &ListRefsOptions,
1240 mut head_symref: Option<String>,
1241) -> RemoteResult<Vec<RemoteRef>> {
1242 use crate::fetch::parse_ls_refs_v2_line;
1243 use crate::pkt_line;
1244
1245 let mut entries: Vec<RemoteRef> = Vec::new();
1246 let mut peel_map: std::collections::HashMap<String, ObjectId> =
1247 std::collections::HashMap::new();
1248 let mut reader = reader;
1249 loop {
1250 match pkt_line::read_packet(&mut reader).map_err(Error::Io)? {
1251 None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => break,
1252 Some(pkt_line::Packet::ResponseEnd) => break,
1253 Some(pkt_line::Packet::Data(line)) => {
1254 let line = line.trim_end_matches('\n');
1255 pkt_line::reject_err_line(line)?;
1256 if let Some(t) = crate::fetch::parse_v2_ls_refs_unborn_head_symref(line) {
1257 head_symref = Some(t);
1258 continue;
1259 }
1260 let Some((name, oid, symref_target, peel)) = parse_ls_refs_v2_line(line) else {
1261 continue;
1262 };
1263 if name.ends_with("^{}") {
1264 continue;
1265 }
1266 if name == "HEAD" {
1267 let sym =
1268 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t));
1269 if let Some(t) = sym.clone() {
1270 head_symref = Some(t);
1271 }
1272 if list_refs_includes_head(opts) && ref_matches_list_opts("HEAD", opts) {
1273 entries.push(RemoteRef {
1274 name: "HEAD".to_owned(),
1275 oid,
1276 symref_target: if opts.symrefs { sym } else { None },
1277 });
1278 }
1279 continue;
1280 }
1281 if !crate::refs::is_valid_fetch_advertised_ref(&name) {
1282 continue;
1283 }
1284 if let Some(p) = peel {
1285 peel_map.insert(name.clone(), p);
1286 }
1287 let sym =
1288 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t));
1289 entries.push(RemoteRef {
1290 name,
1291 oid,
1292 symref_target: sym,
1293 });
1294 }
1295 }
1296 }
1297 finalize_list_refs_output(entries, peel_map, opts, head_symref, None)
1298}
1299
1300fn list_refs_v0_advertisement(
1301 advertised: &[(String, ObjectId)],
1302 head_symref: Option<&str>,
1303 odb: Option<&Odb>,
1304 opts: &ListRefsOptions,
1305) -> RemoteResult<Vec<RemoteRef>> {
1306 let mut entries: Vec<RemoteRef> = Vec::new();
1307 let mut peel_map: std::collections::HashMap<String, ObjectId> =
1308 std::collections::HashMap::new();
1309 for (name, oid) in advertised {
1310 if name.ends_with("^{}") {
1311 if let Some(base) = name.strip_suffix("^{}") {
1312 peel_map.insert(base.to_owned(), *oid);
1313 }
1314 continue;
1315 }
1316 entries.push(RemoteRef {
1317 name: name.clone(),
1318 oid: *oid,
1319 symref_target: None,
1320 });
1321 }
1322 finalize_list_refs_output(entries, peel_map, opts, head_symref.map(str::to_owned), odb)
1323}
1324
1325fn finalize_list_refs_output(
1326 mut entries: Vec<RemoteRef>,
1327 peel_map: std::collections::HashMap<String, ObjectId>,
1328 opts: &ListRefsOptions,
1329 head_symref: Option<String>,
1330 odb: Option<&Odb>,
1331) -> RemoteResult<Vec<RemoteRef>> {
1332 entries.retain(|e| remote_ref_matches_namespace(&e.name, opts));
1333 entries.retain(|e| patterns_match_ls_remote(&e.name, &opts.prefixes));
1334 if opts.peel {
1335 let mut peel_lines: Vec<RemoteRef> = Vec::new();
1336 for e in &entries {
1337 if !e.name.starts_with("refs/tags/") {
1338 continue;
1339 }
1340 let peeled = peel_map
1341 .get(&e.name)
1342 .copied()
1343 .or_else(|| odb.and_then(|o| o.peel_tag_target(e.oid).ok().flatten()));
1344 if let Some(p) = peeled {
1345 let peel_name = format!("{}^{{}}", e.name);
1346 if should_emit_peel_line(&peel_name, &opts.prefixes) {
1347 peel_lines.push(RemoteRef {
1348 name: peel_name,
1349 oid: p,
1350 symref_target: None,
1351 });
1352 }
1353 }
1354 }
1355 entries.extend(peel_lines);
1356 }
1357 if list_refs_includes_head(opts) {
1358 if let Some(sym) = head_symref {
1359 if let Some(oid) = entries.iter().find(|e| e.name == sym).map(|e| e.oid) {
1360 entries.push(RemoteRef {
1361 name: "HEAD".to_owned(),
1362 oid,
1363 symref_target: if opts.symrefs { Some(sym) } else { None },
1364 });
1365 }
1366 }
1367 }
1368 let query_options = RefQueryOptions {
1369 versionsort_suffixes: opts.versionsort_suffixes.clone(),
1370 ..RefQueryOptions::default()
1371 };
1372 sort_remote_refs(
1373 &mut entries,
1374 &opts.sort,
1375 &query_options.versionsort_suffixes,
1376 );
1377 if let Some(limit) = opts.limit {
1378 entries.truncate(limit);
1379 }
1380 Ok(entries)
1381}
1382
1383#[must_use]
1391pub fn query_remote_refs_in_memory(
1392 mut entries: Vec<RemoteRef>,
1393 opts: &ListRefsOptions,
1394 query_options: &RefQueryOptions,
1395) -> Vec<RemoteRef> {
1396 entries.retain(|e| remote_ref_matches_namespace(&e.name, opts));
1397 entries.retain(|e| patterns_match_ls_remote(&e.name, &opts.prefixes));
1398 sort_remote_refs(
1399 &mut entries,
1400 &opts.sort,
1401 &query_options.versionsort_suffixes,
1402 );
1403 if let Some(limit) = opts.limit {
1404 entries.truncate(limit);
1405 }
1406 entries
1407}
1408
1409fn should_emit_peel_line(peel_name: &str, patterns: &[String]) -> bool {
1410 if patterns.is_empty() {
1411 return true;
1412 }
1413 patterns_match_ls_remote(peel_name, patterns)
1414}
1415
1416fn sort_remote_refs(entries: &mut [RemoteRef], sort: &RefSort, versionsort_suffixes: &[String]) {
1417 entries.sort_by(|a, b| {
1418 let primary = match sort.key {
1419 SortKey::Name => a.name.cmp(&b.name),
1420 SortKey::Version => compare_version_names(&a.name, &b.name, versionsort_suffixes),
1421 SortKey::Date => a.name.cmp(&b.name),
1422 };
1423 let primary = if sort.reverse {
1424 primary.reverse()
1425 } else {
1426 primary
1427 };
1428 if primary != std::cmp::Ordering::Equal {
1429 return primary;
1430 }
1431 a.name.cmp(&b.name)
1432 });
1433}
1434
1435fn remote_ref_matches_namespace(refname: &str, opts: &ListRefsOptions) -> bool {
1436 if opts.heads || opts.tags {
1437 let is_branch = opts.heads && refname.starts_with("refs/heads/");
1438 let is_tag = opts.tags && refname.starts_with("refs/tags/");
1439 if refname == "HEAD" {
1440 return false;
1441 }
1442 if refname.ends_with("^{}") {
1443 return is_tag;
1444 }
1445 if !is_branch && !is_tag {
1446 return false;
1447 }
1448 }
1449 true
1450}
1451
1452fn ref_matches_list_opts(refname: &str, opts: &ListRefsOptions) -> bool {
1453 remote_ref_matches_namespace(refname, opts) && patterns_match_ls_remote(refname, &opts.prefixes)
1454}
1455
1456#[must_use]
1458pub fn ref_matches_ls_remote_patterns(refname: &str, patterns: &[String]) -> bool {
1459 patterns_match_ls_remote(refname, patterns)
1460}
1461
1462fn list_refs_from_bundle(path: &Path, opts: &ListRefsOptions) -> RemoteResult<Vec<RemoteRef>> {
1466 let bundle = crate::bundle::Bundle::open(path)
1467 .map_err(|e| RemoteError::Library(Error::Message(e.to_string())))?;
1468 let mut out = Vec::new();
1469 for (name, oid) in &bundle.header().refs {
1470 if !ref_matches_list_opts(name, opts) {
1471 continue;
1472 }
1473 if name != "HEAD" && !crate::refs::is_valid_fetch_advertised_ref(name) {
1474 continue;
1475 }
1476 out.push(RemoteRef {
1477 name: name.clone(),
1478 oid: *oid,
1479 symref_target: None,
1480 });
1481 }
1482 sort_remote_refs(&mut out, &opts.sort, &opts.versionsort_suffixes);
1483 if let Some(limit) = opts.limit {
1484 out.truncate(limit);
1485 }
1486 Ok(out)
1487}
1488
1489pub(crate) fn list_refs_from_git_dir_for_fetch(
1491 git_dir: &Path,
1492 odb: Option<&Odb>,
1493 refspecs: &[String],
1494) -> RemoteResult<Vec<RemoteRef>> {
1495 let prefixes = crate::fetch::fetch_ref_list_prefixes(refspecs);
1496 let opts = ListRefsOptions {
1497 symrefs: true,
1498 ..Default::default()
1499 };
1500 list_refs_from_git_dir_with_scan_prefixes(git_dir, odb, &opts, &prefixes)
1501}
1502
1503pub fn list_refs_from_git_dir(
1506 git_dir: &Path,
1507 odb: Option<&Odb>,
1508 opts: &ListRefsOptions,
1509) -> RemoteResult<Vec<RemoteRef>> {
1510 list_refs_from_git_dir_with_scan_prefixes(git_dir, odb, opts, &list_ref_prefixes(opts))
1511}
1512
1513fn list_refs_from_git_dir_with_scan_prefixes(
1514 git_dir: &Path,
1515 odb: Option<&Odb>,
1516 opts: &ListRefsOptions,
1517 scan_prefixes: &[String],
1518) -> RemoteResult<Vec<RemoteRef>> {
1519 let mut lazy_odb: Option<Odb> = None;
1520
1521 let mut entries = Vec::new();
1522 if list_refs_includes_head(opts) && ref_matches_list_opts("HEAD", opts) {
1523 let symref_target = if opts.symrefs {
1524 crate::refs::read_symbolic_ref(git_dir, "HEAD")
1525 .ok()
1526 .flatten()
1527 } else {
1528 None
1529 };
1530 match crate::refs::resolve_ref(git_dir, "HEAD") {
1531 Ok(head_oid) => {
1532 entries.push(RemoteRef {
1533 name: "HEAD".to_owned(),
1534 oid: head_oid,
1535 symref_target,
1536 });
1537 }
1538 Err(_) if symref_target.is_some() => {
1539 entries.push(RemoteRef {
1540 name: "HEAD".to_owned(),
1541 oid: ObjectId::zero(),
1542 symref_target,
1543 });
1544 }
1545 Err(_) => {}
1546 }
1547 }
1548
1549 for prefix in scan_prefixes {
1550 let listed = crate::refs::list_refs(git_dir, prefix).map_err(RemoteError::Library)?;
1551 for (name, oid) in listed {
1552 if let Some(branch_tail) = name.strip_prefix("refs/heads/") {
1553 if branch_tail.starts_with("refs/") {
1554 continue;
1555 }
1556 }
1557 if !ref_matches_list_opts(&name, opts) {
1558 continue;
1559 }
1560 entries.push(RemoteRef {
1561 name: name.clone(),
1562 oid,
1563 symref_target: None,
1564 });
1565 if opts.peel && name.starts_with("refs/tags/") {
1566 let peel_name = format!("{name}^{{}}");
1567 if should_emit_peel_line(&peel_name, &opts.prefixes) {
1568 let peel_odb = match odb {
1569 Some(o) => o,
1570 None => lazy_odb.get_or_insert_with(|| {
1571 Odb::new(&git_dir.join("objects"))
1572 .with_config_git_dir(git_dir.to_path_buf())
1573 }),
1574 };
1575 if let Ok(Some(peeled)) = peel_odb.peel_tag_target(oid) {
1576 entries.push(RemoteRef {
1577 name: peel_name,
1578 oid: peeled,
1579 symref_target: None,
1580 });
1581 }
1582 }
1583 }
1584 }
1585 }
1586
1587 sort_remote_refs(&mut entries, &opts.sort, &opts.versionsort_suffixes);
1588 if let Some(limit) = opts.limit {
1589 entries.truncate(limit);
1590 }
1591 Ok(entries)
1592}
1593
1594#[cfg(test)]
1595mod tests {
1596 use super::*;
1597
1598 fn remote_ref(name: &str, hex_digit: char) -> RemoteRef {
1599 RemoteRef {
1600 name: name.to_owned(),
1601 oid: ObjectId::from_hex(&hex_digit.to_string().repeat(40)).unwrap(),
1602 symref_target: None,
1603 }
1604 }
1605
1606 #[test]
1607 fn missing_urls_name_the_remote() {
1608 let err = parse_remote_url(" ").expect_err("empty");
1609 assert!(matches!(err, RemoteError::NoUrl { remote: None }));
1610 assert_eq!(err.to_string(), "remote URL is empty");
1611
1612 let config = ConfigSet::new();
1613 let err = Remote::from_config(&config, "upstream").expect_err("no url");
1614 assert!(matches!(&err, RemoteError::NoUrl { remote: Some(name) } if name == "upstream"));
1615 assert_eq!(err.to_string(), "remote 'upstream' has no URL configured");
1616 }
1617
1618 #[test]
1619 fn fold_peeled_refs_attaches_peeled_lines_and_keeps_order() {
1620 let mut head = remote_ref("HEAD", '1');
1621 head.symref_target = Some("refs/heads/main".to_owned());
1622 let refs = vec![
1623 head,
1624 remote_ref("refs/heads/main", '1'),
1625 remote_ref("refs/tags/light", '1'),
1626 remote_ref("refs/tags/v1", '2'),
1627 remote_ref("refs/tags/v1^{}", '1'),
1628 ];
1629 let folded = fold_peeled_refs(&refs);
1630 let names: Vec<&str> = folded.iter().map(|r| r.name.as_str()).collect();
1631 assert_eq!(
1632 names,
1633 ["HEAD", "refs/heads/main", "refs/tags/light", "refs/tags/v1"]
1634 );
1635 assert_eq!(folded[0].symref_target.as_deref(), Some("refs/heads/main"));
1636 assert_eq!(folded[2].peeled, None);
1637 assert_eq!(folded[3].oid, refs[3].oid);
1638 assert_eq!(folded[3].peeled, Some(refs[4].oid));
1639 }
1640
1641 #[test]
1642 fn peeled_tag_only_matches_peel_suffix() {
1643 assert_eq!(
1644 remote_ref("refs/tags/v1^{}", '1').peeled_tag(),
1645 Some("refs/tags/v1")
1646 );
1647 assert_eq!(remote_ref("refs/tags/v1", '1').peeled_tag(), None);
1648 }
1649
1650 #[test]
1651 fn remote_url_parse_table() {
1652 assert!(matches!(
1653 RemoteUrl::try_from("/abs/path").unwrap(),
1654 RemoteUrl::Local(_)
1655 ));
1656 assert!(matches!(
1657 RemoteUrl::try_from("file:///tmp/r.git").unwrap(),
1658 RemoteUrl::File(_)
1659 ));
1660 assert!(matches!(
1661 RemoteUrl::try_from("git://host/repo").unwrap(),
1662 RemoteUrl::Git(_)
1663 ));
1664 assert!(matches!(
1665 RemoteUrl::try_from("git@host:repo.git").unwrap(),
1666 RemoteUrl::Ssh(_)
1667 ));
1668 assert!(matches!(
1669 RemoteUrl::try_from("ssh://git@host/repo").unwrap(),
1670 RemoteUrl::Ssh(_)
1671 ));
1672 assert!(matches!(
1673 RemoteUrl::try_from("https://host/r.git").unwrap(),
1674 RemoteUrl::Https(_)
1675 ));
1676 assert!(matches!(
1677 RemoteUrl::try_from("ftp://x"),
1678 Err(RemoteError::UnsupportedScheme)
1679 ));
1680 }
1681
1682 #[test]
1683 fn pattern_matches_empty_allows_all() {
1684 assert!(ref_matches_ls_remote_patterns("refs/heads/main", &[]));
1685 assert!(ref_matches_ls_remote_patterns("HEAD", &[]));
1686 }
1687
1688 #[test]
1689 fn pattern_matches_exact() {
1690 let pats = vec!["HEAD".to_owned()];
1691 assert!(ref_matches_ls_remote_patterns("HEAD", &pats));
1692 assert!(!ref_matches_ls_remote_patterns("refs/heads/main", &pats));
1693 }
1694
1695 #[test]
1696 fn pattern_matches_suffix_component() {
1697 let pats = vec!["main".to_owned()];
1698 assert!(ref_matches_ls_remote_patterns("refs/heads/main", &pats));
1699 assert!(!ref_matches_ls_remote_patterns("refs/heads/notmain", &pats));
1700 assert!(!ref_matches_ls_remote_patterns("main-branch", &pats));
1701 }
1702
1703 #[cfg_attr(
1704 not(feature = "full-tests"),
1705 ignore = "starts system git or is heavy; run with --features full-tests"
1706 )]
1707 #[test]
1708 fn query_remote_refs_in_memory_empty_pattern_matches_nothing() {
1709 let entries = vec![
1710 RemoteRef {
1711 name: "refs/heads/main".into(),
1712 oid: ObjectId::from_hex("1".repeat(40).as_str()).unwrap(),
1713 symref_target: None,
1714 },
1715 RemoteRef {
1716 name: "refs/tags/v1".into(),
1717 oid: ObjectId::from_hex("2".repeat(40).as_str()).unwrap(),
1718 symref_target: None,
1719 },
1720 ];
1721 let opts = ListRefsOptions {
1722 prefixes: vec![String::new()],
1723 ..Default::default()
1724 };
1725 let out = query_remote_refs_in_memory(entries, &opts, &RefQueryOptions::default());
1726 assert!(out.is_empty());
1727 }
1728
1729 #[cfg_attr(
1730 not(feature = "full-tests"),
1731 ignore = "starts system git or is heavy; run with --features full-tests"
1732 )]
1733 #[test]
1734 fn query_remote_refs_in_memory_version_sort_and_limit() {
1735 let entries = vec![
1736 RemoteRef {
1737 name: "refs/tags/v1.10".into(),
1738 oid: ObjectId::from_hex("1".repeat(40).as_str()).unwrap(),
1739 symref_target: None,
1740 },
1741 RemoteRef {
1742 name: "refs/tags/v1.2".into(),
1743 oid: ObjectId::from_hex("2".repeat(40).as_str()).unwrap(),
1744 symref_target: None,
1745 },
1746 RemoteRef {
1747 name: "refs/tags/v1.1".into(),
1748 oid: ObjectId::from_hex("3".repeat(40).as_str()).unwrap(),
1749 symref_target: None,
1750 },
1751 ];
1752 let opts = ListRefsOptions {
1753 sort: RefSort {
1754 key: SortKey::Version,
1755 reverse: false,
1756 },
1757 limit: Some(2),
1758 ..Default::default()
1759 };
1760 let out = query_remote_refs_in_memory(entries, &opts, &RefQueryOptions::default());
1761 assert_eq!(
1762 out.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(),
1763 vec!["refs/tags/v1.1", "refs/tags/v1.2"]
1764 );
1765 }
1766
1767 #[test]
1768 fn list_opts_heads_excludes_head() {
1769 let heads_only = super::ListRefsOptions {
1770 heads: true,
1771 ..Default::default()
1772 };
1773 assert!(!super::ref_matches_list_opts("HEAD", &heads_only));
1774 assert!(super::ref_matches_list_opts("refs/heads/main", &heads_only));
1775 assert!(!super::ref_matches_list_opts("refs/tags/v1", &heads_only));
1776 }
1777
1778 #[test]
1779 fn insteadof_rewrite_applied_in_from_config() {
1780 let mut cfg = ConfigSet::new();
1781 cfg.add_command_override("url.https://github.com/.insteadOf", "gh:")
1782 .unwrap();
1783 cfg.add_command_override("remote.origin.url", "gh:org/repo.git")
1784 .unwrap();
1785 let remote = Remote::from_config(&cfg, "origin").unwrap();
1786 assert!(matches!(remote.fetch_url(), RemoteUrl::Https(_)));
1787 }
1788
1789 #[test]
1790 fn push_insteadof_applied_when_pushurl_absent() {
1791 let mut cfg = ConfigSet::new();
1792 cfg.add_command_override("url.ssh://push.example/.pushInsteadOf", "short:")
1793 .unwrap();
1794 cfg.add_command_override("remote.origin.url", "short:org/repo.git")
1795 .unwrap();
1796 let remote = Remote::from_config(&cfg, "origin").unwrap();
1797 assert_eq!(
1798 remote.push_url().to_url_string(),
1799 "ssh://push.example/org/repo.git"
1800 );
1801 assert_eq!(remote.fetch_url().to_url_string(), "short:org/repo.git");
1802 }
1803
1804 #[test]
1805 fn list_ref_prefixes_broad_for_ls_remote_patterns() {
1806 let opts = super::ListRefsOptions {
1807 prefixes: vec!["main".to_owned()],
1808 ..Default::default()
1809 };
1810 let prefixes = super::list_ref_prefixes(&opts);
1811 assert_eq!(
1812 prefixes,
1813 vec!["refs/heads/".to_owned(), "refs/tags/".to_owned()]
1814 );
1815 assert!(super::ref_matches_list_opts("refs/heads/main", &opts));
1816 assert!(!super::ref_matches_list_opts("refs/heads/topic", &opts));
1817 }
1818}