summaryrefslogtreecommitdiff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs184
1 files changed, 182 insertions, 2 deletions
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(())
+}