1use std::collections::BTreeMap;
13use std::fs;
14use std::io;
15use std::path::Path;
16
17use crate::error::{Error, Result};
18use crate::objects::{ObjectId, ObjectKind};
19use crate::odb::Odb;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct RefEntry {
24 pub name: String,
27 pub oid: ObjectId,
29 pub symref_target: Option<String>,
33}
34
35#[derive(Debug, Default)]
37pub struct Options {
38 pub heads: bool,
40 pub tags: bool,
42 pub refs_only: bool,
44 pub symref: bool,
46 pub patterns: Vec<String>,
51}
52
53pub fn ls_remote(git_dir: &Path, odb: &Odb, opts: &Options) -> Result<Vec<RefEntry>> {
69 let mut entries = Vec::new();
70
71 let include_head = !opts.heads && !opts.tags && !opts.refs_only;
72 if include_head {
73 if let Ok(head_oid) = crate::refs::resolve_ref(git_dir, "HEAD") {
74 let symref_target = if opts.symref {
75 crate::refs::read_symbolic_ref(git_dir, "HEAD")?
76 } else {
77 None
78 };
79 if pattern_matches("HEAD", &opts.patterns) {
80 entries.push(RefEntry {
81 name: "HEAD".to_owned(),
82 oid: head_oid,
83 symref_target,
84 });
85 }
86 }
87 }
88
89 let mut all_refs: BTreeMap<String, ObjectId> = BTreeMap::new();
90 collect_loose_refs(git_dir, &git_dir.join("refs"), "refs", &mut all_refs)?;
91 for (name, oid) in read_packed_refs(git_dir)? {
92 all_refs.entry(name).or_insert(oid);
93 }
94
95 for (name, oid) in &all_refs {
96 if opts.heads && !name.starts_with("refs/heads/") {
97 continue;
98 }
99 if opts.tags && !name.starts_with("refs/tags/") {
100 continue;
101 }
102 if !pattern_matches(name, &opts.patterns) {
103 continue;
104 }
105
106 entries.push(RefEntry {
107 name: name.clone(),
108 oid: *oid,
109 symref_target: None,
110 });
111
112 if !opts.refs_only && name.starts_with("refs/tags/") {
113 if let Some(peeled) = peel_tag(odb, oid) {
114 entries.push(RefEntry {
115 name: format!("{name}^{{}}"),
116 oid: peeled,
117 symref_target: None,
118 });
119 }
120 }
121 }
122
123 Ok(entries)
124}
125
126fn pattern_matches(refname: &str, patterns: &[String]) -> bool {
133 if patterns.is_empty() {
134 return true;
135 }
136 patterns.iter().any(|pat| {
137 refname == pat
138 || refname
139 .strip_suffix(pat.as_str())
140 .is_some_and(|prefix| prefix.ends_with('/'))
141 })
142}
143
144fn collect_loose_refs(
149 git_dir: &Path,
150 path: &Path,
151 relative: &str,
152 out: &mut BTreeMap<String, ObjectId>,
153) -> Result<()> {
154 let read_dir = match fs::read_dir(path) {
155 Ok(rd) => rd,
156 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
157 Err(e) => return Err(Error::Io(e)),
158 };
159 for entry in read_dir {
160 let entry = entry?;
161 let file_name = entry.file_name().to_string_lossy().to_string();
162 let next_relative = format!("{relative}/{file_name}");
163 let file_type = entry.file_type()?;
164 if file_type.is_dir() {
165 collect_loose_refs(git_dir, &entry.path(), &next_relative, out)?;
166 } else if file_type.is_file() {
167 if let Ok(oid) = crate::refs::resolve_ref(git_dir, &next_relative) {
168 out.insert(next_relative, oid);
169 }
170 }
171 }
172 Ok(())
173}
174
175fn read_packed_refs(git_dir: &Path) -> Result<Vec<(String, ObjectId)>> {
184 let path = git_dir.join("packed-refs");
185 let text = match fs::read_to_string(path) {
186 Ok(t) => t,
187 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
188 Err(e) => return Err(Error::Io(e)),
189 };
190 let mut entries = Vec::new();
191 for line in text.lines() {
192 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
193 continue;
194 }
195 let mut parts = line.split_whitespace();
196 let Some(oid_str) = parts.next() else {
197 continue;
198 };
199 let Some(name) = parts.next() else {
200 continue;
201 };
202 if let Ok(oid) = oid_str.parse::<ObjectId>() {
203 entries.push((name.to_owned(), oid));
204 }
205 }
206 Ok(entries)
207}
208
209fn peel_tag(odb: &Odb, oid: &ObjectId) -> Option<ObjectId> {
215 let obj = odb.read(oid).ok()?;
216 if obj.kind != ObjectKind::Tag {
217 return None;
218 }
219 let text = std::str::from_utf8(&obj.data).ok()?;
220 for line in text.lines() {
221 if let Some(target) = line.strip_prefix("object ") {
222 return target.trim().parse::<ObjectId>().ok();
223 }
224 }
225 None
226}
227
228#[cfg(test)]
229mod tests {
230 use super::pattern_matches;
231
232 #[test]
233 fn pattern_matches_empty_allows_all() {
234 assert!(pattern_matches("refs/heads/main", &[]));
235 assert!(pattern_matches("HEAD", &[]));
236 }
237
238 #[test]
239 fn pattern_matches_exact() {
240 let pats = vec!["HEAD".to_owned()];
241 assert!(pattern_matches("HEAD", &pats));
242 assert!(!pattern_matches("refs/heads/main", &pats));
243 }
244
245 #[test]
246 fn pattern_matches_suffix_component() {
247 let pats = vec!["main".to_owned()];
248 assert!(pattern_matches("refs/heads/main", &pats));
249 assert!(!pattern_matches("refs/heads/notmain", &pats));
250 assert!(!pattern_matches("main-branch", &pats));
251 }
252}