Merge pull request #2287 from gitbutlerapp/core-git

add core gitbutler-git crate
This commit is contained in:
Qix 2024-01-17 16:30:52 +01:00 committed by GitHub
commit 62c3322154
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 207 additions and 0 deletions

View File

@ -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

9
Cargo.lock generated
View File

@ -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"

View File

@ -3,6 +3,7 @@ members = [
"gitbutler-app",
"gitbutler-core",
"gitbutler-diff",
"gitbutler-git",
]
resolver = "2"

15
gitbutler-git/Cargo.toml Normal file
View File

@ -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 }

View File

@ -0,0 +1,2 @@
#[cfg(feature = "git2")]
pub mod git2;

View File

@ -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;

View File

@ -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<P: AsRef<Path>>(path: P) -> Result<Self, git2::Error> {
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<Option<String>, 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),
}
}
}

24
gitbutler-git/src/lib.rs Normal file
View File

@ -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};

30
gitbutler-git/src/ops.rs Normal file
View File

@ -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<R: Repository>(repo: &R) -> Result<bool, R::Error> {
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<R: Repository>(repo: &R, value: bool) -> Result<(), R::Error> {
repo.config_set(
"gitbutler.utmostDiscretion",
if value { "true" } else { "false" },
ConfigScope::Local,
)
.await
}

View File

@ -0,0 +1,2 @@
#[cfg(not(feature = "std"))]
pub use alloc::string::{String, ToString};

View File

@ -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<Option<String>, 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>;
}