1use std::{
2 borrow::Cow,
3 cmp::Ordering,
4 io::Read,
5 iter::Peekable,
6 path::{Path, PathBuf},
7};
8
9use crate::{
10 file::{loose, loose::iter::SortedLoosePaths, path_to_name},
11 store_impl::{file, packed},
12 BString, FullName, Namespace, Reference,
13};
14
15pub struct LooseThenPacked<'p, 's> {
20 git_dir: &'s Path,
21 common_dir: Option<&'s Path>,
22 namespace: Option<&'s Namespace>,
23 iter_packed: Option<Peekable<packed::Iter<'p>>>,
24 iter_git_dir: Peekable<SortedLoosePaths>,
25 #[allow(dead_code)]
26 iter_common_dir: Option<Peekable<SortedLoosePaths>>,
27 buf: Vec<u8>,
28}
29
30enum IterKind {
31 Git,
32 GitAndConsumeCommon,
33 Common,
34}
35
36#[must_use = "Iterators should be obtained from this platform"]
38pub struct Platform<'s> {
39 store: &'s file::Store,
40 packed: Option<file::packed::SharedBufferSnapshot>,
41}
42
43impl<'p, 's> LooseThenPacked<'p, 's> {
44 fn strip_namespace(&self, mut r: Reference) -> Reference {
45 if let Some(namespace) = &self.namespace {
46 r.strip_namespace(namespace);
47 }
48 r
49 }
50
51 fn loose_iter(&mut self, kind: IterKind) -> &mut Peekable<SortedLoosePaths> {
52 match kind {
53 IterKind::GitAndConsumeCommon => {
54 drop(self.iter_common_dir.as_mut().map(|iter| iter.next()));
55 &mut self.iter_git_dir
56 }
57 IterKind::Git => &mut self.iter_git_dir,
58 IterKind::Common => self
59 .iter_common_dir
60 .as_mut()
61 .expect("caller knows there is a common iter"),
62 }
63 }
64
65 fn convert_packed(
66 &mut self,
67 packed: Result<packed::Reference<'p>, packed::iter::Error>,
68 ) -> Result<Reference, Error> {
69 packed
70 .map(Into::into)
71 .map(|r| self.strip_namespace(r))
72 .map_err(|err| match err {
73 packed::iter::Error::Reference {
74 invalid_line,
75 line_number,
76 } => Error::PackedReference {
77 invalid_line,
78 line_number,
79 },
80 packed::iter::Error::Header { .. } => unreachable!("this one only happens on iteration creation"),
81 })
82 }
83
84 fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result<Reference, Error> {
85 let (refpath, name) = res.map_err(Error::Traversal)?;
86 std::fs::File::open(&refpath)
87 .and_then(|mut f| {
88 self.buf.clear();
89 f.read_to_end(&mut self.buf)
90 })
91 .map_err(|err| Error::ReadFileContents {
92 source: err,
93 path: refpath.to_owned(),
94 })?;
95 loose::Reference::try_from_path(name, &self.buf)
96 .map_err(|err| {
97 let relative_path = refpath
98 .strip_prefix(self.git_dir)
99 .ok()
100 .or_else(|| {
101 self.common_dir
102 .and_then(|common_dir| refpath.strip_prefix(common_dir).ok())
103 })
104 .expect("one of our bases contains the path");
105 Error::ReferenceCreation {
106 source: err,
107 relative_path: relative_path.into(),
108 }
109 })
110 .map(Into::into)
111 .map(|r| self.strip_namespace(r))
112 }
113}
114
115impl<'p, 's> Iterator for LooseThenPacked<'p, 's> {
116 type Item = Result<Reference, Error>;
117
118 fn next(&mut self) -> Option<Self::Item> {
119 fn advance_to_non_private(iter: &mut Peekable<SortedLoosePaths>) {
120 while let Some(Ok((_path, name))) = iter.peek() {
121 if name.category().map_or(true, |cat| cat.is_worktree_private()) {
122 iter.next();
123 } else {
124 break;
125 }
126 }
127 }
128
129 fn peek_loose<'a>(
130 git_dir: &'a mut Peekable<SortedLoosePaths>,
131 common_dir: Option<&'a mut Peekable<SortedLoosePaths>>,
132 ) -> Option<(&'a std::io::Result<(PathBuf, FullName)>, IterKind)> {
133 match common_dir {
134 Some(common_dir) => match (git_dir.peek(), {
135 advance_to_non_private(common_dir);
136 common_dir.peek()
137 }) {
138 (None, None) => None,
139 (None, Some(res)) | (Some(_), Some(res @ Err(_))) => Some((res, IterKind::Common)),
140 (Some(res), None) | (Some(res @ Err(_)), Some(_)) => Some((res, IterKind::Git)),
141 (Some(r_gitdir @ Ok((_, git_dir_name))), Some(r_cd @ Ok((_, common_dir_name)))) => {
142 match git_dir_name.cmp(common_dir_name) {
143 Ordering::Less => Some((r_gitdir, IterKind::Git)),
144 Ordering::Equal => Some((r_gitdir, IterKind::GitAndConsumeCommon)),
145 Ordering::Greater => Some((r_cd, IterKind::Common)),
146 }
147 }
148 },
149 None => git_dir.peek().map(|r| (r, IterKind::Git)),
150 }
151 }
152 match self.iter_packed.as_mut() {
153 Some(packed_iter) => match (
154 peek_loose(&mut self.iter_git_dir, self.iter_common_dir.as_mut()),
155 packed_iter.peek(),
156 ) {
157 (None, None) => None,
158 (None, Some(_)) | (Some(_), Some(Err(_))) => {
159 let res = packed_iter.next().expect("peeked value exists");
160 Some(self.convert_packed(res))
161 }
162 (Some((_, kind)), None) | (Some((Err(_), kind)), Some(_)) => {
163 let res = self.loose_iter(kind).next().expect("prior peek");
164 Some(self.convert_loose(res))
165 }
166 (Some((Ok((_, loose_name)), kind)), Some(Ok(packed))) => match loose_name.as_ref().cmp(packed.name) {
167 Ordering::Less => {
168 let res = self.loose_iter(kind).next().expect("prior peek");
169 Some(self.convert_loose(res))
170 }
171 Ordering::Equal => {
172 drop(packed_iter.next());
173 let res = self.loose_iter(kind).next().expect("prior peek");
174 Some(self.convert_loose(res))
175 }
176 Ordering::Greater => {
177 let res = packed_iter.next().expect("name retrieval configured");
178 Some(self.convert_packed(res))
179 }
180 },
181 },
182 None => match peek_loose(&mut self.iter_git_dir, self.iter_common_dir.as_mut()) {
183 None => None,
184 Some((_, kind)) => self.loose_iter(kind).next().map(|res| self.convert_loose(res)),
185 },
186 }
187 }
188}
189
190impl<'s> Platform<'s> {
191 pub fn all(&self) -> std::io::Result<LooseThenPacked<'_, '_>> {
195 self.store.iter_packed(self.packed.as_ref().map(|b| &***b))
196 }
197
198 pub fn prefixed(&self, prefix: impl AsRef<Path>) -> std::io::Result<LooseThenPacked<'_, '_>> {
202 self.store
203 .iter_prefixed_packed(prefix, self.packed.as_ref().map(|b| &***b))
204 }
205}
206
207impl file::Store {
208 pub fn iter(&self) -> Result<Platform<'_>, packed::buffer::open::Error> {
212 Ok(Platform {
213 store: self,
214 packed: self.assure_packed_refs_uptodate()?,
215 })
216 }
217}
218
219#[derive(Debug)]
220pub(crate) enum IterInfo<'a> {
221 Base {
222 base: &'a Path,
223 },
224 BaseAndIterRoot {
225 base: &'a Path,
226 iter_root: PathBuf,
227 prefix: Cow<'a, Path>,
228 },
229 PrefixAndBase {
230 base: &'a Path,
231 prefix: &'a Path,
232 },
233 ComputedIterationRoot {
234 iter_root: PathBuf,
236 base: &'a Path,
238 prefix: Cow<'a, Path>,
240 remainder: Option<BString>,
242 },
243}
244
245impl<'a> IterInfo<'a> {
246 fn prefix(&self) -> Option<&Path> {
247 match self {
248 IterInfo::Base { .. } => None,
249 IterInfo::PrefixAndBase { prefix, .. } => Some(*prefix),
250 IterInfo::ComputedIterationRoot { prefix, .. } | IterInfo::BaseAndIterRoot { prefix, .. } => {
251 prefix.as_ref().into()
252 }
253 }
254 }
255
256 fn into_iter(self) -> Peekable<SortedLoosePaths> {
257 match self {
258 IterInfo::Base { base } => SortedLoosePaths::at(base.join("refs"), base, None),
259 IterInfo::BaseAndIterRoot {
260 base,
261 iter_root,
262 prefix: _,
263 } => SortedLoosePaths::at(iter_root, base, None),
264 IterInfo::PrefixAndBase { base, prefix } => SortedLoosePaths::at(base.join(prefix), base, None),
265 IterInfo::ComputedIterationRoot {
266 iter_root,
267 base,
268 prefix: _,
269 remainder,
270 } => SortedLoosePaths::at(iter_root, base, remainder),
271 }
272 .peekable()
273 }
274
275 fn from_prefix(base: &'a Path, prefix: Cow<'a, Path>) -> std::io::Result<Self> {
276 if prefix.is_absolute() {
277 return Err(std::io::Error::new(
278 std::io::ErrorKind::InvalidInput,
279 "prefix must be a relative path, like 'refs/heads'",
280 ));
281 }
282 use std::path::Component::*;
283 if prefix.components().any(|c| matches!(c, CurDir | ParentDir)) {
284 return Err(std::io::Error::new(
285 std::io::ErrorKind::InvalidInput,
286 "Refusing to handle prefixes with relative path components",
287 ));
288 }
289 let iter_root = base.join(prefix.as_ref());
290 if iter_root.is_dir() {
291 Ok(IterInfo::BaseAndIterRoot {
292 base,
293 iter_root,
294 prefix,
295 })
296 } else {
297 let filename_prefix = iter_root
298 .file_name()
299 .map(ToOwned::to_owned)
300 .map(|p| {
301 git_path::try_into_bstr(PathBuf::from(p))
302 .map(|p| p.into_owned())
303 .map_err(|_| {
304 std::io::Error::new(std::io::ErrorKind::InvalidInput, "prefix contains ill-formed UTF-8")
305 })
306 })
307 .transpose()?;
308 let iter_root = iter_root
309 .parent()
310 .expect("a parent is always there unless empty")
311 .to_owned();
312 Ok(IterInfo::ComputedIterationRoot {
313 base,
314 prefix,
315 iter_root,
316 remainder: filename_prefix,
317 })
318 }
319 }
320}
321
322impl file::Store {
323 pub fn iter_packed<'s, 'p>(
327 &'s self,
328 packed: Option<&'p packed::Buffer>,
329 ) -> std::io::Result<LooseThenPacked<'p, 's>> {
330 match self.namespace.as_ref() {
331 Some(namespace) => self.iter_from_info(
332 IterInfo::PrefixAndBase {
333 base: self.git_dir(),
334 prefix: namespace.to_path(),
335 },
336 self.common_dir().map(|base| IterInfo::PrefixAndBase {
337 base,
338 prefix: namespace.to_path(),
339 }),
340 packed,
341 ),
342 None => self.iter_from_info(
343 IterInfo::Base { base: self.git_dir() },
344 self.common_dir().map(|base| IterInfo::Base { base }),
345 packed,
346 ),
347 }
348 }
349
350 pub fn iter_prefixed_packed<'s, 'p>(
354 &'s self,
355 prefix: impl AsRef<Path>,
356 packed: Option<&'p packed::Buffer>,
357 ) -> std::io::Result<LooseThenPacked<'p, 's>> {
358 match self.namespace.as_ref() {
359 None => {
360 let prefix = prefix.as_ref();
361 let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.into())?;
362 let common_dir_info = self
363 .common_dir()
364 .map(|base| IterInfo::from_prefix(base, prefix.into()))
365 .transpose()?;
366 self.iter_from_info(git_dir_info, common_dir_info, packed)
367 }
368 Some(namespace) => {
369 let prefix = namespace.to_owned().into_namespaced_prefix(prefix);
370 let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.clone().into())?;
371 let common_dir_info = self
372 .common_dir()
373 .map(|base| IterInfo::from_prefix(base, prefix.into()))
374 .transpose()?;
375 self.iter_from_info(git_dir_info, common_dir_info, packed)
376 }
377 }
378 }
379
380 fn iter_from_info<'s, 'p>(
381 &'s self,
382 git_dir_info: IterInfo<'_>,
383 common_dir_info: Option<IterInfo<'_>>,
384 packed: Option<&'p packed::Buffer>,
385 ) -> std::io::Result<LooseThenPacked<'p, 's>> {
386 Ok(LooseThenPacked {
387 git_dir: self.git_dir(),
388 common_dir: self.common_dir(),
389 iter_packed: match packed {
390 Some(packed) => Some(
391 match git_dir_info.prefix() {
392 Some(prefix) => packed.iter_prefixed(path_to_name(prefix).into_owned()),
393 None => packed.iter(),
394 }
395 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?
396 .peekable(),
397 ),
398 None => None,
399 },
400 iter_git_dir: git_dir_info.into_iter(),
401 iter_common_dir: common_dir_info.map(IterInfo::into_iter),
402 buf: Vec::new(),
403 namespace: self.namespace.as_ref(),
404 })
405 }
406}
407
408mod error {
409 use std::{io, path::PathBuf};
410
411 use git_object::bstr::BString;
412
413 use crate::store_impl::file;
414
415 #[derive(Debug, thiserror::Error)]
417 #[allow(missing_docs)]
418 pub enum Error {
419 #[error("The file system could not be traversed")]
420 Traversal(#[source] io::Error),
421 #[error("The ref file {path:?} could not be read in full")]
422 ReadFileContents { source: io::Error, path: PathBuf },
423 #[error("The reference at \"{relative_path}\" could not be instantiated")]
424 ReferenceCreation {
425 source: file::loose::reference::decode::Error,
426 relative_path: PathBuf,
427 },
428 #[error("Invalid reference in line {line_number}: {invalid_line:?}")]
429 PackedReference { invalid_line: BString, line_number: usize },
430 }
431}
432pub use error::Error;