diff options
| author | Mica White <botahamec@outlook.com> | 2026-03-28 20:33:49 -0400 |
|---|---|---|
| committer | Mica White <botahamec@outlook.com> | 2026-03-28 20:33:49 -0400 |
| commit | 6d73f96706814e58d03761ae997972a6551c081e (patch) | |
| tree | 272f4dbb898c2a89daddc07e53c5bea7ce47a1ea /src | |
| parent | 2f71b45a753ff80cd9d7710be47b6777ed43e807 (diff) | |
MVP autosave command
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 141 |
1 files changed, 135 insertions, 6 deletions
@@ -39,11 +39,13 @@ * - allow force */ -use std::path::PathBuf; +use std::fs::Metadata; +use std::path::{Path, PathBuf}; use confy::ConfyError; -use git2::{Error, Repository}; +use git2::{Oid, PushOptions, RemoteCallbacks, Repository}; use serde::{Deserialize, Serialize}; +use thiserror::Error; use uuid::Uuid; #[derive(Default, Serialize, Deserialize)] @@ -51,6 +53,14 @@ pub struct Config { repositories: Vec<PathBuf>, } +#[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") } @@ -59,14 +69,133 @@ 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<(), Error> { +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())?; + repository + .config()? + .set_str("autosave.id", &id.to_string())?; - if let Some(config) = config && let Some(workdir) = workdir { + if let Some(config) = config + && let Some(workdir) = workdir + { config.repositories.push(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(()) } |
