1use tracing::warn;
4
5use crate::error::DiffError;
6use crate::port::{DiffStore, VcsProvider};
7
8pub fn resolve_digest_reference(
10 vcs: &dyn VcsProvider,
11 db: &dyn DiffStore,
12) -> Result<String, DiffError> {
13 if let Ok(Some(sha)) = db.config_get("last_session_sha") {
14 if vcs.resolve_ref(&sha)? {
15 return Ok(sha);
16 }
17 warn!("last_session_sha '{}' not found in repo, falling back", sha);
18 }
19
20 if vcs.resolve_ref("HEAD~5")? {
22 return Ok("HEAD~5".to_string());
23 }
24
25 if let Some(root_sha) = vcs.root_commit_sha()? {
27 return Ok(root_sha);
28 }
29
30 Ok("HEAD".to_string())
31}
32
33pub fn update_session_anchor(vcs: &dyn VcsProvider, db: &dyn DiffStore) -> Result<(), DiffError> {
35 let sha = vcs.head_sha()?;
36 db.config_set("last_session_sha", &sha)?;
37 Ok(())
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use crate::error::{DbError, DiffError};
44 use crate::model::{Config, ImpactEntry, SymbolResult, SymbolWithParent, VcsFileChange};
45 use crate::{ConfigStore, SymbolLookup};
46 use std::cell::RefCell;
47 use std::collections::HashMap;
48 struct MockVcs {
51 resolve_results: HashMap<String, bool>,
52 root_sha: Option<String>,
53 head: String,
54 }
55
56 impl MockVcs {
57 fn new() -> Self {
58 Self {
59 resolve_results: HashMap::new(),
60 root_sha: None,
61 head: "abc123".to_string(),
62 }
63 }
64 }
65
66 impl VcsProvider for MockVcs {
67 fn working_tree_changes(
68 &self,
69 _base_ref: Option<&str>,
70 ) -> Result<Vec<VcsFileChange>, DiffError> {
71 Ok(vec![])
72 }
73 fn changes_between_refs(
74 &self,
75 _from: &str,
76 _to: &str,
77 ) -> Result<Vec<VcsFileChange>, DiffError> {
78 Ok(vec![])
79 }
80 fn read_file_at_ref(&self, _path: &str, _ref: &str) -> Result<Option<String>, DiffError> {
81 Ok(None)
82 }
83 fn commit_count(&self, _from: &str, _to: &str) -> Result<usize, DiffError> {
84 Ok(0)
85 }
86 fn resolve_ref(&self, refspec: &str) -> Result<bool, DiffError> {
87 Ok(*self.resolve_results.get(refspec).unwrap_or(&false))
88 }
89 fn head_sha(&self) -> Result<String, DiffError> {
90 Ok(self.head.clone())
91 }
92 fn root_commit_sha(&self) -> Result<Option<String>, DiffError> {
93 Ok(self.root_sha.clone())
94 }
95 }
96
97 struct MockDiffStore {
100 configs: RefCell<HashMap<String, String>>,
101 }
102
103 impl MockDiffStore {
104 fn new() -> Self {
105 Self {
106 configs: RefCell::new(HashMap::new()),
107 }
108 }
109 fn with_config(key: &str, value: &str) -> Self {
110 let mut configs = HashMap::new();
111 configs.insert(key.to_string(), value.to_string());
112 Self {
113 configs: RefCell::new(configs),
114 }
115 }
116 }
117
118 impl ConfigStore for MockDiffStore {
119 fn config_get(&self, key: &str) -> Result<Option<String>, DbError> {
120 Ok(self.configs.borrow().get(key).cloned())
121 }
122 fn config_set(&self, key: &str, value: &str) -> Result<(), DbError> {
123 self.configs
124 .borrow_mut()
125 .insert(key.to_string(), value.to_string());
126 Ok(())
127 }
128 fn config_list(&self) -> Result<Vec<Config>, DbError> {
129 Ok(vec![])
130 }
131 }
132
133 impl SymbolLookup for MockDiffStore {
134 fn get_file_id(&self, _path: &str) -> Result<Option<i64>, DbError> {
135 Ok(None)
136 }
137 fn get_symbol_by_id(&self, _id: i64) -> Result<Option<crate::Symbol>, DbError> {
138 Ok(None)
139 }
140 fn search_symbols_by_name(
141 &self,
142 _name: &str,
143 _limit: i64,
144 ) -> Result<Vec<SymbolResult>, DbError> {
145 Ok(vec![])
146 }
147 fn get_symbols_for_file(&self, _file_id: i64) -> Result<Vec<crate::Symbol>, DbError> {
148 Ok(vec![])
149 }
150 fn get_children(&self, _symbol_id: i64) -> Result<Vec<crate::Symbol>, DbError> {
151 Ok(vec![])
152 }
153 fn impact_analysis(&self, _id: i64, _depth: i64) -> Result<Vec<ImpactEntry>, DbError> {
154 Ok(vec![])
155 }
156 fn stats(&self) -> Result<(i64, i64, i64), DbError> {
157 Ok((0, 0, 0))
158 }
159 }
160
161 impl DiffStore for MockDiffStore {
162 fn symbols_in_line_range(
163 &self,
164 _path: &str,
165 _start: i64,
166 _end: i64,
167 ) -> Result<Vec<SymbolResult>, DbError> {
168 Ok(vec![])
169 }
170 fn all_symbols_for_file(&self, _path: &str) -> Result<Vec<SymbolWithParent>, DbError> {
171 Ok(vec![])
172 }
173 }
174
175 #[test]
176 fn test_resolve_uses_stored_session_sha() {
177 let mut vcs = MockVcs::new();
178 vcs.resolve_results.insert("sha_stored".to_string(), true);
179 let db = MockDiffStore::with_config("last_session_sha", "sha_stored");
180
181 let result = resolve_digest_reference(&vcs, &db).unwrap();
182 assert_eq!(result, "sha_stored");
183 }
184
185 #[test]
186 fn test_resolve_falls_back_when_stored_sha_invalid() {
187 let mut vcs = MockVcs::new();
188 vcs.resolve_results.insert("HEAD~5".to_string(), true);
190 let db = MockDiffStore::with_config("last_session_sha", "gone_sha");
191
192 let result = resolve_digest_reference(&vcs, &db).unwrap();
193 assert_eq!(result, "HEAD~5");
194 }
195
196 #[test]
197 fn test_resolve_falls_back_to_head_tilde_5() {
198 let mut vcs = MockVcs::new();
199 vcs.resolve_results.insert("HEAD~5".to_string(), true);
200 let db = MockDiffStore::new();
201
202 let result = resolve_digest_reference(&vcs, &db).unwrap();
203 assert_eq!(result, "HEAD~5");
204 }
205
206 #[test]
207 fn test_resolve_falls_back_to_root_commit() {
208 let mut vcs = MockVcs::new();
209 vcs.root_sha = Some("root_abc".to_string());
210 let db = MockDiffStore::new();
211
212 let result = resolve_digest_reference(&vcs, &db).unwrap();
213 assert_eq!(result, "root_abc");
214 }
215
216 #[test]
217 fn test_resolve_falls_back_to_head() {
218 let vcs = MockVcs::new();
219 let db = MockDiffStore::new();
220
221 let result = resolve_digest_reference(&vcs, &db).unwrap();
222 assert_eq!(result, "HEAD");
223 }
224
225 #[test]
226 fn test_update_session_anchor() {
227 let vcs = MockVcs::new();
228 let db = MockDiffStore::new();
229
230 update_session_anchor(&vcs, &db).unwrap();
231 assert_eq!(
232 db.configs.borrow().get("last_session_sha"),
233 Some(&"abc123".to_string())
234 );
235 }
236}