Line data Source code
1 : use std::collections::HashSet;
2 : use std::hash::{Hash, Hasher};
3 : use std::path::{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 : /// Metadata about a git commit for the review mode commit picker
14 : #[derive(Debug, Clone)]
15 : pub struct CommitInfo {
16 : pub sha: String,
17 : pub short_sha: String,
18 : pub summary: String,
19 : pub author: String,
20 : }
21 :
22 : #[derive(Debug, Clone)]
23 : pub struct FileChange {
24 : pub path: PathBuf,
25 : pub status: String,
26 : pub hunks: Vec<Hunk>,
27 : }
28 :
29 : #[derive(Debug, Clone)]
30 : pub struct Hunk {
31 : pub old_start: usize,
32 : pub new_start: usize,
33 : pub lines: Vec<String>,
34 : pub seen: bool,
35 : pub staged: bool,
36 : /// Track which individual lines are staged (by index in lines vec)
37 : pub staged_line_indices: HashSet<usize>,
38 : /// In-memory tracking for review mode: whether this hunk has been accepted
39 : pub accepted: bool,
40 : #[allow(dead_code)]
41 : pub id: HunkId,
42 : }
43 :
44 : impl Hunk {
45 : #[allow(dead_code)]
46 2 : pub fn format(&self) -> String {
47 2 : self.lines.join("")
48 2 : }
49 :
50 2 : pub fn count_changes(&self) -> usize {
51 2 : let mut add_lines = 0;
52 2 : let mut remove_lines = 0;
53 :
54 6 : for line in &self.lines {
55 6 : if line.starts_with('+') && !line.starts_with("+++") {
56 4 : add_lines += 1;
57 4 : } else if line.starts_with('-') && !line.starts_with("---") {
58 2 : remove_lines += 1;
59 2 : }
60 : }
61 :
62 : // Count pairs of add/remove as 1 change, plus any unpaired lines
63 2 : let pairs = add_lines.min(remove_lines);
64 2 : let unpaired = (add_lines + remove_lines) - (2 * pairs);
65 2 : pairs + unpaired
66 2 : }
67 :
68 67 : pub fn new(old_start: usize, new_start: usize, lines: Vec<String>, file_path: &Path) -> Self {
69 67 : let id = HunkId::new(file_path, old_start, new_start, &lines);
70 67 : Self {
71 67 : old_start,
72 67 : new_start,
73 67 : lines,
74 67 : seen: false,
75 67 : staged: false,
76 67 : staged_line_indices: HashSet::new(),
77 67 : accepted: false,
78 67 : id,
79 67 : }
80 67 : }
81 : }
82 :
83 : /// Unique identifier for a hunk based on file path, line numbers, and content hash
84 : #[derive(Debug, Clone, PartialEq, Eq, Hash)]
85 : pub struct HunkId {
86 : pub file_path: PathBuf,
87 : pub old_start: usize,
88 : pub new_start: usize,
89 : pub content_hash: u64,
90 : }
91 :
92 : impl HunkId {
93 75 : pub fn new(file_path: &Path, old_start: usize, new_start: usize, lines: &[String]) -> Self {
94 : use std::collections::hash_map::DefaultHasher;
95 :
96 75 : let mut hasher = DefaultHasher::new();
97 247 : for line in lines {
98 247 : line.hash(&mut hasher);
99 247 : }
100 75 : let content_hash = hasher.finish();
101 :
102 75 : Self {
103 75 : file_path: file_path.to_path_buf(),
104 75 : old_start,
105 75 : new_start,
106 75 : content_hash,
107 75 : }
108 75 : }
109 : }
110 :
111 : /// Tracks which hunks have been seen by the user
112 : #[derive(Debug, Clone)]
113 : pub struct SeenTracker {
114 : seen_hunks: HashSet<HunkId>,
115 : }
116 :
117 : #[allow(dead_code)]
118 : impl SeenTracker {
119 4 : pub fn new() -> Self {
120 4 : Self {
121 4 : seen_hunks: HashSet::new(),
122 4 : }
123 4 : }
124 :
125 6 : pub fn mark_seen(&mut self, hunk_id: &HunkId) {
126 6 : self.seen_hunks.insert(hunk_id.clone());
127 6 : }
128 :
129 12 : pub fn is_seen(&self, hunk_id: &HunkId) -> bool {
130 12 : self.seen_hunks.contains(hunk_id)
131 12 : }
132 :
133 2 : pub fn clear(&mut self) {
134 2 : self.seen_hunks.clear();
135 2 : }
136 :
137 : #[allow(dead_code)]
138 2 : pub fn remove_file_hunks(&mut self, file_path: &PathBuf) {
139 2 : self.seen_hunks
140 2 : .retain(|hunk_id| &hunk_id.file_path != file_path);
141 2 : }
142 : }
143 :
144 : impl Default for SeenTracker {
145 2 : fn default() -> Self {
146 2 : Self::new()
147 2 : }
148 : }
149 :
150 : #[cfg(test)]
151 : #[path = "../tests/diff.rs"]
152 : mod tests;
|