From 96f63ec38fcdc8194a52c2b34b32f6e88ae64c34 Mon Sep 17 00:00:00 2001 From: Mica White Date: Mon, 30 Mar 2026 21:28:48 -0400 Subject: Refactor magic strings --- src/bin/git-restore-autosave.rs | 3 +- src/config.rs | 42 ++++++++++++++++ src/lib.rs | 36 +++++++++----- src/utils.rs | 105 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 src/config.rs create mode 100644 src/utils.rs diff --git a/src/bin/git-restore-autosave.rs b/src/bin/git-restore-autosave.rs index e02bb90..b7571fa 100644 --- a/src/bin/git-restore-autosave.rs +++ b/src/bin/git-restore-autosave.rs @@ -36,8 +36,7 @@ fn main() -> Result<(), anyhow::Error> { let force = std::env::args().any(|arg| arg == "--force"); let repository = Repository::discover(".")?; - let gitconfig = repository.config()?; - let repo_id = gitconfig.get_entry("autosave.id")?; + let repo_id = git_autosave::repository_id(&repository)?; let signature = repository.signature()?; let branch = git_autosave::utils::current_branch(&repository)?; let earliest_time = repository.head()?.peel_to_commit()?.time(); diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9ede643 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,42 @@ +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; + +use confy::ConfyError; +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Config { + repositories: HashSet, + passwords: HashMap, String)>, + passphrases: HashMap, +} + +impl Config { + pub fn load() -> Result { + confy::load("git-autosave", "git-autosaved") + } + + pub fn save(&self) -> Result<(), ConfyError> { + confy::store("git-autosave", "git-autosaved", self) + } + + pub fn repositories(&self) -> &HashSet { + &self.repositories + } + + pub fn repositories_mut(&mut self) -> &mut HashSet { + &mut self.repositories + } + + 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) + } +} diff --git a/src/lib.rs b/src/lib.rs index ecc202f..1cf0a98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,9 @@ /* + * 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 @@ -29,6 +34,13 @@ 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"; + pub struct Autosave { pub repo_id: String, pub branch_name: String, @@ -57,7 +69,7 @@ impl TryFrom<&Reference<'_>> for Autosave { "Reference is not valid UTF-8", ))?; let reference_name = reference_name - .strip_prefix("refs/autosave/autosaves/") + .strip_prefix(AUTOSAVE_ENTRIES_NAMESPACE) .unwrap_or(reference_name); let Some((id, branch)) = reference_name.split_once('/') else { return Err(git2::Error::new( @@ -89,7 +101,7 @@ impl TryFrom<&Reference<'_>> for Autosave { pub fn repository_id(repository: &Repository) -> Result { repository .config()? - .get_entry("autosave.id")? + .get_entry(AUTOSAVE_ID_CONFIG_KEY)? .value() .map(|s| s.to_string()) .ok_or(git2::Error::new( @@ -104,7 +116,7 @@ pub fn init(repository: &Repository, config: Option<&mut Config>) -> Result) -> Result Result { - 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 uid = repository_id(repository)?; let branch = utils::current_branch(repository)?; - let refname = &format!("refs/autosave/autosaves/{uid}/{branch}"); + 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()?; @@ -150,7 +160,7 @@ pub fn push_autosaves( .filter(|reference| { reference .name_bytes() - .starts_with(format!("refs/autosave/autosaves/{id}").as_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}")) @@ -167,7 +177,7 @@ pub fn fetch_autosaves( let remote = repository.branch_upstream_remote(&utils::current_branch_ref(repository)?)?; let mut remote = repository.find_remote(&String::from_utf8_lossy(&remote))?; remote.fetch( - &["+refs/autosave/autosaves/*:refs/autosave/autosaves/*"], + &[AUTOSAVE_REFSPEC], Some(FetchOptions::new().remote_callbacks(callbacks)), Some("Updated autosaves"), )?; @@ -182,7 +192,7 @@ pub fn autosaves(repository: &Repository) -> Result, git2::Error> .filter(|reference| { reference .name_bytes() - .starts_with(b"refs/autosave/autosaves") + .starts_with(AUTOSAVE_ENTRIES_NAMESPACE.as_bytes()) }) .filter_map(|reference| reference.try_into().ok()) .collect()) @@ -199,7 +209,7 @@ pub fn save_undo_tree(repository: &Repository, workdir: &Tree<'_>) -> Result<(), &[&repository.head()?.peel_to_commit()?], )?; repository.reference( - "refs/autosave/undo", + UNDO_RESTORE_REF, commit, true, "Save work before restoring autosave", @@ -228,13 +238,13 @@ pub fn saved_restored_autosave( ); let tree = repository.find_commit(autosave.commit_id)?.tree()?; let parent = repository - .find_reference("refs/autosave/restored") + .find_reference(RESTORED_AUTOSAVE_REF) .ok() .and_then(|reference| reference.peel_to_commit().ok()); let parents = parent.iter().collect::>(); repository.commit( - Some("refs/autosave/restored"), + Some(RESTORED_AUTOSAVE_REF), &author, &committer, &message, diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..dc0c082 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,105 @@ +use std::path::Path; + +use git2::{Commit, MergeOptions, Oid, Repository, Tree}; +use is_executable::is_executable; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum TreeError { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Git(#[from] git2::Error), +} + +pub fn current_branch_ref(repository: &Repository) -> Result { + 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 { + 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(path: &Path) -> std::io::Result { + let metadata = path.metadata()?; + Ok(if metadata.is_dir() { + 0o040000 + } else if metadata.is_symlink() { + 0o120000 + } else if is_executable(path) { + 0o100755 + } else { + 0o100644 + }) +} + +pub fn path_to_tree(repository: &Repository, path: &Path) -> Result { + 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.path())?; + 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 { + let Some(workdir) = repository.workdir() else { + return Err(TreeError::Git(git2::Error::new( + git2::ErrorCode::BareRepo, + git2::ErrorClass::Repository, + "git-autosave does not support bare repositories", + ))); + }; + + path_to_tree(repository, workdir) +} + +pub fn merge_commit_with_tree( + repository: &Repository, + commit: &Commit<'_>, + workdir: &Tree<'_>, +) -> Result { + 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_to(repository)?) +} -- cgit v1.2.3