summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-04-03 20:41:30 -0400
committerMica White <botahamec@outlook.com>2026-04-03 20:41:30 -0400
commit86c4f7743a0a3835d595cb32af7eafdc41f2be34 (patch)
tree9f9a829cbb883c8b3f960c748c99a4d3152fbad8
parent02c306bf2cba2ecab1bcd33fb9a6b5de995163ee (diff)
Revert "Try a single-binary approach"
This reverts commit 02c306bf2cba2ecab1bcd33fb9a6b5de995163ee.
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml1
-rw-r--r--src/bin/git-autosave.rs26
-rw-r--r--src/main.rs498
4 files changed, 26 insertions, 506 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 7c90bca..ecdfdfa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index e7ca138..95a7808 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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);
-}