use crate::Config;
use crate::config::InvalidValue;
mod trailers;
pub use trailers::{
IfExists, IfMissing, Placement, Trailer, TrailerCommandError, TrailerConfig, TrailerKey,
add_trailers, trailers,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Cleanup {
Verbatim,
Whitespace,
Strip,
Scissors,
}
impl Cleanup {
pub fn for_commit(config: &Config, edited: bool) -> Result<Self, InvalidValue> {
let edited_mode = |mode| if edited { mode } else { Self::Whitespace };
match config.value("commit", None, "cleanup") {
None | Some(Some(b"default")) => Ok(edited_mode(Self::Strip)),
Some(Some(b"verbatim")) => Ok(Self::Verbatim),
Some(Some(b"whitespace")) => Ok(Self::Whitespace),
Some(Some(b"strip")) => Ok(Self::Strip),
Some(Some(b"scissors")) => Ok(edited_mode(Self::Scissors)),
Some(value) => Err(InvalidValue::new("commit", "cleanup", value)),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CommentPrefix(Vec<u8>);
const AUTO_CANDIDATES: &[u8] = b"#;@!$%^&|:";
impl CommentPrefix {
pub fn new(prefix: &[u8]) -> Option<Self> {
(!prefix.is_empty() && !prefix.contains(&b'\n')).then(|| Self(prefix.to_vec()))
}
pub fn auto(message: &[u8]) -> Option<Self> {
let mut used = [false; 256];
for line in message.split(|&byte| byte == b'\n') {
if let Some(&first) = line.first() {
used[usize::from(first)] = true;
}
}
AUTO_CANDIDATES
.iter()
.find(|&&candidate| !used[usize::from(candidate)])
.map(|&chosen| Self(vec![chosen]))
}
pub fn from_config(config: &Config) -> Result<Option<Self>, InvalidValue> {
let mut prefix = Some(Self::default());
for entry in config.entries().iter().filter(|entry| {
entry.section.eq_ignore_ascii_case(b"core")
&& entry.subsection.is_none()
&& (entry.name.eq_ignore_ascii_case(b"commentchar")
|| entry.name.eq_ignore_ascii_case(b"commentstring"))
}) {
let name = String::from_utf8_lossy(&entry.name).to_ascii_lowercase();
prefix = match entry.value.as_deref() {
Some(b"auto") => None,
Some(value) => Some(
Self::new(value)
.ok_or_else(|| InvalidValue::new("core", &name, Some(value)))?,
),
None => return Err(InvalidValue::new("core", &name, None)),
};
}
Ok(prefix)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
fn starts(&self, line: &[u8]) -> bool {
line.starts_with(&self.0)
}
}
impl Default for CommentPrefix {
fn default() -> Self {
Self(b"#".to_vec())
}
}
const SCISSORS: &[u8] = b"------------------------ >8 ------------------------";
pub fn cleanup(message: &[u8], mode: Cleanup, comment: &CommentPrefix) -> Vec<u8> {
if mode == Cleanup::Verbatim {
return message.to_vec();
}
let mut output = Vec::with_capacity(message.len() + 1);
let mut pending_blank = false;
for line in message.split(|&byte| byte == b'\n') {
if mode == Cleanup::Scissors && is_scissors(line, comment) {
break;
}
if mode == Cleanup::Strip && comment.starts(line) {
continue;
}
let content = trim_end(line);
if content.is_empty() {
pending_blank = !output.is_empty();
continue;
}
if pending_blank {
output.push(b'\n');
pending_blank = false;
}
output.extend_from_slice(content);
output.push(b'\n');
}
output
}
fn is_scissors(line: &[u8], comment: &CommentPrefix) -> bool {
line.strip_prefix(comment.as_bytes())
.and_then(|rest| rest.strip_prefix(b" "))
.is_some_and(|rest| trim_end(rest) == SCISSORS)
}
fn trim_end(line: &[u8]) -> &[u8] {
let end = line
.iter()
.rposition(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
.map_or(0, |last| last + 1);
&line[..end]
}
#[cfg(test)]
mod tests;