From 6e24cc278443a325f4d39b1829fba0a743e6a669 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 12 May 2026 09:32:44 -0700 Subject: [PATCH 1/2] Reapply "Allow creating git config from strings" This reverts commit 58d206c973c57c58822375bb3e48373776d8df5e. Now that the libgit2 version has been updated (#1242) the upstream fix (libgit2/libgit2#7232) is incorporated and this should work again. A follow-up commit will expand the tests, including the case from #1226 that failed after the first attempt. --- libgit2-sys/lib.rs | 13 ++++++++ src/config.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs index 40de36c54e..01f4b0bbfe 100644 --- a/libgit2-sys/lib.rs +++ b/libgit2-sys/lib.rs @@ -137,6 +137,13 @@ pub struct git_config_backend { pub free: Option, } +#[repr(C)] +pub struct git_config_backend_memory_options { + pub version: c_uint, + pub backend_type: *const c_char, + pub origin_path: *const c_char, +} + pub const GIT_CONFIG_BACKEND_VERSION: c_uint = 1; pub enum git_index {} @@ -3467,6 +3474,12 @@ extern "C" { name: *const c_char, ) -> c_int; pub fn git_config_init_backend(backend: *mut git_config_backend, version: c_uint) -> c_int; + pub fn git_config_backend_from_string( + backend: *mut *mut git_config_backend, + cfg: *const c_char, + len: size_t, + opts: *mut git_config_backend_memory_options, + ) -> c_int; pub fn git_config_iterator_free(iter: *mut git_config_iterator); pub fn git_config_iterator_glob_new( out: *mut *mut git_config_iterator, diff --git a/src/config.rs b/src/config.rs index 9ba0a6da64..9f2889100e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -84,6 +84,30 @@ impl Config { } } + /// Create a new config instance based on the provided string contents + pub fn from_str(contents: &str) -> Result { + let config = Self::new()?; + + let mut raw_backend = ptr::null_mut(); + unsafe { + try_call!(raw::git_config_backend_from_string( + &mut raw_backend, + contents.into_c_string()?, + contents.len(), + ptr::null_mut() + )); + try_call!(raw::git_config_add_backend( + config.raw, + raw_backend, + // Match the level used by git_config_open_ondisk() + ConfigLevel::Local, + ptr::null(), + 0 as libc::c_int + )); + } + Ok(config) + } + /// Open the global, XDG and system configuration files /// /// Utility wrapper that finds the global, XDG and system configuration @@ -740,6 +764,56 @@ mod tests { assert_eq!(count(cfg.multivar("foo.bar", None).unwrap()), 0); } + #[test] + fn cfg_from_str() { + // Empty string, no entries + let empty = Config::from_str("").unwrap(); + let mut entries = empty.entries(None).unwrap(); + // Should start with None because it is empty + assert!(entries.next().is_none()); + + let dump = |entries: super::ConfigEntries<'_>| -> Vec { + let mut string_entries = vec![]; + let _ = entries.for_each(|ent| { + string_entries.push(format!( + "{} = {}", + ent.name().unwrap(), + ent.value().unwrap() + )); + }); + string_entries.sort(); + string_entries + }; + + // One entry + let signed = Config::from_str("[commit]\ngpgsign=true\n").unwrap(); + assert_eq!( + dump(signed.entries(Some("commit.gpgsign")).unwrap()), + ["commit.gpgsign = true"] + ); + + // A few entries + let big_config = Config::from_str( + "[core]\n + \tbare=false\n + \tignorecase=true\n + [remote \"origin\"]\n + \turl = https://github.com/rust-lang/rust.git\n + [branch \"main\"]\n + remote = origin", + ) + .unwrap(); + assert_eq!( + dump(big_config.entries(None).unwrap()), + [ + "branch.main.remote = origin", + "core.bare = false", + "core.ignorecase = true", + "remote.origin.url = https://github.com/rust-lang/rust.git", + ] + ); + } + #[test] fn parse() { assert_eq!(Config::parse_bool("").unwrap(), false); From 0db4a8919cd2440a6c052bec3d8c56781f449f57 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 12 May 2026 10:07:04 -0700 Subject: [PATCH 2/2] Expand tests for config objects created from strings Includes the test case from #1226 that previously failed, as well as other cases confirming that strings, booleans, and integers can be read from the config individually. --- src/config.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 9f2889100e..c49d197f8d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -791,6 +791,7 @@ mod tests { dump(signed.entries(Some("commit.gpgsign")).unwrap()), ["commit.gpgsign = true"] ); + assert_eq!(true, signed.get_bool("commit.gpgsign").unwrap()); // A few entries let big_config = Config::from_str( @@ -800,7 +801,10 @@ mod tests { [remote \"origin\"]\n \turl = https://github.com/rust-lang/rust.git\n [branch \"main\"]\n - remote = origin", + remote = origin\n + [foo]\n + \tbar = 123\n + \tbaz = -123\n", ) .unwrap(); assert_eq!( @@ -809,9 +813,23 @@ mod tests { "branch.main.remote = origin", "core.bare = false", "core.ignorecase = true", + "foo.bar = 123", + "foo.baz = -123", "remote.origin.url = https://github.com/rust-lang/rust.git", ] ); + assert_eq!( + "origin", + big_config.get_string("branch.main.remote").unwrap() + ); + assert_eq!(false, big_config.get_bool("core.bare").unwrap()); + assert_eq!(true, big_config.get_bool("core.ignorecase").unwrap()); + assert_eq!( + "https://github.com/rust-lang/rust.git", + big_config.get_string("remote.origin.url").unwrap() + ); + assert_eq!(123, big_config.get_i32("foo.bar").unwrap()); + assert_eq!(-123, big_config.get_i32("foo.baz").unwrap()); } #[test]