config/
lib.rs

1// LINT-REPLACE-START
2// This section is autogenerated, do not modify directly
3// nightly sometimes removes/renames lints
4#![cfg_attr(allow_unknown_lints, allow(unknown_lints))]
5#![cfg_attr(allow_unknown_lints, allow(renamed_and_removed_lints))]
6// enable all rustc's built-in lints
7#![deny(
8	future_incompatible,
9	nonstandard_style,
10	rust_2018_compatibility,
11	rust_2018_idioms,
12	rust_2021_compatibility,
13	unused,
14	warnings
15)]
16// rustc's additional allowed by default lints
17#![deny(
18	absolute_paths_not_starting_with_crate,
19	deprecated_in_future,
20	elided_lifetimes_in_paths,
21	explicit_outlives_requirements,
22	ffi_unwind_calls,
23	keyword_idents,
24	let_underscore_drop,
25	macro_use_extern_crate,
26	meta_variable_misuse,
27	missing_abi,
28	missing_copy_implementations,
29	missing_debug_implementations,
30	missing_docs,
31	non_ascii_idents,
32	noop_method_call,
33	pointer_structural_match,
34	rust_2021_incompatible_closure_captures,
35	rust_2021_incompatible_or_patterns,
36	rust_2021_prefixes_incompatible_syntax,
37	rust_2021_prelude_collisions,
38	single_use_lifetimes,
39	trivial_casts,
40	trivial_numeric_casts,
41	unreachable_pub,
42	unsafe_code,
43	unsafe_op_in_unsafe_fn,
44	unused_crate_dependencies,
45	unused_extern_crates,
46	unused_import_braces,
47	unused_lifetimes,
48	unused_macro_rules,
49	unused_qualifications,
50	unused_results,
51	unused_tuple_struct_fields,
52	variant_size_differences
53)]
54// enable all of Clippy's lints
55#![deny(clippy::all, clippy::cargo, clippy::pedantic, clippy::restriction)]
56#![cfg_attr(include_nightly_lints, deny(clippy::nursery))]
57#![allow(
58	clippy::arithmetic_side_effects,
59	clippy::arithmetic_side_effects,
60	clippy::blanket_clippy_restriction_lints,
61	clippy::bool_to_int_with_if,
62	clippy::default_numeric_fallback,
63	clippy::else_if_without_else,
64	clippy::expect_used,
65	clippy::float_arithmetic,
66	clippy::implicit_return,
67	clippy::indexing_slicing,
68	clippy::map_err_ignore,
69	clippy::missing_docs_in_private_items,
70	clippy::missing_trait_methods,
71	clippy::mod_module_files,
72	clippy::module_name_repetitions,
73	clippy::new_without_default,
74	clippy::non_ascii_literal,
75	clippy::option_if_let_else,
76	clippy::pub_use,
77	clippy::question_mark_used,
78	clippy::redundant_pub_crate,
79	clippy::ref_patterns,
80	clippy::std_instead_of_alloc,
81	clippy::std_instead_of_core,
82	clippy::tabs_in_doc_comments,
83	clippy::tests_outside_test_module,
84	clippy::too_many_lines,
85	clippy::unwrap_used
86)]
87#![deny(
88	rustdoc::bare_urls,
89	rustdoc::broken_intra_doc_links,
90	rustdoc::invalid_codeblock_attributes,
91	rustdoc::invalid_html_tags,
92	rustdoc::missing_crate_level_docs,
93	rustdoc::private_doc_tests,
94	rustdoc::private_intra_doc_links
95)]
96// allow some things in tests
97#![cfg_attr(
98	test,
99	allow(
100		let_underscore_drop,
101		clippy::cognitive_complexity,
102		clippy::let_underscore_must_use,
103		clippy::let_underscore_untyped,
104		clippy::needless_pass_by_value,
105		clippy::panic,
106		clippy::shadow_reuse,
107		clippy::shadow_unrelated,
108		clippy::undocumented_unsafe_blocks,
109		clippy::unimplemented,
110		clippy::unreachable
111	)
112)]
113// allowable upcoming nightly lints
114#![cfg_attr(
115	include_nightly_lints,
116	allow(
117		clippy::arc_with_non_send_sync,
118		clippy::min_ident_chars,
119		clippy::needless_raw_strings,
120		clippy::pub_with_shorthand,
121		clippy::redundant_closure_call,
122		clippy::single_call_fn
123	)
124)]
125// LINT-REPLACE-END
126
127//! Git Interactive Rebase Tool - Configuration Module
128//!
129//! # Description
130//! This module is used to handle the loading of configuration from the Git config system.
131//!
132//! ```
133//! use config::Config;
134//! use git::Repository;
135//! let config = Config::try_from(&Repository::open_from_env().unwrap());
136//! ```
137//!
138//! ## Test Utilities
139//! To facilitate testing the usages of this crate, a set of testing utilities are provided. Since
140//! these utilities are not tested, and often are optimized for developer experience than
141//! performance should only be used in test code.
142mod color;
143mod diff_ignore_whitespace_setting;
144mod diff_show_whitespace_setting;
145pub mod errors;
146mod git_config;
147mod key_bindings;
148mod theme;
149mod utils;
150
151#[cfg(test)]
152mod testutils;
153
154use git::Repository;
155// TODO: remove override of indirect dependency
156use proc_macro2 as _;
157
158use self::utils::{get_bool, get_diff_ignore_whitespace, get_diff_show_whitespace, get_string, get_unsigned_integer};
159pub use self::{
160	color::Color,
161	diff_ignore_whitespace_setting::DiffIgnoreWhitespaceSetting,
162	diff_show_whitespace_setting::DiffShowWhitespaceSetting,
163	git_config::GitConfig,
164	key_bindings::KeyBindings,
165	theme::Theme,
166};
167use crate::errors::{ConfigError, ConfigErrorCause};
168
169const DEFAULT_SPACE_SYMBOL: &str = "\u{b7}"; // ·
170const DEFAULT_TAB_SYMBOL: &str = "\u{2192}"; // →
171
172/// Represents the configuration options.
173#[derive(Clone, Debug)]
174#[non_exhaustive]
175pub struct Config {
176	/// If to select the next line in the list after performing an action.
177	pub auto_select_next: bool,
178	/// How to handle whitespace when calculating diffs.
179	pub diff_ignore_whitespace: DiffIgnoreWhitespaceSetting,
180	/// If to ignore blank lines when calculating diffs.
181	pub diff_ignore_blank_lines: bool,
182	/// How to show whitespace in diffs.
183	pub diff_show_whitespace: DiffShowWhitespaceSetting,
184	/// The symbol used to replace space characters.
185	pub diff_space_symbol: String,
186	/// The symbol used to replace tab characters.
187	pub diff_tab_symbol: String,
188	/// The display width of the tab character.
189	pub diff_tab_width: u32,
190	/// The maximum number of undo steps.
191	pub undo_limit: u32,
192	/// Configuration options loaded directly from Git.
193	pub git: GitConfig,
194	/// Key binding configuration.
195	pub key_bindings: KeyBindings,
196	/// Theme configuration.
197	pub theme: Theme,
198}
199
200impl Config {
201	/// Create a new configuration with default values.
202	#[inline]
203	#[must_use]
204	#[allow(clippy::missing_panics_doc)]
205	pub fn new() -> Self {
206		Self::new_with_config(None).unwrap() // should never error with None config
207	}
208
209	fn new_with_config(git_config: Option<&git::Config>) -> Result<Self, ConfigError> {
210		Ok(Self {
211			auto_select_next: get_bool(git_config, "interactive-rebase-tool.autoSelectNext", false)?,
212			diff_ignore_whitespace: get_diff_ignore_whitespace(
213				git_config,
214				"interactive-rebase-tool.diffIgnoreWhitespace",
215			)?,
216			diff_ignore_blank_lines: get_bool(git_config, "interactive-rebase-tool.diffIgnoreBlankLines", false)?,
217			diff_show_whitespace: get_diff_show_whitespace(git_config, "interactive-rebase-tool.diffShowWhitespace")?,
218			diff_space_symbol: get_string(
219				git_config,
220				"interactive-rebase-tool.diffSpaceSymbol",
221				DEFAULT_SPACE_SYMBOL,
222			)?,
223			diff_tab_symbol: get_string(git_config, "interactive-rebase-tool.diffTabSymbol", DEFAULT_TAB_SYMBOL)?,
224			diff_tab_width: get_unsigned_integer(git_config, "interactive-rebase-tool.diffTabWidth", 4)?,
225			undo_limit: get_unsigned_integer(git_config, "interactive-rebase-tool.undoLimit", 5000)?,
226			git: GitConfig::new_with_config(git_config)?,
227			key_bindings: KeyBindings::new_with_config(git_config)?,
228			theme: Theme::new_with_config(git_config)?,
229		})
230	}
231}
232
233impl TryFrom<&Repository> for Config {
234	type Error = ConfigError;
235
236	/// Creates a new Config instance loading the Git Config using [`git::Repository`].
237	///
238	/// # Errors
239	///
240	/// Will return an `Err` if there is a problem loading the configuration.
241	#[inline]
242	fn try_from(repo: &Repository) -> Result<Self, Self::Error> {
243		let config = repo
244			.load_config()
245			.map_err(|e| ConfigError::new_read_error("", ConfigErrorCause::GitError(e)))?;
246		Self::new_with_config(Some(&config))
247	}
248}
249
250impl TryFrom<&git::Config> for Config {
251	type Error = ConfigError;
252
253	#[inline]
254	fn try_from(config: &git::Config) -> Result<Self, Self::Error> {
255		Self::new_with_config(Some(config))
256	}
257}
258
259#[cfg(test)]
260mod tests {
261	use std::fmt::Debug;
262
263	use ::testutils::assert_err_eq;
264	use claim::assert_ok;
265	use git::testutil::with_temp_bare_repository;
266	use rstest::rstest;
267
268	use super::*;
269	use crate::testutils::{invalid_utf, with_git_config};
270
271	#[test]
272	fn new() {
273		let _config = Config::new();
274	}
275
276	#[test]
277	fn try_from_repository() {
278		with_temp_bare_repository(|repository| {
279			assert_ok!(Config::try_from(&repository));
280		});
281	}
282
283	#[test]
284	fn try_from_git_config() {
285		with_git_config(&[], |git_config| {
286			assert_ok!(Config::try_from(&git_config));
287		});
288	}
289
290	#[test]
291	fn try_from_git_config_error() {
292		with_git_config(
293			&["[interactive-rebase-tool]", "autoSelectNext = invalid"],
294			|git_config| {
295				_ = Config::try_from(&git_config).unwrap_err();
296			},
297		);
298	}
299
300	#[rstest]
301	#[case::auto_select_next_default("autoSelectNext", "", false, |config: Config| config.auto_select_next)]
302	#[case::auto_select_next_false("autoSelectNext", "false", false, |config: Config| config.auto_select_next)]
303	#[case::auto_select_next_true("autoSelectNext", "true", true, |config: Config| config.auto_select_next)]
304	#[case::diff_ignore_whitespace_default(
305		"diffIgnoreWhitespace",
306		"",
307		DiffIgnoreWhitespaceSetting::None,
308		|config: Config| config.diff_ignore_whitespace)
309	]
310	#[case::diff_ignore_whitespace_true(
311		"diffIgnoreWhitespace",
312		"true",
313		DiffIgnoreWhitespaceSetting::All,
314		|config: Config| config.diff_ignore_whitespace)
315	]
316	#[case::diff_ignore_whitespace_on(
317		"diffIgnoreWhitespace",
318		"on",
319		DiffIgnoreWhitespaceSetting::All,
320		|config: Config| config.diff_ignore_whitespace)
321	]
322	#[case::diff_ignore_whitespace_all(
323		"diffIgnoreWhitespace",
324		"all",
325		DiffIgnoreWhitespaceSetting::All,
326		|config: Config| config.diff_ignore_whitespace)
327	]
328	#[case::diff_ignore_whitespace_change(
329		"diffIgnoreWhitespace",
330		"change",
331		DiffIgnoreWhitespaceSetting::Change,
332		|config: Config| config.diff_ignore_whitespace)
333	]
334	#[case::diff_ignore_whitespace_false(
335		"diffIgnoreWhitespace",
336		"false",
337		DiffIgnoreWhitespaceSetting::None,
338		|config: Config| config.diff_ignore_whitespace)
339	]
340	#[case::diff_ignore_whitespace_off(
341		"diffIgnoreWhitespace",
342		"off",
343		DiffIgnoreWhitespaceSetting::None,
344		|config: Config| config.diff_ignore_whitespace)
345	]
346	#[case::diff_ignore_whitespace_none(
347		"diffIgnoreWhitespace",
348		"none",
349		DiffIgnoreWhitespaceSetting::None,
350		|config: Config| config.diff_ignore_whitespace)
351	]
352	#[case::diff_ignore_whitespace_mixed_case(
353		"diffIgnoreWhitespace",
354		"ChAnGe",
355		DiffIgnoreWhitespaceSetting::Change,
356		|config: Config| config.diff_ignore_whitespace)
357	]
358	#[case::diff_ignore_blank_lines_default(
359		"diffIgnoreBlankLines",
360		"",
361		false,
362		|config: Config| config.diff_ignore_blank_lines
363	)]
364	#[case::diff_ignore_blank_lines_false(
365		"diffIgnoreBlankLines",
366		"false",
367		false,
368		|config: Config| config.diff_ignore_blank_lines
369	)]
370	#[case::diff_ignore_blank_lines_true(
371		"diffIgnoreBlankLines",
372		"true",
373		true,
374		|config: Config| config.diff_ignore_blank_lines
375	)]
376	#[case::diff_show_whitespace_default(
377		"diffShowWhitespace",
378		"",
379		DiffShowWhitespaceSetting::Both,
380		|config: Config| config.diff_show_whitespace)
381	]
382	#[case::diff_show_whitespace_true(
383		"diffShowWhitespace",
384		"true",
385		DiffShowWhitespaceSetting::Both,
386		|config: Config| config.diff_show_whitespace)
387	]
388	#[case::diff_show_whitespace_on(
389		"diffShowWhitespace",
390		"on",
391		DiffShowWhitespaceSetting::Both,
392		|config: Config| config.diff_show_whitespace)
393	]
394	#[case::diff_show_whitespace_both(
395		"diffShowWhitespace",
396		"both",
397		DiffShowWhitespaceSetting::Both,
398		|config: Config| config.diff_show_whitespace)
399	]
400	#[case::diff_show_whitespace_trailing(
401		"diffShowWhitespace",
402		"trailing",
403		DiffShowWhitespaceSetting::Trailing,
404		|config: Config| config.diff_show_whitespace)
405	]
406	#[case::diff_show_whitespace_leading(
407		"diffShowWhitespace",
408		"leading",
409		DiffShowWhitespaceSetting::Leading,
410		|config: Config| config.diff_show_whitespace)
411	]
412	#[case::diff_show_whitespace_false(
413		"diffShowWhitespace",
414		"false",
415		DiffShowWhitespaceSetting::None,
416		|config: Config| config.diff_show_whitespace)
417	]
418	#[case::diff_show_whitespace_off(
419		"diffShowWhitespace",
420		"off",
421		DiffShowWhitespaceSetting::None,
422		|config: Config| config.diff_show_whitespace)
423	]
424	#[case::diff_show_whitespace_none(
425		"diffShowWhitespace",
426		"none",
427		DiffShowWhitespaceSetting::None,
428		|config: Config| config.diff_show_whitespace)
429	]
430	#[case::diff_show_whitespace_mixed_case(
431		"diffShowWhitespace",
432		"tRaIlInG",
433		DiffShowWhitespaceSetting::Trailing,
434		|config: Config| config.diff_show_whitespace)
435	]
436	#[case::diff_tab_width_default("diffTabWidth", "", 4, |config: Config| config.diff_tab_width)]
437	#[case::diff_tab_width("diffTabWidth", "42", 42, |config: Config| config.diff_tab_width)]
438	#[case::diff_tab_symbol_default("diffTabSymbol", "", String::from("→"), |config: Config| config.diff_tab_symbol)]
439	#[case::diff_tab_symbol("diffTabSymbol", "|", String::from("|"), |config: Config| config.diff_tab_symbol)]
440	#[case::diff_tab_symbol("diffTabSymbol", "|", String::from("|"), |config: Config| config.diff_tab_symbol)]
441	#[case::diff_space_symbol_default(
442		"diffSpaceSymbol",
443		"",
444		String::from("·"),
445		|config: Config| config.diff_space_symbol)
446	]
447	#[case::diff_space_symbol("diffSpaceSymbol", "-", String::from("-"), |config: Config| config.diff_space_symbol)]
448	#[case::undo_limit_default("undoLimit", "", 5000, |config: Config| config.undo_limit)]
449	#[case::undo_limit_default("undoLimit", "42", 42, |config: Config| config.undo_limit)]
450	pub(crate) fn theme_color<F, T>(
451		#[case] config_name: &str,
452		#[case] config_value: &str,
453		#[case] expected: T,
454		#[case] access: F,
455	) where
456		F: Fn(Config) -> T + 'static,
457		T: Debug + PartialEq,
458	{
459		let value = format!("{config_name} = \"{config_value}\"");
460		let lines = if config_value.is_empty() {
461			vec![]
462		}
463		else {
464			vec!["[interactive-rebase-tool]", value.as_str()]
465		};
466		with_git_config(&lines, |config| {
467			let config = Config::new_with_config(Some(&config)).unwrap();
468			assert_eq!(access(config), expected);
469		});
470	}
471
472	#[rstest]
473	#[case::auto_select_next("autoSelectNext", "invalid", ConfigErrorCause::InvalidBoolean)]
474	#[case::diff_ignore_whitespace("diffIgnoreWhitespace", "invalid", ConfigErrorCause::InvalidDiffIgnoreWhitespace)]
475	#[case::diff_ignore_blank_lines("diffIgnoreBlankLines", "invalid", ConfigErrorCause::InvalidBoolean)]
476	#[case::diff_show_whitespace("diffShowWhitespace", "invalid", ConfigErrorCause::InvalidShowWhitespace)]
477	#[case::diff_tab_width_non_integer("diffTabWidth", "invalid", ConfigErrorCause::InvalidUnsignedInteger)]
478	#[case::diff_tab_width_non_poitive_integer("diffTabWidth", "-100", ConfigErrorCause::InvalidUnsignedInteger)]
479	#[case::undo_limit_non_integer("undoLimit", "invalid", ConfigErrorCause::InvalidUnsignedInteger)]
480	#[case::undo_limit_non_positive_integer("undoLimit", "-100", ConfigErrorCause::InvalidUnsignedInteger)]
481	fn value_parsing_invalid(#[case] config_name: &str, #[case] config_value: &str, #[case] cause: ConfigErrorCause) {
482		with_git_config(
483			&[
484				"[interactive-rebase-tool]",
485				format!("{config_name} = {config_value}").as_str(),
486			],
487			|git_config| {
488				assert_err_eq!(
489					Config::new_with_config(Some(&git_config)),
490					ConfigError::new(
491						format!("interactive-rebase-tool.{config_name}").as_str(),
492						config_value,
493						cause
494					)
495				);
496			},
497		);
498	}
499
500	#[rstest]
501	#[case::diff_tab_symbol("diffIgnoreWhitespace")]
502	#[case::diff_tab_symbol("diffShowWhitespace")]
503	#[case::diff_tab_symbol("diffTabSymbol")]
504	#[case::diff_space_symbol("diffSpaceSymbol")]
505	fn value_parsing_invalid_utf(#[case] config_name: &str) {
506		with_git_config(
507			&[
508				"[interactive-rebase-tool]",
509				format!("{config_name} = {}", invalid_utf()).as_str(),
510			],
511			|git_config| {
512				assert_err_eq!(
513					Config::new_with_config(Some(&git_config)),
514					ConfigError::new_read_error(
515						format!("interactive-rebase-tool.{config_name}").as_str(),
516						ConfigErrorCause::InvalidUtf
517					)
518				);
519			},
520		);
521	}
522}