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(crate) 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_preflight(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>>;
319}
320
321pub(crate) struct PreflightHttpClientFactory<'a> {
324 inner: Option<&'a dyn HttpClientFactory>,
325 #[cfg(feature = "http-ureq")]
326 default: DefaultHttpClientFactory,
327}
328
329impl<'a> PreflightHttpClientFactory<'a> {
330 pub 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 PreflightHttpClientFactory<'_> {
343 fn create(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>> {
344 if let Some(inner) = self.inner {
345 return inner.create_for_preflight(config);
346 }
347 #[cfg(feature = "http-ureq")]
348 {
349 self.default.create_for_preflight(config)
350 }
351 #[cfg(not(feature = "http-ureq"))]
352 {
353 let _ = config;
354 Err(RemoteError::HttpUnavailable)
355 }
356 }
357
358 fn create_for_preflight(&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 &self,
423 config: &ConfigSet,
424 provider: Box<dyn CredentialProvider + Send + Sync>,
425 ) -> RemoteResult<Box<dyn HttpClient>> {
426 let environment = match &self.environment {
427 Some(env) => env,
428 None => &crate::environment::Environment::empty(),
429 };
430 let client = UreqHttpClient::from_config_with_environment(config, environment)
431 .map_err(|e| RemoteError::InvalidUrl(e.to_string()))?
432 .with_credential_provider(provider);
433 Ok(Box::new(client))
434 }
435}
436
437#[cfg(feature = "http-ureq")]
438impl HttpClientFactory for DefaultHttpClientFactory {
439 fn create(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>> {
440 self.client(config, Box::new(self.credential_provider(config)))
441 }
442
443 fn create_for_preflight(&self, config: &ConfigSet) -> RemoteResult<Box<dyn HttpClient>> {
444 self.client(
445 config,
446 Box::new(KeepRejectedCredentials(self.credential_provider(config))),
447 )
448 }
449}
450
451#[derive(Clone, Debug)]
453pub struct Remote {
454 pub name: Option<String>,
456 fetch_url: RemoteUrl,
457 push_url: Option<RemoteUrl>,
458 pub fetch_refspecs: Vec<String>,
460}
461
462impl Remote {
463 pub fn from_config(config: &ConfigSet, name: &str) -> RemoteResult<Self> {
471 let raw_url = config
472 .get(&format!("remote.{name}.url"))
473 .filter(|u| !u.trim().is_empty())
474 .ok_or_else(|| RemoteError::NoUrl {
475 remote: Some(name.to_owned()),
476 })?;
477 let fetch_url = url_rewrite::rewrite_fetch_url(config, &raw_url);
478 let push_source = config
479 .get(&format!("remote.{name}.pushurl"))
480 .filter(|u| !u.trim().is_empty())
481 .unwrap_or_else(|| raw_url.clone());
482 let push_url = url_rewrite::rewrite_push_url(config, push_source.as_str());
483 let mut fetch_refspecs = config.get_all(&format!("remote.{name}.fetch"));
484 fetch_refspecs.retain(|s| !s.trim().is_empty());
485 if fetch_refspecs.is_empty() {
486 fetch_refspecs = vec![format!("+refs/heads/*:refs/remotes/{name}/*")];
487 }
488 Ok(Self {
489 name: Some(name.to_owned()),
490 fetch_url: RemoteUrl::try_from(fetch_url.as_str())?,
491 push_url: Some(RemoteUrl::try_from(push_url.as_str())?),
492 fetch_refspecs,
493 })
494 }
495
496 pub const ANONYMOUS_FETCH_REFSPEC: &'static str = "refs/heads/*:";
499
500 pub fn from_url(url: &str) -> RemoteResult<Self> {
509 Ok(Self {
510 name: None,
511 fetch_url: RemoteUrl::try_from(url)?,
512 push_url: None,
513 fetch_refspecs: vec![Self::ANONYMOUS_FETCH_REFSPEC.to_owned()],
514 })
515 }
516
517 #[must_use]
519 pub fn fetch_url(&self) -> &RemoteUrl {
520 &self.fetch_url
521 }
522
523 #[must_use]
525 pub fn push_url(&self) -> &RemoteUrl {
526 self.push_url.as_ref().unwrap_or(&self.fetch_url)
527 }
528
529 pub fn fetch(
540 &self,
541 repo: &Repository,
542 mut opts: FetchOptions,
543 progress: &mut dyn Progress,
544 cancel: &dyn crate::progress::Cancel,
545 http_factory: Option<&dyn HttpClientFactory>,
546 ) -> RemoteResult<FetchOutcome> {
547 if opts.configured_refspecs.is_empty() {
548 opts.configured_refspecs = self.fetch_refspecs.clone();
549 }
550 if opts.refspecs.is_empty() {
551 opts.refspecs = self.fetch_refspecs.clone();
552 }
553 if opts.remote_name.is_none() {
554 opts.remote_name = self.name.clone();
555 }
556 if opts.fetch_source.is_none() {
557 opts.fetch_source = Some(self.fetch_url.to_url_string());
558 }
559 if opts.diagnostics.is_none() {
560 opts.diagnostics = Some(repo.diagnostics());
561 opts.network_trace = repo.network_trace_enabled();
562 }
563 let protocol_version = upload_pack_protocol_version(opts.http_protocol_version, repo)?;
564 if opts.http_protocol_version.is_none() {
565 opts.http_protocol_version = Some(protocol_version);
566 }
567 let (stack_watch, _wait_guard) = crate::progress::HttpWaitGuard::bind(cancel);
568 let mut connect = ConnectOptions {
569 protocol_version,
570 diagnostics: opts.diagnostics.clone(),
571 network_trace: opts.network_trace,
572 cancel: Some(crate::progress::connect_cancel_arc(
573 cancel,
574 std::sync::Arc::clone(&stack_watch),
575 )),
576 stack_watch,
577 ..Default::default()
578 };
579 connect.bind_stack_cancel(cancel);
580 self.dispatch_fetch(
581 repo,
582 &self.fetch_url,
583 &mut opts,
584 progress,
585 cancel,
586 http_factory,
587 connect,
588 )
589 }
590
591 pub fn push(
607 &self,
608 repo: &Repository,
609 refs: &[PushRefSpec],
610 mut opts: PushOptions,
611 progress: &mut dyn Progress,
612 cancel: &dyn crate::progress::Cancel,
613 http_factory: Option<&dyn HttpClientFactory>,
614 ) -> RemoteResult<PushOutcome> {
615 if opts.tracking_remote.is_none() {
616 opts.tracking_remote = self.name.clone();
617 }
618 if opts.resumable_threshold.is_none() {
619 opts.resumable_threshold = repo
620 .config()
621 .ok()
622 .and_then(|c| c.get_i64("push.bundleThreshold").and_then(|r| r.ok()))
623 .and_then(|v| u64::try_from(v).ok());
624 }
625 let url = pre_push_hook_url(self.push_url());
626 let gate = PrePushGate {
627 repo,
628 remote: self.name.as_deref().unwrap_or(&url),
629 url: &url,
630 };
631 self.dispatch_push(
632 repo,
633 self.push_url(),
634 refs,
635 &opts,
636 (!opts.no_verify).then_some(&gate),
637 progress,
638 cancel,
639 http_factory,
640 {
641 let (stack_watch, _wait_guard) = crate::progress::HttpWaitGuard::bind(cancel);
642 let mut connect = ConnectOptions {
643 protocol_version: 0,
644 diagnostics: Some(repo.diagnostics()),
645 network_trace: repo.network_trace_enabled(),
646 cancel: Some(crate::progress::connect_cancel_arc(
647 cancel,
648 std::sync::Arc::clone(&stack_watch),
649 )),
650 stack_watch,
651 ..Default::default()
652 };
653 connect.bind_stack_cancel(cancel);
654 connect
655 },
656 )
657 }
658
659 #[must_use]
678 pub(crate) fn seed_from_bundle_uri(
679 &self,
680 repo: &Repository,
681 cache_dir: &Path,
682 progress: &mut dyn Progress,
683 cancel: &dyn crate::progress::Cancel,
684 http_factory: Option<&dyn HttpClientFactory>,
685 ) -> crate::bundle_uri::BundleSeed {
686 use crate::bundle_uri::BundleSeed;
687 let (RemoteUrl::Http(url) | RemoteUrl::Https(url)) = &self.fetch_url else {
688 return BundleSeed::NotOffered;
689 };
690 let enabled = repo
691 .config()
692 .ok()
693 .is_none_or(|c| c.bool_or("transfer.bundleURI", true));
694 if !enabled {
695 return BundleSeed::NotOffered;
696 }
697 let probe = PreflightHttpClientFactory::new(http_factory, repo);
698 let client = match http_client(Some(&probe), repo) {
699 Ok(c) => c,
700 Err(e) => {
701 return BundleSeed::Failed {
702 uri: None,
703 reason: e.to_string(),
704 }
705 }
706 };
707 let (stack_watch, _wait_guard) = crate::progress::HttpWaitGuard::bind(cancel);
708 let mut connect = ConnectOptions {
709 cancel: Some(crate::progress::connect_cancel_arc(
710 cancel,
711 std::sync::Arc::clone(&stack_watch),
712 )),
713 stack_watch,
714 ..Default::default()
715 };
716 connect.bind_stack_cancel(cancel);
717 client.set_operation_cancel(connect.cancel.clone());
718 crate::bundle_uri::seed_from_bundle_uri(
719 repo,
720 std::sync::Arc::from(client),
721 url,
722 cache_dir,
723 progress,
724 cancel,
725 )
726 }
727
728 pub fn list_refs(
736 &self,
737 repo: Option<&Repository>,
738 opts: &ListRefsOptions,
739 http_factory: Option<&dyn HttpClientFactory>,
740 ) -> RemoteResult<Vec<RemoteRef>> {
741 let (git_dir, work_tree, odb) = repo_context(repo);
742 match &self.fetch_url {
743 RemoteUrl::Bundle(path) => list_refs_from_bundle(path, opts),
744 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
745 let remote_git = local_git_dir_from_url(&self.fetch_url, git_dir, work_tree)?;
746 list_refs_from_git_dir(&remote_git, None, opts)
747 }
748 wire => self.list_refs_wire(wire, git_dir, odb.as_ref(), opts, http_factory, repo),
749 }
750 }
751
752 pub fn list_push_refs(
763 &self,
764 repo: &Repository,
765 http_factory: Option<&dyn HttpClientFactory>,
766 ) -> RemoteResult<Vec<RemoteRef>> {
767 let url = self.push_url();
768 match url {
769 RemoteUrl::Bundle(_) => Err(RemoteError::UnsupportedScheme),
770 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
771 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
772 let refs =
773 crate::refs::list_refs(&remote_git, "refs/").map_err(RemoteError::Library)?;
774 Ok(refs
775 .into_iter()
776 .map(|(name, oid)| RemoteRef {
777 name,
778 oid,
779 symref_target: None,
780 })
781 .collect())
782 }
783 wire => self.list_refs_wire(
784 wire,
785 repo.git_dir(),
786 Some(repo.odb()),
787 &ListRefsOptions::default(),
788 http_factory,
789 Some(repo),
790 ),
791 }
792 }
793
794 #[allow(clippy::too_many_arguments)]
795 fn dispatch_fetch(
796 &self,
797 repo: &Repository,
798 url: &RemoteUrl,
799 opts: &mut FetchOptions,
800 progress: &mut dyn Progress,
801 cancel: &dyn crate::progress::Cancel,
802 http_factory: Option<&dyn HttpClientFactory>,
803 connect: ConnectOptions,
804 ) -> RemoteResult<FetchOutcome> {
805 match url {
806 RemoteUrl::Bundle(path) => Ok(crate::bundle_remote::fetch_from_bundle_into(
807 repo, path, opts, progress, cancel,
808 )?),
809 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
810 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
811 let cfg = ConfigSet::load(&opts.environment, Some(repo.git_dir()), true)
812 .map_err(RemoteError::Library)?;
813 if opts.filter.is_none() {
814 if let Some(name) = opts.remote_name.as_deref().or(self.name.as_deref()) {
815 opts.filter = crate::partial_clone::stored_remote_filter(&cfg, name);
816 }
817 }
818 if opts.auto_filter
824 || crate::partial_clone::fetch_requires_upload_pack(
825 opts.filter.as_ref(),
826 &cfg,
827 opts.remote_name.as_deref().or(self.name.as_deref()),
828 )
829 || local_source_has_promisor_remotes(&remote_git)
830 {
831 let client_env = if opts.environment.is_fetch_options_default() {
832 repo.environment()
833 } else {
834 &opts.environment
835 };
836 let mut conn = crate::transport::connect_local_upload_pack(
837 &remote_git,
838 Service::UploadPack,
839 &connect,
840 client_env,
841 repo.caches().promisor_fetcher(),
842 )
843 .map_err(RemoteError::Library)?;
844 let fetched = fetch_remote_into(repo, &mut *conn, opts, progress, cancel);
845 conn.finish().map_err(RemoteError::Library)?;
849 return Ok(fetched?);
850 }
851 Ok(fetch_local_into(repo, &remote_git, opts, progress, cancel)?)
852 }
853 RemoteUrl::Http(u) | RemoteUrl::Https(u) => {
854 let client = http_client_for_operation(http_factory, repo, connect.cancel.clone())?;
855 let transport = SmartHttpTransport::new(client);
856 let mut conn =
857 transport.connect_with_owner(u, Service::UploadPack, &connect, cancel)?;
858 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
859 }
860 RemoteUrl::Git(u) => {
861 let mut conn =
862 GitDaemonTransport::new().connect(u, Service::UploadPack, &connect)?;
863 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
864 }
865 RemoteUrl::Ssh(u) => {
866 let transport = ssh_transport(Some(repo), Some(&opts.environment));
867 let mut conn = transport.connect(u, Service::UploadPack, &connect)?;
868 Ok(fetch_remote_into(repo, &mut *conn, opts, progress, cancel)?)
869 }
870 }
871 }
872
873 #[allow(clippy::too_many_arguments)]
874 fn dispatch_push(
875 &self,
876 repo: &Repository,
877 url: &RemoteUrl,
878 refs: &[PushRefSpec],
879 opts: &PushOptions,
880 pre_push: Option<&PrePushGate<'_>>,
881 progress: &mut dyn Progress,
882 cancel: &dyn crate::progress::Cancel,
883 http_factory: Option<&dyn HttpClientFactory>,
884 connect: ConnectOptions,
885 ) -> RemoteResult<PushOutcome> {
886 let mut over_wire = |conn: &mut dyn crate::transport::Connection| {
887 push_remote_from(repo, conn, refs, opts, progress, cancel, pre_push)
888 };
889 match url {
890 RemoteUrl::Local(_) | RemoteUrl::File(_) => {
891 let remote_git = local_git_dir_from_url(url, repo.git_dir(), repo.work_tree())?;
892 Ok(push_local_from(
893 repo,
894 &remote_git,
895 refs,
896 opts,
897 progress,
898 cancel,
899 pre_push,
900 )?)
901 }
902 RemoteUrl::Http(u) | RemoteUrl::Https(u) => {
903 let client = http_client_for_operation(http_factory, repo, connect.cancel.clone())?;
904 let transport = SmartHttpTransport::new(Arc::clone(&client));
905 let mut conn =
906 transport.connect_with_owner(u, Service::ReceivePack, &connect, cancel)?;
907 Ok(crate::push::push_remote_with(
908 repo,
909 &mut *conn,
910 refs,
911 opts,
912 progress,
913 cancel,
914 Some(crate::push::BundlePushTarget {
915 client: client.as_ref(),
916 repo_url: u,
917 }),
918 pre_push,
919 )?)
920 }
921 RemoteUrl::Git(u) => {
922 let mut conn =
923 GitDaemonTransport::new().connect(u, Service::ReceivePack, &connect)?;
924 Ok(over_wire(&mut *conn)?)
925 }
926 RemoteUrl::Ssh(u) => {
927 let transport = ssh_transport(Some(repo), None);
928 let mut conn = transport.connect(u, Service::ReceivePack, &connect)?;
929 Ok(over_wire(&mut *conn)?)
930 }
931 RemoteUrl::Bundle(_) => Err(RemoteError::UnsupportedScheme),
932 }
933 }
934
935 fn list_refs_wire(
936 &self,
937 url: &RemoteUrl,
938 local_git_dir: &Path,
939 odb: Option<&Odb>,
940 opts: &ListRefsOptions,
941 http_factory: Option<&dyn HttpClientFactory>,
942 repo: Option<&Repository>,
943 ) -> RemoteResult<Vec<RemoteRef>> {
944 let wire = url.as_wire_url().ok_or(RemoteError::UnsupportedScheme)?;
945 let mut opts = opts.clone();
946 if opts.protocol_version.is_none() {
947 opts.protocol_version = Some(list_refs_protocol_version(None, repo)?);
948 }
949 let requested = opts.protocol_version.unwrap_or(2);
950 let connect_opts = ConnectOptions {
951 protocol_version: requested,
952 ..Default::default()
953 };
954 match url {
955 RemoteUrl::Http(_) | RemoteUrl::Https(_) => {
956 list_refs_http(wire, local_git_dir, odb, &opts, http_factory, repo)
957 }
958 RemoteUrl::Git(_) => {
959 let mut conn =
960 GitDaemonTransport::new().connect(wire, Service::UploadPack, &connect_opts)?;
961 list_refs_from_connection(&mut *conn, local_git_dir, odb, &opts)
962 }
963 RemoteUrl::Ssh(_) => {
964 let transport = ssh_transport(repo, None);
965 let mut conn = transport.connect(wire, Service::UploadPack, &connect_opts)?;
966 list_refs_from_connection(&mut *conn, local_git_dir, odb, &opts)
967 }
968 RemoteUrl::Local(_) | RemoteUrl::File(_) | RemoteUrl::Bundle(_) => unreachable!(),
969 }
970 }
971}
972
973fn pre_push_hook_url(url: &RemoteUrl) -> String {
975 match url {
976 RemoteUrl::File(path) => format!("file://{}", path.to_string_lossy()),
977 other => other.to_url_string(),
978 }
979}
980
981fn repo_context(repo: Option<&Repository>) -> (&Path, Option<&Path>, Option<Odb>) {
982 match repo {
983 Some(r) => (r.git_dir(), r.work_tree(), Some(r.odb().clone())),
984 None => (Path::new("."), None, None),
985 }
986}
987
988fn upload_pack_protocol_version(explicit: Option<u8>, repo: &Repository) -> RemoteResult<u8> {
989 if let Some(v) = explicit {
990 return Ok(v);
991 }
992 crate::protocol::try_effective_client_protocol_version_from_config(
993 repo.config().map_err(RemoteError::Library)?.as_ref(),
994 )
995 .map_err(RemoteError::Library)
996}
997
998fn list_refs_protocol_version(explicit: Option<u8>, repo: Option<&Repository>) -> RemoteResult<u8> {
999 if let Some(v) = explicit {
1000 return Ok(v);
1001 }
1002 if let Some(repo) = repo {
1003 return upload_pack_protocol_version(None, repo);
1004 }
1005 Ok(2)
1006}
1007
1008fn local_source_has_promisor_remotes(git_dir: &Path) -> bool {
1011 ConfigSet::load_repo_local_only(git_dir)
1012 .is_ok_and(|cfg| crate::promisor::repo_treats_promisor_packs(git_dir, &cfg))
1013}
1014
1015fn local_git_dir_from_url(
1016 url: &RemoteUrl,
1017 git_dir: &Path,
1018 work_tree: Option<&Path>,
1019) -> RemoteResult<PathBuf> {
1020 let s = url.to_url_string();
1021 let remote_git = resolve_local_remote_git_dir(&s, git_dir, work_tree);
1022 crate::transfer::ensure_local_remote_repository(&remote_git)?;
1023 Ok(remote_git)
1024}
1025
1026fn ssh_transport(
1027 repo: Option<&Repository>,
1028 fetch_env: Option<&crate::environment::Environment>,
1029) -> SshTransport {
1030 let mut transport = SshTransport::new();
1031 if let Some(r) = repo {
1032 transport = transport.with_command_runner(r.command_runner());
1033 }
1034 let env = ssh_wire_environment(repo, fetch_env);
1035 transport = transport.with_ssh_environment(std::sync::Arc::new(env.clone()));
1036 if let Some(r) = repo {
1037 if let Ok(config) = r.config() {
1038 transport = transport.with_ssh_config(config);
1039 }
1040 } else if let Ok(config) = ConfigSet::load(&env, None, true) {
1041 transport = transport.with_ssh_config(Arc::new(config));
1042 }
1043 transport
1044}
1045
1046fn ssh_wire_environment(
1049 repo: Option<&Repository>,
1050 fetch_env: Option<&crate::environment::Environment>,
1051) -> crate::environment::Environment {
1052 if let Some(fe) = fetch_env {
1053 if !fe.is_fetch_options_default() {
1054 return fe.clone();
1055 }
1056 }
1057 repo.map(|r| r.environment().clone())
1058 .unwrap_or_else(crate::environment::Environment::capture_process)
1059}
1060
1061#[cfg(feature = "http-ureq")]
1062fn http_client_for_operation(
1063 factory: Option<&dyn HttpClientFactory>,
1064 repo: &Repository,
1065 cancel: Option<std::sync::Arc<dyn crate::progress::Cancel + Send + Sync>>,
1066) -> RemoteResult<Arc<dyn HttpClient>> {
1067 let client = http_client(factory, repo)?;
1068 client.set_operation_cancel(cancel);
1069 Ok(Arc::from(client))
1070}
1071
1072#[cfg(not(feature = "http-ureq"))]
1073fn http_client_for_operation(
1074 factory: Option<&dyn HttpClientFactory>,
1075 repo: &Repository,
1076 _cancel: Option<std::sync::Arc<dyn crate::progress::Cancel + Send + Sync>>,
1077) -> RemoteResult<Arc<dyn HttpClient>> {
1078 Ok(Arc::from(http_client(factory, repo)?))
1079}
1080
1081fn http_client(
1082 factory: Option<&dyn HttpClientFactory>,
1083 repo: &Repository,
1084) -> RemoteResult<Box<dyn HttpClient>> {
1085 let config = repo.config().map_err(RemoteError::Library)?;
1086 if let Some(f) = factory {
1087 return f.create(config.as_ref());
1088 }
1089 #[cfg(feature = "http-ureq")]
1090 {
1091 DefaultHttpClientFactory::with_environment(repo.environment().clone())
1092 .create(config.as_ref())
1093 }
1094 #[cfg(not(feature = "http-ureq"))]
1095 {
1096 let _ = config;
1097 Err(RemoteError::HttpUnavailable)
1098 }
1099}
1100
1101fn list_refs_http(
1102 repo_url: &str,
1103 local_git_dir: &Path,
1104 odb: Option<&Odb>,
1105 opts: &ListRefsOptions,
1106 http_factory: Option<&dyn HttpClientFactory>,
1107 repo: Option<&Repository>,
1108) -> RemoteResult<Vec<RemoteRef>> {
1109 let config: Arc<ConfigSet> = if let Some(r) = repo {
1110 r.config().map_err(RemoteError::Library)?
1111 } else {
1112 Arc::new(ConfigSet::new())
1113 };
1114 let client: Box<dyn HttpClient> = match http_factory {
1115 Some(f) => f.create(config.as_ref())?,
1116 None => {
1117 #[cfg(feature = "http-ureq")]
1118 {
1119 repo.map_or_else(DefaultHttpClientFactory::default, |r| {
1120 DefaultHttpClientFactory::with_environment(r.environment().clone())
1121 })
1122 .create(config.as_ref())?
1123 }
1124 #[cfg(not(feature = "http-ureq"))]
1125 {
1126 return Err(RemoteError::HttpUnavailable);
1127 }
1128 }
1129 };
1130
1131 let protocol_header_value = opts
1134 .protocol_version
1135 .and_then(crate::protocol::client_git_protocol_header_value);
1136 let git_protocol: Option<&str> = match opts.protocol_version {
1137 None => None,
1138 Some(0) => Some(""),
1139 Some(_) => protocol_header_value.as_deref(),
1140 };
1141
1142 let info_url = crate::transport::http::smart_info_refs_discovery_url(repo_url);
1143 let want_v2 = opts.protocol_version.unwrap_or(2) >= 2;
1144 let (body, final_url) = client
1145 .get_with_final_url_exact(&info_url, git_protocol)
1146 .map_err(RemoteError::Library)?;
1147 let effective_base =
1148 crate::transport::http::rebased_base_from_redirect(repo_url, final_url.as_deref())
1149 .unwrap_or_else(|| repo_url.to_owned());
1150 let (protocol_version, refs, caps, head_symref) =
1151 if effective_base.trim_end_matches('/') != repo_url.trim_end_matches('/') {
1152 client.reset_auth_after_redirect_rebase();
1153 crate::transport::http::discover_upload_pack_for_list_refs(
1154 client.as_ref(),
1155 &effective_base,
1156 git_protocol,
1157 )
1158 .map_err(RemoteError::Library)?
1159 } else {
1160 crate::transport::http::discover_upload_pack_for_list_refs_from_body(&body)
1161 .map_err(RemoteError::Library)?
1162 };
1163
1164 if protocol_version >= 2 && want_v2 {
1165 let local_odb = odb.cloned().unwrap_or_else(|| open_odb(local_git_dir));
1166 let req = build_list_refs_v2_request(&caps, &local_odb, opts)?;
1167 let post_url = format!("{}/git-upload-pack", effective_base.trim_end_matches('/'));
1168 let body = client
1169 .as_ref()
1170 .post(
1171 &post_url,
1172 "application/x-git-upload-pack-request",
1173 "application/x-git-upload-pack-result",
1174 &req,
1175 git_protocol,
1176 )
1177 .map_err(RemoteError::Library)?;
1178 let mut cur = Cursor::new(body);
1179 parse_list_refs_v2_response(&mut cur, opts, head_symref)
1180 } else {
1181 list_refs_v0_advertisement(&refs, head_symref.as_deref(), odb, opts)
1182 }
1183}
1184
1185fn list_refs_from_connection(
1186 conn: &mut dyn crate::transport::Connection,
1187 local_git_dir: &Path,
1188 odb: Option<&Odb>,
1189 opts: &ListRefsOptions,
1190) -> RemoteResult<Vec<RemoteRef>> {
1191 let want_v2 = opts.protocol_version.unwrap_or(2) >= 2;
1192 if conn.protocol_version() >= 2 && want_v2 {
1193 list_refs_v2_connection(conn, local_git_dir, odb, opts)
1194 } else {
1195 let out =
1196 list_refs_v0_advertisement(conn.advertised_refs(), conn.head_symref(), odb, opts)?;
1197 crate::transport::send_client_flush(conn).map_err(RemoteError::Library)?;
1198 Ok(out)
1199 }
1200}
1201
1202fn open_odb(git_dir: &Path) -> Odb {
1203 crate::transfer::open_odb(git_dir)
1204}
1205
1206fn list_refs_v2_connection(
1207 conn: &mut dyn crate::transport::Connection,
1208 local_git_dir: &Path,
1209 odb: Option<&Odb>,
1210 opts: &ListRefsOptions,
1211) -> RemoteResult<Vec<RemoteRef>> {
1212 let local_odb = odb.cloned().unwrap_or_else(|| open_odb(local_git_dir));
1213 let req = build_list_refs_v2_request(conn.capabilities(), &local_odb, opts)?;
1214 conn.writer().write_all(&req).map_err(Error::Io)?;
1215 conn.writer().flush().map_err(Error::Io)?;
1216 let discovery_head = conn.head_symref().map(str::to_owned);
1217 let out = parse_list_refs_v2_response(conn.reader(), opts, discovery_head)?;
1218 conn.finish_send();
1219 Ok(out)
1220}
1221
1222fn build_list_refs_v2_request(
1223 server_caps: &[String],
1224 local_odb: &Odb,
1225 opts: &ListRefsOptions,
1226) -> RemoteResult<Vec<u8>> {
1227 use crate::fetch::v2_object_format;
1228 use crate::pkt_line;
1229 use crate::protocol_v2;
1230
1231 let object_format = v2_object_format(server_caps, local_odb);
1232 let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
1233 let mut req: Vec<u8> = Vec::new();
1234 pkt_line::write_line(&mut req, "command=ls-refs").map_err(Error::Io)?;
1235 if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
1236 for line in &cap_echo {
1237 pkt_line::write_line(&mut req, line).map_err(Error::Io)?;
1238 }
1239 } else {
1240 for line in &cap_echo {
1241 pkt_line::write_line(&mut req, line).map_err(Error::Io)?;
1242 }
1243 pkt_line::write_line(&mut req, &format!("object-format={object_format}"))
1244 .map_err(Error::Io)?;
1245 }
1246 pkt_line::write_delim(&mut req).map_err(Error::Io)?;
1247 if opts.symrefs {
1248 pkt_line::write_line(&mut req, "symrefs").map_err(Error::Io)?;
1249 if crate::protocol_v2::server_advertises_ls_refs_unborn(server_caps) {
1250 pkt_line::write_line(&mut req, "unborn").map_err(Error::Io)?;
1251 }
1252 }
1253 if opts.peel {
1254 pkt_line::write_line(&mut req, "peel").map_err(Error::Io)?;
1255 }
1256 for prefix in list_ref_prefixes(opts) {
1257 pkt_line::write_line(&mut req, &format!("ref-prefix {prefix}")).map_err(Error::Io)?;
1258 }
1259 pkt_line::write_flush(&mut req).map_err(Error::Io)?;
1260 Ok(req)
1261}
1262
1263fn list_refs_includes_head(opts: &ListRefsOptions) -> bool {
1264 !opts.heads && !opts.tags
1265}
1266
1267fn list_ref_prefixes(opts: &ListRefsOptions) -> Vec<String> {
1268 let mut out = Vec::new();
1269 if opts.heads {
1270 out.push("refs/heads/".to_owned());
1271 }
1272 if opts.tags {
1273 out.push("refs/tags/".to_owned());
1274 }
1275 out
1276}
1277
1278fn list_refs_scan_prefixes(opts: &ListRefsOptions) -> Vec<String> {
1279 let wire = list_ref_prefixes(opts);
1280 if wire.is_empty() {
1281 vec!["refs/".to_owned()]
1282 } else {
1283 wire
1284 }
1285}
1286
1287fn parse_list_refs_v2_response(
1288 reader: &mut dyn Read,
1289 opts: &ListRefsOptions,
1290 mut head_symref: Option<String>,
1291) -> RemoteResult<Vec<RemoteRef>> {
1292 use crate::fetch::parse_ls_refs_v2_line;
1293 use crate::pkt_line;
1294
1295 let mut entries: Vec<RemoteRef> = Vec::new();
1296 let mut peel_map: std::collections::HashMap<String, ObjectId> =
1297 std::collections::HashMap::new();
1298 let mut reader = reader;
1299 loop {
1300 match pkt_line::read_packet(&mut reader).map_err(Error::Io)? {
1301 None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => break,
1302 Some(pkt_line::Packet::ResponseEnd) => break,
1303 Some(pkt_line::Packet::Data(line)) => {
1304 let line = line.trim_end_matches('\n');
1305 pkt_line::reject_err_line(line)?;
1306 if let Some(t) = crate::fetch::parse_v2_ls_refs_unborn_head_symref(line) {
1307 head_symref = Some(t);
1308 continue;
1309 }
1310 let Some((name, oid, symref_target, peel)) = parse_ls_refs_v2_line(line) else {
1311 continue;
1312 };
1313 if name.ends_with("^{}") {
1314 continue;
1315 }
1316 if name == "HEAD" {
1317 let sym =
1318 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t));
1319 if let Some(t) = sym.clone() {
1320 head_symref = Some(t);
1321 }
1322 if list_refs_includes_head(opts) && ref_matches_list_opts("HEAD", opts) {
1323 entries.push(RemoteRef {
1324 name: "HEAD".to_owned(),
1325 oid,
1326 symref_target: if opts.symrefs { sym } else { None },
1327 });
1328 }
1329 continue;
1330 }
1331 if !crate::refs::is_valid_fetch_advertised_ref(&name) {
1332 continue;
1333 }
1334 if let Some(p) = peel {
1335 peel_map.insert(name.clone(), p);
1336 }
1337 let sym =
1338 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t));
1339 entries.push(RemoteRef {
1340 name,
1341 oid,
1342 symref_target: sym,
1343 });
1344 }
1345 }
1346 }
1347 finalize_list_refs_output(entries, peel_map, opts, head_symref, None)
1348}
1349
1350fn list_refs_v0_advertisement(
1351 advertised: &[(String, ObjectId)],
1352 head_symref: Option<&str>,
1353 odb: Option<&Odb>,
1354 opts: &ListRefsOptions,
1355) -> RemoteResult<Vec<RemoteRef>> {
1356 let mut entries: Vec<RemoteRef> = Vec::new();
1357 let mut peel_map: std::collections::HashMap<String, ObjectId> =
1358 std::collections::HashMap::new();
1359 for (name, oid) in advertised {
1360 if name.ends_with("^{}") {
1361 if let Some(base) = name.strip_suffix("^{}") {
1362 peel_map.insert(base.to_owned(), *oid);
1363 }
1364 continue;
1365 }
1366 entries.push(RemoteRef {
1367 name: name.clone(),
1368 oid: *oid,
1369 symref_target: None,
1370 });
1371 }
1372 finalize_list_refs_output(entries, peel_map, opts, head_symref.map(str::to_owned), odb)
1373}
1374
1375fn finalize_list_refs_output(
1376 mut entries: Vec<RemoteRef>,
1377 peel_map: std::collections::HashMap<String, ObjectId>,
1378 opts: &ListRefsOptions,
1379 head_symref: Option<String>,
1380 odb: Option<&Odb>,
1381) -> RemoteResult<Vec<RemoteRef>> {
1382 entries.retain(|e| remote_ref_matches_namespace(&e.name, opts));
1383 entries.retain(|e| patterns_match_ls_remote(&e.name, &opts.prefixes));
1384 if opts.peel {
1385 let mut peel_lines: Vec<RemoteRef> = Vec::new();
1386 for e in &entries {
1387 if !e.name.starts_with("refs/tags/") {
1388 continue;
1389 }
1390 let peeled = peel_map
1391 .get(&e.name)
1392 .copied()
1393 .or_else(|| odb.and_then(|o| o.peel_tag_target(e.oid).ok().flatten()));
1394 if let Some(p) = peeled {
1395 let peel_name = format!("{}^{{}}", e.name);
1396 if should_emit_peel_line(&peel_name, &opts.prefixes) {
1397 peel_lines.push(RemoteRef {
1398 name: peel_name,
1399 oid: p,
1400 symref_target: None,
1401 });
1402 }
1403 }
1404 }
1405 entries.extend(peel_lines);
1406 }
1407 if list_refs_includes_head(opts) && ref_matches_list_opts("HEAD", opts) {
1408 if let Some(sym) = head_symref {
1409 if let Some(oid) = entries.iter().find(|e| e.name == sym).map(|e| e.oid) {
1410 if !entries.iter().any(|e| e.name == "HEAD") {
1411 entries.push(RemoteRef {
1412 name: "HEAD".to_owned(),
1413 oid,
1414 symref_target: if opts.symrefs { Some(sym) } else { None },
1415 });
1416 }
1417 }
1418 }
1419 }
1420 let query_options = RefQueryOptions {
1421 versionsort_suffixes: opts.versionsort_suffixes.clone(),
1422 ..RefQueryOptions::default()
1423 };
1424 sort_remote_refs(
1425 &mut entries,
1426 &opts.sort,
1427 &query_options.versionsort_suffixes,
1428 );
1429 if let Some(limit) = opts.limit {
1430 entries.truncate(limit);
1431 }
1432 Ok(entries)
1433}
1434
1435#[must_use]
1443#[allow(dead_code)]
1444pub(crate) fn query_remote_refs_in_memory(
1445 mut entries: Vec<RemoteRef>,
1446 opts: &ListRefsOptions,
1447 query_options: &RefQueryOptions,
1448) -> Vec<RemoteRef> {
1449 entries.retain(|e| remote_ref_matches_namespace(&e.name, opts));
1450 entries.retain(|e| patterns_match_ls_remote(&e.name, &opts.prefixes));
1451 sort_remote_refs(
1452 &mut entries,
1453 &opts.sort,
1454 &query_options.versionsort_suffixes,
1455 );
1456 if let Some(limit) = opts.limit {
1457 entries.truncate(limit);
1458 }
1459 entries
1460}
1461
1462fn should_emit_peel_line(peel_name: &str, patterns: &[String]) -> bool {
1463 if patterns.is_empty() {
1464 return true;
1465 }
1466 patterns_match_ls_remote(peel_name, patterns)
1467}
1468
1469fn sort_remote_refs(entries: &mut [RemoteRef], sort: &RefSort, versionsort_suffixes: &[String]) {
1470 entries.sort_by(|a, b| {
1471 let primary = match sort.key {
1472 SortKey::Name => a.name.cmp(&b.name),
1473 SortKey::Version => compare_version_names(&a.name, &b.name, versionsort_suffixes),
1474 SortKey::Date => a.name.cmp(&b.name),
1475 };
1476 let primary = if sort.reverse {
1477 primary.reverse()
1478 } else {
1479 primary
1480 };
1481 if primary != std::cmp::Ordering::Equal {
1482 return primary;
1483 }
1484 a.name.cmp(&b.name)
1485 });
1486}
1487
1488fn remote_ref_matches_namespace(refname: &str, opts: &ListRefsOptions) -> bool {
1489 if opts.heads || opts.tags {
1490 let is_branch = opts.heads && refname.starts_with("refs/heads/");
1491 let is_tag = opts.tags && refname.starts_with("refs/tags/");
1492 if refname == "HEAD" {
1493 return false;
1494 }
1495 if refname.ends_with("^{}") {
1496 return is_tag;
1497 }
1498 if !is_branch && !is_tag {
1499 return false;
1500 }
1501 }
1502 true
1503}
1504
1505fn ref_matches_list_opts(refname: &str, opts: &ListRefsOptions) -> bool {
1506 remote_ref_matches_namespace(refname, opts) && patterns_match_ls_remote(refname, &opts.prefixes)
1507}
1508
1509#[must_use]
1511#[allow(dead_code)]
1512pub(crate) fn ref_matches_ls_remote_patterns(refname: &str, patterns: &[String]) -> bool {
1513 patterns_match_ls_remote(refname, patterns)
1514}
1515
1516fn list_refs_from_bundle(path: &Path, opts: &ListRefsOptions) -> RemoteResult<Vec<RemoteRef>> {
1520 let bundle = crate::bundle::Bundle::open(path)
1521 .map_err(|e| RemoteError::Library(Error::Message(e.to_string())))?;
1522 let mut out = Vec::new();
1523 for (name, oid) in &bundle.header().refs {
1524 if !ref_matches_list_opts(name, opts) {
1525 continue;
1526 }
1527 if name != "HEAD" && !crate::refs::is_valid_fetch_advertised_ref(name) {
1528 continue;
1529 }
1530 out.push(RemoteRef {
1531 name: name.clone(),
1532 oid: *oid,
1533 symref_target: None,
1534 });
1535 }
1536 sort_remote_refs(&mut out, &opts.sort, &opts.versionsort_suffixes);
1537 if let Some(limit) = opts.limit {
1538 out.truncate(limit);
1539 }
1540 Ok(out)
1541}
1542
1543pub(crate) fn list_refs_from_git_dir_for_fetch(
1545 git_dir: &Path,
1546 odb: Option<&Odb>,
1547 refspecs: &[String],
1548) -> RemoteResult<Vec<RemoteRef>> {
1549 let prefixes = crate::fetch::fetch_ref_list_prefixes(refspecs);
1550 let opts = ListRefsOptions {
1551 symrefs: true,
1552 ..Default::default()
1553 };
1554 list_refs_from_git_dir_with_scan_prefixes(git_dir, odb, &opts, &prefixes)
1555}
1556
1557pub fn list_refs_from_git_dir(
1560 git_dir: &Path,
1561 odb: Option<&Odb>,
1562 opts: &ListRefsOptions,
1563) -> RemoteResult<Vec<RemoteRef>> {
1564 list_refs_from_git_dir_with_scan_prefixes(git_dir, odb, opts, &list_refs_scan_prefixes(opts))
1565}
1566
1567fn list_refs_from_git_dir_with_scan_prefixes(
1568 git_dir: &Path,
1569 odb: Option<&Odb>,
1570 opts: &ListRefsOptions,
1571 scan_prefixes: &[String],
1572) -> RemoteResult<Vec<RemoteRef>> {
1573 let mut lazy_odb: Option<Odb> = None;
1574
1575 let mut entries = Vec::new();
1576 if list_refs_includes_head(opts) && ref_matches_list_opts("HEAD", opts) {
1577 let symref_target = if opts.symrefs {
1578 crate::refs::read_symbolic_ref(git_dir, "HEAD")
1579 .ok()
1580 .flatten()
1581 } else {
1582 None
1583 };
1584 match crate::refs::resolve_ref(git_dir, "HEAD") {
1585 Ok(head_oid) => {
1586 entries.push(RemoteRef {
1587 name: "HEAD".to_owned(),
1588 oid: head_oid,
1589 symref_target,
1590 });
1591 }
1592 Err(_) if symref_target.is_some() => {
1593 entries.push(RemoteRef {
1594 name: "HEAD".to_owned(),
1595 oid: ObjectId::zero(),
1596 symref_target,
1597 });
1598 }
1599 Err(_) => {}
1600 }
1601 }
1602
1603 for prefix in scan_prefixes {
1604 let listed = crate::refs::list_refs(git_dir, prefix).map_err(RemoteError::Library)?;
1605 for (name, oid) in listed {
1606 if let Some(branch_tail) = name.strip_prefix("refs/heads/") {
1607 if branch_tail.starts_with("refs/") {
1608 continue;
1609 }
1610 }
1611 if !ref_matches_list_opts(&name, opts) {
1612 continue;
1613 }
1614 entries.push(RemoteRef {
1615 name: name.clone(),
1616 oid,
1617 symref_target: None,
1618 });
1619 if opts.peel && name.starts_with("refs/tags/") {
1620 let peel_name = format!("{name}^{{}}");
1621 if should_emit_peel_line(&peel_name, &opts.prefixes) {
1622 let peel_odb = match odb {
1623 Some(o) => o,
1624 None => lazy_odb.get_or_insert_with(|| {
1625 Odb::new(&git_dir.join("objects"))
1626 .with_config_git_dir(git_dir.to_path_buf())
1627 }),
1628 };
1629 if let Ok(Some(peeled)) = peel_odb.peel_tag_target(oid) {
1630 entries.push(RemoteRef {
1631 name: peel_name,
1632 oid: peeled,
1633 symref_target: None,
1634 });
1635 }
1636 }
1637 }
1638 }
1639 }
1640
1641 sort_remote_refs(&mut entries, &opts.sort, &opts.versionsort_suffixes);
1642 if let Some(limit) = opts.limit {
1643 entries.truncate(limit);
1644 }
1645 Ok(entries)
1646}
1647
1648#[cfg(test)]
1649mod tests {
1650 use super::*;
1651
1652 fn remote_ref(name: &str, hex_digit: char) -> RemoteRef {
1653 RemoteRef {
1654 name: name.to_owned(),
1655 oid: ObjectId::from_hex(&hex_digit.to_string().repeat(40)).unwrap(),
1656 symref_target: None,
1657 }
1658 }
1659
1660 #[test]
1661 fn missing_urls_name_the_remote() {
1662 let err = parse_remote_url(" ").expect_err("empty");
1663 assert!(matches!(err, RemoteError::NoUrl { remote: None }));
1664 assert_eq!(err.to_string(), "remote URL is empty");
1665
1666 let config = ConfigSet::new();
1667 let err = Remote::from_config(&config, "upstream").expect_err("no url");
1668 assert!(matches!(&err, RemoteError::NoUrl { remote: Some(name) } if name == "upstream"));
1669 assert_eq!(err.to_string(), "remote 'upstream' has no URL configured");
1670 }
1671
1672 #[test]
1673 fn fold_peeled_refs_attaches_peeled_lines_and_keeps_order() {
1674 let mut head = remote_ref("HEAD", '1');
1675 head.symref_target = Some("refs/heads/main".to_owned());
1676 let refs = vec![
1677 head,
1678 remote_ref("refs/heads/main", '1'),
1679 remote_ref("refs/tags/light", '1'),
1680 remote_ref("refs/tags/v1", '2'),
1681 remote_ref("refs/tags/v1^{}", '1'),
1682 ];
1683 let folded = fold_peeled_refs(&refs);
1684 let names: Vec<&str> = folded.iter().map(|r| r.name.as_str()).collect();
1685 assert_eq!(
1686 names,
1687 ["HEAD", "refs/heads/main", "refs/tags/light", "refs/tags/v1"]
1688 );
1689 assert_eq!(folded[0].symref_target.as_deref(), Some("refs/heads/main"));
1690 assert_eq!(folded[2].peeled, None);
1691 assert_eq!(folded[3].oid, refs[3].oid);
1692 assert_eq!(folded[3].peeled, Some(refs[4].oid));
1693 }
1694
1695 #[test]
1696 fn peeled_tag_only_matches_peel_suffix() {
1697 assert_eq!(
1698 remote_ref("refs/tags/v1^{}", '1').peeled_tag(),
1699 Some("refs/tags/v1")
1700 );
1701 assert_eq!(remote_ref("refs/tags/v1", '1').peeled_tag(), None);
1702 }
1703
1704 #[test]
1705 fn remote_url_parse_table() {
1706 assert!(matches!(
1707 RemoteUrl::try_from("/abs/path").unwrap(),
1708 RemoteUrl::Local(_)
1709 ));
1710 assert!(matches!(
1711 RemoteUrl::try_from("file:///tmp/r.git").unwrap(),
1712 RemoteUrl::File(_)
1713 ));
1714 assert!(matches!(
1715 RemoteUrl::try_from("git://host/repo").unwrap(),
1716 RemoteUrl::Git(_)
1717 ));
1718 assert!(matches!(
1719 RemoteUrl::try_from("git@host:repo.git").unwrap(),
1720 RemoteUrl::Ssh(_)
1721 ));
1722 assert!(matches!(
1723 RemoteUrl::try_from("ssh://git@host/repo").unwrap(),
1724 RemoteUrl::Ssh(_)
1725 ));
1726 assert!(matches!(
1727 RemoteUrl::try_from("https://host/r.git").unwrap(),
1728 RemoteUrl::Https(_)
1729 ));
1730 assert!(matches!(
1731 RemoteUrl::try_from("ftp://x"),
1732 Err(RemoteError::UnsupportedScheme)
1733 ));
1734 }
1735
1736 #[test]
1737 fn pattern_matches_empty_allows_all() {
1738 assert!(ref_matches_ls_remote_patterns("refs/heads/main", &[]));
1739 assert!(ref_matches_ls_remote_patterns("HEAD", &[]));
1740 }
1741
1742 #[test]
1743 fn pattern_matches_exact() {
1744 let pats = vec!["HEAD".to_owned()];
1745 assert!(ref_matches_ls_remote_patterns("HEAD", &pats));
1746 assert!(!ref_matches_ls_remote_patterns("refs/heads/main", &pats));
1747 }
1748
1749 #[test]
1750 fn pattern_matches_suffix_component() {
1751 let pats = vec!["main".to_owned()];
1752 assert!(ref_matches_ls_remote_patterns("refs/heads/main", &pats));
1753 assert!(!ref_matches_ls_remote_patterns("refs/heads/notmain", &pats));
1754 assert!(!ref_matches_ls_remote_patterns("main-branch", &pats));
1755 }
1756
1757 #[cfg_attr(
1758 not(feature = "full-tests"),
1759 ignore = "starts system git or is heavy; run with --features full-tests"
1760 )]
1761 #[test]
1762 fn query_remote_refs_in_memory_empty_pattern_matches_nothing() {
1763 let entries = vec![
1764 RemoteRef {
1765 name: "refs/heads/main".into(),
1766 oid: ObjectId::from_hex("1".repeat(40).as_str()).unwrap(),
1767 symref_target: None,
1768 },
1769 RemoteRef {
1770 name: "refs/tags/v1".into(),
1771 oid: ObjectId::from_hex("2".repeat(40).as_str()).unwrap(),
1772 symref_target: None,
1773 },
1774 ];
1775 let opts = ListRefsOptions {
1776 prefixes: vec![String::new()],
1777 ..Default::default()
1778 };
1779 let out = query_remote_refs_in_memory(entries, &opts, &RefQueryOptions::default());
1780 assert!(out.is_empty());
1781 }
1782
1783 #[cfg_attr(
1784 not(feature = "full-tests"),
1785 ignore = "starts system git or is heavy; run with --features full-tests"
1786 )]
1787 #[test]
1788 fn query_remote_refs_in_memory_version_sort_and_limit() {
1789 let entries = vec![
1790 RemoteRef {
1791 name: "refs/tags/v1.10".into(),
1792 oid: ObjectId::from_hex("1".repeat(40).as_str()).unwrap(),
1793 symref_target: None,
1794 },
1795 RemoteRef {
1796 name: "refs/tags/v1.2".into(),
1797 oid: ObjectId::from_hex("2".repeat(40).as_str()).unwrap(),
1798 symref_target: None,
1799 },
1800 RemoteRef {
1801 name: "refs/tags/v1.1".into(),
1802 oid: ObjectId::from_hex("3".repeat(40).as_str()).unwrap(),
1803 symref_target: None,
1804 },
1805 ];
1806 let opts = ListRefsOptions {
1807 sort: RefSort {
1808 key: SortKey::Version,
1809 reverse: false,
1810 },
1811 limit: Some(2),
1812 ..Default::default()
1813 };
1814 let out = query_remote_refs_in_memory(entries, &opts, &RefQueryOptions::default());
1815 assert_eq!(
1816 out.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(),
1817 vec!["refs/tags/v1.1", "refs/tags/v1.2"]
1818 );
1819 }
1820
1821 #[test]
1822 fn list_opts_heads_excludes_head() {
1823 let heads_only = super::ListRefsOptions {
1824 heads: true,
1825 ..Default::default()
1826 };
1827 assert!(!super::ref_matches_list_opts("HEAD", &heads_only));
1828 assert!(super::ref_matches_list_opts("refs/heads/main", &heads_only));
1829 assert!(!super::ref_matches_list_opts("refs/tags/v1", &heads_only));
1830 }
1831
1832 #[test]
1833 fn insteadof_rewrite_applied_in_from_config() {
1834 let mut cfg = ConfigSet::new();
1835 cfg.add_command_override("url.https://github.com/.insteadOf", "gh:")
1836 .unwrap();
1837 cfg.add_command_override("remote.origin.url", "gh:org/repo.git")
1838 .unwrap();
1839 let remote = Remote::from_config(&cfg, "origin").unwrap();
1840 assert!(matches!(remote.fetch_url(), RemoteUrl::Https(_)));
1841 }
1842
1843 #[test]
1844 fn push_insteadof_applied_when_pushurl_absent() {
1845 let mut cfg = ConfigSet::new();
1846 cfg.add_command_override("url.ssh://push.example/.pushInsteadOf", "short:")
1847 .unwrap();
1848 cfg.add_command_override("remote.origin.url", "short:org/repo.git")
1849 .unwrap();
1850 let remote = Remote::from_config(&cfg, "origin").unwrap();
1851 assert_eq!(
1852 remote.push_url().to_url_string(),
1853 "ssh://push.example/org/repo.git"
1854 );
1855 assert_eq!(remote.fetch_url().to_url_string(), "short:org/repo.git");
1856 }
1857
1858 #[test]
1859 fn list_ref_prefixes_unrestricted_for_ls_remote_patterns() {
1860 let opts = super::ListRefsOptions {
1861 prefixes: vec!["main".to_owned()],
1862 ..Default::default()
1863 };
1864 assert!(super::list_ref_prefixes(&opts).is_empty());
1865 assert_eq!(
1866 super::list_refs_scan_prefixes(&opts),
1867 vec!["refs/".to_owned()]
1868 );
1869 assert!(super::ref_matches_list_opts("refs/heads/main", &opts));
1870 assert!(!super::ref_matches_list_opts("refs/heads/topic", &opts));
1871 }
1872
1873 #[test]
1874 fn list_ref_prefixes_heads_and_tags_only_when_requested() {
1875 assert_eq!(
1876 super::list_ref_prefixes(&super::ListRefsOptions {
1877 heads: true,
1878 ..Default::default()
1879 }),
1880 vec!["refs/heads/".to_owned()]
1881 );
1882 assert_eq!(
1883 super::list_ref_prefixes(&super::ListRefsOptions {
1884 tags: true,
1885 ..Default::default()
1886 }),
1887 vec!["refs/tags/".to_owned()]
1888 );
1889 assert_eq!(
1890 super::list_ref_prefixes(&super::ListRefsOptions {
1891 heads: true,
1892 tags: true,
1893 ..Default::default()
1894 }),
1895 vec!["refs/heads/".to_owned(), "refs/tags/".to_owned()]
1896 );
1897 assert!(super::list_ref_prefixes(&super::ListRefsOptions::default()).is_empty());
1898 }
1899
1900 #[test]
1901 fn list_refs_includes_head_with_explicit_pattern() {
1902 let opts = super::ListRefsOptions {
1903 prefixes: vec!["HEAD".to_owned()],
1904 ..Default::default()
1905 };
1906 assert!(super::list_refs_includes_head(&opts));
1907 assert!(super::ref_matches_list_opts("HEAD", &opts));
1908 let opts = super::ListRefsOptions {
1909 prefixes: vec!["refs/pull/1/head".to_owned()],
1910 ..Default::default()
1911 };
1912 assert!(super::list_refs_includes_head(&opts));
1913 assert!(!super::ref_matches_list_opts("HEAD", &opts));
1914 }
1915}