From bf607c0029b35f3fd733e84a48379816b1c0509a Mon Sep 17 00:00:00 2001 From: Josh Junon Date: Wed, 17 Jan 2024 16:21:21 +0100 Subject: [PATCH] add core gitbutler-git crate --- .github/workflows/push.yaml | 1 + Cargo.lock | 9 +++ Cargo.toml | 1 + gitbutler-git/Cargo.toml | 15 +++++ gitbutler-git/src/backend.rs | 2 + gitbutler-git/src/backend/git2.rs | 8 +++ gitbutler-git/src/backend/git2/repository.rs | 71 ++++++++++++++++++++ gitbutler-git/src/lib.rs | 24 +++++++ gitbutler-git/src/ops.rs | 30 +++++++++ gitbutler-git/src/prelude.rs | 2 + gitbutler-git/src/repository.rs | 44 ++++++++++++ 11 files changed, 207 insertions(+) create mode 100644 gitbutler-git/Cargo.toml create mode 100644 gitbutler-git/src/backend.rs create mode 100644 gitbutler-git/src/backend/git2.rs create mode 100644 gitbutler-git/src/backend/git2/repository.rs create mode 100644 gitbutler-git/src/lib.rs create mode 100644 gitbutler-git/src/ops.rs create mode 100644 gitbutler-git/src/prelude.rs create mode 100644 gitbutler-git/src/repository.rs diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 9b3580a46..6efd58ad5 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -77,3 +77,4 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/init-env-rust - run: cargo clippy --all-targets --all-features --tests + - run: cargo clippy --no-default-features --tests -p gitbutler-git diff --git a/Cargo.lock b/Cargo.lock index 44dd58a33..8d8ecc2f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1871,6 +1871,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gitbutler-git" +version = "0.0.0" +dependencies = [ + "git2", + "serde", + "thiserror", +] + [[package]] name = "glib" version = "0.15.12" diff --git a/Cargo.toml b/Cargo.toml index 4a0694dc5..da0c13091 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "gitbutler-app", "gitbutler-core", "gitbutler-diff", + "gitbutler-git", ] resolver = "2" diff --git a/gitbutler-git/Cargo.toml b/gitbutler-git/Cargo.toml new file mode 100644 index 000000000..1d998825a --- /dev/null +++ b/gitbutler-git/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "gitbutler-git" +version = "0.0.0" +edition = "2021" + +[features] +default = ["git2", "serde"] +git2 = ["dep:git2", "std"] +serde = ["dep:serde"] +std = [] + +[dependencies] +git2 = { workspace = true, optional = true } +thiserror.workspace = true +serde = { workspace = true, optional = true } \ No newline at end of file diff --git a/gitbutler-git/src/backend.rs b/gitbutler-git/src/backend.rs new file mode 100644 index 000000000..4dde2af1a --- /dev/null +++ b/gitbutler-git/src/backend.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "git2")] +pub mod git2; diff --git a/gitbutler-git/src/backend/git2.rs b/gitbutler-git/src/backend/git2.rs new file mode 100644 index 000000000..74b308f46 --- /dev/null +++ b/gitbutler-git/src/backend/git2.rs @@ -0,0 +1,8 @@ +//! [libgit2](https://libgit2.org/) implementation of +//! the core `gitbutler-git` library traits. +//! +//! The entry point for this module is the [`Repository`] struct. + +mod repository; + +pub use self::repository::Repository; diff --git a/gitbutler-git/src/backend/git2/repository.rs b/gitbutler-git/src/backend/git2/repository.rs new file mode 100644 index 000000000..af541dd3f --- /dev/null +++ b/gitbutler-git/src/backend/git2/repository.rs @@ -0,0 +1,71 @@ +use std::path::Path; + +use crate::ConfigScope; + +/// A [`crate::Repository`] implementation using the `git2` crate. +pub struct Repository { + repo: git2::Repository, +} + +impl Repository { + /// Opens a repository at the given path. + #[inline] + pub fn open>(path: P) -> Result { + Ok(Self { + repo: git2::Repository::open(path)?, + }) + } +} + +impl crate::Repository for Repository { + type Error = git2::Error; + + async fn config_get( + &self, + key: &str, + scope: ConfigScope, + ) -> Result, Self::Error> { + let config = self.repo.config()?; + + let res = match scope { + ConfigScope::Auto => config.get_string(key), + ConfigScope::Local => config.open_level(git2::ConfigLevel::Local)?.get_string(key), + ConfigScope::System => config + .open_level(git2::ConfigLevel::System)? + .get_string(key), + ConfigScope::Global => config + .open_level(git2::ConfigLevel::Global)? + .get_string(key), + }; + + res.map(Some).or_else(|e| { + if e.code() == git2::ErrorCode::NotFound { + Ok(None) + } else { + Err(e) + } + }) + } + + async fn config_set( + &self, + key: &str, + value: &str, + scope: ConfigScope, + ) -> Result<(), Self::Error> { + let mut config = self.repo.config()?; + + match scope { + ConfigScope::Auto => config.set_str(key, value), + ConfigScope::Local => config + .open_level(git2::ConfigLevel::Local)? + .set_str(key, value), + ConfigScope::System => config + .open_level(git2::ConfigLevel::System)? + .set_str(key, value), + ConfigScope::Global => config + .open_level(git2::ConfigLevel::Global)? + .set_str(key, value), + } + } +} diff --git a/gitbutler-git/src/lib.rs b/gitbutler-git/src/lib.rs new file mode 100644 index 000000000..14aaf6687 --- /dev/null +++ b/gitbutler-git/src/lib.rs @@ -0,0 +1,24 @@ +//! GitButler core library for interacting with Git. +//! +//! This library houses a number of Git implementations, +//! over which we abstract a common interface and provide +//! higher-level operations that are implementation-agnostic. + +#![cfg_attr(not(feature = "std"), no_std)] // must be first +#![feature(error_in_core)] +#![deny(missing_docs, unsafe_code)] +#![allow(async_fn_in_trait)] + +#[cfg(not(feature = "std"))] +extern crate alloc; + +mod backend; +pub mod ops; +mod repository; + +pub(crate) mod prelude; + +#[cfg(feature = "git2")] +pub use backend::git2; + +pub use self::repository::{ConfigScope, Repository}; diff --git a/gitbutler-git/src/ops.rs b/gitbutler-git/src/ops.rs new file mode 100644 index 000000000..a317cc66b --- /dev/null +++ b/gitbutler-git/src/ops.rs @@ -0,0 +1,30 @@ +//! High-level operations that are implementation-agnostic. +//! +//! These operations are similar to Git's non-plumbing commands, +//! in that they compose both high- and low-level operations +//! into more complex operations, without caring about the +//! underlying implementation. + +#[allow(unused_imports)] +use crate::prelude::*; + +use crate::{ConfigScope, Repository}; + +/// Returns whether or not the repository has GitButler's +/// utmost discretion enabled. +pub async fn has_utmost_discretion(repo: &R) -> Result { + let config = repo + .config_get("gitbutler.utmostDiscretion", ConfigScope::Auto) + .await?; + Ok(config == Some("true".to_string())) +} + +/// Sets whether or not the repository has GitButler's utmost discretion. +pub async fn set_utmost_discretion(repo: &R, value: bool) -> Result<(), R::Error> { + repo.config_set( + "gitbutler.utmostDiscretion", + if value { "true" } else { "false" }, + ConfigScope::Local, + ) + .await +} diff --git a/gitbutler-git/src/prelude.rs b/gitbutler-git/src/prelude.rs new file mode 100644 index 000000000..365484d3b --- /dev/null +++ b/gitbutler-git/src/prelude.rs @@ -0,0 +1,2 @@ +#[cfg(not(feature = "std"))] +pub use alloc::string::{String, ToString}; diff --git a/gitbutler-git/src/repository.rs b/gitbutler-git/src/repository.rs new file mode 100644 index 000000000..ff190fa6a --- /dev/null +++ b/gitbutler-git/src/repository.rs @@ -0,0 +1,44 @@ +#[allow(unused_imports)] +use crate::prelude::*; + +/// The scope from/to which a configuration value is read/written. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum ConfigScope { + /// Pull from the most appropriate scope. + /// This is the default, and will fall back to a higher + /// scope if the value is not initially found. + #[default] + Auto = 0, + /// Pull from the local scope (`.git/config`) _only_. + Local = 1, + /// Pull from the system-wide scope (`${prefix}/etc/gitconfig`) _only_. + System = 2, + /// Pull from the global (user) scope (typically `~/.gitconfig`) _only_. + Global = 3, +} + +/// A handle to an open Git repository. +pub trait Repository { + /// The type of error returned by this repository. + type Error: core::error::Error + Send + Sync + 'static; + + /// Reads a configuration value. + /// + /// Errors if the value is not valid UTF-8. + async fn config_get( + &self, + key: &str, + scope: ConfigScope, + ) -> Result, Self::Error>; + + /// Writes a configuration value. + /// + /// Errors if the new value is not valid UTF-8. + async fn config_set( + &self, + key: &str, + value: &str, + scope: ConfigScope, + ) -> Result<(), Self::Error>; +}