use std::ffi::OsString;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FastForwardChoice {
Default,
Allowed,
Never,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Signing {
Default,
Never,
Sign(Option<Vec<u8>>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MergeShape {
pub reference: OsString,
pub message: Option<Vec<u8>>,
pub fast_forward: FastForwardChoice,
pub signing: Signing,
pub no_edit: bool,
pub quiet: bool,
pub stat: Option<bool>,
pub allow_unrelated_histories: bool,
pub log: Option<bool>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ParsedMerge {
Supported(MergeShape),
Unsupported(String),
}
fn append_message(buffer: &mut Vec<u8>, value: &[u8]) {
if !buffer.is_empty() {
buffer.push(b'\n');
}
buffer.extend_from_slice(value);
if buffer.last() != Some(&b'\n') {
buffer.push(b'\n');
}
}
fn is_ignorable(text: &str) -> bool {
return matches!(
text,
"-v" | "--verbose" | "--no-verbose" | "--progress" | "--no-progress" | "--no-verify" | "--verify"
| "--summary" | "--no-summary" | "--no-signoff" | "--no-autostash" | "--no-verify-signatures"
);
}
pub fn parse_merge_shape(region: &[OsString]) -> ParsedMerge {
let mut positional: Vec<OsString> = Vec::new();
let mut message: Vec<u8> = Vec::new();
let mut has_message: bool = false;
let mut shape: MergeShape = MergeShape {
reference: OsString::new(),
message: None,
fast_forward: FastForwardChoice::Default,
signing: Signing::Default,
no_edit: false,
quiet: false,
stat: None,
allow_unrelated_histories: false,
log: None,
};
let mut tokens = region.iter();
while let Some(token) = tokens.next() {
let bytes: &[u8] = token.as_encoded_bytes();
let Ok(text) = std::str::from_utf8(bytes) else {
if bytes.starts_with(b"-") {
return ParsedMerge::Unsupported(String::from("an option that is not UTF-8 text"));
}
positional.push(token.clone());
continue;
};
if !text.starts_with('-') || text == "-" {
positional.push(token.clone());
continue;
}
match text {
"--no-edit" => shape.no_edit = true,
"-q" | "--quiet" => shape.quiet = true,
"--ff" => shape.fast_forward = FastForwardChoice::Allowed,
"--no-ff" => shape.fast_forward = FastForwardChoice::Never,
"--stat" => shape.stat = Some(true),
"--no-stat" | "-n" => shape.stat = Some(false),
"--allow-unrelated-histories" => shape.allow_unrelated_histories = true,
"--log" => shape.log = Some(true),
"--no-log" => shape.log = Some(false),
"--no-gpg-sign" => shape.signing = Signing::Never,
"-S" | "--gpg-sign" => shape.signing = Signing::Sign(None),
"-m" | "--message" => {
let Some(value) = tokens.next() else {
return ParsedMerge::Unsupported(String::from("-m without a value"));
};
append_message(&mut message, value.as_encoded_bytes());
has_message = true;
}
other if is_ignorable(other) => {}
other if other.starts_with("--message=") => {
append_message(&mut message, &bytes["--message=".len()..]);
has_message = true;
}
other if other.starts_with("--gpg-sign=") => {
shape.signing = Signing::Sign(Some(bytes["--gpg-sign=".len()..].to_vec()));
}
other if other.starts_with("-S") && !other.starts_with("--") => {
shape.signing = Signing::Sign(Some(bytes[2..].to_vec()));
}
other if other.starts_with("-m") && !other.starts_with("--") => {
append_message(&mut message, &bytes[2..]);
has_message = true;
}
other => return ParsedMerge::Unsupported(format!("the option {other}")),
}
}
if shape.log.is_some() && has_message {
return ParsedMerge::Unsupported(String::from("--log or --no-log together with -m"));
}
if positional.len() != 1 {
return ParsedMerge::Unsupported(format!("{} commits to merge, not exactly one", positional.len()));
}
shape.reference = positional.remove(0);
if shape.reference.as_encoded_bytes().is_empty() {
return ParsedMerge::Unsupported(String::from("an empty commit name"));
}
if has_message {
shape.message = Some(message);
}
return ParsedMerge::Supported(shape);
}
#[cfg(test)]
#[path = "merge_options_tests.rs"]
mod tests;