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::{askpass_program, HelperCredentialProvider};
16#[cfg(feature = "http-ureq")]
17use crate::environment::Environment;
18use crate::error::Error;
19use crate::fetch::{fetch_remote_into, Progress};
20use crate::objects::ObjectId;
21use crate::odb::Odb;
22use crate::push::push_remote_from;
23use crate::refs::listing::{
24 compare_version_names, patterns_match_ls_remote, RefQueryOptions, RefSort, SortKey,
25};
26use crate::repo::Repository;
27use crate::transfer::{
28 fetch_local_into, push_local_from, FetchOptions, FetchOutcome, PushOptions, PushOutcome,
29 PushRefSpec,
30};
31use crate::transport::http::{HttpClient, SmartHttpTransport};
32use crate::transport::{ConnectOptions, GitDaemonTransport, Service, SshTransport, Transport};
33use crate::transport_path::{
34 is_local_path_remote_url, resolve_local_remote_git_dir, url_is_local_not_ssh,
35};
36use crate::url_rewrite;
37
38#[cfg(feature = "http-ureq")]
39use crate::transport::http::ureq_client::UreqHttpClient;
40
41#[cfg(feature = "http-ureq")]
43pub mod http_client {
44 pub use crate::transport::http::ureq_client::UreqHttpClient;
45 pub use crate::transport::http::HttpClient;
46}
47
48pub type RemoteResult<T> = std::result::Result<T, RemoteError>;
50
51pub const DEFAULT_REMOTE: &str = "origin";
53
54#[must_use]
56pub(crate) fn is_valid_remote_name(name: &str) -> bool {
57 use crate::check_ref_format::{check_refname_format, RefNameOptions};
58 !name.is_empty()
59 && check_refname_format(
60 &format!("refs/remotes/{name}/test"),
61 &RefNameOptions::default(),
62 )
63 .is_ok()
64}
65
66#[derive(Debug, Error)]
68pub enum RemoteError {
69 #[error("{}", match remote {
71 Some(name) => format!("remote '{name}' has no URL configured"),
72 None => "remote URL is empty".to_owned(),
73 })]
74 NoUrl {
75 remote: Option<String>,
77 },
78 #[error("unsupported remote URL scheme")]
80 UnsupportedScheme,
81 #[error("{0}")]
83 InvalidUrl(String),
84 #[error("HTTP remotes require the http-ureq feature or an HttpClientFactory")]
86 HttpUnavailable,
87 #[error("unknown remote '{0}'")]
89 UnknownRemote(String),
90 #[error("remote '{0}' already exists")]
93 RemoteExists(String),
94 #[error("'{0}' is not a valid remote name")]
96 InvalidRemoteName(String),
97 #[error(transparent)]
99 Library(#[from] Error),
100}
101
102impl RemoteError {
103 #[must_use]
105 pub fn is_remote_unreadable(&self) -> bool {
106 matches!(self, Self::Library(e) if e.is_remote_unreadable())
107 }
108
109 #[must_use]
111 pub fn is_cancelled(&self) -> bool {
112 matches!(self, Self::Library(e) if e.is_cancelled())
113 }
114}
115
116impl From<RemoteError> for Error {
117 fn from(e: RemoteError) -> Self {
118 match e {
119 RemoteError::Library(err) => err,
120 other => Error::Message(other.to_string()),
121 }
122 }
123}
124
125#[derive(Clone, Debug, PartialEq, Eq)]
127pub enum RemoteUrl {
128 Local(PathBuf),
130 File(PathBuf),
132 Git(String),
134 Ssh(String),
136 Http(String),
138 Https(String),
140 Bundle(PathBuf),
142}
143
144impl RemoteUrl {
145 #[must_use]
147 pub fn is_local(&self) -> bool {
148 matches!(self, Self::Local(_) | Self::File(_))
149 }
150
151 #[must_use]
153 pub fn as_wire_url(&self) -> Option<&str> {
154 match self {
155 Self::Git(s) | Self::Ssh(s) | Self::Http(s) | Self::Https(s) => Some(s.as_str()),
156 Self::Local(_) | Self::File(_) | Self::Bundle(_) => None,
157 }
158 }
159
160 #[must_use]
162 pub fn to_url_string(&self) -> String {
163 match self {
164 Self::Local(p) | Self::File(p) | Self::Bundle(p) => p.to_string_lossy().into_owned(),
165 Self::Git(s) | Self::Ssh(s) | Self::Http(s) | Self::Https(s) => s.clone(),
166 }
167 }
168}
169
170impl TryFrom<&str> for RemoteUrl {
171 type Error = RemoteError;
172
173 fn try_from(url: &str) -> std::result::Result<Self, Self::Error> {
174 parse_remote_url(url)
175 }
176}
177
178pub fn parse_remote_url(url: &str) -> RemoteResult<RemoteUrl> {
180 let url = url.trim();
181 if url.is_empty() {
182 return Err(RemoteError::NoUrl { remote: None });
183 }
184 if url.starts_with("file://") {
185 let path = crate::transport_path::file_url_to_local_path(url)
186 .map_err(|e| RemoteError::InvalidUrl(e.to_string()))?;
187 let path = PathBuf::from(path);
188 if crate::bundle_remote::is_bundle_path(&path) {
189 return Ok(RemoteUrl::Bundle(path));
190 }
191 return Ok(RemoteUrl::File(path));
192 }
193 if url.starts_with("git://") {
194 crate::transport::parse_git_url(url).map_err(|e| RemoteError::InvalidUrl(e.to_string()))?;
195 return Ok(RemoteUrl::Git(url.to_owned()));
196 }
197 if url.starts_with("http://") {
198 return Ok(RemoteUrl::Http(url.to_owned()));
199 }
200 if url.starts_with("https://") {
201 return Ok(RemoteUrl::Https(url.to_owned()));
202 }
203 if crate::transport::is_ssh_url(url) {
204 crate::transport::parse_ssh_url(url).map_err(|e| RemoteError::InvalidUrl(e.to_string()))?;
205 return Ok(RemoteUrl::Ssh(url.to_owned()));
206 }
207 if is_local_path_remote_url(url) && url_is_local_not_ssh(url) {
208 let path = PathBuf::from(url);
209 if crate::bundle_remote::is_bundle_path(&path) {
210 return Ok(RemoteUrl::Bundle(path));
211 }
212 return Ok(RemoteUrl::Local(path));
213 }
214 Err(RemoteError::UnsupportedScheme)
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct RemoteRef {
220 pub name: String,
222 pub oid: ObjectId,
224 pub symref_target: Option<String>,
226}
227
228impl RemoteRef {
229 #[must_use]
232 pub fn peeled_tag(&self) -> Option<&str> {
233 self.name.strip_suffix("^{}")
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct PeeledRemoteRef {
240 pub name: String,
242 pub oid: ObjectId,
244 pub peeled: Option<ObjectId>,
246 pub symref_target: Option<String>,
248}
249
250#[must_use]
254pub fn fold_peeled_refs(refs: &[RemoteRef]) -> Vec<PeeledRemoteRef> {
255 let peeled: std::collections::HashMap<&str, ObjectId> = refs
256 .iter()
257 .filter_map(|r| Some((r.peeled_tag()?, r.oid)))
258 .collect();
259 refs.iter()
260 .filter(|r| r.peeled_tag().is_none())
261 .map(|r| PeeledRemoteRef {
262 name: r.name.clone(),
263 oid: r.oid,
264 peeled: peeled.get(r.name.as_str()).copied(),
265 symref_target: r.symref_target.clone(),
266 })
267 .collect()
268}
269
270#[derive(Debug, Default, Clone)]
272pub struct ListRefsOptions {
273 pub prefixes: Vec<String>,
275 pub heads: bool,
277 pub tags: bool,
279 pub symrefs: bool,
281 pub peel: bool,
283 pub protocol_version: Option<u8>,
285 pub sort: RefSort,
287 pub limit: Option<usize>,
289 pub versionsort_suffixes: Vec<String>,
291}
292
293pub trait HttpClientFactory: Send + Sync {
295 fn create(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>>;
301}
302
303#[cfg(feature = "http-ureq")]
311#[derive(Debug, Default, Clone)]
312pub struct DefaultHttpClientFactory {
313 environment: Option<Environment>,
314}
315
316#[cfg(feature = "http-ureq")]
317impl DefaultHttpClientFactory {
318 #[must_use]
321 pub const fn new() -> Self {
322 Self { environment: None }
323 }
324
325 #[must_use]
328 pub fn with_environment(environment: Environment) -> Self {
329 Self {
330 environment: Some(environment),
331 }
332 }
333}
334
335#[cfg(feature = "http-ureq")]
336impl HttpClientFactory for DefaultHttpClientFactory {
337 fn create(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>> {
338 let askpass = match &self.environment {
339 Some(env) => askpass_program(env, config),
340 None => askpass_program(&Environment::empty(), config),
341 };
342 let provider =
343 Box::new(HelperCredentialProvider::new(config.clone()).with_askpass(askpass));
344 let client = UreqHttpClient::from_config(config)
345 .map_err(|e| RemoteError::InvalidUrl(e.to_string()))?
346 .with_credential_provider(provider);
347 Ok(Box::new(client))
348 }
349}
350
351#[derive(Clone, Debug)]
353pub struct Remote {
354 pub name: Option<String>,
356 fetch_url: RemoteUrl,
357 push_url: Option<RemoteUrl>,
358 pub fetch_refspecs: Vec<String>,
360}
361
362impl Remote {
363 pub fn from_config(config: &ConfigSet, name: &str) -> RemoteResult<Self> {
371 let raw_url = config
372 .get(&format!("remote.{name}.url"))
373 .filter(|u| !u.trim().is_empty())
374 .ok_or_else(|| RemoteError::NoUrl {
375 remote: Some(name.to_owned()),
376 })?;
377 let fetch_url = url_rewrite::rewrite_fetch_url(config, &raw_url);
378 let push_source = config
379 .get(&format!("remote.{name}.pushurl"))
380 .filter(|u| !u.trim().is_empty())
381 .unwrap_or_else(|| raw_url.clone());
382 let push_url = url_rewrite::rewrite_push_url(config, push_source.as_str());
383 let mut fetch_refspecs = config.get_all(&format!("remote.{name}.fetch"));
384 fetch_refspecs.retain(|s| !s.trim().is_empty());
385 if fetch_refspecs.is_empty() {
386 fetch_refspecs = vec![format!("+refs/heads/*:refs/remotes/{name}/*")];
387 }
388 Ok(Self {
389 name: Some(name.to_owned()),
390 fetch_url: RemoteUrl::try_from(fetch_url.as_str())?,
391 push_url: Some(RemoteUrl::try_from(push_url.as_str())?),
392 fetch_refspecs,
393 })
394 }
395
396 pub const ANONYMOUS_FETCH_REFSPEC: &'static str = "refs/heads/*:";
399
400 pub fn from_url(url: &str) -> RemoteResult<Self> {
409 Ok(Self {
410 name: None,
411 fetch_url: RemoteUrl::try_from(url)?,
412 push_url: None,
413 fetch_refspecs: vec![Self::ANONYMOUS_FETCH_REFSPEC.to_owned()],
414 })
415 }
416
417 #[must_use]
419 pub fn fetch_url(&self) -> &RemoteUrl {
420 &self.fetch_url
421 }
422
423 #[must_use]
425 pub fn push_url(&self) -> &RemoteUrl {
426 self.push_url.as_ref().unwrap_or(&self.fetch_url)
427 }
428
429 pub fn fetch(
442 &self,
443 repo: &Repository,
444 mut opts: FetchOptions,
445 progress: &mut dyn Progress,
446 cancel: &dyn crate::progress::Cancel,
447 http_factory: Option<&dyn HttpClientFactory>,
448 ) -> RemoteResult<FetchOutcome> {
449 repo.odb().require_files_primary("fetch")?;
450 if opts.configured_refspecs.is_empty() {
451 opts.configured_refspecs = self.fetch_refspecs.clone();
452 }
453 if opts.refspecs.is_empty() {
454 opts.refspecs = self.fetch_refspecs.clone();
455 }
456 if opts.remote_name.is_none() {
457 opts.remote_name = self.name.clone();
458 }
459 if opts.fetch_source.is_none() {
460 opts.fetch_source = Some(self.fetch_url.to_url_string());
461 }
462 if opts.diagnostics.is_none() {
463 opts.diagnostics = Some(repo.diagnostics());
464 opts.network_trace = repo.network_trace_enabled();
465 }
466 let connect = ConnectOptions {
467 protocol_version: 2,
468 diagnostics: opts.diagnostics.clone(),
469 network_trace: opts.network_trace,
470 ..Default::default()
471 };
472 self.dispatch_fetch(
473 repo,
474 &self.fetch_url,
475 &mut opts,
476 progress,
477 cancel,
478 http_factory,
479 connect,
480 )
481 }
482
483 pub fn push(
492 &self,
493 repo: &Repository,
494 refs: &[PushRefSpec],
495 mut opts: PushOptions,
496 progress: &mut dyn Progress,
497 cancel: &dyn crate::progress::Cancel,
498 http_factory: Option<&dyn HttpClientFactory>,
499 ) -> RemoteResult<PushOutcome> {
500 repo.odb().require_files_primary("push")?;
501 if opts.tracking_remote.is_none() {
502 opts.tracking_remote = self.name.clone();
503 }
504 self.dispatch_push(
505 repo,
506 self.push_url(),
507 refs,
508 &opts,
509 progress,
510 cancel,
511 http_factory,
512 ConnectOptions {
513 protocol_version: 0,
514 diagnostics: Some(repo.diagnostics()),
515 network_trace: repo.network_trace_enabled(),
516 ..Default::default()
517 },
518 )
519 }
520
521 pub fn list_refs(
529 &self,
530 repo: Option<&Repository>,
531 opts: &ListRefsOptions,
532 http_factory: Option<&dyn HttpClientFactory>,
533 ) -> RemoteResult<Vec<RemoteRef>> {
534 let (git_dir, work_tree, odb) = repo_context(repo);
535 match &self.fetch_url {
536 RemoteUrl::Bundle(path) => list_refs_from_bundle(path, opts),
537 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
538 let remote_git = local_git_dir_from_url(&self.fetch_url, git_dir, work_tree)?;
539 list_refs_from_git_dir(&remote_git, None, opts)
540 }
541 wire => self.list_refs_wire(wire, git_dir, odb.as_ref(), opts, http_factory, repo),
542 }
543 }
544
545 pub fn list_push_refs(
556 &self,
557 repo: &Repository,
558 http_factory: Option<&dyn HttpClientFactory>,
559 ) -> RemoteResult<Vec<RemoteRef>> {
560 let url = self.push_url();
561 match url {
562 RemoteUrl::Bundle(_) => Err(RemoteError::UnsupportedScheme),
563 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
564 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
565 let refs =
566 crate::refs::list_refs(&remote_git, "refs/").map_err(RemoteError::Library)?;
567 Ok(refs
568 .into_iter()
569 .map(|(name, oid)| RemoteRef {
570 name,
571 oid,
572 symref_target: None,
573 })
574 .collect())
575 }
576 wire => self.list_refs_wire(
577 wire,
578 repo.git_dir(),
579 Some(repo.odb()),
580 &ListRefsOptions::default(),
581 http_factory,
582 Some(repo),
583 ),
584 }
585 }
586
587 #[allow(clippy::too_many_arguments)]
588 fn dispatch_fetch(
589 &self,
590 repo: &Repository,
591 url: &RemoteUrl,
592 opts: &mut FetchOptions,
593 progress: &mut dyn Progress,
594 cancel: &dyn crate::progress::Cancel,
595 http_factory: Option<&dyn HttpClientFactory>,
596 connect: ConnectOptions,
597 ) -> RemoteResult<FetchOutcome> {
598 match url {
599 RemoteUrl::Bundle(path) => Ok(crate::bundle_remote::fetch_from_bundle_into(
600 repo, path, opts, progress, cancel,
601 )?),
602 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
603 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
604 let cfg = ConfigSet::load(&opts.environment, Some(repo.git_dir()), true)
605 .map_err(RemoteError::Library)?;
606 if opts.filter.is_none() {
607 if let Some(name) = opts.remote_name.as_deref().or(self.name.as_deref()) {
608 opts.filter = crate::partial_clone::stored_remote_filter(&cfg, name);
609 }
610 }
611 if crate::partial_clone::fetch_requires_upload_pack(
612 opts.filter.as_ref(),
613 &cfg,
614 opts.remote_name.as_deref().or(self.name.as_deref()),
615 ) {
616 let mut conn = crate::transport::connect_local_upload_pack(
617 &remote_git,
618 Service::UploadPack,
619 &connect,
620 )
621 .map_err(RemoteError::Library)?;
622 return Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?);
623 }
624 Ok(fetch_local_into(repo, &remote_git, opts, progress, cancel)?)
625 }
626 RemoteUrl::Http(u) | RemoteUrl::Https(u) => {
627 let client: Arc<dyn HttpClient> = Arc::from(http_client(http_factory, repo)?);
628 let transport = SmartHttpTransport::new(client);
629 let mut conn = transport.connect(u, Service::UploadPack, &connect)?;
630 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
631 }
632 RemoteUrl::Git(u) => {
633 let mut conn =
634 GitDaemonTransport::new().connect(u, Service::UploadPack, &connect)?;
635 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
636 }
637 RemoteUrl::Ssh(u) => {
638 let transport = ssh_transport(Some(repo), Some(&opts.environment));
639 let mut conn = transport.connect(u, Service::UploadPack, &connect)?;
640 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
641 }
642 }
643 }
644
645 #[allow(clippy::too_many_arguments)]
646 fn dispatch_push(
647 &self,
648 repo: &Repository,
649 url: &RemoteUrl,
650 refs: &[PushRefSpec],
651 opts: &PushOptions,
652 progress: &mut dyn Progress,
653 cancel: &dyn crate::progress::Cancel,
654 http_factory: Option<&dyn HttpClientFactory>,
655 connect: ConnectOptions,
656 ) -> RemoteResult<PushOutcome> {
657 match url {
658 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
659 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
660 Ok(push_local_from(repo, &remote_git, refs, opts, cancel)?)
661 }
662 RemoteUrl::Http(u) | RemoteUrl::Https(u) => {
663 let client: Arc<dyn HttpClient> = Arc::from(http_client(http_factory, repo)?);
664 let transport = SmartHttpTransport::new(client);
665 let mut conn = transport.connect(u, Service::ReceivePack, &connect)?;
666 Ok(push_remote_from(
667 repo, &mut *conn, refs, opts, progress, cancel,
668 )?)
669 }
670 RemoteUrl::Git(u) => {
671 let mut conn =
672 GitDaemonTransport::new().connect(u, Service::ReceivePack, &connect)?;
673 Ok(push_remote_from(
674 repo, &mut *conn, refs, opts, progress, cancel,
675 )?)
676 }
677 RemoteUrl::Ssh(u) => {
678 let transport = ssh_transport(Some(repo), None);
679 let mut conn = transport.connect(u, Service::ReceivePack, &connect)?;
680 Ok(push_remote_from(
681 repo, &mut *conn, refs, opts, progress, cancel,
682 )?)
683 }
684 RemoteUrl::Bundle(_) => Err(RemoteError::UnsupportedScheme),
685 }
686 }
687
688 fn list_refs_wire(
689 &self,
690 url: &RemoteUrl,
691 local_git_dir: &Path,
692 odb: Option<&Odb>,
693 opts: &ListRefsOptions,
694 http_factory: Option<&dyn HttpClientFactory>,
695 repo: Option<&Repository>,
696 ) -> RemoteResult<Vec<RemoteRef>> {
697 let wire = url.as_wire_url().ok_or(RemoteError::UnsupportedScheme)?;
698 let requested = opts.protocol_version.unwrap_or(2);
699 let connect_opts = ConnectOptions {
700 protocol_version: requested,
701 ..Default::default()
702 };
703 match url {
704 RemoteUrl::Http(_) | RemoteUrl::Https(_) => {
705 list_refs_http(wire, local_git_dir, odb, opts, http_factory, repo)
706 }
707 RemoteUrl::Git(_) => {
708 let mut conn =
709 GitDaemonTransport::new().connect(wire, Service::UploadPack, &connect_opts)?;
710 list_refs_from_connection(&mut *conn, local_git_dir, odb, opts)
711 }
712 RemoteUrl::Ssh(_) => {
713 let transport = ssh_transport(repo, None);
714 let mut conn = transport.connect(wire, Service::UploadPack, &connect_opts)?;
715 list_refs_from_connection(&mut *conn, local_git_dir, odb, opts)
716 }
717 RemoteUrl::Local(_) | RemoteUrl::File(_) | RemoteUrl::Bundle(_) => unreachable!(),
718 }
719 }
720}
721
722fn repo_context(repo: Option<&Repository>) -> (&Path, Option<&Path>, Option<Odb>) {
723 match repo {
724 Some(r) => (r.git_dir(), r.work_tree(), Some(r.odb().clone())),
725 None => (Path::new("."), None, None),
726 }
727}
728
729fn local_git_dir_from_url(
730 url: &RemoteUrl,
731 git_dir: &Path,
732 work_tree: Option<&Path>,
733) -> RemoteResult<PathBuf> {
734 let s = url.to_url_string();
735 let remote_git = resolve_local_remote_git_dir(&s, git_dir, work_tree);
736 crate::transfer::ensure_local_remote_repository(&remote_git)?;
737 Ok(remote_git)
738}
739
740fn ssh_transport(
741 repo: Option<&Repository>,
742 fetch_env: Option<&crate::environment::Environment>,
743) -> SshTransport {
744 let mut transport = SshTransport::new();
745 if let Some(r) = repo {
746 transport = transport.with_command_runner(r.command_runner());
747 }
748 let env = ssh_wire_environment(repo, fetch_env);
749 transport = transport.with_ssh_environment(std::sync::Arc::new(env.clone()));
750 if let Some(r) = repo {
751 if let Ok(config) = r.config() {
752 transport = transport.with_ssh_config(config);
753 }
754 } else if let Ok(config) = ConfigSet::load(&env, None, true) {
755 transport = transport.with_ssh_config(Arc::new(config));
756 }
757 transport
758}
759
760fn ssh_wire_environment(
763 repo: Option<&Repository>,
764 fetch_env: Option<&crate::environment::Environment>,
765) -> crate::environment::Environment {
766 if let Some(fe) = fetch_env {
767 if !fe.is_fetch_options_default() {
768 return fe.clone();
769 }
770 }
771 repo.map(|r| r.environment().clone())
772 .unwrap_or_else(crate::environment::Environment::capture_process)
773}
774
775fn http_client(
776 factory: Option<&dyn HttpClientFactory>,
777 repo: &Repository,
778) -> RemoteResult<Box<dyn HttpClient>> {
779 let config = repo.config().map_err(RemoteError::Library)?;
780 if let Some(f) = factory {
781 return f.create(config.as_ref());
782 }
783 #[cfg(feature = "http-ureq")]
784 {
785 DefaultHttpClientFactory::with_environment(repo.environment().clone())
786 .create(config.as_ref())
787 }
788 #[cfg(not(feature = "http-ureq"))]
789 {
790 let _ = config;
791 Err(RemoteError::HttpUnavailable)
792 }
793}
794
795fn list_refs_http(
796 repo_url: &str,
797 local_git_dir: &Path,
798 odb: Option<&Odb>,
799 opts: &ListRefsOptions,
800 http_factory: Option<&dyn HttpClientFactory>,
801 repo: Option<&Repository>,
802) -> RemoteResult<Vec<RemoteRef>> {
803 let config: Arc<ConfigSet> = if let Some(r) = repo {
804 r.config().map_err(RemoteError::Library)?
805 } else {
806 Arc::new(ConfigSet::new())
807 };
808 let client: Box<dyn HttpClient> = match http_factory {
809 Some(f) => f.create(config.as_ref())?,
810 None => {
811 #[cfg(feature = "http-ureq")]
812 {
813 repo.map_or_else(DefaultHttpClientFactory::default, |r| {
814 DefaultHttpClientFactory::with_environment(r.environment().clone())
815 })
816 .create(config.as_ref())?
817 }
818 #[cfg(not(feature = "http-ureq"))]
819 {
820 return Err(RemoteError::HttpUnavailable);
821 }
822 }
823 };
824
825 let protocol_header_value = opts
828 .protocol_version
829 .and_then(crate::protocol::client_git_protocol_header_value);
830 let git_protocol: Option<&str> = match opts.protocol_version {
831 None => None,
832 Some(0) => Some(""),
833 Some(_) => protocol_header_value.as_deref(),
834 };
835
836 let info_url = crate::transport::http::smart_info_refs_discovery_url(repo_url);
837 let want_v2 = opts.protocol_version.unwrap_or(2) >= 2;
838 let (body, final_url) = client
839 .get_with_final_url_exact(&info_url, git_protocol)
840 .map_err(RemoteError::Library)?;
841 let effective_base =
842 crate::transport::http::rebased_base_from_redirect(repo_url, final_url.as_deref())
843 .unwrap_or_else(|| repo_url.to_owned());
844 let (protocol_version, refs, caps, head_symref) =
845 if effective_base.trim_end_matches('/') != repo_url.trim_end_matches('/') {
846 client.reset_auth_after_redirect_rebase();
847 crate::transport::http::discover_upload_pack_for_list_refs(
848 client.as_ref(),
849 &effective_base,
850 git_protocol,
851 )
852 .map_err(RemoteError::Library)?
853 } else {
854 crate::transport::http::discover_upload_pack_for_list_refs_from_body(&body)
855 .map_err(RemoteError::Library)?
856 };
857
858 if protocol_version >= 2 && want_v2 {
859 let local_odb = odb.cloned().unwrap_or_else(|| open_odb(local_git_dir));
860 let req = build_list_refs_v2_request(&caps, &local_odb, opts)?;
861 let post_url = format!("{}/git-upload-pack", effective_base.trim_end_matches('/'));
862 let body = client
863 .as_ref()
864 .post(
865 &post_url,
866 "application/x-git-upload-pack-request",
867 "application/x-git-upload-pack-result",
868 &req,
869 git_protocol,
870 )
871 .map_err(RemoteError::Library)?;
872 let mut cur = Cursor::new(body);
873 parse_list_refs_v2_response(&mut cur, opts, head_symref)
874 } else {
875 list_refs_v0_advertisement(&refs, head_symref.as_deref(), odb, opts)
876 }
877}
878
879fn list_refs_from_connection(
880 conn: &mut dyn crate::transport::Connection,
881 local_git_dir: &Path,
882 odb: Option<&Odb>,
883 opts: &ListRefsOptions,
884) -> RemoteResult<Vec<RemoteRef>> {
885 let want_v2 = opts.protocol_version.unwrap_or(2) >= 2;
886 if conn.protocol_version() >= 2 && want_v2 {
887 list_refs_v2_connection(conn, local_git_dir, odb, opts)
888 } else {
889 let out =
890 list_refs_v0_advertisement(conn.advertised_refs(), conn.head_symref(), odb, opts)?;
891 crate::transport::send_client_flush(conn).map_err(RemoteError::Library)?;
892 Ok(out)
893 }
894}
895
896fn open_odb(git_dir: &Path) -> Odb {
897 crate::transfer::open_odb(git_dir)
898}
899
900fn list_refs_v2_connection(
901 conn: &mut dyn crate::transport::Connection,
902 local_git_dir: &Path,
903 odb: Option<&Odb>,
904 opts: &ListRefsOptions,
905) -> RemoteResult<Vec<RemoteRef>> {
906 let local_odb = odb.cloned().unwrap_or_else(|| open_odb(local_git_dir));
907 let req = build_list_refs_v2_request(conn.capabilities(), &local_odb, opts)?;
908 conn.writer().write_all(&req).map_err(Error::Io)?;
909 conn.writer().flush().map_err(Error::Io)?;
910 let discovery_head = conn.head_symref().map(str::to_owned);
911 let out = parse_list_refs_v2_response(conn.reader(), opts, discovery_head)?;
912 conn.finish_send();
913 Ok(out)
914}
915
916fn build_list_refs_v2_request(
917 server_caps: &[String],
918 local_odb: &Odb,
919 opts: &ListRefsOptions,
920) -> RemoteResult<Vec<u8>> {
921 use crate::fetch::v2_object_format;
922 use crate::pkt_line;
923 use crate::protocol_v2;
924
925 let object_format = v2_object_format(server_caps, local_odb);
926 let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
927 let mut req: Vec<u8> = Vec::new();
928 pkt_line::write_line(&mut req, "command=ls-refs").map_err(Error::Io)?;
929 if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
930 for line in &cap_echo {
931 pkt_line::write_line(&mut req, line).map_err(Error::Io)?;
932 }
933 } else {
934 for line in &cap_echo {
935 pkt_line::write_line(&mut req, line).map_err(Error::Io)?;
936 }
937 pkt_line::write_line(&mut req, &format!("object-format={object_format}"))
938 .map_err(Error::Io)?;
939 }
940 pkt_line::write_delim(&mut req).map_err(Error::Io)?;
941 if opts.symrefs {
942 pkt_line::write_line(&mut req, "symrefs").map_err(Error::Io)?;
943 if crate::protocol_v2::server_advertises_ls_refs_unborn(server_caps) {
944 pkt_line::write_line(&mut req, "unborn").map_err(Error::Io)?;
945 }
946 }
947 if opts.peel {
948 pkt_line::write_line(&mut req, "peel").map_err(Error::Io)?;
949 }
950 if list_refs_includes_head(opts) {
951 pkt_line::write_line(&mut req, "ref-prefix HEAD").map_err(Error::Io)?;
952 }
953 for prefix in list_ref_prefixes(opts) {
954 pkt_line::write_line(&mut req, &format!("ref-prefix {prefix}")).map_err(Error::Io)?;
955 }
956 pkt_line::write_flush(&mut req).map_err(Error::Io)?;
957 Ok(req)
958}
959
960fn list_refs_includes_head(opts: &ListRefsOptions) -> bool {
961 !opts.heads && !opts.tags && opts.prefixes.is_empty()
962}
963
964fn list_ref_prefixes(opts: &ListRefsOptions) -> Vec<String> {
965 if !opts.prefixes.is_empty() {
966 if opts.heads && !opts.tags {
967 return vec!["refs/heads/".to_owned()];
968 }
969 if opts.tags && !opts.heads {
970 return vec!["refs/tags/".to_owned()];
971 }
972 return vec!["refs/heads/".to_owned(), "refs/tags/".to_owned()];
973 }
974 let mut out = Vec::new();
975 let all = !opts.heads && !opts.tags;
976 if all || opts.heads {
977 out.push("refs/heads/".to_owned());
978 }
979 if all || opts.tags {
980 out.push("refs/tags/".to_owned());
981 }
982 out
983}
984
985fn parse_list_refs_v2_response(
986 reader: &mut dyn Read,
987 opts: &ListRefsOptions,
988 mut head_symref: Option<String>,
989) -> RemoteResult<Vec<RemoteRef>> {
990 use crate::fetch::parse_ls_refs_v2_line;
991 use crate::pkt_line;
992
993 let mut entries: Vec<RemoteRef> = Vec::new();
994 let mut peel_map: std::collections::HashMap<String, ObjectId> =
995 std::collections::HashMap::new();
996 let mut reader = reader;
997 loop {
998 match pkt_line::read_packet(&mut reader).map_err(Error::Io)? {
999 None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => break,
1000 Some(pkt_line::Packet::ResponseEnd) => break,
1001 Some(pkt_line::Packet::Data(line)) => {
1002 let line = line.trim_end_matches('\n');
1003 if let Some(msg) = line.strip_prefix("ERR ") {
1004 return Err(RemoteError::Library(Error::Message(format!(
1005 "remote error: {}",
1006 msg.trim_end()
1007 ))));
1008 }
1009 if let Some(t) = crate::fetch::parse_v2_ls_refs_unborn_head_symref(line) {
1010 head_symref = Some(t);
1011 continue;
1012 }
1013 let Some((name, oid, symref_target, peel)) = parse_ls_refs_v2_line(line) else {
1014 continue;
1015 };
1016 if name.ends_with("^{}") {
1017 continue;
1018 }
1019 if name == "HEAD" {
1020 let sym =
1021 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t));
1022 if let Some(t) = sym.clone() {
1023 head_symref = Some(t);
1024 }
1025 if list_refs_includes_head(opts) && ref_matches_list_opts("HEAD", opts) {
1026 entries.push(RemoteRef {
1027 name: "HEAD".to_owned(),
1028 oid,
1029 symref_target: if opts.symrefs { sym } else { None },
1030 });
1031 }
1032 continue;
1033 }
1034 if !crate::refs::is_valid_fetch_advertised_ref(&name) {
1035 continue;
1036 }
1037 if let Some(p) = peel {
1038 peel_map.insert(name.clone(), p);
1039 }
1040 let sym =
1041 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t));
1042 entries.push(RemoteRef {
1043 name,
1044 oid,
1045 symref_target: sym,
1046 });
1047 }
1048 }
1049 }
1050 finalize_list_refs_output(entries, peel_map, opts, head_symref, None)
1051}
1052
1053fn list_refs_v0_advertisement(
1054 advertised: &[(String, ObjectId)],
1055 head_symref: Option<&str>,
1056 odb: Option<&Odb>,
1057 opts: &ListRefsOptions,
1058) -> RemoteResult<Vec<RemoteRef>> {
1059 let mut entries: Vec<RemoteRef> = Vec::new();
1060 let mut peel_map: std::collections::HashMap<String, ObjectId> =
1061 std::collections::HashMap::new();
1062 for (name, oid) in advertised {
1063 if name.ends_with("^{}") {
1064 if let Some(base) = name.strip_suffix("^{}") {
1065 peel_map.insert(base.to_owned(), *oid);
1066 }
1067 continue;
1068 }
1069 entries.push(RemoteRef {
1070 name: name.clone(),
1071 oid: *oid,
1072 symref_target: None,
1073 });
1074 }
1075 finalize_list_refs_output(entries, peel_map, opts, head_symref.map(str::to_owned), odb)
1076}
1077
1078fn finalize_list_refs_output(
1079 mut entries: Vec<RemoteRef>,
1080 peel_map: std::collections::HashMap<String, ObjectId>,
1081 opts: &ListRefsOptions,
1082 head_symref: Option<String>,
1083 odb: Option<&Odb>,
1084) -> RemoteResult<Vec<RemoteRef>> {
1085 entries.retain(|e| remote_ref_matches_namespace(&e.name, opts));
1086 entries.retain(|e| patterns_match_ls_remote(&e.name, &opts.prefixes));
1087 if opts.peel {
1088 let mut peel_lines: Vec<RemoteRef> = Vec::new();
1089 for e in &entries {
1090 if !e.name.starts_with("refs/tags/") {
1091 continue;
1092 }
1093 let peeled = peel_map
1094 .get(&e.name)
1095 .copied()
1096 .or_else(|| odb.and_then(|o| o.peel_tag_target(e.oid).ok().flatten()));
1097 if let Some(p) = peeled {
1098 let peel_name = format!("{}^{{}}", e.name);
1099 if should_emit_peel_line(&peel_name, &opts.prefixes) {
1100 peel_lines.push(RemoteRef {
1101 name: peel_name,
1102 oid: p,
1103 symref_target: None,
1104 });
1105 }
1106 }
1107 }
1108 entries.extend(peel_lines);
1109 }
1110 if list_refs_includes_head(opts) {
1111 if let Some(sym) = head_symref {
1112 if let Some(oid) = entries.iter().find(|e| e.name == sym).map(|e| e.oid) {
1113 entries.push(RemoteRef {
1114 name: "HEAD".to_owned(),
1115 oid,
1116 symref_target: if opts.symrefs { Some(sym) } else { None },
1117 });
1118 }
1119 }
1120 }
1121 let query_options = RefQueryOptions {
1122 versionsort_suffixes: opts.versionsort_suffixes.clone(),
1123 ..RefQueryOptions::default()
1124 };
1125 sort_remote_refs(
1126 &mut entries,
1127 &opts.sort,
1128 &query_options.versionsort_suffixes,
1129 );
1130 if let Some(limit) = opts.limit {
1131 entries.truncate(limit);
1132 }
1133 Ok(entries)
1134}
1135
1136#[must_use]
1144pub fn query_remote_refs_in_memory(
1145 mut entries: Vec<RemoteRef>,
1146 opts: &ListRefsOptions,
1147 query_options: &RefQueryOptions,
1148) -> Vec<RemoteRef> {
1149 entries.retain(|e| remote_ref_matches_namespace(&e.name, opts));
1150 entries.retain(|e| patterns_match_ls_remote(&e.name, &opts.prefixes));
1151 sort_remote_refs(
1152 &mut entries,
1153 &opts.sort,
1154 &query_options.versionsort_suffixes,
1155 );
1156 if let Some(limit) = opts.limit {
1157 entries.truncate(limit);
1158 }
1159 entries
1160}
1161
1162fn should_emit_peel_line(peel_name: &str, patterns: &[String]) -> bool {
1163 if patterns.is_empty() {
1164 return true;
1165 }
1166 patterns_match_ls_remote(peel_name, patterns)
1167}
1168
1169fn sort_remote_refs(entries: &mut [RemoteRef], sort: &RefSort, versionsort_suffixes: &[String]) {
1170 entries.sort_by(|a, b| {
1171 let primary = match sort.key {
1172 SortKey::Name => a.name.cmp(&b.name),
1173 SortKey::Version => compare_version_names(&a.name, &b.name, versionsort_suffixes),
1174 SortKey::Date => a.name.cmp(&b.name),
1175 };
1176 let primary = if sort.reverse {
1177 primary.reverse()
1178 } else {
1179 primary
1180 };
1181 if primary != std::cmp::Ordering::Equal {
1182 return primary;
1183 }
1184 a.name.cmp(&b.name)
1185 });
1186}
1187
1188fn remote_ref_matches_namespace(refname: &str, opts: &ListRefsOptions) -> bool {
1189 if opts.heads || opts.tags {
1190 let is_branch = opts.heads && refname.starts_with("refs/heads/");
1191 let is_tag = opts.tags && refname.starts_with("refs/tags/");
1192 if refname == "HEAD" {
1193 return false;
1194 }
1195 if refname.ends_with("^{}") {
1196 return is_tag;
1197 }
1198 if !is_branch && !is_tag {
1199 return false;
1200 }
1201 }
1202 true
1203}
1204
1205fn ref_matches_list_opts(refname: &str, opts: &ListRefsOptions) -> bool {
1206 remote_ref_matches_namespace(refname, opts) && patterns_match_ls_remote(refname, &opts.prefixes)
1207}
1208
1209#[must_use]
1211pub fn ref_matches_ls_remote_patterns(refname: &str, patterns: &[String]) -> bool {
1212 patterns_match_ls_remote(refname, patterns)
1213}
1214
1215fn list_refs_from_bundle(path: &Path, opts: &ListRefsOptions) -> RemoteResult<Vec<RemoteRef>> {
1219 let bundle = crate::bundle::Bundle::open(path)
1220 .map_err(|e| RemoteError::Library(Error::Message(e.to_string())))?;
1221 let mut out = Vec::new();
1222 for (name, oid) in &bundle.header().refs {
1223 if !ref_matches_list_opts(name, opts) {
1224 continue;
1225 }
1226 if name != "HEAD" && !crate::refs::is_valid_fetch_advertised_ref(name) {
1227 continue;
1228 }
1229 out.push(RemoteRef {
1230 name: name.clone(),
1231 oid: *oid,
1232 symref_target: None,
1233 });
1234 }
1235 sort_remote_refs(&mut out, &opts.sort, &opts.versionsort_suffixes);
1236 if let Some(limit) = opts.limit {
1237 out.truncate(limit);
1238 }
1239 Ok(out)
1240}
1241
1242pub(crate) fn list_refs_from_git_dir_for_fetch(
1244 git_dir: &Path,
1245 odb: Option<&Odb>,
1246 refspecs: &[String],
1247) -> RemoteResult<Vec<RemoteRef>> {
1248 let prefixes = crate::fetch::fetch_ref_list_prefixes(refspecs);
1249 let opts = ListRefsOptions {
1250 symrefs: true,
1251 ..Default::default()
1252 };
1253 list_refs_from_git_dir_with_scan_prefixes(git_dir, odb, &opts, &prefixes)
1254}
1255
1256pub fn list_refs_from_git_dir(
1259 git_dir: &Path,
1260 odb: Option<&Odb>,
1261 opts: &ListRefsOptions,
1262) -> RemoteResult<Vec<RemoteRef>> {
1263 list_refs_from_git_dir_with_scan_prefixes(git_dir, odb, opts, &list_ref_prefixes(opts))
1264}
1265
1266fn list_refs_from_git_dir_with_scan_prefixes(
1267 git_dir: &Path,
1268 odb: Option<&Odb>,
1269 opts: &ListRefsOptions,
1270 scan_prefixes: &[String],
1271) -> RemoteResult<Vec<RemoteRef>> {
1272 let mut lazy_odb: Option<Odb> = None;
1273
1274 let mut entries = Vec::new();
1275 if list_refs_includes_head(opts) && ref_matches_list_opts("HEAD", opts) {
1276 let symref_target = if opts.symrefs {
1277 crate::refs::read_symbolic_ref(git_dir, "HEAD")
1278 .ok()
1279 .flatten()
1280 } else {
1281 None
1282 };
1283 match crate::refs::resolve_ref(git_dir, "HEAD") {
1284 Ok(head_oid) => {
1285 entries.push(RemoteRef {
1286 name: "HEAD".to_owned(),
1287 oid: head_oid,
1288 symref_target,
1289 });
1290 }
1291 Err(_) if symref_target.is_some() => {
1292 entries.push(RemoteRef {
1293 name: "HEAD".to_owned(),
1294 oid: ObjectId::zero(),
1295 symref_target,
1296 });
1297 }
1298 Err(_) => {}
1299 }
1300 }
1301
1302 for prefix in scan_prefixes {
1303 let listed = crate::refs::list_refs(git_dir, prefix).map_err(RemoteError::Library)?;
1304 for (name, oid) in listed {
1305 if let Some(branch_tail) = name.strip_prefix("refs/heads/") {
1306 if branch_tail.starts_with("refs/") {
1307 continue;
1308 }
1309 }
1310 if !ref_matches_list_opts(&name, opts) {
1311 continue;
1312 }
1313 entries.push(RemoteRef {
1314 name: name.clone(),
1315 oid,
1316 symref_target: None,
1317 });
1318 if opts.peel && name.starts_with("refs/tags/") {
1319 let peel_name = format!("{name}^{{}}");
1320 if should_emit_peel_line(&peel_name, &opts.prefixes) {
1321 let peel_odb = match odb {
1322 Some(o) => o,
1323 None => lazy_odb.get_or_insert_with(|| {
1324 Odb::new(&git_dir.join("objects"))
1325 .with_config_git_dir(git_dir.to_path_buf())
1326 }),
1327 };
1328 if let Ok(Some(peeled)) = peel_odb.peel_tag_target(oid) {
1329 entries.push(RemoteRef {
1330 name: peel_name,
1331 oid: peeled,
1332 symref_target: None,
1333 });
1334 }
1335 }
1336 }
1337 }
1338 }
1339
1340 sort_remote_refs(&mut entries, &opts.sort, &opts.versionsort_suffixes);
1341 if let Some(limit) = opts.limit {
1342 entries.truncate(limit);
1343 }
1344 Ok(entries)
1345}
1346
1347#[cfg(test)]
1348mod tests {
1349 use super::*;
1350
1351 fn remote_ref(name: &str, hex_digit: char) -> RemoteRef {
1352 RemoteRef {
1353 name: name.to_owned(),
1354 oid: ObjectId::from_hex(&hex_digit.to_string().repeat(40)).unwrap(),
1355 symref_target: None,
1356 }
1357 }
1358
1359 #[test]
1360 fn missing_urls_name_the_remote() {
1361 let err = parse_remote_url(" ").expect_err("empty");
1362 assert!(matches!(err, RemoteError::NoUrl { remote: None }));
1363 assert_eq!(err.to_string(), "remote URL is empty");
1364
1365 let config = ConfigSet::new();
1366 let err = Remote::from_config(&config, "upstream").expect_err("no url");
1367 assert!(matches!(&err, RemoteError::NoUrl { remote: Some(name) } if name == "upstream"));
1368 assert_eq!(err.to_string(), "remote 'upstream' has no URL configured");
1369 }
1370
1371 #[test]
1372 fn fold_peeled_refs_attaches_peeled_lines_and_keeps_order() {
1373 let mut head = remote_ref("HEAD", '1');
1374 head.symref_target = Some("refs/heads/main".to_owned());
1375 let refs = vec![
1376 head,
1377 remote_ref("refs/heads/main", '1'),
1378 remote_ref("refs/tags/light", '1'),
1379 remote_ref("refs/tags/v1", '2'),
1380 remote_ref("refs/tags/v1^{}", '1'),
1381 ];
1382 let folded = fold_peeled_refs(&refs);
1383 let names: Vec<&str> = folded.iter().map(|r| r.name.as_str()).collect();
1384 assert_eq!(
1385 names,
1386 ["HEAD", "refs/heads/main", "refs/tags/light", "refs/tags/v1"]
1387 );
1388 assert_eq!(folded[0].symref_target.as_deref(), Some("refs/heads/main"));
1389 assert_eq!(folded[2].peeled, None);
1390 assert_eq!(folded[3].oid, refs[3].oid);
1391 assert_eq!(folded[3].peeled, Some(refs[4].oid));
1392 }
1393
1394 #[test]
1395 fn peeled_tag_only_matches_peel_suffix() {
1396 assert_eq!(
1397 remote_ref("refs/tags/v1^{}", '1').peeled_tag(),
1398 Some("refs/tags/v1")
1399 );
1400 assert_eq!(remote_ref("refs/tags/v1", '1').peeled_tag(), None);
1401 }
1402
1403 #[test]
1404 fn remote_url_parse_table() {
1405 assert!(matches!(
1406 RemoteUrl::try_from("/abs/path").unwrap(),
1407 RemoteUrl::Local(_)
1408 ));
1409 assert!(matches!(
1410 RemoteUrl::try_from("file:///tmp/r.git").unwrap(),
1411 RemoteUrl::File(_)
1412 ));
1413 assert!(matches!(
1414 RemoteUrl::try_from("git://host/repo").unwrap(),
1415 RemoteUrl::Git(_)
1416 ));
1417 assert!(matches!(
1418 RemoteUrl::try_from("git@host:repo.git").unwrap(),
1419 RemoteUrl::Ssh(_)
1420 ));
1421 assert!(matches!(
1422 RemoteUrl::try_from("ssh://git@host/repo").unwrap(),
1423 RemoteUrl::Ssh(_)
1424 ));
1425 assert!(matches!(
1426 RemoteUrl::try_from("https://host/r.git").unwrap(),
1427 RemoteUrl::Https(_)
1428 ));
1429 assert!(matches!(
1430 RemoteUrl::try_from("ftp://x"),
1431 Err(RemoteError::UnsupportedScheme)
1432 ));
1433 }
1434
1435 #[test]
1436 fn pattern_matches_empty_allows_all() {
1437 assert!(ref_matches_ls_remote_patterns("refs/heads/main", &[]));
1438 assert!(ref_matches_ls_remote_patterns("HEAD", &[]));
1439 }
1440
1441 #[test]
1442 fn pattern_matches_exact() {
1443 let pats = vec!["HEAD".to_owned()];
1444 assert!(ref_matches_ls_remote_patterns("HEAD", &pats));
1445 assert!(!ref_matches_ls_remote_patterns("refs/heads/main", &pats));
1446 }
1447
1448 #[test]
1449 fn pattern_matches_suffix_component() {
1450 let pats = vec!["main".to_owned()];
1451 assert!(ref_matches_ls_remote_patterns("refs/heads/main", &pats));
1452 assert!(!ref_matches_ls_remote_patterns("refs/heads/notmain", &pats));
1453 assert!(!ref_matches_ls_remote_patterns("main-branch", &pats));
1454 }
1455
1456 #[test]
1457 fn query_remote_refs_in_memory_empty_pattern_matches_nothing() {
1458 let entries = vec![
1459 RemoteRef {
1460 name: "refs/heads/main".into(),
1461 oid: ObjectId::from_hex("1".repeat(40).as_str()).unwrap(),
1462 symref_target: None,
1463 },
1464 RemoteRef {
1465 name: "refs/tags/v1".into(),
1466 oid: ObjectId::from_hex("2".repeat(40).as_str()).unwrap(),
1467 symref_target: None,
1468 },
1469 ];
1470 let opts = ListRefsOptions {
1471 prefixes: vec![String::new()],
1472 ..Default::default()
1473 };
1474 let out = query_remote_refs_in_memory(entries, &opts, &RefQueryOptions::default());
1475 assert!(out.is_empty());
1476 }
1477
1478 #[test]
1479 fn query_remote_refs_in_memory_version_sort_and_limit() {
1480 let entries = vec![
1481 RemoteRef {
1482 name: "refs/tags/v1.10".into(),
1483 oid: ObjectId::from_hex("1".repeat(40).as_str()).unwrap(),
1484 symref_target: None,
1485 },
1486 RemoteRef {
1487 name: "refs/tags/v1.2".into(),
1488 oid: ObjectId::from_hex("2".repeat(40).as_str()).unwrap(),
1489 symref_target: None,
1490 },
1491 RemoteRef {
1492 name: "refs/tags/v1.1".into(),
1493 oid: ObjectId::from_hex("3".repeat(40).as_str()).unwrap(),
1494 symref_target: None,
1495 },
1496 ];
1497 let opts = ListRefsOptions {
1498 sort: RefSort {
1499 key: SortKey::Version,
1500 reverse: false,
1501 },
1502 limit: Some(2),
1503 ..Default::default()
1504 };
1505 let out = query_remote_refs_in_memory(entries, &opts, &RefQueryOptions::default());
1506 assert_eq!(
1507 out.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(),
1508 vec!["refs/tags/v1.1", "refs/tags/v1.2"]
1509 );
1510 }
1511
1512 #[test]
1513 fn list_opts_heads_excludes_head() {
1514 let heads_only = super::ListRefsOptions {
1515 heads: true,
1516 ..Default::default()
1517 };
1518 assert!(!super::ref_matches_list_opts("HEAD", &heads_only));
1519 assert!(super::ref_matches_list_opts("refs/heads/main", &heads_only));
1520 assert!(!super::ref_matches_list_opts("refs/tags/v1", &heads_only));
1521 }
1522
1523 #[test]
1524 fn insteadof_rewrite_applied_in_from_config() {
1525 let mut cfg = ConfigSet::new();
1526 cfg.add_command_override("url.https://github.com/.insteadOf", "gh:")
1527 .unwrap();
1528 cfg.add_command_override("remote.origin.url", "gh:org/repo.git")
1529 .unwrap();
1530 let remote = Remote::from_config(&cfg, "origin").unwrap();
1531 assert!(matches!(remote.fetch_url(), RemoteUrl::Https(_)));
1532 }
1533
1534 #[test]
1535 fn push_insteadof_applied_when_pushurl_absent() {
1536 let mut cfg = ConfigSet::new();
1537 cfg.add_command_override("url.ssh://push.example/.pushInsteadOf", "short:")
1538 .unwrap();
1539 cfg.add_command_override("remote.origin.url", "short:org/repo.git")
1540 .unwrap();
1541 let remote = Remote::from_config(&cfg, "origin").unwrap();
1542 assert_eq!(
1543 remote.push_url().to_url_string(),
1544 "ssh://push.example/org/repo.git"
1545 );
1546 assert_eq!(remote.fetch_url().to_url_string(), "short:org/repo.git");
1547 }
1548
1549 #[test]
1550 fn list_ref_prefixes_broad_for_ls_remote_patterns() {
1551 let opts = super::ListRefsOptions {
1552 prefixes: vec!["main".to_owned()],
1553 ..Default::default()
1554 };
1555 let prefixes = super::list_ref_prefixes(&opts);
1556 assert_eq!(
1557 prefixes,
1558 vec!["refs/heads/".to_owned(), "refs/tags/".to_owned()]
1559 );
1560 assert!(super::ref_matches_list_opts("refs/heads/main", &opts));
1561 assert!(!super::ref_matches_list_opts("refs/heads/topic", &opts));
1562 }
1563}