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 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 refs_dir_root = resolve_common_git_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
93
94 let mut all_refs: BTreeMap<String, ObjectId> = BTreeMap::new();
95 collect_loose_refs(
96 &refs_dir_root,
97 &refs_dir_root.join("refs"),
98 "refs",
99 &mut all_refs,
100 )?;
101 for (name, oid) in read_packed_refs(&refs_dir_root)? {
102 all_refs.entry(name).or_insert(oid);
103 }
104
105 for (name, oid) in &all_refs {
106 if let Some(branch_tail) = name.strip_prefix("refs/heads/") {
110 if branch_tail.starts_with("refs/") {
111 continue;
112 }
113 }
114
115 if opts.heads && !name.starts_with("refs/heads/") {
116 continue;
117 }
118 if opts.tags && !name.starts_with("refs/tags/") {
119 continue;
120 }
121 if !pattern_matches(name, &opts.patterns) {
122 continue;
123 }
124
125 entries.push(RefEntry {
126 name: name.clone(),
127 oid: *oid,
128 symref_target: None,
129 });
130
131 if !opts.refs_only && name.starts_with("refs/tags/") {
132 if let Some(peeled) = peel_tag(odb, oid) {
133 entries.push(RefEntry {
134 name: format!("{name}^{{}}"),
135 oid: peeled,
136 symref_target: None,
137 });
138 }
139 }
140 }
141
142 Ok(entries)
143}
144
145fn resolve_common_git_dir(git_dir: &Path) -> Option<PathBuf> {
149 let raw = fs::read_to_string(git_dir.join("commondir")).ok()?;
150 let rel = raw.trim();
151 if rel.is_empty() {
152 return None;
153 }
154 let candidate = if Path::new(rel).is_absolute() {
155 PathBuf::from(rel)
156 } else {
157 git_dir.join(rel)
158 };
159 candidate.canonicalize().ok()
160}
161
162pub fn ref_matches_ls_remote_patterns(refname: &str, patterns: &[String]) -> bool {
172 pattern_matches(refname, patterns)
173}
174
175fn pattern_matches(refname: &str, patterns: &[String]) -> bool {
176 if patterns.is_empty() {
177 return true;
178 }
179 patterns.iter().any(|pat| {
180 if pat.contains('*') || pat.contains('?') {
181 glob_match(pat, refname)
183 } else {
184 refname == pat
185 || refname
186 .strip_suffix(pat.as_str())
187 .is_some_and(|prefix| prefix.ends_with('/'))
188 }
189 })
190}
191
192fn glob_match(pattern: &str, text: &str) -> bool {
194 let pat: Vec<char> = pattern.chars().collect();
195 let txt: Vec<char> = text.chars().collect();
196 let (mut pi, mut ti) = (0, 0);
197 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
198 while ti < txt.len() {
199 if pi < pat.len() && (pat[pi] == '?' || pat[pi] == txt[ti]) {
200 pi += 1;
201 ti += 1;
202 } else if pi < pat.len() && pat[pi] == '*' {
203 star_pi = pi;
204 star_ti = ti;
205 pi += 1;
206 } else if star_pi != usize::MAX {
207 pi = star_pi + 1;
208 star_ti += 1;
209 ti = star_ti;
210 } else {
211 return false;
212 }
213 }
214 while pi < pat.len() && pat[pi] == '*' {
215 pi += 1;
216 }
217 pi == pat.len()
218}
219
220fn collect_loose_refs(
225 git_dir: &Path,
226 path: &Path,
227 relative: &str,
228 out: &mut BTreeMap<String, ObjectId>,
229) -> Result<()> {
230 let read_dir = match fs::read_dir(path) {
231 Ok(rd) => rd,
232 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
233 Err(e) => return Err(Error::Io(e)),
234 };
235 for entry in read_dir {
236 let entry = entry?;
237 let file_name = entry.file_name().to_string_lossy().to_string();
238 let next_relative = format!("{relative}/{file_name}");
239 let file_type = entry.file_type()?;
240 if file_type.is_dir() {
241 collect_loose_refs(git_dir, &entry.path(), &next_relative, out)?;
242 } else if file_type.is_file() {
243 if let Ok(oid) = crate::refs::resolve_ref(git_dir, &next_relative) {
244 out.insert(next_relative, oid);
245 }
246 }
247 }
248 Ok(())
249}
250
251fn read_packed_refs(git_dir: &Path) -> Result<Vec<(String, ObjectId)>> {
260 let path = git_dir.join("packed-refs");
261 let text = match fs::read_to_string(path) {
262 Ok(t) => t,
263 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
264 Err(e) => return Err(Error::Io(e)),
265 };
266 let mut entries = Vec::new();
267 for line in text.lines() {
268 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
269 continue;
270 }
271 let mut parts = line.split_whitespace();
272 let Some(oid_str) = parts.next() else {
273 continue;
274 };
275 let Some(name) = parts.next() else {
276 continue;
277 };
278 if let Ok(oid) = oid_str.parse::<ObjectId>() {
279 entries.push((name.to_owned(), oid));
280 }
281 }
282 Ok(entries)
283}
284
285fn peel_tag(odb: &Odb, oid: &ObjectId) -> Option<ObjectId> {
291 let obj = odb.read(oid).ok()?;
292 if obj.kind != ObjectKind::Tag {
293 return None;
294 }
295 let text = std::str::from_utf8(&obj.data).ok()?;
296 for line in text.lines() {
297 if let Some(target) = line.strip_prefix("object ") {
298 return target.trim().parse::<ObjectId>().ok();
299 }
300 }
301 None
302}
303
304#[cfg(test)]
305mod tests {
306 use super::pattern_matches;
307
308 #[test]
309 fn pattern_matches_empty_allows_all() {
310 assert!(pattern_matches("refs/heads/main", &[]));
311 assert!(pattern_matches("HEAD", &[]));
312 }
313
314 #[test]
315 fn pattern_matches_exact() {
316 let pats = vec!["HEAD".to_owned()];
317 assert!(pattern_matches("HEAD", &pats));
318 assert!(!pattern_matches("refs/heads/main", &pats));
319 }
320
321 #[test]
322 fn pattern_matches_suffix_component() {
323 let pats = vec!["main".to_owned()];
324 assert!(pattern_matches("refs/heads/main", &pats));
325 assert!(!pattern_matches("refs/heads/notmain", &pats));
326 assert!(!pattern_matches("main-branch", &pats));
327 }
328}