#![deny(missing_docs)]
mod lex;
mod parse;
use lazy_regex::regex_captures;
pub mod changes;
pub mod textwrap;
pub use crate::parse::{ChangeLog, Entry, Error, ParseError, Urgency};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[allow(non_camel_case_types)]
#[repr(u16)]
#[allow(missing_docs)]
pub enum SyntaxKind {
IDENTIFIER = 0,
INDENT,
TEXT,
WHITESPACE,
VERSION, SEMICOLON, EQUALS, DETAIL, NEWLINE, ERROR, COMMENT,
ROOT, ENTRY, ENTRY_HEADER,
ENTRY_FOOTER,
METADATA,
METADATA_ENTRY,
METADATA_KEY,
METADATA_VALUE,
ENTRY_BODY,
DISTRIBUTIONS,
EMPTY_LINE,
TIMESTAMP,
MAINTAINER,
EMAIL,
}
impl From<SyntaxKind> for rowan::SyntaxKind {
fn from(kind: SyntaxKind) -> Self {
Self(kind as u16)
}
}
pub fn parseaddr(s: &str) -> (Option<&str>, &str) {
if let Some((_, name, email)) = regex_captures!(r"^(.*)\s+<(.*)>$", s) {
if name.is_empty() {
(None, email)
} else {
(Some(name), email)
}
} else {
(None, s)
}
}
pub fn get_maintainer_from_env(
get_env: impl Fn(&str) -> Option<String>,
) -> Option<(String, String)> {
use std::io::BufRead;
let mut debemail = get_env("DEBEMAIL");
let mut debfullname = get_env("DEBFULLNAME");
if let Some(email) = debemail.as_ref() {
let (parsed_name, parsed_email) = parseaddr(email);
if let Some(parsed_name) = parsed_name {
if debfullname.is_none() {
debfullname = Some(parsed_name.to_string());
}
}
debemail = Some(parsed_email.to_string());
}
if debfullname.is_none() || debemail.is_none() {
if let Some(email) = get_env("EMAIL") {
let (parsed_name, parsed_email) = parseaddr(email.as_str());
if let Some(parsed_name) = parsed_name {
if debfullname.is_none() {
debfullname = Some(parsed_name.to_string());
}
}
debemail = Some(parsed_email.to_string());
}
}
let maintainer = if let Some(m) = debfullname {
Some(m.trim().to_string())
} else if let Some(m) = get_env("NAME") {
Some(m.trim().to_string())
} else {
Some(whoami::realname())
};
let email_address = if let Some(email) = debemail {
Some(email)
} else if let Some(email) = get_env("EMAIL") {
Some(email)
} else {
let mut addr: Option<String> = None;
if let Ok(mailname_file) = std::fs::File::open("/etc/mailname") {
let mut reader = std::io::BufReader::new(mailname_file);
if let Ok(line) = reader.fill_buf() {
if !line.is_empty() {
addr = Some(String::from_utf8_lossy(line).trim().to_string());
}
}
}
if addr.is_none() {
match whoami::fallible::hostname() {
Ok(hostname) => {
addr = Some(hostname);
}
Err(e) => {
log::debug!("Failed to get hostname: {}", e);
addr = None;
}
}
}
addr.map(|hostname| format!("{}@{}", whoami::username(), hostname))
};
if let (Some(maintainer), Some(email_address)) = (maintainer, email_address) {
Some((maintainer, email_address))
} else {
None
}
}
pub fn get_maintainer() -> Option<(String, String)> {
get_maintainer_from_env(|s| std::env::var(s).ok())
}
#[cfg(test)]
mod get_maintainer_from_env_tests {
use super::*;
#[test]
fn test_normal() {
get_maintainer();
}
#[test]
fn test_deb_vars() {
let mut d = std::collections::HashMap::new();
d.insert("DEBFULLNAME".to_string(), "Jelmer".to_string());
d.insert("DEBEMAIL".to_string(), "jelmer@example.com".to_string());
let t = get_maintainer_from_env(|s| d.get(s).cloned());
assert_eq!(
Some(("Jelmer".to_string(), "jelmer@example.com".to_string())),
t
);
}
#[test]
fn test_email_var() {
let mut d = std::collections::HashMap::new();
d.insert("NAME".to_string(), "Jelmer".to_string());
d.insert("EMAIL".to_string(), "foo@example.com".to_string());
let t = get_maintainer_from_env(|s| d.get(s).cloned());
assert_eq!(
Some(("Jelmer".to_string(), "foo@example.com".to_string())),
t
);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Identity {
pub name: String,
pub email: String,
}
impl Identity {
pub fn new(name: String, email: String) -> Self {
Self { name, email }
}
pub fn from_env() -> Option<Self> {
get_maintainer().map(|(name, email)| Self { name, email })
}
}
impl From<(String, String)> for Identity {
fn from((name, email): (String, String)) -> Self {
Self { name, email }
}
}
impl std::fmt::Display for Identity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} <{}>", self.name, self.email)
}
}
pub fn distribution_is_unreleased(distribution: &str) -> bool {
distribution == "UNRELEASED" || distribution.starts_with("UNRELEASED-")
}
pub fn distributions_is_unreleased(distributions: &[&str]) -> bool {
distributions.iter().any(|x| distribution_is_unreleased(x))
}
#[test]
fn test_distributions_is_unreleased() {
assert!(distributions_is_unreleased(&["UNRELEASED"]));
assert!(distributions_is_unreleased(&[
"UNRELEASED-1",
"UNRELEASED-2"
]));
assert!(distributions_is_unreleased(&["UNRELEASED", "UNRELEASED-2"]));
assert!(!distributions_is_unreleased(&["stable"]));
}
pub fn is_unreleased_inaugural(cl: &ChangeLog) -> bool {
let mut entries = cl.iter();
if let Some(entry) = entries.next() {
if entry.is_unreleased() == Some(false) {
return false;
}
let changes = entry.change_lines().collect::<Vec<_>>();
if changes.len() > 1 || !changes[0].starts_with("* Initial release") {
return false;
}
entries.next().is_none()
} else {
false
}
}
#[cfg(test)]
mod is_unreleased_inaugural_tests {
use super::*;
#[test]
fn test_empty() {
assert!(!is_unreleased_inaugural(&ChangeLog::new()));
}
#[test]
fn test_unreleased_inaugural() {
let mut cl = ChangeLog::new();
cl.new_entry()
.maintainer(("Jelmer Vernooij".into(), "jelmer@debian.org".into()))
.distribution("UNRELEASED".to_string())
.version("1.0.0".parse().unwrap())
.change_line("* Initial release".to_string())
.finish();
assert!(is_unreleased_inaugural(&cl));
}
#[test]
fn test_not_unreleased_inaugural() {
let mut cl = ChangeLog::new();
cl.new_entry()
.maintainer(("Jelmer Vernooij".into(), "jelmer@debian.org".into()))
.distributions(vec!["unstable".to_string()])
.version("1.0.0".parse().unwrap())
.change_line("* Initial release".to_string())
.finish();
assert_eq!(cl.iter().next().unwrap().is_unreleased(), Some(false));
assert!(!is_unreleased_inaugural(&cl));
cl.new_entry()
.maintainer(("Jelmer Vernooij".into(), "jelmer@debian.org".into()))
.distribution("UNRELEASED".to_string())
.version("1.0.1".parse().unwrap())
.change_line("* Some change".to_string())
.finish();
assert!(!is_unreleased_inaugural(&cl));
}
}
const DEFAULT_DISTRIBUTION: &[&str] = &["UNRELEASED"];
pub fn release(
cl: &mut ChangeLog,
distribution: Option<Vec<String>>,
timestamp: Option<chrono::DateTime<chrono::FixedOffset>>,
maintainer: Option<(String, String)>,
) -> bool {
let mut entries = cl.iter();
let mut first_entry = entries.next().unwrap();
let second_entry = entries.next();
let distribution = if let Some(d) = distribution.as_ref() {
d.clone()
} else {
if let Some(d) = second_entry.and_then(|e| e.distributions()) {
d
} else {
DEFAULT_DISTRIBUTION
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
}
};
if first_entry.is_unreleased() == Some(false) {
take_uploadership(&mut first_entry, maintainer);
first_entry.set_distributions(distribution);
let timestamp = timestamp.unwrap_or(chrono::offset::Utc::now().into());
first_entry.set_datetime(timestamp);
true
} else {
false
}
}
pub fn take_uploadership(entry: &mut Entry, maintainer: Option<(String, String)>) {
let (maintainer_name, maintainer_email) = if let Some(m) = maintainer {
m
} else {
get_maintainer().unwrap()
};
if let (Some(current_maintainer), Some(current_email)) = (entry.maintainer(), entry.email()) {
if current_maintainer != maintainer_name || current_email != maintainer_email {
if let Some(first_line) = entry.change_lines().next() {
if first_line.starts_with("[ ") {
entry.prepend_change_line(
crate::changes::format_section_title(current_maintainer.as_str()).as_str(),
);
}
}
}
}
entry.set_maintainer((maintainer_name, maintainer_email));
}
pub fn gbp_dch(path: &std::path::Path) -> std::result::Result<(), std::io::Error> {
let output = std::process::Command::new("gbp")
.arg("dch")
.arg("--ignore-branch")
.current_dir(path)
.output()?;
if !output.status.success() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"gbp dch failed: {}",
String::from_utf8_lossy(&output.stderr)
),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parseaddr() {
assert_eq!(
(Some("Jelmer"), "jelmer@jelmer.uk"),
parseaddr("Jelmer <jelmer@jelmer.uk>")
);
assert_eq!((None, "jelmer@jelmer.uk"), parseaddr("jelmer@jelmer.uk"));
}
#[test]
fn test_parseaddr_empty() {
assert_eq!((None, ""), parseaddr(""));
}
}