diff options
| author | Mica White <botahamec@outlook.com> | 2026-03-29 19:55:31 -0400 |
|---|---|---|
| committer | Mica White <botahamec@outlook.com> | 2026-03-29 19:55:31 -0400 |
| commit | 628dbdaffdf97db4db4e00777dbb3c4072019f64 (patch) | |
| tree | a0ab69ad98c85f76086e0465dc090a89a15e03dc /src | |
| parent | 634f6da6186cd9bdcc0e3dde3a16085ad59536e3 (diff) | |
Added git-restore-autosave
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/git-autosave-daemon.rs | 4 | ||||
| -rw-r--r-- | src/bin/git-restore-autosave.rs | 128 | ||||
| -rw-r--r-- | src/lib.rs | 184 |
3 files changed, 312 insertions, 4 deletions
diff --git a/src/bin/git-autosave-daemon.rs b/src/bin/git-autosave-daemon.rs index 05ead05..a0066a5 100644 --- a/src/bin/git-autosave-daemon.rs +++ b/src/bin/git-autosave-daemon.rs @@ -170,7 +170,7 @@ fn main() -> Result<(), anyhow::Error> { log::info!("Starting repository watcher..."); let repo_watcher = Box::leak(Box::new(notify_debouncer_full::new_debouncer( - Duration::from_secs(1), + Duration::from_secs(5), None, Watcher(config), )?)); @@ -186,7 +186,7 @@ fn main() -> Result<(), anyhow::Error> { log::info!("Started repository watcher"); log::info!("Starting configuration watcher..."); notify_debouncer_full::new_debouncer( - Duration::from_secs(1), + Duration::from_secs(5), None, ConfigWatcher { config, diff --git a/src/bin/git-restore-autosave.rs b/src/bin/git-restore-autosave.rs new file mode 100644 index 0000000..51cc14c --- /dev/null +++ b/src/bin/git-restore-autosave.rs @@ -0,0 +1,128 @@ +// git restore-autosave +// - --user and --all-users cannot both be present +// - --branch and --all-branches cannot both be present +// - if --user or -u is not present, the selected user is the repository signature +// - if --branch or -b is not present, the selected branch is the checked out branch +// - if --device or -d is present (UUID or hostname), filter to autosaves from that device +// - filter to autosaves from the current user (name or email) if --all-users is not present +// - filter to autosaves on the current branch +// - filter to autosaves after the head commit +// - if there is more than one option, enter pick mode +// - if there are no options, tell the user to use --all-users or --all-branches or --anytime + +use std::fmt::Display; + +use auth_git2::GitAuthenticator; +use chrono::Local; +use git_autosave::{Autosave, authenticate::Inquirer}; +use git2::{RemoteCallbacks, Repository, build::CheckoutBuilder}; + +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) + } +} + +fn main() -> 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 anytime = std::env::args().any(|arg| arg == "--anytime"); + let force = std::env::args().any(|arg| arg == "--force"); + + let repository = Repository::discover(".")?; + let signature = repository.signature()?; + let branch = git_autosave::current_branch(&repository)?; + let earliest_time = repository.head()?.peel_to_commit()?.time(); + + let gitconfig = repository.config()?; + let config: &'static _ = Box::leak(Box::new(git_autosave::load_config()?)); + 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) + .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, 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::workdir_to_tree(&repository)?)?; + let new_tree = git_autosave::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(()) +} @@ -15,6 +15,7 @@ * - check default merge strategy * - merge autosave using default strategy * - allow a different merge strategy to be specified + * - allow --no-commit * - display available autosaves if there is more than one * * git undo-restore-autosave: @@ -31,7 +32,10 @@ use std::fs::Metadata; use std::path::{Path, PathBuf}; use confy::ConfyError; -use git2::{Oid, PushOptions, RemoteCallbacks, Repository}; +use git2::{ + Commit, FetchOptions, MergeOptions, Oid, PushOptions, Reference, RemoteCallbacks, Repository, + Signature, Time, Tree, +}; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; @@ -79,6 +83,19 @@ pub fn save_config(config: &Config) -> Result<(), ConfyError> { confy::store("git-autosave", "git-autosaved", config) } +pub fn repository_id(repository: &Repository) -> Result<String, git2::Error> { + repository + .config()? + .get_entry("autosave.id")? + .value() + .map(|s| s.to_string()) + .ok_or(git2::Error::new( + git2::ErrorCode::Invalid, + git2::ErrorClass::Config, + "Repository ID is not valid UTF-8", + )) +} + pub fn init(repository: &Repository, config: Option<&mut Config>) -> Result<Uuid, git2::Error> { let id = Uuid::new_v4(); let workdir = repository.workdir(); @@ -192,6 +209,7 @@ pub fn push_autosaves( repository: &Repository, callbacks: RemoteCallbacks<'_>, ) -> Result<(), git2::Error> { + let id = repository_id(repository)?; let remote = repository.branch_upstream_remote(¤t_branch_ref(repository)?)?; let mut remote = repository.find_remote(&String::from_utf8_lossy(&remote))?; let refs = repository @@ -200,7 +218,7 @@ pub fn push_autosaves( .filter(|reference| { reference .name_bytes() - .starts_with(b"refs/autosave/autosaves") + .starts_with(format!("refs/autosave/autosaves/{id}").as_bytes()) }) .filter_map(|reference| reference.name().map(|n| n.to_string())) .map(|reference| format!("+{reference}:{reference}")) @@ -209,3 +227,165 @@ pub fn push_autosaves( Ok(()) } + +pub fn fetch_autosaves( + repository: &Repository, + callbacks: RemoteCallbacks<'_>, +) -> Result<(), git2::Error> { + let remote = repository.branch_upstream_remote(¤t_branch_ref(repository)?)?; + let mut remote = repository.find_remote(&String::from_utf8_lossy(&remote))?; + remote.fetch( + &["+refs/autosave/autosaves/*:refs/autosave/autosaves/*"], + Some(FetchOptions::new().remote_callbacks(callbacks)), + Some("Updated autosaves"), + )?; + + Ok(()) +} + +pub struct Autosave { + pub repo_id: String, + pub branch_name: String, + pub commit_id: Oid, + pub author: Option<String>, + pub email: Option<String>, + pub host_name: Option<String>, + pub time: Time, +} + +impl TryFrom<Reference<'_>> for Autosave { + type Error = git2::Error; + + fn try_from(value: Reference<'_>) -> Result<Self, Self::Error> { + Self::try_from(&value) + } +} + +impl TryFrom<&Reference<'_>> for Autosave { + type Error = git2::Error; + + fn try_from(reference: &Reference<'_>) -> Result<Self, Self::Error> { + let reference_name = reference.name().ok_or(git2::Error::new( + git2::ErrorCode::Invalid, + git2::ErrorClass::Reference, + "Reference is not valid UTF-8", + ))?; + let reference_name = reference_name + .strip_prefix("refs/autosave/autosaves/") + .unwrap_or(reference_name); + let Some((id, branch)) = reference_name.split_once('/') else { + return Err(git2::Error::new( + git2::ErrorCode::Invalid, + git2::ErrorClass::Reference, + "Reference does not follow format of refs/autosave/autosaves/{UUID}/{BRANCH}", + )); + }; + let commit = reference.peel_to_commit()?; + let message = commit.message(); + let host_name = message.and_then(|m| m.strip_prefix("Autosave: ")); + let author = commit.author(); + let author_name = author.name(); + let author_email = author.email(); + let time = commit.author().when(); + + Ok(Autosave { + repo_id: id.to_string(), + branch_name: branch.to_string(), + commit_id: commit.id(), + author: author_name.map(|s| s.to_string()), + email: author_email.map(|s| s.to_string()), + host_name: host_name.map(|s| s.to_string()), + time, + }) + } +} + +pub fn autosaves(repository: &Repository) -> Result<Vec<Autosave>, git2::Error> { + Ok(repository + .references()? + .filter_map(|reference| reference.ok()) + .filter(|reference| { + reference + .name_bytes() + .starts_with(b"refs/autosave/autosaves") + }) + .filter_map(|reference| reference.try_into().ok()) + .collect()) +} + +pub fn merge_commit_with_tree( + repository: &Repository, + commit: &Commit<'_>, + workdir: &Tree<'_>, +) -> Result<Oid, TreeError> { + Ok(repository + .merge_trees( + &repository + .find_commit( + repository + .merge_base(repository.head()?.peel_to_commit()?.id(), commit.id())?, + )? + .tree()?, + workdir, + &commit.tree()?, + Some(MergeOptions::new().find_renames(true).patience(true)), + )? + .write_tree()?) +} + +pub fn save_undo_tree(repository: &Repository, workdir: &Tree<'_>) -> Result<(), TreeError> { + let signature = repository.signature()?; + let commit = repository.commit( + None, + &signature, + &signature, + "Work done before autosave was restored", + workdir, + &[&repository.head()?.peel_to_commit()?], + )?; + repository.reference( + "refs/autosave/undo", + commit, + true, + "Save work before restoring autosave", + )?; + + Ok(()) +} + +pub fn saved_restored_autosave( + repository: &Repository, + autosave: &Autosave, +) -> Result<(), git2::Error> { + let author = if let Some(author) = &autosave.author + && let Some(email) = &autosave.email + { + Signature::new(author, email, &autosave.time)? + } else { + repository.signature()? + }; + let committer = repository.signature()?; + let message = format!( + "{}:{}/{}", + autosave.host_name.clone().unwrap_or_default(), + &autosave.repo_id, + &autosave.branch_name + ); + let tree = repository.find_commit(autosave.commit_id)?.tree()?; + let parent = repository + .find_reference("refs/autosave/restored") + .ok() + .and_then(|reference| reference.peel_to_commit().ok()); + let parents = parent.iter().collect::<Vec<_>>(); + + repository.commit( + Some("refs/autosave/restored"), + &author, + &committer, + &message, + &tree, + &parents, + )?; + + Ok(()) +} |
