318 lines · 13.4 KB · Rust
RawDownload
//! lower-level access to filters which are applied to create working tree checkouts or to 'clean' working tree contents for storage in git.pub use gix_filter as plumbing;use gix_object::Find; use crate::{    Repository,    bstr::BStr,    config::{        cache::util::{ApplyLeniency, ApplyLeniencyDefaultValue},        tree::Core,    },    prelude::ObjectIdExt,}; ///pub mod pipeline {    ///    pub mod options {        use crate::{bstr::BString, config};         /// The error returned by [Pipeline::options()](crate::filter::Pipeline::options()).        #[derive(Debug, thiserror::Error)]        #[expect(missing_docs)]        pub enum Error {            #[error(transparent)]            CheckRoundTripEncodings(#[from] config::encoding::Error),            #[error(transparent)]            SafeCrlf(#[from] config::key::GenericErrorWithValue),            #[error("Could not interpret 'filter.{name}.required' configuration")]            Driver {                name: BString,                source: gix_config::value::Error,            },            #[error(transparent)]            CommandContext(#[from] config::command_context::Error),        }    }     ///    pub mod convert_to_git {        /// The error returned by [Pipeline::convert_to_git()](crate::filter::Pipeline::convert_to_git()).        #[derive(Debug, thiserror::Error)]        #[expect(missing_docs)]        pub enum Error {            #[error("Failed to prime attributes to the path at which the data resides")]            WorktreeCacheAtPath(#[from] std::io::Error),            #[error(transparent)]            Convert(#[from] gix_filter::pipeline::convert::to_git::Error),        }    }     ///    pub mod convert_to_worktree {        /// The error returned by [Pipeline::convert_to_worktree()](crate::filter::Pipeline::convert_to_worktree()).        #[derive(Debug, thiserror::Error)]        #[expect(missing_docs)]        pub enum Error {            #[error("Failed to prime attributes to the path at which the data resides")]            WorktreeCacheAtPath(#[from] std::io::Error),            #[error(transparent)]            Convert(#[from] gix_filter::pipeline::convert::to_worktree::Error),        }    }     ///    pub mod worktree_file_to_object {        use std::path::PathBuf;         /// The error returned by [Pipeline::worktree_file_to_object()](crate::filter::Pipeline::worktree_file_to_object()).        #[derive(Debug, thiserror::Error)]        #[expect(missing_docs)]        pub enum Error {            #[error("Cannot add worktree files in bare repositories")]            MissingWorktree,            #[error("Failed to perform IO for object creation for '{}'", path.display())]            IO { source: std::io::Error, path: PathBuf },            #[error(transparent)]            WriteBlob(#[from] crate::object::write::Error),            #[error(transparent)]            ConvertToGit(#[from] crate::filter::pipeline::convert_to_git::Error),        }    }} /// A git pipeline for transforming data *to-git* and *to-worktree*, based/// [on git configuration and attributes](https://git-scm.com/docs/gitattributes).#[derive(Clone)]pub struct Pipeline<'repo> {    inner: gix_filter::Pipeline,    cache: gix_worktree::Stack,    /// The repository this pipeline is associated with.    pub repo: &'repo Repository,} /// Lifecycleimpl<'repo> Pipeline<'repo> {    /// Extract options from `repo` that are needed to properly drive a standard git filter pipeline.    pub fn options(repo: &'repo Repository) -> Result<gix_filter::pipeline::Options, pipeline::options::Error> {        let config = &repo.config.resolved;        let encodings =            Core::CHECK_ROUND_TRIP_ENCODING.try_into_encodings(config.string("core.checkRoundtripEncoding"))?;        let safe_crlf = config            .string("core.safecrlf")            .map(|value| Core::SAFE_CRLF.try_into_safecrlf(value))            .transpose()            .map(Option::unwrap_or_default)            .with_lenient_default_value(                repo.config.lenient_config,                // in lenient mode, we prefer the safe option, instead of just (trying) to output warnings.                gix_filter::pipeline::CrlfRoundTripCheck::Fail,            )?;        let auto_crlf = config            .string("core.autocrlf")            .map(|value| Core::AUTO_CRLF.try_into_autocrlf(value))            .transpose()            .with_leniency(repo.config.lenient_config)?            .unwrap_or_default();        let eol = config            .string("core.eol")            .map(|value| Core::EOL.try_into_eol(value))            .transpose()?;        let drivers = extract_drivers(repo)?;        Ok(gix_filter::pipeline::Options {            drivers,            eol_config: gix_filter::eol::Configuration { auto_crlf, eol },            encodings_with_roundtrip_check: encodings,            crlf_roundtrip_check: safe_crlf,            object_hash: repo.object_hash(),        })    }     /// Create a new instance by extracting all necessary information and configuration from a `repo` along with `cache` for accessing    /// attributes. The `index` is used for some filters which may access it under very specific circumstances.    pub fn new(repo: &'repo Repository, cache: gix_worktree::Stack) -> Result<Self, pipeline::options::Error> {        let pipeline = gix_filter::Pipeline::new(repo.command_context()?, Self::options(repo)?);        Ok(Pipeline {            inner: pipeline,            cache,            repo,        })    }     /// Detach the repository and obtain the individual functional parts.    pub fn into_parts(self) -> (gix_filter::Pipeline, gix_worktree::Stack) {        (self.inner, self.cache)    }} /// Conversionsimpl Pipeline<'_> {    /// Convert a `src` stream (to be found at `rela_path`, a repo-relative path) to a representation suitable for storage in `git`    /// by using all attributes at `rela_path` and configuration of the repository to know exactly which filters apply.    /// `index` is used in particularly rare cases where the CRLF filter in auto-mode tries to determine whether to apply itself,    /// and it should match the state used when [instantiating this instance](Self::new()).    /// Note that the return-type implements [`std::io::Read`].    pub fn convert_to_git<R>(        &mut self,        src: R,        rela_path: &std::path::Path,        index: &gix_index::State,    ) -> Result<gix_filter::pipeline::convert::ToGitOutcome<'_, R>, pipeline::convert_to_git::Error>    where        R: std::io::Read,    {        let entry = self.cache.at_path(rela_path, None, &self.repo.objects)?;        Ok(self.inner.convert_to_git(            src,            rela_path,            &mut |_, attrs| {                entry.matching_attributes(attrs);            },            &mut |buf| -> Result<_, gix_object::find::Error> {                let entry = match index                    .entry_by_path(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)).as_ref())                {                    None => return Ok(None),                    Some(entry) => entry,                };                let obj = self.repo.objects.try_find(&entry.id, buf)?;                Ok(obj.filter(|obj| obj.kind == gix_object::Kind::Blob).map(|_| ()))            },        )?)    }     /// Convert a `src` buffer located at `rela_path` (in the index) from what's in `git` to the worktree representation.    /// This method will obtain all attributes and configuration necessary to know exactly which filters to apply.    /// Note that the return-type implements [`std::io::Read`].    ///    /// Use `can_delay` to tell driver processes that they may delay the return of data. Doing this will require the caller to specifically    /// handle delayed files by keeping state and using [`Self::into_parts()`] to get access to the driver state to follow the delayed-files    /// protocol. For simplicity, most will want to disallow delayed processing.    pub fn convert_to_worktree<'input>(        &mut self,        src: &'input [u8],        rela_path: &BStr,        can_delay: gix_filter::driver::apply::Delay,    ) -> Result<gix_filter::pipeline::convert::ToWorktreeOutcome<'input, '_>, pipeline::convert_to_worktree::Error>    {        let entry = self.cache.at_entry(rela_path, None, &self.repo.objects)?;        Ok(self.inner.convert_to_worktree(            src,            rela_path,            &mut |_, attrs| {                entry.matching_attributes(attrs);            },            can_delay,        )?)    }     /// Add the worktree file at `rela_path` to the object database and return its `(id, entry, symlink_metadata)` for use in a tree or in the index, for instance.    /// If `rela_path` is a directory *and* is a repository with its `HEAD` pointing to a commit, it will also be provided with the appropriate kind.    /// Note that this can easily lead to embedded repositories as no submodule-crosscheck is performed. Otherwise, unreadable repositories or directories    /// are ignored with `None` as return value.    ///    /// `index` is used in particularly rare cases where the CRLF filter in auto-mode tries to determine whether to apply itself,    /// and it should match the state used when [instantiating this instance](Self::new()).    ///    /// Return `Ok(None)` the file didn't exist in the worktree, or if it was of an untrackable type.    pub fn worktree_file_to_object(        &mut self,        rela_path: &BStr,        index: &gix_index::State,    ) -> Result<        Option<(gix_hash::ObjectId, gix_object::tree::EntryKind, std::fs::Metadata)>,        pipeline::worktree_file_to_object::Error,    > {        use pipeline::worktree_file_to_object::Error;         let rela_path_as_path = gix_path::from_bstr(rela_path);        let repo = self.repo;        let worktree_dir = repo.workdir().ok_or(Error::MissingWorktree)?;        let path = worktree_dir.join(&rela_path_as_path);        let md = match std::fs::symlink_metadata(&path) {            Ok(md) => md,            Err(err) => {                if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) {                    return Ok(None);                } else {                    return Err(Error::IO { source: err, path });                }            }        };        let (id, kind) = if md.is_symlink() {            let target = std::fs::read_link(&path).map_err(|source| Error::IO { source, path })?;            let id = repo.write_blob(gix_path::into_bstr(target).as_ref())?;            (id, gix_object::tree::EntryKind::Link)        } else if md.is_file() {            use gix_filter::pipeline::convert::ToGitOutcome;             let file = std::fs::File::open(&path).map_err(|source| Error::IO { source, path })?;            let file_for_git = self.convert_to_git(file, rela_path_as_path.as_ref(), index)?;            let id = match file_for_git {                ToGitOutcome::Unchanged(mut file) => repo.write_blob_stream(&mut file)?,                ToGitOutcome::Buffer(buf) => repo.write_blob(buf)?,                ToGitOutcome::Process(mut read) => repo.write_blob_stream(&mut read)?,            };             let kind = if gix_fs::is_executable(&md) {                gix_object::tree::EntryKind::BlobExecutable            } else {                gix_object::tree::EntryKind::Blob            };            (id, kind)        } else if md.is_dir() {            let Some(submodule_repo) = crate::open_opts(&path, repo.open_options().clone()).ok() else {                return Ok(None);            };            let Some(id) = submodule_repo.head_id().ok() else {                return Ok(None);            };            (id.detach().attach(repo), gix_object::tree::EntryKind::Commit)        } else {            // This is probably a type-change to something we can't track.            return Ok(None);        };         Ok(Some((id.detach(), kind, md)))    }     /// Retrieve the static context that is made available to the process filters.    ///    /// The context set here is relevant for the [`convert_to_git()`][Self::convert_to_git()] and    /// [`convert_to_worktree()`][Self::convert_to_worktree()] methods.    pub fn driver_context_mut(&mut self) -> &mut gix_filter::pipeline::Context {        self.inner.driver_context_mut()    }} /// Obtain a list of all configured driver, but ignore those in sections that we don't trust enough.fn extract_drivers(repo: &Repository) -> Result<Vec<gix_filter::Driver>, pipeline::options::Error> {    repo.config        .resolved        .sections_by_name("filter")        .into_iter()        .flatten()        .filter(|s| repo.filter_config_section()(s.meta()))        .filter_map(|s| {            s.header().subsection_name().map(|name| {                Ok(gix_filter::Driver {                    name: name.to_owned(),                    clean: s.value("clean"),                    smudge: s.value("smudge"),                    process: s.value("process"),                    required: s                        .value("required")                        .map(|value| gix_config::Boolean::try_from(BStr::new(&value)))                        .transpose()                        .map_err(|err| pipeline::options::Error::Driver {                            name: name.to_owned(),                            source: err,                        })?                        .unwrap_or_default()                        .into(),                })            })        })        .collect::<Result<Vec<_>, pipeline::options::Error>>()}