summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-03-28 20:33:49 -0400
committerMica White <botahamec@outlook.com>2026-03-28 20:33:49 -0400
commit6d73f96706814e58d03761ae997972a6551c081e (patch)
tree272f4dbb898c2a89daddc07e53c5bea7ce47a1ea /src
parent2f71b45a753ff80cd9d7710be47b6777ed43e807 (diff)
MVP autosave command
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs141
1 files changed, 135 insertions, 6 deletions
diff --git a/src/lib.rs b/src/lib.rs
index f359745..1126b55 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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(&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(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(())
}