summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bin/git-restore-autosave.rs3
-rw-r--r--src/config.rs42
-rw-r--r--src/lib.rs36
-rw-r--r--src/utils.rs105
4 files changed, 171 insertions, 15 deletions
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<PathBuf>,
+ passwords: HashMap<String, (Option<String>, String)>,
+ passphrases: HashMap<PathBuf, String>,
+}
+
+impl Config {
+ pub fn load() -> Result<Config, ConfyError> {
+ confy::load("git-autosave", "git-autosaved")
+ }
+
+ pub fn save(&self) -> Result<(), ConfyError> {
+ confy::store("git-autosave", "git-autosaved", self)
+ }
+
+ pub fn repositories(&self) -> &HashSet<PathBuf> {
+ &self.repositories
+ }
+
+ pub fn repositories_mut(&mut self) -> &mut HashSet<PathBuf> {
+ &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<String, git2::Error> {
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<Uuid
let workdir = repository.workdir();
repository
.config()?
- .set_str("autosave.id", &id.to_string())?;
+ .set_str(AUTOSAVE_ID_CONFIG_KEY, &id.to_string())?;
if let Some(config) = config
&& let Some(workdir) = workdir
@@ -116,12 +128,10 @@ pub fn init(repository: &Repository, config: Option<&mut Config>) -> Result<Uuid
}
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 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<Vec<Autosave>, 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::<Vec<_>>();
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<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(path: &Path) -> std::io::Result<i32> {
+ 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<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.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<Oid, TreeError> {
+ 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<Oid, TreeError> {
+ 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)?)
+}