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() {
86 PathBuf::from(rel)
87 } else {
88 git_dir.join(rel)
89 };
90 path.canonicalize().ok()
91}
92
93fn resolve_ref_depth(
99 git_dir: &Path,
100 common: Option<&Path>,
101 refname: &str,
102 depth: usize,
103) -> Result<ObjectId> {
104 if depth > 10 {
105 return Err(Error::InvalidRef(format!(
106 "ref symlink too deep: {refname}"
107 )));
108 }
109
110 let path = git_dir.join(refname);
112 match read_ref_file(&path) {
113 Ok(Ref::Direct(oid)) => return Ok(oid),
114 Ok(Ref::Symbolic(target)) => {
115 return resolve_ref_depth(git_dir, common, &target, depth + 1);
116 }
117 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
118 Err(e) => return Err(e),
119 }
120
121 if let Some(cdir) = common {
123 if cdir != git_dir {
124 let cpath = cdir.join(refname);
125 match read_ref_file(&cpath) {
126 Ok(Ref::Direct(oid)) => return Ok(oid),
127 Ok(Ref::Symbolic(target)) => {
128 return resolve_ref_depth(git_dir, common, &target, depth + 1);
129 }
130 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
131 Err(e) => return Err(e),
132 }
133 }
134 }
135
136 let packed_dir = common.unwrap_or(git_dir);
138 if let Some(oid) = lookup_packed_ref(packed_dir, refname)? {
139 return Ok(oid);
140 }
141 if common.is_some() && common != Some(git_dir) {
143 if let Some(oid) = lookup_packed_ref(git_dir, refname)? {
144 return Ok(oid);
145 }
146 }
147
148 Err(Error::InvalidRef(format!("ref not found: {refname}")))
149}
150
151fn lookup_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
153 let packed = git_dir.join("packed-refs");
154 let content = match fs::read_to_string(&packed) {
155 Ok(c) => c,
156 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
157 Err(e) => return Err(Error::Io(e)),
158 };
159
160 for line in content.lines() {
161 if line.starts_with('#') || line.starts_with('^') {
162 continue;
163 }
164 let mut parts = line.splitn(2, ' ');
165 let hash = parts.next().unwrap_or("");
166 let name = parts.next().unwrap_or("").trim();
167 if name == refname && hash.len() == 40 {
168 let oid: ObjectId = hash.parse()?;
169 return Ok(Some(oid));
170 }
171 }
172 Ok(None)
173}
174
175pub fn write_ref(git_dir: &Path, refname: &str, oid: &ObjectId) -> Result<()> {
189 if crate::reftable::is_reftable_repo(git_dir) {
190 return crate::reftable::reftable_write_ref(git_dir, refname, oid, None, None);
191 }
192 let storage_dir = ref_storage_dir(git_dir, refname);
193 let path = storage_dir.join(refname);
194 if let Some(parent) = path.parent() {
195 fs::create_dir_all(parent)?;
196 }
197 let content = format!("{oid}\n");
198 let lock = path.with_extension("lock");
200 fs::write(&lock, &content)?;
201 fs::rename(&lock, &path)?;
202 Ok(())
203}
204
205pub fn delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
213 if crate::reftable::is_reftable_repo(git_dir) {
214 return crate::reftable::reftable_delete_ref(git_dir, refname);
215 }
216 let storage_dir = ref_storage_dir(git_dir, refname);
217 let path = storage_dir.join(refname);
219 match fs::remove_file(&path) {
220 Ok(()) => {}
221 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
222 Err(e) => return Err(Error::Io(e)),
223 }
224
225 remove_packed_ref(&storage_dir, refname)?;
227
228 let log_path = storage_dir.join("logs").join(refname);
229 let _ = fs::remove_file(&log_path);
230
231 Ok(())
232}
233
234fn remove_packed_ref(git_dir: &Path, refname: &str) -> Result<()> {
236 let packed_path = git_dir.join("packed-refs");
237 let content = match fs::read_to_string(&packed_path) {
238 Ok(c) => c,
239 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
240 Err(e) => return Err(Error::Io(e)),
241 };
242
243 let mut out = String::new();
244 let mut skip_peeled = false;
245 let mut changed = false;
246 let mut header_written = false;
249
250 for line in content.lines() {
251 if skip_peeled {
252 if line.starts_with('^') {
253 changed = true;
254 continue;
255 }
256 skip_peeled = false;
257 }
258
259 if line.starts_with('#') {
260 continue;
262 }
263 if line.starts_with('^') {
264 out.push_str(line);
265 out.push('\n');
266 continue;
267 }
268
269 if !header_written {
271 out.insert_str(0, "# pack-refs with: peeled fully-peeled sorted\n");
272 header_written = true;
273 }
274
275 let mut parts = line.splitn(2, ' ');
277 let _hash = parts.next().unwrap_or("");
278 let name = parts.next().unwrap_or("").trim();
279 if name == refname {
280 changed = true;
281 skip_peeled = true;
282 continue;
283 }
284
285 out.push_str(line);
286 out.push('\n');
287 }
288
289 if changed {
290 let lock = packed_path.with_extension("lock");
291 fs::write(&lock, &out).map_err(Error::Io)?;
292 fs::rename(&lock, &packed_path).map_err(Error::Io)?;
293 }
294
295 Ok(())
296}
297
298pub fn read_head(git_dir: &Path) -> Result<Option<String>> {
306 match read_ref_file(&git_dir.join("HEAD"))? {
307 Ref::Symbolic(target) => Ok(Some(target)),
308 Ref::Direct(_) => Ok(None),
309 }
310}
311
312pub fn read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
319 if crate::reftable::is_reftable_repo(git_dir) {
320 return crate::reftable::reftable_read_symbolic_ref(git_dir, refname);
321 }
322 let path = git_dir.join(refname);
323 match read_ref_file(&path) {
324 Ok(Ref::Symbolic(target)) => Ok(Some(target)),
325 Ok(Ref::Direct(_)) => Ok(None),
326 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {
327 if let Some(common) = common_dir(git_dir) {
328 if common != git_dir {
329 let cpath = common.join(refname);
330 match read_ref_file(&cpath) {
331 Ok(Ref::Symbolic(target)) => return Ok(Some(target)),
332 Ok(Ref::Direct(_)) => return Ok(None),
333 Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::NotFound => {}
334 Err(e) => return Err(e),
335 }
336 }
337 }
338 Ok(None)
339 }
340 Err(e) => Err(e),
341 }
342}
343
344#[derive(Clone, Copy, Debug, PartialEq, Eq)]
346pub enum LogRefsConfig {
347 Unset,
349 None,
351 Normal,
353 Always,
355}
356
357pub fn read_log_refs_config(git_dir: &Path) -> LogRefsConfig {
361 let config_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
362 let config_path = config_dir.join("config");
363 let content = match fs::read_to_string(config_path) {
364 Ok(c) => c,
365 Err(_) => return LogRefsConfig::Unset,
366 };
367
368 let mut in_core = false;
369 for line in content.lines() {
370 let trimmed = line.trim();
371 if trimmed.starts_with('[') {
372 in_core = trimmed.to_ascii_lowercase().starts_with("[core]");
373 continue;
374 }
375 if !in_core {
376 continue;
377 }
378 let Some((key, value)) = trimmed.split_once('=') else {
379 continue;
380 };
381 if !key.trim().eq_ignore_ascii_case("logallrefupdates") {
382 continue;
383 }
384 let v = value.trim();
385 let lower = v.to_ascii_lowercase();
386 return match lower.as_str() {
387 "always" => LogRefsConfig::Always,
388 "1" | "true" | "yes" | "on" => LogRefsConfig::Normal,
389 "0" | "false" | "no" | "off" | "never" => LogRefsConfig::None,
390 _ => LogRefsConfig::Unset,
391 };
392 }
393 LogRefsConfig::Unset
394}
395
396fn read_core_bare(git_dir: &Path) -> bool {
397 let config_dir = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
398 let config_path = config_dir.join("config");
399 let Ok(content) = fs::read_to_string(config_path) else {
400 return false;
401 };
402 let mut in_core = false;
403 for line in content.lines() {
404 let trimmed = line.trim();
405 if trimmed.starts_with('[') {
406 in_core = trimmed.to_ascii_lowercase().starts_with("[core]");
407 continue;
408 }
409 if !in_core {
410 continue;
411 }
412 let Some((key, value)) = trimmed.split_once('=') else {
413 continue;
414 };
415 if key.trim().eq_ignore_ascii_case("bare") {
416 let v = value.trim().to_ascii_lowercase();
417 return matches!(v.as_str(), "1" | "true" | "yes" | "on");
418 }
419 }
420 false
421}
422
423pub fn effective_log_refs_config(git_dir: &Path) -> LogRefsConfig {
425 match read_log_refs_config(git_dir) {
426 LogRefsConfig::Unset => {
427 if read_core_bare(git_dir) {
428 LogRefsConfig::None
429 } else {
430 LogRefsConfig::Normal
431 }
432 }
433 other => other,
434 }
435}
436
437#[must_use]
439pub fn should_autocreate_reflog(git_dir: &Path, refname: &str) -> bool {
440 match effective_log_refs_config(git_dir) {
441 LogRefsConfig::Always => true,
442 LogRefsConfig::Normal => {
443 refname == "HEAD"
444 || refname.starts_with("refs/heads/")
445 || refname.starts_with("refs/remotes/")
446 || refname.starts_with("refs/notes/")
447 }
448 LogRefsConfig::None | LogRefsConfig::Unset => false,
449 }
450}
451
452pub fn append_reflog(
470 git_dir: &Path,
471 refname: &str,
472 old_oid: &ObjectId,
473 new_oid: &ObjectId,
474 identity: &str,
475 message: &str,
476 force_create: bool,
477) -> Result<()> {
478 if crate::reftable::is_reftable_repo(git_dir) {
479 return crate::reftable::reftable_append_reflog(
480 git_dir,
481 refname,
482 old_oid,
483 new_oid,
484 identity,
485 message,
486 force_create,
487 );
488 }
489 let storage_dir = ref_storage_dir(git_dir, refname);
490 let log_path = storage_dir.join("logs").join(refname);
491 let may_write =
492 force_create || should_autocreate_reflog(git_dir, refname) || !message.is_empty();
493 if !may_write && !log_path.exists() {
494 return Ok(());
495 }
496 if let Some(parent) = log_path.parent() {
497 fs::create_dir_all(parent)?;
498 }
499 let line = if message.is_empty() {
500 format!("{old_oid} {new_oid} {identity}\n")
501 } else {
502 format!("{old_oid} {new_oid} {identity}\t{message}\n")
503 };
504 let mut file = fs::OpenOptions::new()
505 .create(true)
506 .append(true)
507 .open(&log_path)?;
508 use io::Write;
509 file.write_all(line.as_bytes())?;
510 Ok(())
511}
512
513fn ref_storage_dir(git_dir: &Path, refname: &str) -> PathBuf {
514 if refname == "HEAD" || refname.starts_with("refs/bisect/") {
515 return git_dir.to_path_buf();
516 }
517 common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
518}
519
520pub fn list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
530 if crate::reftable::is_reftable_repo(git_dir) {
531 return crate::reftable::reftable_list_refs(git_dir, prefix);
532 }
533 let mut results = Vec::new();
534 let base = git_dir.join(prefix);
535 collect_refs(&base, prefix, git_dir, &mut results)?;
536 collect_packed_refs(git_dir, prefix, &mut results)?;
537
538 if let Some(cdir) = common_dir(git_dir) {
540 if cdir != git_dir {
541 let cbase = cdir.join(prefix);
542 collect_refs(&cbase, prefix, &cdir, &mut results)?;
543 collect_packed_refs(&cdir, prefix, &mut results)?;
544 results.sort_by(|a, b| a.0.cmp(&b.0));
546 results.dedup_by(|b, a| a.0 == b.0);
547 }
548 }
549
550 results.sort_by(|a, b| a.0.cmp(&b.0));
551 Ok(results)
552}
553
554pub fn list_refs_glob(git_dir: &Path, pattern: &str) -> Result<Vec<(String, ObjectId)>> {
556 let glob_pos = pattern.find(['*', '?', '[']);
557 let prefix = match glob_pos {
558 Some(pos) => match pattern[..pos].rfind('/') {
559 Some(slash) => &pattern[..=slash],
560 None => "",
561 },
562 None => pattern,
563 };
564 let all = list_refs(git_dir, prefix)?;
565 let mut results = Vec::new();
566 for (refname, oid) in all {
567 if glob_match(pattern, &refname) {
568 results.push((refname, oid));
569 }
570 }
571 Ok(results)
572}
573
574pub fn ref_matches_glob(refname: &str, pattern: &str) -> bool {
578 if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
580 return refname == pattern
581 || refname.ends_with(&format!("/{pattern}"))
582 || refname.starts_with(&format!("{pattern}/"));
583 }
584 glob_match(pattern, refname)
585}
586
587fn glob_match(pattern: &str, text: &str) -> bool {
588 let pat = pattern.as_bytes();
589 let txt = text.as_bytes();
590 let (mut pi, mut ti) = (0, 0);
591 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
592 while ti < txt.len() {
593 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
594 pi += 1;
595 ti += 1;
596 } else if pi < pat.len() && pat[pi] == b'*' {
597 star_pi = pi;
598 star_ti = ti;
599 pi += 1;
600 } else if star_pi != usize::MAX {
601 pi = star_pi + 1;
602 star_ti += 1;
603 ti = star_ti;
604 } else {
605 return false;
606 }
607 }
608 while pi < pat.len() && pat[pi] == b'*' {
609 pi += 1;
610 }
611 pi == pat.len()
612}
613
614fn collect_refs(
615 dir: &Path,
616 prefix: &str,
617 git_dir: &Path,
618 out: &mut Vec<(String, ObjectId)>,
619) -> Result<()> {
620 let read = match fs::read_dir(dir) {
621 Ok(r) => r,
622 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
623 Err(e) => return Err(Error::Io(e)),
624 };
625
626 for entry in read {
627 let entry = entry?;
628 let file_type = entry.file_type()?;
629 let name = entry.file_name();
630 let name_str = name.to_string_lossy();
631 let refname = format!("{prefix}{name_str}");
632
633 if file_type.is_dir() {
634 collect_refs(&entry.path(), &format!("{refname}/"), git_dir, out)?;
635 } else if file_type.is_file() {
636 if let Ok(oid) = resolve_ref(git_dir, &refname) {
637 out.push((refname, oid))
638 }
639 }
640 }
641 Ok(())
642}
643
644pub fn resolve_at_n_branch(git_dir: &Path, spec: &str) -> Result<String> {
647 let inner = spec
649 .strip_prefix("@{-")
650 .and_then(|s| s.strip_suffix('}'))
651 .ok_or_else(|| Error::InvalidRef(format!("not an @{{-N}} ref: {spec}")))?;
652 let n: usize = inner
653 .parse()
654 .map_err(|_| Error::InvalidRef(format!("invalid N in {spec}")))?;
655 if n == 0 {
656 return Err(Error::InvalidRef("@{-0} is not valid".to_string()));
657 }
658 let entries = crate::reflog::read_reflog(git_dir, "HEAD")?;
659 let mut count = 0usize;
660 for entry in entries.iter().rev() {
661 let msg = &entry.message;
662 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
663 count += 1;
664 if count == n {
665 if let Some(to_pos) = rest.find(" to ") {
666 return Ok(rest[..to_pos].to_string());
667 }
668 }
669 }
670 }
671 Err(Error::InvalidRef(format!(
672 "{spec}: only {count} checkout(s) in reflog"
673 )))
674}
675
676fn collect_packed_refs(
677 git_dir: &Path,
678 prefix: &str,
679 out: &mut Vec<(String, ObjectId)>,
680) -> Result<()> {
681 let packed_path = git_dir.join("packed-refs");
682 let content = match fs::read_to_string(&packed_path) {
683 Ok(c) => c,
684 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
685 Err(e) => return Err(Error::Io(e)),
686 };
687
688 for line in content.lines() {
689 if line.starts_with('#') || line.starts_with('^') || line.is_empty() {
690 continue;
691 }
692 let mut parts = line.splitn(2, ' ');
693 let hash = parts.next().unwrap_or("");
694 let refname = parts.next().unwrap_or("").trim();
695 if !refname.starts_with(prefix) || hash.len() != 40 {
696 continue;
697 }
698 let oid: ObjectId = hash.parse()?;
699 out.push((refname.to_string(), oid));
700 }
701 Ok(())
702}