Skip to content

Commit 65c10aa

Browse files
calvin-wan-googlesteadmon
authored andcommitted
libgit: add higher-level libgit crate
The C functions exported by libgit-sys do not provide an idiomatic Rust interface. To make it easier to use these functions via Rust, add a higher-level "libgit" crate, that wraps the lower-level configset API with an interface that is more Rust-y. This combination of $X and $X-sys crates is a common pattern for FFI in Rust, as documented in "The Cargo Book" [1]. [1] https://doc.rust-lang.org/cargo/reference/build-scripts.html#-sys-packages Co-authored-by: Josh Steadmon <[email protected]> Signed-off-by: Josh Steadmon <[email protected]> Signed-off-by: Calvin Wan <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent d76eb0d commit 65c10aa

File tree

13 files changed

+242
-8
lines changed

13 files changed

+242
-8
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,5 @@ Release/
250250
/git.VC.db
251251
*.dSYM
252252
/contrib/buildsystems/out
253+
/contrib/libgit-rs/target
253254
/contrib/libgit-sys/target

Makefile

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ include shared.mak
417417
# programs in oss-fuzz/.
418418
#
419419
# Define INCLUDE_LIBGIT_RS if you want `make all` and `make test` to build and
420-
# test the Rust crate in contrib/libgit-sys.
420+
# test the Rust crates in contrib/libgit-sys and contrib/libgit-rs.
421421
#
422422
# === Optional library: libintl ===
423423
#
@@ -3742,7 +3742,7 @@ clean: profile-clean coverage-clean cocciclean
37423742
$(RM) $(htmldocs).tar.gz $(manpages).tar.gz
37433743
$(MAKE) -C Documentation/ clean
37443744
$(RM) Documentation/GIT-EXCLUDED-PROGRAMS
3745-
$(RM) -r contrib/libgit-sys/target
3745+
$(RM) -r contrib/libgit-sys/target contrib/libgit-rs/target
37463746
$(RM) contrib/libgit-sys/partial_symbol_export.o
37473747
$(RM) contrib/libgit-sys/hidden_symbol_export.o
37483748
$(RM) contrib/libgit-sys/libgitpub.a
@@ -3908,14 +3908,14 @@ build-unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG)
39083908
unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG) t/helper/test-tool$X
39093909
$(MAKE) -C t/ unit-tests
39103910

3911-
.PHONY: libgit-sys
3912-
libgit-sys:
3911+
.PHONY: libgit-sys libgit-rs
3912+
libgit-sys libgit-rs:
39133913
$(QUIET)(\
3914-
cd contrib/libgit-sys && \
3914+
cd contrib/$@ && \
39153915
cargo build \
39163916
)
39173917
ifdef INCLUDE_LIBGIT_RS
3918-
all:: libgit-sys
3918+
all:: libgit-sys libgit-rs
39193919
endif
39203920

39213921
LIBGIT_PUB_OBJS += contrib/libgit-sys/public_symbol_export.o

contrib/libgit-rs/Cargo.lock

Lines changed: 77 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contrib/libgit-rs/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "libgit"
3+
version = "0.1.0"
4+
edition = "2021"
5+
build = "build.rs"
6+
rust-version = "1.63" # TODO: Once we hit 1.84 or newer, we may want to remove Cargo.lock from
7+
# version control. See https://lore.kernel.org/git/[email protected]/
8+
9+
10+
[lib]
11+
path = "src/lib.rs"
12+
13+
[dependencies]
14+
libgit-sys = { version = "0.1.0", path = "../libgit-sys" }
15+
16+
[build-dependencies]
17+
autocfg = "1.4.0"

contrib/libgit-rs/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# libgit-rs
2+
3+
Proof-of-concept Git bindings for Rust.
4+
5+
```toml
6+
[dependencies]
7+
libgit = "0.1.0"
8+
```
9+
10+
## Rust version requirements
11+
12+
libgit-rs should support Rust versions at least as old as the version included
13+
in Debian stable (currently 1.63).

contrib/libgit-rs/build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub fn main() {
2+
let ac = autocfg::new();
3+
ac.emit_has_path("std::ffi::c_char");
4+
}

contrib/libgit-rs/src/config.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use std::ffi::{c_void, CStr, CString};
2+
use std::path::Path;
3+
4+
#[cfg(has_std__ffi__c_char)]
5+
use std::ffi::{c_char, c_int};
6+
7+
#[cfg(not(has_std__ffi__c_char))]
8+
#[allow(non_camel_case_types)]
9+
type c_char = i8;
10+
11+
#[cfg(not(has_std__ffi__c_char))]
12+
#[allow(non_camel_case_types)]
13+
type c_int = i32;
14+
15+
use libgit_sys::*;
16+
17+
/// A ConfigSet is an in-memory cache for config-like files such as `.gitmodules` or `.gitconfig`.
18+
/// It does not support all config directives; notably, it will not process `include` or
19+
/// `includeIf` directives (but it will store them so that callers can choose whether and how to
20+
/// handle them).
21+
pub struct ConfigSet(*mut libgit_config_set);
22+
impl ConfigSet {
23+
/// Allocate a new ConfigSet
24+
pub fn new() -> Self {
25+
unsafe { ConfigSet(libgit_configset_alloc()) }
26+
}
27+
28+
/// Load the given files into the ConfigSet; conflicting directives in later files will
29+
/// override those given in earlier files.
30+
pub fn add_files(&mut self, files: &[&Path]) {
31+
for file in files {
32+
let pstr = file.to_str().expect("Invalid UTF-8");
33+
let rs = CString::new(pstr).expect("Couldn't convert to CString");
34+
unsafe {
35+
libgit_configset_add_file(self.0, rs.as_ptr());
36+
}
37+
}
38+
}
39+
40+
/// Load the value for the given key and attempt to parse it as an i32. Dies with a fatal error
41+
/// if the value cannot be parsed. Returns None if the key is not present.
42+
pub fn get_int(&mut self, key: &str) -> Option<i32> {
43+
let key = CString::new(key).expect("Couldn't convert to CString");
44+
let mut val: c_int = 0;
45+
unsafe {
46+
if libgit_configset_get_int(self.0, key.as_ptr(), &mut val as *mut c_int) != 0 {
47+
return None;
48+
}
49+
}
50+
51+
Some(val.into())
52+
}
53+
54+
/// Clones the value for the given key. Dies with a fatal error if the value cannot be
55+
/// converted to a String. Returns None if the key is not present.
56+
pub fn get_string(&mut self, key: &str) -> Option<String> {
57+
let key = CString::new(key).expect("Couldn't convert key to CString");
58+
let mut val: *mut c_char = std::ptr::null_mut();
59+
unsafe {
60+
if libgit_configset_get_string(self.0, key.as_ptr(), &mut val as *mut *mut c_char) != 0
61+
{
62+
return None;
63+
}
64+
let borrowed_str = CStr::from_ptr(val);
65+
let owned_str =
66+
String::from(borrowed_str.to_str().expect("Couldn't convert val to str"));
67+
free(val as *mut c_void); // Free the xstrdup()ed pointer from the C side
68+
Some(owned_str)
69+
}
70+
}
71+
}
72+
73+
impl Default for ConfigSet {
74+
fn default() -> Self {
75+
Self::new()
76+
}
77+
}
78+
79+
impl Drop for ConfigSet {
80+
fn drop(&mut self) {
81+
unsafe {
82+
libgit_configset_free(self.0);
83+
}
84+
}
85+
}
86+
87+
#[cfg(test)]
88+
mod tests {
89+
use super::*;
90+
91+
#[test]
92+
fn load_configs_via_configset() {
93+
let mut cs = ConfigSet::new();
94+
cs.add_files(&[
95+
Path::new("testdata/config1"),
96+
Path::new("testdata/config2"),
97+
Path::new("testdata/config3"),
98+
]);
99+
// ConfigSet retrieves correct value
100+
assert_eq!(cs.get_int("trace2.eventTarget"), Some(1));
101+
// ConfigSet respects last config value set
102+
assert_eq!(cs.get_int("trace2.eventNesting"), Some(3));
103+
// ConfigSet returns None for missing key
104+
assert_eq!(cs.get_string("foo.bar"), None);
105+
}
106+
}

contrib/libgit-rs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod config;

contrib/libgit-rs/testdata/config1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[trace2]
2+
eventNesting = 1

contrib/libgit-rs/testdata/config2

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[trace2]
2+
eventTarget = 1

0 commit comments

Comments
 (0)