differential_engine/
review_state.rs1use std::collections::BTreeSet;
11
12use serde::{Deserialize, Serialize};
13use sha1::{Digest, Sha1};
14
15use crate::schema;
16
17use crate::model::DiffView;
18
19pub use crate::plan::{class_content_key, review_id};
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct ReviewState {
26 #[serde(default)]
28 pub reviewed_classes: BTreeSet<String>,
29 #[serde(default)]
32 pub cursor: Option<(String, usize)>,
33 #[serde(default)]
35 pub split_diff: bool,
36 #[serde(default)]
38 pub file_view: bool,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum FindingStatus {
44 Open,
45 Resolved,
46 Orphaned,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Anchor {
51 pub file: String,
52 pub side: String,
54 pub line: u32,
55 pub hunk_digest: String,
56 #[serde(default)]
58 pub line_text: String,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Finding {
63 pub id: String,
64 pub created: u64,
66 pub body: String,
67 pub status: FindingStatus,
68 #[serde(default)]
70 pub moved: bool,
71 pub plan_hash: String,
72 pub anchor: Anchor,
73}
74
75impl Finding {
76 pub fn new(created: u64, body: String, plan_hash: String, anchor: Anchor) -> Self {
77 let mut h = Sha1::new();
78 h.update(anchor.hunk_digest.as_bytes());
79 h.update(created.to_le_bytes());
80 h.update(body.as_bytes());
81 Finding {
82 id: hex::encode(h.finalize())[..12].to_string(),
83 created,
84 body,
85 status: FindingStatus::Open,
86 moved: false,
87 plan_hash,
88 anchor,
89 }
90 }
91}
92
93pub fn now_unix() -> u64 {
101 std::time::SystemTime::now()
102 .duration_since(std::time::UNIX_EPOCH)
103 .map(|d| d.as_secs())
104 .unwrap_or(0)
105}
106
107pub fn reanchor(
112 findings: &mut [Finding],
113 doc: &schema::PlanDocument,
114 view: &DiffView,
115 plan_hash: &str,
116) {
117 for f in findings.iter_mut() {
118 if f.plan_hash == plan_hash {
119 continue; }
121 if let Some((idx, h)) = doc
123 .hunks
124 .iter()
125 .enumerate()
126 .find(|(_, h)| h.digest == f.anchor.hunk_digest)
127 {
128 let _ = idx;
129 f.anchor.file = h.file.clone();
130 f.anchor.line = if f.anchor.side == "old" {
131 h.old_start.max(1)
132 } else {
133 h.new_start.max(1)
134 };
135 f.plan_hash = plan_hash.to_string();
136 f.moved = false;
137 if f.status == FindingStatus::Orphaned {
138 f.status = FindingStatus::Open;
139 }
140 continue;
141 }
142 let text = f.anchor.line_text.as_bytes();
144 let matched = (!text.is_empty())
145 .then(|| {
146 view.hunks.iter().enumerate().find(|(_, h)| {
147 let file = view.file_of(h);
148 file.path == f.anchor.file.as_bytes()
149 && (h.added.iter().any(|l| l == text)
150 || h.removed.iter().any(|l| l == text))
151 })
152 })
153 .flatten();
154 if let Some((hi, h)) = matched {
155 f.anchor.hunk_digest = doc.hunks[hi].digest.clone();
156 f.anchor.line = if f.anchor.side == "old" {
157 h.old_start.max(1)
158 } else {
159 h.new_start.max(1)
160 };
161 f.plan_hash = plan_hash.to_string();
162 f.moved = true;
163 if f.status == FindingStatus::Orphaned {
164 f.status = FindingStatus::Open;
165 }
166 } else {
167 f.status = FindingStatus::Orphaned;
168 }
169 }
170}