use anyhow::Result;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn get_system_storage_dir() -> Result<PathBuf> {
let base_dir = if cfg!(target_os = "macos") {
dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Unable to determine home directory"))?
.join(".local")
.join("share")
.join("octocode")
} else if cfg!(target_os = "windows") {
dirs::data_dir()
.ok_or_else(|| anyhow::anyhow!("Unable to determine data directory"))?
.join("octocode")
} else {
if let Ok(xdg_data_home) = std::env::var("XDG_DATA_HOME") {
PathBuf::from(xdg_data_home).join("octocode")
} else {
dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Unable to determine home directory"))?
.join(".local")
.join("share")
.join("octocode")
}
};
if !base_dir.exists() {
fs::create_dir_all(&base_dir)?;
}
Ok(base_dir)
}
pub fn get_project_identifier(project_path: &Path) -> Result<String> {
if let Ok(git_remote) = get_git_remote_url(project_path) {
let mut hasher = Sha256::new();
hasher.update(git_remote.as_bytes());
let result = hasher.finalize();
return Ok(format!("{:x}", result)[..16].to_string()); }
let absolute_path = project_path.canonicalize().or_else(|_| {
if project_path.is_absolute() {
Ok(project_path.to_path_buf())
} else {
std::env::current_dir().map(|cwd| cwd.join(project_path))
}
})?;
let mut hasher = Sha256::new();
hasher.update(absolute_path.to_string_lossy().as_bytes());
let result = hasher.finalize();
Ok(format!("{:x}", result)[..16].to_string()) }
fn get_git_remote_url(project_path: &Path) -> Result<String> {
let output = Command::new("git")
.arg("-C")
.arg(project_path)
.arg("remote")
.arg("get-url")
.arg("origin")
.output()?;
if output.status.success() {
let url = String::from_utf8(output.stdout)?.trim().to_string();
if !url.is_empty() {
return Ok(normalize_git_url(&url));
}
}
Err(anyhow::anyhow!("No git remote found"))
}
fn normalize_git_url(url: &str) -> String {
let url = url.trim();
let url = if let Some(stripped) = url.strip_suffix(".git") {
stripped
} else {
url
};
if url.contains("@") && url.contains(":") && !url.contains("://") {
if let Some(at_pos) = url.find('@') {
if let Some(colon_pos) = url[at_pos..].find(':') {
let host = &url[at_pos + 1..at_pos + colon_pos];
let path = &url[at_pos + colon_pos + 1..];
return format!("{}/{}", host, path);
}
}
}
if url.starts_with("http://") || url.starts_with("https://") {
if let Some(scheme_end) = url.find("://") {
return url[scheme_end + 3..].to_string();
}
}
url.to_string()
}
pub fn get_project_storage_path(project_path: &Path) -> Result<PathBuf> {
let system_dir = get_system_storage_dir()?;
let project_id = get_project_identifier(project_path)?;
Ok(system_dir.join(project_id))
}
pub fn get_project_database_path(project_path: &Path) -> Result<PathBuf> {
let project_storage = get_project_storage_path(project_path)?;
Ok(project_storage.join("storage"))
}
pub fn get_project_config_path(project_path: &Path) -> Result<PathBuf> {
Ok(project_path.join(".octocode"))
}
pub fn get_fastembed_cache_dir() -> Result<PathBuf> {
let cache_dir = get_system_storage_dir()?.join("fastembed");
if !cache_dir.exists() {
fs::create_dir_all(&cache_dir)?;
}
Ok(cache_dir)
}
pub fn get_huggingface_cache_dir() -> Result<PathBuf> {
let cache_dir = get_system_storage_dir()?.join("sentencetransformer");
if !cache_dir.exists() {
fs::create_dir_all(&cache_dir)?;
}
Ok(cache_dir)
}
pub fn ensure_project_storage_exists(project_path: &Path) -> Result<PathBuf> {
let storage_path = get_project_storage_path(project_path)?;
if !storage_path.exists() {
fs::create_dir_all(&storage_path)?;
}
Ok(storage_path)
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_normalize_git_url() {
assert_eq!(
normalize_git_url("https://github.com/user/repo.git"),
"github.com/user/repo"
);
assert_eq!(
normalize_git_url("https://github.com/user/repo"),
"github.com/user/repo"
);
assert_eq!(
normalize_git_url("git@github.com:user/repo.git"),
"github.com/user/repo"
);
assert_eq!(
normalize_git_url("git@github.com:user/repo"),
"github.com/user/repo"
);
assert_eq!(
normalize_git_url("local/path/to/repo"),
"local/path/to/repo"
);
}
#[test]
fn test_project_identifier() {
let temp_dir = env::temp_dir().join("test_octocode");
let _ = fs::create_dir_all(&temp_dir);
let id1 = get_project_identifier(&temp_dir).expect("Should get project identifier");
let id2 =
get_project_identifier(&temp_dir).expect("Should get consistent project identifier");
assert_eq!(id1, id2);
assert_eq!(id1.len(), 16);
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_system_storage_dir() {
let storage_dir = get_system_storage_dir().expect("Should get system storage directory");
assert!(storage_dir.to_string_lossy().contains("octocode"));
assert!(storage_dir.is_absolute());
}
#[test]
fn test_fastembed_cache_dir() {
let fastembed_cache =
get_fastembed_cache_dir().expect("Should get fastembed cache directory");
assert!(fastembed_cache.to_string_lossy().contains("octocode"));
assert!(fastembed_cache.to_string_lossy().contains("fastembed"));
assert!(fastembed_cache.is_absolute());
let storage_dir = get_system_storage_dir().expect("Should get system storage directory");
assert!(fastembed_cache.starts_with(&storage_dir));
assert_eq!(fastembed_cache, storage_dir.join("fastembed"));
}
#[test]
fn test_sentencetransformer_cache_dir() {
let st_cache = get_huggingface_cache_dir().expect("Should get huggingface cache directory");
assert!(st_cache.to_string_lossy().contains("octocode"));
assert!(st_cache.to_string_lossy().contains("sentencetransformer"));
assert!(st_cache.is_absolute());
let storage_dir = get_system_storage_dir().expect("Should get system storage directory");
assert!(st_cache.starts_with(&storage_dir));
assert_eq!(st_cache, storage_dir.join("sentencetransformer"));
}
}