1use std::fs;
17use std::io;
18use std::path::{Path, PathBuf};
19
20use crate::error::{Error, Result};
21use crate::objects::ObjectId;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Ref {
26 Direct(ObjectId),
28 Symbolic(String),
30}
31
32pub fn read_ref_file(path: &Path) -> Result<Ref> {
39 let content = fs::read_to_string(path).map_err(Error::Io)?;
40 let content = content.trim_end_matches('\n');
41 parse_ref_content(content)
42}
43
44pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
46 if let Some(target) = content.strip_prefix("ref: ") {
47 Ok(Ref::Symbolic(target.trim().to_owned()))
48 } else if content.len() == 40 && content.chars().all(|c| c.is_ascii_hexdigit()) {
49 let oid: ObjectId = content.parse()?;
50 Ok(Ref::Direct(oid))
51 } else {
52 Err(Error::InvalidRef(content.to_owned()))
53 }
54}
55
56pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
70 if crate::reftable::is_reftable_repo(git_dir) {
71 return crate::reftable::reftable_resolve_ref(git_dir, refname);
72 }
73 let common = common_dir(git_dir);
74 resolve_ref_depth(git_dir, common.as_deref(), refname, 0)
75}
76
77pub fn common_dir(git_dir: &Path) -> Option<PathBuf> {
82 let commondir_file = git_dir.join("commondir");
83 let raw = fs::read_to_string(commondir_file).ok()?;
84 let rel = raw.trim();
85 let path = if Path::new(rel).is_absolute() {
88 PathBuf::from(rel)
89 } else {
90 git_dir.join(rel)
91 };
92 path.canonicalize().ok()
93}
94
95fn notes_merge_state_ref(refname: &str) -> bool {
96 matches!(refname, "NOTES_MERGE_REF" | "NOTES_MERGE_PARTIAL")
97}
98
99fn resolve_ref_depth(
105 git_dir: &Path,
106 common: Option<&Path>,
107 refname: &str,
108 depth: usize,
109) -> Result<ObjectId> {
110 if depth > 10 {
111 return Err(Error::InvalidRef(format!(
112 "ref symlink too deep: {refname}"
113 )));
114 }
115
116 let path = git_dir.join(refname);
118 match read_ref_file(&path) {
119 Ok(Ref::Direct(oid)) => return Ok(oid),
120 Ok(Ref::Symbolic(target)) => {
121 return resolve_ref_depth(git_dir, common, &target, depth + 1);
122 }
123 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
124 Err(e) => return Err(e),
125 }
126
127 if let Some(cdir) = common {
129 if notes_merge_state_ref(refname) {
130 } else if cdir != git_dir {
132 let cpath = cdir.join(refname);
133 match read_ref_file(&cpath) {
134 Ok(Ref::Direct(oid)) => return Ok(oid),
135 Ok(Ref::Symbolic(target)) => {
136 return resolve_ref_depth(git_dir, common, &target, depth + 1);
137 }
138 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
139 Err(e) => return Err(e),
140 }
141 }
142 }
143
144 let packed_dir = common.unwrap_or(git_dir);
146 if let Some(oid) = lookup_packed_ref(packed_dir, refname)? {
147 return Ok(oid);
148 }
149 if common.is_some() && common != Some(git_dir) {
151 if let Some(oid) = lookup_packed_ref(git_dir, refname)? {
152 return Ok(oid);
153 }
154 }
155
156 Err(Error::InvalidRef(format!("ref not found: {refname}")))
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum RawRefLookup {
166 Exists,
168 NotFound,
170 IsDirectory,
172}
173
174pub fn read_raw_ref(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
189 if crate::reftable::is_reftable_repo(git_dir) {
190 read_raw_ref_reftable(git_dir, refname)
191 } else {
192 read_raw_ref_files(git_dir, refname)
193 }
194}
195
196fn read_raw_ref_files(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
197 let common = common_dir(git_dir);
198
199 if let Some(lookup) = read_raw_ref_at(git_dir.join(refname))? {
200 return Ok(lookup);
201 }
202
203 if let Some(cdir) = common.as_ref() {
204 if *cdir != git_dir && !notes_merge_state_ref(refname) {
205 if let Some(lookup) = read_raw_ref_at(cdir.join(refname))? {
206 return Ok(lookup);
207 }
208 }
209 }
210
211 let packed_dir = common.as_deref().unwrap_or(git_dir);
212 if packed_ref_name_exists(packed_dir, refname)? {
213 return Ok(RawRefLookup::Exists);
214 }
215 if common.is_some()
216 && common.as_deref() != Some(git_dir)
217 && packed_ref_name_exists(git_dir, refname)?
218 {
219 return Ok(RawRefLookup::Exists);
220 }
221
222 Ok(RawRefLookup::NotFound)
223}
224
225fn read_raw_ref_at(path: PathBuf) -> Result<Option<RawRefLookup>> {
226 match fs::symlink_metadata(&path) {
227 Ok(meta) => {
228 if meta.is_dir() {
229 return Ok(Some(RawRefLookup::IsDirectory));
230 }
231 Ok(Some(RawRefLookup::Exists))
232 }
233 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
234 Err(e) => Err(Error::Io(e)),
235 }
236}
237
238fn packed_ref_name_exists(git_dir: &Path, refname: &str) -> Result<bool> {
239 let packed = git_dir.join("packed-refs");
240 let content = match fs::read_to_string(&packed) {
241 Ok(c) => c,
242 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
243 Err(e) => return Err(Error::Io(e)),
244 };
245 for line in content.lines() {
246 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
247 continue;
248 }
249 let mut parts = line.split_whitespace();
250 let _oid = parts.next();
251 if let Some(name) = parts.next() {
252 if name == refname {
253 return Ok(true);
254 }
255 }
256 }
257 Ok(false)
258}
259
260fn read_raw_ref_reftable(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
261 if refname == "HEAD" {
262 let head_path = git_dir.join("HEAD");
263 match fs::symlink_metadata(&head_path) {
264 Ok(meta) => {
265 if meta.is_dir() {
266 return Ok(RawRefLookup::IsDirectory);
267 }
268 return Ok(RawRefLookup::Exists);
269 }
270 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(RawRefLookup::NotFound),
271 Err(e) => return Err(Error::Io(e)),
272 }
273 }
274
275 if let Some(lookup) = read_raw_ref_at(git_dir.join(refname))? {
276 return Ok(lookup);
277 }
278
279 let stack = crate::reftable::ReftableStack::open(git_dir)?;
280 match stack.lookup_ref(refname)? {
281 Some(rec) => match rec.value {
282 crate::reftable::RefValue::Deletion => Ok(RawRefLookup::NotFound),
283 _ => Ok(RawRefLookup::Exists),
284 },
285 None => Ok(RawRefLookup::NotFound),
286 }
287}
288
289fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
291 let packed = git_dir.join("packed-refs");
292 let content = match fs::read_to_string(&packed) {
293 Ok(c) => c,
294 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
295 Err(e) => return Err(Error::Io(e)),
296 };
297
298 for line in content.lines() {
299 if line.starts_with('#') || line.starts_with('^') {
300 continue;
301 }
302 let mut parts = line.splitn(2, ' ');
303 let hash = parts.next().unwrap_or("");
304 let name = parts.next().unwrap_or("").trim();
305 if name == refname && hash.len() == 40 {
306 let oid: ObjectId = hash.parse()?;
307 return Ok(Some(oid));
308 }
309 }
310 Ok(None)
311}
312
313pub fn write_symbolic_ref(git_dir: &Path, refname: &str, target: &str) -> Result<()> {
330 if crate::reftable::is_reftable_repo(git_dir) {
331 return crate::reftable::reftable_write_symref(git_dir, refname, target, None, None);
332 }
333 let storage_dir = ref_storage_dir(git_dir, refname);
334 let path = storage_dir.join(refname);
335 if let Some(parent) = path.parent() {
336 fs::create_dir_all(parent)?;
337 }
338 let content = format!("ref: {target}\n");
339 let lock = path.with_extension("lock");
340 fs::write(&lock, &content)?;
341 fs::rename(&lock, &path)?;
342 Ok(())
343}
344
345pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
346 if crate::reftable::is_reftable_repo(git_dir) {
347 return crate::reftable::reftable_write_ref(git_dir, refname, oid, None, None);
348 }
349 let storage_dir = ref_storage_dir(git_dir, refname);
350 let path = storage_dir.join(refname);
351 if let Some(parent) = path.parent() {
352 fs::create_dir_all(parent)?;
353 }
354 let content = format!("{oid}\n");
355 let lock = path.with_extension("lock");
357 fs::write(&lock, &content)?;
358 fs::rename(&lock, &path)?;
359 Ok(())
360}
361
362pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
370 if crate::reftable::is_reftable_repo(git_dir) {
371 return crate::reftable::reftable_delete_ref(git_dir, refname);
372 }
373 let storage_dir = ref_storage_dir(git_dir, refname);
374 let path = storage_dir.join(refname);
376 match fs::remove_file(&path) {
377 Ok(()) => {}
378 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
379 Err(e) => return Err(Error::Io(e)),
380 }
381
382 remove_packed_ref(&storage_dir, refname)?;
384
385 let log_path = storage_dir.join("logs").join(refname);
386
387 if !refname.starts_with("refs/heads/") {
390 let _ = fs::remove_file(&log_path);
391
392 let logs_heads = storage_dir.join("logs/refs/heads");
396 let mut parent = log_path.parent();
397 while let Some(p) = parent {
398 if p == logs_heads.as_path() || !p.starts_with(&logs_heads) {
399 break;
400 }
401 if fs::remove_dir(p).is_err() {
402 break;
403 }
404 parent = p.parent();
405 }
406 }
407
408 Ok(())
409}
410
411fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
413 let packed_path = git_dir.join("packed-refs");
414 let content = match fs::read_to_string(&packed_path) {
415 Ok(c) => c,
416 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
417 Err(e) => return Err(Error::Io(e)),
418 };
419
420 let mut out = String::new();
421 let mut skip_peeled = false;
422 let mut changed = false;
423 let mut header_written = false;
426
427 for line in content.lines() {
428 if skip_peeled {
429 if line.starts_with('^') {
430 changed = true;
431 continue;
432 }
433 skip_peeled = false;
434 }
435
436 if line.starts_with('#') {
437 continue;
439 }
440 if line.starts_with('^') {
441 out.push_str(line);
442 out.push('\n');
443 continue;
444 }
445
446 if !header_written {
448 out.insert_str(0, "# pack-refs with: peeled fully-peeled sorted\n");
449 header_written = true;
450 }
451
452 let mut parts = line.splitn(2, ' ');
454 let _hash = parts.next().unwrap_or("");
455 let name = parts.next().unwrap_or("").trim();
456 if name == refname {
457 changed = true;
458 skip_peeled = true;
459 continue;
460 }
461
462 out.push_str(line);
463 out.push('\n');
464 }
465
466 if changed {
467 let lock = packed_path.with_extension("lock");
468 fs::write(&lock, &out).map_err(Error::Io)?;
469 fs::rename(&lock, &packed_path).map_err(Error::Io)?;
470 }
471
472 Ok(())
473}
474
475pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
483 match read_ref_file(&git_dir.join("HEAD"))? {
484 Ref::Symbolic(target) => Ok(Some(target)),
485 Ref::Direct(_) => Ok(None),
486 }
487}
488
489pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
496 if crate::reftable::is_reftable_repo(git_dir) {
497 return crate::reftable::reftable_read_symbolic_ref(git_dir, refname);
498 }
499 let path = git_dir.join(refname);
500 match read_ref_file(&path) {
501 Ok(Ref::Symbolic(target)) => Ok(Some(target)),
502 Ok(Ref::Direct(_)) => Ok(None),
503 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {
504 if !notes_merge_state_ref(refname) {
505 if let Some(common) = common_dir(git_dir) {
506 if common != git_dir {
507 let cpath = common.join(refname);
508 match read_ref_file(&cpath) {
509 Ok(Ref::Symbolic(target)) => return Ok(Some(target)),
510 Ok(Ref::Direct(_)) => return Ok(None),
511 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
512 Err(e) => return Err(e),
513 }
514 }
515 }
516 }
517 Ok(None)
518 }
519 Err(e) => Err(e),
520 }
521}
522
523#[derive(Clone, Copy, Debug, PartialEq, Eq)]
525pub enum LogRefsConfig {
526 Unset,
528 None,
530 Normal,
532 Always,
534}
535
536pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
540 let config_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
541 let config_path = config_dir.join("config");
542 let content = match fs::read_to_string(config_path) {
543 Ok(c) => c,
544 Err(_) => return LogRefsConfig::Unset,
545 };
546
547 let mut in_core = false;
548 for line in content.lines() {
549 let trimmed = line.trim();
550 if trimmed.starts_with('[') {
551 in_core = trimmed.to_ascii_lowercase().starts_with("[core]");
552 continue;
553 }
554 if !in_core {
555 continue;
556 }
557 let Some((key, value)) = trimmed.split_once('=') else {
558 continue;
559 };
560 if !key.trim().eq_ignore_ascii_case("logallrefupdates") {
561 continue;
562 }
563 let v = value.trim();
564 let lower = v.to_ascii_lowercase();
565 return match lower.as_str() {
566 "always" => LogRefsConfig::Always,
567 "1" | "true" | "yes" | "on" => LogRefsConfig::Normal,
568 "0" | "false" | "no" | "off" | "never" => LogRefsConfig::None,
569 _ => LogRefsConfig::Unset,
570 };
571 }
572 LogRefsConfig::Unset
573}
574
575fn read_core_bare(git_dir: &Path) -> bool {
576 let config_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
577 let config_path = config_dir.join("config");
578 let Ok(content) = fs::read_to_string(config_path) else {
579 return false;
580 };
581 let mut in_core = false;
582 for line in content.lines() {
583 let trimmed = line.trim();
584 if trimmed.starts_with('[') {
585 in_core = trimmed.to_ascii_lowercase().starts_with("[core]");
586 continue;
587 }
588 if !in_core {
589 continue;
590 }
591 let Some((key, value)) = trimmed.split_once('=') else {
592 continue;
593 };
594 if key.trim().eq_ignore_ascii_case("bare") {
595 let v = value.trim().to_ascii_lowercase();
596 return matches!(v.as_str(), "1" | "true" | "yes" | "on");
597 }
598 }
599 false
600}
601
602pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
604 match read_log_refs_config(git_dir) {
605 LogRefsConfig::Unset => {
606 if read_core_bare(git_dir) {
607 LogRefsConfig::None
608 } else {
609 LogRefsConfig::Normal
610 }
611 }
612 other => other,
613 }
614}
615
616#[must_use]
619pub fn should_autocreate_reflog_for_mode(refname: &str, mode: LogRefsConfig) -> bool {
620 match mode {
621 LogRefsConfig::Always => true,
622 LogRefsConfig::Normal => {
623 refname == "HEAD"
624 || refname.starts_with("refs/heads/")
625 || refname.starts_with("refs/remotes/")
626 || refname.starts_with("refs/notes/")
627 }
628 LogRefsConfig::None | LogRefsConfig::Unset => false,
629 }
630}
631
632#[must_use]
634pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
635 should_autocreate_reflog_for_mode(refname, effective_log_refs_config(git_dir))
636}
637
638pub fn append_reflog(
656 git_dir: &Path,
657 refname: &str,
658 old_oid: &ObjectId,
659 new_oid: &ObjectId,
660 identity: &str,
661 message: &str,
662 force_create: bool,
663) -> Result<()> {
664 if crate::reftable::is_reftable_repo(git_dir) {
665 return crate::reftable::reftable_append_reflog(
666 git_dir,
667 refname,
668 old_oid,
669 new_oid,
670 identity,
671 message,
672 force_create,
673 );
674 }
675 let storage_dir = ref_storage_dir(git_dir, refname);
676 let log_path = storage_dir.join("logs").join(refname);
677 let may_write =
678 force_create || should_autocreate_reflog(git_dir, refname) || !message.is_empty();
679 if !may_write && !log_path.exists() {
680 return Ok(());
681 }
682 if let Some(parent) = log_path.parent() {
683 fs::create_dir_all(parent)?;
684 }
685 let line = if message.is_empty() {
686 format!("{old_oid} {new_oid} {identity}\n")
687 } else {
688 format!("{old_oid} {new_oid} {identity}\t{message}\n")
689 };
690 let mut file = fs::OpenOptions::new()
691 .create(true)
692 .append(true)
693 .open(&log_path)?;
694 use io::Write;
695 file.write_all(line.as_bytes())?;
696 Ok(())
697}
698
699fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
700 if refname == "HEAD" || refname == "NOTES_MERGE_PARTIAL" || refname == "NOTES_MERGE_REF" {
704 return git_dir.to_path_buf();
705 }
706 common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
707}
708
709pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
719 if crate::reftable::is_reftable_repo(git_dir) {
720 return crate::reftable::reftable_list_refs(git_dir, prefix);
721 }
722 let mut results = Vec::new();
723 let base = git_dir.join(prefix);
724 collect_refs(&base, prefix, git_dir, &mut results)?;
725 collect_packed_refs(git_dir, prefix, &mut results)?;
726
727 if let Some(cdir) = common_dir(git_dir) {
729 if cdir != git_dir {
730 let cbase = cdir.join(prefix);
731 collect_refs(&cbase, prefix, &cdir, &mut results)?;
732 collect_packed_refs(&cdir, prefix, &mut results)?;
733 results.sort_by(|a, b| a.0.cmp(&b.0));
735 results.dedup_by(|b, a| a.0 == b.0);
736 }
737 }
738
739 results.sort_by(|a, b| a.0.cmp(&b.0));
740 Ok(results)
741}
742
743pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
745 let glob_pos = pattern.find(['*', '?', '[']);
746 let prefix = match glob_pos {
747 Some(pos) => match pattern[..pos].rfind('/') {
748 Some(slash) => &pattern[..=slash],
749 None => "",
750 },
751 None => pattern,
752 };
753 let all = list_refs(git_dir, prefix)?;
754 let mut results = Vec::new();
755 for (refname, oid) in all {
756 if glob_match(pattern, &refname) {
757 results.push((refname, oid));
758 }
759 }
760 Ok(results)
761}
762
763pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
767 if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
769 return refname == pattern
770 || refname.ends_with(&format!("/{pattern}"))
771 || refname.starts_with(&format!("{pattern}/"));
772 }
773 glob_match(pattern, refname)
774}
775
776fn glob_match(pattern: &str, text: &str) -> bool {
777 let pat = pattern.as_bytes();
778 let txt = text.as_bytes();
779 let (mut pi, mut ti) = (0, 0);
780 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
781 while ti < txt.len() {
782 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
783 pi += 1;
784 ti += 1;
785 } else if pi < pat.len() && pat[pi] == b'*' {
786 star_pi = pi;
787 star_ti = ti;
788 pi += 1;
789 } else if star_pi != usize::MAX {
790 pi = star_pi + 1;
791 star_ti += 1;
792 ti = star_ti;
793 } else {
794 return false;
795 }
796 }
797 while pi < pat.len() && pat[pi] == b'*' {
798 pi += 1;
799 }
800 pi == pat.len()
801}
802
803fn collect_refs(
804 dir: &Path,
805 prefix: &str,
806 git_dir: &Path,
807 out: &mut Vec<(String, ObjectId)>,
808) -> Result<()> {
809 let read = match fs::read_dir(dir) {
810 Ok(r) => r,
811 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
812 Err(e) => return Err(Error::Io(e)),
813 };
814
815 for entry in read {
816 let entry = entry?;
817 let name = entry.file_name();
818 let name_str = name.to_string_lossy();
819 let refname = format!("{prefix}{name_str}");
820 let path = entry.path();
821 let meta = match fs::metadata(&path) {
823 Ok(m) => m,
824 Err(_) => continue,
825 };
826
827 if meta.is_dir() {
828 collect_refs(&path, &format!("{refname}/"), git_dir, out)?;
829 } else if meta.is_file() {
830 if let Ok(oid) = resolve_ref(git_dir, &refname) {
831 out.push((refname, oid))
832 }
833 }
834 }
835 Ok(())
836}
837
838pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
841 let inner = spec
843 .strip_prefix("@{-")
844 .and_then(|s| s.strip_suffix('}'))
845 .ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
846 let n: usize = inner
847 .parse()
848 .map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
849 if n == 0 {
850 return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
851 }
852 let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
853 let mut count = 0usize;
854 for entry in entries.iter().rev() {
855 let msg = &entry.message;
856 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
857 count += 1;
858 if count == n {
859 if let Some(to_pos) = rest.find(" to ") {
860 return Ok(rest[..to_pos].to_string());
861 }
862 }
863 }
864 }
865 Err(Error::InvalidRef(format!(
866 "{spec}: only {count} checkout(s) in reflog"
867 )))
868}
869
870fn collect_packed_refs(
871 git_dir: &Path,
872 prefix: &str,
873 out: &mut Vec<(String, ObjectId)>,
874) -> Result<()> {
875 let packed_path = git_dir.join("packed-refs");
876 let content = match fs::read_to_string(&packed_path) {
877 Ok(c) => c,
878 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
879 Err(e) => return Err(Error::Io(e)),
880 };
881
882 for line in content.lines() {
883 if line.starts_with('#') || line.starts_with('^') || line.is_empty() {
884 continue;
885 }
886 let mut parts = line.splitn(2, ' ');
887 let hash = parts.next().unwrap_or("");
888 let refname = parts.next().unwrap_or("").trim();
889 if !refname.starts_with(prefix) || hash.len() != 40 {
890 continue;
891 }
892 let oid: ObjectId = hash.parse()?;
893 out.push((refname.to_string(), oid));
894 }
895 Ok(())
896}
897
898#[cfg(test)]
899mod read_raw_ref_tests {
900 use super::*;
901 use tempfile::tempdir;
902
903 #[test]
904 fn loose_ref_file_is_exists() {
905 let dir = tempdir().unwrap();
906 let git_dir = dir.path();
907 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
908 fs::write(
909 git_dir.join("refs/heads/side"),
910 "0000000000000000000000000000000000000000\n",
911 )
912 .unwrap();
913 assert_eq!(
914 read_raw_ref(git_dir, "refs/heads/side").unwrap(),
915 RawRefLookup::Exists
916 );
917 }
918
919 #[test]
920 fn missing_ref_is_not_found() {
921 let dir = tempdir().unwrap();
922 let git_dir = dir.path();
923 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
924 assert_eq!(
925 read_raw_ref(git_dir, "refs/heads/nope").unwrap(),
926 RawRefLookup::NotFound
927 );
928 }
929
930 #[test]
931 fn directory_where_ref_expected_is_is_directory() {
932 let dir = tempdir().unwrap();
933 let git_dir = dir.path();
934 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
935 assert_eq!(
936 read_raw_ref(git_dir, "refs/heads").unwrap(),
937 RawRefLookup::IsDirectory
938 );
939 }
940
941 #[test]
942 fn packed_ref_name_is_exists() {
943 let dir = tempdir().unwrap();
944 let git_dir = dir.path();
945 fs::write(
946 git_dir.join("packed-refs"),
947 "# pack-refs with: peeled fully-peeled \n\
948 0000000000000000000000000000000000000000 refs/heads/packed\n",
949 )
950 .unwrap();
951 assert_eq!(
952 read_raw_ref(git_dir, "refs/heads/packed").unwrap(),
953 RawRefLookup::Exists
954 );
955 }
956}