Skip to main content

grit_lib/
worktree_ref.rs

1//! Per-worktree ref name parsing and storage location (Git `parse_worktree_ref` / `files_ref_path`).
2
3use std::borrow::Cow;
4use std::path::{Path, PathBuf};
5
6use crate::refs::common_dir;
7
8/// How a ref name maps to on-disk storage across worktrees.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum RefWorktreeType {
11    /// `HEAD`, `refs/worktree/*`, `refs/bisect/*`, `refs/rewritten/*` in this checkout's git dir.
12    Current,
13    /// `main-worktree/HEAD` → common dir + bare name.
14    Main,
15    /// `worktrees/<id>/HEAD` → `common/worktrees/<id>/` + bare name.
16    Other,
17    /// Shared refs (`refs/heads/*`, …) in the common git directory.
18    Shared,
19}
20
21/// `refs/worktree/*`, `refs/bisect/*`, and `refs/rewritten/*` are per-worktree.
22#[must_use]
23pub fn is_per_worktree_ref(refname: &str) -> bool {
24    refname.starts_with("refs/worktree/")
25        || refname.starts_with("refs/bisect/")
26        || refname.starts_with("refs/rewritten/")
27}
28
29fn is_root_ref_syntax(refname: &str) -> bool {
30    !refname.is_empty()
31        && refname
32            .chars()
33            .all(|c| c.is_ascii_uppercase() || c == '-' || c == '_')
34}
35
36fn is_current_worktree_ref(refname: &str) -> bool {
37    is_root_ref_syntax(refname) || is_per_worktree_ref(refname)
38}
39
40/// Parse `maybe_worktree_ref` like Git `parse_worktree_ref`.
41///
42/// Returns worktree kind, bare ref name for storage, and worktree id for `worktrees/<id>/…`.
43#[must_use]
44pub fn parse_worktree_ref(maybe: &str) -> (RefWorktreeType, Cow<'_, str>, Option<Cow<'_, str>>) {
45    if let Some(rest) = maybe.strip_prefix("worktrees/") {
46        if let Some((id, bare)) = rest.split_once('/') {
47            if is_current_worktree_ref(bare) {
48                return (
49                    RefWorktreeType::Other,
50                    Cow::Borrowed(bare),
51                    Some(Cow::Borrowed(id)),
52                );
53            }
54        }
55        return (
56            RefWorktreeType::Other,
57            Cow::Borrowed(""),
58            Some(Cow::Borrowed(rest)),
59        );
60    }
61
62    if let Some(bare) = maybe.strip_prefix("main-worktree/") {
63        if is_current_worktree_ref(bare) {
64            return (RefWorktreeType::Main, Cow::Borrowed(bare), None);
65        }
66    }
67
68    if is_current_worktree_ref(maybe) {
69        return (RefWorktreeType::Current, Cow::Borrowed(maybe), None);
70    }
71
72    (RefWorktreeType::Shared, Cow::Borrowed(maybe), None)
73}
74
75/// Git directory and on-disk ref path for `refname` from the current process's linked checkout.
76#[must_use]
77pub fn resolve_ref_storage(git_dir: &Path, refname: &str) -> (PathBuf, String) {
78    let common = common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
79    resolve_ref_storage_in(git_dir, common, refname)
80}
81
82/// [`resolve_ref_storage`] with the shared directory `common` already resolved, so no
83/// `commondir` lookup happens per ref.
84pub(crate) fn resolve_ref_storage_in(
85    git_dir: &Path,
86    common: PathBuf,
87    refname: &str,
88) -> (PathBuf, String) {
89    let (kind, bare, wt_id) = parse_worktree_ref(refname);
90    match kind {
91        RefWorktreeType::Main => (common, bare.into_owned()),
92        RefWorktreeType::Other => {
93            let id = wt_id.map(|c| c.into_owned()).unwrap_or_default();
94            (common.join("worktrees").join(id), bare.into_owned())
95        }
96        RefWorktreeType::Current => (git_dir.to_path_buf(), refname.to_owned()),
97        RefWorktreeType::Shared => (common, refname.to_owned()),
98    }
99}
100
101/// Whether `refname` should appear in `for-each-ref` from a linked worktree.
102#[must_use]
103pub fn ref_visible_from_worktree(git_dir: &Path, refname: &str) -> bool {
104    if !is_per_worktree_ref(refname) {
105        return true;
106    }
107    let (store, stor_name) = resolve_ref_storage(git_dir, refname);
108    store.join(&stor_name).is_file()
109}
110
111/// True when `git_dir` is a linked worktree administrative directory.
112#[must_use]
113pub fn is_linked_worktree_git_dir(git_dir: &Path) -> bool {
114    common_dir(git_dir).is_some()
115}
116
117/// DWIM rules matching Git `ref_rev_parse_rules` (`refs.c`).
118pub(crate) const DWIM_RULES: &[&str] = &[
119    "{0}",
120    "refs/{0}",
121    "refs/tags/{0}",
122    "refs/heads/{0}",
123    "refs/remotes/{0}",
124    "refs/remotes/{0}/HEAD",
125];
126
127/// Resolve `spec` using Git `ref_rev_parse_rules` / `expand_ref`.
128///
129/// Returns the number of matching rules and the OID from the first match.
130pub fn resolve_ref_dwim<F>(mut resolve: F, spec: &str) -> (usize, Option<crate::objects::ObjectId>)
131where
132    F: FnMut(&str) -> Option<crate::objects::ObjectId>,
133{
134    let mut count = 0usize;
135    let mut first = None;
136    for rule in DWIM_RULES {
137        let candidate = rule.replace("{0}", spec);
138        if let Some(oid) = resolve(&candidate) {
139            count += 1;
140            if first.is_none() {
141                first = Some(oid);
142            }
143        }
144    }
145    (count, first)
146}
147
148/// When exactly one DWIM ref rule matches `spec`, return that expanded ref name.
149///
150/// Rule order matches Git (`refs/tags` before `refs/heads`). Returns `None` when zero or
151/// more than one rule matches.
152#[must_use]
153pub fn sole_dwim_ref_name<F>(mut exists: F, spec: &str) -> Option<String>
154where
155    F: FnMut(&str) -> bool,
156{
157    let mut count = 0usize;
158    let mut sole = None;
159    for rule in DWIM_RULES {
160        let candidate = rule.replace("{0}", spec);
161        if exists(&candidate) {
162            count += 1;
163            if sole.is_none() {
164                sole = Some(candidate);
165            }
166        }
167    }
168    if count == 1 {
169        sole
170    } else {
171        None
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::objects::ObjectId;
179    use std::borrow::Cow;
180
181    #[test]
182    fn parse_worktree_ref_prefixes() {
183        let (kind, bare, id) = parse_worktree_ref("main-worktree/HEAD");
184        assert_eq!(kind, RefWorktreeType::Main);
185        assert_eq!(bare, "HEAD");
186        assert!(id.is_none());
187
188        let (kind, bare, id) = parse_worktree_ref("worktrees/wt1/HEAD");
189        assert_eq!(kind, RefWorktreeType::Other);
190        assert_eq!(bare, "HEAD");
191        assert_eq!(id, Some(Cow::Borrowed("wt1")));
192
193        let (kind, bare, _) = parse_worktree_ref("refs/heads/main");
194        assert_eq!(kind, RefWorktreeType::Shared);
195        assert_eq!(bare, "refs/heads/main");
196    }
197
198    #[test]
199    fn is_per_worktree_ref_matches_git_prefixes() {
200        assert!(is_per_worktree_ref("refs/worktree/foo"));
201        assert!(is_per_worktree_ref("refs/bisect/foo"));
202        assert!(!is_per_worktree_ref("refs/heads/main"));
203    }
204
205    #[test]
206    fn resolve_ref_dwim_finds_head_alias() {
207        let oid = ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap();
208        let resolve = |name: &str| {
209            if name == "refs/heads/main" {
210                Some(oid)
211            } else {
212                None
213            }
214        };
215        let (count, first) = resolve_ref_dwim(resolve, "main");
216        assert!(count >= 1);
217        assert_eq!(first, Some(oid));
218    }
219
220    #[test]
221    fn ref_visible_from_worktree_per_worktree_file() {
222        let tmp = tempfile::TempDir::new().unwrap();
223        let git_dir = tmp.path().join(".git");
224        std::fs::create_dir_all(git_dir.join("refs/worktree")).unwrap();
225        std::fs::write(git_dir.join("refs/worktree/x"), b"ref: refs/heads/main\n").unwrap();
226        assert!(ref_visible_from_worktree(&git_dir, "refs/worktree/x"));
227        assert!(!ref_visible_from_worktree(
228            &git_dir,
229            "refs/worktree/missing"
230        ));
231    }
232}