//! 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>>()}
//! 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<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)
}
}
/// 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<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>>()
}