summaryrefslogtreecommitdiff
path: root/src/bin/git-autosave-daemon.rs
blob: 05ead057756c1ae4246f0cc9ce6f81ec2f46d0bb (plain)
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();
	}
}