diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/git-autosave.rs | 26 | ||||
| -rw-r--r-- | src/main.rs | 498 |
2 files changed, 498 insertions, 26 deletions
diff --git a/src/bin/git-autosave.rs b/src/bin/git-autosave.rs deleted file mode 100644 index fa0d9e6..0000000 --- a/src/bin/git-autosave.rs +++ /dev/null @@ -1,26 +0,0 @@ -use auth_git2::GitAuthenticator; -use git_autosave::{Config, authenticate::Inquirer, commit_autosave, push_autosaves}; -use git2::{RemoteCallbacks, Repository}; - -fn main() -> Result<(), anyhow::Error> { - let repository = Repository::discover(".")?; - let gitconfig = repository.config()?; - let config: &'static mut Config = Box::leak(Box::new(Config::load()?)); - - if std::env::args().any(|arg| arg == "--init") { - let id = git_autosave::init(&repository, Some(config))?; - config.save()?; - println!("Initialized autosave for repository: {id}"); - } - - let auth = GitAuthenticator::new().set_prompter(Inquirer(config)); - let mut callbacks = RemoteCallbacks::new(); - callbacks.credentials(auth.credentials(&gitconfig)); - - let commit = commit_autosave(&repository)?; - println!("Commited autosave: {commit}"); - push_autosaves(&repository, callbacks)?; - println!("Successfully pushed autosave to remote"); - - Ok(()) -} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c71db37 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,498 @@ +use std::collections::HashSet; +use std::fmt::Display; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{Receiver, Sender}; +use std::time::Duration; + +use auth_git2::GitAuthenticator; +use chrono::{Local, Utc}; +use git_autosave::{Autosave, Config, authenticate::Inquirer, commit_autosave, push_autosaves}; +use git2::build::CheckoutBuilder; +use git2::{RemoteCallbacks, Repository, StatusOptions}; +use happylock::{Mutex, ThreadKey}; +use notify::{EventKind, RecommendedWatcher, RecursiveMode}; +use notify_debouncer_full::{DebounceEventHandler, DebounceEventResult, Debouncer, FileIdCache}; + +const THREE_MONTHS: i64 = 60 * 60 * 24 * 30 * 3; + +struct AutosaveOption { + text: String, + value: Autosave, +} + +impl Display for AutosaveOption { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.text.fmt(f) + } +} + +struct ConfigWatcher<Cache: FileIdCache + 'static> { + config: &'static Mutex<Config>, + repo_watcher: &'static mut Debouncer<RecommendedWatcher, Cache>, +} + +struct RepoWatcher { + config: &'static Mutex<Config>, + push_queue: Sender<PathBuf>, +} + +fn push_queue( + consumer: Receiver<PathBuf>, + config: &'static Mutex<Config>, + mut key: ThreadKey, +) -> ! { + let mut queue = HashSet::new(); + + loop { + while ping::new("8.8.8.8".parse().unwrap()) + .socket_type(ping::SocketType::DGRAM) + .timeout(Duration::from_secs(60)) + .ttl(128) + .send() + .is_err() + { + let mut added_item = false; + while let Ok(workdir) = consumer.try_recv() { + added_item = true; + queue.insert(workdir); + } + if added_item { + log::warn!( + "There is no Internet connection, so pushes to the repository have been queued" + ); + } + std::thread::yield_now(); + } + + while let Ok(workdir) = consumer.try_recv() { + queue.insert(workdir); + } + + config.scoped_lock(&mut key, |config| { + for workdir in &queue { + log::info!("Pushing {}...", workdir.to_string_lossy()); + let Ok(repository) = Repository::open(workdir) else { + log::error!("Failed to open repository: {}", workdir.to_string_lossy()); + continue; + }; + 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) = push_autosaves(&repository, callbacks) { + log::error!("Failed to push autosaves: {e}"); + } + + log::info!("Successfully pushed {}", workdir.to_string_lossy()); + } + }); + queue.clear(); + } +} + +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 events + .iter() + .all(|event| matches!(event.kind, EventKind::Access(_))) + { + 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 Config::load() { + 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 RepoWatcher { + 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; + } + }; + + let mut workdirs_to_autosave = HashSet::new(); + let mut repositories_to_autosave = Vec::new(); + for (event, path) in events + .iter() + .filter(|event| !matches!(event.kind, EventKind::Access(_))) + .flat_map(|event| event.paths.iter().map(move |path| (event, path))) + { + 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; + }; + let Some(workdir) = repository.workdir() else { + log::warn!("Skipping bare repository: {:?}", &path); + continue; + }; + if workdirs_to_autosave.contains(workdir) { + 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}"); + } + } + if let Ok(status) = repository.statuses(Some( + StatusOptions::new() + .include_untracked(true) + .include_ignored(false), + )) && status.is_empty() + { + continue; + } + + log::info!("Event: {:?}", event); + log::info!("Updated path: {:?}", &path); + workdirs_to_autosave.insert(workdir.to_path_buf()); + repositories_to_autosave.push(repository); + } + self.config.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) = self + .push_queue + .send(repository.workdir().unwrap().to_path_buf()) + { + log::error!("Failed to add repository to push queue: {e}"); + } + + log::info!("Successfully autosaved {:?}", workdir); + } + }); + } +} + +fn daemon() -> Result<(), anyhow::Error> { + let mut 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(Config::load()?))); + log::info!("Loaded autosave config"); + + log::info!("Starting push queue..."); + let (sender, receiver) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let key = ThreadKey::get().unwrap(); + push_queue(receiver, config, key); + }); + config.scoped_lock(&mut key, |config| { + for repository in config.repositories() { + if let Err(e) = sender.send(repository.clone()) { + log::error!("Failed to queue {}: {e}", repository.to_string_lossy()); + } + } + }); + log::info!("Started push queue"); + + log::info!("Starting repository watcher..."); + let repo_watcher = Box::leak(Box::new(notify_debouncer_full::new_debouncer( + Duration::from_secs(5), + None, + RepoWatcher { + config, + push_queue: sender, + }, + )?)); + 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(); + } +} + +fn init() -> Result<(), anyhow::Error> { + let repository = Repository::discover(".")?; + let mut config = Config::load()?; + let id = git_autosave::init(&repository, Some(&mut config))?; + config.save()?; + + println!("Initialized autosave for repository: {id}"); + + Ok(()) +} + +fn autosave() -> Result<(), anyhow::Error> { + let repository = Repository::discover(".")?; + let gitconfig = repository.config()?; + let config: &'static mut Config = Box::leak(Box::new(Config::load()?)); + + if std::env::args().any(|arg| arg == "--init") { + let id = git_autosave::init(&repository, Some(config))?; + config.save()?; + println!("Initialized autosave for repository: {id}"); + } + + let auth = GitAuthenticator::new().set_prompter(Inquirer(config)); + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(auth.credentials(&gitconfig)); + + let commit = commit_autosave(&repository)?; + println!("Commited autosave: {commit}"); + push_autosaves(&repository, callbacks)?; + println!("Successfully pushed autosave to remote"); + + Ok(()) +} + +fn clean_autosaves() -> Result<(), anyhow::Error> { + let repository = Repository::discover(".")?; + for reference in repository.references()? { + let Ok(mut reference) = reference else { + continue; + }; + let Ok(autosave): Result<Autosave, git2::Error> = (&reference).try_into() else { + continue; + }; + + if Utc::now().timestamp() - autosave.time.seconds() > THREE_MONTHS { + reference.delete()?; + } + } + + Ok(()) +} + +fn restore_autosave() -> Result<(), anyhow::Error> { + let all_users = std::env::args().any(|arg| arg == "--all-users"); + let all_branches = std::env::args().any(|arg| arg == "--all-branches"); + let all_devices = std::env::args().any(|arg| arg == "--all-devices"); + let anytime = std::env::args().any(|arg| arg == "--anytime"); + let force = std::env::args().any(|arg| arg == "--force"); + + let repository = Repository::discover(".")?; + let repo_id = git_autosave::repository_id(&repository)?; + let signature = repository.signature()?; + let branch = git_autosave::utils::current_branch(&repository)?; + let earliest_time = repository.head()?.peel_to_commit()?.time(); + + let gitconfig = repository.config()?; + let config: &'static _ = Box::leak(Box::new(Config::load()?)); + let auth = GitAuthenticator::new().set_prompter(Inquirer(config)); + let mut callbacks = RemoteCallbacks::new(); + callbacks.credentials(auth.credentials(&gitconfig)); + git_autosave::fetch_autosaves(&repository, callbacks)?; + + let mut autosaves = git_autosave::autosaves(&repository)? + .into_iter() + .filter(|autosave| { + all_users + || signature + .name() + .zip(autosave.author.clone()) + .is_some_and(|(a, b)| a == b) + || signature + .email() + .zip(autosave.email.clone()) + .is_some_and(|(a, b)| a == b) + }) + .filter(|autosave| all_branches || autosave.branch_name == branch) + .filter(|autosave| anytime || autosave.time > earliest_time) + .filter(|autosave| all_devices || autosave.repo_id.as_bytes() != repo_id.as_bytes()) + .collect::<Vec<_>>(); + + if autosaves.is_empty() { + eprintln!("ERROR: There are no available autosaves for the given filters."); + if !all_users || !all_branches || !anytime { + eprintln!( + "hint: Use --all-users, --all-branches, --all-devices, or --anytime to include more options." + ); + } + std::process::exit(1); + } + + let autosave = if autosaves.len() > 1 { + let autosaves = autosaves + .into_iter() + .map(|autosave| { + let device = autosave.host_name.as_ref().unwrap_or(&autosave.repo_id); + let time = chrono::DateTime::from_timestamp(autosave.time.seconds(), 0) + .map(|time| time.with_timezone(&Local)) + .map(|time| time.to_rfc2822()) + .unwrap_or(autosave.time.seconds().to_string()); + let branch = if all_branches { + format!(" on {}", &autosave.branch_name) + } else { + String::new() + }; + let author = if let Some(author) = + autosave.author.as_ref().or(autosave.email.as_ref()) + && all_users + { + format!(" by {author}") + } else { + String::new() + }; + AutosaveOption { + text: format!("{device} ({time}{branch}{author})"), + value: autosave, + } + }) + .collect(); + inquire::Select::new("Select an autosave:", autosaves) + .prompt()? + .value + } else { + autosaves + .pop() + .expect("There should be an autosave to select but there isn't. This is a bug!") + }; + let autosaved_commit = repository.find_commit(autosave.commit_id)?; + let workdir = repository.find_tree(git_autosave::utils::workdir_to_tree(&repository)?)?; + let new_tree = + git_autosave::utils::merge_commit_with_tree(&repository, &autosaved_commit, &workdir)?; + git_autosave::save_undo_tree(&repository, &workdir)?; + git_autosave::saved_restored_autosave(&repository, &autosave)?; + + let mut options = CheckoutBuilder::new(); + if force { + options.force(); + } + repository.checkout_tree( + &repository.find_object(new_tree, Some(git2::ObjectType::Tree))?, + Some(&mut options), + )?; + + Ok(()) +} + +fn subcommand(command: &Path, name: &str, f: fn() -> Result<(), anyhow::Error>) { + let Some(command) = command.components().next_back() else { + eprintln!("Invalid binary name: {}", command.to_string_lossy()); + std::process::exit(1); + }; + if command.as_os_str().to_string_lossy() != name { + return; + } + + match f() { + Ok(()) => std::process::exit(0), + Err(e) => { + eprintln!("{e}"); + std::process::exit(1); + } + } +} + +fn main() { + let Some(command) = std::env::args_os().next() else { + eprintln!("No command provided"); + std::process::exit(1); + }; + let path = PathBuf::from(command); + + subcommand(&path, "git-init-autosave", init); + subcommand(&path, "git-autosave", autosave); + subcommand(&path, "git-clean-autosaves", clean_autosaves); + subcommand(&path, "git-restore-autosave", restore_autosave); + subcommand(&path, "git-autosave-daemon", daemon); + + eprintln!("Unrecognized command: {}", path.to_string_lossy()); + std::process::exit(1); +} |
