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> LooseThenPacked<'p, '_> {
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(Iterator::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 Iterator for LooseThenPacked<'_, '_> {
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().is_some_and(|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 Platform<'_> {
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: &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> {
215 Ok(Platform {
216 store: self,
217 packed: self.assure_packed_refs_uptodate()?,
218 })
219 }
220}
221
222#[derive(Debug)]
223pub(crate) enum IterInfo<'a> {
224 Base {
225 base: &'a Path,
226 precompose_unicode: bool,
227 },
228 BaseAndIterRoot {
229 base: &'a Path,
230 iter_root: PathBuf,
231 prefix: Cow<'a, Path>,
232 precompose_unicode: bool,
233 },
234 PrefixAndBase {
235 base: &'a Path,
236 prefix: &'a Path,
237 precompose_unicode: bool,
238 },
239 ComputedIterationRoot {
240 iter_root: PathBuf,
242 base: &'a Path,
244 prefix: Cow<'a, Path>,
246 remainder: Option<BString>,
248 precompose_unicode: bool,
250 },
251}
252
253impl<'a> IterInfo<'a> {
254 fn prefix(&self) -> Option<&Path> {
255 match self {
256 IterInfo::Base { .. } => None,
257 IterInfo::PrefixAndBase { prefix, .. } => Some(*prefix),
258 IterInfo::ComputedIterationRoot { prefix, .. } | IterInfo::BaseAndIterRoot { prefix, .. } => {
259 prefix.as_ref().into()
260 }
261 }
262 }
263
264 fn into_iter(self) -> Peekable<SortedLoosePaths> {
265 match self {
266 IterInfo::Base {
267 base,
268 precompose_unicode,
269 } => SortedLoosePaths::at(&base.join("refs"), base.into(), None, precompose_unicode),
270 IterInfo::BaseAndIterRoot {
271 base,
272 iter_root,
273 prefix: _,
274 precompose_unicode,
275 } => SortedLoosePaths::at(&iter_root, base.into(), None, precompose_unicode),
276 IterInfo::PrefixAndBase {
277 base,
278 prefix,
279 precompose_unicode,
280 } => SortedLoosePaths::at(&base.join(prefix), base.into(), None, precompose_unicode),
281 IterInfo::ComputedIterationRoot {
282 iter_root,
283 base,
284 prefix: _,
285 remainder,
286 precompose_unicode,
287 } => SortedLoosePaths::at(&iter_root, base.into(), remainder, precompose_unicode),
288 }
289 .peekable()
290 }
291
292 fn from_prefix(base: &'a Path, prefix: Cow<'a, Path>, precompose_unicode: bool) -> std::io::Result<Self> {
293 if prefix.is_absolute() {
294 return Err(std::io::Error::new(
295 std::io::ErrorKind::InvalidInput,
296 "prefix must be a relative path, like 'refs/heads'",
297 ));
298 }
299 use std::path::Component::*;
300 if prefix.components().any(|c| matches!(c, CurDir | ParentDir)) {
301 return Err(std::io::Error::new(
302 std::io::ErrorKind::InvalidInput,
303 "Refusing to handle prefixes with relative path components",
304 ));
305 }
306 let iter_root = base.join(prefix.as_ref());
307 if iter_root.is_dir() {
308 Ok(IterInfo::BaseAndIterRoot {
309 base,
310 iter_root,
311 prefix,
312 precompose_unicode,
313 })
314 } else {
315 let filename_prefix = iter_root
316 .file_name()
317 .map(ToOwned::to_owned)
318 .map(|p| {
319 gix_path::try_into_bstr(PathBuf::from(p))
320 .map(std::borrow::Cow::into_owned)
321 .map_err(|_| {
322 std::io::Error::new(std::io::ErrorKind::InvalidInput, "prefix contains ill-formed UTF-8")
323 })
324 })
325 .transpose()?;
326 let iter_root = iter_root
327 .parent()
328 .expect("a parent is always there unless empty")
329 .to_owned();
330 Ok(IterInfo::ComputedIterationRoot {
331 base,
332 prefix,
333 iter_root,
334 remainder: filename_prefix,
335 precompose_unicode,
336 })
337 }
338 }
339}
340
341impl file::Store {
342 pub fn iter_packed<'s, 'p>(
346 &'s self,
347 packed: Option<&'p packed::Buffer>,
348 ) -> std::io::Result<LooseThenPacked<'p, 's>> {
349 match self.namespace.as_ref() {
350 Some(namespace) => self.iter_from_info(
351 IterInfo::PrefixAndBase {
352 base: self.git_dir(),
353 prefix: namespace.to_path(),
354 precompose_unicode: self.precompose_unicode,
355 },
356 self.common_dir().map(|base| IterInfo::PrefixAndBase {
357 base,
358 prefix: namespace.to_path(),
359 precompose_unicode: self.precompose_unicode,
360 }),
361 packed,
362 ),
363 None => self.iter_from_info(
364 IterInfo::Base {
365 base: self.git_dir(),
366 precompose_unicode: self.precompose_unicode,
367 },
368 self.common_dir().map(|base| IterInfo::Base {
369 base,
370 precompose_unicode: self.precompose_unicode,
371 }),
372 packed,
373 ),
374 }
375 }
376
377 pub fn iter_prefixed_packed<'s, 'p>(
381 &'s self,
382 prefix: &Path,
383 packed: Option<&'p packed::Buffer>,
384 ) -> std::io::Result<LooseThenPacked<'p, 's>> {
385 match self.namespace.as_ref() {
386 None => {
387 let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix.into(), self.precompose_unicode)?;
388 let common_dir_info = self
389 .common_dir()
390 .map(|base| IterInfo::from_prefix(base, prefix.into(), self.precompose_unicode))
391 .transpose()?;
392 self.iter_from_info(git_dir_info, common_dir_info, packed)
393 }
394 Some(namespace) => {
395 let prefix = namespace.to_owned().into_namespaced_prefix(prefix);
396 let git_dir_info =
397 IterInfo::from_prefix(self.git_dir(), prefix.clone().into(), self.precompose_unicode)?;
398 let common_dir_info = self
399 .common_dir()
400 .map(|base| IterInfo::from_prefix(base, prefix.into(), self.precompose_unicode))
401 .transpose()?;
402 self.iter_from_info(git_dir_info, common_dir_info, packed)
403 }
404 }
405 }
406
407 fn iter_from_info<'s, 'p>(
408 &'s self,
409 git_dir_info: IterInfo<'_>,
410 common_dir_info: Option<IterInfo<'_>>,
411 packed: Option<&'p packed::Buffer>,
412 ) -> std::io::Result<LooseThenPacked<'p, 's>> {
413 Ok(LooseThenPacked {
414 git_dir: self.git_dir(),
415 common_dir: self.common_dir(),
416 iter_packed: match packed {
417 Some(packed) => Some(
418 match git_dir_info.prefix() {
419 Some(prefix) => packed.iter_prefixed(path_to_name(prefix).into_owned()),
420 None => packed.iter(),
421 }
422 .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?
423 .peekable(),
424 ),
425 None => None,
426 },
427 iter_git_dir: git_dir_info.into_iter(),
428 iter_common_dir: common_dir_info.map(IterInfo::into_iter),
429 buf: Vec::new(),
430 namespace: self.namespace.as_ref(),
431 })
432 }
433}
434
435mod error {
436 use std::{io, path::PathBuf};
437
438 use gix_object::bstr::BString;
439
440 use crate::store_impl::file;
441
442 #[derive(Debug, thiserror::Error)]
444 #[allow(missing_docs)]
445 pub enum Error {
446 #[error("The file system could not be traversed")]
447 Traversal(#[source] io::Error),
448 #[error("The ref file {path:?} could not be read in full")]
449 ReadFileContents { source: io::Error, path: PathBuf },
450 #[error("The reference at \"{relative_path}\" could not be instantiated")]
451 ReferenceCreation {
452 source: file::loose::reference::decode::Error,
453 relative_path: PathBuf,
454 },
455 #[error("Invalid reference in line {line_number}: {invalid_line:?}")]
456 PackedReference { invalid_line: BString, line_number: usize },
457 }
458}
459pub use error::Error;