summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock121
-rw-r--r--Cargo.toml1
-rw-r--r--src/bin/git-autosave-daemon.rs4
-rw-r--r--src/bin/git-restore-autosave.rs128
-rw-r--r--src/lib.rs184
5 files changed, 434 insertions, 4 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 7e6c496..7553132 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -12,6 +12,15 @@ dependencies = [
]
[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -79,6 +88,12 @@ dependencies = [
]
[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -115,6 +130,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
name = "colog"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -163,6 +191,12 @@ dependencies = [
]
[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
name = "crossterm"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -398,6 +432,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"auth-git2",
+ "chrono",
"colog",
"confy",
"git2",
@@ -480,6 +515,30 @@ dependencies = [
]
[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
name = "icu_collections"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -874,6 +933,15 @@ dependencies = [
]
[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1527,12 +1595,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 85de251..c047e85 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,7 @@ notify = "8"
notify-debouncer-full = "0.7"
confy = "2"
happylock = "0.5"
+chrono = "0.4"
log = "0.4"
colog = "1"
hostname = "0.4"
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(())
+}