use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ChangeType {
Add,
Modify,
Delete,
Rename,
}
impl std::fmt::Display for ChangeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Add => write!(f, "add"),
Self::Modify => write!(f, "modify"),
Self::Delete => write!(f, "delete"),
Self::Rename => write!(f, "rename"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileChange {
pub path: PathBuf,
pub change_type: ChangeType,
pub commit_id: String,
pub timestamp: u64,
pub author: String,
pub is_bugfix: bool,
pub lines_added: u32,
pub lines_deleted: u32,
pub old_path: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct HistoryOptions {
pub max_commits: usize,
pub since_timestamp: u64,
pub until_timestamp: u64,
pub path_filter: Vec<PathBuf>,
}
impl Default for HistoryOptions {
fn default() -> Self {
Self {
max_commits: 10000,
since_timestamp: 0,
until_timestamp: 0,
path_filter: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ChangeHistory {
by_path: HashMap<PathBuf, Vec<FileChange>>,
chronological: Vec<FileChange>,
commits: HashMap<String, Vec<FileChange>>,
}
impl ChangeHistory {
pub fn new() -> Self {
Self::default()
}
pub fn add_change(&mut self, change: FileChange) {
self.by_path
.entry(change.path.clone())
.or_default()
.push(change.clone());
self.commits
.entry(change.commit_id.clone())
.or_default()
.push(change.clone());
self.chronological.push(change);
}
pub fn changes_for_path(&self, path: &Path) -> &[FileChange] {
self.by_path.get(path).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn files_in_commit(&self, commit_id: &str) -> &[FileChange] {
self.commits
.get(commit_id)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn change_count(&self, path: &Path) -> usize {
self.by_path.get(path).map(|v| v.len()).unwrap_or(0)
}
pub fn bugfix_count(&self, path: &Path) -> usize {
self.by_path
.get(path)
.map(|v| v.iter().filter(|c| c.is_bugfix).count())
.unwrap_or(0)
}
pub fn all_commits(&self) -> Vec<&str> {
self.commits.keys().map(|s| s.as_str()).collect()
}
pub fn chronological(&self) -> &[FileChange] {
&self.chronological
}
pub fn all_paths(&self) -> Vec<&Path> {
self.by_path.keys().map(|p| p.as_path()).collect()
}
pub fn authors_for_path(&self, path: &Path) -> Vec<String> {
let mut authors: Vec<String> = self
.by_path
.get(path)
.map(|v| v.iter().map(|c| c.author.clone()).collect())
.unwrap_or_default();
authors.sort();
authors.dedup();
authors
}
pub fn total_churn(&self, path: &Path) -> u64 {
self.by_path
.get(path)
.map(|v| {
v.iter()
.map(|c| c.lines_added as u64 + c.lines_deleted as u64)
.sum()
})
.unwrap_or(0)
}
pub fn total_changes(&self) -> usize {
self.chronological.len()
}
pub fn total_commits(&self) -> usize {
self.commits.len()
}
pub fn latest_timestamp(&self, path: &Path) -> u64 {
self.by_path
.get(path)
.and_then(|v| v.iter().map(|c| c.timestamp).max())
.unwrap_or(0)
}
pub fn oldest_timestamp(&self, path: &Path) -> u64 {
self.by_path
.get(path)
.and_then(|v| v.iter().map(|c| c.timestamp).min())
.unwrap_or(0)
}
}