1use std::fmt;
30use std::fs;
31use std::path::{Path, PathBuf};
32
33use crate::error::{Error, Result};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub enum ConfigScope {
38 System,
40 Global,
42 Local,
44 Worktree,
46 Command,
48}
49
50impl fmt::Display for ConfigScope {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::System => write!(f, "system"),
54 Self::Global => write!(f, "global"),
55 Self::Local => write!(f, "local"),
56 Self::Worktree => write!(f, "worktree"),
57 Self::Command => write!(f, "command"),
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct ConfigEntry {
65 pub key: String,
68 pub value: Option<String>,
70 pub scope: ConfigScope,
72 pub file: Option<PathBuf>,
74 pub line: usize,
76}
77
78#[derive(Debug, Clone)]
81pub struct ConfigFile {
82 pub path: PathBuf,
84 pub scope: ConfigScope,
86 pub entries: Vec<ConfigEntry>,
88 raw_lines: Vec<String>,
90}
91
92#[derive(Debug, Clone, Default)]
97pub struct ConfigSet {
98 entries: Vec<ConfigEntry>,
100}
101
102pub fn canonical_key(raw: &str) -> Result<String> {
118 if raw.contains('\n') || raw.contains('\r') {
120 return Err(Error::ConfigError(format!("invalid key: '{}'" , raw.replace('\n', "\\n"))));
121 }
122
123 let first_dot = raw
124 .find('.')
125 .ok_or_else(|| Error::ConfigError(format!("key does not contain a section: '{raw}'")))?;
126 let last_dot = raw
127 .rfind('.')
128 .ok_or_else(|| Error::ConfigError(format!("key does not contain a section: '{raw}'")))?;
129
130 if last_dot == raw.len() - 1 {
131 return Err(Error::ConfigError(format!(
132 "key does not contain variable name: '{raw}'"
133 )));
134 }
135
136 let section = &raw[..first_dot];
137 let name = &raw[last_dot + 1..];
138
139 if section.is_empty() || !section.chars().all(|c| c.is_alphanumeric() || c == '-') {
141 return Err(Error::ConfigError(format!("invalid key (bad section): '{raw}'")));
142 }
143
144 if name.is_empty()
146 || !name.chars().next().unwrap().is_ascii_alphabetic()
147 || !name.chars().all(|c| c.is_alphanumeric() || c == '-')
148 {
149 return Err(Error::ConfigError(format!("invalid key (bad variable name): '{raw}'")));
150 }
151
152 if first_dot == last_dot {
153 Ok(format!(
155 "{}.{}",
156 section.to_lowercase(),
157 name.to_lowercase()
158 ))
159 } else {
160 let subsection = &raw[first_dot + 1..last_dot];
162 Ok(format!(
163 "{}.{}.{}",
164 section.to_lowercase(),
165 subsection,
166 name.to_lowercase()
167 ))
168 }
169}
170
171struct Parser {
175 section: String,
176 subsection: Option<String>,
177}
178
179impl Parser {
180 fn new() -> Self {
181 Self {
182 section: String::new(),
183 subsection: None,
184 }
185 }
186
187 fn make_key(&self, name: &str) -> String {
189 let sec = self.section.to_lowercase();
190 let var = name.to_lowercase();
191 match &self.subsection {
192 Some(sub) => format!("{sec}.{sub}.{var}"),
193 None => format!("{sec}.{var}"),
194 }
195 }
196
197 fn try_parse_section(&mut self, line: &str) -> bool {
201 let trimmed = line.trim();
202 if !trimmed.starts_with('[') {
203 return false;
204 }
205 let end = match trimmed.find(']') {
206 Some(i) => i,
207 None => return false,
208 };
209 let inside = &trimmed[1..end];
210 if let Some(quote_start) = inside.find('"') {
212 self.section = inside[..quote_start].trim().to_owned();
213 let rest = &inside[quote_start + 1..];
214 if let Some(quote_end) = rest.find('"') {
215 self.subsection = Some(rest[..quote_end].to_owned());
216 } else {
217 self.subsection = Some(rest.to_owned());
218 }
219 } else {
220 self.section = inside.trim().to_owned();
221 self.subsection = None;
222 }
223 true
224 }
225
226 fn try_parse_entry(&self, line: &str) -> Option<(String, Option<String>)> {
230 let trimmed = line.trim();
231 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
232 return None;
233 }
234 if trimmed.starts_with('[') {
235 return None;
236 }
237 if self.section.is_empty() {
238 return None;
239 }
240
241 if let Some(eq_pos) = trimmed.find('=') {
242 let raw_name = trimmed[..eq_pos].trim();
243 let raw_value = trimmed[eq_pos + 1..].trim();
244 let value = strip_inline_comment(raw_value);
246 let value = unescape_value(&value);
247 let key = self.make_key(raw_name);
248 Some((key, Some(value)))
249 } else {
250 let raw_name = strip_inline_comment(trimmed);
252 let key = self.make_key(raw_name.trim());
253 Some((key, None))
254 }
255 }
256}
257
258fn value_line_continues(line: &str) -> bool {
264 let trimmed = line.trim();
265 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
266 return false;
267 }
268 let value_part = match trimmed.find('=') {
271 Some(pos) => &trimmed[pos + 1..],
272 None => return false,
273 };
274 let mut in_quote = false;
276 let mut last_was_backslash = false;
277 let mut in_comment = false;
278 for ch in value_part.chars() {
279 if in_comment {
280 last_was_backslash = false;
282 continue;
283 }
284 match ch {
285 '"' if !last_was_backslash => {
286 in_quote = !in_quote;
287 last_was_backslash = false;
288 }
289 '\\' if !last_was_backslash => {
290 last_was_backslash = true;
291 continue;
292 }
293 '#' | ';' if !in_quote && !last_was_backslash => {
294 in_comment = true;
295 last_was_backslash = false;
296 }
297 _ => {
298 last_was_backslash = false;
299 }
300 }
301 }
302 last_was_backslash && !in_comment
304}
305
306fn strip_inline_comment(s: &str) -> String {
308 let mut in_quote = false;
309 let mut result = String::with_capacity(s.len());
310 let mut chars = s.chars().peekable();
311 while let Some(ch) = chars.next() {
312 match ch {
313 '"' => {
314 in_quote = !in_quote;
315 result.push(ch);
316 }
317 '\\' if in_quote => {
318 result.push(ch);
319 if let Some(&next) = chars.peek() {
320 result.push(next);
321 chars.next();
322 }
323 }
324 '#' | ';' if !in_quote => break,
325 _ => result.push(ch),
326 }
327 }
328 let trimmed = result.trim_end();
330 trimmed.to_owned()
331}
332
333fn unescape_value(s: &str) -> String {
336 let mut result = String::with_capacity(s.len());
337 let mut chars = s.chars();
338 while let Some(ch) = chars.next() {
339 match ch {
340 '"' => { }
341 '\\' => match chars.next() {
342 Some('n') => result.push('\n'),
343 Some('t') => result.push('\t'),
344 Some('\\') => result.push('\\'),
345 Some('"') => result.push('"'),
346 Some(other) => {
347 result.push('\\');
348 result.push(other);
349 }
350 None => result.push('\\'),
351 },
352 _ => result.push(ch),
353 }
354 }
355 result
356}
357
358fn escape_value(s: &str) -> String {
363 let needs_quoting = s.starts_with(' ')
364 || s.starts_with('\t')
365 || s.ends_with(' ')
366 || s.ends_with('\t')
367 || s.contains('"')
368 || s.contains('\\')
369 || s.contains('\n')
370 || s.contains('#')
371 || s.contains(';');
372
373 if !needs_quoting {
374 return s.to_owned();
375 }
376
377 let mut out = String::with_capacity(s.len() + 4);
378 out.push('"');
379 for ch in s.chars() {
380 match ch {
381 '"' => out.push_str("\\\""),
382 '\\' => out.push_str("\\\\"),
383 '\n' => out.push_str("\\n"),
384 '\t' => out.push_str("\\t"),
385 other => out.push(other),
386 }
387 }
388 out.push('"');
389 out
390}
391
392impl ConfigFile {
395 pub fn parse(path: &Path, content: &str, scope: ConfigScope) -> Result<Self> {
407 let raw_lines: Vec<String> = content.lines().map(String::from).collect();
408 let mut entries = Vec::new();
409 let mut parser = Parser::new();
410
411 let mut idx = 0;
412 while idx < raw_lines.len() {
413 let start_idx = idx;
414 let line = &raw_lines[idx];
415 idx += 1;
416
417 let trimmed = line.trim();
419 if trimmed.starts_with('#') || trimmed.starts_with(';') {
420 continue;
421 }
422
423 if parser.try_parse_section(line) {
424 continue;
425 }
426
427 let mut logical_line = line.clone();
430 while value_line_continues(&logical_line) && idx < raw_lines.len() {
431 let t = logical_line.trim_end();
433 logical_line = t[..t.len() - 1].to_string();
434 let next = raw_lines[idx].trim_start();
436 logical_line.push_str(next);
437 idx += 1;
438 }
439
440 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
441 entries.push(ConfigEntry {
442 key,
443 value,
444 scope,
445 file: Some(path.to_path_buf()),
446 line: start_idx + 1,
447 });
448 }
449 }
450
451 Ok(Self {
452 path: path.to_path_buf(),
453 scope,
454 entries,
455 raw_lines,
456 })
457 }
458
459 pub fn from_path(path: &Path, scope: ConfigScope) -> Result<Option<Self>> {
468 match fs::read_to_string(path) {
469 Ok(content) => Ok(Some(Self::parse(path, &content, scope)?)),
470 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
471 Err(e) => Err(Error::Io(e)),
472 }
473 }
474
475 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
486 let canon = canonical_key(key)?;
487 let var_lower = raw_variable_name(key).to_lowercase();
489
490 let existing_idx = self.entries.iter().rposition(|e| e.key == canon);
492
493 if let Some(idx) = existing_idx {
494 let line_idx = self.entries[idx].line - 1;
495 self.raw_lines[line_idx] = format!("\t{} = {}", var_lower, escape_value(value));
496 self.entries[idx].value = Some(value.to_owned());
497 } else {
498 let (section, subsection, _var) = split_key(&canon)?;
500 let (_raw_sec, raw_sub) = raw_section_parts(key);
502 let section_line = self.find_or_create_section_preserving_case(
503 §ion, subsection.as_deref(),
504 §ion, raw_sub.as_deref(),
505 );
506 let new_line = format!("\t{} = {}", var_lower, escape_value(value));
507
508 let insert_at = self.last_line_in_section(section_line) + 1;
510 self.raw_lines.insert(insert_at, new_line);
511
512 let content = self.raw_lines.join("\n");
514 let reparsed = Self::parse(&self.path, &content, self.scope)?;
515 self.entries = reparsed.entries;
516 self.raw_lines = reparsed.raw_lines;
517 }
518
519 Ok(())
520 }
521
522 pub fn replace_all(&mut self, key: &str, value: &str, value_pattern: Option<&str>) -> Result<()> {
527 let canon = canonical_key(key)?;
528 let var_lower = raw_variable_name(key).to_lowercase();
529
530 let re = match value_pattern {
532 Some(pat) => Some(
533 regex::Regex::new(pat)
534 .map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?),
535 None => None,
536 };
537
538 let matching_indices: Vec<usize> = self
540 .entries
541 .iter()
542 .enumerate()
543 .filter(|(_, e)| {
544 if e.key != canon {
545 return false;
546 }
547 if let Some(ref re) = re {
548 let v = e.value.as_deref().unwrap_or("");
549 re.is_match(v)
550 } else {
551 true
552 }
553 })
554 .map(|(i, _)| i)
555 .collect();
556
557 if matching_indices.is_empty() {
558 return self.set(key, value);
560 }
561
562 let first_match = matching_indices[0];
564 let lines_to_remove: Vec<usize> = matching_indices
565 .iter()
566 .skip(1)
567 .map(|&i| self.entries[i].line - 1)
568 .collect();
569
570 let first_line_idx = self.entries[first_match].line - 1;
572 self.raw_lines[first_line_idx] = format!("\t{} = {}", var_lower, escape_value(value));
573 self.entries[first_match].value = Some(value.to_owned());
574
575 for &line_idx in lines_to_remove.iter().rev() {
577 self.raw_lines.remove(line_idx);
578 }
579
580 let content = self.raw_lines.join("\n");
582 let reparsed = Self::parse(&self.path, &content, self.scope)?;
583 self.entries = reparsed.entries;
584 self.raw_lines = reparsed.raw_lines;
585
586 Ok(())
587 }
588
589 pub fn count(&self, key: &str) -> Result<usize> {
591 let canon = canonical_key(key)?;
592 Ok(self.entries.iter().filter(|e| e.key == canon).count())
593 }
594
595 pub fn unset_last(&mut self, key: &str) -> Result<usize> {
599 let canon = canonical_key(key)?;
600 let last_idx = self.entries.iter().rposition(|e| e.key == canon);
601
602 if let Some(idx) = last_idx {
603 let line_idx = self.entries[idx].line - 1;
604 self.raw_lines.remove(line_idx);
605 let content = self.raw_lines.join("\n");
606 let reparsed = Self::parse(&self.path, &content, self.scope)?;
607 self.entries = reparsed.entries;
608 self.raw_lines = reparsed.raw_lines;
609 Ok(1)
610 } else {
611 Ok(0)
612 }
613 }
614
615 pub fn unset(&mut self, key: &str) -> Result<usize> {
625 let canon = canonical_key(key)?;
626 let line_indices: Vec<usize> = self
627 .entries
628 .iter()
629 .filter(|e| e.key == canon)
630 .map(|e| e.line - 1)
631 .collect();
632
633 let count = line_indices.len();
634 for &idx in line_indices.iter().rev() {
636 self.raw_lines.remove(idx);
637 }
638
639 if count > 0 {
640 let content = self.raw_lines.join("\n");
641 let reparsed = Self::parse(&self.path, &content, self.scope)?;
642 self.entries = reparsed.entries;
643 self.raw_lines = reparsed.raw_lines;
644 }
645
646 Ok(count)
647 }
648
649 pub fn unset_matching(&mut self, key: &str, value_pattern: Option<&str>) -> Result<usize> {
654 let canon = canonical_key(key)?;
655 let re = match value_pattern {
656 Some(pat) => Some(
657 regex::Regex::new(pat)
658 .map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?),
659 None => None,
660 };
661
662 let line_indices: Vec<usize> = self
663 .entries
664 .iter()
665 .filter(|e| {
666 if e.key != canon {
667 return false;
668 }
669 if let Some(ref re) = re {
670 let v = e.value.as_deref().unwrap_or("");
671 re.is_match(v)
672 } else {
673 true
674 }
675 })
676 .map(|e| e.line - 1)
677 .collect();
678
679 let count = line_indices.len();
680 for &idx in line_indices.iter().rev() {
681 self.raw_lines.remove(idx);
682 }
683
684 if count > 0 {
685 let content = self.raw_lines.join("\n");
686 let reparsed = Self::parse(&self.path, &content, self.scope)?;
687 self.entries = reparsed.entries;
688 self.raw_lines = reparsed.raw_lines;
689 }
690
691 Ok(count)
692 }
693
694 pub fn remove_section(&mut self, section: &str) -> Result<bool> {
700 let (sec_name, sub_name) = parse_section_name(section);
701 let sec_lower = sec_name.to_lowercase();
702
703 let mut start = None;
705 let mut end = 0;
706 let mut parser = Parser::new();
707
708 for (idx, line) in self.raw_lines.iter().enumerate() {
709 if parser.try_parse_section(line) {
710 if parser.section.to_lowercase() == sec_lower
711 && parser.subsection.as_deref() == sub_name
712 {
713 start = Some(idx);
714 end = idx;
715 } else if start.is_some() {
716 break;
717 }
718 } else if start.is_some() {
719 end = idx;
720 }
721 }
722
723 if let Some(s) = start {
724 self.raw_lines.drain(s..=end);
725 let content = self.raw_lines.join("\n");
726 let reparsed = Self::parse(&self.path, &content, self.scope)?;
727 self.entries = reparsed.entries;
728 self.raw_lines = reparsed.raw_lines;
729 Ok(true)
730 } else {
731 Ok(false)
732 }
733 }
734
735 pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
742 let (old_sec, old_sub) = parse_section_name(old_name);
743 let (new_sec, new_sub) = parse_section_name(new_name);
744 let old_lower = old_sec.to_lowercase();
745
746 let mut found = false;
747 let mut parser = Parser::new();
748
749 for idx in 0..self.raw_lines.len() {
750 let line = &self.raw_lines[idx];
751 if parser.try_parse_section(line)
752 && parser.section.to_lowercase() == old_lower
753 && parser.subsection.as_deref() == old_sub
754 {
755 let header = match new_sub {
757 Some(sub) => format!("[{} \"{}\"]", new_sec, sub),
758 None => format!("[{}]", new_sec),
759 };
760 self.raw_lines[idx] = header;
761 found = true;
762 }
763 }
764
765 if found {
766 let content = self.raw_lines.join("\n");
767 let reparsed = Self::parse(&self.path, &content, self.scope)?;
768 self.entries = reparsed.entries;
769 self.raw_lines = reparsed.raw_lines;
770 }
771
772 Ok(found)
773 }
774
775 pub fn add_value(&mut self, key: &str, value: &str) -> Result<()> {
780 let canon = canonical_key(key)?;
781 let raw_var = raw_variable_name(key);
782 let (section, subsection, _var) = split_key(&canon)?;
783 let (raw_sec, raw_sub) = raw_section_parts(key);
784
785 let section_line = self.find_or_create_section_preserving_case(
786 §ion, subsection.as_deref(),
787 &raw_sec, raw_sub.as_deref(),
788 );
789 let new_line = format!("\t{} = {}", raw_var, escape_value(value));
790 let insert_at = self.last_line_in_section(section_line) + 1;
791 self.raw_lines.insert(insert_at, new_line);
792
793 let content = self.raw_lines.join("\n");
795 let reparsed = Self::parse(&self.path, &content, self.scope)?;
796 self.entries = reparsed.entries;
797 self.raw_lines = reparsed.raw_lines;
798
799 Ok(())
800 }
801
802 pub fn write(&self) -> Result<()> {
808 let content = self.raw_lines.join("\n");
809 let content = if content.ends_with('\n') {
811 content
812 } else {
813 format!("{content}\n")
814 };
815 fs::write(&self.path, content)?;
816 Ok(())
817 }
818
819 #[allow(dead_code)]
821 fn find_or_create_section(&mut self, section: &str, subsection: Option<&str>) -> usize {
822 let sec_lower = section.to_lowercase();
823 let mut parser = Parser::new();
824
825 for (idx, line) in self.raw_lines.iter().enumerate() {
826 if parser.try_parse_section(line)
827 && parser.section.to_lowercase() == sec_lower
828 && parser.subsection.as_deref() == subsection
829 {
830 return idx;
831 }
832 }
833
834 let header = match subsection {
836 Some(sub) => format!("[{} \"{}\"]", section, sub),
837 None => format!("[{}]", section),
838 };
839 self.raw_lines.push(header);
840 self.raw_lines.len() - 1
841 }
842
843 fn find_or_create_section_preserving_case(
846 &mut self,
847 section: &str,
848 subsection: Option<&str>,
849 raw_section: &str,
850 raw_subsection: Option<&str>,
851 ) -> usize {
852 let sec_lower = section.to_lowercase();
853 let mut parser = Parser::new();
854
855 for (idx, line) in self.raw_lines.iter().enumerate() {
856 if parser.try_parse_section(line)
857 && parser.section.to_lowercase() == sec_lower
858 && parser.subsection.as_deref() == subsection
859 {
860 return idx;
861 }
862 }
863
864 let header = match raw_subsection {
866 Some(sub) => format!("[{} \"{}\"]", raw_section, sub),
867 None => format!("[{}]", raw_section),
868 };
869 self.raw_lines.push(header);
870 self.raw_lines.len() - 1
871 }
872
873 fn last_line_in_section(&self, section_line: usize) -> usize {
875 let mut last = section_line;
876 for idx in (section_line + 1)..self.raw_lines.len() {
877 let trimmed = self.raw_lines[idx].trim();
878 if trimmed.starts_with('[') {
879 break;
880 }
881 last = idx;
882 }
883 last
884 }
885}
886
887impl ConfigSet {
890 #[must_use]
892 pub fn new() -> Self {
893 Self {
894 entries: Vec::new(),
895 }
896 }
897
898 pub fn merge(&mut self, file: &ConfigFile) {
903 self.entries.extend(file.entries.iter().cloned());
904 }
905
906 pub fn add_command_override(&mut self, key: &str, value: &str) -> Result<()> {
908 let canon = canonical_key(key)?;
909 self.entries.push(ConfigEntry {
910 key: canon,
911 value: Some(value.to_owned()),
912 scope: ConfigScope::Command,
913 file: None,
914 line: 0,
915 });
916 Ok(())
917 }
918
919 #[must_use]
930 pub fn get(&self, key: &str) -> Option<String> {
931 let canon = canonical_key(key).ok()?;
932 self.entries
933 .iter()
934 .rev()
935 .find(|e| e.key == canon)
936 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
937 }
938
939 #[must_use]
941 pub fn get_all(&self, key: &str) -> Vec<String> {
942 let canon = match canonical_key(key) {
943 Ok(c) => c,
944 Err(_) => return Vec::new(),
945 };
946 self.entries
947 .iter()
948 .filter(|e| e.key == canon)
949 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
950 .collect()
951 }
952
953 pub fn get_bool(&self, key: &str) -> Option<std::result::Result<bool, String>> {
956 self.get(key).map(|v| parse_bool(&v))
957 }
958
959 pub fn get_i64(&self, key: &str) -> Option<std::result::Result<i64, String>> {
961 self.get(key).map(|v| parse_i64(&v))
962 }
963
964 pub fn get_regexp(&self, pattern: &str) -> std::result::Result<Vec<&ConfigEntry>, String> {
969 let re = regex::Regex::new(pattern)
970 .map_err(|e| format!("invalid key pattern: {e}"))?;
971 Ok(self.entries
972 .iter()
973 .filter(|e| re.is_match(&e.key))
974 .collect())
975 }
976
977 #[must_use]
979 pub fn entries(&self) -> &[ConfigEntry] {
980 &self.entries
981 }
982
983 pub fn load(git_dir: Option<&Path>, include_system: bool) -> Result<Self> {
994 let mut set = Self::new();
995
996 if include_system {
998 if let Ok(Some(f)) =
999 ConfigFile::from_path(Path::new("/etc/gitconfig"), ConfigScope::System)
1000 {
1001 Self::merge_with_includes(&mut set, &f, true)?;
1002 }
1003 }
1004
1005 for path in global_config_paths() {
1007 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
1008 Self::merge_with_includes(&mut set, &f, true)?;
1009 break; }
1011 }
1012
1013 if let Some(gd) = git_dir {
1015 let local_path = gd.join("config");
1016 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
1017 Self::merge_with_includes(&mut set, &f, true)?;
1018 }
1019
1020 let wt_path = gd.join("config.worktree");
1022 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
1023 Self::merge_with_includes(&mut set, &f, true)?;
1024 }
1025 }
1026
1027 if let Ok(path) = std::env::var("GIT_CONFIG") {
1029 if let Ok(Some(f)) = ConfigFile::from_path(Path::new(&path), ConfigScope::Command) {
1030 set.merge(&f);
1031 }
1032 }
1033
1034 if let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") {
1036 if let Ok(count) = count_str.parse::<usize>() {
1037 for i in 0..count {
1038 let key_var = format!("GIT_CONFIG_KEY_{i}");
1039 let val_var = format!("GIT_CONFIG_VALUE_{i}");
1040 if let (Ok(key), Ok(val)) = (std::env::var(&key_var), std::env::var(&val_var)) {
1041 let _ = set.add_command_override(&key, &val);
1042 }
1043 }
1044 }
1045 }
1046
1047 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
1050 for entry in parse_config_parameters(¶ms) {
1051 if let Some((key, val)) = entry.split_once('=') {
1052 let _ = set.add_command_override(key.trim(), val.trim());
1053 } else {
1054 let _ = set.add_command_override(entry.trim(), "true");
1056 }
1057 }
1058 }
1059
1060 Ok(set)
1061 }
1062
1063 fn merge_with_includes(
1065 set: &mut Self,
1066 file: &ConfigFile,
1067 process_includes: bool,
1068 ) -> Result<()> {
1069 let mut includes: Vec<(String, Option<String>)> = Vec::new();
1071
1072 for entry in &file.entries {
1073 if entry.key == "include.path" {
1074 if let Some(ref val) = entry.value {
1075 includes.push((val.clone(), None));
1076 }
1077 } else if entry.key.starts_with("includeif.") && entry.key.ends_with(".path") {
1078 let mid = &entry.key["includeif.".len()..entry.key.len() - ".path".len()];
1080 if let Some(ref val) = entry.value {
1081 includes.push((val.clone(), Some(mid.to_owned())));
1082 }
1083 }
1084 }
1085
1086 set.merge(file);
1088
1089 if process_includes {
1091 for (inc_path, condition) in includes {
1092 if let Some(ref cond) = condition {
1093 if !evaluate_include_condition(cond, file) {
1094 continue;
1095 }
1096 }
1097
1098 let resolved = resolve_include_path(&inc_path, file.path.parent());
1099 if let Ok(Some(inc_file)) = ConfigFile::from_path(&resolved, file.scope) {
1100 Self::merge_with_includes(set, &inc_file, true)?;
1101 }
1102 }
1103 }
1104
1105 Ok(())
1106 }
1107}
1108
1109pub fn parse_bool(s: &str) -> std::result::Result<bool, String> {
1116 match s.to_lowercase().as_str() {
1117 "true" | "yes" | "on" | "" => Ok(true),
1118 "false" | "no" | "off" => Ok(false),
1119 _ => {
1120 if let Ok(n) = s.parse::<i64>() {
1122 return Ok(n != 0);
1123 }
1124 Err(format!("bad boolean config value '{s}'"))
1125 }
1126 }
1127}
1128
1129pub fn parse_i64(s: &str) -> std::result::Result<i64, String> {
1131 let s = s.trim();
1132 if s.is_empty() {
1133 return Err("empty integer value".to_owned());
1134 }
1135
1136 let (num_str, multiplier) = match s.as_bytes().last() {
1137 Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
1138 Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
1139 Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
1140 _ => (s, 1_i64),
1141 };
1142
1143 let base: i64 = num_str
1144 .parse()
1145 .map_err(|_| format!("invalid integer: '{s}'"))?;
1146 base.checked_mul(multiplier)
1147 .ok_or_else(|| format!("integer overflow: '{s}'"))
1148}
1149
1150pub fn parse_path(s: &str) -> String {
1152 if let Some(rest) = s.strip_prefix("~/") {
1153 if let Some(home) = home_dir() {
1154 return home.join(rest).to_string_lossy().to_string();
1155 }
1156 }
1157 s.to_owned()
1158}
1159
1160fn parse_config_parameters(raw: &str) -> Vec<String> {
1165 let mut out: Vec<String> = Vec::new();
1166 let mut iter = raw.chars().peekable();
1167 while let Some(&c) = iter.peek() {
1168 if c == '\'' {
1169 iter.next();
1170 let mut s = String::new();
1171 loop {
1172 match iter.next() {
1173 Some('\'') | None => break,
1174 Some(x) => s.push(x),
1175 }
1176 }
1177 if !s.is_empty() {
1178 out.push(s);
1179 }
1180 } else {
1181 iter.next();
1182 }
1183 }
1184 out
1185}
1186
1187fn global_config_paths() -> Vec<PathBuf> {
1189 let mut paths = Vec::new();
1190
1191 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
1193 paths.push(PathBuf::from(p));
1194 return paths;
1195 }
1196
1197 if let Some(home) = home_dir() {
1199 paths.push(home.join(".gitconfig"));
1200 }
1201
1202 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
1204 paths.push(PathBuf::from(xdg).join("git/config"));
1205 } else if let Some(home) = home_dir() {
1206 paths.push(home.join(".config/git/config"));
1207 }
1208
1209 paths
1210}
1211
1212fn home_dir() -> Option<PathBuf> {
1214 std::env::var("HOME").ok().map(PathBuf::from)
1215}
1216
1217fn resolve_include_path(path: &str, base: Option<&Path>) -> PathBuf {
1219 let expanded = parse_path(path);
1220 let p = Path::new(&expanded);
1221 if p.is_absolute() {
1222 p.to_path_buf()
1223 } else if let Some(base) = base {
1224 base.join(p)
1225 } else {
1226 p.to_path_buf()
1227 }
1228}
1229
1230fn evaluate_include_condition(condition: &str, _file: &ConfigFile) -> bool {
1236 let _ = condition;
1239 false
1240}
1241
1242fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
1244 let first_dot = key
1245 .find('.')
1246 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
1247 let last_dot = key
1248 .rfind('.')
1249 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
1250
1251 let section = key[..first_dot].to_owned();
1252 let variable = key[last_dot + 1..].to_owned();
1253
1254 let subsection = if first_dot == last_dot {
1255 None
1256 } else {
1257 Some(key[first_dot + 1..last_dot].to_owned())
1258 };
1259
1260 Ok((section, subsection, variable))
1261}
1262
1263#[allow(dead_code)]
1265fn variable_name_from_key(key: &str) -> &str {
1266 match key.rfind('.') {
1267 Some(i) => &key[i + 1..],
1268 None => key,
1269 }
1270}
1271
1272fn parse_section_name(name: &str) -> (&str, Option<&str>) {
1276 match name.find('.') {
1277 Some(i) => (&name[..i], Some(&name[i + 1..])),
1278 None => (name, None),
1279 }
1280}
1281
1282fn raw_variable_name(raw_key: &str) -> &str {
1286 match raw_key.rfind('.') {
1287 Some(i) => &raw_key[i + 1..],
1288 None => raw_key,
1289 }
1290}
1291
1292fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
1297 let first_dot = match raw_key.find('.') {
1298 Some(i) => i,
1299 None => return (raw_key.to_owned(), None),
1300 };
1301 let last_dot = match raw_key.rfind('.') {
1303 Some(i) => i,
1304 None => return (raw_key[..first_dot].to_owned(), None),
1305 };
1306 let section = raw_key[..first_dot].to_owned();
1307 if first_dot == last_dot {
1308 (section, None)
1309 } else {
1310 let subsection = raw_key[first_dot + 1..last_dot].to_owned();
1311 (section, Some(subsection))
1312 }
1313}