Skip to content

Commit 067c334

Browse files
committed
feat: open::Options::lenient_config(…) to default otherwise invalid configuration values where possible
Originally required by starship/starship#4266 .
1 parent 232784a commit 067c334

File tree

4 files changed

+127
-61
lines changed

4 files changed

+127
-61
lines changed

Diff for: git-repository/src/config/cache.rs

+66-39
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ pub(crate) struct StageOne {
2020

2121
/// Initialization
2222
impl StageOne {
23-
pub fn new(git_dir: &std::path::Path, git_dir_trust: git_sec::Trust, lossy: Option<bool>) -> Result<Self, Error> {
23+
pub fn new(
24+
git_dir: &std::path::Path,
25+
git_dir_trust: git_sec::Trust,
26+
lossy: Option<bool>,
27+
lenient: bool,
28+
) -> Result<Self, Error> {
2429
let mut buf = Vec::with_capacity(512);
2530
let config = {
2631
let config_path = git_dir.join("config");
@@ -38,7 +43,7 @@ impl StageOne {
3843
)?
3944
};
4045

41-
let is_bare = config_bool(&config, "core.bare", false)?;
46+
let is_bare = config_bool(&config, "core.bare", false, lenient)?;
4247
let repo_format_version = config
4348
.value::<Integer>("core", None, "repositoryFormatVersion")
4449
.map_or(0, |v| v.to_decimal().unwrap_or_default());
@@ -99,6 +104,7 @@ impl Cache {
99104
env: use_env,
100105
includes: use_includes,
101106
}: repository::permissions::Config,
107+
lenient_config: bool,
102108
) -> Result<Self, Error> {
103109
let options = git_config::file::init::Options {
104110
includes: if use_includes {
@@ -174,45 +180,25 @@ impl Cache {
174180
globals
175181
};
176182

177-
let excludes_file = config
183+
let excludes_file = match config
178184
.path_filter("core", None, "excludesFile", &mut filter_config_section)
179185
.map(|p| p.interpolate(options.includes.interpolate).map(|p| p.into_owned()))
180-
.transpose()?;
186+
.transpose()
187+
{
188+
Ok(f) => f,
189+
Err(_err) if lenient_config => None,
190+
Err(err) => return Err(err.into()),
191+
};
181192

182-
let mut hex_len = None;
183-
if let Some(hex_len_str) = config.string("core", None, "abbrev") {
184-
if hex_len_str.trim().is_empty() {
185-
return Err(Error::EmptyValue { key: "core.abbrev" });
186-
}
187-
if !hex_len_str.eq_ignore_ascii_case(b"auto") {
188-
let value_bytes = hex_len_str.as_ref();
189-
if let Ok(false) = Boolean::try_from(value_bytes).map(Into::into) {
190-
hex_len = object_hash.len_in_hex().into();
191-
} else {
192-
let value = Integer::try_from(value_bytes)
193-
.map_err(|_| Error::CoreAbbrev {
194-
value: hex_len_str.clone().into_owned(),
195-
max: object_hash.len_in_hex() as u8,
196-
})?
197-
.to_decimal()
198-
.ok_or_else(|| Error::CoreAbbrev {
199-
value: hex_len_str.clone().into_owned(),
200-
max: object_hash.len_in_hex() as u8,
201-
})?;
202-
if value < 4 || value as usize > object_hash.len_in_hex() {
203-
return Err(Error::CoreAbbrev {
204-
value: hex_len_str.clone().into_owned(),
205-
max: object_hash.len_in_hex() as u8,
206-
});
207-
}
208-
hex_len = Some(value as usize);
209-
}
210-
}
211-
}
193+
let hex_len = match parse_core_abbrev(&config, object_hash) {
194+
Ok(v) => v,
195+
Err(_err) if lenient_config => None,
196+
Err(err) => return Err(err),
197+
};
212198

213199
let reflog = query_refupdates(&config);
214-
let ignore_case = config_bool(&config, "core.ignoreCase", false)?;
215-
let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true)?;
200+
let ignore_case = config_bool(&config, "core.ignoreCase", false, lenient_config)?;
201+
let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true, lenient_config)?;
216202
let object_kind_hint = config.string("core", None, "disambiguate").and_then(|value| {
217203
Some(match value.as_ref().as_ref() {
218204
b"commit" => ObjectKindHint::Commit,
@@ -292,15 +278,19 @@ fn base_options(lossy: Option<bool>) -> git_config::file::init::Options<'static>
292278
}
293279
}
294280

295-
fn config_bool(config: &git_config::File<'_>, key: &str, default: bool) -> Result<bool, Error> {
281+
fn config_bool(config: &git_config::File<'_>, key: &str, default: bool, lenient: bool) -> Result<bool, Error> {
296282
let (section, key) = key.split_once('.').expect("valid section.key format");
297-
config
283+
match config
298284
.boolean(section, None, key)
299285
.unwrap_or(Ok(default))
300286
.map_err(|err| Error::DecodeBoolean {
301287
value: err.input,
302288
key: key.into(),
303-
})
289+
}) {
290+
Ok(v) => Ok(v),
291+
Err(_err) if lenient => Ok(default),
292+
Err(err) => Err(err),
293+
}
304294
}
305295

306296
fn query_refupdates(config: &git_config::File<'static>) -> Option<git_ref::store::WriteReflog> {
@@ -315,3 +305,40 @@ fn query_refupdates(config: &git_config::File<'static>) -> Option<git_ref::store
315305
.unwrap_or(git_ref::store::WriteReflog::Disable)
316306
})
317307
}
308+
309+
fn parse_core_abbrev(config: &git_config::File<'static>, object_hash: git_hash::Kind) -> Result<Option<usize>, Error> {
310+
match config.string("core", None, "abbrev") {
311+
Some(hex_len_str) => {
312+
if hex_len_str.trim().is_empty() {
313+
return Err(Error::EmptyValue { key: "core.abbrev" });
314+
}
315+
if hex_len_str.trim().eq_ignore_ascii_case(b"auto") {
316+
Ok(None)
317+
} else {
318+
let value_bytes = hex_len_str.as_ref();
319+
if let Ok(false) = Boolean::try_from(value_bytes).map(Into::into) {
320+
Ok(object_hash.len_in_hex().into())
321+
} else {
322+
let value = Integer::try_from(value_bytes)
323+
.map_err(|_| Error::CoreAbbrev {
324+
value: hex_len_str.clone().into_owned(),
325+
max: object_hash.len_in_hex() as u8,
326+
})?
327+
.to_decimal()
328+
.ok_or_else(|| Error::CoreAbbrev {
329+
value: hex_len_str.clone().into_owned(),
330+
max: object_hash.len_in_hex() as u8,
331+
})?;
332+
if value < 4 || value as usize > object_hash.len_in_hex() {
333+
return Err(Error::CoreAbbrev {
334+
value: hex_len_str.clone().into_owned(),
335+
max: object_hash.len_in_hex() as u8,
336+
});
337+
}
338+
Ok(Some(value as usize))
339+
}
340+
}
341+
}
342+
None => Ok(None),
343+
}
344+
}

Diff for: git-repository/src/open.rs

+21-21
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::path::PathBuf;
22

33
use git_features::threading::OwnShared;
44

5-
use crate::{config, config::cache::interpolate_context, permission, permissions, Permissions, ThreadSafeRepository};
5+
use crate::{config, config::cache::interpolate_context, permission, Permissions, ThreadSafeRepository};
66

77
/// A way to configure the usage of replacement objects, see `git replace`.
88
#[derive(Debug, Clone)]
@@ -68,6 +68,7 @@ pub struct Options {
6868
pub(crate) git_dir_trust: Option<git_sec::Trust>,
6969
pub(crate) filter_config_section: Option<fn(&git_config::file::Metadata) -> bool>,
7070
pub(crate) lossy_config: Option<bool>,
71+
pub(crate) lenient_config: bool,
7172
pub(crate) bail_if_untrusted: bool,
7273
}
7374

@@ -103,23 +104,7 @@ impl Options {
103104
/// Options configured to prevent accessing anything else than the repository configuration file, prohibiting
104105
/// accessing the environment or spreading beyond the git repository location.
105106
pub fn isolated() -> Self {
106-
Options::default().permissions(Permissions {
107-
config: permissions::Config {
108-
system: false,
109-
git: false,
110-
user: false,
111-
env: false,
112-
includes: false,
113-
},
114-
env: {
115-
let deny = permission::env_var::Resource::resource(git_sec::Permission::Deny);
116-
permissions::Environment {
117-
xdg_config_home: deny.clone(),
118-
home: deny.clone(),
119-
git_prefix: deny,
120-
}
121-
},
122-
})
107+
Options::default().permissions(Permissions::isolated())
123108
}
124109
}
125110

@@ -190,12 +175,23 @@ impl Options {
190175
/// By default, in release mode configuration will be read without retaining non-essential information like
191176
/// comments or whitespace to optimize lookup performance.
192177
///
193-
/// Some application might want to toggle this to false in they want to display or edit configuration losslessly.
178+
/// Some application might want to toggle this to false in they want to display or edit configuration losslessly
179+
/// with all whitespace and comments included.
194180
pub fn lossy_config(mut self, toggle: bool) -> Self {
195181
self.lossy_config = toggle.into();
196182
self
197183
}
198184

185+
/// If set, default is false, invalid configuration values will be defaulted to acceptable values where when possible,
186+
/// instead of yielding an error during startup.
187+
///
188+
/// This is recommended for all applications that prefer usability over correctness. `git` itslef by default is not lenient
189+
/// towards malconfigured repositories.
190+
pub fn lenient_config(mut self, toggle: bool) -> Self {
191+
self.lenient_config = toggle;
192+
self
193+
}
194+
199195
/// Open a repository at `path` with the options set so far.
200196
pub fn open(self, path: impl Into<PathBuf>) -> Result<ThreadSafeRepository, Error> {
201197
ThreadSafeRepository::open_opts(path, self)
@@ -213,6 +209,7 @@ impl git_sec::trust::DefaultForLevel for Options {
213209
filter_config_section: Some(config::section::is_trusted),
214210
lossy_config: None,
215211
bail_if_untrusted: false,
212+
lenient_config: false,
216213
},
217214
git_sec::Trust::Reduced => Options {
218215
object_store_slots: git_odb::store::init::Slots::Given(32), // limit resource usage
@@ -221,6 +218,7 @@ impl git_sec::trust::DefaultForLevel for Options {
221218
git_dir_trust: git_sec::Trust::Reduced.into(),
222219
filter_config_section: Some(config::section::is_trusted),
223220
bail_if_untrusted: false,
221+
lenient_config: false,
224222
lossy_config: None,
225223
},
226224
}
@@ -231,7 +229,7 @@ impl git_sec::trust::DefaultForLevel for Options {
231229
#[derive(Debug, thiserror::Error)]
232230
#[allow(missing_docs)]
233231
pub enum Error {
234-
#[error(transparent)]
232+
#[error("Failed to load the git configuration")]
235233
Config(#[from] config::Error),
236234
#[error(transparent)]
237235
NotARepository(#[from] git_discover::is_git::Error),
@@ -314,6 +312,7 @@ impl ThreadSafeRepository {
314312
filter_config_section,
315313
ref replacement_objects,
316314
lossy_config,
315+
lenient_config,
317316
bail_if_untrusted,
318317
permissions: Permissions { ref env, config },
319318
} = options;
@@ -328,7 +327,7 @@ impl ThreadSafeRepository {
328327
.map(|cd| git_dir.join(cd));
329328
let common_dir_ref = common_dir.as_deref().unwrap_or(&git_dir);
330329

331-
let repo_config = config::cache::StageOne::new(common_dir_ref, git_dir_trust, lossy_config)?;
330+
let repo_config = config::cache::StageOne::new(common_dir_ref, git_dir_trust, lossy_config, lenient_config)?;
332331
let mut refs = {
333332
let reflog = repo_config.reflog.unwrap_or(git_ref::store::WriteReflog::Disable);
334333
let object_hash = repo_config.object_hash;
@@ -351,6 +350,7 @@ impl ThreadSafeRepository {
351350
home.as_deref(),
352351
env.clone(),
353352
config,
353+
lenient_config,
354354
)?;
355355

356356
if bail_if_untrusted && git_dir_trust != git_sec::Trust::Full {

Diff for: git-repository/src/repository/permissions.rs

+22-1
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ pub struct Config {
2626
/// Whether to use the user configuration.
2727
/// This is usually `~/.gitconfig` on unix.
2828
pub user: bool,
29-
/// Whether to use worktree configuration from `config.worktree`.
3029
// TODO: figure out how this really applies and provide more information here.
30+
// Whether to use worktree configuration from `config.worktree`.
3131
// pub worktree: bool,
3232
/// Whether to use the configuration from environment variables.
3333
pub env: bool,
@@ -100,6 +100,27 @@ impl Permissions {
100100
config: Config::all(),
101101
}
102102
}
103+
104+
/// Don't read any but the local git configuration and deny reading any environment variables.
105+
pub fn isolated() -> Self {
106+
Permissions {
107+
config: Config {
108+
system: false,
109+
git: false,
110+
user: false,
111+
env: false,
112+
includes: false,
113+
},
114+
env: {
115+
let deny = permission::env_var::Resource::resource(git_sec::Permission::Deny);
116+
Environment {
117+
xdg_config_home: deny.clone(),
118+
home: deny.clone(),
119+
git_prefix: deny,
120+
}
121+
},
122+
}
123+
}
103124
}
104125

105126
impl git_sec::trust::DefaultForLevel for Permissions {

Diff for: git-repository/tests/id/mod.rs

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::cmp::Ordering;
22

3+
use git_repository as git;
34
use git_repository::prelude::ObjectIdExt;
45
use git_testtools::hex_to_id;
56

@@ -22,6 +23,23 @@ fn prefix() -> crate::Result {
2223
let prefix = id.shorten()?;
2324
assert_eq!(prefix.cmp_oid(&id), Ordering::Equal);
2425
assert_eq!(prefix.hex_len(), 5, "preconfigured via core.abbrev in the repo file");
26+
27+
assert!(
28+
git_testtools::run_git(worktree_dir.path(), &["config", "core.abbrev", ""])?.success(),
29+
"set core abbrev value to empty successfully"
30+
);
31+
32+
assert!(
33+
matches!(
34+
git_repository::open(worktree_dir.path()).unwrap_err(),
35+
git::open::Error::Config(git::config::Error::EmptyValue { .. })
36+
),
37+
"an empty core.abbrev fails the open operation in strict config mode, emulating git behaviour"
38+
);
39+
assert!(
40+
git_repository::open_opts(worktree_dir.path(), git::open::Options::isolated().lenient_config(true)).is_ok(),
41+
"but it can be made to work when we are lenient (good for APIs)"
42+
);
2543
Ok(())
2644
}
2745

0 commit comments

Comments
 (0)