1use std::collections::BTreeMap;
13use std::fs;
14use std::io;
15use std::path::{Path, PathBuf};
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 all_symrefs: bool,
53 pub patterns: Vec<String>,
58}
59
60pub fn ls_remote(git_dir: &Path, odb: &Odb, opts: &Options) -> Result<Vec<RefEntry>> {
76 let mut entries = Vec::new();
77
78 let include_head = !opts.heads && !opts.tags && !opts.refs_only;
79 if include_head {
80 if let Ok(head_oid) = crate::refs::resolve_ref(git_dir, "HEAD") {
81 let symref_target = if opts.symref {
82 crate::refs::read_symbolic_ref(git_dir, "HEAD")?
83 } else {
84 None
85 };
86 if pattern_matches("HEAD", &opts.patterns) {
87 entries.push(RefEntry {
88 name: "HEAD".to_owned(),
89 oid: head_oid,
90 symref_target,
91 });
92 }
93 }
94 }
95
96 let refs_dir_root = resolve_common_git_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
100
101 let mut all_refs: BTreeMap<String, ObjectId> = BTreeMap::new();
102 collect_loose_refs(
103 &refs_dir_root,
104 &refs_dir_root.join("refs"),
105 "refs",
106 &mut all_refs,
107 )?;
108 for (name, oid) in read_packed_refs(&refs_dir_root)? {
109 all_refs.entry(name).or_insert(oid);
110 }
111
112 for (name, oid) in &all_refs {
113 if let Some(branch_tail) = name.strip_prefix("refs/heads/") {
117 if branch_tail.starts_with("refs/") {
118 continue;
119 }
120 }
121
122 if opts.heads || opts.tags {
126 let is_branch = opts.heads && name.starts_with("refs/heads/");
127 let is_tag = opts.tags && name.starts_with("refs/tags/");
128 if !is_branch && !is_tag {
129 continue;
130 }
131 }
132 if !pattern_matches(name, &opts.patterns) {
133 continue;
134 }
135
136 let symref_target = if opts.symref && opts.all_symrefs {
137 crate::refs::read_symbolic_ref(&refs_dir_root, name)
138 .ok()
139 .flatten()
140 } else {
141 None
142 };
143
144 entries.push(RefEntry {
145 name: name.clone(),
146 oid: *oid,
147 symref_target,
148 });
149
150 if !opts.refs_only && name.starts_with("refs/tags/") {
151 if let Some(peeled) = peel_tag(odb, oid) {
152 entries.push(RefEntry {
153 name: format!("{name}^{{}}"),
154 oid: peeled,
155 symref_target: None,
156 });
157 }
158 }
159 }
160
161 Ok(entries)
162}
163
164fn resolve_common_git_dir(git_dir: &Path) -> Option<PathBuf> {
168 let raw = fs::read_to_string(git_dir.join("commondir")).ok()?;
169 let rel = raw.trim();
170 if rel.is_empty() {
171 return None;
172 }
173 let candidate = if Path::new(rel).is_absolute() {
174 PathBuf::from(rel)
175 } else {
176 git_dir.join(rel)
177 };
178 candidate.canonicalize().ok()
179}
180
181pub fn ref_matches_ls_remote_patterns(refname: &str, patterns: &[String]) -> bool {
191 pattern_matches(refname, patterns)
192}
193
194fn pattern_matches(refname: &str, patterns: &[String]) -> bool {
195 if patterns.is_empty() {
196 return true;
197 }
198 let path = format!("/{refname}");
201 patterns.iter().any(|pat| {
202 let full = format!("*/{pat}");
203 glob_match(&full, &path)
204 })
205}
206
207fn glob_match(pattern: &str, text: &str) -> bool {
209 let pat: Vec<char> = pattern.chars().collect();
210 let txt: Vec<char> = text.chars().collect();
211 let (mut pi, mut ti) = (0, 0);
212 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
213 while ti < txt.len() {
214 if pi < pat.len() && (pat[pi] == '?' || pat[pi] == txt[ti]) {
215 pi += 1;
216 ti += 1;
217 } else if pi < pat.len() && pat[pi] == '*' {
218 star_pi = pi;
219 star_ti = ti;
220 pi += 1;
221 } else if star_pi != usize::MAX {
222 pi = star_pi + 1;
223 star_ti += 1;
224 ti = star_ti;
225 } else {
226 return false;
227 }
228 }
229 while pi < pat.len() && pat[pi] == '*' {
230 pi += 1;
231 }
232 pi == pat.len()
233}
234
235fn collect_loose_refs(
240 git_dir: &Path,
241 path: &Path,
242 relative: &str,
243 out: &mut BTreeMap<String, ObjectId>,
244) -> Result<()> {
245 let read_dir = match fs::read_dir(path) {
246 Ok(rd) => rd,
247 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
248 Err(e) => return Err(Error::Io(e)),
249 };
250 for entry in read_dir {
251 let entry = entry?;
252 let file_name = entry.file_name().to_string_lossy().to_string();
253 let next_relative = format!("{relative}/{file_name}");
254 let file_type = entry.file_type()?;
255 if file_type.is_dir() {
256 collect_loose_refs(git_dir, &entry.path(), &next_relative, out)?;
257 } else if file_type.is_file() {
258 if let Ok(oid) = crate::refs::resolve_ref(git_dir, &next_relative) {
259 out.insert(next_relative, oid);
260 }
261 }
262 }
263 Ok(())
264}
265
266fn read_packed_refs(git_dir: &Path) -> Result<Vec<(String, ObjectId)>> {
275 let path = git_dir.join("packed-refs");
276 let text = match fs::read_to_string(path) {
277 Ok(t) => t,
278 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
279 Err(e) => return Err(Error::Io(e)),
280 };
281 let mut entries = Vec::new();
282 for line in text.lines() {
283 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
284 continue;
285 }
286 let mut parts = line.split_whitespace();
287 let Some(oid_str) = parts.next() else {
288 continue;
289 };
290 let Some(name) = parts.next() else {
291 continue;
292 };
293 if let Ok(oid) = oid_str.parse::<ObjectId>() {
294 entries.push((name.to_owned(), oid));
295 }
296 }
297 Ok(entries)
298}
299
300fn peel_tag(odb: &Odb, oid: &ObjectId) -> Option<ObjectId> {
306 let obj = odb.read(oid).ok()?;
307 if obj.kind != ObjectKind::Tag {
308 return None;
309 }
310 let text = std::str::from_utf8(&obj.data).ok()?;
311 for line in text.lines() {
312 if let Some(target) = line.strip_prefix("object ") {
313 return target.trim().parse::<ObjectId>().ok();
314 }
315 }
316 None
317}
318
319#[cfg(test)]
320mod tests {
321 use super::pattern_matches;
322
323 #[test]
324 fn pattern_matches_empty_allows_all() {
325 assert!(pattern_matches("refs/heads/main", &[]));
326 assert!(pattern_matches("HEAD", &[]));
327 }
328
329 #[test]
330 fn pattern_matches_exact() {
331 let pats = vec!["HEAD".to_owned()];
332 assert!(pattern_matches("HEAD", &pats));
333 assert!(!pattern_matches("refs/heads/main", &pats));
334 }
335
336 #[test]
337 fn pattern_matches_suffix_component() {
338 let pats = vec!["main".to_owned()];
339 assert!(pattern_matches("refs/heads/main", &pats));
340 assert!(!pattern_matches("refs/heads/notmain", &pats));
341 assert!(!pattern_matches("main-branch", &pats));
342 }
343}