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