Merge branch 'js/libgit-rust'

Foreign language interface for Rust into our code base has been added.

* js/libgit-rust:
  libgit: add higher-level libgit crate
  libgit-sys: also export some config_set functions
  libgit-sys: introduce Rust wrapper for libgit.a
  common-main: split init and exit code into new files
This commit is contained in:
Junio C Hamano
2025-02-12 10:08:53 -08:00
24 changed files with 673 additions and 81 deletions

77
contrib/libgit-rs/Cargo.lock generated Normal file
View File

@ -0,0 +1,77 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "cc"
version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
dependencies = [
"shlex",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libgit"
version = "0.1.0"
dependencies = [
"autocfg",
"libgit-sys",
]
[[package]]
name = "libgit-sys"
version = "0.1.0"
dependencies = [
"autocfg",
"libz-sys",
"make-cmd",
]
[[package]]
name = "libz-sys"
version = "1.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "make-cmd"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"

View File

@ -0,0 +1,17 @@
[package]
name = "libgit"
version = "0.1.0"
edition = "2021"
build = "build.rs"
rust-version = "1.63" # TODO: Once we hit 1.84 or newer, we may want to remove Cargo.lock from
# version control. See https://lore.kernel.org/git/Z47jgK-oMjFRSslr@tapette.crustytoothpaste.net/
[lib]
path = "src/lib.rs"
[dependencies]
libgit-sys = { version = "0.1.0", path = "../libgit-sys" }
[build-dependencies]
autocfg = "1.4.0"

View File

@ -0,0 +1,13 @@
# libgit-rs
Proof-of-concept Git bindings for Rust.
```toml
[dependencies]
libgit = "0.1.0"
```
## Rust version requirements
libgit-rs should support Rust versions at least as old as the version included
in Debian stable (currently 1.63).

View File

@ -0,0 +1,4 @@
pub fn main() {
let ac = autocfg::new();
ac.emit_has_path("std::ffi::c_char");
}

View File

@ -0,0 +1,106 @@
use std::ffi::{c_void, CStr, CString};
use std::path::Path;
#[cfg(has_std__ffi__c_char)]
use std::ffi::{c_char, c_int};
#[cfg(not(has_std__ffi__c_char))]
#[allow(non_camel_case_types)]
type c_char = i8;
#[cfg(not(has_std__ffi__c_char))]
#[allow(non_camel_case_types)]
type c_int = i32;
use libgit_sys::*;
/// A ConfigSet is an in-memory cache for config-like files such as `.gitmodules` or `.gitconfig`.
/// It does not support all config directives; notably, it will not process `include` or
/// `includeIf` directives (but it will store them so that callers can choose whether and how to
/// handle them).
pub struct ConfigSet(*mut libgit_config_set);
impl ConfigSet {
/// Allocate a new ConfigSet
pub fn new() -> Self {
unsafe { ConfigSet(libgit_configset_alloc()) }
}
/// Load the given files into the ConfigSet; conflicting directives in later files will
/// override those given in earlier files.
pub fn add_files(&mut self, files: &[&Path]) {
for file in files {
let pstr = file.to_str().expect("Invalid UTF-8");
let rs = CString::new(pstr).expect("Couldn't convert to CString");
unsafe {
libgit_configset_add_file(self.0, rs.as_ptr());
}
}
}
/// Load the value for the given key and attempt to parse it as an i32. Dies with a fatal error
/// if the value cannot be parsed. Returns None if the key is not present.
pub fn get_int(&mut self, key: &str) -> Option<i32> {
let key = CString::new(key).expect("Couldn't convert to CString");
let mut val: c_int = 0;
unsafe {
if libgit_configset_get_int(self.0, key.as_ptr(), &mut val as *mut c_int) != 0 {
return None;
}
}
Some(val.into())
}
/// Clones the value for the given key. Dies with a fatal error if the value cannot be
/// converted to a String. Returns None if the key is not present.
pub fn get_string(&mut self, key: &str) -> Option<String> {
let key = CString::new(key).expect("Couldn't convert key to CString");
let mut val: *mut c_char = std::ptr::null_mut();
unsafe {
if libgit_configset_get_string(self.0, key.as_ptr(), &mut val as *mut *mut c_char) != 0
{
return None;
}
let borrowed_str = CStr::from_ptr(val);
let owned_str =
String::from(borrowed_str.to_str().expect("Couldn't convert val to str"));
free(val as *mut c_void); // Free the xstrdup()ed pointer from the C side
Some(owned_str)
}
}
}
impl Default for ConfigSet {
fn default() -> Self {
Self::new()
}
}
impl Drop for ConfigSet {
fn drop(&mut self) {
unsafe {
libgit_configset_free(self.0);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_configs_via_configset() {
let mut cs = ConfigSet::new();
cs.add_files(&[
Path::new("testdata/config1"),
Path::new("testdata/config2"),
Path::new("testdata/config3"),
]);
// ConfigSet retrieves correct value
assert_eq!(cs.get_int("trace2.eventTarget"), Some(1));
// ConfigSet respects last config value set
assert_eq!(cs.get_int("trace2.eventNesting"), Some(3));
// ConfigSet returns None for missing key
assert_eq!(cs.get_string("foo.bar"), None);
}
}

View File

@ -0,0 +1 @@
pub mod config;

2
contrib/libgit-rs/testdata/config1 vendored Normal file
View File

@ -0,0 +1,2 @@
[trace2]
eventNesting = 1

2
contrib/libgit-rs/testdata/config2 vendored Normal file
View File

@ -0,0 +1,2 @@
[trace2]
eventTarget = 1

2
contrib/libgit-rs/testdata/config3 vendored Normal file
View File

@ -0,0 +1,2 @@
[trace2]
eventNesting = 3

69
contrib/libgit-sys/Cargo.lock generated Normal file
View File

@ -0,0 +1,69 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "cc"
version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
dependencies = [
"shlex",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libgit-sys"
version = "0.1.0"
dependencies = [
"autocfg",
"libz-sys",
"make-cmd",
]
[[package]]
name = "libz-sys"
version = "1.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "make-cmd"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"

View File

@ -0,0 +1,19 @@
[package]
name = "libgit-sys"
version = "0.1.0"
edition = "2021"
build = "build.rs"
links = "gitpub"
rust-version = "1.63" # TODO: Once we hit 1.84 or newer, we may want to remove Cargo.lock from
# version control. See https://lore.kernel.org/git/Z47jgK-oMjFRSslr@tapette.crustytoothpaste.net/
description = "Native bindings to a portion of libgit"
[lib]
path = "src/lib.rs"
[dependencies]
libz-sys = "1.1.19"
[build-dependencies]
autocfg = "1.4.0"
make-cmd = "0.1.0"

View File

@ -0,0 +1,4 @@
# libgit-sys
A small proof-of-concept crate showing how to provide a Rust FFI to Git
internals.

View File

@ -0,0 +1,35 @@
use std::env;
use std::path::PathBuf;
pub fn main() -> std::io::Result<()> {
let ac = autocfg::new();
ac.emit_has_path("std::ffi::c_char");
let crate_root = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let git_root = crate_root.join("../..");
let dst = PathBuf::from(env::var_os("OUT_DIR").unwrap());
let make_output = make_cmd::gnu_make()
.env("DEVELOPER", "1")
.env_remove("PROFILE")
.current_dir(git_root.clone())
.args([
"INCLUDE_LIBGIT_RS=YesPlease",
"contrib/libgit-sys/libgitpub.a",
])
.output()
.expect("Make failed to run");
if !make_output.status.success() {
panic!(
"Make failed:\n stdout = {}\n stderr = {}\n",
String::from_utf8(make_output.stdout).unwrap(),
String::from_utf8(make_output.stderr).unwrap()
);
}
std::fs::copy(crate_root.join("libgitpub.a"), dst.join("libgitpub.a"))?;
println!("cargo:rustc-link-search=native={}", dst.display());
println!("cargo:rustc-link-lib=gitpub");
println!("cargo:rerun-if-changed={}", git_root.display());
Ok(())
}

View File

@ -0,0 +1,59 @@
/*
* Shim to publicly export Git symbols. These must be renamed so that the
* original symbols can be hidden. Renaming these with a "libgit_" prefix also
* avoids conflicts with other libraries such as libgit2.
*/
#include "git-compat-util.h"
#include "config.h"
#include "contrib/libgit-sys/public_symbol_export.h"
#include "version.h"
#pragma GCC visibility push(default)
struct libgit_config_set {
struct config_set cs;
};
struct libgit_config_set *libgit_configset_alloc(void)
{
struct libgit_config_set *cs =
xmalloc(sizeof(struct libgit_config_set));
git_configset_init(&cs->cs);
return cs;
}
void libgit_configset_free(struct libgit_config_set *cs)
{
git_configset_clear(&cs->cs);
free(cs);
}
int libgit_configset_add_file(struct libgit_config_set *cs, const char *filename)
{
return git_configset_add_file(&cs->cs, filename);
}
int libgit_configset_get_int(struct libgit_config_set *cs, const char *key,
int *dest)
{
return git_configset_get_int(&cs->cs, key, dest);
}
int libgit_configset_get_string(struct libgit_config_set *cs, const char *key,
char **dest)
{
return git_configset_get_string(&cs->cs, key, dest);
}
const char *libgit_user_agent(void)
{
return git_user_agent();
}
const char *libgit_user_agent_sanitized(void)
{
return git_user_agent_sanitized();
}
#pragma GCC visibility pop

View File

@ -0,0 +1,18 @@
#ifndef PUBLIC_SYMBOL_EXPORT_H
#define PUBLIC_SYMBOL_EXPORT_H
struct libgit_config_set *libgit_configset_alloc(void);
void libgit_configset_free(struct libgit_config_set *cs);
int libgit_configset_add_file(struct libgit_config_set *cs, const char *filename);
int libgit_configset_get_int(struct libgit_config_set *cs, const char *key, int *dest);
int libgit_configset_get_string(struct libgit_config_set *cs, const char *key, char **dest);
const char *libgit_user_agent(void);
const char *libgit_user_agent_sanitized(void);
#endif /* PUBLIC_SYMBOL_EXPORT_H */

View File

@ -0,0 +1,79 @@
use std::ffi::c_void;
#[cfg(has_std__ffi__c_char)]
use std::ffi::{c_char, c_int};
#[cfg(not(has_std__ffi__c_char))]
#[allow(non_camel_case_types)]
pub type c_char = i8;
#[cfg(not(has_std__ffi__c_char))]
#[allow(non_camel_case_types)]
pub type c_int = i32;
extern crate libz_sys;
#[allow(non_camel_case_types)]
#[repr(C)]
pub struct libgit_config_set {
_data: [u8; 0],
_marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}
extern "C" {
pub fn free(ptr: *mut c_void);
pub fn libgit_user_agent() -> *const c_char;
pub fn libgit_user_agent_sanitized() -> *const c_char;
pub fn libgit_configset_alloc() -> *mut libgit_config_set;
pub fn libgit_configset_free(cs: *mut libgit_config_set);
pub fn libgit_configset_add_file(cs: *mut libgit_config_set, filename: *const c_char) -> c_int;
pub fn libgit_configset_get_int(
cs: *mut libgit_config_set,
key: *const c_char,
int: *mut c_int,
) -> c_int;
pub fn libgit_configset_get_string(
cs: *mut libgit_config_set,
key: *const c_char,
dest: *mut *mut c_char,
) -> c_int;
}
#[cfg(test)]
mod tests {
use std::ffi::CStr;
use super::*;
#[test]
fn user_agent_starts_with_git() {
let c_str = unsafe { CStr::from_ptr(libgit_user_agent()) };
let agent = c_str
.to_str()
.expect("User agent contains invalid UTF-8 data");
assert!(
agent.starts_with("git/"),
r#"Expected user agent to start with "git/", got: {}"#,
agent
);
}
#[test]
fn sanitized_user_agent_starts_with_git() {
let c_str = unsafe { CStr::from_ptr(libgit_user_agent_sanitized()) };
let agent = c_str
.to_str()
.expect("Sanitized user agent contains invalid UTF-8 data");
assert!(
agent.starts_with("git/"),
r#"Expected user agent to start with "git/", got: {}"#,
agent
);
}
}