1use std::borrow::Cow;
4use std::path::{Path, PathBuf};
5
6use crate::refs::common_dir;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum RefWorktreeType {
11 Current,
13 Main,
15 Other,
17 Shared,
19}
20
21#[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#[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#[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
82pub(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#[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#[must_use]
113pub fn is_linked_worktree_git_dir(git_dir: &Path) -> bool {
114 common_dir(git_dir).is_some()
115}
116
117pub(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
127pub 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#[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}