Skip to main content

git_core/
refs.rs

1use std::path::Path;
2
3use crate::error::{GitError, Result};
4use crate::hash::GitHash;
5
6/// Enumerate every ref in `git_dir`: the loose `refs/**` tree, `packed-refs`,
7/// and `HEAD` (when it resolves to a hash). Returns `(refname, target_hash)`
8/// pairs. Unresolvable or malformed refs are skipped; never panics.
9#[must_use]
10pub fn list_refs(git_dir: &Path) -> Vec<(String, GitHash)> {
11    let mut out: Vec<(String, GitHash)> = Vec::new();
12
13    // Loose refs under refs/ (recursively).
14    let refs_root = git_dir.join("refs");
15    collect_loose_refs(&refs_root, "refs", git_dir, &mut out);
16
17    // packed-refs: lines of "<sha> <refname>"; "^<sha>" peel lines are skipped
18    // (the peeled tag commit is reachable via the tag object itself).
19    if let Ok(text) = std::fs::read_to_string(git_dir.join("packed-refs")) {
20        for line in text.lines() {
21            let line = line.trim();
22            if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
23                continue;
24            }
25            if let Some((sha, name)) = line.split_once(' ') {
26                if out.iter().any(|(n, _)| n == name) {
27                    continue; // a loose ref shadows the packed one.
28                }
29                if let Ok(hash) = GitHash::from_hex(sha.trim()) {
30                    out.push((name.trim().to_string(), hash));
31                }
32            }
33        }
34    }
35
36    // HEAD, resolved to a hash (symbolic or detached).
37    if let Ok(hash) = resolve_ref(git_dir, "HEAD") {
38        if !out.iter().any(|(n, _)| n == "HEAD") {
39            out.push(("HEAD".to_string(), hash));
40        }
41    }
42
43    out
44}
45
46/// Recursively walk a loose-ref directory, appending `(refname, hash)` pairs.
47fn collect_loose_refs(dir: &Path, prefix: &str, git_dir: &Path, out: &mut Vec<(String, GitHash)>) {
48    let Ok(entries) = std::fs::read_dir(dir) else {
49        return;
50    };
51    for entry in entries.flatten() {
52        let name = entry.file_name();
53        let Some(name) = name.to_str() else {
54            continue;
55        };
56        let refname = format!("{prefix}/{name}");
57        let path = entry.path();
58        if path.is_dir() {
59            collect_loose_refs(&path, &refname, git_dir, out);
60        } else if let Ok(hash) = resolve_ref(git_dir, &refname) {
61            out.push((refname, hash));
62        }
63    }
64}
65
66/// Resolve a ref name to its target hash.
67///
68/// Handles:
69/// - `HEAD` (may be symbolic or a detached commit hash)
70/// - `refs/heads/<branch>`
71/// - bare 40-hex strings
72pub fn resolve_ref(git_dir: &Path, refname: &str) -> Result<GitHash> {
73    if refname.len() == 40 && refname.chars().all(|c| c.is_ascii_hexdigit()) {
74        return GitHash::from_hex(refname);
75    }
76
77    let ref_path = git_dir.join(refname);
78    let content = std::fs::read_to_string(&ref_path).map_err(|e| {
79        if e.kind() == std::io::ErrorKind::NotFound {
80            GitError::RefNotFound(refname.to_string())
81        } else {
82            GitError::Io(e)
83        }
84    })?;
85
86    let content = content.trim();
87
88    // Symbolic ref: "ref: refs/heads/main"
89    if let Some(target) = content.strip_prefix("ref: ") {
90        return resolve_ref(git_dir, target);
91    }
92
93    GitHash::from_hex(content)
94        .map_err(|_| GitError::RefNotFound(format!("{refname}: invalid hash {content:?}")))
95}