#[cfg(feature = "http")]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use sley_core::{GitError, ObjectFormat, ObjectId, Result};
use sley_object::ObjectType;
use sley_odb::{FileObjectDatabase, ObjectReader};
use sley_refs::{FileRefStore, Ref, RefTarget};
use sley_transport::RemoteUrl;
use crate::CredentialProvider;
pub enum LsRemoteSource {
Http(RemoteUrl),
Ssh(RemoteUrl),
Git(RemoteUrl),
Local {
git_dir: PathBuf,
},
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LsRemoteFilter {
pub heads: bool,
pub tags: bool,
pub refs_only: bool,
}
#[derive(Debug, Clone)]
pub struct LsRemoteRecord {
pub oid: ObjectId,
pub name: String,
pub symref: Option<String>,
}
pub fn ls_remote(
source: &LsRemoteSource,
format: ObjectFormat,
filter: &LsRemoteFilter,
matches: &dyn Fn(&str) -> bool,
#[cfg_attr(not(feature = "http"), allow(unused_variables))]
credentials: &mut dyn CredentialProvider,
) -> Result<(Vec<LsRemoteRecord>, ObjectFormat)> {
match source {
#[cfg(feature = "http")]
LsRemoteSource::Http(remote) => {
ls_remote_http(remote, format, filter, matches, credentials)
}
#[cfg(not(feature = "http"))]
LsRemoteSource::Http(_) => Err(GitError::Unsupported(
"HTTP transport is not enabled in this build".into(),
)),
LsRemoteSource::Ssh(remote) => crate::ssh::ls_remote_ssh(remote, filter, matches),
LsRemoteSource::Git(remote) => crate::git::ls_remote_git(remote, filter, matches),
LsRemoteSource::Local { git_dir } => ls_remote_local(git_dir, format, filter, matches),
}
}
#[cfg(feature = "http")]
fn ls_remote_http(
remote: &RemoteUrl,
format: ObjectFormat,
filter: &LsRemoteFilter,
matches: &dyn Fn(&str) -> bool,
credentials: &mut dyn CredentialProvider,
) -> Result<(Vec<LsRemoteRecord>, ObjectFormat)> {
let client = crate::http::new_http_client();
let (refs, features) =
crate::http::http_upload_pack_advertisements(&client, remote, format, credentials)?;
let format = features.object_format.unwrap_or(ObjectFormat::Sha1);
if format != ObjectFormat::Sha1 {
return Err(GitError::Unsupported(format!(
"http ls-remote currently supports SHA-1 advertisements, got {}",
format.name()
)));
}
let symrefs = features
.symrefs
.iter()
.filter_map(|symref| symref.split_once(':'))
.map(|(name, target)| (name.to_string(), target.to_string()))
.collect::<HashMap<_, _>>();
let mut records = Vec::new();
for advertisement in refs {
if advertisement.oid.is_null() {
continue;
}
if filter.refs_only && (advertisement.name == "HEAD" || advertisement.name.ends_with("^{}"))
{
continue;
}
if !ref_class_selected(&advertisement.name, filter) {
continue;
}
if !matches(&advertisement.name) {
continue;
}
records.push(LsRemoteRecord {
oid: advertisement.oid,
symref: symrefs.get(&advertisement.name).cloned(),
name: advertisement.name,
});
}
Ok((records, format))
}
fn ls_remote_local(
git_dir: &Path,
format: ObjectFormat,
filter: &LsRemoteFilter,
matches: &dyn Fn(&str) -> bool,
) -> Result<(Vec<LsRemoteRecord>, ObjectFormat)> {
let store = FileRefStore::new(git_dir, format);
let db = FileObjectDatabase::from_git_dir(git_dir, format);
let mut records = Vec::new();
if !filter.refs_only
&& !filter.heads
&& !filter.tags
&& let Some(target) = store.read_ref("HEAD")?
{
let reference = Ref {
name: "HEAD".to_string(),
target,
};
if matches(&reference.name)
&& let Some((oid, symref)) = resolve_for_each_ref_target(&store, &reference)?
{
records.push(LsRemoteRecord {
oid,
name: reference.name,
symref,
});
}
}
for reference in store.list_refs()? {
if !ref_class_selected(&reference.name, filter) {
continue;
}
if !matches(&reference.name) {
continue;
}
let Some((oid, symref)) = resolve_for_each_ref_target(&store, &reference)? else {
continue;
};
records.push(LsRemoteRecord {
oid,
name: reference.name.clone(),
symref,
});
if !filter.refs_only
&& let Some(record) = peeled_tag_record(&db, format, &oid, &reference.name, matches)?
{
records.push(record);
}
}
Ok((records, format))
}
fn peeled_tag_record(
db: &FileObjectDatabase,
format: ObjectFormat,
oid: &ObjectId,
name: &str,
matches: &dyn Fn(&str) -> bool,
) -> Result<Option<LsRemoteRecord>> {
let object = db.read_object(oid)?;
if object.object_type != ObjectType::Tag {
return Ok(None);
}
let peeled_name = format!("{name}^{{}}");
if !matches(&peeled_name) {
return Ok(None);
}
let peeled = sley_rev::peel_tags(db, format, oid)?;
Ok(Some(LsRemoteRecord {
oid: peeled,
name: peeled_name,
symref: None,
}))
}
fn ref_class_selected(name: &str, filter: &LsRemoteFilter) -> bool {
if !filter.heads && !filter.tags {
return true;
}
let is_head = name.starts_with("refs/heads/");
let is_tag = name.starts_with("refs/tags/");
(filter.heads && is_head) || (filter.tags && is_tag)
}
fn resolve_for_each_ref_target(
store: &FileRefStore,
reference: &Ref,
) -> Result<Option<(ObjectId, Option<String>)>> {
let mut target = reference.target.clone();
let mut symref = None;
for _ in 0..5 {
match target {
RefTarget::Direct(oid) => return Ok(Some((oid, symref))),
RefTarget::Symbolic(name) => {
symref.get_or_insert_with(|| name.clone());
let Some(next) = store.read_ref(&name)? else {
return Ok(None);
};
target = next;
}
}
}
Ok(None)
}