//! 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, } /// Lifecycle impl<'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 { 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 { 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) } } /// Conversions impl 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( &mut self, src: R, rela_path: &std::path::Path, index: &gix_index::State, ) -> Result, 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, 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, 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::, pipeline::options::Error>>() }