use std::collections::HashMap;
use std::io::BufRead;
pub const DEFAULT_PATCHES_DIR: &str = "patches";
pub const DEFAULT_SERIES_FILE: &str = "series";
pub fn find_common_patch_suffix<'a>(names: impl Iterator<Item = &'a str>) -> Option<&'a str> {
let mut suffix_count = HashMap::new();
for name in names {
if name == "series" || name == "00list" {
continue;
}
if name.starts_with("README") {
continue;
}
let suffix = name.find('.').map(|index| &name[index..]).unwrap_or("");
suffix_count
.entry(suffix)
.and_modify(|count| *count += 1)
.or_insert(1);
}
suffix_count
.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(suffix, _)| suffix)
}
#[cfg(test)]
mod find_common_patch_suffix_tests {
#[test]
fn test_find_common_patch_suffix() {
let names = vec![
"0001-foo.patch",
"0002-bar.patch",
"0003-baz.patch",
"0004-qux.patch",
];
assert_eq!(
super::find_common_patch_suffix(names.into_iter()),
Some(".patch")
);
}
#[test]
fn test_find_common_patch_suffix_no_common_suffix() {
let names = vec![
"0001-foo.patch",
"0002-bar.patch",
"0003-baz.patch",
"0004-qux",
];
assert_eq!(
super::find_common_patch_suffix(names.into_iter()),
Some(".patch")
);
}
#[test]
fn test_find_common_patch_suffix_no_patches() {
let names = vec![
"README",
"0001-foo.patch",
"0002-bar.patch",
"0003-baz.patch",
];
assert_eq!(
super::find_common_patch_suffix(names.into_iter()),
Some(".patch")
);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeriesEntry {
Patch {
name: String,
options: Vec<String>,
},
Comment(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Series {
pub entries: Vec<SeriesEntry>,
}
impl Series {
pub fn new() -> Self {
Self { entries: vec![] }
}
pub fn len(&self) -> usize {
self.entries
.iter()
.filter(|entry| matches!(entry, SeriesEntry::Patch { .. }))
.count()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn contains(&self, name: &str) -> bool {
self.entries.iter().any(|entry| match entry {
SeriesEntry::Patch {
name: entry_name, ..
} => entry_name == name,
_ => false,
})
}
pub fn read<R: std::io::Read>(reader: R) -> std::io::Result<Self> {
let mut series = Self::new();
let reader = std::io::BufReader::new(reader);
for line in reader.lines() {
let line = line?;
let line = line.trim();
if line.is_empty() {
continue;
}
if line.starts_with('#') {
series.entries.push(SeriesEntry::Comment(line.to_string()));
continue;
}
let mut parts = line.split_whitespace();
let name = parts.next().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"missing patch name in series file",
)
})?;
let options = parts.map(|s| s.to_string()).collect();
series.entries.push(SeriesEntry::Patch {
name: name.to_string(),
options,
});
}
Ok(series)
}
pub fn remove(&mut self, name: &str) {
self.entries.retain(|entry| match entry {
SeriesEntry::Patch {
name: entry_name, ..
} => entry_name != name,
_ => true,
});
}
pub fn patches(&self) -> impl Iterator<Item = &str> {
self.entries.iter().filter_map(|entry| match entry {
SeriesEntry::Patch { name, .. } => Some(name.as_str()),
_ => None,
})
}
pub fn append(&mut self, name: &str, options: Option<&[String]>) {
self.entries.push(SeriesEntry::Patch {
name: name.to_string(),
options: options.map(|options| options.to_vec()).unwrap_or_default(),
});
}
pub fn write<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
for entry in &self.entries {
match entry {
SeriesEntry::Patch { name, options } => {
write!(writer, "{}", name)?;
for option in options {
write!(writer, " {}", option)?;
}
writeln!(writer)?;
}
SeriesEntry::Comment(comment) => {
writeln!(writer, "# {}", comment)?;
}
}
}
Ok(())
}
pub fn iter<'a>(&'a self) -> std::slice::Iter<'a, SeriesEntry> {
self.entries.iter()
}
}
impl std::ops::Index<usize> for Series {
type Output = SeriesEntry;
fn index(&self, index: usize) -> &Self::Output {
&self.entries[index]
}
}
impl Default for Series {
fn default() -> Self {
Self::new()
}
}
pub fn read_quilt_patches<R: std::io::Read>(mut reader: R) -> std::path::PathBuf {
let mut p = String::new();
reader.read_to_string(&mut p).unwrap();
p.into()
}
pub fn read_quilt_series<R: std::io::Read>(mut reader: R) -> std::path::PathBuf {
let mut s = String::new();
reader.read_to_string(&mut s).unwrap();
s.into()
}
pub struct QuiltPatch {
pub name: String,
pub options: Vec<String>,
pub patch: Vec<u8>,
}
impl QuiltPatch {
pub fn as_bytes(&self) -> &[u8] {
&self.patch
}
pub fn name(&self) -> &str {
&self.name
}
pub fn options(&self) -> &[String] {
&self.options
}
pub fn parse(&self) -> Result<Vec<crate::unified::UnifiedPatch>, crate::unified::Error> {
let lines = self.patch.split_inclusive(|&b| b == b'\n');
crate::unified::parse_patches(lines.map(|x| x.to_vec()))
.filter_map(|patch| match patch {
Ok(crate::unified::PlainOrBinaryPatch::Plain(patch)) => Some(Ok(patch)),
Ok(crate::unified::PlainOrBinaryPatch::Binary(_)) => None,
Err(err) => Some(Err(err)),
})
.collect()
}
}
pub fn iter_quilt_patches(directory: &std::path::Path) -> impl Iterator<Item = QuiltPatch> + '_ {
let series_path = directory.join("series");
let series = if series_path.exists() {
Series::read(std::fs::File::open(series_path).unwrap()).unwrap()
} else {
Series::new()
};
series
.iter()
.filter_map(move |entry| {
let (patch, options) = match entry {
SeriesEntry::Patch { name, options } => (name, options),
SeriesEntry::Comment(_) => return None,
};
let p = directory.join(patch);
let lines = std::fs::read_to_string(p).unwrap();
Some(QuiltPatch {
name: patch.to_string(),
patch: lines.into_bytes(),
options: options.clone(),
})
})
.collect::<Vec<_>>()
.into_iter()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_series_read() {
let series = Series::read(
r#"0001-foo.patch
# This is a comment
0002-bar.patch --reverse
0003-baz.patch --reverse --fuzz=3
"#
.as_bytes(),
)
.unwrap();
assert_eq!(series.len(), 3);
assert_eq!(
series[0],
SeriesEntry::Patch {
name: "0001-foo.patch".to_string(),
options: vec![]
}
);
assert_eq!(
series[1],
SeriesEntry::Comment("# This is a comment".to_string())
);
assert_eq!(
series[2],
SeriesEntry::Patch {
name: "0002-bar.patch".to_string(),
options: vec!["--reverse".to_string()]
}
);
assert_eq!(
series[3],
SeriesEntry::Patch {
name: "0003-baz.patch".to_string(),
options: vec!["--reverse".to_string(), "--fuzz=3".to_string()]
}
);
}
#[test]
fn test_series_write() {
let mut series = Series::new();
series.append("0001-foo.patch", None);
series.append("0002-bar.patch", Some(&["--reverse".to_string()]));
series.append(
"0003-baz.patch",
Some(&["--reverse".to_string(), "--fuzz=3".to_string()]),
);
let mut writer = vec![];
series.write(&mut writer).unwrap();
let series = String::from_utf8(writer).unwrap();
assert_eq!(
series,
"0001-foo.patch\n0002-bar.patch --reverse\n0003-baz.patch --reverse --fuzz=3\n"
);
}
#[test]
fn test_series_remove() {
let mut series = Series::new();
series.append("0001-foo.patch", None);
series.append("0002-bar.patch", Some(&["--reverse".to_string()]));
series.append(
"0003-baz.patch",
Some(&["--reverse".to_string(), "--fuzz=3".to_string()]),
);
series.remove("0002-bar.patch");
let mut writer = vec![];
series.write(&mut writer).unwrap();
let series = String::from_utf8(writer).unwrap();
assert_eq!(
series,
"0001-foo.patch\n0003-baz.patch --reverse --fuzz=3\n"
);
}
#[test]
fn test_series_contains() {
let mut series = Series::new();
series.append("0001-foo.patch", None);
series.append("0002-bar.patch", Some(&["--reverse".to_string()]));
series.append(
"0003-baz.patch",
Some(&["--reverse".to_string(), "--fuzz=3".to_string()]),
);
assert!(series.contains("0002-bar.patch"));
assert!(!series.contains("0004-qux.patch"));
}
#[test]
fn test_series_patches() {
let mut series = Series::new();
series.append("0001-foo.patch", None);
series.append("0002-bar.patch", Some(&["--reverse".to_string()]));
series.append(
"0003-baz.patch",
Some(&["--reverse".to_string(), "--fuzz=3".to_string()]),
);
let patches: Vec<_> = series.patches().collect();
assert_eq!(
patches,
&["0001-foo.patch", "0002-bar.patch", "0003-baz.patch"]
);
}
#[test]
fn test_series_is_empty() {
let series = Series::new();
assert!(series.is_empty());
let mut series = Series::new();
series.append("0001-foo.patch", None);
assert!(!series.is_empty());
}
#[test]
fn test_quilt_patch_parse() {
let patch = QuiltPatch {
name: "0001-foo.patch".to_string(),
options: vec![],
patch: b"--- a/foo\n+++ b/foo\n@@ -1,3 +1,3 @@\n foo\n bar\n-bar\n+bar\n".to_vec(),
};
let patches = patch.parse().unwrap();
assert_eq!(patches.len(), 1);
assert_eq!(
patches[0],
crate::unified::UnifiedPatch {
orig_name: b"a/foo".to_vec(),
mod_name: b"b/foo".to_vec(),
orig_ts: None,
mod_ts: None,
hunks: vec![crate::unified::Hunk {
orig_pos: 1,
orig_range: 3,
mod_pos: 1,
mod_range: 3,
lines: vec![
crate::unified::HunkLine::ContextLine(b"foo\n".to_vec()),
crate::unified::HunkLine::ContextLine(b"bar\n".to_vec()),
crate::unified::HunkLine::RemoveLine(b"bar\n".to_vec()),
crate::unified::HunkLine::InsertLine(b"bar\n".to_vec())
],
tail: None
}]
}
);
}
#[test]
fn test_series_read_empty_lines() {
let series = Series::read(
r#"0001-foo.patch
0002-bar.patch
"#
.as_bytes(),
)
.unwrap();
assert_eq!(series.len(), 2);
assert_eq!(
series[0],
SeriesEntry::Patch {
name: "0001-foo.patch".to_string(),
options: vec![]
}
);
assert_eq!(
series[1],
SeriesEntry::Patch {
name: "0002-bar.patch".to_string(),
options: vec![]
}
);
}
#[test]
fn test_series_read_whitespace_lines() {
let series = Series::read("0001-foo.patch \n \n0002-bar.patch\n".as_bytes()).unwrap();
assert_eq!(series.len(), 2);
assert_eq!(
series[0],
SeriesEntry::Patch {
name: "0001-foo.patch".to_string(),
options: vec![]
}
);
assert_eq!(
series[1],
SeriesEntry::Patch {
name: "0002-bar.patch".to_string(),
options: vec![]
}
);
}
}