use std::path::{Path, PathBuf};
const WATCHER_LOCK_FILE: &str = "watcher.lock";
const SYNC_LOCK_FILE: &str = "sync.lock";
const LOCK_TIMEOUT_SECS: u64 = 30;
const VERSION_FILE: &str = "version.txt";
#[cfg(windows)]
fn create_command(program: &str) -> std::process::Command {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let mut cmd = std::process::Command::new(program);
cmd.creation_flags(CREATE_NO_WINDOW);
cmd
}
#[cfg(not(windows))]
fn create_command(program: &str) -> std::process::Command {
std::process::Command::new(program)
}
pub fn is_git_repository(project_path: &Path) -> bool {
create_command("git")
.args(["rev-parse", "--is-inside-work-tree"])
.current_dir(project_path)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn get_git_head_sha(project_path: &Path) -> Option<String> {
create_command("git")
.args(["rev-parse", "HEAD"])
.current_dir(project_path)
.output()
.ok()
.and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
} else {
None
}
})
}
#[allow(dead_code)]
pub fn get_git_tracked_files(project_path: &Path) -> Vec<PathBuf> {
create_command("git")
.args(["ls-files"])
.current_dir(project_path)
.output()
.ok()
.and_then(|o| {
if o.status.success() {
Some(
String::from_utf8_lossy(&o.stdout)
.lines()
.filter_map(|line| {
let path = project_path.join(line);
if is_source_file(&path) {
Some(path)
} else {
None
}
})
.collect(),
)
} else {
None
}
})
.unwrap_or_default()
}
pub fn get_git_status_changes(project_path: &Path) -> GitStatusChanges {
let output = create_command("git")
.args(["status", "--porcelain"])
.current_dir(project_path)
.output();
let mut changes = GitStatusChanges::default();
if let Ok(o) = output {
if o.status.success() {
for line in String::from_utf8_lossy(&o.stdout).lines() {
if line.len() < 2 {
continue;
}
let status = &line[..2];
let path = line[3..].trim();
let file_path = if path.contains(" -> ") {
path.split(" -> ").nth(1).unwrap_or(path)
} else {
path
};
let full_path = project_path.join(file_path);
if !is_source_file(&full_path) {
continue;
}
match status.trim() {
"M" | "MM" | "AM" => changes.modified.push(full_path),
"A" | "??" => changes.added.push(full_path),
"D" | "AD" | "MD" => changes.deleted.push(full_path),
"R" => {
if let Some(old_path) = path.split(" -> ").next() {
changes.deleted.push(project_path.join(old_path));
}
changes.added.push(full_path);
}
_ => {}
}
}
}
}
changes
}
pub fn start_git_fsmonitor(project_path: &Path) -> bool {
create_command("git")
.args(["fsmonitor--daemon", "start"])
.current_dir(project_path)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn is_git_fsmonitor_running(project_path: &Path) -> bool {
create_command("git")
.args(["fsmonitor--daemon", "status"])
.current_dir(project_path)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[derive(Debug, Default)]
pub struct GitStatusChanges {
pub modified: Vec<PathBuf>,
pub added: Vec<PathBuf>,
pub deleted: Vec<PathBuf>,
}
impl GitStatusChanges {
pub fn has_changes(&self) -> bool {
!self.modified.is_empty() || !self.added.is_empty() || !self.deleted.is_empty()
}
#[allow(dead_code)]
pub fn total_count(&self) -> usize {
self.modified.len() + self.added.len() + self.deleted.len()
}
}
pub fn is_source_file(path: &Path) -> bool {
use super::ignore::WATCH_EXTENSIONS;
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| WATCH_EXTENSIONS.contains(&ext))
.unwrap_or(false)
}
struct WatcherLock {
instance_id: String,
pid: u32,
acquired_at: i64,
}
impl WatcherLock {
fn new() -> Self {
Self {
instance_id: uuid::Uuid::new_v4().to_string(),
pid: std::process::id(),
acquired_at: chrono::Utc::now().timestamp(),
}
}
fn decode(s: &str) -> Option<Self> {
let parts: Vec<&str> = s.split('|').collect();
if parts.len() != 3 {
return None;
}
Some(Self {
instance_id: parts[0].to_string(),
pid: parts[1].parse().ok()?,
acquired_at: parts[2].parse().ok()?,
})
}
fn encode(&self) -> String {
format!("{}|{}|{}", self.instance_id, self.pid, self.acquired_at)
}
fn is_stale(&self) -> bool {
let now = chrono::Utc::now().timestamp();
now - self.acquired_at > LOCK_TIMEOUT_SECS as i64
}
}
pub fn try_acquire_watcher_lock(project_path: &Path) -> bool {
let lock_path = project_path.join(".codegraph").join(WATCHER_LOCK_FILE);
if let Some(parent) = lock_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if lock_path.exists() {
let content = std::fs::read_to_string(&lock_path).ok();
if let Some(s) = content {
if let Some(lock) = WatcherLock::decode(&s) {
if !lock.is_stale() {
log::info!(
"CodeGraph: watcher lock held by instance {} (PID {}), skipping",
lock.instance_id, lock.pid
);
return false;
}
log::info!(
"CodeGraph: stealing stale watcher lock from instance {} (PID {})",
lock.instance_id, lock.pid
);
}
}
}
let lock = WatcherLock::new();
let _ = std::fs::write(&lock_path, lock.encode());
log::info!("CodeGraph: acquired watcher lock (instance {})", lock.instance_id);
true
}
pub fn release_watcher_lock(project_path: &Path) {
let lock_path = project_path.join(".codegraph").join(WATCHER_LOCK_FILE);
if lock_path.exists() {
let _ = std::fs::remove_file(&lock_path);
log::info!("CodeGraph: released watcher lock");
}
}
pub fn update_watcher_heartbeat(project_path: &Path) {
let lock_path = project_path.join(".codegraph").join(WATCHER_LOCK_FILE);
if lock_path.exists() {
let lock = WatcherLock::new();
let _ = std::fs::write(&lock_path, lock.encode());
}
}
pub fn try_acquire_sync_lock(project_path: &Path) -> i64 {
let lock_path = project_path.join(".codegraph").join(SYNC_LOCK_FILE);
if lock_path.exists() {
let content = std::fs::read_to_string(&lock_path).ok();
if let Some(s) = content {
let timestamp: i64 = s.parse().ok().unwrap_or(0);
let now = chrono::Utc::now().timestamp();
if now - timestamp < 5 {
log::debug!("CodeGraph: sync in progress by another instance, skipping");
return 0; }
}
}
let timestamp = chrono::Utc::now().timestamp();
let _ = std::fs::write(&lock_path, timestamp.to_string());
timestamp }
pub fn check_sync_lock_owner(project_path: &Path, our_timestamp: i64) -> bool {
let lock_path = project_path.join(".codegraph").join(SYNC_LOCK_FILE);
if !lock_path.exists() {
return false; }
let content = std::fs::read_to_string(&lock_path).ok();
if let Some(s) = content {
let current_timestamp: i64 = s.parse().ok().unwrap_or(0);
if current_timestamp != our_timestamp {
log::debug!("CodeGraph: sync lock stolen by another process (ours: {}, current: {})", our_timestamp, current_timestamp);
return false;
}
}
true
}
pub fn release_sync_lock(project_path: &Path) {
let lock_path = project_path.join(".codegraph").join(SYNC_LOCK_FILE);
if lock_path.exists() {
let _ = std::fs::remove_file(&lock_path);
}
}
pub fn check_mcp_daemon_active(project_path: &Path) -> bool {
let daemon_log_path = project_path.join(".codegraph").join("daemon.log");
if !daemon_log_path.exists() {
return false;
}
if let Ok(metadata) = std::fs::metadata(&daemon_log_path) {
if let Ok(modified) = metadata.modified() {
let now = std::time::SystemTime::now();
let elapsed = now.duration_since(modified).unwrap_or(std::time::Duration::MAX);
if elapsed < std::time::Duration::from_secs(30) {
return true;
}
}
}
false
}
fn save_version_sha(project_path: &Path, sha: &str) {
let version_path = project_path.join(".codegraph").join(VERSION_FILE);
if let Some(parent) = version_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&version_path, sha);
}
fn load_version_sha(project_path: &Path) -> Option<String> {
let version_path = project_path.join(".codegraph").join(VERSION_FILE);
std::fs::read_to_string(&version_path)
.ok()
.map(|s| s.trim().to_string())
}
pub fn has_version_changed(project_path: &Path) -> bool {
let current_sha = get_git_head_sha(project_path);
let stored_sha = load_version_sha(project_path);
current_sha != stored_sha
}
pub fn update_version_after_sync(project_path: &Path) {
if let Some(sha) = get_git_head_sha(project_path) {
save_version_sha(project_path, &sha);
log::debug!("CodeGraph: version updated to SHA {}", sha);
}
}