use crate::changes::{ChangesSummary, FileChangeType, PackageChanges};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnalysisMode {
WorkingDirectory,
CommitRange,
SingleCommit,
CommitList,
}
impl AnalysisMode {
#[must_use]
pub fn is_working_directory(&self) -> bool {
matches!(self, Self::WorkingDirectory)
}
#[must_use]
pub fn is_commit_range(&self) -> bool {
matches!(self, Self::CommitRange)
}
#[must_use]
pub fn is_single_commit(&self) -> bool {
matches!(self, Self::SingleCommit)
}
#[must_use]
pub fn is_commit_list(&self) -> bool {
matches!(self, Self::CommitList)
}
#[must_use]
pub fn has_commits(&self) -> bool {
!matches!(self, Self::WorkingDirectory)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangesReport {
pub analyzed_at: DateTime<Utc>,
pub analysis_mode: AnalysisMode,
pub base_ref: Option<String>,
pub head_ref: Option<String>,
pub packages: Vec<PackageChanges>,
pub summary: ChangesSummary,
pub is_monorepo: bool,
}
impl ChangesReport {
#[must_use]
pub fn new(analysis_mode: AnalysisMode, is_monorepo: bool) -> Self {
Self {
analyzed_at: Utc::now(),
analysis_mode,
base_ref: None,
head_ref: None,
packages: Vec::new(),
summary: ChangesSummary::new(),
is_monorepo,
}
}
#[must_use]
pub fn new_for_range(base_ref: &str, head_ref: &str, is_monorepo: bool) -> Self {
Self {
analyzed_at: Utc::now(),
analysis_mode: AnalysisMode::CommitRange,
base_ref: Some(base_ref.to_string()),
head_ref: Some(head_ref.to_string()),
packages: Vec::new(),
summary: ChangesSummary::new(),
is_monorepo,
}
}
#[must_use]
pub fn get_package(&self, name: &str) -> Option<&PackageChanges> {
self.packages.iter().find(|p| p.package_name() == name)
}
#[must_use]
pub fn packages_with_changes(&self) -> Vec<&PackageChanges> {
self.packages.iter().filter(|p| p.has_changes).collect()
}
#[must_use]
pub fn packages_without_changes(&self) -> Vec<&PackageChanges> {
self.packages.iter().filter(|p| !p.has_changes).collect()
}
#[must_use]
pub fn filter_by_change_type(&self, change_type: FileChangeType) -> Vec<&PackageChanges> {
self.packages
.iter()
.filter(|p| p.files.iter().any(|f| f.change_type == change_type))
.collect()
}
#[must_use]
pub fn packages_with_package_json_changes(&self) -> Vec<&PackageChanges> {
self.packages.iter().filter(|p| p.package_json_modified()).collect()
}
#[must_use]
pub fn has_changes(&self) -> bool {
self.summary.has_changes()
}
#[must_use]
pub fn total_files_changed(&self) -> usize {
self.summary.total_files_changed
}
#[must_use]
pub fn total_commits(&self) -> usize {
self.summary.total_commits
}
pub fn add_package(&mut self, package: PackageChanges) {
self.summary.total_packages += 1;
if package.has_changes {
self.summary.packages_with_changes += 1;
} else {
self.summary.packages_without_changes += 1;
}
self.summary.total_files_changed += package.stats.files_changed;
self.summary.total_lines_added += package.stats.lines_added;
self.summary.total_lines_deleted += package.stats.lines_deleted;
self.summary.total_commits += package.stats.commits;
self.packages.push(package);
}
pub fn recalculate_summary(&mut self) {
let mut summary = ChangesSummary::new();
summary.total_packages = self.packages.len();
for package in &self.packages {
if package.has_changes {
summary.packages_with_changes += 1;
} else {
summary.packages_without_changes += 1;
}
summary.total_files_changed += package.stats.files_changed;
summary.total_lines_added += package.stats.lines_added;
summary.total_lines_deleted += package.stats.lines_deleted;
summary.total_commits += package.stats.commits;
}
self.summary = summary;
}
}