1use std::collections::{BTreeSet, HashMap, HashSet};
17use std::fs;
18use std::io;
19use std::path::{Path, PathBuf};
20
21use crate::config::ConfigSet;
22use crate::error::{Error, Result};
23use crate::objects::ObjectId;
24use crate::pack;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Ref {
29 Direct(ObjectId),
31 Symbolic(String),
33}
34
35pub fn read_ref_file(path: &Path) -> Result<Ref> {
42 let content = match fs::read_to_string(path) {
43 Ok(c) => c,
44 Err(e)
48 if e.kind() == io::ErrorKind::IsADirectory
49 || e.raw_os_error() == Some(libc::EISDIR) =>
50 {
51 return Err(Error::Io(io::Error::new(io::ErrorKind::NotFound, e)));
52 }
53 Err(e) => return Err(Error::Io(e)),
54 };
55 let content = content.trim_end_matches('\n');
56 parse_ref_content(content)
57}
58
59pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
61 if let Some(target) = content.strip_prefix("ref: ") {
62 Ok(Ref::Symbolic(target.trim().to_owned()))
63 } else if content.len() == 40 && content.chars().all(|c| c.is_ascii_hexdigit()) {
64 let oid: ObjectId = content.parse()?;
65 Ok(Ref::Direct(oid))
66 } else if content == "unknown-oid" {
67 const PLACEHOLDER: &[u8; 20] = b"GritUnknownOidPlc!X!";
71 let oid = ObjectId::from_bytes(PLACEHOLDER)?;
72 Ok(Ref::Direct(oid))
73 } else {
74 Err(Error::InvalidRef(content.to_owned()))
75 }
76}
77
78pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
92 if crate::reftable::is_reftable_repo(git_dir) {
93 return crate::reftable::reftable_resolve_ref(git_dir, refname);
94 }
95 let common = common_dir(git_dir);
96 resolve_ref_depth(git_dir, common.as_deref(), refname, 0)
97}
98
99pub fn common_dir(git_dir: &Path) -> Option<PathBuf> {
104 let commondir_file = git_dir.join("commondir");
105 let raw = fs::read_to_string(commondir_file).ok()?;
106 let rel = raw.trim();
107 let path = if Path::new(rel).is_absolute() {
110 PathBuf::from(rel)
111 } else {
112 git_dir.join(rel)
113 };
114 path.canonicalize().ok()
115}
116
117fn notes_merge_state_ref(refname: &str) -> bool {
118 matches!(refname, "NOTES_MERGE_REF" | "NOTES_MERGE_PARTIAL")
119}
120
121fn resolve_ref_depth(
127 git_dir: &Path,
128 common: Option<&Path>,
129 refname: &str,
130 depth: usize,
131) -> Result<ObjectId> {
132 if depth > 10 {
133 return Err(Error::InvalidRef(format!(
134 "ref symlink too deep: {refname}"
135 )));
136 }
137
138 let storage_owned = crate::ref_namespace::storage_ref_name(refname);
139 let try_names: Vec<&str> =
140 if refname == "HEAD" && crate::ref_namespace::ref_storage_prefix().is_some() {
141 vec![storage_owned.as_str()]
142 } else if storage_owned != refname {
143 vec![storage_owned.as_str(), refname]
144 } else {
145 vec![refname]
146 };
147
148 for name in try_names {
149 let path = git_dir.join(name);
150 match read_ref_file(&path) {
151 Ok(Ref::Direct(oid)) => return Ok(oid),
152 Ok(Ref::Symbolic(target)) => {
153 return resolve_ref_depth(git_dir, common, &target, depth + 1);
154 }
155 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
156 Err(e) => return Err(e),
157 }
158
159 if let Some(cdir) = common {
160 if notes_merge_state_ref(name) {
161 } else if cdir != git_dir {
162 let cpath = cdir.join(name);
163 match read_ref_file(&cpath) {
164 Ok(Ref::Direct(oid)) => return Ok(oid),
165 Ok(Ref::Symbolic(target)) => {
166 return resolve_ref_depth(git_dir, common, &target, depth + 1);
167 }
168 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
169 Err(e) => return Err(e),
170 }
171 }
172 }
173
174 let packed_dir = common.unwrap_or(git_dir);
175 if let Some(oid) = lookup_packed_ref(packed_dir, name)? {
176 return Ok(oid);
177 }
178 if common.is_some() && common != Some(git_dir) {
179 if let Some(oid) = lookup_packed_ref(git_dir, name)? {
180 return Ok(oid);
181 }
182 }
183 }
184
185 Err(Error::InvalidRef(format!("ref not found: {refname}")))
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum RawRefLookup {
195 Exists,
197 NotFound,
199 IsDirectory,
201}
202
203pub fn read_raw_ref(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
218 if crate::reftable::is_reftable_repo(git_dir) {
219 read_raw_ref_reftable(git_dir, refname)
220 } else {
221 read_raw_ref_files(git_dir, refname)
222 }
223}
224
225fn read_raw_ref_files(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
226 let common = common_dir(git_dir);
227 let storage_owned = crate::ref_namespace::storage_ref_name(refname);
228 let (names, n): ([&str; 2], usize) = if storage_owned != refname {
229 ([storage_owned.as_str(), refname], 2)
230 } else {
231 ([refname, refname], 1)
232 };
233
234 for name in names.iter().take(n) {
235 if let Some(lookup) = read_raw_ref_at(git_dir.join(name))? {
236 return Ok(lookup);
237 }
238
239 if let Some(cdir) = common.as_ref() {
240 if *cdir != git_dir && !notes_merge_state_ref(name) {
241 if let Some(lookup) = read_raw_ref_at(cdir.join(name))? {
242 return Ok(lookup);
243 }
244 }
245 }
246
247 let packed_dir = common.as_deref().unwrap_or(git_dir);
248 if packed_ref_name_exists(packed_dir, name)? {
249 return Ok(RawRefLookup::Exists);
250 }
251 if common.is_some()
252 && common.as_deref() != Some(git_dir)
253 && packed_ref_name_exists(git_dir, name)?
254 {
255 return Ok(RawRefLookup::Exists);
256 }
257 }
258
259 Ok(RawRefLookup::NotFound)
260}
261
262#[must_use]
264pub fn lock_path_for_ref(path: &Path) -> PathBuf {
265 let mut s = path.as_os_str().to_owned();
266 s.push(".lock");
267 PathBuf::from(s)
268}
269
270fn read_raw_ref_at(path: PathBuf) -> Result<Option<RawRefLookup>> {
271 match fs::symlink_metadata(&path) {
272 Ok(meta) => {
273 if meta.is_dir() {
274 return Ok(Some(RawRefLookup::IsDirectory));
275 }
276 Ok(Some(RawRefLookup::Exists))
277 }
278 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
279 Err(e) => Err(Error::Io(e)),
280 }
281}
282
283fn packed_ref_with_prefix(git_dir: &Path, prefix_with_slash: &str) -> Result<Option<String>> {
284 let packed = git_dir.join("packed-refs");
285 let content = match fs::read_to_string(&packed) {
286 Ok(c) => c,
287 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
288 Err(e) => return Err(Error::Io(e)),
289 };
290 let mut best: Option<String> = None;
291 for line in content.lines() {
292 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
293 continue;
294 }
295 let mut parts = line.split_whitespace();
296 let _oid = parts.next();
297 let Some(name) = parts.next() else {
298 continue;
299 };
300 let name = name.trim();
301 if name.starts_with(prefix_with_slash) {
302 let take = match &best {
303 None => true,
304 Some(b) => name < b.as_str(),
305 };
306 if take {
307 best = Some(name.to_owned());
308 }
309 }
310 }
311 Ok(best)
312}
313
314fn packed_ref_name_exists(git_dir: &Path, refname: &str) -> Result<bool> {
315 let packed = git_dir.join("packed-refs");
316 let content = match fs::read_to_string(&packed) {
317 Ok(c) => c,
318 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
319 Err(e) => return Err(Error::Io(e)),
320 };
321 for line in content.lines() {
322 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
323 continue;
324 }
325 let mut parts = line.split_whitespace();
326 let _oid = parts.next();
327 if let Some(name) = parts.next() {
328 if name == refname {
329 return Ok(true);
330 }
331 }
332 }
333 Ok(false)
334}
335
336fn refname_namespace_conflicts(existing: &str, candidate: &str) -> bool {
337 if existing == candidate {
338 return false;
339 }
340 existing
341 .strip_prefix(candidate)
342 .is_some_and(|rest| rest.starts_with('/'))
343 || candidate
344 .strip_prefix(existing)
345 .is_some_and(|rest| rest.starts_with('/'))
346}
347
348fn packed_ref_namespace_conflict(git_dir: &Path, refname: &str) -> Result<bool> {
349 let packed = git_dir.join("packed-refs");
350 let content = match fs::read_to_string(&packed) {
351 Ok(c) => c,
352 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
353 Err(e) => return Err(Error::Io(e)),
354 };
355 for line in content.lines() {
356 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
357 continue;
358 }
359 let mut parts = line.split_whitespace();
360 let _oid = parts.next();
361 if let Some(name) = parts.next() {
362 if refname_namespace_conflicts(name, refname) {
363 return Ok(true);
364 }
365 }
366 }
367 Ok(false)
368}
369
370pub fn packed_refs_entry_exists(git_dir: &Path, refname: &str) -> Result<bool> {
382 if crate::reftable::is_reftable_repo(git_dir) || refname == "HEAD" {
383 return Ok(false);
384 }
385 let storage_dir = ref_storage_dir(git_dir, refname);
386 packed_ref_name_exists(&storage_dir, refname)
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
391pub enum RefnameUnavailable {
392 AncestorExists {
394 blocking: String,
396 new_ref: String,
398 },
399 DescendantExists {
401 blocking: String,
403 new_ref: String,
405 },
406 SameBatch {
408 refname: String,
410 other: String,
412 },
413}
414
415impl RefnameUnavailable {
416 #[must_use]
418 pub fn lock_message_suffix(&self) -> String {
419 match self {
420 RefnameUnavailable::AncestorExists { blocking, new_ref } => {
421 format!("'{blocking}' exists; cannot create '{new_ref}'")
422 }
423 RefnameUnavailable::DescendantExists { blocking, new_ref } => {
424 format!("'{blocking}' exists; cannot create '{new_ref}'")
425 }
426 RefnameUnavailable::SameBatch { refname, other } => {
427 format!("cannot process '{refname}' and '{other}' at the same time")
428 }
429 }
430 }
431}
432
433fn find_descendant_in_sorted_extras(
434 dirname_with_slash: &str,
435 extras: &BTreeSet<String>,
436) -> Option<String> {
437 let start = extras
438 .range(dirname_with_slash.to_string()..)
439 .next()
440 .cloned()?;
441 if start.starts_with(dirname_with_slash) {
442 Some(start)
443 } else {
444 None
445 }
446}
447
448pub fn verify_refname_available_for_create(
461 git_dir: &Path,
462 refname: &str,
463 extras: &BTreeSet<String>,
464 skip: &HashSet<String>,
465) -> std::result::Result<(), RefnameUnavailable> {
466 let git_dir = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
469 let mut seen_dirnames: HashSet<String> = HashSet::new();
470 let segments: Vec<&str> = refname.split('/').filter(|s| !s.is_empty()).collect();
471 if segments.len() <= 1 {
472 } else {
474 let mut dirname = String::new();
475 for part in &segments[..segments.len() - 1] {
476 if !dirname.is_empty() {
477 dirname.push('/');
478 }
479 dirname.push_str(part);
480
481 if !seen_dirnames.insert(dirname.clone()) {
482 continue;
483 }
484
485 if skip.contains(&dirname) {
486 continue;
487 }
488
489 match read_raw_ref(&git_dir, &dirname) {
490 Ok(RawRefLookup::Exists) => {
491 return Err(RefnameUnavailable::AncestorExists {
492 blocking: dirname.clone(),
493 new_ref: refname.to_owned(),
494 });
495 }
496 Ok(RawRefLookup::NotFound | RawRefLookup::IsDirectory) => {}
499 Err(_) => {}
500 }
501
502 if extras.contains(&dirname) {
503 return Err(RefnameUnavailable::SameBatch {
504 refname: refname.to_owned(),
505 other: dirname.clone(),
506 });
507 }
508 }
509 }
510
511 let mut leaf_dir = String::with_capacity(refname.len() + 1);
512 leaf_dir.push_str(refname);
513 leaf_dir.push('/');
514
515 let under = list_refs(&git_dir, &leaf_dir).unwrap_or_default();
516 if under.is_empty() {
517 let packed_dir = common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
518 if let Ok(Some(name)) = packed_ref_with_prefix(&packed_dir, &leaf_dir) {
519 if !skip.contains(&name) {
520 return Err(RefnameUnavailable::DescendantExists {
521 blocking: name,
522 new_ref: refname.to_owned(),
523 });
524 }
525 }
526 if packed_dir != git_dir {
527 if let Ok(Some(name)) = packed_ref_with_prefix(&git_dir, &leaf_dir) {
528 if !skip.contains(&name) {
529 return Err(RefnameUnavailable::DescendantExists {
530 blocking: name,
531 new_ref: refname.to_owned(),
532 });
533 }
534 }
535 }
536 }
537 if under.is_empty()
538 && fs::symlink_metadata(git_dir.join(refname))
539 .map(|m| m.is_dir())
540 .unwrap_or(false)
541 {
542 let mut blocking: Option<String> = None;
543 let dir_path = git_dir.join(refname);
544 if let Ok(read) = fs::read_dir(&dir_path) {
545 for entry in read.flatten() {
546 let path = entry.path();
547 let Ok(meta) = fs::metadata(&path) else {
548 continue;
549 };
550 if !meta.is_file() {
551 continue;
552 }
553 let name = entry.file_name().to_string_lossy().into_owned();
554 let full = format!("{refname}/{name}");
555 blocking = Some(full);
556 break;
557 }
558 }
559 if let Some(b) = blocking {
560 if !skip.contains(&b) {
561 return Err(RefnameUnavailable::DescendantExists {
562 blocking: b,
563 new_ref: refname.to_owned(),
564 });
565 }
566 }
567 }
568
569 for (existing, _) in under {
570 if skip.contains(&existing) {
571 continue;
572 }
573 return Err(RefnameUnavailable::DescendantExists {
574 blocking: existing,
575 new_ref: refname.to_owned(),
576 });
577 }
578
579 if let Some(extra) = find_descendant_in_sorted_extras(&leaf_dir, extras) {
580 if !skip.contains(&extra) {
581 return Err(RefnameUnavailable::SameBatch {
582 refname: refname.to_owned(),
583 other: extra,
584 });
585 }
586 }
587
588 Ok(())
589}
590
591fn read_raw_ref_reftable(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
592 if refname == "HEAD" {
593 let head_path = git_dir.join("HEAD");
594 match fs::symlink_metadata(&head_path) {
595 Ok(meta) => {
596 if meta.is_dir() {
597 return Ok(RawRefLookup::IsDirectory);
598 }
599 return Ok(RawRefLookup::Exists);
600 }
601 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(RawRefLookup::NotFound),
602 Err(e) => return Err(Error::Io(e)),
603 }
604 }
605
606 if let Some(lookup) = read_raw_ref_at(git_dir.join(refname))? {
607 return Ok(lookup);
608 }
609
610 let stack = crate::reftable::ReftableStack::open(git_dir)?;
611 match stack.lookup_ref(refname)? {
612 Some(rec) => match rec.value {
613 crate::reftable::RefValue::Deletion => Ok(RawRefLookup::NotFound),
614 _ => Ok(RawRefLookup::Exists),
615 },
616 None => Ok(RawRefLookup::NotFound),
617 }
618}
619
620fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
622 let packed = git_dir.join("packed-refs");
623 let content = match fs::read_to_string(&packed) {
624 Ok(c) => c,
625 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
626 Err(e) => return Err(Error::Io(e)),
627 };
628
629 for line in content.lines() {
630 if line.starts_with('#') || line.starts_with('^') {
631 continue;
632 }
633 let mut parts = line.splitn(2, ' ');
634 let hash = parts.next().unwrap_or("");
635 let name = parts.next().unwrap_or("").trim();
636 if name == refname && hash.len() == 40 {
637 let oid: ObjectId = hash.parse()?;
638 return Ok(Some(oid));
639 }
640 }
641 Ok(None)
642}
643
644pub fn write_symbolic_ref(git_dir: &Path, refname: &str, target: &str) -> Result<()> {
661 if crate::reftable::is_reftable_repo(git_dir) {
662 return crate::reftable::reftable_write_symref(git_dir, refname, target, None, None);
663 }
664 let storage_dir = ref_storage_dir(git_dir, refname);
665 if packed_ref_namespace_conflict(&storage_dir, refname)? {
666 return Err(Error::InvalidRef(format!(
667 "cannot update ref '{refname}': reference namespace conflict"
668 )));
669 }
670 let stor = crate::ref_namespace::storage_ref_name(refname);
671 let path = storage_dir.join(stor);
672 if let Some(parent) = path.parent() {
673 fs::create_dir_all(parent)?;
674 }
675 let content = format!("ref: {target}\n");
676 let lock = lock_path_for_ref(&path);
677 fs::write(&lock, &content)?;
678 fs::rename(&lock, &path)?;
679 Ok(())
680}
681
682pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
683 if crate::reftable::is_reftable_repo(git_dir) {
684 return crate::reftable::reftable_write_ref(git_dir, refname, oid, None, None);
685 }
686 let storage_dir = ref_storage_dir(git_dir, refname);
687 if packed_ref_namespace_conflict(&storage_dir, refname)? {
688 return Err(Error::InvalidRef(format!(
689 "cannot update ref '{refname}': reference namespace conflict"
690 )));
691 }
692 let stor = crate::ref_namespace::storage_ref_name(refname);
693 let path = storage_dir.join(stor);
694 if let Some(parent) = path.parent() {
695 fs::create_dir_all(parent)?;
696 }
697 let content = format!("{oid}\n");
698 let lock = lock_path_for_ref(&path);
700 fs::write(&lock, &content)?;
701 fs::rename(&lock, &path)?;
702 Ok(())
703}
704
705pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
713 if crate::reftable::is_reftable_repo(git_dir) {
714 return crate::reftable::reftable_delete_ref(git_dir, refname);
715 }
716 let storage_dir = ref_storage_dir(git_dir, refname);
717 let stor = crate::ref_namespace::storage_ref_name(refname);
718 let path = storage_dir.join(&stor);
720 match fs::remove_file(&path) {
721 Ok(()) => {}
722 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
723 Err(e) => return Err(Error::Io(e)),
724 }
725
726 remove_packed_ref(&storage_dir, &stor)?;
728
729 let log_path = storage_dir.join("logs").join(&stor);
730
731 if !refname.starts_with("refs/heads/") {
734 let _ = fs::remove_file(&log_path);
735
736 let logs_heads = storage_dir.join("logs/refs/heads");
740 let mut parent = log_path.parent();
741 while let Some(p) = parent {
742 if p == logs_heads.as_path() || !p.starts_with(&logs_heads) {
743 break;
744 }
745 if fs::remove_dir(p).is_err() {
746 break;
747 }
748 parent = p.parent();
749 }
750 }
751
752 Ok(())
753}
754
755fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
757 let packed_path = git_dir.join("packed-refs");
758 let content = match fs::read_to_string(&packed_path) {
759 Ok(c) => c,
760 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
761 Err(e) => return Err(Error::Io(e)),
762 };
763
764 let mut out = String::new();
765 let mut skip_peeled = false;
766 let mut changed = false;
767 let mut header_written = false;
770
771 for line in content.lines() {
772 if skip_peeled {
773 if line.starts_with('^') {
774 changed = true;
775 continue;
776 }
777 skip_peeled = false;
778 }
779
780 if line.starts_with('#') {
781 continue;
783 }
784 if line.starts_with('^') {
785 out.push_str(line);
786 out.push('\n');
787 continue;
788 }
789
790 if !header_written {
792 out.insert_str(0, "# pack-refs with: peeled fully-peeled sorted\n");
793 header_written = true;
794 }
795
796 let mut parts = line.splitn(2, ' ');
798 let _hash = parts.next().unwrap_or("");
799 let name = parts.next().unwrap_or("").trim();
800 if name == refname {
801 changed = true;
802 skip_peeled = true;
803 continue;
804 }
805
806 out.push_str(line);
807 out.push('\n');
808 }
809
810 if changed {
811 let lock = packed_path.with_extension("new");
814 let mut file = std::fs::OpenOptions::new()
815 .write(true)
816 .create_new(true)
817 .open(&lock)
818 .map_err(Error::Io)?;
819 use std::io::Write as _;
820 file.write_all(out.as_bytes()).map_err(Error::Io)?;
821 drop(file);
822 fs::rename(&lock, &packed_path).map_err(Error::Io)?;
823 }
824
825 Ok(())
826}
827
828pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
836 match read_ref_file(&git_dir.join("HEAD"))? {
837 Ref::Symbolic(target) => Ok(Some(target)),
838 Ref::Direct(_) => Ok(None),
839 }
840}
841
842pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
849 if crate::reftable::is_reftable_repo(git_dir) {
850 return crate::reftable::reftable_read_symbolic_ref(git_dir, refname);
851 }
852 let storage_owned = crate::ref_namespace::storage_ref_name(refname);
853 let try_names: Vec<&str> =
854 if refname == "HEAD" && crate::ref_namespace::ref_storage_prefix().is_some() {
855 vec![storage_owned.as_str()]
856 } else if storage_owned != refname {
857 vec![storage_owned.as_str(), refname]
858 } else {
859 vec![refname]
860 };
861
862 for name in try_names {
863 let path = git_dir.join(name);
864 match read_ref_file(&path) {
865 Ok(Ref::Symbolic(target)) => return Ok(Some(target)),
866 Ok(Ref::Direct(_)) => return Ok(None),
867 Err(Error::Io(ref e))
868 if e.kind() == io::ErrorKind::NotFound
869 || e.kind() == io::ErrorKind::NotADirectory
870 || e.kind() == io::ErrorKind::IsADirectory => {}
871 Err(e) => return Err(e),
872 }
873
874 if !notes_merge_state_ref(name) {
875 if let Some(common) = common_dir(git_dir) {
876 if common != git_dir {
877 let cpath = common.join(name);
878 match read_ref_file(&cpath) {
879 Ok(Ref::Symbolic(target)) => return Ok(Some(target)),
880 Ok(Ref::Direct(_)) => return Ok(None),
881 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
882 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotADirectory => {}
883 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::IsADirectory => {}
884 Err(e) => return Err(e),
885 }
886 }
887 }
888 }
889 }
890
891 Ok(None)
892}
893
894#[derive(Clone, Copy, Debug, PartialEq, Eq)]
896pub enum LogRefsConfig {
897 Unset,
899 None,
901 Normal,
903 Always,
905}
906
907pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
911 let config_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
912 let config_path = config_dir.join("config");
913 let content = match fs::read_to_string(config_path) {
914 Ok(c) => c,
915 Err(_) => return LogRefsConfig::Unset,
916 };
917
918 let mut in_core = false;
919 for line in content.lines() {
920 let trimmed = line.trim();
921 if trimmed.starts_with('[') {
922 in_core = trimmed.to_ascii_lowercase().starts_with("[core]");
923 continue;
924 }
925 if !in_core {
926 continue;
927 }
928 let Some((key, value)) = trimmed.split_once('=') else {
929 continue;
930 };
931 if !key.trim().eq_ignore_ascii_case("logallrefupdates") {
932 continue;
933 }
934 let v = value.trim();
935 let lower = v.to_ascii_lowercase();
936 return match lower.as_str() {
937 "always" => LogRefsConfig::Always,
938 "1" | "true" | "yes" | "on" => LogRefsConfig::Normal,
939 "0" | "false" | "no" | "off" | "never" => LogRefsConfig::None,
940 _ => LogRefsConfig::Unset,
941 };
942 }
943 LogRefsConfig::Unset
944}
945
946fn read_core_bare(git_dir: &Path) -> bool {
947 let config_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
948 let config_path = config_dir.join("config");
949 let Ok(content) = fs::read_to_string(config_path) else {
950 return false;
951 };
952 let mut in_core = false;
953 for line in content.lines() {
954 let trimmed = line.trim();
955 if trimmed.starts_with('[') {
956 in_core = trimmed.to_ascii_lowercase().starts_with("[core]");
957 continue;
958 }
959 if !in_core {
960 continue;
961 }
962 let Some((key, value)) = trimmed.split_once('=') else {
963 continue;
964 };
965 if key.trim().eq_ignore_ascii_case("bare") {
966 let v = value.trim().to_ascii_lowercase();
967 return matches!(v.as_str(), "1" | "true" | "yes" | "on");
968 }
969 }
970 false
971}
972
973pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
975 match read_log_refs_config(git_dir) {
976 LogRefsConfig::Unset => {
977 if read_core_bare(git_dir) {
978 LogRefsConfig::None
979 } else {
980 LogRefsConfig::Normal
981 }
982 }
983 other => other,
984 }
985}
986
987#[must_use]
990pub fn should_autocreate_reflog_for_mode(refname: &str, mode: LogRefsConfig) -> bool {
991 match mode {
992 LogRefsConfig::Always => true,
993 LogRefsConfig::Normal => {
994 refname == "HEAD"
995 || refname.starts_with("refs/heads/")
996 || refname.starts_with("refs/remotes/")
997 || refname.starts_with("refs/notes/")
998 }
999 LogRefsConfig::None | LogRefsConfig::Unset => false,
1000 }
1001}
1002
1003#[must_use]
1005pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
1006 should_autocreate_reflog_for_mode(refname, effective_log_refs_config(git_dir))
1007}
1008
1009pub fn append_reflog(
1027 git_dir: &Path,
1028 refname: &str,
1029 old_oid: &ObjectId,
1030 new_oid: &ObjectId,
1031 identity: &str,
1032 message: &str,
1033 force_create: bool,
1034) -> Result<()> {
1035 if crate::reftable::is_reftable_repo(git_dir) {
1036 return crate::reftable::reftable_append_reflog(
1037 git_dir,
1038 refname,
1039 old_oid,
1040 new_oid,
1041 identity,
1042 message,
1043 force_create,
1044 );
1045 }
1046 let storage_dir = ref_storage_dir(git_dir, refname);
1047 let stor = crate::ref_namespace::storage_ref_name(refname);
1048 let log_path = storage_dir.join("logs").join(&stor);
1049 let may_write =
1050 force_create || should_autocreate_reflog(git_dir, refname) || !message.is_empty();
1051 if !may_write && !log_path.exists() {
1052 return Ok(());
1053 }
1054 if let Some(parent) = log_path.parent() {
1055 fs::create_dir_all(parent)?;
1056 }
1057 let line = if message.is_empty() {
1058 format!("{old_oid} {new_oid} {identity}\n")
1059 } else {
1060 format!("{old_oid} {new_oid} {identity}\t{message}\n")
1061 };
1062 let mut file = fs::OpenOptions::new()
1063 .create(true)
1064 .append(true)
1065 .open(&log_path)?;
1066 use io::Write;
1067 file.write_all(line.as_bytes())?;
1068 Ok(())
1069}
1070
1071#[must_use]
1077pub fn reflog_file_path(git_dir: &Path, refname: &str) -> PathBuf {
1078 ref_storage_dir(git_dir, refname).join("logs").join(refname)
1079}
1080
1081fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
1082 if refname == "HEAD" || refname == "NOTES_MERGE_PARTIAL" || refname == "NOTES_MERGE_REF" {
1086 return git_dir.to_path_buf();
1087 }
1088 if refname.starts_with("refs/worktree/") {
1090 return git_dir.to_path_buf();
1091 }
1092 common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
1093}
1094
1095fn normalize_list_refs_prefix(git_dir: &Path, prefix: &str) -> String {
1104 if prefix.is_empty() {
1105 return String::new();
1106 }
1107 if prefix.ends_with('/') {
1108 return prefix.to_string();
1109 }
1110 let candidate = ref_storage_dir(git_dir, prefix).join(prefix);
1111 if candidate.is_file() {
1112 prefix.to_string()
1113 } else {
1114 format!("{prefix}/")
1115 }
1116}
1117
1118pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
1128 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
1129 let prefix = prefix_norm.as_str();
1130 if crate::reftable::is_reftable_repo(git_dir) {
1131 return crate::reftable::reftable_list_refs(git_dir, prefix);
1132 }
1133 let mut by_name: HashMap<String, ObjectId> = HashMap::new();
1137
1138 let stored_prefixes: Vec<String> = if let Some(ns) = crate::ref_namespace::ref_storage_prefix()
1139 {
1140 if prefix.starts_with("refs/namespaces/") {
1141 vec![prefix.to_owned()]
1142 } else if prefix.starts_with("refs/") {
1143 vec![format!("{ns}{prefix}")]
1144 } else {
1145 vec![prefix.to_owned()]
1146 }
1147 } else {
1148 vec![prefix.to_owned()]
1149 };
1150
1151 for stored_prefix in stored_prefixes {
1152 if let Some(cdir) = common_dir(git_dir) {
1153 if cdir != git_dir {
1154 collect_packed_refs_into_map(&cdir, &stored_prefix, false, &mut by_name)?;
1155 let cbase = cdir.join(&stored_prefix);
1156 collect_loose_refs_into_map(&cbase, &stored_prefix, &cdir, false, &mut by_name)?;
1157 }
1158 }
1159
1160 collect_packed_refs_into_map(git_dir, &stored_prefix, false, &mut by_name)?;
1161 let base = git_dir.join(&stored_prefix);
1162 collect_loose_refs_into_map(&base, &stored_prefix, git_dir, false, &mut by_name)?;
1163 }
1164
1165 let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
1166 results.sort_by(|a, b| a.0.cmp(&b.0));
1167 Ok(results)
1168}
1169
1170pub fn list_refs_physical(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
1175 if crate::reftable::is_reftable_repo(git_dir) {
1176 return crate::reftable::reftable_list_refs(git_dir, prefix);
1177 }
1178 let mut by_name: HashMap<String, ObjectId> = HashMap::new();
1179 let stored_prefix = prefix.to_owned();
1180
1181 if let Some(cdir) = common_dir(git_dir) {
1182 if cdir != git_dir {
1183 collect_packed_refs_into_map(&cdir, &stored_prefix, true, &mut by_name)?;
1184 let cbase = cdir.join(&stored_prefix);
1185 collect_loose_refs_into_map(&cbase, &stored_prefix, &cdir, true, &mut by_name)?;
1186 }
1187 }
1188
1189 collect_packed_refs_into_map(git_dir, &stored_prefix, true, &mut by_name)?;
1190 let base = git_dir.join(&stored_prefix);
1191 collect_loose_refs_into_map(&base, &stored_prefix, git_dir, true, &mut by_name)?;
1192
1193 let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
1194 results.sort_by(|a, b| a.0.cmp(&b.0));
1195 Ok(results)
1196}
1197
1198pub fn collect_alternate_ref_oids(receiving_git_dir: &Path) -> Result<Vec<ObjectId>> {
1207 let config = ConfigSet::load(Some(receiving_git_dir), true)?;
1208 let objects_dir = receiving_git_dir.join("objects");
1209 let alternates = pack::read_alternates_recursive(&objects_dir).unwrap_or_default();
1210 let mut out = Vec::new();
1211 let mut seen = std::collections::HashSet::new();
1212 for alt_objects in alternates {
1213 let Some(alt_git_dir) = alt_objects.parent().map(PathBuf::from) else {
1214 continue;
1215 };
1216 if !alt_git_dir.join("refs").is_dir() {
1217 continue;
1218 }
1219 if let Some(prefixes) = config.get("core.alternateRefsPrefixes") {
1220 for part in prefixes.split_whitespace() {
1221 for (_, oid) in list_refs(&alt_git_dir, part)? {
1222 if seen.insert(oid) {
1223 out.push(oid);
1224 }
1225 }
1226 }
1227 } else {
1228 for (_, oid) in list_refs(&alt_git_dir, "refs/")? {
1229 if seen.insert(oid) {
1230 out.push(oid);
1231 }
1232 }
1233 }
1234 }
1235 Ok(out)
1236}
1237
1238pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
1240 let glob_pos = pattern.find(['*', '?', '[']);
1241 let prefix_owned: String = match glob_pos {
1242 Some(pos) => match pattern[..pos].rfind('/') {
1243 Some(slash) => pattern[..=slash].to_owned(),
1244 None => String::new(),
1245 },
1246 None => {
1247 let mut p = pattern.trim_end_matches('/').to_owned();
1248 if !p.is_empty() {
1249 p.push('/');
1250 }
1251 p
1252 }
1253 };
1254 let prefix = prefix_owned.as_str();
1255 let all = list_refs(git_dir, prefix)?;
1256 let mut results = Vec::new();
1257 for (refname, oid) in all {
1258 if ref_matches_glob(&refname, pattern) {
1259 results.push((refname, oid));
1260 }
1261 }
1262 Ok(results)
1263}
1264
1265pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
1269 if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
1271 return refname == pattern
1272 || refname.ends_with(&format!("/{pattern}"))
1273 || refname.starts_with(&format!("{pattern}/"));
1274 }
1275 glob_match(pattern, refname)
1276}
1277
1278fn glob_match(pattern: &str, text: &str) -> bool {
1279 let pat = pattern.as_bytes();
1280 let txt = text.as_bytes();
1281 let (mut pi, mut ti) = (0, 0);
1282 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
1283 while ti < txt.len() {
1284 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
1285 pi += 1;
1286 ti += 1;
1287 } else if pi < pat.len() && pat[pi] == b'*' {
1288 star_pi = pi;
1289 star_ti = ti;
1290 pi += 1;
1291 } else if star_pi != usize::MAX {
1292 pi = star_pi + 1;
1293 star_ti += 1;
1294 ti = star_ti;
1295 } else {
1296 return false;
1297 }
1298 }
1299 while pi < pat.len() && pat[pi] == b'*' {
1300 pi += 1;
1301 }
1302 pi == pat.len()
1303}
1304
1305fn loose_ref_file_direct_oid(path: &Path) -> Option<ObjectId> {
1307 let content = fs::read_to_string(path).ok()?;
1308 let content = content.trim_end_matches('\n').trim();
1309 if content.len() == 40 && content.chars().all(|c| c.is_ascii_hexdigit()) {
1310 content.parse().ok()
1311 } else {
1312 None
1313 }
1314}
1315
1316fn collect_loose_refs_into_map(
1317 dir: &Path,
1318 prefix: &str,
1319 resolve_git_dir: &Path,
1320 physical_keys: bool,
1321 out: &mut HashMap<String, ObjectId>,
1322) -> Result<()> {
1323 let read = match fs::read_dir(dir) {
1324 Ok(r) => r,
1325 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
1326 Err(e) => return Err(Error::Io(e)),
1327 };
1328
1329 for entry in read {
1330 let entry = entry?;
1331 let name = entry.file_name();
1332 let name_str = name.to_string_lossy();
1333 let refname = format!("{prefix}{name_str}");
1334 let path = entry.path();
1335 let meta = match fs::metadata(&path) {
1336 Ok(m) => m,
1337 Err(_) => continue,
1338 };
1339
1340 if meta.is_dir() {
1341 collect_loose_refs_into_map(
1342 &path,
1343 &format!("{refname}/"),
1344 resolve_git_dir,
1345 physical_keys,
1346 out,
1347 )?;
1348 } else if meta.is_file() {
1349 if physical_keys {
1350 if let Some(oid) = loose_ref_file_direct_oid(&path) {
1351 out.insert(refname, oid);
1352 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
1353 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
1354 out.insert(refname, oid);
1355 }
1356 }
1357 } else {
1358 let logical = crate::ref_namespace::logical_ref_name_from_storage(&refname)
1359 .unwrap_or_else(|| refname.clone());
1360 if let Ok(oid) = resolve_ref(resolve_git_dir, &logical) {
1361 out.insert(logical, oid);
1362 }
1363 }
1364 }
1365 }
1366 Ok(())
1367}
1368
1369pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
1372 let inner = spec
1374 .strip_prefix("@{-")
1375 .and_then(|s| s.strip_suffix('}'))
1376 .ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
1377 let n: usize = inner
1378 .parse()
1379 .map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
1380 if n == 0 {
1381 return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
1382 }
1383 let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
1384 let mut count = 0usize;
1385 for entry in entries.iter().rev() {
1386 let msg = &entry.message;
1387 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
1388 count += 1;
1389 if count == n {
1390 if let Some(to_pos) = rest.find(" to ") {
1391 return Ok(rest[..to_pos].to_string());
1392 }
1393 }
1394 }
1395 }
1396 Err(Error::InvalidRef(format!(
1397 "{spec}: only {count} checkout(s) in reflog"
1398 )))
1399}
1400
1401fn ref_name_matches_list_prefix(refname: &str, prefix: &str) -> bool {
1402 if refname.starts_with(prefix) {
1403 return true;
1404 }
1405 if prefix.ends_with('/') {
1406 let trimmed = prefix.trim_end_matches('/');
1407 if refname == trimmed {
1408 return true;
1409 }
1410 }
1411 false
1412}
1413
1414fn collect_packed_refs_into_map(
1415 git_dir: &Path,
1416 prefix: &str,
1417 physical_keys: bool,
1418 out: &mut HashMap<String, ObjectId>,
1419) -> Result<()> {
1420 let packed_path = git_dir.join("packed-refs");
1421 let content = match fs::read_to_string(&packed_path) {
1422 Ok(c) => c,
1423 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
1424 Err(e) => return Err(Error::Io(e)),
1425 };
1426
1427 for line in content.lines() {
1428 if line.starts_with('#') || line.starts_with('^') || line.is_empty() {
1429 continue;
1430 }
1431 let mut parts = line.splitn(2, ' ');
1432 let hash = parts.next().unwrap_or("");
1433 let refname = parts.next().unwrap_or("").trim();
1434 if !ref_name_matches_list_prefix(refname, prefix) || hash.len() != 40 {
1435 continue;
1436 }
1437 let oid: ObjectId = hash.parse()?;
1438 let key = if physical_keys {
1439 refname.to_owned()
1440 } else {
1441 crate::ref_namespace::logical_ref_name_from_storage(refname)
1442 .unwrap_or_else(|| refname.to_owned())
1443 };
1444 out.insert(key, oid);
1445 }
1446 Ok(())
1447}
1448
1449#[cfg(test)]
1450mod refname_available_tests {
1451 use super::*;
1452 use std::collections::{BTreeSet, HashSet};
1453 use tempfile::tempdir;
1454
1455 #[test]
1456 fn loose_parent_blocks_child_create() {
1457 let dir = tempdir().unwrap();
1458 let git_dir = dir.path();
1459 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
1460 fs::write(
1461 git_dir.join("refs/1l/c"),
1462 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1463 )
1464 .unwrap();
1465 assert_eq!(
1466 read_raw_ref(git_dir, "refs/1l/c").unwrap(),
1467 RawRefLookup::Exists
1468 );
1469 let extras = BTreeSet::from([
1470 "refs/1l/b".to_string(),
1471 "refs/1l/c/x".to_string(),
1472 "refs/1l/d".to_string(),
1473 ]);
1474 let skip = HashSet::new();
1475 let err = verify_refname_available_for_create(git_dir, "refs/1l/c/x", &extras, &skip)
1476 .unwrap_err();
1477 assert!(matches!(
1478 err,
1479 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
1480 ));
1481 }
1482
1483 #[test]
1484 fn verify_sees_loose_ref_after_canonical_git_dir() {
1485 let dir = tempdir().unwrap();
1486 let git_dir = dir.path().join(".git");
1487 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
1488 fs::write(
1489 git_dir.join("refs/1l/c"),
1490 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1491 )
1492 .unwrap();
1493 let skip = HashSet::new();
1494 let extras = BTreeSet::new();
1495 let err = verify_refname_available_for_create(&git_dir, "refs/1l/c/x", &extras, &skip)
1496 .unwrap_err();
1497 assert!(matches!(
1498 err,
1499 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
1500 ));
1501 }
1502
1503 #[test]
1504 fn list_refs_finds_sibling_under_parent_directory() {
1505 let dir = tempdir().unwrap();
1506 let git_dir = dir.path();
1507 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
1508 fs::write(
1509 git_dir.join("refs/ns/p/x"),
1510 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1511 )
1512 .unwrap();
1513 let listed = list_refs(git_dir, "refs/ns/p/").unwrap();
1514 assert!(
1515 listed.iter().any(|(n, _)| n == "refs/ns/p/x"),
1516 "got {listed:?}"
1517 );
1518 }
1519
1520 #[test]
1521 fn verify_blocks_parent_when_child_ref_exists() {
1522 let dir = tempdir().unwrap();
1523 let git_dir = dir.path();
1524 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
1525 fs::write(
1526 git_dir.join("refs/ns/p/x"),
1527 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1528 )
1529 .unwrap();
1530 let extras = BTreeSet::from(["refs/ns/p".to_string()]);
1531 let skip = HashSet::new();
1532 let err =
1533 verify_refname_available_for_create(git_dir, "refs/ns/p", &extras, &skip).unwrap_err();
1534 assert!(matches!(
1535 err,
1536 RefnameUnavailable::DescendantExists { ref blocking, .. }
1537 if blocking == "refs/ns/p/x"
1538 ));
1539 }
1540
1541 #[test]
1542 fn verify_blocks_parent_git_style_nested_path() {
1543 let dir = tempdir().unwrap();
1544 let git_dir = dir.path();
1545 fs::create_dir_all(git_dir.join("refs/3l/c")).unwrap();
1546 fs::write(
1547 git_dir.join("refs/3l/c/x"),
1548 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1549 )
1550 .unwrap();
1551 let extras = BTreeSet::from(["refs/3l/c".to_string()]);
1552 let skip = HashSet::new();
1553 let err =
1554 verify_refname_available_for_create(git_dir, "refs/3l/c", &extras, &skip).unwrap_err();
1555 assert!(matches!(
1556 err,
1557 RefnameUnavailable::DescendantExists { ref blocking, .. }
1558 if blocking == "refs/3l/c/x"
1559 ));
1560 }
1561
1562 #[test]
1563 fn intermediate_directory_does_not_block_nested_create() {
1564 let dir = tempdir().unwrap();
1565 let git_dir = dir.path();
1566 fs::create_dir_all(git_dir.join("refs/ns")).unwrap();
1567 fs::write(
1568 git_dir.join("refs/ns/existing"),
1569 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1570 )
1571 .unwrap();
1572 assert_eq!(
1573 read_raw_ref(git_dir, "refs/ns").unwrap(),
1574 RawRefLookup::IsDirectory
1575 );
1576 let extras = BTreeSet::from(["refs/ns/newchild".to_string()]);
1577 let skip = HashSet::new();
1578 verify_refname_available_for_create(git_dir, "refs/ns/newchild", &extras, &skip).unwrap();
1579 }
1580}
1581
1582#[cfg(test)]
1583mod read_raw_ref_tests {
1584 use super::*;
1585 use tempfile::tempdir;
1586
1587 #[test]
1588 fn loose_ref_file_is_exists() {
1589 let dir = tempdir().unwrap();
1590 let git_dir = dir.path();
1591 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
1592 fs::write(
1593 git_dir.join("refs/heads/side"),
1594 "0000000000000000000000000000000000000000\n",
1595 )
1596 .unwrap();
1597 assert_eq!(
1598 read_raw_ref(git_dir, "refs/heads/side").unwrap(),
1599 RawRefLookup::Exists
1600 );
1601 }
1602
1603 #[test]
1604 fn missing_ref_is_not_found() {
1605 let dir = tempdir().unwrap();
1606 let git_dir = dir.path();
1607 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
1608 assert_eq!(
1609 read_raw_ref(git_dir, "refs/heads/nope").unwrap(),
1610 RawRefLookup::NotFound
1611 );
1612 }
1613
1614 #[test]
1615 fn directory_where_ref_expected_is_is_directory() {
1616 let dir = tempdir().unwrap();
1617 let git_dir = dir.path();
1618 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
1619 assert_eq!(
1620 read_raw_ref(git_dir, "refs/heads").unwrap(),
1621 RawRefLookup::IsDirectory
1622 );
1623 }
1624
1625 #[test]
1626 fn packed_ref_name_is_exists() {
1627 let dir = tempdir().unwrap();
1628 let git_dir = dir.path();
1629 fs::write(
1630 git_dir.join("packed-refs"),
1631 "# pack-refs with: peeled fully-peeled \n\
1632 0000000000000000000000000000000000000000 refs/heads/packed\n",
1633 )
1634 .unwrap();
1635 assert_eq!(
1636 read_raw_ref(git_dir, "refs/heads/packed").unwrap(),
1637 RawRefLookup::Exists
1638 );
1639 }
1640}