Skip to content

Commit fb7e660

Browse files
committed
parse GIT_CONFIG_PARAMETERS, add to config
1 parent 3907137 commit fb7e660

File tree

6 files changed

+121
-41
lines changed

6 files changed

+121
-41
lines changed

src/application.rs

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ where ModuleProvider: module::ModuleProvider + Send + 'static
3434
impl<ModuleProvider> Application<ModuleProvider>
3535
where ModuleProvider: module::ModuleProvider + Send + 'static
3636
{
37-
pub(crate) fn new<EventProvider, Tui>(args: Args, event_provider: EventProvider, tui: Tui) -> Result<Self, Exit>
37+
pub(crate) fn new<EventProvider, Tui>(args: Args, git_config_parameters: Vec<(String, String)>, event_provider: EventProvider, tui: Tui) -> Result<Self, Exit>
3838
where
3939
EventProvider: EventReaderFn,
4040
Tui: crate::display::Tui + Send + 'static,
4141
{
4242
let filepath = Self::filepath_from_args(args)?;
4343
let repository = Self::open_repository()?;
44-
let config_loader = ConfigLoader::from(repository);
44+
let config_loader = ConfigLoader::with_overrides(repository, git_config_parameters);
4545
let config = Self::load_config(&config_loader)
4646
.map_err(|err| Exit::new(ExitStatus::ConfigError, format!("{err:#}").as_str()))?;
4747
let todo_file = Arc::new(Mutex::new(Self::load_todo_file(filepath.as_str(), &config)?));
@@ -221,12 +221,7 @@ mod tests {
221221
module::Modules,
222222
runtime::{Installer, RuntimeError},
223223
test_helpers::{
224-
DefaultTestModule,
225-
TestModuleProvider,
226-
create_config,
227-
create_event_reader,
228-
mocks,
229-
with_git_directory,
224+
DefaultTestModule, TestModuleProvider, create_config, create_event_reader, mocks, with_git_directory,
230225
},
231226
};
232227

@@ -240,8 +235,7 @@ mod tests {
240235
($app:expr) => {
241236
if let Err(e) = $app {
242237
e
243-
}
244-
else {
238+
} else {
245239
panic!("Application is not in an error state");
246240
}
247241
};
@@ -251,8 +245,12 @@ mod tests {
251245
#[serial_test::serial]
252246
fn load_filepath_from_args_failure() {
253247
let event_provider = create_event_reader(|| Ok(None));
254-
let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> =
255-
Application::new(Args::from_os_strings(Vec::new()).unwrap(), event_provider, create_mocked_crossterm());
248+
let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> = Application::new(
249+
Args::from_os_strings(Vec::new()).unwrap(),
250+
Vec::new(),
251+
event_provider,
252+
create_mocked_crossterm(),
253+
);
256254
let exit = application_error!(application);
257255
assert_eq!(exit.get_status(), &ExitStatus::StateError);
258256
assert!(
@@ -266,8 +264,12 @@ mod tests {
266264
fn load_repository_failure() {
267265
with_git_directory("fixtures/not-a-repository", |_| {
268266
let event_provider = create_event_reader(|| Ok(None));
269-
let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> =
270-
Application::new(Args::from_strs(["todofile"]).unwrap(), event_provider, create_mocked_crossterm());
267+
let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> = Application::new(
268+
Args::from_strs(["todofile"]).unwrap(),
269+
Vec::new(),
270+
event_provider,
271+
create_mocked_crossterm(),
272+
);
271273
let exit = application_error!(application);
272274
assert_eq!(exit.get_status(), &ExitStatus::StateError);
273275
assert!(exit.get_message().unwrap().contains("Unable to load Git repository: "));
@@ -278,8 +280,12 @@ mod tests {
278280
fn load_config_failure() {
279281
with_git_directory("fixtures/invalid-config", |_| {
280282
let event_provider = create_event_reader(|| Ok(None));
281-
let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> =
282-
Application::new(Args::from_strs(["rebase-todo"]).unwrap(), event_provider, create_mocked_crossterm());
283+
let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> = Application::new(
284+
Args::from_strs(["rebase-todo"]).unwrap(),
285+
Vec::new(),
286+
event_provider,
287+
create_mocked_crossterm(),
288+
);
283289
let exit = application_error!(application);
284290
assert_eq!(exit.get_status(), &ExitStatus::ConfigError);
285291
});
@@ -319,8 +325,12 @@ mod tests {
319325
fn load_todo_file_load_error() {
320326
with_git_directory("fixtures/simple", |_| {
321327
let event_provider = create_event_reader(|| Ok(None));
322-
let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> =
323-
Application::new(Args::from_strs(["does-not-exist"]).unwrap(), event_provider, create_mocked_crossterm());
328+
let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> = Application::new(
329+
Args::from_strs(["does-not-exist"]).unwrap(),
330+
Vec::new(),
331+
event_provider,
332+
create_mocked_crossterm(),
333+
);
324334
let exit = application_error!(application);
325335
assert_eq!(exit.get_status(), &ExitStatus::FileReadError);
326336
});
@@ -333,6 +343,7 @@ mod tests {
333343
let event_provider = create_event_reader(|| Ok(None));
334344
let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> = Application::new(
335345
Args::from_strs([rebase_todo]).unwrap(),
346+
Vec::new(),
336347
event_provider,
337348
create_mocked_crossterm(),
338349
);
@@ -348,6 +359,7 @@ mod tests {
348359
let event_provider = create_event_reader(|| Ok(None));
349360
let application: Result<Application<TestModuleProvider<DefaultTestModule>>, Exit> = Application::new(
350361
Args::from_strs([rebase_todo]).unwrap(),
362+
Vec::new(),
351363
event_provider,
352364
create_mocked_crossterm(),
353365
);
@@ -381,6 +393,7 @@ mod tests {
381393
let event_provider = create_event_reader(|| Ok(Some(Event::Key(KeyEvent::from(KeyCode::Char('W'))))));
382394
let mut application: Application<Modules> = Application::new(
383395
Args::from_strs([rebase_todo]).unwrap(),
396+
Vec::new(),
384397
event_provider,
385398
create_mocked_crossterm(),
386399
)
@@ -407,6 +420,7 @@ mod tests {
407420
let event_provider = create_event_reader(|| Ok(Some(Event::Key(KeyEvent::from(KeyCode::Char('W'))))));
408421
let mut application: Application<Modules> = Application::new(
409422
Args::from_strs([rebase_todo]).unwrap(),
423+
Vec::new(),
410424
event_provider,
411425
create_mocked_crossterm(),
412426
)
@@ -432,6 +446,7 @@ mod tests {
432446
});
433447
let mut application: Application<Modules> = Application::new(
434448
Args::from_strs([rebase_todo]).unwrap(),
449+
Vec::new(),
435450
event_provider,
436451
create_mocked_crossterm(),
437452
)
@@ -448,6 +463,7 @@ mod tests {
448463
let event_provider = create_event_reader(|| Ok(Some(Event::Key(KeyEvent::from(KeyCode::Char('W'))))));
449464
let mut application: Application<Modules> = Application::new(
450465
Args::from_strs([rebase_todo]).unwrap(),
466+
Vec::new(),
451467
event_provider,
452468
create_mocked_crossterm(),
453469
)

src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ mod tests {
113113
#[test]
114114
fn try_from_config_loader() {
115115
with_temp_bare_repository(|repository| {
116-
let loader = ConfigLoader::from(repository);
116+
let loader = ConfigLoader::new(repository);
117117
let config = assert_ok!(loader.load_config());
118118
assert_ok!(Config::try_from(&config));
119119
});

src/config/config_loader.rs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,44 @@ use crate::git::{Config, GitError};
66

77
pub(crate) struct ConfigLoader {
88
repository: Repository,
9+
overrides: Vec<(String, String)>,
910
}
1011

1112
impl ConfigLoader {
12-
/// Load the git configuration for the repository.
13+
#[cfg(test)]
14+
pub(crate) fn new(repository: Repository) -> Self {
15+
let overrides = Vec::new();
16+
Self { repository, overrides }
17+
}
18+
19+
pub(crate) fn with_overrides(repository: Repository, overrides: Vec<(String, String)>) -> Self {
20+
Self { repository, overrides }
21+
}
22+
23+
/// Load the git configuration for the repository,
24+
/// with any overrides taking priority over the values defined in the repositroy
1325
///
1426
/// # Errors
1527
/// Will result in an error if the configuration is invalid.
1628
pub(crate) fn load_config(&self) -> Result<Config, GitError> {
17-
self.repository.config().map_err(|e| GitError::ConfigLoad { cause: e })
29+
let into_git_error = |cause| GitError::ConfigLoad { cause };
30+
31+
let mut config = self.repository.config().map_err(into_git_error)?;
32+
for (name, value) in &self.overrides {
33+
if value.is_empty() {
34+
config.set_bool(name, true).map_err(into_git_error)?;
35+
} else {
36+
config.set_str(name, value).map_err(into_git_error)?;
37+
}
38+
}
39+
Ok(config)
1840
}
1941

2042
pub(crate) fn eject_repository(self) -> Repository {
2143
self.repository
2244
}
2345
}
2446

25-
impl From<Repository> for ConfigLoader {
26-
fn from(repository: Repository) -> Self {
27-
Self { repository }
28-
}
29-
}
30-
3147
impl Debug for ConfigLoader {
3248
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
3349
f.debug_struct("ConfigLoader")
@@ -49,7 +65,7 @@ mod unix_tests {
4965
#[test]
5066
fn load_config() {
5167
with_temp_bare_repository(|repository| {
52-
let config = ConfigLoader::from(repository);
68+
let config = ConfigLoader::new(repository);
5369
assert_ok!(config.load_config());
5470
});
5571
}
@@ -58,7 +74,7 @@ mod unix_tests {
5874
fn fmt() {
5975
with_temp_repository(|repository| {
6076
let path = repository.path().canonicalize().unwrap();
61-
let loader = ConfigLoader::from(repository);
77+
let loader = ConfigLoader::new(repository);
6278
let formatted = format!("{loader:?}");
6379
assert_eq!(
6480
formatted,

src/editor.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use crate::{
1111
};
1212

1313
#[cfg(not(tarpaulin_include))]
14-
pub(crate) fn run(args: Args) -> Exit {
15-
let mut application: Application<Modules> = match Application::new(args, read_event, CrossTerm::new()) {
14+
pub(crate) fn run(args: Args, git_config_parameters: Vec<(String, String)>) -> Exit {
15+
let mut application: Application<Modules> = match Application::new(args, git_config_parameters, read_event, CrossTerm::new()) {
1616
Ok(app) => app,
1717
Err(exit) => return exit,
1818
};
@@ -35,8 +35,9 @@ mod tests {
3535
with_git_directory("fixtures/simple", |path| {
3636
let todo_file = Path::new(path).join("rebase-todo-empty").into_os_string();
3737
let args = Args::from_os_strings(vec![todo_file]).unwrap();
38+
let git_config_parameters = Vec::new();
3839
assert_eq!(
39-
run(args).get_status(),
40+
run(args, git_config_parameters).get_status(),
4041
&ExitStatus::Good
4142
);
4243
});
@@ -47,8 +48,9 @@ mod tests {
4748
with_git_directory("fixtures/simple", |path| {
4849
let todo_file = Path::new(path).join("does-not-exist").into_os_string();
4950
let args = Args::from_os_strings(vec![todo_file]).unwrap();
51+
let git_config_parameters = Vec::new();
5052
assert_eq!(
51-
run(args).get_status(),
53+
run(args, git_config_parameters).get_status(),
5254
&ExitStatus::FileReadError
5355
);
5456
});

src/main.rs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,24 +72,65 @@ use crate::{
7272
};
7373

7474
#[must_use]
75-
fn run(os_args: Vec<OsString>) -> Exit {
75+
fn run(os_args: Vec<OsString>, git_config_parameters: Vec<(String, String)>) -> Exit {
7676
match Args::from_os_strings(os_args) {
7777
Err(err) => err,
7878
Ok(args) => {
7979
match *args.mode() {
8080
Mode::Help => help::run(),
8181
Mode::Version => version::run(),
8282
Mode::License => license::run(),
83-
Mode::Editor => editor::run(args),
83+
Mode::Editor => editor::run(args, git_config_parameters),
8484
}
8585
},
8686
}
8787
}
88+
/// returns a pair of name/value pairs passed as overrides to `git`.
89+
///
90+
/// input here is expected to come from `git -c`, and is in the form
91+
/// of `-c <name>=<value>` pairs, for example `-c interactive-rebase-tool.diffTabWidth=4`.
92+
/// separated by a single whitespace
93+
///
94+
/// notes:
95+
/// 1. the key/value have both gone through git's shell quoting.
96+
/// from `gix-quote`: "every single-quote `'` is escaped as `'\''`,
97+
/// every exclamation mark `!` is escaped as `'\!'`, and the entire string
98+
/// is enclosed in single quotes."
99+
/// 2. if input doesn't contain a '=', a `=` is appended between
100+
/// `key` and `value`. `value`, will be an empty string.
101+
/// 3. if input does contain a '=' but `value` is empty, `value`
102+
/// will also be an empty string.
103+
/// 4. an empty string will later be interpreted as `true`,
104+
/// but we don't do this here.
105+
pub(crate) fn parse_git_config_parameters(env_var: OsString) -> Vec<(String, String)> {
106+
// we expect valid UTF-8 from the shell/git, so we don't handle errors here.
107+
let Some(env_var) = env_var.to_str() else {
108+
return Vec::new();
109+
};
110+
111+
// naive implementation: assumes correctly-escaped strings, efficiency isn't a priority
112+
fn unescape(s: &str) -> String {
113+
let s = s.trim_matches('\'');
114+
let mut s = s.replace("\\'", "\'");
115+
s = s.replace("\\!", "!");
116+
s
117+
}
118+
119+
env_var
120+
.split_ascii_whitespace()
121+
.filter_map(|pair| pair.split_once('='))
122+
.map(|(name, value)| (unescape(name), unescape(value)))
123+
.collect()
124+
}
88125

89126
#[expect(clippy::print_stderr, reason = "Required to print error message.")]
90127
#[cfg(not(tarpaulin_include))]
91128
fn main() -> impl Termination {
92-
let exit = run(env::args_os().skip(1).collect());
129+
let args = env::args_os().skip(1).collect();
130+
let git_config_parameters = env::var_os("GIT_CONFIG_PARAMETERS")
131+
.map(parse_git_config_parameters)
132+
.unwrap_or_default();
133+
let exit = run(args, git_config_parameters);
93134
if let Some(message) = exit.get_message() {
94135
eprintln!("{message}");
95136
}

src/tests.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ use crate::{module::ExitStatus, test_helpers::with_git_directory};
77
#[serial_test::serial]
88
fn successful_run_help() {
99
let args = ["--help"].into_iter().map(OsString::from).collect();
10-
let exit = run(args);
10+
let git_config_parameters = Vec::new();
11+
let exit = run(args, git_config_parameters);
1112
assert!(exit.get_message().unwrap().contains("USAGE:"));
1213
assert_eq!(exit.get_status(), &ExitStatus::Good);
1314
}
@@ -16,7 +17,8 @@ fn successful_run_help() {
1617
#[serial_test::serial]
1718
fn successful_run_version() {
1819
let args = ["--version"].into_iter().map(OsString::from).collect();
19-
let exit = run(args);
20+
let git_config_parameters = Vec::new();
21+
let exit = run(args, git_config_parameters);
2022
assert!(exit.get_message().unwrap().starts_with("interactive-rebase-tool"));
2123
assert_eq!(exit.get_status(), &ExitStatus::Good);
2224
}
@@ -25,7 +27,8 @@ fn successful_run_version() {
2527
#[serial_test::serial]
2628
fn successful_run_license() {
2729
let args = ["--license"].into_iter().map(OsString::from).collect();
28-
let exit = run(args);
30+
let git_config_parameters = Vec::new();
31+
let exit = run(args, git_config_parameters);
2932
assert!(
3033
exit.get_message()
3134
.unwrap()
@@ -39,8 +42,9 @@ fn successful_run_editor() {
3942
with_git_directory("fixtures/simple", |path| {
4043
let todo_file = Path::new(path).join("rebase-todo-empty").into_os_string();
4144
let args = vec![todo_file];
45+
let git_config_parameters = Vec::new();
4246
assert_eq!(
43-
run(args).get_status(),
47+
run(args, git_config_parameters).get_status(),
4448
&ExitStatus::Good
4549
);
4650
});
@@ -52,5 +56,6 @@ fn successful_run_editor() {
5256
#[expect(unsafe_code)]
5357
fn error() {
5458
let args = unsafe { vec![OsString::from(String::from_utf8_unchecked(vec![0xC3, 0x28]))] };
55-
assert_eq!(run(args).get_status(), &ExitStatus::StateError);
59+
let git_config_parameters = Vec::new();
60+
assert_eq!(run(args, git_config_parameters).get_status(), &ExitStatus::StateError);
5661
}

0 commit comments

Comments
 (0)