git-ref 0.5.4

A crate to handle git references
Documentation
use std::{
    cmp::Ordering,
    io::Read,
    iter::Peekable,
    path::{Path, PathBuf},
};

use crate::{
    file::{loose, path_to_name, Reference},
    mutable::FullName,
    store::{file, packed},
};

/// An iterator stepping through sorted input of loose references and packed references, preferring loose refs over otherwise
/// equivalent packed references.
///
/// All errors will be returned verbatim, while packed errors are depleted first if loose refs also error.
pub struct LooseThenPacked<'p, 's> {
    base: &'s Path,
    packed: Option<Peekable<packed::Iter<'p>>>,
    loose: Peekable<loose::iter::SortedLoosePaths>,
    buf: Vec<u8>,
}

impl<'p, 's> LooseThenPacked<'p, 's> {
    fn convert_packed(
        &mut self,
        packed: Result<packed::Reference<'p>, packed::iter::Error>,
    ) -> Result<Reference<'p>, Error> {
        packed.map(Reference::Packed).map_err(|err| match err {
            packed::iter::Error::Reference {
                invalid_line,
                line_number,
            } => Error::PackedReference {
                invalid_line,
                line_number,
            },
            packed::iter::Error::Header { .. } => unreachable!("this one only happens on iteration creation"),
        })
    }

    fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result<Reference<'p>, Error> {
        let (refpath, name) = res.map_err(Error::Traversal)?;
        std::fs::File::open(&refpath)
            .and_then(|mut f| {
                self.buf.clear();
                f.read_to_end(&mut self.buf)
            })
            .map_err(Error::ReadFileContents)?;
        loose::Reference::try_from_path(name, &self.buf)
            .map_err(|err| Error::ReferenceCreation {
                err,
                relative_path: refpath.strip_prefix(&self.base).expect("base contains path").into(),
            })
            .map(Reference::Loose)
    }
}

impl<'p, 's> Iterator for LooseThenPacked<'p, 's> {
    type Item = Result<Reference<'p>, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.packed.as_mut() {
            Some(packed_iter) => match (self.loose.peek(), packed_iter.peek()) {
                (None, None) => None,
                (None, Some(_)) | (Some(_), Some(Err(_))) => {
                    let res = packed_iter.next().expect("peeked value exists");
                    Some(self.convert_packed(res))
                }
                (Some(_), None) | (Some(Err(_)), Some(_)) => {
                    let res = self.loose.next().expect("peeked value exists");
                    Some(self.convert_loose(res))
                }
                (Some(Ok(loose)), Some(Ok(packed))) => {
                    let loose_name = loose.1.as_bstr();
                    match loose_name.cmp(packed.name.as_bstr()) {
                        Ordering::Less => {
                            let res = self.loose.next().expect("name retrieval configured");
                            Some(self.convert_loose(res))
                        }
                        Ordering::Equal => {
                            drop(packed_iter.next());
                            let res = self.loose.next().expect("peeked value exists");
                            Some(self.convert_loose(res))
                        }
                        Ordering::Greater => {
                            let res = packed_iter.next().expect("name retrieval configured");
                            Some(self.convert_packed(res))
                        }
                    }
                }
            },
            None => self.loose.next().map(|res| self.convert_loose(res)),
        }
    }
}

impl file::Store {
    /// Return an iterator over all references, loose or `packed`, sorted by their name.
    ///
    /// Note that the caller is responsible for the freshness of the `packed` references buffer.
    /// If a reference cannot be parsed or read, the error will be visible to the caller and the iteration
    /// continues.
    ///
    /// Errors are returned similarly to what would happen when loose and packed refs where iterated by themeselves.
    pub fn iter<'p, 's>(&'s self, packed: Option<&'p packed::Buffer>) -> std::io::Result<LooseThenPacked<'p, 's>> {
        Ok(LooseThenPacked {
            base: &self.base,
            packed: match packed {
                Some(packed) => Some(
                    packed
                        .iter()
                        .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?
                        .peekable(),
                ),
                None => None,
            },
            loose: loose::iter::SortedLoosePaths::at_root_with_names(self.refs_dir(), self.base.to_owned()).peekable(),
            buf: Vec::new(),
        })
    }

    /// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads".
    ///
    /// Please note that "refs/heads` or "refs\\heads" is equivalent to "refs/heads/"
    pub fn iter_prefixed<'p, 's>(
        &'s self,
        packed: Option<&'p packed::Buffer>,
        prefix: impl AsRef<Path>,
    ) -> std::io::Result<LooseThenPacked<'p, 's>> {
        let packed_prefix = path_to_name(self.validate_prefix(prefix.as_ref())?);
        Ok(LooseThenPacked {
            base: &self.base,
            packed: match packed {
                Some(packed) => Some(
                    packed
                        .iter_prefixed(packed_prefix)
                        .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?
                        .peekable(),
                ),
                None => None,
            },
            loose: loose::iter::SortedLoosePaths::at_root_with_names(self.base.join(prefix), self.base.to_owned())
                .peekable(),
            buf: Vec::new(),
        })
    }
}

mod error {
    use std::{io, path::PathBuf};

    use bstr::BString;
    use quick_error::quick_error;

    use crate::store::file;

    quick_error! {
        /// The error returned by the [`LooseThenPacked`][super::LooseThenPacked] iterator.
        #[derive(Debug)]
        #[allow(missing_docs)]
        pub enum Error {
            Traversal(err: io::Error) {
                display("The file system could not be traversed")
                source(err)
            }
            ReadFileContents(err: io::Error) {
                display("The ref file could not be read in full")
                source(err)
            }
            ReferenceCreation{ err: file::loose::reference::decode::Error, relative_path: PathBuf } {
                display("The reference at '{}' could not be instantiated", relative_path.display())
                source(err)
            }
            PackedReference { invalid_line: BString, line_number: usize } {
                display("Invalid reference in line {}: '{}'", line_number, invalid_line)
            }
        }
    }
}

pub use error::Error;