use crate::error::{Error, Result};
pub const LOG_FORMAT: &str = "%H\x1f%h\x1f%an\x1f%ae\x1f%aI\x1f%cn\x1f%ce\x1f%cI\x1f%s\x1f%b\x1e";
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CommitEntry {
pub sha: String,
pub short_sha: String,
pub author_name: String,
pub author_email: String,
pub author_date: String,
pub committer_name: String,
pub committer_email: String,
pub committer_date: String,
pub subject: String,
pub body: String,
}
pub fn parse_log(input: &str) -> Result<Vec<CommitEntry>> {
let mut out = Vec::new();
for record in input.split('\x1e') {
let trimmed = record.trim_matches('\n');
if trimmed.is_empty() {
continue;
}
let fields: Vec<&str> = trimmed.split('\x1f').collect();
if fields.len() < 10 {
return Err(Error::parse_error(format!(
"expected 10 fields, got {}",
fields.len()
)));
}
out.push(CommitEntry {
sha: fields[0].to_string(),
short_sha: fields[1].to_string(),
author_name: fields[2].to_string(),
author_email: fields[3].to_string(),
author_date: fields[4].to_string(),
committer_name: fields[5].to_string(),
committer_email: fields[6].to_string(),
committer_date: fields[7].to_string(),
subject: fields[8].to_string(),
body: fields[9..].join("\x1f"),
});
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_single_commit() {
let input = "sha1\x1fsh\x1fAlice\x1fa@x\x1f2024-01-01T00:00:00Z\x1fBob\x1fb@y\x1f2024-01-02T00:00:00Z\x1fhello\x1fbody text\x1e";
let out = parse_log(input).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].sha, "sha1");
assert_eq!(out[0].author_name, "Alice");
assert_eq!(out[0].body, "body text");
}
#[test]
fn parses_multiple_commits() {
let input = concat!(
"a\x1fa\x1fA\x1fa@x\x1fd1\x1fB\x1fb@y\x1fd2\x1fone\x1f\x1e",
"b\x1fb\x1fA\x1fa@x\x1fd3\x1fB\x1fb@y\x1fd4\x1ftwo\x1f\x1e",
);
let out = parse_log(input).unwrap();
assert_eq!(out.len(), 2);
assert_eq!(out[0].subject, "one");
assert_eq!(out[1].subject, "two");
}
#[test]
fn empty_yields_no_commits() {
assert!(parse_log("").unwrap().is_empty());
}
#[test]
fn too_few_fields_errors() {
assert!(parse_log("a\x1fb\x1e").is_err());
}
}