1#[cfg(feature = "http")]
20use std::collections::HashMap;
21use std::path::{Path, PathBuf};
22
23use sley_core::{GitError, ObjectFormat, ObjectId, Result};
24use sley_object::ObjectType;
25use sley_odb::{FileObjectDatabase, ObjectReader};
26use sley_refs::{FileRefStore, Ref, RefTarget};
27use sley_transport::RemoteUrl;
28
29use crate::CredentialProvider;
30
31pub enum LsRemoteSource {
36 Http(RemoteUrl),
38 Ssh(RemoteUrl),
41 Git(RemoteUrl),
43 Local {
47 git_dir: PathBuf,
49 },
50}
51
52#[derive(Debug, Clone, Copy, Default)]
55pub struct LsRemoteFilter {
56 pub heads: bool,
58 pub tags: bool,
60 pub refs_only: bool,
62}
63
64#[derive(Debug, Clone)]
68pub struct LsRemoteRecord {
69 pub oid: ObjectId,
72 pub name: String,
74 pub symref: Option<String>,
77}
78
79pub fn ls_remote(
94 source: &LsRemoteSource,
95 format: ObjectFormat,
96 filter: &LsRemoteFilter,
97 matches: &dyn Fn(&str) -> bool,
98 #[cfg_attr(not(feature = "http"), allow(unused_variables))]
99 credentials: &mut dyn CredentialProvider,
100) -> Result<(Vec<LsRemoteRecord>, ObjectFormat)> {
101 match source {
102 #[cfg(feature = "http")]
103 LsRemoteSource::Http(remote) => {
104 ls_remote_http(remote, format, filter, matches, credentials)
105 }
106 #[cfg(not(feature = "http"))]
107 LsRemoteSource::Http(_) => Err(GitError::Unsupported(
108 "HTTP transport is not enabled in this build".into(),
109 )),
110 LsRemoteSource::Ssh(remote) => crate::ssh::ls_remote_ssh(remote, filter, matches),
111 LsRemoteSource::Git(remote) => crate::git::ls_remote_git(remote, filter, matches),
112 LsRemoteSource::Local { git_dir } => ls_remote_local(git_dir, format, filter, matches),
113 }
114}
115
116#[cfg(feature = "http")]
120fn ls_remote_http(
121 remote: &RemoteUrl,
122 format: ObjectFormat,
123 filter: &LsRemoteFilter,
124 matches: &dyn Fn(&str) -> bool,
125 credentials: &mut dyn CredentialProvider,
126) -> Result<(Vec<LsRemoteRecord>, ObjectFormat)> {
127 let client = crate::http::new_http_client();
128 let (refs, features) =
129 crate::http::http_upload_pack_advertisements(&client, remote, format, credentials)?;
130 let format = features.object_format.unwrap_or(ObjectFormat::Sha1);
131 if format != ObjectFormat::Sha1 {
132 return Err(GitError::Unsupported(format!(
133 "http ls-remote currently supports SHA-1 advertisements, got {}",
134 format.name()
135 )));
136 }
137 let symrefs = features
138 .symrefs
139 .iter()
140 .filter_map(|symref| symref.split_once(':'))
141 .map(|(name, target)| (name.to_string(), target.to_string()))
142 .collect::<HashMap<_, _>>();
143 let mut records = Vec::new();
144 for advertisement in refs {
145 if advertisement.oid.is_null() {
146 continue;
147 }
148 if filter.refs_only && (advertisement.name == "HEAD" || advertisement.name.ends_with("^{}"))
149 {
150 continue;
151 }
152 if !ref_class_selected(&advertisement.name, filter) {
153 continue;
154 }
155 if !matches(&advertisement.name) {
156 continue;
157 }
158 records.push(LsRemoteRecord {
159 oid: advertisement.oid,
160 symref: symrefs.get(&advertisement.name).cloned(),
161 name: advertisement.name,
162 });
163 }
164 Ok((records, format))
165}
166
167fn ls_remote_local(
171 git_dir: &Path,
172 format: ObjectFormat,
173 filter: &LsRemoteFilter,
174 matches: &dyn Fn(&str) -> bool,
175) -> Result<(Vec<LsRemoteRecord>, ObjectFormat)> {
176 let store = FileRefStore::new(git_dir, format);
177 let db = FileObjectDatabase::from_git_dir(git_dir, format);
178 let mut records = Vec::new();
179
180 if !filter.refs_only
181 && !filter.heads
182 && !filter.tags
183 && let Some(target) = store.read_ref("HEAD")?
184 {
185 let reference = Ref {
186 name: "HEAD".to_string(),
187 target,
188 };
189 if matches(&reference.name)
190 && let Some((oid, symref)) = resolve_for_each_ref_target(&store, &reference)?
191 {
192 records.push(LsRemoteRecord {
193 oid,
194 name: reference.name,
195 symref,
196 });
197 }
198 }
199
200 for reference in store.list_refs()? {
201 if !ref_class_selected(&reference.name, filter) {
202 continue;
203 }
204 if !matches(&reference.name) {
205 continue;
206 }
207 let Some((oid, symref)) = resolve_for_each_ref_target(&store, &reference)? else {
208 continue;
209 };
210 records.push(LsRemoteRecord {
211 oid,
212 name: reference.name.clone(),
213 symref,
214 });
215 if !filter.refs_only
216 && let Some(record) = peeled_tag_record(&db, format, &oid, &reference.name, matches)?
217 {
218 records.push(record);
219 }
220 }
221
222 Ok((records, format))
223}
224
225fn peeled_tag_record(
228 db: &FileObjectDatabase,
229 format: ObjectFormat,
230 oid: &ObjectId,
231 name: &str,
232 matches: &dyn Fn(&str) -> bool,
233) -> Result<Option<LsRemoteRecord>> {
234 let object = db.read_object(oid)?;
235 if object.object_type != ObjectType::Tag {
236 return Ok(None);
237 }
238 let peeled_name = format!("{name}^{{}}");
239 if !matches(&peeled_name) {
240 return Ok(None);
241 }
242 let peeled = sley_rev::peel_tags(db, format, oid)?;
243 Ok(Some(LsRemoteRecord {
244 oid: peeled,
245 name: peeled_name,
246 symref: None,
247 }))
248}
249
250fn ref_class_selected(name: &str, filter: &LsRemoteFilter) -> bool {
253 if !filter.heads && !filter.tags {
254 return true;
255 }
256 let is_head = name.starts_with("refs/heads/");
257 let is_tag = name.starts_with("refs/tags/");
258 (filter.heads && is_head) || (filter.tags && is_tag)
259}
260
261fn resolve_for_each_ref_target(
264 store: &FileRefStore,
265 reference: &Ref,
266) -> Result<Option<(ObjectId, Option<String>)>> {
267 let mut target = reference.target.clone();
268 let mut symref = None;
269 for _ in 0..5 {
270 match target {
271 RefTarget::Direct(oid) => return Ok(Some((oid, symref))),
272 RefTarget::Symbolic(name) => {
273 symref.get_or_insert_with(|| name.clone());
274 let Some(next) = store.read_ref(&name)? else {
275 return Ok(None);
276 };
277 target = next;
278 }
279 }
280 }
281 Ok(None)
282}