diff options
| author | Mica White <botahamec@outlook.com> | 2026-04-03 20:41:30 -0400 |
|---|---|---|
| committer | Mica White <botahamec@outlook.com> | 2026-04-03 20:41:30 -0400 |
| commit | 86c4f7743a0a3835d595cb32af7eafdc41f2be34 (patch) | |
| tree | 9f9a829cbb883c8b3f960c748c99a4d3152fbad8 | |
| parent | 02c306bf2cba2ecab1bcd33fb9a6b5de995163ee (diff) | |
Revert "Try a single-binary approach"
This reverts commit 02c306bf2cba2ecab1bcd33fb9a6b5de995163ee.
| -rw-r--r-- | Cargo.lock | 7 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/bin/git-autosave.rs | 26 | ||||
| -rw-r--r-- | src/main.rs | 498 |
4 files changed, 26 insertions, 506 deletions
@@ -106,12 +106,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] -name = "bpaf" -version = "0.9.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2435ff2f08be8436bdcd06a3de2bd7696fd10e45eb630ecfc09af7fbfa3e69a" - -[[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -438,7 +432,6 @@ version = "0.1.0" dependencies = [ "anyhow", "auth-git2", - "bpaf", "chrono", "colog", "confy", @@ -17,7 +17,6 @@ happylock = "0.5" chrono = "0.4" log = "0.4" colog = "1" -bpaf = "0.9" hostname = "0.4" inquire = "0.9" auth-git2 = "0.5" diff --git a/src/bin/git-autosave.rs b/src/bin/git-autosave.rs new file mode 100644 index 0000000..fa0d9e6 --- /dev/null +++ b/src/bin/git-autosave.rs @@ -0,0 +1,26 @@ +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 deleted file mode 100644 index c71db37..0000000 --- a/src/main.rs +++ /dev/null @@ -1,498 +0,0 @@ -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); -} |
