differential_engine/
review_session.rs1use std::collections::{HashMap, HashSet};
10use std::path::PathBuf;
11
12use differential_schema as schema;
13
14use crate::EngineError;
15use crate::gitio::Repo;
16use crate::model::DiffView;
17use crate::review_state::{Anchor, Finding, ReviewState, ReviewStore, class_content_key, reanchor};
18
19pub struct ReviewSession {
20 store: ReviewStore,
21 doc: schema::PlanDocument,
22 view: DiffView,
23 plan_hash: String,
24 class_key: HashMap<String, String>,
26 hunk_key: HashMap<usize, String>,
28 state: ReviewState,
29 findings: Vec<Finding>,
30}
31
32impl ReviewSession {
33 pub fn open(
40 repo: &Repo,
41 review_base: &str,
42 head_spec: &str,
43 doc: schema::PlanDocument,
44 view: DiffView,
45 ) -> Result<Self, EngineError> {
46 let store = ReviewStore::open(repo, review_base, head_spec)?;
47 Self::from_store(store, doc, view)
48 }
49
50 pub fn open_at(
52 dir: PathBuf,
53 doc: schema::PlanDocument,
54 view: DiffView,
55 ) -> Result<Self, EngineError> {
56 Self::from_store(ReviewStore::open_at(dir)?, doc, view)
57 }
58
59 fn from_store(
60 store: ReviewStore,
61 doc: schema::PlanDocument,
62 view: DiffView,
63 ) -> Result<Self, EngineError> {
64 let plan_hash = store.save_plan(&doc)?;
65 let mut findings = store.load_findings()?;
66 reanchor(&mut findings, &doc, &view, &plan_hash);
67 store.save_findings(&findings)?;
68 let state = store.load_state()?;
69
70 let class_by_id: HashMap<&str, &schema::ClassEntry> =
71 doc.classes.iter().map(|c| (c.id.as_str(), c)).collect();
72 let mut class_key = HashMap::new();
73 let mut hunk_key = HashMap::new();
74 for c in class_by_id.values() {
75 let digests: Vec<String> = c
76 .hunk_ids
77 .iter()
78 .map(|hid| doc.hunks[hunk_index(hid)].digest.clone())
79 .collect();
80 let key = class_content_key(&digests);
81 for hid in &c.hunk_ids {
82 hunk_key.insert(hunk_index(hid), key.clone());
83 }
84 class_key.insert(c.id.clone(), key);
85 }
86
87 Ok(ReviewSession {
88 store,
89 doc,
90 view,
91 plan_hash,
92 class_key,
93 hunk_key,
94 state,
95 findings,
96 })
97 }
98
99 pub fn doc(&self) -> &schema::PlanDocument {
102 &self.doc
103 }
104
105 pub fn view(&self) -> &DiffView {
106 &self.view
107 }
108
109 pub fn plan_hash(&self) -> &str {
110 &self.plan_hash
111 }
112
113 pub fn findings(&self) -> &[Finding] {
114 &self.findings
115 }
116
117 pub fn class_key(&self, class_id: &str) -> &str {
119 &self.class_key[class_id]
120 }
121
122 pub fn is_reviewed(&self, class_key: &str) -> bool {
123 self.state.reviewed_classes.contains(class_key)
124 }
125
126 pub fn reviewed_count(&self) -> usize {
127 self.state.reviewed_classes.len()
128 }
129
130 pub fn reviewed_hunks(&self) -> HashSet<usize> {
133 self.hunk_key
134 .iter()
135 .filter(|(_, key)| self.state.reviewed_classes.contains(*key))
136 .map(|(hi, _)| *hi)
137 .collect()
138 }
139
140 pub fn cursor(&self) -> Option<&(String, usize)> {
141 self.state.cursor.as_ref()
142 }
143
144 pub fn split_diff(&self) -> bool {
145 self.state.split_diff
146 }
147
148 pub fn file_view(&self) -> bool {
149 self.state.file_view
150 }
151
152 pub fn toggle_reviewed(&mut self, hunk: usize) -> Result<bool, EngineError> {
157 let key = self.hunk_key[&hunk].clone();
158 let now = self.state.reviewed_classes.insert(key.clone());
159 if !now {
160 self.state.reviewed_classes.remove(&key);
161 }
162 self.store.save_state(&self.state)?;
163 Ok(now)
164 }
165
166 pub fn save_cursor(&mut self, id: String, row: usize) -> Result<(), EngineError> {
168 self.state.cursor = Some((id, row));
169 self.store.save_state(&self.state)
170 }
171
172 pub fn set_split_diff(&mut self, on: bool) -> Result<(), EngineError> {
174 self.state.split_diff = on;
175 self.store.save_state(&self.state)
176 }
177
178 pub fn set_file_view(&mut self, on: bool) -> Result<(), EngineError> {
180 self.state.file_view = on;
181 self.store.save_state(&self.state)
182 }
183
184 pub fn add_finding(&mut self, hunk: usize, body: String) -> Result<&Finding, EngineError> {
186 let h = &self.doc.hunks[hunk];
187 let side = if h.new_count > 0 { "new" } else { "old" };
188 let line = if h.new_count > 0 {
189 h.new_start.max(1)
190 } else {
191 h.old_start.max(1)
192 };
193 let vh = &self.view.hunks[hunk];
194 let line_text = vh
195 .added
196 .first()
197 .or(vh.removed.first())
198 .map(|l| String::from_utf8_lossy(l).into_owned())
199 .unwrap_or_default();
200 let finding = Finding::new(
201 body,
202 self.plan_hash.clone(),
203 Anchor {
204 file: h.file.clone(),
205 side: side.into(),
206 line,
207 hunk_digest: h.digest.clone(),
208 line_text,
209 },
210 );
211 self.findings.push(finding);
212 self.store.save_findings(&self.findings)?;
213 Ok(self.findings.last().expect("just pushed"))
214 }
215
216 pub fn delete_finding(&mut self, id: &str) -> Result<bool, EngineError> {
218 let before = self.findings.len();
219 self.findings.retain(|f| f.id != id);
220 if self.findings.len() == before {
221 return Ok(false);
222 }
223 self.store.save_findings(&self.findings)?;
224 Ok(true)
225 }
226}
227
228fn hunk_index(hid: &str) -> usize {
230 hid[1..].parse().expect("hunk id of the form h<N>")
231}