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#[derive(Clone, Debug)]
19#[non_exhaustive]
20pub struct GitConfig {
21 pub comment_char: String,
25 pub diff_context: u32,
29 pub diff_interhunk_lines: u32,
33 pub diff_rename_limit: u32,
37 pub diff_renames: bool,
41 pub diff_copies: bool,
45 pub editor: String,
49}
50
51impl GitConfig {
52 #[inline]
54 #[must_use]
55 #[allow(clippy::missing_panics_doc)]
56 pub fn new() -> Self {
57 Self::new_with_config(None).unwrap() }
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}