/*
* git-cat-autosave:
* - select an autosave
* - create a tree that merges autosave and workdir
* - show the diff of tree with workdir
*
* git-merge-autosave:
* - error is workdir is dirty
* - 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:
* - check for differences between workdir and restored save
* - error if there are unsaved changes
* - apply `refs/autosave/undo` to tree
* - allow force
*
* git repair-autosave
*/
use git2::{
FetchOptions, Oid, PushOptions, Reference, RemoteCallbacks, Repository, Signature, Time, Tree,
};
use uuid::Uuid;
pub mod authenticate;
pub mod config;
pub mod utils;
pub use config::Config;
use utils::TreeError;
pub const AUTOSAVE_ID_CONFIG_KEY: &str = "autosave.id";
pub const AUTOSAVE_REF_NAMESPACE: &str = "refs/autosave/";
pub const AUTOSAVE_ENTRIES_NAMESPACE: &str = "refs/autosave/autosaves/";
pub const AUTOSAVE_REFSPEC: &str = "+refs/autosave/autosaves/*:refs/autosave/autosaves/*";
pub const UNDO_RESTORE_REF: &str = "refs/autosave/undo";
pub const RESTORED_AUTOSAVE_REF: &str = "refs/autosave/restored";
#[derive(Debug)]
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(AUTOSAVE_ENTRIES_NAMESPACE)
.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 repository_id(repository: &Repository) -> Result<String, git2::Error> {
repository
.config()?
.get_entry(AUTOSAVE_ID_CONFIG_KEY)?
.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();
repository
.config()?
.set_str(AUTOSAVE_ID_CONFIG_KEY, &id.to_string())?;
if let Some(config) = config
&& let Some(workdir) = workdir
{
config.repositories_mut().insert(workdir.into());
}
Ok(id)
}
pub fn commit_autosave(repository: &Repository) -> Result<Oid, TreeError> {
let head = repository.head()?;
let uid = repository_id(repository)?;
let branch = utils::current_branch(repository)?;
let refname = &format!("{AUTOSAVE_ENTRIES_NAMESPACE}{uid}/{branch}");
let signature = repository.signature()?;
let tree = repository.find_tree(utils::workdir_to_tree(repository)?)?;
let parent = head.peel_to_commit()?;
let hostname = hostname::get();
let hostname = hostname
.map(|hostname| hostname.to_string_lossy().into_owned())
.map(|hostname| format!("Autosave: {hostname}"))
.unwrap_or_else(|_| "Autosave".to_string());
let commit = repository.commit(None, &signature, &signature, &hostname, &tree, &[&parent])?;
repository.reference(refname, commit, true, "Autosaved {branch}")?;
Ok(commit)
}
pub fn push_autosaves(
repository: &Repository,
callbacks: RemoteCallbacks<'_>,
) -> Result<(), git2::Error> {
let id = repository_id(repository)?;
let remote = repository.branch_upstream_remote(&utils::current_branch_ref(repository)?)?;
let mut remote = repository.find_remote(&String::from_utf8_lossy(&remote))?;
let refs = repository
.references()?
.filter_map(|reference| reference.ok())
.filter(|reference| {
reference
.name_bytes()
.starts_with(format!("{AUTOSAVE_ENTRIES_NAMESPACE}{id}").as_bytes())
})
.filter_map(|reference| reference.name().map(|n| n.to_string()))
.map(|reference| format!("+{reference}:{reference}"))
.collect::<Vec<_>>();
remote.push(&refs, Some(PushOptions::new().remote_callbacks(callbacks)))?;
Ok(())
}
pub fn fetch_autosaves(
repository: &Repository,
callbacks: RemoteCallbacks<'_>,
) -> Result<(), git2::Error> {
let remote = repository.branch_upstream_remote(&utils::current_branch_ref(repository)?)?;
let mut remote = repository.find_remote(&String::from_utf8_lossy(&remote))?;
remote.fetch(
&[AUTOSAVE_REFSPEC],
Some(FetchOptions::new().remote_callbacks(callbacks)),
Some("Updated autosaves"),
)?;
Ok(())
}
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(AUTOSAVE_ENTRIES_NAMESPACE.as_bytes())
})
.filter_map(|reference| reference.try_into().ok())
.collect())
}
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(
UNDO_RESTORE_REF,
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(RESTORED_AUTOSAVE_REF)
.ok()
.and_then(|reference| reference.peel_to_commit().ok());
let parents = parent.iter().collect::<Vec<_>>();
repository.commit(
Some(RESTORED_AUTOSAVE_REF),
&author,
&committer,
&message,
&tree,
&parents,
)?;
Ok(())
}
|