use tracing::warn;
use crate::error::DiffError;
use crate::port::{DiffStore, VcsProvider};
pub fn resolve_digest_reference(
vcs: &dyn VcsProvider,
db: &dyn DiffStore,
) -> Result<String, DiffError> {
if let Ok(Some(sha)) = db.config_get("last_session_sha") {
if vcs.resolve_ref(&sha)? {
return Ok(sha);
}
warn!("last_session_sha '{}' not found in repo, falling back", sha);
}
if vcs.resolve_ref("HEAD~5")? {
return Ok("HEAD~5".to_string());
}
if let Some(root_sha) = vcs.root_commit_sha()? {
return Ok(root_sha);
}
Ok("HEAD".to_string())
}
pub fn update_session_anchor(vcs: &dyn VcsProvider, db: &dyn DiffStore) -> Result<(), DiffError> {
let sha = vcs.head_sha()?;
db.config_set("last_session_sha", &sha)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::{DbError, DiffError};
use crate::model::{Config, ImpactEntry, SymbolResult, SymbolWithParent, VcsFileChange};
use crate::{ConfigStore, SymbolLookup};
use std::cell::RefCell;
use std::collections::HashMap;
struct MockVcs {
resolve_results: HashMap<String, bool>,
root_sha: Option<String>,
head: String,
}
impl MockVcs {
fn new() -> Self {
Self {
resolve_results: HashMap::new(),
root_sha: None,
head: "abc123".to_string(),
}
}
}
impl VcsProvider for MockVcs {
fn working_tree_changes(
&self,
_base_ref: Option<&str>,
) -> Result<Vec<VcsFileChange>, DiffError> {
Ok(vec![])
}
fn changes_between_refs(
&self,
_from: &str,
_to: &str,
) -> Result<Vec<VcsFileChange>, DiffError> {
Ok(vec![])
}
fn read_file_at_ref(&self, _path: &str, _ref: &str) -> Result<Option<String>, DiffError> {
Ok(None)
}
fn commit_count(&self, _from: &str, _to: &str) -> Result<usize, DiffError> {
Ok(0)
}
fn resolve_ref(&self, refspec: &str) -> Result<bool, DiffError> {
Ok(*self.resolve_results.get(refspec).unwrap_or(&false))
}
fn head_sha(&self) -> Result<String, DiffError> {
Ok(self.head.clone())
}
fn root_commit_sha(&self) -> Result<Option<String>, DiffError> {
Ok(self.root_sha.clone())
}
}
struct MockDiffStore {
configs: RefCell<HashMap<String, String>>,
}
impl MockDiffStore {
fn new() -> Self {
Self {
configs: RefCell::new(HashMap::new()),
}
}
fn with_config(key: &str, value: &str) -> Self {
let mut configs = HashMap::new();
configs.insert(key.to_string(), value.to_string());
Self {
configs: RefCell::new(configs),
}
}
}
impl ConfigStore for MockDiffStore {
fn config_get(&self, key: &str) -> Result<Option<String>, DbError> {
Ok(self.configs.borrow().get(key).cloned())
}
fn config_set(&self, key: &str, value: &str) -> Result<(), DbError> {
self.configs
.borrow_mut()
.insert(key.to_string(), value.to_string());
Ok(())
}
fn config_list(&self) -> Result<Vec<Config>, DbError> {
Ok(vec![])
}
}
impl SymbolLookup for MockDiffStore {
fn get_file_id(&self, _path: &str) -> Result<Option<i64>, DbError> {
Ok(None)
}
fn get_symbol_by_id(&self, _id: i64) -> Result<Option<crate::Symbol>, DbError> {
Ok(None)
}
fn search_symbols_by_name(
&self,
_name: &str,
_limit: i64,
) -> Result<Vec<SymbolResult>, DbError> {
Ok(vec![])
}
fn get_symbols_for_file(&self, _file_id: i64) -> Result<Vec<crate::Symbol>, DbError> {
Ok(vec![])
}
fn get_children(&self, _symbol_id: i64) -> Result<Vec<crate::Symbol>, DbError> {
Ok(vec![])
}
fn impact_analysis(&self, _id: i64, _depth: i64) -> Result<Vec<ImpactEntry>, DbError> {
Ok(vec![])
}
fn stats(&self) -> Result<(i64, i64, i64), DbError> {
Ok((0, 0, 0))
}
}
impl DiffStore for MockDiffStore {
fn symbols_in_line_range(
&self,
_path: &str,
_start: i64,
_end: i64,
) -> Result<Vec<SymbolResult>, DbError> {
Ok(vec![])
}
fn all_symbols_for_file(&self, _path: &str) -> Result<Vec<SymbolWithParent>, DbError> {
Ok(vec![])
}
}
#[test]
fn test_resolve_uses_stored_session_sha() {
let mut vcs = MockVcs::new();
vcs.resolve_results.insert("sha_stored".to_string(), true);
let db = MockDiffStore::with_config("last_session_sha", "sha_stored");
let result = resolve_digest_reference(&vcs, &db).unwrap();
assert_eq!(result, "sha_stored");
}
#[test]
fn test_resolve_falls_back_when_stored_sha_invalid() {
let mut vcs = MockVcs::new();
vcs.resolve_results.insert("HEAD~5".to_string(), true);
let db = MockDiffStore::with_config("last_session_sha", "gone_sha");
let result = resolve_digest_reference(&vcs, &db).unwrap();
assert_eq!(result, "HEAD~5");
}
#[test]
fn test_resolve_falls_back_to_head_tilde_5() {
let mut vcs = MockVcs::new();
vcs.resolve_results.insert("HEAD~5".to_string(), true);
let db = MockDiffStore::new();
let result = resolve_digest_reference(&vcs, &db).unwrap();
assert_eq!(result, "HEAD~5");
}
#[test]
fn test_resolve_falls_back_to_root_commit() {
let mut vcs = MockVcs::new();
vcs.root_sha = Some("root_abc".to_string());
let db = MockDiffStore::new();
let result = resolve_digest_reference(&vcs, &db).unwrap();
assert_eq!(result, "root_abc");
}
#[test]
fn test_resolve_falls_back_to_head() {
let vcs = MockVcs::new();
let db = MockDiffStore::new();
let result = resolve_digest_reference(&vcs, &db).unwrap();
assert_eq!(result, "HEAD");
}
#[test]
fn test_update_session_anchor() {
let vcs = MockVcs::new();
let db = MockDiffStore::new();
update_session_anchor(&vcs, &db).unwrap();
assert_eq!(
db.configs.borrow().get("last_session_sha"),
Some(&"abc123".to_string())
);
}
}