use std::{collections::HashSet, time::Duration};
use auth_git2::GitAuthenticator;
use git_autosave::{Config, authenticate::Inquirer, commit_autosave, push_autosaves};
use git2::{RemoteCallbacks, Repository, StatusOptions};
use happylock::{Mutex, ThreadKey};
use notify::{EventKind, INotifyWatcher, RecursiveMode};
use notify_debouncer_full::{
DebounceEventHandler, DebounceEventResult, DebouncedEvent, Debouncer, FileIdCache,
};
struct ConfigWatcher<Cache: FileIdCache + 'static> {
config: &'static Mutex<Config>,
repo_watcher: &'static mut Debouncer<INotifyWatcher, Cache>,
}
struct Watcher(&'static Mutex<Config>);
fn is_event_useful(events: &[DebouncedEvent]) -> bool {
events.iter().all(|event| {
event.kind == EventKind::Any
|| event.kind == EventKind::Other
|| matches!(event.kind, EventKind::Access(_))
})
}
impl<Cache: FileIdCache + Send + 'static> DebounceEventHandler for ConfigWatcher<Cache> {
fn handle_event(&mut self, events: DebounceEventResult) {
let events = match events {
Ok(events) => events,
Err(errors) => {
for error in errors {
log::error!("Failed to load event: {error}");
}
return;
}
};
if !is_event_useful(&events) {
return;
}
log::info!("The config was updated. Reloading...");
let Some(key) = ThreadKey::get() else {
log::error!("Failed to acquire thread key when reloading config. This is a bug!");
return;
};
let config = match git_autosave::load_config() {
Ok(config) => config,
Err(e) => {
log::error!("Failed to reload autosave config: {e}");
return;
}
};
self.config.scoped_lock(key, |old_config| {
let paths_to_unwatch = old_config.repositories().difference(config.repositories());
let paths_to_watch = config.repositories().difference(old_config.repositories());
for path in paths_to_unwatch {
if let Err(e) = self.repo_watcher.unwatch(path) {
log::error!("Error when removing path from being watched: {e}");
}
log::info!("Removed {path:?} from list of repos to watch");
}
for path in paths_to_watch {
if let Err(e) = self.repo_watcher.watch(path, RecursiveMode::Recursive) {
log::error!("Error when adding path to repositories to watch: {e}");
}
log::info!("Added {path:?} from list of repos to watch");
}
*old_config = config;
});
log::info!("Successfully reloaded autosave config");
}
}
impl DebounceEventHandler for Watcher {
fn handle_event(&mut self, events: DebounceEventResult) {
let Some(mut key) = ThreadKey::get() else {
log::error!("Failed to acquire thread key when autosaving repository. This is a bug!");
return;
};
let events = match events {
Ok(events) => events,
Err(errors) => {
for error in errors {
log::error!("Failed to get update events: {error}");
}
return;
}
};
if !is_event_useful(&events) {
return;
}
let mut workdirs_to_autosave = HashSet::new();
let mut repositories_to_autosave = Vec::new();
for path in events.iter().flat_map(|event| &event.paths) {
if path
.components()
.any(|component| component.as_os_str() == ".git")
{
// Prevent infinite loop from commits triggering autosaves
continue;
}
let Ok(repository) = Repository::discover(path) else {
log::warn!("Skipping non-repository: {:?}", &path);
continue;
};
match repository.is_path_ignored(path) {
Ok(true) => {
log::trace!("Skipping event for ignored path: {:?}", path);
continue;
}
Ok(false) => {}
Err(e) => {
log::error!("Failed to determine if path is ignore: {e}");
}
}
let Some(workdir) = repository.workdir() else {
log::warn!("Skipping bare repository: {:?}", &path);
continue;
};
if workdirs_to_autosave.contains(workdir) {
continue;
}
if let Ok(status) = repository.statuses(Some(
StatusOptions::new()
.include_untracked(true)
.include_ignored(false),
)) && status.is_empty()
{
continue;
}
log::info!("Updated path: {:?}", &path);
workdirs_to_autosave.insert(workdir.to_path_buf());
repositories_to_autosave.push(repository);
}
self.0.scoped_lock(&mut key, |config| {
for repository in repositories_to_autosave {
let workdir = repository
.workdir()
.map(|path| path.to_string_lossy())
.unwrap_or_default();
log::info!("Autosaving {:?}...", workdir);
let Ok(gitconfig) = repository.config() else {
log::error!("Failed to load gitconfig for repository: {:?}", workdir);
return;
};
let auth = GitAuthenticator::new().set_prompter(Inquirer(&*config));
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(auth.credentials(&gitconfig));
if let Err(e) = commit_autosave(&repository) {
log::error!("Failed to commit autosave: {e}");
}
if let Err(e) = push_autosaves(&repository, callbacks) {
log::error!("Failed to push autosaves: {e}");
}
log::info!("Successfully autosaved {:?}", workdir);
}
});
}
}
fn main() -> Result<(), anyhow::Error> {
let key = ThreadKey::get().expect("Could not get ThreadKey on startup. This is a bug!");
colog::init();
log::info!("Loading autosave config...");
let config: &'static Mutex<Config> =
Box::leak(Box::new(Mutex::new(git_autosave::load_config()?)));
log::info!("Loaded autosave config");
log::info!("Starting repository watcher...");
let repo_watcher = Box::leak(Box::new(notify_debouncer_full::new_debouncer(
Duration::from_secs(5),
None,
Watcher(config),
)?));
config.scoped_lock(key, |config| {
log::info!("Adding repositories to watch...");
for repository in config.repositories() {
if let Err(e) = repo_watcher.watch(repository, RecursiveMode::Recursive) {
log::error!("Failed to watch {repository:?}: {e}");
}
log::info!("Added {repository:?}");
}
});
log::info!("Started repository watcher");
log::info!("Starting configuration watcher...");
notify_debouncer_full::new_debouncer(
Duration::from_secs(5),
None,
ConfigWatcher {
config,
repo_watcher,
},
)?
.watch(
&confy::get_configuration_file_path("git-autosave", "git-autosaved")?,
RecursiveMode::NonRecursive,
)?;
log::info!("Started configuration watcher");
log::info!("Initializing complete. Parking...");
loop {
std::thread::yield_now();
}
}
|