summaryrefslogtreecommitdiff
path: root/src/bin/git-autosave-daemon.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/git-autosave-daemon.rs')
-rw-r--r--src/bin/git-autosave-daemon.rs206
1 files changed, 206 insertions, 0 deletions
diff --git a/src/bin/git-autosave-daemon.rs b/src/bin/git-autosave-daemon.rs
new file mode 100644
index 0000000..05ead05
--- /dev/null
+++ b/src/bin/git-autosave-daemon.rs
@@ -0,0 +1,206 @@
+use std::{collections::HashSet, time::Duration};
+
+use auth_git2::GitAuthenticator;
+use git_autosave::{Config, authenticate::Inquirer, commit_autosave, push_autosaves};
+use git2::{RemoteCallbacks, Repository};
+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;
+ }
+
+ 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(1),
+ 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(1),
+ 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();
+ }
+}