use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::path::Path;
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 mut all_refs: BTreeMap<String, ObjectId> = BTreeMap::new();
collect_loose_refs(git_dir, &git_dir.join("refs"), "refs", &mut all_refs)?;
for (name, oid) in read_packed_refs(git_dir)? {
all_refs.entry(name).or_insert(oid);
}
for (name, oid) in &all_refs {
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 pattern_matches(refname: &str, patterns: &[String]) -> bool {
if patterns.is_empty() {
return true;
}
patterns.iter().any(|pat| {
refname == pat
|| refname
.strip_suffix(pat.as_str())
.is_some_and(|prefix| prefix.ends_with('/'))
})
}
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));
}
}