use std::fs;
use std::io::{BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::SystemTime;
use serde::{Serialize, de::DeserializeOwned};
use xxhash_rust::xxh3::xxh3_64;
const CACHE_VERSION: u8 = 2;
fn sha_cache_path(workspace_path: &Path) -> PathBuf {
workspace_path
.join(".unfault")
.join("cache")
.join("commit_sha")
}
fn mtime_secs(path: &Path) -> u64 {
path.metadata()
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub fn current_commit_sha(workspace_path: &Path) -> String {
let cache_file = sha_cache_path(workspace_path);
if cache_file.exists() {
let cache_mtime = mtime_secs(&cache_file);
let head_mtime = mtime_secs(&workspace_path.join(".git").join("HEAD"));
let packed_mtime = mtime_secs(&workspace_path.join(".git").join("packed-refs"));
let git_mtime = head_mtime.max(packed_mtime);
if cache_mtime >= git_mtime && git_mtime > 0 {
if let Ok(sha) = fs::read_to_string(&cache_file) {
let sha = sha.trim().to_string();
if !sha.is_empty() {
return sha;
}
}
}
}
let sha = Command::new("git")
.args(["rev-parse", "--short=12", "HEAD"])
.current_dir(workspace_path)
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout)
.ok()
.map(|s| s.trim().to_string())
} else {
None
}
})
.unwrap_or_else(|| "no-git".to_string());
if let Some(parent) = cache_file.parent() {
let _ = fs::create_dir_all(parent);
}
let _ = fs::write(&cache_file, &sha);
sha
}
pub fn workspace_state_key(workspace_path: &Path) -> String {
let commit_sha = current_commit_sha(workspace_path);
let status_output = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(workspace_path)
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout).ok()
} else {
None
}
});
match status_output {
None => commit_sha,
Some(status) if status.trim().is_empty() => {
commit_sha
}
Some(status) => {
let dirty_hash = xxh3_64(status.as_bytes());
format!("{commit_sha}+dirty:{dirty_hash:016x}")
}
}
}
pub fn query_cache_dir(workspace_path: &Path) -> PathBuf {
workspace_path.join(".unfault").join("cache").join("query")
}
fn cache_path(workspace_path: &Path, query_type: &str, params: &str, commit_sha: &str) -> PathBuf {
let params_hash = xxh3_64(params.as_bytes());
let filename = format!(
"v{}_{query_type}_{params_hash:016x}_{commit_sha}.msgpack",
CACHE_VERSION
);
query_cache_dir(workspace_path).join(filename)
}
pub fn get<T: DeserializeOwned>(
workspace_path: &Path,
query_type: &str,
params: &str,
commit_sha: &str,
) -> Option<T> {
let path = cache_path(workspace_path, query_type, params, commit_sha);
let file = fs::File::open(&path).ok()?;
rmp_serde::from_read(BufReader::new(file)).ok()
}
pub fn set<T: Serialize>(
workspace_path: &Path,
query_type: &str,
params: &str,
commit_sha: &str,
value: &T,
) {
let dir = query_cache_dir(workspace_path);
if fs::create_dir_all(&dir).is_err() {
return;
}
let path = cache_path(workspace_path, query_type, params, commit_sha);
if let Ok(file) = fs::File::create(&path) {
let mut writer = BufWriter::new(file);
let mut ser = rmp_serde::Serializer::new(&mut writer).with_struct_map();
if value.serialize(&mut ser).is_ok() {
let _ = writer.flush();
}
}
}
pub fn clear(workspace_path: &Path) -> std::io::Result<()> {
let dir = query_cache_dir(workspace_path);
if !dir.exists() {
return Ok(());
}
for entry in fs::read_dir(&dir)? {
let entry = entry?;
if entry.path().extension().map_or(false, |e| e == "msgpack") {
fs::remove_file(entry.path())?;
}
}
Ok(())
}
use unfault_core::types::graph_query::{
CallersContext, FlowContext, GraphContext, WorkspaceContext,
};
pub fn get_callers(
workspace_path: &Path,
function_name: &str,
file_hint: Option<&str>,
commit_sha: &str,
) -> Option<CallersContext> {
let params = format!("{}|{}", function_name, file_hint.unwrap_or(""));
get(workspace_path, "callers", ¶ms, commit_sha)
}
pub fn set_callers(
workspace_path: &Path,
function_name: &str,
file_hint: Option<&str>,
commit_sha: &str,
ctx: &CallersContext,
) {
let params = format!("{}|{}", function_name, file_hint.unwrap_or(""));
set(workspace_path, "callers", ¶ms, commit_sha, ctx);
}
pub fn get_impact(
workspace_path: &Path,
file_path: &str,
max_depth: usize,
commit_sha: &str,
) -> Option<GraphContext> {
let params = format!("{}|{}", file_path, max_depth);
get(workspace_path, "impact", ¶ms, commit_sha)
}
pub fn set_impact(
workspace_path: &Path,
file_path: &str,
max_depth: usize,
commit_sha: &str,
ctx: &GraphContext,
) {
let params = format!("{}|{}", file_path, max_depth);
set(workspace_path, "impact", ¶ms, commit_sha, ctx);
}
pub fn get_function_impact(
workspace_path: &Path,
function: &str,
max_depth: usize,
commit_sha: &str,
) -> Option<FlowContext> {
let params = format!("{}|{}", function, max_depth);
get(workspace_path, "function_impact", ¶ms, commit_sha)
}
pub fn set_function_impact(
workspace_path: &Path,
function: &str,
max_depth: usize,
commit_sha: &str,
ctx: &FlowContext,
) {
let params = format!("{}|{}", function, max_depth);
set(workspace_path, "function_impact", ¶ms, commit_sha, ctx);
}
pub fn get_deps(workspace_path: &Path, target: &str, commit_sha: &str) -> Option<GraphContext> {
get(workspace_path, "deps", target, commit_sha)
}
pub fn set_deps(workspace_path: &Path, target: &str, commit_sha: &str, ctx: &GraphContext) {
set(workspace_path, "deps", target, commit_sha, ctx);
}
pub fn get_library(
workspace_path: &Path,
library_name: &str,
commit_sha: &str,
) -> Option<GraphContext> {
get(workspace_path, "library", library_name, commit_sha)
}
pub fn set_library(
workspace_path: &Path,
library_name: &str,
commit_sha: &str,
ctx: &GraphContext,
) {
set(workspace_path, "library", library_name, commit_sha, ctx);
}
pub fn get_stats(workspace_path: &Path, commit_sha: &str) -> Option<WorkspaceContext> {
get(workspace_path, "stats", "workspace", commit_sha)
}
pub fn set_stats(workspace_path: &Path, commit_sha: &str, ctx: &WorkspaceContext) {
set(workspace_path, "stats", "workspace", commit_sha, ctx);
}
pub fn get_critical(
workspace_path: &Path,
metric: &str,
limit: usize,
commit_sha: &str,
) -> Option<GraphContext> {
let params = format!("{}|{}", metric, limit);
get(workspace_path, "critical", ¶ms, commit_sha)
}
pub fn set_critical(
workspace_path: &Path,
metric: &str,
limit: usize,
commit_sha: &str,
ctx: &GraphContext,
) {
let params = format!("{}|{}", metric, limit);
set(workspace_path, "critical", ¶ms, commit_sha, ctx);
}
use unfault_core::types::graph_query::{HandlersContext, PathContext};
pub fn get_path(
workspace_path: &Path,
from: &str,
to: &str,
commit_sha: &str,
) -> Option<PathContext> {
let params = format!("{}|{}", from, to);
get(workspace_path, "path", ¶ms, commit_sha)
}
pub fn set_path(workspace_path: &Path, from: &str, to: &str, commit_sha: &str, ctx: &PathContext) {
let params = format!("{}|{}", from, to);
set(workspace_path, "path", ¶ms, commit_sha, ctx);
}
pub fn get_handlers(
workspace_path: &Path,
pattern: &str,
commit_sha: &str,
) -> Option<HandlersContext> {
get(workspace_path, "handlers", pattern, commit_sha)
}
pub fn set_handlers(workspace_path: &Path, pattern: &str, commit_sha: &str, ctx: &HandlersContext) {
set(workspace_path, "handlers", pattern, commit_sha, ctx);
}