use super::*;
use anyhow::Result;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct Commit {
pub hash: String,
pub short_hash: String,
pub message: String,
pub author_name: String,
pub author_email: String,
pub body: String,
}
pub fn parse_commit_output(output: &str) -> Vec<Commit> {
if output.is_empty() {
return vec![];
}
output
.split('\x1e')
.filter(|record| !record.trim().is_empty())
.filter_map(|record| {
let fields: Vec<&str> = record.split('\x1f').collect();
if fields.len() >= 5 {
Some(Commit {
hash: fields[0].trim().to_string(),
short_hash: fields[1].to_string(),
message: fields[2].to_string(),
author_name: fields[3].to_string(),
author_email: fields[4].to_string(),
body: fields.get(5).unwrap_or(&"").trim().to_string(),
})
} else {
None
}
})
.collect()
}
pub(super) fn cwd_or_dot() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
pub fn get_commits_between(from: &str, to: &str, path_filter: Option<&str>) -> Result<Vec<Commit>> {
get_commits_between_in(&cwd_or_dot(), from, to, path_filter)
}
pub fn get_commits_between_in(
cwd: &Path,
from: &str,
to: &str,
path_filter: Option<&str>,
) -> Result<Vec<Commit>> {
get_commits_between_paths_in(
cwd,
from,
to,
&path_filter
.into_iter()
.map(String::from)
.collect::<Vec<_>>(),
)
}
pub fn get_commits_between_paths(from: &str, to: &str, paths: &[String]) -> Result<Vec<Commit>> {
get_commits_between_paths_in(&cwd_or_dot(), from, to, paths)
}
pub fn get_commits_between_paths_in(
cwd: &Path,
from: &str,
to: &str,
paths: &[String],
) -> Result<Vec<Commit>> {
let range = format!("{}..{}", from, to);
let mut args = vec![
"-c".to_string(),
"log.showSignature=false".to_string(),
"log".to_string(),
"--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
range,
];
if !paths.is_empty() {
args.push("--".to_string());
for p in paths {
args.push(p.clone());
}
}
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let output = git_output_in(cwd, &arg_refs)?;
Ok(parse_commit_output(&output))
}
pub fn get_all_commits(path_filter: Option<&str>) -> Result<Vec<Commit>> {
get_all_commits_in(&cwd_or_dot(), path_filter)
}
pub fn get_all_commits_in(cwd: &Path, path_filter: Option<&str>) -> Result<Vec<Commit>> {
get_all_commits_paths_in(
cwd,
&path_filter
.into_iter()
.map(String::from)
.collect::<Vec<_>>(),
)
}
pub fn get_all_commits_paths(paths: &[String]) -> Result<Vec<Commit>> {
get_all_commits_paths_in(&cwd_or_dot(), paths)
}
pub fn get_all_commits_paths_in(cwd: &Path, paths: &[String]) -> Result<Vec<Commit>> {
let mut args = vec![
"-c".to_string(),
"log.showSignature=false".to_string(),
"log".to_string(),
"--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
"HEAD".to_string(),
];
if !paths.is_empty() {
args.push("--".to_string());
for p in paths {
args.push(p.clone());
}
}
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let output = git_output_in(cwd, &arg_refs)?;
Ok(parse_commit_output(&output))
}
#[derive(Debug, Clone)]
pub struct CommitWithFiles {
pub commit: Commit,
pub files: Vec<String>,
}
pub fn parse_commit_output_with_files(output: &str) -> Vec<CommitWithFiles> {
if output.is_empty() {
return vec![];
}
let segments: Vec<&str> = output.split('\x1e').collect();
let mut out: Vec<CommitWithFiles> = Vec::new();
for (idx, seg) in segments.iter().enumerate() {
let metadata = if idx == 0 {
seg.trim_start_matches(['\n', '\r']).to_string()
} else {
let lines: Vec<&str> = seg.split('\n').collect();
match lines.iter().position(|line| line.contains('\x1f')) {
Some(start) => lines[start..].join("\n"),
None => String::new(),
}
};
if metadata.trim().is_empty() {
continue;
}
let commits = parse_commit_output(&metadata);
let Some(commit) = commits.into_iter().next() else {
continue;
};
let files = match segments.get(idx + 1) {
Some(next) => next
.split('\n')
.map(str::trim)
.take_while(|line| !line.contains('\x1f'))
.filter(|line| !line.is_empty())
.map(str::to_string)
.collect(),
None => Vec::new(),
};
out.push(CommitWithFiles { commit, files });
}
out
}
pub fn get_commits_between_paths_with_files_in(
cwd: &Path,
from: &str,
to: &str,
paths: &[String],
) -> Result<Vec<CommitWithFiles>> {
let range = format!("{}..{}", from, to);
let mut args = vec![
"-c".to_string(),
"log.showSignature=false".to_string(),
"log".to_string(),
"--name-only".to_string(),
"--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
range,
];
if !paths.is_empty() {
args.push("--".to_string());
for p in paths {
args.push(p.clone());
}
}
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let output = git_output_in(cwd, &arg_refs)?;
Ok(parse_commit_output_with_files(&output))
}
pub fn get_all_commits_paths_with_files_in(
cwd: &Path,
paths: &[String],
) -> Result<Vec<CommitWithFiles>> {
let mut args = vec![
"-c".to_string(),
"log.showSignature=false".to_string(),
"log".to_string(),
"--name-only".to_string(),
"--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
"HEAD".to_string(),
];
if !paths.is_empty() {
args.push("--".to_string());
for p in paths {
args.push(p.clone());
}
}
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let output = git_output_in(cwd, &arg_refs)?;
Ok(parse_commit_output_with_files(&output))
}
pub fn get_commits_reachable_paths_in(
cwd: &Path,
rev: &str,
paths: &[String],
) -> Result<Vec<Commit>> {
let mut args = vec![
"-c".to_string(),
"log.showSignature=false".to_string(),
"log".to_string(),
"--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
rev.to_string(),
];
if !paths.is_empty() {
args.push("--".to_string());
for p in paths {
args.push(p.clone());
}
}
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let output = git_output_in(cwd, &arg_refs)?;
Ok(parse_commit_output(&output))
}
pub fn get_commits_reachable_paths_with_files_in(
cwd: &Path,
rev: &str,
paths: &[String],
) -> Result<Vec<CommitWithFiles>> {
let mut args = vec![
"-c".to_string(),
"log.showSignature=false".to_string(),
"log".to_string(),
"--name-only".to_string(),
"--pretty=format:%H%x1f%h%x1f%s%x1f%an%x1f%ae%x1f%b%x1e".to_string(),
rev.to_string(),
];
if !paths.is_empty() {
args.push("--".to_string());
for p in paths {
args.push(p.clone());
}
}
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let output = git_output_in(cwd, &arg_refs)?;
Ok(parse_commit_output_with_files(&output))
}
pub fn get_last_commit_messages(count: usize) -> Result<Vec<String>> {
get_last_commit_messages_in(&cwd_or_dot(), count)
}
pub fn get_last_commit_messages_in(cwd: &Path, count: usize) -> Result<Vec<String>> {
let output = git_output_in(
cwd,
&[
"-c",
"log.showSignature=false",
"log",
&format!("-{count}"),
"--pretty=format:%s",
],
)?;
Ok(output.lines().map(str::to_string).collect())
}
pub fn get_commit_messages_between(from: &str, to: &str) -> Result<Vec<String>> {
get_commit_messages_between_in(&cwd_or_dot(), from, to)
}
pub fn get_commit_messages_between_in(cwd: &Path, from: &str, to: &str) -> Result<Vec<String>> {
let output = git_output_in(
cwd,
&[
"-c",
"log.showSignature=false",
"log",
"--pretty=format:%s",
&format!("{from}..{to}"),
],
)?;
Ok(output.lines().map(str::to_string).collect())
}