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 <steadmon@google.com> Signed-off-by: Josh Steadmon <steadmon@google.com> Signed-off-by: Calvin Wan <calvinwan@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Calvin Wan committed
Jan 29, 2025 at 13:50 UTC
65c10aa8d5000e0ecab34a9652056f0520fe51ed
13 files changed
+242
-8
.gitignore
+1
@@ -250,4 +250,5 @@ Release/
250
/git.VC.db
251
*.dSYM
252
/contrib/buildsystems/out
253
+/contrib/libgit-rs/target
254
/contrib/libgit-sys/target
Makefile
+6
-6
@@ -417,7 +417,7 @@ include shared.mak
417
# programs in oss-fuzz/.
418
#
419
# 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.
421
#
422
# === Optional library: libintl ===
423
#
@@ -3742,7 +3742,7 @@ clean: profile-clean coverage-clean cocciclean
3742
$(RM) $(htmldocs).tar.gz $(manpages).tar.gz
3743
$(MAKE) -C Documentation/ clean
3744
$(RM) Documentation/GIT-EXCLUDED-PROGRAMS
3745
- $(RM) -r contrib/libgit-sys/target
3745
+ $(RM) -r contrib/libgit-sys/target contrib/libgit-rs/target
3746
$(RM) contrib/libgit-sys/partial_symbol_export.o
3747
$(RM) contrib/libgit-sys/hidden_symbol_export.o
3748
$(RM) contrib/libgit-sys/libgitpub.a
@@ -3908,14 +3908,14 @@ build-unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG)
3908
unit-tests: $(UNIT_TEST_PROGS) $(CLAR_TEST_PROG) t/helper/test-tool$X
3909
$(MAKE) -C t/ unit-tests
3910
3911
-.PHONY: libgit-sys
3912
-libgit-sys:
3911
+.PHONY: libgit-sys libgit-rs
3912
+libgit-sys libgit-rs:
3913
$(QUIET)(\
3914
- cd contrib/libgit-sys && \
3914
+ cd contrib/$@ && \
3915
cargo build \
3916
)
3917
ifdef INCLUDE_LIBGIT_RS
3918
-all:: libgit-sys
3918
+all:: libgit-sys libgit-rs
3919
endif
3920
3921
LIBGIT_PUB_OBJS += contrib/libgit-sys/public_symbol_export.o
contrib/libgit-rs/Cargo.lock
new
+77
@@ -0,0 +1,77 @@
1
+# This file is automatically @generated by Cargo.
2
+# It is not intended for manual editing.
3
+version = 3
4
+
5
+[[package]]
6
+name = "autocfg"
7
+version = "1.4.0"
8
+source = "registry+https://github.com/rust-lang/crates.io-index"
9
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
10
+
11
+[[package]]
12
+name = "cc"
13
+version = "1.1.15"
14
+source = "registry+https://github.com/rust-lang/crates.io-index"
15
+checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
16
+dependencies = [
17
+ "shlex",
18
+]
19
+
20
+[[package]]
21
+name = "libc"
22
+version = "0.2.158"
23
+source = "registry+https://github.com/rust-lang/crates.io-index"
24
+checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
25
+
26
+[[package]]
27
+name = "libgit"
28
+version = "0.1.0"
29
+dependencies = [
30
+ "autocfg",
31
+ "libgit-sys",
32
+]
33
+
34
+[[package]]
35
+name = "libgit-sys"
36
+version = "0.1.0"
37
+dependencies = [
38
+ "autocfg",
39
+ "libz-sys",
40
+ "make-cmd",
41
+]
42
+
43
+[[package]]
44
+name = "libz-sys"
45
+version = "1.1.20"
46
+source = "registry+https://github.com/rust-lang/crates.io-index"
47
+checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
48
+dependencies = [
49
+ "cc",
50
+ "libc",
51
+ "pkg-config",
52
+ "vcpkg",
53
+]
54
+
55
+[[package]]
56
+name = "make-cmd"
57
+version = "0.1.0"
58
+source = "registry+https://github.com/rust-lang/crates.io-index"
59
+checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3"
60
+
61
+[[package]]
62
+name = "pkg-config"
63
+version = "0.3.30"
64
+source = "registry+https://github.com/rust-lang/crates.io-index"
65
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
66
+
67
+[[package]]
68
+name = "shlex"
69
+version = "1.3.0"
70
+source = "registry+https://github.com/rust-lang/crates.io-index"
71
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
72
+
73
+[[package]]
74
+name = "vcpkg"
75
+version = "0.2.15"
76
+source = "registry+https://github.com/rust-lang/crates.io-index"
77
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
contrib/libgit-rs/Cargo.toml
new
+17
@@ -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/Z47jgK-oMjFRSslr@tapette.crustytoothpaste.net/
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
new
+13
@@ -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
new
+4
@@ -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
new
+106
@@ -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
new
+1
@@ -0,0 +1 @@
1
+pub mod config;
contrib/libgit-rs/testdata/config1
new
+2
@@ -0,0 +1,2 @@
1
+[trace2]
2
+ eventNesting = 1
contrib/libgit-rs/testdata/config2
new
+2
@@ -0,0 +1,2 @@
1
+[trace2]
2
+ eventTarget = 1
contrib/libgit-rs/testdata/config3
new
+2
@@ -0,0 +1,2 @@
1
+[trace2]
2
+ eventNesting = 3
contrib/libgit-sys/src/lib.rs
+4
@@ -1,3 +1,5 @@
1
+use std::ffi::c_void;
2
+
3
#[cfg(has_std__ffi__c_char)]
4
use std::ffi::{c_char, c_int};
5
@@ -19,6 +21,8 @@ pub struct libgit_config_set {
21
}
22
23
extern "C" {
24
+ pub fn free(ptr: *mut c_void);
25
+
26
pub fn libgit_user_agent() -> *const c_char;
27
pub fn libgit_user_agent_sanitized() -> *const c_char;
28
t/Makefile
+7
-2
@@ -178,12 +178,17 @@ perf:
178
.PHONY: pre-clean $(T) aggregate-results clean valgrind perf \
179
check-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)
180
181
-.PHONY: libgit-sys-test
181
+.PHONY: libgit-sys-test libgit-rs-test
182
libgit-sys-test:
183
$(QUIET)(\
184
cd ../contrib/libgit-sys && \
185
cargo test \
186
)
187
+libgit-rs-test:
188
+ $(QUIET)(\
189
+ cd ../contrib/libgit-rs && \
190
+ cargo test \
191
+ )
192
ifdef INCLUDE_LIBGIT_RS
188
-all:: libgit-sys-test
193
+all:: libgit-sys-test libgit-rs-test
194
endif