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