mod debouncer;
mod event_handler;
use notify::event::EventKind; use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::process::Command;
use std::time::Duration;
use tokio::sync::mpsc;
fn run_git(args: &[&str]) -> Result<(bool, String, String), Box<dyn std::error::Error>> {
let output = Command::new("git").args(args).output()?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok((output.status.success(), stdout, stderr))
}
fn run_git_env(
args: &[&str],
env_remove: &[&str],
) -> Result<(bool, String, String), Box<dyn std::error::Error>> {
let mut cmd = Command::new("git");
cmd.args(args);
for var in env_remove {
cmd.env_remove(var);
}
let output = cmd.output()?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok((output.status.success(), stdout, stderr))
}
fn ensure_repo_initialised() -> Result<(), Box<dyn std::error::Error>> {
let work_tree = std::env::var("GIT_WORK_TREE").ok();
let git_dir = std::env::var("GIT_DIR").ok();
let (work_tree, git_dir) = match (work_tree, git_dir) {
(Some(wt), Some(gd)) => (wt, gd),
_ => return Ok(()),
};
let wt_path = Path::new(&work_tree);
let gd_path = Path::new(&git_dir);
if !wt_path.exists() {
println!(
"📁 Work tree '{}' does not exist. Creating...",
work_tree
);
std::fs::create_dir_all(wt_path)?;
println!(" Created work tree directory: {}", work_tree);
}
if !gd_path.join("HEAD").exists() {
println!(" Initialising bare repository at: {}", git_dir);
std::fs::create_dir_all(gd_path)?;
let (ok, _, stderr) =
run_git_env(&["init", "--bare", &git_dir], &["GIT_WORK_TREE", "GIT_DIR"])?;
if !ok {
return Err(format!("Failed to init bare repository: {}", stderr.trim()).into());
}
} else {
println!(
" Bare repository already exists at: {}",
git_dir
);
}
Ok(())
}
fn preflight_checks() -> Result<(), Box<dyn std::error::Error>> {
ensure_repo_initialised()?;
let (ok, _, stderr) = run_git(&["rev-parse", "--is-inside-work-tree"])?;
if !ok {
return Err(format!("Not inside a git repository: {}", stderr.trim()).into());
}
let has_unstaged = Command::new("git")
.args(["diff", "--quiet"])
.status()?
.code()
.map_or(true, |c| c != 0);
let has_staged = Command::new("git")
.args(["diff", "--cached", "--quiet"])
.status()?
.code()
.map_or(true, |c| c != 0);
if has_unstaged || has_staged {
let mut msg = String::from("Repository has uncommitted tracked changes:\n");
if has_unstaged {
msg.push_str(" - Unstaged modifications detected.\n");
}
if has_staged {
msg.push_str(" - Staged (indexed) changes detected.\n");
}
msg.push_str("Please commit or stash your changes before running the watcher.");
return Err(msg.into());
}
let (ok, stdout, _) = run_git(&["ls-files", "--others", "--exclude-standard"])?;
if ok && !stdout.trim().is_empty() {
let untracked_files: Vec<&str> = stdout.trim().lines().collect();
let preview: Vec<&str> = untracked_files.iter().take(10).copied().collect();
let mut msg = format!(
"Repository has {} untracked file(s):\n",
untracked_files.len()
);
for f in &preview {
msg.push_str(&format!(" - {}\n", f));
}
if untracked_files.len() > 10 {
msg.push_str(&format!(" ... and {} more.\n", untracked_files.len() - 10));
}
msg.push_str("Please commit, remove, or .gitignore them before running the watcher.");
return Err(msg.into());
}
let (has_remote, remotes, _) = run_git(&["remote"])?;
if has_remote && !remotes.trim().is_empty() {
println!("Fetching from remote...");
let (ok, _, stderr) = run_git(&["fetch"])?;
if !ok {
return Err(format!("Failed to fetch from remote: {}", stderr.trim()).into());
}
println!("Fetch complete.");
let (ok, upstream, stderr) =
run_git(&["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])?;
if !ok {
let trimmed = stderr.trim();
if trimmed.contains("no upstream configured")
|| trimmed.contains("does not point to a branch")
{
println!(
"[WARNING] No upstream tracking branch configured. Skipping sync check.\n\
Consider running: git branch --set-upstream-to=origin/<branch>"
);
return Ok(());
}
return Err(format!(
"Failed to determine upstream tracking branch: {}",
trimmed
)
.into());
}
let upstream = upstream.trim();
let (_, local_hash, _) = run_git(&["rev-parse", "HEAD"])?;
let (_, remote_hash, _) = run_git(&["rev-parse", &format!("{}", upstream)])?;
let local_hash = local_hash.trim();
let remote_hash = remote_hash.trim();
if local_hash != remote_hash {
let (_, ahead_str, _) = run_git(&[
"rev-list",
"--count",
&format!("{}..HEAD", upstream),
])?;
let (_, behind_str, _) = run_git(&[
"rev-list",
"--count",
&format!("HEAD..{}", upstream),
])?;
let ahead: usize = ahead_str.trim().parse().unwrap_or(0);
let behind: usize = behind_str.trim().parse().unwrap_or(0);
let mut msg = format!(
"HEAD is not in sync with upstream '{}':\n",
upstream
);
msg.push_str(&format!(" Local: {}\n", local_hash));
msg.push_str(&format!(" Remote: {}\n", remote_hash));
if ahead > 0 && behind > 0 {
msg.push_str(&format!(
" Branch has DIVERGED: {} commit(s) ahead, {} commit(s) behind.\n",
ahead, behind
));
msg.push_str(" Please rebase or merge to reconcile.");
} else if ahead > 0 {
msg.push_str(&format!(" Local is {} commit(s) AHEAD of remote.\n", ahead));
msg.push_str(" Please push your changes before running the watcher.");
} else if behind > 0 {
msg.push_str(&format!(
" Local is {} commit(s) BEHIND remote.\n",
behind
));
msg.push_str(" Please pull the latest changes before running the watcher.");
}
return Err(msg.into());
}
println!("✅ Repository is clean and in sync with '{}'.", upstream);
} else {
println!("✅ Repository is clean. No remote configured — skipping sync check.");
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("--- Async File Watcher with 3-Second Debounce ---");
preflight_checks()?;
let path_to_watch = Path::new(".");
println!("Monitoring changes in: '{}'", path_to_watch.display());
println!("Press Ctrl+C to exit.");
let (tx, rx) = mpsc::channel(100);
let debounce_duration = Duration::from_secs(3);
tokio::spawn(debouncer::debouncer(rx, debounce_duration));
let mut watcher = RecommendedWatcher::new(
move |res: Result<notify::Event, notify::Error>| {
if let Ok(event) = res {
if let EventKind::Access(_) = event.kind {
return;
}
if tx.try_send(event).is_err() {
println!(
"[Warning] Channel is full, event dropped. This might happen under heavy load."
);
}
}
},
Config::default(),
)?;
watcher.watch(path_to_watch, RecursiveMode::Recursive)?;
tokio::signal::ctrl_c().await?;
println!("\nShutdown signal received. Exiting.");
Ok(())
}