use super::{CommentPrefix, is_scissors};
use crate::Config;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Trailer {
pub key: Vec<u8>,
pub value: Vec<u8>,
}
impl Trailer {
pub fn parse_argument(argument: &[u8], config: &TrailerConfig) -> Option<Self> {
let split = argument
.iter()
.position(|byte| *byte == b'=' || config.separators.contains(byte));
let (key, value) = match split {
Some(at) => (&argument[..at], &argument[at + 1..]),
None => (argument, &b""[..]),
};
let key = trim(key);
(!key.is_empty()).then(|| Self {
key: key.to_vec(),
value: trim(value).to_vec(),
})
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Placement {
#[default]
End,
Start,
After,
Before,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum IfExists {
#[default]
AddIfDifferentNeighbor,
AddIfDifferent,
Add,
Replace,
DoNothing,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum IfMissing {
#[default]
Add,
DoNothing,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
struct Rules {
placement: Placement,
if_exists: IfExists,
if_missing: IfMissing,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TrailerKey {
name: Vec<u8>,
key: Option<Vec<u8>>,
placement: Option<Placement>,
if_exists: Option<IfExists>,
if_missing: Option<IfMissing>,
}
impl TrailerKey {
pub fn new(name: &[u8]) -> Self {
Self {
name: name.to_vec(),
key: None,
placement: None,
if_exists: None,
if_missing: None,
}
}
pub fn with_key(mut self, key: &[u8]) -> Self {
self.key = Some(key.to_vec());
self
}
pub fn with_placement(mut self, placement: Placement) -> Self {
self.placement = Some(placement);
self
}
pub fn with_if_exists(mut self, if_exists: IfExists) -> Self {
self.if_exists = Some(if_exists);
self
}
pub fn with_if_missing(mut self, if_missing: IfMissing) -> Self {
self.if_missing = Some(if_missing);
self
}
fn matches(&self, key: &[u8]) -> bool {
let end = key
.iter()
.rposition(|byte| *byte != b'-')
.map_or(0, |last| last + 1);
let key = &key[..end];
is_prefix(key, &self.name) || self.key.as_deref().is_some_and(|full| is_prefix(key, full))
}
}
#[derive(Clone, Debug)]
pub struct TrailerConfig {
separators: Vec<u8>,
rules: Rules,
keys: Vec<TrailerKey>,
comment: CommentPrefix,
divider: bool,
}
impl Default for TrailerConfig {
fn default() -> Self {
Self {
separators: b":".to_vec(),
rules: Rules::default(),
keys: Vec::new(),
comment: CommentPrefix::default(),
divider: true,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[error("trailer.{name}.{variable} runs a command, which girt does not support")]
#[non_exhaustive]
pub struct TrailerCommandError {
pub name: String,
pub variable: String,
}
impl TrailerConfig {
pub fn from_config(config: &Config) -> Result<Self, TrailerCommandError> {
let mut result = Self::default();
if let Ok(Some(comment)) = CommentPrefix::from_config(config) {
result.comment = comment;
}
for entry in config.entries() {
if !entry.section.eq_ignore_ascii_case(b"trailer") {
continue;
}
let name = String::from_utf8_lossy(&entry.name).to_ascii_lowercase();
let value = entry.value.as_deref();
let Some(subsection) = &entry.subsection else {
match (name.as_str(), value) {
("separators", Some(value)) if !value.is_empty() => {
result.separators = value.to_vec();
}
("where", Some(value)) => {
if let Some(placement) = placement(value) {
result.rules.placement = placement;
}
}
("ifexists", Some(value)) => {
if let Some(if_exists) = if_exists(value) {
result.rules.if_exists = if_exists;
}
}
("ifmissing", Some(value)) => {
if let Some(if_missing) = if_missing(value) {
result.rules.if_missing = if_missing;
}
}
_ => {}
}
continue;
};
if name == "command" || name == "cmd" {
return Err(TrailerCommandError {
name: String::from_utf8_lossy(subsection).into_owned(),
variable: name,
});
}
let index = match result.keys.iter().position(|key| key.name == *subsection) {
Some(index) => index,
None => {
result.keys.push(TrailerKey::new(subsection));
result.keys.len() - 1
}
};
let rules = &mut result.keys[index];
let Some(value) = value else { continue };
match name.as_str() {
"key" => rules.key = Some(value.to_vec()),
"where" => rules.placement = placement(value).or(rules.placement),
"ifexists" => rules.if_exists = if_exists(value).or(rules.if_exists),
"ifmissing" => rules.if_missing = if_missing(value).or(rules.if_missing),
_ => {}
}
}
Ok(result)
}
pub fn with_placement(mut self, placement: Placement) -> Self {
self.rules.placement = placement;
self
}
pub fn with_if_exists(mut self, if_exists: IfExists) -> Self {
self.rules.if_exists = if_exists;
self
}
pub fn with_if_missing(mut self, if_missing: IfMissing) -> Self {
self.rules.if_missing = if_missing;
self
}
pub fn with_separators(mut self, separators: &[u8]) -> Option<Self> {
if separators.is_empty() {
return None;
}
self.separators = separators.to_vec();
Some(self)
}
pub fn with_key(mut self, key: TrailerKey) -> Self {
match self
.keys
.iter_mut()
.find(|existing| existing.name == key.name)
{
Some(existing) => *existing = key,
None => self.keys.push(key),
}
self
}
pub fn with_divider(mut self, divider: bool) -> Self {
self.divider = divider;
self
}
pub fn with_comment_prefix(mut self, comment: CommentPrefix) -> Self {
self.comment = comment;
self
}
fn section(&self, key: &[u8]) -> Option<&TrailerKey> {
self.keys.iter().find(|rules| rules.matches(key))
}
fn resolve(&self, key: &[u8]) -> (Vec<u8>, Rules) {
let Some(section) = self.section(key) else {
return (key.to_vec(), self.rules);
};
let rules = Rules {
placement: section.placement.unwrap_or(self.rules.placement),
if_exists: section.if_exists.unwrap_or(self.rules.if_exists),
if_missing: section.if_missing.unwrap_or(self.rules.if_missing),
};
(section.key.clone().unwrap_or_else(|| key.to_vec()), rules)
}
fn format(&self, key: &[u8], value: &[u8], output: &mut Vec<u8>) {
output.extend_from_slice(key);
if !key
.trim_ascii_end()
.last()
.is_some_and(|last| self.separators.contains(last))
{
output.push(self.separators[0]);
output.push(b' ');
}
output.extend_from_slice(value);
}
}
fn placement(value: &[u8]) -> Option<Placement> {
Some(match value.to_ascii_lowercase().as_slice() {
b"end" => Placement::End,
b"start" => Placement::Start,
b"after" => Placement::After,
b"before" => Placement::Before,
_ => return None,
})
}
fn if_exists(value: &[u8]) -> Option<IfExists> {
Some(match value.to_ascii_lowercase().as_slice() {
b"addifdifferentneighbor" => IfExists::AddIfDifferentNeighbor,
b"addifdifferent" => IfExists::AddIfDifferent,
b"add" => IfExists::Add,
b"replace" => IfExists::Replace,
b"donothing" => IfExists::DoNothing,
_ => return None,
})
}
fn if_missing(value: &[u8]) -> Option<IfMissing> {
Some(match value.to_ascii_lowercase().as_slice() {
b"add" => IfMissing::Add,
b"donothing" => IfMissing::DoNothing,
_ => return None,
})
}
#[derive(Clone, Debug)]
enum Line<'m> {
Trailer {
key: Vec<u8>,
value: Vec<u8>,
continuation: Vec<&'m [u8]>,
},
Other(&'m [u8]),
}
struct Block<'m> {
before: &'m [u8],
after: &'m [u8],
lines: Option<Vec<Line<'m>>>,
}
fn lines(message: &[u8]) -> Vec<(usize, &[u8])> {
let mut result = Vec::new();
let mut start = 0;
while start < message.len() {
let end = message[start..]
.iter()
.position(|byte| *byte == b'\n')
.map_or(message.len(), |at| start + at + 1);
result.push((start, &message[start..end]));
start = end;
}
result
}
fn content(line: &[u8]) -> &[u8] {
line.strip_suffix(b"\n").unwrap_or(line)
}
fn is_blank(line: &[u8]) -> bool {
line.iter().all(u8::is_ascii_whitespace)
}
fn is_divider(line: &[u8]) -> bool {
content(line)
.strip_prefix(b"---")
.is_some_and(|rest| rest.first().is_none_or(u8::is_ascii_whitespace))
}
fn is_prefix(prefix: &[u8], of: &[u8]) -> bool {
of.len() >= prefix.len() && of[..prefix.len()].eq_ignore_ascii_case(prefix)
}
fn same_key(left: &[u8], right: &[u8]) -> bool {
let length = left.len().min(right.len());
left[..length].eq_ignore_ascii_case(&right[..length])
}
fn trim(bytes: &[u8]) -> &[u8] {
let start = bytes
.iter()
.position(|byte| !byte.is_ascii_whitespace())
.unwrap_or(bytes.len());
let end = bytes
.iter()
.rposition(|byte| !byte.is_ascii_whitespace())
.map_or(start, |last| last + 1);
&bytes[start..end]
}
impl TrailerConfig {
fn split<'l>(&self, line: &'l [u8]) -> Option<(&'l [u8], &'l [u8])> {
let line = content(line);
let at = line
.iter()
.position(|byte| self.separators.contains(byte))?;
let key = trim(&line[..at]);
let valid = !key.is_empty()
&& !line[at..].starts_with(b"://")
&& key.len() == line[..at].trim_ascii_end().len()
&& key
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-');
valid.then(|| (key, trim(&line[at + 1..])))
}
fn recognized(&self, line: &[u8], is_trailer: bool) -> bool {
let configured = || {
line.iter()
.position(|byte| self.separators.contains(byte))
.is_some_and(|at| self.section(&line[..at]).is_some())
};
line.starts_with(b"Signed-off-by: ")
|| line.starts_with(b"(cherry picked from commit ")
|| (is_trailer && configured())
}
fn block<'m>(&self, message: &'m [u8]) -> Block<'m> {
let lines = lines(message);
let comment = |line: &[u8]| self.comment.starts(line);
let end = lines
.iter()
.position(|(_, line)| {
is_scissors(content(line), &self.comment) || (self.divider && is_divider(line))
})
.unwrap_or(lines.len());
let mut content_end = end;
while content_end > 0 {
let line = lines[content_end - 1].1;
if !is_blank(line) && !comment(line) {
break;
}
content_end -= 1;
}
let offset = |index: usize| lines.get(index).map_or(message.len(), |(at, _)| *at);
let no_block = Block {
before: &message[..offset(content_end)],
after: &message[offset(content_end)..],
lines: None,
};
let subject = lines
.iter()
.position(|(_, line)| !is_blank(line) && !comment(line));
let mut start = content_end;
while start > 0 && !is_blank(lines[start - 1].1) {
start -= 1;
}
if subject.is_none_or(|subject| start <= subject) {
return no_block;
}
let mut trailers = 0usize;
let mut others = 0usize;
let mut recognized = false;
let mut after_trailer = false;
let mut block = Vec::new();
for &(_, line) in &lines[start..content_end] {
if comment(line) {
after_trailer = false;
continue;
}
let continues = after_trailer
&& line
.first()
.is_some_and(|byte| matches!(byte, b' ' | b'\t'));
if continues {
match block.last_mut() {
Some(Line::Trailer {
value,
continuation,
..
}) if value.is_empty() && continuation.is_empty() => {
*value = trim(line).to_vec();
}
Some(Line::Trailer { continuation, .. }) => continuation.push(line),
_ => block.push(Line::Other(line)),
}
continue;
}
let split = self.split(line);
recognized |= self.recognized(line, split.is_some());
if let Some((key, value)) = split {
trailers += 1;
after_trailer = true;
block.push(Line::Trailer {
key: self.resolve(key).0,
value: value.to_vec(),
continuation: Vec::new(),
});
} else {
if line.starts_with(b"(cherry picked from commit ")
|| line.starts_with(b"Signed-off-by: ")
{
trailers += 1;
after_trailer = true;
} else {
others += 1;
after_trailer = false;
}
block.push(Line::Other(line));
}
}
let is_block = others == 0 || (recognized && trailers * 3 >= others);
if !is_block {
return no_block;
}
Block {
before: &message[..offset(start)],
after: &message[offset(content_end)..],
lines: Some(block),
}
}
}
pub fn trailers(message: &[u8], config: &TrailerConfig) -> Vec<Trailer> {
let Some(lines) = config.block(message).lines else {
return Vec::new();
};
lines
.into_iter()
.filter_map(|line| match line {
Line::Trailer {
key,
mut value,
continuation,
} => {
for line in continuation {
value.push(b' ');
value.extend_from_slice(trim(line));
}
Some(Trailer { key, value })
}
Line::Other(_) => None,
})
.collect()
}
pub fn add_trailers(message: &[u8], new: &[Trailer], config: &TrailerConfig) -> Vec<u8> {
let block = config.block(message);
let had_block = block.lines.is_some();
let mut lines = block.lines.unwrap_or_default();
for trailer in new {
let (key, rules) = config.resolve(trim(&trailer.key));
let value = trim(&trailer.value).to_vec();
add(&mut lines, key, value, rules);
}
let mut output = Vec::with_capacity(message.len() + 64);
output.extend_from_slice(block.before);
if !had_block {
if !output.is_empty() && !output.ends_with(b"\n") {
output.push(b'\n');
}
output.push(b'\n');
}
for line in &lines {
match line {
Line::Trailer {
key,
value,
continuation,
} => {
config.format(key, value, &mut output);
output.push(b'\n');
for line in continuation {
output.extend_from_slice(line);
if !line.ends_with(b"\n") {
output.push(b'\n');
}
}
}
Line::Other(line) => {
output.extend_from_slice(line);
if !line.ends_with(b"\n") {
output.push(b'\n');
}
}
}
}
output.extend_from_slice(block.after);
output
}
fn add(lines: &mut Vec<Line<'_>>, key: Vec<u8>, value: Vec<u8>, rules: Rules) {
let matching: Vec<usize> = lines
.iter()
.enumerate()
.filter_map(|(index, line)| match line {
Line::Trailer { key: existing, .. } if same_key(existing, &key) => Some(index),
_ => None,
})
.collect();
let towards_end = matches!(rules.placement, Placement::End | Placement::After);
let anchor = if towards_end {
matching.last()
} else {
matching.first()
}
.copied();
let position = |lines: &[Line<'_>], anchor: Option<usize>| match (rules.placement, anchor) {
(Placement::After, Some(anchor)) => anchor + 1,
(Placement::Before, Some(anchor)) => anchor,
(Placement::End | Placement::After, _) => lines.len(),
(Placement::Start | Placement::Before, _) => 0,
};
let new = Line::Trailer {
key,
value,
continuation: Vec::new(),
};
let same = |line: &Line<'_>| match (line, &new) {
(
Line::Trailer {
key,
value,
continuation,
},
Line::Trailer {
key: new_key,
value: new_value,
..
},
) => same_key(key, new_key) && value == new_value && continuation.is_empty(),
_ => false,
};
let Some(anchor) = anchor else {
if rules.if_missing == IfMissing::Add {
let at = position(lines, None);
lines.insert(at, new);
}
return;
};
match rules.if_exists {
IfExists::DoNothing => {}
IfExists::Add => {
let at = position(lines, Some(anchor));
lines.insert(at, new);
}
IfExists::AddIfDifferent => {
if !matching.iter().any(|&index| same(&lines[index])) {
let at = position(lines, Some(anchor));
lines.insert(at, new);
}
}
IfExists::AddIfDifferentNeighbor => {
let at = position(lines, Some(anchor));
let neighbor = if towards_end {
at.checked_sub(1)
} else {
Some(at)
};
if !neighbor
.and_then(|index| lines.get(index))
.is_some_and(same)
{
lines.insert(at, new);
}
}
IfExists::Replace => {
lines.remove(anchor);
let at = match rules.placement {
Placement::After | Placement::Before => anchor,
Placement::End | Placement::Start => position(lines, None),
};
lines.insert(at, new);
}
}
}