LCOV - code coverage report
Current view: top level - src - diff.rs (source / functions) Coverage Total Hit
Test: Hunky Coverage Lines: 100.0 % 108 108
Test Date: 2026-02-20 16:10:39 Functions: 100.0 % 16 16

            Line data    Source code
       1              : use std::collections::HashSet;
       2              : use std::hash::{Hash, Hasher};
       3              : use std::path::PathBuf;
       4              : use std::time::SystemTime;
       5              : 
       6              : #[derive(Debug, Clone)]
       7              : pub struct DiffSnapshot {
       8              :     #[allow(dead_code)]
       9              :     pub timestamp: SystemTime,
      10              :     pub files: Vec<FileChange>,
      11              : }
      12              : 
      13              : #[derive(Debug, Clone)]
      14              : pub struct FileChange {
      15              :     pub path: PathBuf,
      16              :     pub status: String,
      17              :     pub hunks: Vec<Hunk>,
      18              : }
      19              : 
      20              : #[derive(Debug, Clone)]
      21              : pub struct Hunk {
      22              :     pub old_start: usize,
      23              :     pub new_start: usize,
      24              :     pub lines: Vec<String>,
      25              :     pub seen: bool,
      26              :     pub staged: bool,
      27              :     /// Track which individual lines are staged (by index in lines vec)
      28              :     pub staged_line_indices: HashSet<usize>,
      29              :     pub id: HunkId,
      30              : }
      31              : 
      32              : impl Hunk {
      33              :     #[allow(dead_code)]
      34            1 :     pub fn format(&self) -> String {
      35            1 :         self.lines.join("")
      36            1 :     }
      37              :     
      38            1 :     pub fn count_changes(&self) -> usize {
      39            1 :         let mut add_lines = 0;
      40            1 :         let mut remove_lines = 0;
      41              :         
      42            3 :         for line in &self.lines {
      43            3 :             if line.starts_with('+') && !line.starts_with("+++") {
      44            2 :                 add_lines += 1;
      45            2 :             } else if line.starts_with('-') && !line.starts_with("---") {
      46            1 :                 remove_lines += 1;
      47            1 :             }
      48              :         }
      49              :         
      50              :         // Count pairs of add/remove as 1 change, plus any unpaired lines
      51            1 :         let pairs = add_lines.min(remove_lines);
      52            1 :         let unpaired = (add_lines + remove_lines) - (2 * pairs);
      53            1 :         pairs + unpaired
      54            1 :     }
      55              :     
      56           19 :     pub fn new(old_start: usize, new_start: usize, lines: Vec<String>, file_path: &PathBuf) -> Self {
      57           19 :         let id = HunkId::new(file_path, old_start, new_start, &lines);
      58           19 :         Self {
      59           19 :             old_start,
      60           19 :             new_start,
      61           19 :             lines,
      62           19 :             seen: false,
      63           19 :             staged: false,
      64           19 :             staged_line_indices: HashSet::new(),
      65           19 :             id,
      66           19 :         }
      67           19 :     }
      68              : }
      69              : 
      70              : /// Unique identifier for a hunk based on file path, line numbers, and content hash
      71              : #[derive(Debug, Clone, PartialEq, Eq, Hash)]
      72              : pub struct HunkId {
      73              :     pub file_path: PathBuf,
      74              :     pub old_start: usize,
      75              :     pub new_start: usize,
      76              :     pub content_hash: u64,
      77              : }
      78              : 
      79              : impl HunkId {
      80           23 :     pub fn new(file_path: &PathBuf, old_start: usize, new_start: usize, lines: &[String]) -> Self {
      81              :         use std::collections::hash_map::DefaultHasher;
      82              :         
      83           23 :         let mut hasher = DefaultHasher::new();
      84           55 :         for line in lines {
      85           55 :             line.hash(&mut hasher);
      86           55 :         }
      87           23 :         let content_hash = hasher.finish();
      88              :         
      89           23 :         Self {
      90           23 :             file_path: file_path.clone(),
      91           23 :             old_start,
      92           23 :             new_start,
      93           23 :             content_hash,
      94           23 :         }
      95           23 :     }
      96              : }
      97              : 
      98              : /// Tracks which hunks have been seen by the user
      99              : #[derive(Debug, Clone)]
     100              : pub struct SeenTracker {
     101              :     seen_hunks: HashSet<HunkId>,
     102              : }
     103              : 
     104              : impl SeenTracker {
     105            2 :     pub fn new() -> Self {
     106            2 :         Self {
     107            2 :             seen_hunks: HashSet::new(),
     108            2 :         }
     109            2 :     }
     110              :     
     111            3 :     pub fn mark_seen(&mut self, hunk_id: &HunkId) {
     112            3 :         self.seen_hunks.insert(hunk_id.clone());
     113            3 :     }
     114              :     
     115            6 :     pub fn is_seen(&self, hunk_id: &HunkId) -> bool {
     116            6 :         self.seen_hunks.contains(hunk_id)
     117            6 :     }
     118              :     
     119            1 :     pub fn clear(&mut self) {
     120            1 :         self.seen_hunks.clear();
     121            1 :     }
     122              :     
     123              :     #[allow(dead_code)]
     124            1 :     pub fn remove_file_hunks(&mut self, file_path: &PathBuf) {
     125            1 :         self.seen_hunks.retain(|hunk_id| &hunk_id.file_path != file_path);
     126            1 :     }
     127              : }
     128              : 
     129              : impl Default for SeenTracker {
     130            1 :     fn default() -> Self {
     131            1 :         Self::new()
     132            1 :     }
     133              : }
     134              : 
     135              : #[cfg(test)]
     136              : mod tests {
     137              :     use super::*;
     138              : 
     139              :     #[test]
     140            1 :     fn count_changes_pairs_adds_and_removes() {
     141            1 :         let file_path = PathBuf::from("src/main.rs");
     142            1 :         let hunk = Hunk::new(
     143              :             1,
     144              :             1,
     145            1 :             vec![
     146            1 :                 "-old line\n".to_string(),
     147            1 :                 "+new line\n".to_string(),
     148            1 :                 "+extra line\n".to_string(),
     149              :             ],
     150            1 :             &file_path,
     151              :         );
     152              : 
     153            1 :         assert_eq!(hunk.count_changes(), 2);
     154            1 :     }
     155              : 
     156              :     #[test]
     157            1 :     fn hunk_id_changes_when_content_changes() {
     158            1 :         let file_path = PathBuf::from("src/main.rs");
     159            1 :         let base = HunkId::new(&file_path, 10, 10, &["-a\n".to_string(), "+b\n".to_string()]);
     160            1 :         let changed =
     161            1 :             HunkId::new(&file_path, 10, 10, &["-a\n".to_string(), "+c\n".to_string()]);
     162              : 
     163            1 :         assert_ne!(base, changed);
     164            1 :     }
     165              : 
     166              :     #[test]
     167            1 :     fn seen_tracker_marks_and_clears_hunks() {
     168            1 :         let file_path = PathBuf::from("src/lib.rs");
     169            1 :         let hunk_id = HunkId::new(&file_path, 3, 3, &["+line\n".to_string()]);
     170            1 :         let mut tracker = SeenTracker::new();
     171              : 
     172            1 :         assert!(!tracker.is_seen(&hunk_id));
     173            1 :         tracker.mark_seen(&hunk_id);
     174            1 :         assert!(tracker.is_seen(&hunk_id));
     175              : 
     176            1 :         tracker.remove_file_hunks(&file_path);
     177            1 :         assert!(!tracker.is_seen(&hunk_id));
     178              : 
     179            1 :         tracker.mark_seen(&hunk_id);
     180            1 :         tracker.clear();
     181            1 :         assert!(!tracker.is_seen(&hunk_id));
     182            1 :     }
     183              : 
     184              :     #[test]
     185            1 :     fn hunk_format_and_constructor_defaults() {
     186            1 :         let file_path = PathBuf::from("src/main.rs");
     187            1 :         let lines = vec![" context\n".to_string(), "+added\n".to_string()];
     188            1 :         let hunk = Hunk::new(4, 7, lines.clone(), &file_path);
     189              : 
     190            1 :         assert_eq!(hunk.format(), lines.concat());
     191            1 :         assert!(!hunk.seen);
     192            1 :         assert!(!hunk.staged);
     193            1 :         assert!(hunk.staged_line_indices.is_empty());
     194            1 :     }
     195              : 
     196              :     #[test]
     197            1 :     fn seen_tracker_default_is_empty() {
     198            1 :         let file_path = PathBuf::from("src/default.rs");
     199            1 :         let hunk_id = HunkId::new(&file_path, 1, 1, &["+x\n".to_string()]);
     200            1 :         let mut tracker = SeenTracker::default();
     201            1 :         assert!(!tracker.is_seen(&hunk_id));
     202              : 
     203            1 :         tracker.mark_seen(&hunk_id);
     204            1 :         assert!(tracker.is_seen(&hunk_id));
     205            1 :     }
     206              : }
        

Generated by: LCOV version 2.0-1