use std::borrow::Cow;
use std::collections::HashMap;
use crate::attributes::{Attributes, LoadError, Lookup, State};
use crate::config::{Config, InvalidValue};
use crate::workflow::Environment;
use crate::{ObjectId, Objects, ReadBlobError, Repository};
mod driver;
mod encoding;
mod eol;
mod ident;
mod process;
pub use driver::{Commands, Direction, DriverError, Drivers, Limits};
use eol::Stats;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum AutoCrlf {
#[default]
False,
True,
Input,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum LineEnding {
Lf,
Crlf,
}
impl LineEnding {
pub const NATIVE: Self = if cfg!(windows) { Self::Crlf } else { Self::Lf };
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum SafeCrlf {
Allow,
#[default]
Warn,
Refuse,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Settings {
autocrlf: AutoCrlf,
eol: LineEnding,
safecrlf: SafeCrlf,
}
impl Default for Settings {
fn default() -> Self {
Self {
autocrlf: AutoCrlf::False,
eol: LineEnding::NATIVE,
safecrlf: SafeCrlf::Warn,
}
}
}
impl Settings {
pub fn from_config(config: &Config) -> Result<Self, InvalidValue> {
let mut settings = Self::default();
if let Some(value) = config.value("core", None, "autocrlf") {
settings.autocrlf = if value.is_some_and(|value| value.eq_ignore_ascii_case(b"input")) {
AutoCrlf::Input
} else {
match crate::config::boolean(value) {
Some(true) => AutoCrlf::True,
Some(false) => AutoCrlf::False,
None => return Err(InvalidValue::new("core", "autocrlf", value)),
}
};
}
if let Some(value) = config.value("core", None, "eol") {
settings.eol = match value.map(<[u8]>::to_ascii_lowercase).as_deref() {
Some(b"lf") => LineEnding::Lf,
Some(b"crlf") => LineEnding::Crlf,
Some(b"native") => LineEnding::NATIVE,
_ => return Err(InvalidValue::new("core", "eol", value)),
};
}
if let Some(value) = config.value("core", None, "safecrlf") {
settings.safecrlf = if value.is_some_and(|value| value.eq_ignore_ascii_case(b"warn")) {
SafeCrlf::Warn
} else {
match crate::config::boolean(value) {
Some(true) => SafeCrlf::Refuse,
Some(false) => SafeCrlf::Allow,
None => return Err(InvalidValue::new("core", "safecrlf", value)),
}
};
}
Ok(settings)
}
pub fn autocrlf(&self) -> AutoCrlf {
self.autocrlf
}
pub fn eol(&self) -> LineEnding {
self.eol
}
pub fn safecrlf(&self) -> SafeCrlf {
self.safecrlf
}
#[must_use]
pub fn with_autocrlf(mut self, autocrlf: AutoCrlf) -> Self {
self.autocrlf = autocrlf;
self
}
#[must_use]
pub fn with_eol(mut self, eol: LineEnding) -> Self {
self.eol = eol;
self
}
#[must_use]
pub fn with_safecrlf(mut self, safecrlf: SafeCrlf) -> Self {
self.safecrlf = safecrlf;
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Text {
Binary,
Auto,
Always,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Plan {
text: Text,
eol: LineEnding,
ident: bool,
safecrlf: SafeCrlf,
driver: Option<Vec<u8>>,
encoding: Option<(Vec<u8>, encoding::Encoding)>,
}
impl Plan {
pub fn text(&self) -> Text {
self.text
}
pub fn eol(&self) -> LineEnding {
self.eol
}
pub fn encoding(&self) -> Option<&[u8]> {
self.encoding.as_ref().map(|(name, _)| name.as_slice())
}
pub fn ident(&self) -> bool {
self.ident
}
pub fn driver(&self) -> Option<&[u8]> {
self.driver.as_deref()
}
fn builtin_to_worktree<'a>(&self, bytes: Cow<'a, [u8]>, id: ObjectId) -> Cow<'a, [u8]> {
let bytes = if self.ident {
ident::expand(bytes, id)
} else {
bytes
};
self.eol_to_worktree(bytes)
}
fn eol_to_worktree<'a>(&self, bytes: Cow<'a, [u8]>) -> Cow<'a, [u8]> {
if self.eol != LineEnding::Crlf {
return bytes;
}
let needed = match self.text {
Text::Binary => false,
Text::Always => Stats::new(&bytes).lone_lf > 0,
Text::Auto => {
let stats = Stats::new(&bytes);
stats.lone_lf > 0 && !stats.has_cr() && !stats.binary()
}
};
eol::convert_if(bytes, needed, eol::lf_to_crlf)
}
fn decode<'a>(&self, path: &[u8], bytes: Cow<'a, [u8]>) -> Result<Cow<'a, [u8]>, Error> {
let Some((name, encoding)) = &self.encoding else {
return Ok(bytes);
};
encoding
.decode(&bytes)
.map(Cow::Owned)
.map_err(|failure| Error::Encoding {
path: path.to_vec(),
encoding: name.clone(),
reason: match failure {
encoding::Failure::BomRequired => "BOM is required",
encoding::Failure::BomProhibited => "BOM is prohibited",
encoding::Failure::Invalid => "invalid content",
},
})
}
fn builtin_to_repository<'a>(
&self,
path: &[u8],
file: Cow<'a, [u8]>,
staged: Staged<'_>,
) -> Result<(Cow<'a, [u8]>, Option<CrlfWarning>), Error> {
let stats = Stats::new(&file);
let convert = stats.crlf > 0
&& match self.text {
Text::Binary => false,
Text::Always => true,
Text::Auto => !stats.binary() && !staged.has_crlf()?,
};
let converted = convert.then(|| eol::crlf_to_lf(&file));
let warning = if self.text == Text::Binary {
None
} else {
let stored = converted.as_deref().unwrap_or(&file);
let written = self.eol_to_worktree(Cow::Borrowed(stored));
(*written != *file).then(|| {
if stats.crlf > 0 && self.eol == LineEnding::Lf {
CrlfWarning::CrlfToLf
} else {
CrlfWarning::LfToCrlf
}
})
};
let safecrlf = if matches!(staged, Staged::Renormalize) {
SafeCrlf::Allow
} else {
self.safecrlf
};
let warning = match (warning, safecrlf) {
(Some(change), SafeCrlf::Refuse) => {
return Err(Error::UnsafeCrlf {
path: path.to_vec(),
change,
});
}
(warning, SafeCrlf::Warn) => warning,
_ => None,
};
let bytes = converted.map_or(file, Cow::Owned);
let bytes = if self.ident {
ident::collapse(bytes)
} else {
bytes
};
Ok((bytes, warning))
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum Staged<'a> {
Absent,
Blob(&'a [u8]),
Object(&'a Objects, ObjectId),
Renormalize,
}
impl Staged<'_> {
fn has_crlf(self) -> Result<bool, Error> {
let crlf = |bytes: &[u8]| bytes.windows(2).any(|pair| pair == b"\r\n");
Ok(match self {
Self::Absent | Self::Renormalize => false,
Self::Blob(bytes) => crlf(bytes),
Self::Object(objects, id) => crlf(
&objects
.read_blob(id)
.map_err(|source| Error::Staged(Box::new(source)))?,
),
})
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Converted<'a> {
pub bytes: Cow<'a, [u8]>,
pub warning: Option<CrlfWarning>,
pub skipped: Option<DriverError>,
pub unencoded: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CrlfWarning {
CrlfToLf,
LfToCrlf,
}
impl std::fmt::Display for CrlfWarning {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::CrlfToLf => "CRLF would be replaced by LF",
Self::LfToCrlf => "LF would be replaced by CRLF",
})
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Attributes(#[from] LoadError),
#[error("{change} in {}", String::from_utf8_lossy(path))]
UnsafeCrlf {
path: Vec<u8>,
change: CrlfWarning,
},
#[error(
"{direction} filter {:?} for {} needs commands to run",
String::from_utf8_lossy(driver),
String::from_utf8_lossy(path)
)]
Refused {
path: Vec<u8>,
driver: Vec<u8>,
direction: Direction,
},
#[error(
"{} filter failed for {}",
source_direction(source),
String::from_utf8_lossy(path)
)]
Driver {
path: Vec<u8>,
#[source]
source: DriverError,
},
#[error("reading the staged blob")]
Staged(#[source] Box<ReadBlobError>),
#[error(
"{} in {} as {}",
reason,
String::from_utf8_lossy(path),
String::from_utf8_lossy(encoding)
)]
Encoding {
path: Vec<u8>,
encoding: Vec<u8>,
reason: &'static str,
},
#[error(
"unsupported conversion for {}: {attribute}",
String::from_utf8_lossy(path)
)]
Unsupported {
path: Vec<u8>,
attribute: &'static str,
},
}
enum Rules {
Lookup(Lookup),
Attributes(Attributes),
}
fn source_direction(error: &DriverError) -> Direction {
match error {
DriverError::Io { direction, .. }
| DriverError::Failed { direction, .. }
| DriverError::TooLarge { direction, .. }
| DriverError::Rejected { direction, .. }
| DriverError::Aborted { direction, .. }
| DriverError::Missing { direction, .. } => *direction,
}
}
pub struct Pipeline {
rules: Rules,
settings: Settings,
drivers: Drivers,
processes: HashMap<Vec<u8>, process::Process>,
}
impl std::fmt::Debug for Pipeline {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Pipeline")
.field("settings", &self.settings)
.field("drivers", &self.drivers)
.finish_non_exhaustive()
}
}
impl Pipeline {
pub fn new(lookup: Lookup, settings: Settings, drivers: Drivers) -> Self {
Self {
rules: Rules::Lookup(lookup),
settings,
drivers,
processes: HashMap::new(),
}
}
pub fn from_attributes(attributes: Attributes, settings: Settings, drivers: Drivers) -> Self {
Self {
rules: Rules::Attributes(attributes),
settings,
drivers,
processes: HashMap::new(),
}
}
pub fn settings(&self) -> Settings {
self.settings
}
#[must_use]
pub fn with_settings(mut self, settings: Settings) -> Self {
self.settings = settings;
self
}
fn get(&mut self, path: &[u8], name: &[u8]) -> Result<State<'_>, Error> {
match &mut self.rules {
Rules::Lookup(lookup) => Ok(lookup.get(path, name)?),
Rules::Attributes(attributes) => attributes
.get(path, name)
.map_err(|source| LoadError::Rules {
path: path.to_vec(),
source,
})
.map_err(Error::from),
}
}
pub fn plan(&mut self, path: &[u8]) -> Result<Plan, Error> {
let encoding = match self.get(path, b"working-tree-encoding")? {
State::Value(name) => {
let name = name.to_vec();
match encoding::Encoding::parse(&name) {
Ok(Some(encoding)) => Some((name, encoding)),
Ok(None) => None,
Err(()) => {
return Err(Error::Unsupported {
path: path.to_vec(),
attribute: "working-tree-encoding",
});
}
}
}
_ => None,
};
let driver = match self.get(path, b"filter")? {
State::Value(name) => Some(name.to_vec()),
_ => None,
};
let eol = match self.get(path, b"eol")? {
State::Value(b"lf") => Some(LineEnding::Lf),
State::Value(b"crlf") => Some(LineEnding::Crlf),
_ => None,
};
let autocrlf = self.settings.autocrlf;
let text = match self.get(path, b"text")? {
State::Set => Text::Always,
State::Unset => Text::Binary,
State::Value(b"auto") => Text::Auto,
_ if eol.is_some() => Text::Always,
_ if autocrlf == AutoCrlf::False => Text::Binary,
_ => Text::Auto,
};
let ident = self.get(path, b"ident")? == State::Set;
let settings = self.settings;
Ok(Plan {
text,
eol: eol.unwrap_or(match settings.autocrlf {
AutoCrlf::True => LineEnding::Crlf,
AutoCrlf::Input => LineEnding::Lf,
AutoCrlf::False => settings.eol,
}),
ident,
safecrlf: settings.safecrlf,
driver,
encoding,
})
}
pub fn to_worktree<'a>(
&mut self,
path: &[u8],
blob: &'a [u8],
id: ObjectId,
) -> Result<Converted<'a>, Error> {
let plan = self.plan(path)?;
let bytes = plan.builtin_to_worktree(Cow::Borrowed(blob), id);
let (bytes, unencoded) = match &plan.encoding {
Some((_, encoding)) => match encoding.encode(&bytes) {
Ok(encoded) => (Cow::Owned(encoded), false),
Err(_) => (bytes, true),
},
None => (bytes, false),
};
let (bytes, skipped) = self.drive(&plan, path, Direction::Smudge, Some(id), bytes)?;
Ok(Converted {
bytes,
warning: None,
skipped,
unencoded,
})
}
pub fn to_repository<'a>(
&mut self,
path: &[u8],
file: &'a [u8],
staged: Staged<'_>,
) -> Result<Converted<'a>, Error> {
let plan = self.plan(path)?;
let (bytes, skipped) =
self.drive(&plan, path, Direction::Clean, None, Cow::Borrowed(file))?;
let bytes = plan.decode(path, bytes)?;
let (bytes, warning) = plan.builtin_to_repository(path, bytes, staged)?;
Ok(Converted {
bytes,
warning,
skipped,
unencoded: false,
})
}
fn drive<'a>(
&mut self,
plan: &Plan,
path: &[u8],
direction: Direction,
blob: Option<ObjectId>,
bytes: Cow<'a, [u8]>,
) -> Result<(Cow<'a, [u8]>, Option<DriverError>), Error> {
let Some(name) = plan.driver() else {
return Ok((bytes, None));
};
let Some(driver) = self.drivers.get(name) else {
return Ok((bytes, None));
};
let required = driver.required;
let failure = |source| Error::Driver {
path: path.to_vec(),
source,
};
let process = driver.process.clone();
let command = driver.command(direction).map(<[u8]>::to_vec);
if process.is_none() && command.is_none() {
if required {
return Err(failure(DriverError::Missing {
driver: name.to_vec(),
direction,
}));
}
return Ok((bytes, None));
}
if self.drivers.commands == Commands::Refuse {
return Err(Error::Refused {
path: path.to_vec(),
driver: name.to_vec(),
direction,
});
}
let result = match (process, command) {
(Some(process), _) => self.request(name, &process, direction, path, blob, &bytes),
(None, Some(command)) => {
driver::run(name, direction, &command, path, &bytes, &self.drivers).map(Some)
}
(None, None) => Ok(None),
};
match result {
Ok(Some(output)) => Ok((Cow::Owned(output), None)),
Ok(None) if required => Err(failure(DriverError::Missing {
driver: name.to_vec(),
direction,
})),
Ok(None) => Ok((bytes, None)),
Err(error) if required => Err(failure(error)),
Err(error) => Ok((bytes, Some(error))),
}
}
fn request(
&mut self,
name: &[u8],
command: &[u8],
direction: Direction,
path: &[u8],
blob: Option<ObjectId>,
content: &[u8],
) -> Result<Option<Vec<u8>>, DriverError> {
let io = |source| DriverError::Io {
driver: name.to_vec(),
direction,
source,
};
if !self.processes.contains_key(name) {
let process = process::Process::start(command, &self.drivers).map_err(io)?;
self.processes.insert(name.to_vec(), process);
}
let Some(process) = self.processes.get_mut(name) else {
return Err(io(std::io::Error::other("filter process missing")));
};
if !process.supports(direction) {
return Ok(None);
}
let limit = self.drivers.limits.max_output_bytes();
match process.request(direction, path, blob, content, limit) {
Ok(process::Reply::Converted(output)) => Ok(Some(output)),
Ok(process::Reply::Error) => Err(DriverError::Rejected {
driver: name.to_vec(),
direction,
}),
Ok(process::Reply::Abort) => {
process.disable(direction);
Err(DriverError::Aborted {
driver: name.to_vec(),
direction,
})
}
Err(error) => {
self.processes.remove(name);
Err(error.into_driver_error(name, direction))
}
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl Pipeline {
pub(crate) fn stored_for_status<'a>(
&mut self,
path: &[u8],
file: &'a [u8],
staged: Staged<'_>,
) -> Result<Cow<'a, [u8]>, Error> {
let mut plan = self.plan(path)?;
plan.safecrlf = SafeCrlf::Allow;
let (bytes, _) = self.drive(&plan, path, Direction::Clean, None, Cow::Borrowed(file))?;
let bytes = plan.decode(path, bytes)?;
Ok(plan.builtin_to_repository(path, bytes, staged)?.0)
}
}
impl Repository {
pub fn filters(
&self,
environment: &Environment,
commands: Commands,
) -> Result<Pipeline, LoadError> {
let source = self.attribute_source(environment)?;
self.filters_with_source(environment, source, commands)
}
pub fn filters_for_checkout(
&self,
environment: &Environment,
target: ObjectId,
commands: Commands,
) -> Result<Pipeline, LoadError> {
let source = match self.attribute_source(environment)? {
crate::attributes::DirectorySource::Worktree => {
crate::attributes::DirectorySource::Tree(target)
}
chosen => chosen,
};
self.filters_with_source(environment, source, commands)
}
pub fn filters_with_source(
&self,
environment: &Environment,
source: crate::attributes::DirectorySource,
commands: Commands,
) -> Result<Pipeline, LoadError> {
let lookup = self.attributes(environment, source)?;
let mut drivers = Drivers::from_config(self.config())?.with_commands(commands);
if let Some(worktree) = self.worktree() {
drivers = drivers.with_directory(worktree);
}
Ok(Pipeline::new(
lookup,
Settings::from_config(self.config())?,
drivers,
))
}
}