summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-03-29 19:55:31 -0400
committerMica White <botahamec@outlook.com>2026-03-29 19:55:31 -0400
commit628dbdaffdf97db4db4e00777dbb3c4072019f64 (patch)
treea0ab69ad98c85f76086e0465dc090a89a15e03dc /src
parent634f6da6186cd9bdcc0e3dde3a16085ad59536e3 (diff)
Added git-restore-autosave
Diffstat (limited to 'src')
-rw-r--r--src/bin/git-autosave-daemon.rs4
-rw-r--r--src/bin/git-restore-autosave.rs128
-rw-r--r--src/lib.rs184
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(())
+}
diff --git a/src/lib.rs b/src/lib.rs
index add6e56..839403b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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(&current_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(&current_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(())
+}