1use std::collections::{HashMap, HashSet};
5
6use chrono::{DateTime, Utc};
7use serde::Serialize;
8
9use crate::{AnalysisRun, EffectiveCounts};
10
11#[derive(Debug, Serialize)]
12pub struct SummaryDelta {
13 pub baseline_run_id: String,
14 pub current_run_id: String,
15 pub baseline_timestamp: DateTime<Utc>,
16 pub current_timestamp: DateTime<Utc>,
17 pub baseline_files: u64,
18 pub current_files: u64,
19 pub files_analyzed_delta: i64,
20 pub baseline_code: u64,
21 pub current_code: u64,
22 pub code_lines_delta: i64,
23 pub baseline_comments: u64,
24 pub current_comments: u64,
25 pub comment_lines_delta: i64,
26 pub blank_lines_delta: i64,
27 pub total_lines_delta: i64,
28}
29
30#[derive(Debug, Serialize, PartialEq, Eq, Clone, Copy)]
31#[serde(rename_all = "snake_case")]
32pub enum FileChangeStatus {
33 Added,
34 Removed,
35 Modified,
36 Unchanged,
37}
38
39#[derive(Debug, Serialize)]
40pub struct FileDelta {
41 pub relative_path: String,
42 pub language: Option<String>,
43 pub status: FileChangeStatus,
44 pub baseline_code: i64,
45 pub current_code: i64,
46 pub code_delta: i64,
47 pub baseline_comment: i64,
48 pub current_comment: i64,
49 pub comment_delta: i64,
50 pub baseline_blank: i64,
51 pub current_blank: i64,
52 pub blank_delta: i64,
53 pub total_delta: i64,
54}
55
56#[derive(Debug, Serialize)]
57pub struct ScanComparison {
58 pub summary: SummaryDelta,
59 pub file_deltas: Vec<FileDelta>,
60 pub files_added: usize,
61 pub files_removed: usize,
62 pub files_modified: usize,
63 pub files_unchanged: usize,
64}
65
66#[must_use]
67#[allow(clippy::too_many_lines)]
68pub fn compute_delta(baseline: &AnalysisRun, current: &AnalysisRun) -> ScanComparison {
69 let baseline_map: HashMap<&str, &EffectiveCounts> = baseline
71 .per_file_records
72 .iter()
73 .map(|f| (f.relative_path.as_str(), &f.effective_counts))
74 .collect();
75
76 let current_paths: HashSet<&str> = current
77 .per_file_records
78 .iter()
79 .map(|f| f.relative_path.as_str())
80 .collect();
81
82 let mut file_deltas: Vec<FileDelta> = Vec::new();
83
84 for record in ¤t.per_file_records {
85 let path = record.relative_path.as_str();
86 let curr = &record.effective_counts;
87 let lang = record.language.map(|l| l.display_name().to_string());
88
89 if let Some(base) = baseline_map.get(path) {
90 let code_delta = curr.code_lines.cast_signed() - base.code_lines.cast_signed();
91 let comment_delta = curr.comment_lines.cast_signed() - base.comment_lines.cast_signed();
92 let blank_delta = curr.blank_lines.cast_signed() - base.blank_lines.cast_signed();
93 let status = if code_delta == 0 && comment_delta == 0 && blank_delta == 0 {
94 FileChangeStatus::Unchanged
95 } else {
96 FileChangeStatus::Modified
97 };
98 file_deltas.push(FileDelta {
99 relative_path: record.relative_path.clone(),
100 language: lang,
101 status,
102 baseline_code: base.code_lines.cast_signed(),
103 current_code: curr.code_lines.cast_signed(),
104 code_delta,
105 baseline_comment: base.comment_lines.cast_signed(),
106 current_comment: curr.comment_lines.cast_signed(),
107 comment_delta,
108 baseline_blank: base.blank_lines.cast_signed(),
109 current_blank: curr.blank_lines.cast_signed(),
110 blank_delta,
111 total_delta: code_delta + comment_delta + blank_delta,
112 });
113 } else {
114 let total = (curr.code_lines + curr.comment_lines + curr.blank_lines).cast_signed();
115 file_deltas.push(FileDelta {
116 relative_path: record.relative_path.clone(),
117 language: lang,
118 status: FileChangeStatus::Added,
119 baseline_code: 0,
120 current_code: curr.code_lines.cast_signed(),
121 code_delta: curr.code_lines.cast_signed(),
122 baseline_comment: 0,
123 current_comment: curr.comment_lines.cast_signed(),
124 comment_delta: curr.comment_lines.cast_signed(),
125 baseline_blank: 0,
126 current_blank: curr.blank_lines.cast_signed(),
127 blank_delta: curr.blank_lines.cast_signed(),
128 total_delta: total,
129 });
130 }
131 }
132
133 for record in &baseline.per_file_records {
134 if !current_paths.contains(record.relative_path.as_str()) {
135 let base = &record.effective_counts;
136 let lang = record.language.map(|l| l.display_name().to_string());
137 let total = (base.code_lines + base.comment_lines + base.blank_lines).cast_signed();
138 file_deltas.push(FileDelta {
139 relative_path: record.relative_path.clone(),
140 language: lang,
141 status: FileChangeStatus::Removed,
142 baseline_code: base.code_lines.cast_signed(),
143 current_code: 0,
144 code_delta: -(base.code_lines.cast_signed()),
145 baseline_comment: base.comment_lines.cast_signed(),
146 current_comment: 0,
147 comment_delta: -(base.comment_lines.cast_signed()),
148 baseline_blank: base.blank_lines.cast_signed(),
149 current_blank: 0,
150 blank_delta: -(base.blank_lines.cast_signed()),
151 total_delta: -total,
152 });
153 }
154 }
155
156 file_deltas.sort_by(|a, b| {
157 const fn order(s: FileChangeStatus) -> u8 {
158 match s {
159 FileChangeStatus::Modified => 0,
160 FileChangeStatus::Added => 1,
161 FileChangeStatus::Removed => 2,
162 FileChangeStatus::Unchanged => 3,
163 }
164 }
165 order(a.status)
166 .cmp(&order(b.status))
167 .then(a.relative_path.cmp(&b.relative_path))
168 });
169
170 let files_added = file_deltas
171 .iter()
172 .filter(|f| f.status == FileChangeStatus::Added)
173 .count();
174 let files_removed = file_deltas
175 .iter()
176 .filter(|f| f.status == FileChangeStatus::Removed)
177 .count();
178 let files_modified = file_deltas
179 .iter()
180 .filter(|f| f.status == FileChangeStatus::Modified)
181 .count();
182 let files_unchanged = file_deltas
183 .iter()
184 .filter(|f| f.status == FileChangeStatus::Unchanged)
185 .count();
186
187 let s = ¤t.summary_totals;
188 let b = &baseline.summary_totals;
189
190 ScanComparison {
191 summary: SummaryDelta {
192 baseline_run_id: baseline.tool.run_id.clone(),
193 current_run_id: current.tool.run_id.clone(),
194 baseline_timestamp: baseline.tool.timestamp_utc,
195 current_timestamp: current.tool.timestamp_utc,
196 baseline_files: b.files_analyzed,
197 current_files: s.files_analyzed,
198 files_analyzed_delta: s.files_analyzed.cast_signed() - b.files_analyzed.cast_signed(),
199 baseline_code: b.code_lines,
200 current_code: s.code_lines,
201 code_lines_delta: s.code_lines.cast_signed() - b.code_lines.cast_signed(),
202 baseline_comments: b.comment_lines,
203 current_comments: s.comment_lines,
204 comment_lines_delta: s.comment_lines.cast_signed() - b.comment_lines.cast_signed(),
205 blank_lines_delta: s.blank_lines.cast_signed() - b.blank_lines.cast_signed(),
206 total_lines_delta: s
207 .total_physical_lines
208 .cast_signed()
209 .wrapping_sub(b.total_physical_lines.cast_signed()),
210 },
211 file_deltas,
212 files_added,
213 files_removed,
214 files_modified,
215 files_unchanged,
216 }
217}