/*
* git-init-autosave:
* - generate local repo UUID
* - add repo to autosave configuration (optionally)
*
* git-autosave:
* - convert workdir to tree
* - note hostname
* - commit workdir to `refs/autosave/{UUID}-{branch}`
* - push autosave ref to branch upstream
*
* git-autosave-daemon:
* - watch configuration directory
* - watch configured repositories
* - run autosave on change
*
* git-restore-autosave:
* - fetch upstream's autosaves
* - filter to autosaves from the local user and branch
* - display available autosaves if there is more than one
* - check for conflicts between workdir and autosave
* - save workdir to `refs/autosave/undo`
* - copy autosave to `refs/autosave/restored` with new commit
* - apply tree to workdir
* - allow `git restore-autosave --force`
* - allow a device to be specified by name or UUID
*
* 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
* - 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
*/
use std::collections::{HashMap, HashSet};
use std::fs::Metadata;
use std::path::{Path, PathBuf};
use confy::ConfyError;
use git2::{Oid, PushOptions, RemoteCallbacks, Repository};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Config {
repositories: HashSet<PathBuf>,
passwords: HashMap<String, (Option<String>, String)>,
passphrases: HashMap<PathBuf, String>,
}
impl Config {
pub fn username_for_url(&self, url: &str) -> Option<&String> {
self.passwords.get(url)?.0.as_ref()
}
pub fn password_for_url(&self, url: &str) -> Option<&String> {
Some(&self.passwords.get(url)?.1)
}
pub fn passphrase_for_key(&self, key: &Path) -> Option<&String> {
self.passphrases.get(key)
}
}
#[derive(Debug, Error)]
pub enum TreeError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Git(#[from] git2::Error),
}
pub fn load_config() -> Result<Config, ConfyError> {
confy::load("git-autosave", "git-autosaved")
}
pub fn save_config(config: &Config) -> Result<(), ConfyError> {
confy::store("git-autosave", "git-autosaved", config)
}
pub fn init(repository: &Repository, config: Option<&mut Config>) -> Result<(), git2::Error> {
let id = Uuid::new_v4();
let workdir = repository.workdir();
repository
.config()?
.set_str("autosave.id", &id.to_string())?;
if let Some(config) = config
&& let Some(workdir) = workdir
{
config.repositories.insert(workdir.into());
}
Ok(())
}
pub fn current_branch_ref(repository: &Repository) -> Result<String, git2::Error> {
let head = repository.head()?;
let ref_name = match head.kind() {
Some(git2::ReferenceType::Symbolic) => head.symbolic_target(),
_ => head.name(),
}
.ok_or(git2::Error::new(
git2::ErrorCode::Invalid,
git2::ErrorClass::Reference,
"The current branch's name is not valid UTF-8",
))?
.to_string();
Ok(ref_name)
}
pub fn current_branch(repository: &Repository) -> Result<String, git2::Error> {
let ref_name = current_branch_ref(repository)?;
let branch_name = ref_name
.strip_prefix("refs/heads/")
.unwrap_or(&ref_name)
.to_string();
Ok(branch_name)
}
pub fn filemode_for_dir_entry(metadata: &Metadata) -> i32 {
if metadata.is_dir() {
0o040000
} else if metadata.is_symlink() {
0o120000
} else if metadata.permissions().readonly() {
0o100644
} else {
0o100755
}
}
pub fn path_to_tree(repository: &Repository, path: &Path) -> Result<Oid, TreeError> {
let workdir = repository.workdir().expect("a non-bare repo");
if path.is_dir() {
let mut tree = repository.treebuilder(None)?;
for entry in path.read_dir()? {
let entry = entry?;
let full_path = entry.path();
let relative_path = full_path.strip_prefix(workdir).unwrap_or(path);
if repository.is_path_ignored(relative_path)? {
continue;
}
let filemode = filemode_for_dir_entry(&entry.metadata()?);
let oid = path_to_tree(repository, &entry.path())?;
let filename = entry.file_name();
tree.insert(filename, oid, filemode)?;
}
Ok(tree.write()?)
} else {
Ok(repository.blob_path(path)?)
}
}
pub fn workdir_to_tree(repository: &Repository) -> Result<Oid, TreeError> {
let Some(workdir) = repository.workdir() else {
return Err(TreeError::Git(git2::Error::new(
git2::ErrorCode::BareRepo,
git2::ErrorClass::Repository,
"git-autosave only supports worktrees",
)));
};
path_to_tree(repository, workdir)
}
pub fn commit_autosave(repository: &Repository) -> Result<Oid, TreeError> {
let config = repository.config()?;
let head = repository.head()?;
let uid = config.get_entry("autosave.id")?;
let uid = String::from_utf8_lossy(uid.value_bytes());
let branch = current_branch(repository)?;
let refname = &format!("refs/autosave/autosaves/{uid}/{branch}");
let signature = repository.signature()?;
let tree = repository.find_tree(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 remote = repository.branch_upstream_remote(¤t_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(b"refs/autosave/autosaves")
})
.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(())
}
|