1use std::fs;
19use std::io;
20use std::path::Path;
21
22use crate::error::{Error, Result};
23use crate::objects::ObjectId;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum Ref {
28 Direct(ObjectId),
30 Symbolic(String),
32}
33
34pub fn read_ref_file(path: &Path) -> Result<Ref> {
41 let content = fs::read_to_string(path).map_err(Error::Io)?;
42 let content = content.trim_end_matches('\n');
43 parse_ref_content(content)
44}
45
46pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
48 if let Some(target) = content.strip_prefix("ref: ") {
49 Ok(Ref::Symbolic(target.trim().to_owned()))
50 } else if content.len() == 40 && content.chars().all(|c| c.is_ascii_hexdigit()) {
51 let oid: ObjectId = content.parse()?;
52 Ok(Ref::Direct(oid))
53 } else {
54 Err(Error::InvalidRef(content.to_owned()))
55 }
56}
57
58pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
70 resolve_ref_depth(git_dir, refname, 0)
71}
72
73fn resolve_ref_depth(git_dir: &Path, refname: &str, depth: usize) -> Result<ObjectId> {
75 if depth > 10 {
76 return Err(Error::InvalidRef(format!(
77 "ref symlink too deep: {refname}"
78 )));
79 }
80
81 let path = git_dir.join(refname);
83 match read_ref_file(&path) {
84 Ok(Ref::Direct(oid)) => return Ok(oid),
85 Ok(Ref::Symbolic(target)) => {
86 return resolve_ref_depth(git_dir, &target, depth + 1);
87 }
88 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
89 Err(e) => return Err(e),
90 }
91
92 if let Some(oid) = lookup_packed_ref(git_dir, refname)? {
94 return Ok(oid);
95 }
96
97 Err(Error::InvalidRef(format!("ref not found: {refname}")))
98}
99
100fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
102 let packed = git_dir.join("packed-refs");
103 let content = match fs::read_to_string(&packed) {
104 Ok(c) => c,
105 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
106 Err(e) => return Err(Error::Io(e)),
107 };
108
109 for line in content.lines() {
110 if line.starts_with('#') || line.starts_with('^') {
111 continue;
112 }
113 let mut parts = line.splitn(2, ' ');
114 let hash = parts.next().unwrap_or("");
115 let name = parts.next().unwrap_or("").trim();
116 if name == refname && hash.len() == 40 {
117 let oid: ObjectId = hash.parse()?;
118 return Ok(Some(oid));
119 }
120 }
121 Ok(None)
122}
123
124pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
136 let path = git_dir.join(refname);
137 if let Some(parent) = path.parent() {
138 fs::create_dir_all(parent)?;
139 }
140 let content = format!("{oid}\n");
141 let lock = path.with_extension("lock");
143 fs::write(&lock, &content)?;
144 fs::rename(&lock, &path)?;
145 Ok(())
146}
147
148pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
156 let path = git_dir.join(refname);
157 match fs::remove_file(&path) {
158 Ok(()) => Ok(()),
159 Err(e) if e.kind() == io::ErrorKind::NotFound => {
160 Err(Error::InvalidRef(format!("cannot delete '{refname}': not found")))
161 }
162 Err(e) => Err(Error::Io(e)),
163 }
164}
165
166pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
174 match read_ref_file(&git_dir.join("HEAD"))? {
175 Ref::Symbolic(target) => Ok(Some(target)),
176 Ref::Direct(_) => Ok(None),
177 }
178}
179
180pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
185 let path = git_dir.join(refname);
186 match read_ref_file(&path) {
187 Ok(Ref::Symbolic(target)) => Ok(Some(target)),
188 Ok(Ref::Direct(_)) => Ok(None),
189 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => Ok(None),
190 Err(e) => Err(e),
191 }
192}
193
194pub fn append_reflog(
212 git_dir: &Path,
213 refname: &str,
214 old_oid: &ObjectId,
215 new_oid: &ObjectId,
216 identity: &str,
217 message: &str,
218) -> Result<()> {
219 let log_path = git_dir.join("logs").join(refname);
220 if let Some(parent) = log_path.parent() {
221 fs::create_dir_all(parent)?;
222 }
223 let line = format!("{old_oid} {new_oid} {identity}\t{message}\n");
224 let mut file = fs::OpenOptions::new()
225 .create(true)
226 .append(true)
227 .open(&log_path)?;
228 use io::Write;
229 file.write_all(line.as_bytes())?;
230 Ok(())
231}
232
233pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
241 let base = git_dir.join(prefix);
242 let mut results = Vec::new();
243 collect_refs(&base, prefix, git_dir, &mut results)?;
244 results.sort_by(|a, b| a.0.cmp(&b.0));
245 Ok(results)
246}
247
248pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
250 let glob_pos = pattern.find(|c: char| c == '*' || c == '?' || c == '[');
251 let prefix = match glob_pos {
252 Some(pos) => match pattern[..pos].rfind('/') {
253 Some(slash) => &pattern[..=slash],
254 None => "",
255 },
256 None => pattern,
257 };
258 let all = list_refs(git_dir, prefix)?;
259 let mut results = Vec::new();
260 for (refname, oid) in all {
261 if glob_match(pattern, &refname) {
262 results.push((refname, oid));
263 }
264 }
265 Ok(results)
266}
267
268fn glob_match(pattern: &str, text: &str) -> bool {
269 let pat = pattern.as_bytes();
270 let txt = text.as_bytes();
271 let (mut pi, mut ti) = (0, 0);
272 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
273 while ti < txt.len() {
274 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
275 pi += 1;
276 ti += 1;
277 } else if pi < pat.len() && pat[pi] == b'*' {
278 star_pi = pi;
279 star_ti = ti;
280 pi += 1;
281 } else if star_pi != usize::MAX {
282 pi = star_pi + 1;
283 star_ti += 1;
284 ti = star_ti;
285 } else {
286 return false;
287 }
288 }
289 while pi < pat.len() && pat[pi] == b'*' {
290 pi += 1;
291 }
292 pi == pat.len()
293}
294
295fn collect_refs(
296 dir: &Path,
297 prefix: &str,
298 git_dir: &Path,
299 out: &mut Vec<(String, ObjectId)>,
300) -> Result<()> {
301 let read = match fs::read_dir(dir) {
302 Ok(r) => r,
303 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
304 Err(e) => return Err(Error::Io(e)),
305 };
306
307 for entry in read {
308 let entry = entry?;
309 let file_type = entry.file_type()?;
310 let name = entry.file_name();
311 let name_str = name.to_string_lossy();
312 let refname = format!("{prefix}{name_str}");
313
314 if file_type.is_dir() {
315 collect_refs(&entry.path(), &format!("{refname}/"), git_dir, out)?;
316 } else if file_type.is_file() {
317 if let Ok(oid) = resolve_ref(git_dir, &refname) {
318 out.push((refname, oid))
319 }
320 }
321 }
322 Ok(())
323}