use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::objects::{ObjectId, ObjectKind};
use crate::odb::Odb;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefEntry {
pub name: String,
pub oid: ObjectId,
pub symref_target: Option<String>,
}
#[derive(Debug, Default)]
pub struct Options {
pub heads: bool,
pub tags: bool,
pub refs_only: bool,
pub symref: bool,
pub patterns: Vec<String>,
}
pub fn ls_remote(git_dir: &Path, odb: &Odb, opts: &Options) -> Result<Vec<RefEntry>> {
let mut entries = Vec::new();
let include_head = !opts.heads && !opts.tags && !opts.refs_only;
if include_head {
if let Ok(head_oid) = crate::refs::resolve_ref(git_dir, "HEAD") {
let symref_target = if opts.symref {
crate::refs::read_symbolic_ref(git_dir, "HEAD")?
} else {
None
};
if pattern_matches("HEAD", &opts.patterns) {
entries.push(RefEntry {
name: "HEAD".to_owned(),
oid: head_oid,
symref_target,
});
}
}
}
let refs_dir_root = resolve_common_git_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
let mut all_refs: BTreeMap<String, ObjectId> = BTreeMap::new();
collect_loose_refs(
&refs_dir_root,
&refs_dir_root.join("refs"),
"refs",
&mut all_refs,
)?;
for (name, oid) in read_packed_refs(&refs_dir_root)? {
all_refs.entry(name).or_insert(oid);
}
for (name, oid) in &all_refs {
if let Some(branch_tail) = name.strip_prefix("refs/heads/") {
if branch_tail.starts_with("refs/") {
continue;
}
}
if opts.heads && !name.starts_with("refs/heads/") {
continue;
}
if opts.tags && !name.starts_with("refs/tags/") {
continue;
}
if !pattern_matches(name, &opts.patterns) {
continue;
}
entries.push(RefEntry {
name: name.clone(),
oid: *oid,
symref_target: None,
});
if !opts.refs_only && name.starts_with("refs/tags/") {
if let Some(peeled) = peel_tag(odb, oid) {
entries.push(RefEntry {
name: format!("{name}^{{}}"),
oid: peeled,
symref_target: None,
});
}
}
}
Ok(entries)
}
fn resolve_common_git_dir(git_dir: &Path) -> Option<PathBuf> {
let raw = fs::read_to_string(git_dir.join("commondir")).ok()?;
let rel = raw.trim();
if rel.is_empty() {
return None;
}
let candidate = if Path::new(rel).is_absolute() {
PathBuf::from(rel)
} else {
git_dir.join(rel)
};
candidate.canonicalize().ok()
}
pub fn ref_matches_ls_remote_patterns(refname: &str, patterns: &[String]) -> bool {
pattern_matches(refname, patterns)
}
fn pattern_matches(refname: &str, patterns: &[String]) -> bool {
if patterns.is_empty() {
return true;
}
patterns.iter().any(|pat| {
if pat.contains('*') || pat.contains('?') {
glob_match(pat, refname)
} else {
refname == pat
|| refname
.strip_suffix(pat.as_str())
.is_some_and(|prefix| prefix.ends_with('/'))
}
})
}
fn glob_match(pattern: &str, text: &str) -> bool {
let pat: Vec<char> = pattern.chars().collect();
let txt: Vec<char> = text.chars().collect();
let (mut pi, mut ti) = (0, 0);
let (mut star_pi, mut star_ti) = (usize::MAX, 0);
while ti < txt.len() {
if pi < pat.len() && (pat[pi] == '?' || pat[pi] == txt[ti]) {
pi += 1;
ti += 1;
} else if pi < pat.len() && pat[pi] == '*' {
star_pi = pi;
star_ti = ti;
pi += 1;
} else if star_pi != usize::MAX {
pi = star_pi + 1;
star_ti += 1;
ti = star_ti;
} else {
return false;
}
}
while pi < pat.len() && pat[pi] == '*' {
pi += 1;
}
pi == pat.len()
}
fn collect_loose_refs(
git_dir: &Path,
path: &Path,
relative: &str,
out: &mut BTreeMap<String, ObjectId>,
) -> Result<()> {
let read_dir = match fs::read_dir(path) {
Ok(rd) => rd,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(Error::Io(e)),
};
for entry in read_dir {
let entry = entry?;
let file_name = entry.file_name().to_string_lossy().to_string();
let next_relative = format!("{relative}/{file_name}");
let file_type = entry.file_type()?;
if file_type.is_dir() {
collect_loose_refs(git_dir, &entry.path(), &next_relative, out)?;
} else if file_type.is_file() {
if let Ok(oid) = crate::refs::resolve_ref(git_dir, &next_relative) {
out.insert(next_relative, oid);
}
}
}
Ok(())
}
fn read_packed_refs(git_dir: &Path) -> Result<Vec<(String, ObjectId)>> {
let path = git_dir.join("packed-refs");
let text = match fs::read_to_string(path) {
Ok(t) => t,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(Error::Io(e)),
};
let mut entries = Vec::new();
for line in text.lines() {
if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
continue;
}
let mut parts = line.split_whitespace();
let Some(oid_str) = parts.next() else {
continue;
};
let Some(name) = parts.next() else {
continue;
};
if let Ok(oid) = oid_str.parse::<ObjectId>() {
entries.push((name.to_owned(), oid));
}
}
Ok(entries)
}
fn peel_tag(odb: &Odb, oid: &ObjectId) -> Option<ObjectId> {
let obj = odb.read(oid).ok()?;
if obj.kind != ObjectKind::Tag {
return None;
}
let text = std::str::from_utf8(&obj.data).ok()?;
for line in text.lines() {
if let Some(target) = line.strip_prefix("object ") {
return target.trim().parse::<ObjectId>().ok();
}
}
None
}
#[cfg(test)]
mod tests {
use super::pattern_matches;
#[test]
fn pattern_matches_empty_allows_all() {
assert!(pattern_matches("refs/heads/main", &[]));
assert!(pattern_matches("HEAD", &[]));
}
#[test]
fn pattern_matches_exact() {
let pats = vec!["HEAD".to_owned()];
assert!(pattern_matches("HEAD", &pats));
assert!(!pattern_matches("refs/heads/main", &pats));
}
#[test]
fn pattern_matches_suffix_component() {
let pats = vec!["main".to_owned()];
assert!(pattern_matches("refs/heads/main", &pats));
assert!(!pattern_matches("refs/heads/notmain", &pats));
assert!(!pattern_matches("main-branch", &pats));
}
}