use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use crate::git::types::FileDelta;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileReviewStatus {
Unreviewed,
Reviewed,
ChangedSinceReview,
New,
}
#[derive(Debug, Default)]
pub struct ReviewState {
files: HashMap<String, (FileReviewStatus, Option<u64>)>,
current_hashes: HashMap<String, u64>,
}
impl ReviewState {
pub fn mark_reviewed(&mut self, path: &str) {
let hash = self.current_hashes.get(path).copied();
self.files
.insert(path.to_string(), (FileReviewStatus::Reviewed, hash));
}
pub fn toggle_reviewed(&mut self, path: &str) {
match self.files.get(path).map(|(s, _)| *s) {
Some(FileReviewStatus::Reviewed) => {
self.files
.insert(path.to_string(), (FileReviewStatus::Unreviewed, None));
}
_ => {
self.mark_reviewed(path);
}
}
}
pub fn status(&self, path: &str) -> FileReviewStatus {
self.files
.get(path)
.map(|(s, _)| *s)
.unwrap_or(FileReviewStatus::Unreviewed)
}
pub fn on_diff_refresh(&mut self, new_hashes: HashMap<String, u64>) {
let first_load = self.current_hashes.is_empty();
if first_load {
for path in new_hashes.keys() {
self.files
.entry(path.clone())
.or_insert((FileReviewStatus::Unreviewed, None));
}
} else {
let old_paths: std::collections::HashSet<&String> =
self.current_hashes.keys().collect();
for (path, &new_hash) in &new_hashes {
if !old_paths.contains(path) {
self.files
.insert(path.clone(), (FileReviewStatus::New, None));
} else if let Some((
FileReviewStatus::Reviewed | FileReviewStatus::ChangedSinceReview,
reviewed_hash,
)) = self.files.get(path)
{
if *reviewed_hash != Some(new_hash) {
self.files.insert(
path.clone(),
(FileReviewStatus::ChangedSinceReview, *reviewed_hash),
);
} else {
self.files
.insert(path.clone(), (FileReviewStatus::Reviewed, *reviewed_hash));
}
}
}
let new_paths: std::collections::HashSet<&String> = new_hashes.keys().collect();
self.files.retain(|k, _| new_paths.contains(k));
}
self.current_hashes = new_hashes;
}
pub fn reset(&mut self) {
self.files.clear();
self.current_hashes.clear();
}
pub fn reviewed_count(&self) -> usize {
self.files
.values()
.filter(|(status, _)| matches!(status, FileReviewStatus::Reviewed))
.count()
}
}
pub fn hash_file_diff(delta: &FileDelta) -> u64 {
let mut hasher = DefaultHasher::new();
for hunk in &delta.hunks {
hunk.header.hash(&mut hasher);
for line in &hunk.lines {
line.content.hash(&mut hasher);
match line.origin {
crate::git::types::DiffLineOrigin::Context => 0u8.hash(&mut hasher),
crate::git::types::DiffLineOrigin::Addition => 1u8.hash(&mut hasher),
crate::git::types::DiffLineOrigin::Deletion => 2u8.hash(&mut hasher),
}
}
}
hasher.finish()
}
pub fn compute_diff_hashes(deltas: &[FileDelta]) -> HashMap<String, u64> {
deltas
.iter()
.map(|d| {
let path = d.path.to_string_lossy().to_string();
let hash = hash_file_diff(d);
(path, hash)
})
.collect()
}