use std::collections::HashSet;
use std::path::Path;
use crate::manifest::{self, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FreshnessState {
Fresh,
Stale {
indexed_head: String,
current_head: String,
changed_files: usize,
},
Unknown {
reason: UnknownReason,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnknownReason {
NoIndexState,
GitUnavailable,
}
impl UnknownReason {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
UnknownReason::NoIndexState => "no-index-state",
UnknownReason::GitUnavailable => "git-unavailable",
}
}
}
fn default_scope() -> Vec<String> {
vec![".".to_string()]
}
fn string_array(v: &Value) -> Option<Vec<String>> {
match v {
Value::Array(items) => Some(
items
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect(),
),
_ => None,
}
}
#[must_use]
pub fn index_freshness_state(root: &Path) -> FreshnessState {
let Some(state) = manifest::read_index_state(root) else {
return FreshnessState::Unknown {
reason: UnknownReason::NoIndexState,
};
};
let Some(stored_head) = state
.get("head")
.and_then(Value::as_str)
.map(str::to_string)
else {
return FreshnessState::Unknown {
reason: UnknownReason::NoIndexState,
};
};
let Some(current_head) = manifest::git_head(root) else {
return FreshnessState::Unknown {
reason: UnknownReason::GitUnavailable,
};
};
let manifest_doc = manifest::read_manifest(root).ok().flatten();
let indexed_files: HashSet<String> = manifest_doc
.as_ref()
.and_then(|m| m.get("entries"))
.and_then(Value::as_object)
.map(|entries| entries.iter().map(|(k, _)| k.clone()).collect())
.unwrap_or_default();
let scope: Vec<String> = manifest_doc
.as_ref()
.and_then(|m| m.get("scoped_dirs"))
.and_then(string_array)
.filter(|v: &Vec<String>| !v.is_empty())
.unwrap_or_else(default_scope);
let current_dirty = manifest::dirty_indexed_files_at(root, &scope, &indexed_files);
let baseline_dirty: HashSet<String> = state
.get("dirty_indexed_files")
.and_then(string_array)
.map(|v| v.into_iter().collect())
.unwrap_or_default();
let changed_files = current_dirty
.iter()
.filter(|p| !baseline_dirty.contains(*p))
.count();
if current_head == stored_head && changed_files == 0 {
FreshnessState::Fresh
} else {
FreshnessState::Stale {
indexed_head: stored_head,
current_head,
changed_files,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn no_index_state_sidecar_is_unknown_with_that_reason() {
let dir = std::env::temp_dir().join(format!(
"devscout-freshness-unit-{}-{:?}",
std::process::id(),
std::time::SystemTime::now()
));
fs::create_dir_all(&dir).unwrap();
assert_eq!(
index_freshness_state(&dir),
FreshnessState::Unknown {
reason: UnknownReason::NoIndexState
}
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn unknown_reason_words_are_stable() {
assert_eq!(UnknownReason::NoIndexState.as_str(), "no-index-state");
assert_eq!(UnknownReason::GitUnavailable.as_str(), "git-unavailable");
}
}