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 if let Ok(target) = fs::read_link(path) {
43 return Ok(Ref::Symbolic(target.to_string_lossy().into_owned()));
44 }
45
46 let content = match fs::read_to_string(path) {
47 Ok(c) => c,
48 Err(e)
52 if e.kind() == io::ErrorKind::IsADirectory
53 || e.kind() == io::ErrorKind::NotADirectory
54 || e.raw_os_error() == Some(libc::EISDIR)
55 || e.raw_os_error() == Some(libc::ENOTDIR) =>
56 {
57 return Err(Error::Io(io::Error::new(io::ErrorKind::NotFound, e)));
58 }
59 Err(e) => return Err(Error::Io(e)),
60 };
61 let content = content.trim_end_matches('\n');
62 parse_ref_content(content)
63}
64
65pub(crate) fn parse_ref_content(content: &str) -> Result<Ref> {
67 if let Some(target) = content.strip_prefix("ref: ") {
68 Ok(Ref::Symbolic(target.trim().to_owned()))
69 } else if ObjectId::is_full_hex(content) {
70 let oid: ObjectId = content.parse()?;
71 Ok(Ref::Direct(oid))
72 } else if content == "unknown-oid" {
73 const PLACEHOLDER: &[u8; 20] = b"GritUnknownOidPlc!X!";
77 let oid = ObjectId::from_bytes(PLACEHOLDER)?;
78 Ok(Ref::Direct(oid))
79 } else {
80 Err(Error::InvalidRef(content.to_owned()))
81 }
82}
83
84pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
98 if crate::reftable::is_reftable_repo(git_dir) {
99 return crate::reftable::reftable_resolve_ref(git_dir, refname);
100 }
101 let common = common_dir(git_dir);
102 resolve_ref_depth(git_dir, common.as_deref(), refname, 0)
103}
104
105pub fn common_dir(git_dir: &Path) -> Option<PathBuf> {
110 let commondir_file = git_dir.join("commondir");
111 let raw = fs::read_to_string(commondir_file).ok()?;
112 let rel = raw.trim();
113 let path = if Path::new(rel).is_absolute() {
116 PathBuf::from(rel)
117 } else {
118 git_dir.join(rel)
119 };
120 path.canonicalize().ok()
121}
122
123fn resolve_ref_depth(
129 git_dir: &Path,
130 _common: Option<&Path>,
131 refname: &str,
132 depth: usize,
133) -> Result<ObjectId> {
134 if depth > 10 {
135 return Err(Error::InvalidRef(format!(
136 "ref symlink too deep: {refname}"
137 )));
138 }
139
140 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
141 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
142 let try_names: Vec<&str> =
143 if stor_name == "HEAD" && crate::ref_namespace::ref_storage_prefix().is_some() {
144 vec![storage_owned.as_str()]
145 } else if storage_owned != stor_name {
146 vec![storage_owned.as_str(), stor_name.as_str()]
147 } else {
148 vec![stor_name.as_str()]
149 };
150
151 for name in try_names {
152 let path = store.join(name);
153 match read_ref_file(&path) {
154 Ok(Ref::Direct(oid)) => return Ok(oid),
155 Ok(Ref::Symbolic(target)) => {
156 return resolve_ref_depth(git_dir, None, &target, depth + 1);
157 }
158 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
159 Err(e) => return Err(e),
160 }
161
162 if let Some(oid) = lookup_packed_ref(&store, name)? {
163 return Ok(oid);
164 }
165 }
166
167 Err(Error::InvalidRef(format!("ref not found: {refname}")))
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum RawRefLookup {
177 Exists,
179 NotFound,
181 IsDirectory,
183}
184
185pub fn read_raw_ref(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
200 if crate::reftable::is_reftable_repo(git_dir) {
201 read_raw_ref_reftable(git_dir, refname)
202 } else {
203 read_raw_ref_files(git_dir, refname)
204 }
205}
206
207fn read_raw_ref_files(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
208 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
209 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
210 let (names, n): ([&str; 2], usize) = if storage_owned != stor_name {
211 ([storage_owned.as_str(), stor_name.as_str()], 2)
212 } else {
213 ([stor_name.as_str(), stor_name.as_str()], 1)
214 };
215
216 for name in names.iter().take(n) {
217 if let Some(lookup) = read_raw_ref_at(store.join(name))? {
218 return Ok(lookup);
219 }
220
221 if packed_ref_name_exists(&store, name)? {
222 return Ok(RawRefLookup::Exists);
223 }
224 }
225
226 Ok(RawRefLookup::NotFound)
227}
228
229#[must_use]
231pub fn lock_path_for_ref(path: &Path) -> PathBuf {
232 let mut s = path.as_os_str().to_owned();
233 s.push(".lock");
234 PathBuf::from(s)
235}
236
237fn read_raw_ref_at(path: PathBuf) -> Result<Option<RawRefLookup>> {
238 match fs::symlink_metadata(&path) {
239 Ok(meta) => {
240 if meta.is_dir() {
241 return Ok(Some(RawRefLookup::IsDirectory));
242 }
243 Ok(Some(RawRefLookup::Exists))
244 }
245 Err(e)
246 if e.kind() == io::ErrorKind::NotFound
247 || e.kind() == io::ErrorKind::NotADirectory
248 || e.raw_os_error() == Some(libc::ENOTDIR) =>
249 {
250 Ok(None)
251 }
252 Err(e) => Err(Error::Io(e)),
253 }
254}
255
256fn packed_ref_with_prefix(git_dir: &Path, prefix_with_slash: &str) -> Result<Option<String>> {
257 let packed = git_dir.join("packed-refs");
258 let content = match fs::read_to_string(&packed) {
259 Ok(c) => c,
260 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
261 Err(e) => return Err(Error::Io(e)),
262 };
263 let mut best: Option<String> = None;
264 for line in content.lines() {
265 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
266 continue;
267 }
268 let mut parts = line.split_whitespace();
269 let _oid = parts.next();
270 let Some(name) = parts.next() else {
271 continue;
272 };
273 let name = name.trim();
274 if name.starts_with(prefix_with_slash) {
275 let take = match &best {
276 None => true,
277 Some(b) => name < b.as_str(),
278 };
279 if take {
280 best = Some(name.to_owned());
281 }
282 }
283 }
284 Ok(best)
285}
286
287fn packed_ref_name_exists(git_dir: &Path, refname: &str) -> Result<bool> {
288 let packed = git_dir.join("packed-refs");
289 let content = match fs::read_to_string(&packed) {
290 Ok(c) => c,
291 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
292 Err(e) => return Err(Error::Io(e)),
293 };
294 for line in content.lines() {
295 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
296 continue;
297 }
298 let mut parts = line.split_whitespace();
299 let _oid = parts.next();
300 if let Some(name) = parts.next() {
301 if name == refname {
302 return Ok(true);
303 }
304 }
305 }
306 Ok(false)
307}
308
309fn refname_namespace_conflicts(existing: &str, candidate: &str) -> bool {
310 if existing == candidate {
311 return false;
312 }
313 existing
314 .strip_prefix(candidate)
315 .is_some_and(|rest| rest.starts_with('/'))
316 || candidate
317 .strip_prefix(existing)
318 .is_some_and(|rest| rest.starts_with('/'))
319}
320
321fn packed_ref_namespace_conflict(git_dir: &Path, refname: &str) -> Result<bool> {
322 let packed = git_dir.join("packed-refs");
323 let content = match fs::read_to_string(&packed) {
324 Ok(c) => c,
325 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
326 Err(e) => return Err(Error::Io(e)),
327 };
328 for line in content.lines() {
329 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
330 continue;
331 }
332 let mut parts = line.split_whitespace();
333 let _oid = parts.next();
334 if let Some(name) = parts.next() {
335 if refname_namespace_conflicts(name, refname) {
336 return Ok(true);
337 }
338 }
339 }
340 Ok(false)
341}
342
343pub fn packed_refs_entry_exists(git_dir: &Path, refname: &str) -> Result<bool> {
355 if crate::reftable::is_reftable_repo(git_dir) || refname == "HEAD" {
356 return Ok(false);
357 }
358 let storage_dir = ref_storage_dir(git_dir, refname);
359 packed_ref_name_exists(&storage_dir, refname)
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
364pub enum RefnameUnavailable {
365 AncestorExists {
367 blocking: String,
369 new_ref: String,
371 },
372 DescendantExists {
374 blocking: String,
376 new_ref: String,
378 },
379 SameBatch {
381 refname: String,
383 other: String,
385 },
386}
387
388impl RefnameUnavailable {
389 #[must_use]
391 pub fn lock_message_suffix(&self) -> String {
392 match self {
393 RefnameUnavailable::AncestorExists { blocking, new_ref } => {
394 format!("'{blocking}' exists; cannot create '{new_ref}'")
395 }
396 RefnameUnavailable::DescendantExists { blocking, new_ref } => {
397 format!("'{blocking}' exists; cannot create '{new_ref}'")
398 }
399 RefnameUnavailable::SameBatch { refname, other } => {
400 format!("cannot process '{refname}' and '{other}' at the same time")
401 }
402 }
403 }
404}
405
406fn find_descendant_in_sorted_extras(
407 dirname_with_slash: &str,
408 extras: &BTreeSet<String>,
409) -> Option<String> {
410 let start = extras
411 .range(dirname_with_slash.to_string()..)
412 .next()
413 .cloned()?;
414 if start.starts_with(dirname_with_slash) {
415 Some(start)
416 } else {
417 None
418 }
419}
420
421pub fn verify_refname_available_for_create(
434 git_dir: &Path,
435 refname: &str,
436 extras: &BTreeSet<String>,
437 skip: &HashSet<String>,
438) -> std::result::Result<(), RefnameUnavailable> {
439 let git_dir = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
442 let mut seen_dirnames: HashSet<String> = HashSet::new();
443 let segments: Vec<&str> = refname.split('/').filter(|s| !s.is_empty()).collect();
444 if segments.len() <= 1 {
445 } else {
447 let mut dirname = String::new();
448 for part in &segments[..segments.len() - 1] {
449 if !dirname.is_empty() {
450 dirname.push('/');
451 }
452 dirname.push_str(part);
453
454 if !seen_dirnames.insert(dirname.clone()) {
455 continue;
456 }
457
458 if skip.contains(&dirname) {
459 continue;
460 }
461
462 match read_raw_ref(&git_dir, &dirname) {
463 Ok(RawRefLookup::Exists) => {
464 return Err(RefnameUnavailable::AncestorExists {
465 blocking: dirname.clone(),
466 new_ref: refname.to_owned(),
467 });
468 }
469 Ok(RawRefLookup::NotFound | RawRefLookup::IsDirectory) => {}
472 Err(_) => {}
473 }
474
475 if extras.contains(&dirname) {
476 return Err(RefnameUnavailable::SameBatch {
477 refname: refname.to_owned(),
478 other: dirname.clone(),
479 });
480 }
481 }
482 }
483
484 let mut leaf_dir = String::with_capacity(refname.len() + 1);
485 leaf_dir.push_str(refname);
486 leaf_dir.push('/');
487
488 let under = list_refs(&git_dir, &leaf_dir).unwrap_or_default();
489 if under.is_empty() {
490 let packed_dir = common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
491 if let Ok(Some(name)) = packed_ref_with_prefix(&packed_dir, &leaf_dir) {
492 if !skip.contains(&name) {
493 return Err(RefnameUnavailable::DescendantExists {
494 blocking: name,
495 new_ref: refname.to_owned(),
496 });
497 }
498 }
499 if packed_dir != git_dir {
500 if let Ok(Some(name)) = packed_ref_with_prefix(&git_dir, &leaf_dir) {
501 if !skip.contains(&name) {
502 return Err(RefnameUnavailable::DescendantExists {
503 blocking: name,
504 new_ref: refname.to_owned(),
505 });
506 }
507 }
508 }
509 }
510 if under.is_empty()
511 && fs::symlink_metadata(git_dir.join(refname))
512 .map(|m| m.is_dir())
513 .unwrap_or(false)
514 {
515 let mut blocking: Option<String> = None;
516 let dir_path = git_dir.join(refname);
517 if let Ok(read) = fs::read_dir(&dir_path) {
518 for entry in read.flatten() {
519 let path = entry.path();
520 let Ok(meta) = fs::metadata(&path) else {
521 continue;
522 };
523 if !meta.is_file() {
524 continue;
525 }
526 let name = entry.file_name().to_string_lossy().into_owned();
527 let full = format!("{refname}/{name}");
528 blocking = Some(full);
529 break;
530 }
531 }
532 if let Some(b) = blocking {
533 if !skip.contains(&b) {
534 return Err(RefnameUnavailable::DescendantExists {
535 blocking: b,
536 new_ref: refname.to_owned(),
537 });
538 }
539 }
540 }
541
542 for (existing, _) in under {
543 if skip.contains(&existing) {
544 continue;
545 }
546 return Err(RefnameUnavailable::DescendantExists {
547 blocking: existing,
548 new_ref: refname.to_owned(),
549 });
550 }
551
552 if let Some(extra) = find_descendant_in_sorted_extras(&leaf_dir, extras) {
553 if !skip.contains(&extra) {
554 return Err(RefnameUnavailable::SameBatch {
555 refname: refname.to_owned(),
556 other: extra,
557 });
558 }
559 }
560
561 Ok(())
562}
563
564fn read_raw_ref_reftable(git_dir: &Path, refname: &str) -> Result<RawRefLookup> {
565 if refname == "HEAD" {
566 let head_path = git_dir.join("HEAD");
567 match fs::symlink_metadata(&head_path) {
568 Ok(meta) => {
569 if meta.is_dir() {
570 return Ok(RawRefLookup::IsDirectory);
571 }
572 return Ok(RawRefLookup::Exists);
573 }
574 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(RawRefLookup::NotFound),
575 Err(e) => return Err(Error::Io(e)),
576 }
577 }
578
579 if let Some(lookup) = read_raw_ref_at(git_dir.join(refname))? {
580 return Ok(lookup);
581 }
582
583 let stack = crate::reftable::ReftableStack::open(git_dir)?;
584 match stack.lookup_ref(refname)? {
585 Some(rec) => match rec.value {
586 crate::reftable::RefValue::Deletion => Ok(RawRefLookup::NotFound),
587 _ => Ok(RawRefLookup::Exists),
588 },
589 None => Ok(RawRefLookup::NotFound),
590 }
591}
592
593fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
595 let packed = git_dir.join("packed-refs");
596 let content = match fs::read_to_string(&packed) {
597 Ok(c) => c,
598 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
599 Err(e) => return Err(Error::Io(e)),
600 };
601
602 for line in content.lines() {
603 if line.starts_with('#') || line.starts_with('^') {
604 continue;
605 }
606 let mut parts = line.splitn(2, ' ');
607 let hash = parts.next().unwrap_or("");
608 let name = parts.next().unwrap_or("").trim();
609 if name == refname && ObjectId::is_hex_len(hash.len()) {
610 let oid: ObjectId = hash.parse()?;
611 return Ok(Some(oid));
612 }
613 }
614 Ok(None)
615}
616
617pub fn write_symbolic_ref(git_dir: &Path, refname: &str, target: &str) -> Result<()> {
634 if crate::reftable::is_reftable_repo(git_dir) {
635 return crate::reftable::reftable_write_symref(git_dir, refname, target, None, None);
636 }
637 let storage_dir = ref_storage_dir(git_dir, refname);
638 if packed_ref_namespace_conflict(&storage_dir, refname)? {
639 return Err(Error::InvalidRef(format!(
640 "cannot update ref '{refname}': reference namespace conflict"
641 )));
642 }
643 let stor = crate::ref_namespace::storage_ref_name(refname);
644 let path = storage_dir.join(stor);
645 if let Some(parent) = path.parent() {
646 fs::create_dir_all(parent)?;
647 }
648 let content = format!("ref: {target}\n");
649 let lock = lock_path_for_ref(&path);
650 fs::write(&lock, &content)?;
651 fs::rename(&lock, &path)?;
652 Ok(())
653}
654
655pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
656 if crate::reftable::is_reftable_repo(git_dir) {
657 return crate::reftable::reftable_write_ref(git_dir, refname, oid, None, None);
658 }
659 let storage_dir = ref_storage_dir(git_dir, refname);
660 if packed_ref_namespace_conflict(&storage_dir, refname)? {
661 return Err(Error::InvalidRef(format!(
662 "cannot update ref '{refname}': reference namespace conflict"
663 )));
664 }
665 let stor = crate::ref_namespace::storage_ref_name(refname);
666 let path = storage_dir.join(stor);
667 remove_empty_ref_directory(&path);
674 if fs::symlink_metadata(&path)
681 .map(|m| m.file_type().is_dir())
682 .unwrap_or(false)
683 {
684 let display = ref_path_for_display(&path);
685 return Err(Error::Message(format!(
686 "fatal: cannot lock ref '{refname}': there is a non-empty directory '{display}' blocking reference '{refname}'"
687 )));
688 }
689 if let Some(parent) = path.parent() {
690 fs::create_dir_all(parent)?;
691 }
692 let content = format!("{oid}\n");
693 let lock = lock_path_for_ref(&path);
700 {
701 use std::io::Write as _;
702 let mut file = fs::OpenOptions::new()
703 .write(true)
704 .create_new(true)
705 .open(&lock)?;
706 file.write_all(content.as_bytes())?;
707 }
708 fs::rename(&lock, &path)?;
709 Ok(())
710}
711
712fn ref_path_for_display(path: &Path) -> String {
717 if let Ok(cwd) = std::env::current_dir() {
718 if let Ok(rel) = path.strip_prefix(&cwd) {
719 return rel.to_string_lossy().into_owned();
720 }
721 if let (Ok(cwd_c), Ok(path_c)) = (cwd.canonicalize(), path.canonicalize()) {
725 if let Ok(rel) = path_c.strip_prefix(&cwd_c) {
726 return rel.to_string_lossy().into_owned();
727 }
728 }
729 }
730 path.to_string_lossy().into_owned()
731}
732
733fn remove_empty_ref_directory(path: &Path) {
739 match fs::symlink_metadata(path) {
740 Ok(meta) if meta.file_type().is_dir() => {}
741 _ => return,
742 }
743 if dir_tree_has_files(path) {
746 return;
747 }
748 let _ = remove_dir_tree(path);
749}
750
751fn dir_tree_has_files(dir: &Path) -> bool {
753 let Ok(entries) = fs::read_dir(dir) else {
754 return true;
756 };
757 for entry in entries.flatten() {
758 match entry.file_type() {
759 Ok(ft) if ft.is_dir() => {
760 if dir_tree_has_files(&entry.path()) {
761 return true;
762 }
763 }
764 Ok(_) => return true, Err(_) => return true,
766 }
767 }
768 false
769}
770
771fn remove_dir_tree(dir: &Path) -> io::Result<()> {
773 for entry in fs::read_dir(dir)? {
774 let entry = entry?;
775 if entry.file_type()?.is_dir() {
776 remove_dir_tree(&entry.path())?;
777 }
778 }
779 fs::remove_dir(dir)
780}
781
782pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
790 if crate::reftable::is_reftable_repo(git_dir) {
791 return crate::reftable::reftable_delete_ref(git_dir, refname);
792 }
793 let storage_dir = ref_storage_dir(git_dir, refname);
794 let stor = crate::ref_namespace::storage_ref_name(refname);
795 let path = storage_dir.join(&stor);
796
797 remove_packed_ref(&storage_dir, &stor)?;
803
804 remove_empty_ref_directory(&path);
808 match fs::remove_file(&path) {
809 Ok(()) => {}
810 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
811 Err(e)
812 if e.kind() == io::ErrorKind::NotADirectory
813 || e.raw_os_error() == Some(libc::ENOTDIR) => {}
814 Err(e)
817 if e.raw_os_error() == Some(libc::EISDIR) || e.raw_os_error() == Some(libc::EPERM) => {}
818 Err(e) => return Err(Error::Io(e)),
819 }
820
821 let log_path = storage_dir.join("logs").join(&stor);
822
823 let _ = fs::remove_file(&log_path);
829
830 let logs_root = storage_dir.join("logs");
831 let mut parent = log_path.parent();
832 while let Some(p) = parent {
833 if p == logs_root.as_path() || !p.starts_with(&logs_root) {
834 break;
835 }
836 if fs::remove_dir(p).is_err() {
837 break; }
839 parent = p.parent();
840 }
841
842 Ok(())
843}
844
845fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
847 let packed_path = git_dir.join("packed-refs");
848 let content = match fs::read_to_string(&packed_path) {
849 Ok(c) => c,
850 Err(e)
851 if e.kind() == io::ErrorKind::NotFound
852 || e.kind() == io::ErrorKind::NotADirectory
853 || e.raw_os_error() == Some(libc::ENOTDIR) =>
854 {
855 return Ok(());
856 }
857 Err(e) => return Err(Error::Io(e)),
858 };
859
860 let mut out = String::new();
861 let mut skip_peeled = false;
862 let mut changed = false;
863 let mut header_written = false;
866
867 for line in content.lines() {
868 if skip_peeled {
869 if line.starts_with('^') {
870 changed = true;
871 continue;
872 }
873 skip_peeled = false;
874 }
875
876 if line.starts_with('#') {
877 continue;
879 }
880 if line.starts_with('^') {
881 out.push_str(line);
882 out.push('\n');
883 continue;
884 }
885
886 if !header_written {
888 out.insert_str(0, "# pack-refs with: peeled fully-peeled sorted\n");
889 header_written = true;
890 }
891
892 let mut parts = line.splitn(2, ' ');
894 let _hash = parts.next().unwrap_or("");
895 let name = parts.next().unwrap_or("").trim();
896 if name == refname {
897 changed = true;
898 skip_peeled = true;
899 continue;
900 }
901
902 out.push_str(line);
903 out.push('\n');
904 }
905
906 if changed {
907 let lock = lock_path_for_ref(&packed_path); let abs_lock = fs::canonicalize(git_dir)
915 .map(|d| d.join("packed-refs.lock"))
916 .unwrap_or_else(|_| lock.clone());
917 let timeout_ms = ConfigSet::load(Some(git_dir), true)
921 .ok()
922 .and_then(|cfg| cfg.get("core.packedrefstimeout"))
923 .and_then(|v| v.trim().parse::<i64>().ok())
924 .unwrap_or(0);
925 let deadline =
926 std::time::Instant::now() + std::time::Duration::from_millis(timeout_ms.max(0) as u64);
927 loop {
928 match std::fs::OpenOptions::new()
929 .write(true)
930 .create_new(true)
931 .open(&lock)
932 {
933 Ok(_) => break,
934 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
935 if timeout_ms > 0 && std::time::Instant::now() < deadline {
936 std::thread::sleep(std::time::Duration::from_millis(50));
937 continue;
938 }
939 return Err(Error::Message(format!(
940 "Unable to create '{}': File exists.",
941 abs_lock.display()
942 )));
943 }
944 Err(e) => return Err(Error::Io(e)),
945 }
946 }
947
948 let tmp = packed_path.with_extension("new");
949 let mut created_tmp = false;
950 let write_result = (|| -> Result<()> {
951 let mut file = std::fs::OpenOptions::new()
952 .write(true)
953 .create_new(true)
954 .open(&tmp)
955 .map_err(Error::Io)?;
956 created_tmp = true;
957 use std::io::Write as _;
958 file.write_all(out.as_bytes()).map_err(Error::Io)?;
959 drop(file);
960 fs::rename(&tmp, &packed_path).map_err(Error::Io)?;
961 created_tmp = false; Ok(())
963 })();
964
965 let _ = fs::remove_file(&lock);
968 if write_result.is_err() && created_tmp {
969 let _ = fs::remove_file(&tmp);
970 }
971 write_result?;
972 }
973
974 Ok(())
975}
976
977pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
985 match read_ref_file(&git_dir.join("HEAD"))? {
986 Ref::Symbolic(target) => Ok(Some(target)),
987 Ref::Direct(_) => Ok(None),
988 }
989}
990
991pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
998 if crate::reftable::is_reftable_repo(git_dir) {
999 return crate::reftable::reftable_read_symbolic_ref(git_dir, refname);
1000 }
1001 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
1002 let storage_owned = crate::ref_namespace::storage_ref_name(&stor_name);
1003 let try_names: Vec<&str> =
1004 if stor_name == "HEAD" && crate::ref_namespace::ref_storage_prefix().is_some() {
1005 vec![storage_owned.as_str()]
1006 } else if storage_owned != stor_name {
1007 vec![storage_owned.as_str(), stor_name.as_str()]
1008 } else {
1009 vec![stor_name.as_str()]
1010 };
1011
1012 for name in try_names {
1013 let path = store.join(name);
1014 match read_ref_file(&path) {
1015 Ok(Ref::Symbolic(target)) => return Ok(Some(target)),
1016 Ok(Ref::Direct(_)) => return Ok(None),
1017 Err(Error::Io(ref e))
1018 if e.kind() == io::ErrorKind::NotFound
1019 || e.kind() == io::ErrorKind::NotADirectory
1020 || e.kind() == io::ErrorKind::IsADirectory => {}
1021 Err(e) => return Err(e),
1022 }
1023 }
1024
1025 Ok(None)
1026}
1027
1028#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1030pub enum LogRefsConfig {
1031 Unset,
1033 None,
1035 Normal,
1037 Always,
1039}
1040
1041pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
1045 let config_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
1046 let config_path = config_dir.join("config");
1047 let content = match fs::read_to_string(config_path) {
1048 Ok(c) => c,
1049 Err(_) => return LogRefsConfig::Unset,
1050 };
1051
1052 let mut in_core = false;
1053 for line in content.lines() {
1054 let trimmed = line.trim();
1055 if trimmed.starts_with('[') {
1056 in_core = trimmed.to_ascii_lowercase().starts_with("[core]");
1057 continue;
1058 }
1059 if !in_core {
1060 continue;
1061 }
1062 let Some((key, value)) = trimmed.split_once('=') else {
1063 continue;
1064 };
1065 if !key.trim().eq_ignore_ascii_case("logallrefupdates") {
1066 continue;
1067 }
1068 let v = value.trim();
1069 let lower = v.to_ascii_lowercase();
1070 return match lower.as_str() {
1071 "always" => LogRefsConfig::Always,
1072 "1" | "true" | "yes" | "on" => LogRefsConfig::Normal,
1073 "0" | "false" | "no" | "off" | "never" => LogRefsConfig::None,
1074 _ => LogRefsConfig::Unset,
1075 };
1076 }
1077 LogRefsConfig::Unset
1078}
1079
1080fn read_core_bare(git_dir: &Path) -> bool {
1081 let config_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
1082 let config_path = config_dir.join("config");
1083 let Ok(content) = fs::read_to_string(config_path) else {
1084 return false;
1085 };
1086 let mut in_core = false;
1087 for line in content.lines() {
1088 let trimmed = line.trim();
1089 if trimmed.starts_with('[') {
1090 in_core = trimmed.to_ascii_lowercase().starts_with("[core]");
1091 continue;
1092 }
1093 if !in_core {
1094 continue;
1095 }
1096 let Some((key, value)) = trimmed.split_once('=') else {
1097 continue;
1098 };
1099 if key.trim().eq_ignore_ascii_case("bare") {
1100 let v = value.trim().to_ascii_lowercase();
1101 return matches!(v.as_str(), "1" | "true" | "yes" | "on");
1102 }
1103 }
1104 false
1105}
1106
1107pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
1109 match read_log_refs_config(git_dir) {
1110 LogRefsConfig::Unset => {
1111 if read_core_bare(git_dir) {
1112 LogRefsConfig::None
1113 } else {
1114 LogRefsConfig::Normal
1115 }
1116 }
1117 other => other,
1118 }
1119}
1120
1121#[must_use]
1124pub fn should_autocreate_reflog_for_mode(refname: &str, mode: LogRefsConfig) -> bool {
1125 match mode {
1126 LogRefsConfig::Always => true,
1127 LogRefsConfig::Normal => {
1128 refname == "HEAD"
1129 || refname.starts_with("refs/heads/")
1130 || refname.starts_with("refs/remotes/")
1131 || refname.starts_with("refs/notes/")
1132 }
1133 LogRefsConfig::None | LogRefsConfig::Unset => false,
1134 }
1135}
1136
1137#[must_use]
1139pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
1140 should_autocreate_reflog_for_mode(refname, effective_log_refs_config(git_dir))
1141}
1142
1143fn clear_conflicting_reflog_files(logs_root: &Path, target: &Path) {
1169 let Ok(rel) = target.strip_prefix(logs_root) else {
1170 return;
1171 };
1172 let mut cur = logs_root.to_path_buf();
1173 for component in rel.components() {
1174 cur.push(component);
1175 match fs::symlink_metadata(&cur) {
1176 Ok(meta) if meta.file_type().is_dir() => {}
1177 Ok(_) => {
1178 let _ = fs::remove_file(&cur);
1181 }
1182 Err(_) => break, }
1184 }
1185}
1186
1187fn reflog_oid_hex_pair(old: &ObjectId, new: &ObjectId) -> (String, String) {
1190 let width = if !old.is_zero() {
1191 old.algo().hex_len()
1192 } else if !new.is_zero() {
1193 new.algo().hex_len()
1194 } else {
1195 old.algo().hex_len()
1196 };
1197 let render = |oid: &ObjectId| {
1198 if oid.is_zero() {
1199 "0".repeat(width)
1200 } else {
1201 oid.to_hex()
1202 }
1203 };
1204 (render(old), render(new))
1205}
1206
1207pub fn append_reflog(
1208 git_dir: &Path,
1209 refname: &str,
1210 old_oid: &ObjectId,
1211 new_oid: &ObjectId,
1212 identity: &str,
1213 message: &str,
1214 force_create: bool,
1215) -> Result<()> {
1216 if crate::reftable::is_reftable_repo(git_dir) {
1217 return crate::reftable::reftable_append_reflog(
1218 git_dir,
1219 refname,
1220 old_oid,
1221 new_oid,
1222 identity,
1223 message,
1224 force_create,
1225 );
1226 }
1227 let storage_dir = ref_storage_dir(git_dir, refname);
1228 let stor = crate::ref_namespace::storage_ref_name(refname);
1229 let log_path = storage_dir.join("logs").join(&stor);
1230 let may_create = force_create || should_autocreate_reflog(git_dir, refname);
1231 if !may_create && !log_path.exists() {
1232 return Ok(());
1233 }
1234 if let Some(parent) = log_path.parent() {
1235 let logs_root = storage_dir.join("logs");
1240 clear_conflicting_reflog_files(&logs_root, parent);
1241 fs::create_dir_all(parent)?;
1242 }
1243 let (old_hex, new_hex) = reflog_oid_hex_pair(old_oid, new_oid);
1248 let line = if message.is_empty() {
1249 format!("{old_hex} {new_hex} {identity}\n")
1250 } else {
1251 format!("{old_hex} {new_hex} {identity}\t{message}\n")
1252 };
1253 let mut file = fs::OpenOptions::new()
1254 .create(true)
1255 .append(true)
1256 .open(&log_path)?;
1257 use io::Write;
1258 file.write_all(line.as_bytes())?;
1259 Ok(())
1260}
1261
1262#[must_use]
1268pub fn reflog_file_path(git_dir: &Path, refname: &str) -> PathBuf {
1269 let (store, stor_name) = crate::worktree_ref::resolve_ref_storage(git_dir, refname);
1270 store.join("logs").join(stor_name)
1271}
1272
1273fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
1274 crate::worktree_ref::resolve_ref_storage(git_dir, refname).0
1275}
1276
1277fn normalize_list_refs_prefix(git_dir: &Path, prefix: &str) -> String {
1286 if prefix.is_empty() {
1287 return String::new();
1288 }
1289 if prefix.ends_with('/') {
1290 return prefix.to_string();
1291 }
1292 let candidate = ref_storage_dir(git_dir, prefix).join(prefix);
1293 if candidate.is_file() {
1294 prefix.to_string()
1295 } else {
1296 format!("{prefix}/")
1297 }
1298}
1299
1300pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
1310 let prefix_norm = normalize_list_refs_prefix(git_dir, prefix);
1311 let prefix = prefix_norm.as_str();
1312 if crate::reftable::is_reftable_repo(git_dir) {
1313 return crate::reftable::reftable_list_refs(git_dir, prefix);
1314 }
1315 let mut by_name: HashMap<String, ObjectId> = HashMap::new();
1319
1320 let stored_prefixes: Vec<String> = if let Some(ns) = crate::ref_namespace::ref_storage_prefix()
1321 {
1322 if prefix.starts_with("refs/namespaces/") {
1323 vec![prefix.to_owned()]
1324 } else if prefix.starts_with("refs/") {
1325 vec![format!("{ns}{prefix}")]
1326 } else {
1327 vec![prefix.to_owned()]
1328 }
1329 } else {
1330 vec![prefix.to_owned()]
1331 };
1332
1333 for stored_prefix in stored_prefixes {
1334 if let Some(cdir) = common_dir(git_dir) {
1335 if cdir != git_dir {
1336 collect_packed_refs_into_map(&cdir, &stored_prefix, false, &mut by_name)?;
1337 let cbase = cdir.join(&stored_prefix);
1338 collect_loose_refs_into_map(&cbase, &stored_prefix, &cdir, false, &mut by_name)?;
1339 }
1340 }
1341
1342 collect_packed_refs_into_map(git_dir, &stored_prefix, false, &mut by_name)?;
1343 let base = git_dir.join(&stored_prefix);
1344 collect_loose_refs_into_map(&base, &stored_prefix, git_dir, false, &mut by_name)?;
1345 }
1346
1347 let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
1348 if crate::worktree_ref::is_linked_worktree_git_dir(git_dir) {
1349 results.retain(|(name, _)| crate::worktree_ref::ref_visible_from_worktree(git_dir, name));
1350 }
1351 results.sort_by(|a, b| a.0.cmp(&b.0));
1352 Ok(results)
1353}
1354
1355pub fn resolve_ref_dwim(git_dir: &Path, spec: &str) -> (usize, Option<ObjectId>) {
1357 crate::worktree_ref::resolve_ref_dwim(|candidate| resolve_ref(git_dir, candidate).ok(), spec)
1358}
1359
1360pub fn list_refs_physical(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
1365 if crate::reftable::is_reftable_repo(git_dir) {
1366 return crate::reftable::reftable_list_refs(git_dir, prefix);
1367 }
1368 let mut by_name: HashMap<String, ObjectId> = HashMap::new();
1369 let stored_prefix = prefix.to_owned();
1370
1371 if let Some(cdir) = common_dir(git_dir) {
1372 if cdir != git_dir {
1373 collect_packed_refs_into_map(&cdir, &stored_prefix, true, &mut by_name)?;
1374 let cbase = cdir.join(&stored_prefix);
1375 collect_loose_refs_into_map(&cbase, &stored_prefix, &cdir, true, &mut by_name)?;
1376 }
1377 }
1378
1379 collect_packed_refs_into_map(git_dir, &stored_prefix, true, &mut by_name)?;
1380 let base = git_dir.join(&stored_prefix);
1381 collect_loose_refs_into_map(&base, &stored_prefix, git_dir, true, &mut by_name)?;
1382
1383 let mut results: Vec<(String, ObjectId)> = by_name.into_iter().collect();
1384 results.sort_by(|a, b| a.0.cmp(&b.0));
1385 Ok(results)
1386}
1387
1388pub fn collect_alternate_ref_oids(receiving_git_dir: &Path) -> Result<Vec<ObjectId>> {
1397 let config = ConfigSet::load(Some(receiving_git_dir), true)?;
1398 let objects_dir = receiving_git_dir.join("objects");
1399 let alternates = pack::read_alternates_recursive(&objects_dir).unwrap_or_default();
1400 let mut out = Vec::new();
1401 let mut seen = std::collections::HashSet::new();
1402 for alt_objects in alternates {
1403 let Some(alt_git_dir) = alt_objects.parent().map(PathBuf::from) else {
1404 continue;
1405 };
1406 if !alt_git_dir.join("refs").is_dir() {
1407 continue;
1408 }
1409 if let Some(prefixes) = config
1410 .get("core.alternaterefsprefixes")
1411 .or_else(|| config.get("core.alternateRefsPrefixes"))
1412 {
1413 for part in prefixes.split_whitespace() {
1414 if let Ok(oid) = resolve_ref(&alt_git_dir, part) {
1415 if seen.insert(oid) {
1416 out.push(oid);
1417 }
1418 continue;
1419 }
1420 for (_, oid) in list_refs(&alt_git_dir, part)? {
1421 if seen.insert(oid) {
1422 out.push(oid);
1423 }
1424 }
1425 }
1426 } else {
1427 for (_, oid) in list_refs(&alt_git_dir, "refs/")? {
1428 if seen.insert(oid) {
1429 out.push(oid);
1430 }
1431 }
1432 }
1433 }
1434 Ok(out)
1435}
1436
1437pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
1439 let glob_pos = pattern.find(['*', '?', '[']);
1440 let prefix_owned: String = match glob_pos {
1441 Some(pos) => match pattern[..pos].rfind('/') {
1442 Some(slash) => pattern[..=slash].to_owned(),
1443 None => String::new(),
1444 },
1445 None => {
1446 let mut p = pattern.trim_end_matches('/').to_owned();
1447 if !p.is_empty() {
1448 p.push('/');
1449 }
1450 p
1451 }
1452 };
1453 let prefix = prefix_owned.as_str();
1454 let all = list_refs(git_dir, prefix)?;
1455 let mut results = Vec::new();
1456 for (refname, oid) in all {
1457 if ref_matches_glob(&refname, pattern) {
1458 results.push((refname, oid));
1459 }
1460 }
1461 Ok(results)
1462}
1463
1464pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
1468 if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
1470 return refname == pattern
1471 || refname.ends_with(&format!("/{pattern}"))
1472 || refname.starts_with(&format!("{pattern}/"));
1473 }
1474 glob_match(pattern, refname)
1475}
1476
1477fn glob_match(pattern: &str, text: &str) -> bool {
1478 let pat = pattern.as_bytes();
1479 let txt = text.as_bytes();
1480 let (mut pi, mut ti) = (0, 0);
1481 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
1482 while ti < txt.len() {
1483 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
1484 pi += 1;
1485 ti += 1;
1486 } else if pi < pat.len() && pat[pi] == b'*' {
1487 star_pi = pi;
1488 star_ti = ti;
1489 pi += 1;
1490 } else if star_pi != usize::MAX {
1491 pi = star_pi + 1;
1492 star_ti += 1;
1493 ti = star_ti;
1494 } else {
1495 return false;
1496 }
1497 }
1498 while pi < pat.len() && pat[pi] == b'*' {
1499 pi += 1;
1500 }
1501 pi == pat.len()
1502}
1503
1504fn loose_ref_file_direct_oid(path: &Path) -> Option<ObjectId> {
1506 let content = fs::read_to_string(path).ok()?;
1507 let content = content.trim_end_matches('\n').trim();
1508 if ObjectId::is_full_hex(content) {
1509 content.parse().ok()
1510 } else {
1511 None
1512 }
1513}
1514
1515fn collect_loose_refs_into_map(
1516 dir: &Path,
1517 prefix: &str,
1518 resolve_git_dir: &Path,
1519 physical_keys: bool,
1520 out: &mut HashMap<String, ObjectId>,
1521) -> Result<()> {
1522 let read = match fs::read_dir(dir) {
1523 Ok(r) => r,
1524 Err(e)
1525 if e.kind() == io::ErrorKind::NotFound
1526 || e.kind() == io::ErrorKind::NotADirectory
1527 || e.raw_os_error() == Some(libc::ENOTDIR) =>
1528 {
1529 return Ok(());
1530 }
1531 Err(e) => return Err(Error::Io(e)),
1532 };
1533
1534 for entry in read {
1535 let entry = entry?;
1536 let name = entry.file_name();
1537 let name_str = name.to_string_lossy();
1538 let refname = format!("{prefix}{name_str}");
1539 let path = entry.path();
1540 let meta = match fs::metadata(&path) {
1541 Ok(m) => m,
1542 Err(_) => continue,
1543 };
1544
1545 if meta.is_dir() {
1546 collect_loose_refs_into_map(
1547 &path,
1548 &format!("{refname}/"),
1549 resolve_git_dir,
1550 physical_keys,
1551 out,
1552 )?;
1553 } else if meta.is_file() {
1554 if physical_keys {
1555 if let Some(oid) = loose_ref_file_direct_oid(&path) {
1556 out.insert(refname, oid);
1557 } else if let Ok(Ref::Symbolic(target)) = read_ref_file(&path) {
1558 if let Ok(oid) = resolve_ref(resolve_git_dir, target.trim()) {
1559 out.insert(refname, oid);
1560 }
1561 }
1562 } else {
1563 let logical = crate::ref_namespace::logical_ref_name_from_storage(&refname)
1564 .unwrap_or_else(|| refname.clone());
1565 if let Ok(oid) = resolve_ref(resolve_git_dir, &logical) {
1566 out.insert(logical, oid);
1567 }
1568 }
1569 }
1570 }
1571 Ok(())
1572}
1573
1574pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
1577 let inner = spec
1579 .strip_prefix("@{-")
1580 .and_then(|s| s.strip_suffix('}'))
1581 .ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
1582 let n: usize = inner
1583 .parse()
1584 .map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
1585 if n == 0 {
1586 return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
1587 }
1588 let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
1589 let mut count = 0usize;
1590 for entry in entries.iter().rev() {
1591 let msg = &entry.message;
1592 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
1593 count += 1;
1594 if count == n {
1595 if let Some(to_pos) = rest.find(" to ") {
1596 return Ok(rest[..to_pos].to_string());
1597 }
1598 }
1599 }
1600 }
1601 Err(Error::InvalidRef(format!(
1602 "{spec}: only {count} checkout(s) in reflog"
1603 )))
1604}
1605
1606fn ref_name_matches_list_prefix(refname: &str, prefix: &str) -> bool {
1607 if refname.starts_with(prefix) {
1608 return true;
1609 }
1610 if prefix.ends_with('/') {
1611 let trimmed = prefix.trim_end_matches('/');
1612 if refname == trimmed {
1613 return true;
1614 }
1615 }
1616 false
1617}
1618
1619fn collect_packed_refs_into_map(
1620 git_dir: &Path,
1621 prefix: &str,
1622 physical_keys: bool,
1623 out: &mut HashMap<String, ObjectId>,
1624) -> Result<()> {
1625 let packed_path = git_dir.join("packed-refs");
1626 let content = match fs::read_to_string(&packed_path) {
1627 Ok(c) => c,
1628 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
1629 Err(e) => return Err(Error::Io(e)),
1630 };
1631
1632 for line in content.lines() {
1633 if line.starts_with('#') || line.starts_with('^') || line.is_empty() {
1634 continue;
1635 }
1636 let mut parts = line.splitn(2, ' ');
1637 let hash = parts.next().unwrap_or("");
1638 let refname = parts.next().unwrap_or("").trim();
1639 if !ref_name_matches_list_prefix(refname, prefix) || !ObjectId::is_hex_len(hash.len()) {
1640 continue;
1641 }
1642 let oid: ObjectId = hash.parse()?;
1643 let key = if physical_keys {
1644 refname.to_owned()
1645 } else {
1646 crate::ref_namespace::logical_ref_name_from_storage(refname)
1647 .unwrap_or_else(|| refname.to_owned())
1648 };
1649 out.insert(key, oid);
1650 }
1651 Ok(())
1652}
1653
1654#[cfg(test)]
1655mod refname_available_tests {
1656 use super::*;
1657 use std::collections::{BTreeSet, HashSet};
1658 use tempfile::tempdir;
1659
1660 #[test]
1661 fn loose_parent_blocks_child_create() {
1662 let dir = tempdir().unwrap();
1663 let git_dir = dir.path();
1664 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
1665 fs::write(
1666 git_dir.join("refs/1l/c"),
1667 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1668 )
1669 .unwrap();
1670 assert_eq!(
1671 read_raw_ref(git_dir, "refs/1l/c").unwrap(),
1672 RawRefLookup::Exists
1673 );
1674 let extras = BTreeSet::from([
1675 "refs/1l/b".to_string(),
1676 "refs/1l/c/x".to_string(),
1677 "refs/1l/d".to_string(),
1678 ]);
1679 let skip = HashSet::new();
1680 let err = verify_refname_available_for_create(git_dir, "refs/1l/c/x", &extras, &skip)
1681 .unwrap_err();
1682 assert!(matches!(
1683 err,
1684 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
1685 ));
1686 }
1687
1688 #[test]
1689 fn verify_sees_loose_ref_after_canonical_git_dir() {
1690 let dir = tempdir().unwrap();
1691 let git_dir = dir.path().join(".git");
1692 fs::create_dir_all(git_dir.join("refs/1l")).unwrap();
1693 fs::write(
1694 git_dir.join("refs/1l/c"),
1695 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1696 )
1697 .unwrap();
1698 let skip = HashSet::new();
1699 let extras = BTreeSet::new();
1700 let err = verify_refname_available_for_create(&git_dir, "refs/1l/c/x", &extras, &skip)
1701 .unwrap_err();
1702 assert!(matches!(
1703 err,
1704 RefnameUnavailable::AncestorExists { ref blocking, .. } if blocking == "refs/1l/c"
1705 ));
1706 }
1707
1708 #[test]
1709 fn list_refs_finds_sibling_under_parent_directory() {
1710 let dir = tempdir().unwrap();
1711 let git_dir = dir.path();
1712 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
1713 fs::write(
1714 git_dir.join("refs/ns/p/x"),
1715 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1716 )
1717 .unwrap();
1718 let listed = list_refs(git_dir, "refs/ns/p/").unwrap();
1719 assert!(
1720 listed.iter().any(|(n, _)| n == "refs/ns/p/x"),
1721 "got {listed:?}"
1722 );
1723 }
1724
1725 #[test]
1726 fn verify_blocks_parent_when_child_ref_exists() {
1727 let dir = tempdir().unwrap();
1728 let git_dir = dir.path();
1729 fs::create_dir_all(git_dir.join("refs/ns/p")).unwrap();
1730 fs::write(
1731 git_dir.join("refs/ns/p/x"),
1732 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1733 )
1734 .unwrap();
1735 let extras = BTreeSet::from(["refs/ns/p".to_string()]);
1736 let skip = HashSet::new();
1737 let err =
1738 verify_refname_available_for_create(git_dir, "refs/ns/p", &extras, &skip).unwrap_err();
1739 assert!(matches!(
1740 err,
1741 RefnameUnavailable::DescendantExists { ref blocking, .. }
1742 if blocking == "refs/ns/p/x"
1743 ));
1744 }
1745
1746 #[test]
1747 fn verify_blocks_parent_git_style_nested_path() {
1748 let dir = tempdir().unwrap();
1749 let git_dir = dir.path();
1750 fs::create_dir_all(git_dir.join("refs/3l/c")).unwrap();
1751 fs::write(
1752 git_dir.join("refs/3l/c/x"),
1753 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1754 )
1755 .unwrap();
1756 let extras = BTreeSet::from(["refs/3l/c".to_string()]);
1757 let skip = HashSet::new();
1758 let err =
1759 verify_refname_available_for_create(git_dir, "refs/3l/c", &extras, &skip).unwrap_err();
1760 assert!(matches!(
1761 err,
1762 RefnameUnavailable::DescendantExists { ref blocking, .. }
1763 if blocking == "refs/3l/c/x"
1764 ));
1765 }
1766
1767 #[test]
1768 fn intermediate_directory_does_not_block_nested_create() {
1769 let dir = tempdir().unwrap();
1770 let git_dir = dir.path();
1771 fs::create_dir_all(git_dir.join("refs/ns")).unwrap();
1772 fs::write(
1773 git_dir.join("refs/ns/existing"),
1774 "67bf698f3ab735e92fb011a99cff3497c44d30c1\n",
1775 )
1776 .unwrap();
1777 assert_eq!(
1778 read_raw_ref(git_dir, "refs/ns").unwrap(),
1779 RawRefLookup::IsDirectory
1780 );
1781 let extras = BTreeSet::from(["refs/ns/newchild".to_string()]);
1782 let skip = HashSet::new();
1783 verify_refname_available_for_create(git_dir, "refs/ns/newchild", &extras, &skip).unwrap();
1784 }
1785}
1786
1787#[cfg(test)]
1788mod read_raw_ref_tests {
1789 use super::*;
1790 use tempfile::tempdir;
1791
1792 #[test]
1793 fn loose_ref_file_is_exists() {
1794 let dir = tempdir().unwrap();
1795 let git_dir = dir.path();
1796 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
1797 fs::write(
1798 git_dir.join("refs/heads/side"),
1799 "0000000000000000000000000000000000000000\n",
1800 )
1801 .unwrap();
1802 assert_eq!(
1803 read_raw_ref(git_dir, "refs/heads/side").unwrap(),
1804 RawRefLookup::Exists
1805 );
1806 }
1807
1808 #[test]
1809 fn missing_ref_is_not_found() {
1810 let dir = tempdir().unwrap();
1811 let git_dir = dir.path();
1812 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
1813 assert_eq!(
1814 read_raw_ref(git_dir, "refs/heads/nope").unwrap(),
1815 RawRefLookup::NotFound
1816 );
1817 }
1818
1819 #[test]
1820 fn directory_where_ref_expected_is_is_directory() {
1821 let dir = tempdir().unwrap();
1822 let git_dir = dir.path();
1823 fs::create_dir_all(git_dir.join("refs/heads")).unwrap();
1824 assert_eq!(
1825 read_raw_ref(git_dir, "refs/heads").unwrap(),
1826 RawRefLookup::IsDirectory
1827 );
1828 }
1829
1830 #[test]
1831 fn packed_ref_name_is_exists() {
1832 let dir = tempdir().unwrap();
1833 let git_dir = dir.path();
1834 fs::write(
1835 git_dir.join("packed-refs"),
1836 "# pack-refs with: peeled fully-peeled \n\
1837 0000000000000000000000000000000000000000 refs/heads/packed\n",
1838 )
1839 .unwrap();
1840 assert_eq!(
1841 read_raw_ref(git_dir, "refs/heads/packed").unwrap(),
1842 RawRefLookup::Exists
1843 );
1844 }
1845}