config/
git_config.rs

1use std::env;
2
3use git::Config;
4
5use crate::{
6	errors::ConfigError,
7	get_string,
8	utils::{get_unsigned_integer, git_diff_renames},
9};
10
11fn editor_from_env() -> String {
12	env::var("VISUAL")
13		.or_else(|_| env::var("EDITOR"))
14		.unwrap_or_else(|_| String::from("vi"))
15}
16
17/// Represents the git configuration options.
18#[derive(Clone, Debug)]
19#[non_exhaustive]
20pub struct GitConfig {
21	/// The Git comment character, from [`core.commentChar`](
22	///     https://git-scm.com/docs/git-config#Documentation/git-config.txt-corecommentChar
23	/// ).
24	pub comment_char: String,
25	/// Number of context lines, from [`diff.context`](
26	///     https://git-scm.com/docs/diff-config/#Documentation/diff-config.txt-diffcontext
27	/// ).
28	pub diff_context: u32,
29	/// Number of interhunk lines, from [`diff.interhunk_lines`](
30	///     https://git-scm.com/docs/diff-config/#Documentation/diff-config.txt-diffinterHunkContext
31	/// ).
32	pub diff_interhunk_lines: u32,
33	/// The limit for detecting renames, from [`diff.renameLimit`](
34	///     https://git-scm.com/docs/diff-config/#Documentation/diff-config.txt-diffrenameLimit
35	/// ).
36	pub diff_rename_limit: u32,
37	/// If to detect renames, from [`diff.renames`](
38	///     https://git-scm.com/docs/diff-config/#Documentation/diff-config.txt-diffrenames
39	/// ).
40	pub diff_renames: bool,
41	/// If to detect copies, from [`diff.renames`](
42	///     https://git-scm.com/docs/diff-config/#Documentation/diff-config.txt-diffrenames
43	/// ).
44	pub diff_copies: bool,
45	/// The Git editor, from [`core.editor`](
46	///     https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreeditor
47	/// ).
48	pub editor: String,
49}
50
51impl GitConfig {
52	/// Create a new configuration with default values.
53	#[inline]
54	#[must_use]
55	#[allow(clippy::missing_panics_doc)]
56	pub fn new() -> Self {
57		Self::new_with_config(None).unwrap() // should never error with None config
58	}
59
60	pub(super) fn new_with_config(git_config: Option<&Config>) -> Result<Self, ConfigError> {
61		let mut comment_char = get_string(git_config, "core.commentChar", "#")?;
62		if comment_char.as_str().eq("auto") {
63			comment_char = String::from("#");
64		}
65
66		let (diff_renames, diff_copies) = git_diff_renames(git_config, "diff.renames")?;
67
68		Ok(Self {
69			comment_char,
70			diff_context: get_unsigned_integer(git_config, "diff.context", 3)?,
71			diff_interhunk_lines: get_unsigned_integer(git_config, "diff.interHunkContext", 0)?,
72			diff_rename_limit: get_unsigned_integer(git_config, "diff.renameLimit", 200)?,
73			diff_renames,
74			diff_copies,
75			editor: get_string(git_config, "core.editor", editor_from_env().as_str())?,
76		})
77	}
78}
79
80impl TryFrom<&Config> for GitConfig {
81	type Error = ConfigError;
82
83	#[inline]
84	fn try_from(config: &Config) -> Result<Self, Self::Error> {
85		Self::new_with_config(Some(config))
86	}
87}
88
89#[cfg(test)]
90mod tests {
91	use std::env::{remove_var, set_var};
92
93	use claim::assert_ok;
94	use rstest::rstest;
95	use testutils::assert_err_eq;
96
97	use super::*;
98	use crate::{
99		testutils::{invalid_utf, with_git_config},
100		ConfigErrorCause,
101	};
102
103	macro_rules! config_test {
104		(
105			$key:ident,
106			$config_parent:literal,
107			$config_name:literal,
108			default $default:literal,
109			$($value: literal => $expected: literal),*
110		) => {
111			let config = GitConfig::new();
112			let value = config.$key;
113			assert_eq!(
114				value,
115				$default,
116				"Default value for '{}' was expected to be '{}' but '{}' was found",
117				stringify!($key),
118				$default,
119				value
120			);
121
122			for (value, expected) in [$( ($value, $expected), )*] {
123				let config_parent = format!("[{}]", $config_parent);
124				let config_value = format!("{} = \"{value}\"", $config_name);
125				with_git_config(&[config_parent.as_str(), config_value.as_str()], |git_config| {
126					let config = GitConfig::new_with_config(Some(&git_config)).unwrap();
127					assert_eq!(
128						config.$key,
129						expected,
130						"Value for '{}' was expected to be '{}' but '{}' was found",
131						stringify!($key),
132						$default,
133						value
134					);
135				});
136			}
137		};
138	}
139
140	#[test]
141	fn new() {
142		let _config = GitConfig::new();
143	}
144
145	#[test]
146	fn try_from_git_config() {
147		with_git_config(&[], |git_config| {
148			assert_ok!(GitConfig::try_from(&git_config));
149		});
150	}
151
152	#[test]
153	fn try_from_git_config_error() {
154		with_git_config(&["[diff]", "renames = invalid"], |git_config| {
155			_ = GitConfig::try_from(&git_config).unwrap_err();
156		});
157	}
158
159	#[rstest]
160	fn config_values() {
161		config_test!(comment_char, "core", "commentChar", default "#", ";" => ";", "auto" => "#");
162		config_test!(diff_context, "diff", "context", default 3, "5" => 5);
163		config_test!(diff_interhunk_lines, "diff", "interHunkContext", default 0, "5" => 5);
164		config_test!(diff_interhunk_lines, "diff", "interHunkContext", default 0, "5" => 5);
165		config_test!(diff_rename_limit, "diff", "renameLimit", default 200, "5" => 5);
166		config_test!(diff_renames, "diff", "renames", default true, "true" => true, "false" => false, "copy" => true);
167		config_test!(diff_copies, "diff", "renames",default false, "true" => false, "false" => false, "copy" => true);
168	}
169
170	#[test]
171	#[serial_test::serial]
172	fn git_editor_default_no_env() {
173		remove_var("VISUAL");
174		remove_var("EDITOR");
175		let config = GitConfig::new();
176		assert_eq!(config.editor, "vi");
177	}
178
179	#[test]
180	#[serial_test::serial]
181	fn git_editor_default_visual_env() {
182		remove_var("EDITOR");
183		set_var("VISUAL", "visual-editor");
184		let config = GitConfig::new();
185		assert_eq!(config.editor, "visual-editor");
186	}
187
188	#[test]
189	#[serial_test::serial]
190	fn git_editor_default_editor_env() {
191		remove_var("VISUAL");
192		set_var("EDITOR", "editor");
193
194		let config = GitConfig::new();
195		assert_eq!(config.editor, "editor");
196	}
197
198	#[test]
199	#[serial_test::serial]
200	fn git_editor() {
201		remove_var("VISUAL");
202		remove_var("EDITOR");
203		with_git_config(&["[core]", "editor = custom"], |git_config| {
204			let config = GitConfig::new_with_config(Some(&git_config)).unwrap();
205			assert_eq!(config.editor, "custom");
206		});
207	}
208
209	#[test]
210	fn diff_rename_limit_invalid() {
211		with_git_config(&["[diff]", "renameLimit = invalid"], |git_config| {
212			assert_err_eq!(
213				GitConfig::new_with_config(Some(&git_config)),
214				ConfigError::new("diff.renameLimit", "invalid", ConfigErrorCause::InvalidUnsignedInteger),
215			);
216		});
217	}
218
219	#[test]
220	fn diff_rename_limit_invalid_range() {
221		with_git_config(&["[diff]", "renameLimit = -100"], |git_config| {
222			assert_err_eq!(
223				GitConfig::new_with_config(Some(&git_config)),
224				ConfigError::new("diff.renameLimit", "-100", ConfigErrorCause::InvalidUnsignedInteger),
225			);
226		});
227	}
228
229	#[test]
230	fn diff_renames_invalid() {
231		with_git_config(&["[diff]", "renames = invalid"], |git_config| {
232			assert_err_eq!(
233				GitConfig::new_with_config(Some(&git_config)),
234				ConfigError::new("diff.renames", "invalid", ConfigErrorCause::InvalidDiffRenames),
235			);
236		});
237	}
238
239	#[test]
240	#[serial_test::serial]
241	fn git_editor_invalid() {
242		remove_var("VISUAL");
243		remove_var("EDITOR");
244		with_git_config(
245			&["[core]", format!("editor = {}", invalid_utf()).as_str()],
246			|git_config| {
247				assert_err_eq!(
248					GitConfig::new_with_config(Some(&git_config)),
249					ConfigError::new_read_error("core.editor", ConfigErrorCause::InvalidUtf),
250				);
251			},
252		);
253	}
254
255	#[test]
256	fn comment_char_invalid() {
257		with_git_config(
258			&["[core]", format!("commentChar = {}", invalid_utf()).as_str()],
259			|git_config| {
260				assert_err_eq!(
261					GitConfig::new_with_config(Some(&git_config)),
262					ConfigError::new_read_error("core.commentChar", ConfigErrorCause::InvalidUtf),
263				);
264			},
265		);
266	}
267
268	#[test]
269	fn diff_context_invalid() {
270		with_git_config(&["[diff]", "context = invalid"], |git_config| {
271			assert_err_eq!(
272				GitConfig::new_with_config(Some(&git_config)),
273				ConfigError::new("diff.context", "invalid", ConfigErrorCause::InvalidUnsignedInteger),
274			);
275		});
276	}
277
278	#[test]
279	fn diff_context_invalid_range() {
280		with_git_config(&["[diff]", "context = -100"], |git_config| {
281			assert_err_eq!(
282				GitConfig::new_with_config(Some(&git_config)),
283				ConfigError::new("diff.context", "-100", ConfigErrorCause::InvalidUnsignedInteger),
284			);
285		});
286	}
287
288	#[test]
289	fn diff_interhunk_lines_invalid() {
290		with_git_config(&["[diff]", "interHunkContext = invalid"], |git_config| {
291			assert_err_eq!(
292				GitConfig::new_with_config(Some(&git_config)),
293				ConfigError::new(
294					"diff.interHunkContext",
295					"invalid",
296					ConfigErrorCause::InvalidUnsignedInteger
297				),
298			);
299		});
300	}
301
302	#[test]
303	fn diff_interhunk_lines_invalid_range() {
304		with_git_config(&["[diff]", "interHunkContext = -100"], |git_config| {
305			assert_err_eq!(
306				GitConfig::new_with_config(Some(&git_config)),
307				ConfigError::new(
308					"diff.interHunkContext",
309					"-100",
310					ConfigErrorCause::InvalidUnsignedInteger
311				),
312			);
313		});
314	}
315}