1use std::fmt;
30use std::fs;
31use std::path::{Path, PathBuf};
32
33use crate::error::{Error, Result};
34use crate::refs;
35use crate::wildmatch::{wildmatch, WM_CASEFOLD, WM_PATHNAME};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub enum ConfigScope {
40 System,
42 Global,
44 Local,
46 Worktree,
48 Command,
50}
51
52impl fmt::Display for ConfigScope {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 Self::System => write!(f, "system"),
56 Self::Global => write!(f, "global"),
57 Self::Local => write!(f, "local"),
58 Self::Worktree => write!(f, "worktree"),
59 Self::Command => write!(f, "command"),
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct ConfigEntry {
67 pub key: String,
70 pub value: Option<String>,
72 pub scope: ConfigScope,
74 pub file: Option<PathBuf>,
76 pub line: usize,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum ConfigIncludeOrigin {
83 Disk,
85 Stdin,
87 CommandLine,
89 Blob,
91}
92
93#[derive(Debug, Clone)]
96pub struct ConfigFile {
97 pub path: PathBuf,
99 pub scope: ConfigScope,
101 pub entries: Vec<ConfigEntry>,
103 raw_lines: Vec<String>,
105 pub include_origin: ConfigIncludeOrigin,
107}
108
109#[derive(Debug, Clone, Default)]
114pub struct ConfigSet {
115 entries: Vec<ConfigEntry>,
117}
118
119#[derive(Debug, Clone, Default)]
121pub struct IncludeContext {
122 pub git_dir: Option<PathBuf>,
124 pub command_line_relative_include_is_error: bool,
126}
127
128#[derive(Debug, Clone)]
130pub struct LoadConfigOptions {
131 pub include_system: bool,
133 pub process_includes: bool,
135 pub command_includes: bool,
137 pub include_ctx: IncludeContext,
138}
139
140impl Default for LoadConfigOptions {
141 fn default() -> Self {
142 Self {
143 include_system: true,
144 process_includes: true,
145 command_includes: true,
146 include_ctx: IncludeContext::default(),
147 }
148 }
149}
150
151pub fn canonical_key(raw: &str) -> Result<String> {
167 if raw.contains('\n') || raw.contains('\r') {
169 return Err(Error::ConfigError(format!(
170 "invalid key: '{}'",
171 raw.replace('\n', "\\n")
172 )));
173 }
174
175 let first_dot = raw
176 .find('.')
177 .ok_or_else(|| Error::ConfigError(format!("key does not contain a section: '{raw}'")))?;
178 let last_dot = raw
179 .rfind('.')
180 .ok_or_else(|| Error::ConfigError(format!("key does not contain a section: '{raw}'")))?;
181
182 if last_dot == raw.len() - 1 {
183 return Err(Error::ConfigError(format!(
184 "key does not contain variable name: '{raw}'"
185 )));
186 }
187
188 let section = &raw[..first_dot];
189 let name = &raw[last_dot + 1..];
190
191 if section.is_empty() || !section.chars().all(|c| c.is_alphanumeric() || c == '-') {
193 return Err(Error::ConfigError(format!(
194 "invalid key (bad section): '{raw}'"
195 )));
196 }
197
198 if name.is_empty()
200 || !name.chars().next().unwrap().is_ascii_alphabetic()
201 || !name.chars().all(|c| c.is_alphanumeric() || c == '-')
202 {
203 return Err(Error::ConfigError(format!(
204 "invalid key (bad variable name): '{raw}'"
205 )));
206 }
207
208 if first_dot == last_dot {
209 Ok(format!(
211 "{}.{}",
212 section.to_lowercase(),
213 name.to_lowercase()
214 ))
215 } else {
216 let subsection = &raw[first_dot + 1..last_dot];
218 Ok(format!(
219 "{}.{}.{}",
220 section.to_lowercase(),
221 subsection,
222 name.to_lowercase()
223 ))
224 }
225}
226
227#[must_use]
231pub fn config_file_display_for_error(path: &Path) -> String {
232 config_error_path_display(path)
233}
234
235fn config_error_path_display(path: &Path) -> String {
236 if path.file_name().and_then(|s| s.to_str()) == Some("config")
237 && path
238 .parent()
239 .and_then(|p| p.file_name())
240 .and_then(|s| s.to_str())
241 == Some(".git")
242 {
243 return ".git/config".to_owned();
244 }
245 path.display().to_string()
246}
247
248struct Parser {
250 section: String,
251 subsection: Option<String>,
252}
253
254impl Parser {
255 fn new() -> Self {
256 Self {
257 section: String::new(),
258 subsection: None,
259 }
260 }
261
262 fn make_key(&self, name: &str) -> String {
264 let sec = self.section.to_lowercase();
265 let var = name.to_lowercase();
266 match &self.subsection {
267 Some(sub) => format!("{sec}.{sub}.{var}"),
268 None => format!("{sec}.{var}"),
269 }
270 }
271
272 fn try_parse_section_with_remainder<'a>(
278 &mut self,
279 line: &'a str,
280 inline_remainder: &mut Option<&'a str>,
281 ) -> bool {
282 let trimmed = line.trim();
283 if !trimmed.starts_with('[') {
284 return false;
285 }
286 let end = {
290 let bytes = trimmed.as_bytes();
291 let mut i = 1; let mut in_quotes = false;
293 let mut found = None;
294 while i < bytes.len() {
295 if in_quotes {
296 if bytes[i] == b'\\' {
297 i += 2; continue;
299 }
300 if bytes[i] == b'"' {
301 in_quotes = false;
302 }
303 } else {
304 if bytes[i] == b'"' {
305 in_quotes = true;
306 }
307 if bytes[i] == b']' {
308 found = Some(i);
309 break;
310 }
311 }
312 i += 1;
313 }
314 match found {
315 Some(i) => i,
316 None => return false,
317 }
318 };
319 let inside = &trimmed[1..end];
320 if let Some(quote_start) = inside.find('"') {
322 self.section = inside[..quote_start].trim().to_owned();
323 let rest = &inside[quote_start + 1..];
324 let mut sub = String::new();
326 let mut chars = rest.chars();
327 while let Some(ch) = chars.next() {
328 if ch == '\\' {
329 if let Some(escaped) = chars.next() {
330 sub.push(escaped);
331 }
332 } else if ch == '"' {
333 break;
334 } else {
335 sub.push(ch);
336 }
337 }
338 self.subsection = Some(sub);
339 } else {
340 self.section = inside.trim().to_owned();
341 self.subsection = None;
342 }
343 let after = trimmed[end + 1..].trim();
345 if !after.is_empty() && !after.starts_with('#') && !after.starts_with(';') {
346 *inline_remainder = Some(after);
347 } else {
348 *inline_remainder = None;
349 }
350 true
351 }
352
353 fn try_parse_section(&mut self, line: &str) -> bool {
355 let mut _remainder = None;
356 self.try_parse_section_with_remainder(line, &mut _remainder)
357 }
358
359 fn try_parse_entry(&self, line: &str) -> Option<(String, Option<String>)> {
363 let trimmed = line.trim();
364 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
365 return None;
366 }
367 if trimmed.starts_with('[') {
368 return None;
369 }
370 if self.section.is_empty() {
371 return None;
372 }
373
374 if let Some(eq_pos) = trimmed.find('=') {
375 let raw_name = trimmed[..eq_pos].trim();
376 let raw_value = trimmed[eq_pos + 1..].trim();
377 let value = strip_inline_comment(raw_value);
379 let value = unescape_value(&value);
380 let key = self.make_key(raw_name);
381 Some((key, Some(value)))
382 } else {
383 let raw_name = strip_inline_comment(trimmed);
385 let key = self.make_key(raw_name.trim());
386 Some((key, None))
387 }
388 }
389}
390
391fn entry_line_value_has_unclosed_quote(line: &str) -> bool {
401 let trimmed = line.trim();
402 let Some(eq_pos) = trimmed.find('=') else {
403 return false;
404 };
405 let raw_value = trimmed[eq_pos + 1..].trim_start();
406 let mut in_quote = false;
407 let mut last_was_backslash = false;
408 for ch in raw_value.chars() {
409 match ch {
410 '"' if !last_was_backslash => {
411 in_quote = !in_quote;
412 last_was_backslash = false;
413 }
414 '\\' if in_quote && !last_was_backslash => {
415 last_was_backslash = true;
416 continue;
417 }
418 '#' | ';' if !in_quote && !last_was_backslash => return false,
419 _ => {
420 last_was_backslash = false;
421 }
422 }
423 }
424 in_quote
425}
426
427fn value_line_continues(line: &str) -> bool {
428 let trimmed = line.trim();
429 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
430 return false;
431 }
432 let value_part = match trimmed.find('=') {
435 Some(pos) => &trimmed[pos + 1..],
436 None => return false,
437 };
438 let mut in_quote = false;
440 let mut last_was_backslash = false;
441 let mut in_comment = false;
442 for ch in value_part.chars() {
443 if in_comment {
444 last_was_backslash = false;
446 continue;
447 }
448 match ch {
449 '"' if !last_was_backslash => {
450 in_quote = !in_quote;
451 last_was_backslash = false;
452 }
453 '\\' if !last_was_backslash => {
454 last_was_backslash = true;
455 continue;
456 }
457 '#' | ';' if !in_quote && !last_was_backslash => {
458 in_comment = true;
459 last_was_backslash = false;
460 }
461 _ => {
462 last_was_backslash = false;
463 }
464 }
465 }
466 last_was_backslash && !in_comment
468}
469
470fn strip_inline_comment(s: &str) -> String {
472 let mut in_quote = false;
473 let mut result = String::with_capacity(s.len());
474 let mut chars = s.chars().peekable();
475 while let Some(ch) = chars.next() {
476 match ch {
477 '"' => {
478 in_quote = !in_quote;
479 result.push(ch);
480 }
481 '\\' if in_quote => {
482 result.push(ch);
483 if let Some(&next) = chars.peek() {
484 result.push(next);
485 chars.next();
486 }
487 }
488 '#' | ';' if !in_quote => break,
489 _ => result.push(ch),
490 }
491 }
492 let trimmed = result.trim_end();
494 trimmed.to_owned()
495}
496
497fn unescape_value(s: &str) -> String {
500 let mut result = String::with_capacity(s.len());
501 let mut chars = s.chars();
502 while let Some(ch) = chars.next() {
503 match ch {
504 '"' => { }
505 '\\' => match chars.next() {
506 Some('n') => result.push('\n'),
507 Some('t') => result.push('\t'),
508 Some('\\') => result.push('\\'),
509 Some('"') => result.push('"'),
510 Some(other) => {
511 result.push('\\');
512 result.push(other);
513 }
514 None => result.push('\\'),
515 },
516 _ => result.push(ch),
517 }
518 }
519 result
520}
521
522fn escape_subsection(s: &str) -> String {
529 let mut out = String::with_capacity(s.len());
530 for ch in s.chars() {
531 match ch {
532 '"' => out.push_str("\\\""),
533 '\\' => out.push_str("\\\\"),
534 other => out.push(other),
535 }
536 }
537 out
538}
539
540fn escape_value(s: &str) -> String {
541 let needs_quoting = s.starts_with('-')
544 || s.starts_with(' ')
545 || s.starts_with('\t')
546 || s.ends_with(' ')
547 || s.ends_with('\t')
548 || s.contains('"')
549 || s.contains('\\')
550 || s.contains('\n')
551 || s.contains('#')
552 || s.contains(';');
553
554 if !needs_quoting {
555 return s.to_owned();
556 }
557
558 let mut out = String::with_capacity(s.len() + 4);
559 out.push('"');
560 for ch in s.chars() {
561 match ch {
562 '"' => out.push_str("\\\""),
563 '\\' => out.push_str("\\\\"),
564 '\n' => out.push_str("\\n"),
565 '\t' => out.push_str("\\t"),
566 other => out.push(other),
567 }
568 }
569 out.push('"');
570 out
571}
572
573fn format_comment_suffix(comment: Option<&str>) -> String {
580 match comment {
581 None => String::new(),
582 Some(c) => {
583 if c.starts_with(' ') || c.starts_with('\t') {
584 c.to_owned()
586 } else if c.starts_with('#') {
587 format!(" {c}")
589 } else {
590 format!(" # {c}")
592 }
593 }
594 }
595}
596
597impl ConfigFile {
598 pub fn parse(path: &Path, content: &str, scope: ConfigScope) -> Result<Self> {
610 let raw_lines: Vec<String> = content
611 .lines()
612 .map(|l| l.strip_suffix('\r').unwrap_or(l))
613 .map(String::from)
614 .collect();
615 let mut entries = Vec::new();
616 let mut parser = Parser::new();
617
618 let mut idx = 0;
619 while idx < raw_lines.len() {
620 let start_idx = idx;
621 let line = &raw_lines[idx];
622 idx += 1;
623
624 let trimmed = line.trim();
626 if trimmed.starts_with('#') || trimmed.starts_with(';') {
627 continue;
628 }
629
630 let mut inline_remainder = None;
631 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
632 if let Some(remainder) = inline_remainder {
634 if let Some((key, value)) = parser.try_parse_entry(remainder) {
635 if key == "fetch.negotiationalgorithm" && value.is_none() {
636 let file_disp = config_error_path_display(path);
637 return Err(Error::Message(format!(
638 "error: missing value for 'fetch.negotiationalgorithm'\n\
639fatal: bad config variable 'fetch.negotiationalgorithm' in file '{file_disp}' at line {}",
640 start_idx + 1
641 )));
642 }
643 entries.push(ConfigEntry {
644 key,
645 value,
646 scope,
647 file: Some(path.to_path_buf()),
648 line: start_idx + 1,
649 });
650 }
651 }
652 continue;
653 }
654
655 let mut logical_line = line.clone();
658 while value_line_continues(&logical_line) && idx < raw_lines.len() {
659 let t = logical_line.trim_end();
661 logical_line = t[..t.len() - 1].to_string();
662 let next = raw_lines[idx].trim_start();
664 logical_line.push_str(next);
665 idx += 1;
666 }
667
668 while entry_line_value_has_unclosed_quote(&logical_line) && idx < raw_lines.len() {
669 let next = raw_lines[idx].trim_start();
670 logical_line.push_str(next);
671 idx += 1;
672 }
673 if entry_line_value_has_unclosed_quote(&logical_line) {
674 let file_disp = config_error_path_display(path);
675 return Err(Error::ConfigError(format!(
676 "bad config line {} in file '{file_disp}'",
677 start_idx + 1
678 )));
679 }
680
681 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
682 if key == "fetch.negotiationalgorithm" && value.is_none() {
683 let file_disp = config_error_path_display(path);
684 return Err(Error::Message(format!(
685 "error: missing value for 'fetch.negotiationalgorithm'\n\
686fatal: bad config variable 'fetch.negotiationalgorithm' in file '{file_disp}' at line {}",
687 start_idx + 1
688 )));
689 }
690 entries.push(ConfigEntry {
691 key,
692 value,
693 scope,
694 file: Some(path.to_path_buf()),
695 line: start_idx + 1,
696 });
697 } else if logical_line.trim().is_empty() {
698 continue;
699 } else {
700 let file_disp = config_error_path_display(path);
701 return Err(Error::Message(format!(
702 "fatal: bad config line {} in file {file_disp}",
703 start_idx + 1
704 )));
705 }
706 }
707
708 Ok(Self {
709 path: path.to_path_buf(),
710 scope,
711 entries,
712 raw_lines,
713 include_origin: ConfigIncludeOrigin::Disk,
714 })
715 }
716
717 pub fn parse_gitmodules_best_effort(
723 path: &Path,
724 content: &str,
725 scope: ConfigScope,
726 ) -> (Vec<ConfigEntry>, Option<usize>) {
727 let raw_lines: Vec<String> = content
728 .lines()
729 .map(|l| l.strip_suffix('\r').unwrap_or(l))
730 .map(String::from)
731 .collect();
732 let mut entries = Vec::new();
733 let mut parser = Parser::new();
734
735 let mut idx = 0;
736 while idx < raw_lines.len() {
737 let start_idx = idx;
738 let line = &raw_lines[idx];
739 idx += 1;
740
741 let trimmed = line.trim();
742 if trimmed.starts_with('#') || trimmed.starts_with(';') {
743 continue;
744 }
745
746 let mut inline_remainder = None;
747 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
748 if let Some(remainder) = inline_remainder {
749 if let Some((key, value)) = parser.try_parse_entry(remainder) {
750 entries.push(ConfigEntry {
751 key,
752 value,
753 scope,
754 file: Some(path.to_path_buf()),
755 line: start_idx + 1,
756 });
757 }
758 }
759 continue;
760 }
761
762 let mut logical_line = line.clone();
763 while value_line_continues(&logical_line) && idx < raw_lines.len() {
764 let t = logical_line.trim_end();
765 logical_line = t[..t.len() - 1].to_string();
766 let next = raw_lines[idx].trim_start();
767 logical_line.push_str(next);
768 idx += 1;
769 }
770
771 while entry_line_value_has_unclosed_quote(&logical_line) && idx < raw_lines.len() {
772 let next = raw_lines[idx].trim_start();
773 logical_line.push_str(next);
774 idx += 1;
775 }
776 if entry_line_value_has_unclosed_quote(&logical_line) {
777 return (entries, Some(start_idx + 1));
778 }
779
780 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
781 entries.push(ConfigEntry {
782 key,
783 value,
784 scope,
785 file: Some(path.to_path_buf()),
786 line: start_idx + 1,
787 });
788 }
789 }
790
791 (entries, None)
792 }
793
794 #[must_use]
796 pub fn get(&self, key: &str) -> Option<String> {
797 let canon = canonical_key(key).ok()?;
798 self.entries
799 .iter()
800 .rev()
801 .find(|e| e.key == canon)
802 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
803 }
804
805 pub fn parse_with_origin(
807 path: &Path,
808 content: &str,
809 scope: ConfigScope,
810 include_origin: ConfigIncludeOrigin,
811 ) -> Result<Self> {
812 let mut f = Self::parse(path, content, scope)?;
813 f.include_origin = include_origin;
814 Ok(f)
815 }
816
817 pub fn from_git_config_parameters(path: &Path, raw: &str) -> Result<Self> {
822 let mut entries = Vec::new();
823 let pseudo_path = path.to_path_buf();
824 for entry in parse_config_parameters(raw) {
825 if let Some((key, val)) = entry.split_once('=') {
826 let canon = canonical_key(key.trim())?;
827 entries.push(ConfigEntry {
828 key: canon,
829 value: Some(val.to_owned()),
830 scope: ConfigScope::Command,
831 file: Some(pseudo_path.clone()),
832 line: 0,
833 });
834 } else {
835 let canon = canonical_key(entry.trim())?;
836 entries.push(ConfigEntry {
837 key: canon,
838 value: None,
839 scope: ConfigScope::Command,
840 file: Some(pseudo_path.clone()),
841 line: 0,
842 });
843 }
844 }
845 Ok(Self {
846 path: path.to_path_buf(),
847 scope: ConfigScope::Command,
848 entries,
849 raw_lines: Vec::new(),
850 include_origin: ConfigIncludeOrigin::CommandLine,
851 })
852 }
853
854 pub fn from_path(path: &Path, scope: ConfigScope) -> Result<Option<Self>> {
863 match fs::read_to_string(path) {
864 Ok(content) => Ok(Some(Self::parse(path, &content, scope)?)),
865 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
866 Err(e) => Err(Error::Io(e)),
867 }
868 }
869
870 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
881 self.set_with_comment(key, value, None)
882 }
883
884 pub fn set_with_comment(
886 &mut self,
887 key: &str,
888 value: &str,
889 comment: Option<&str>,
890 ) -> Result<()> {
891 let canon = canonical_key(key)?;
892 let raw_var = raw_variable_name(key);
893 let comment_suffix = format_comment_suffix(comment);
894
895 let existing_idx = self.entries.iter().rposition(|e| e.key == canon);
897
898 if let Some(idx) = existing_idx {
899 let line_idx = self.entries[idx].line - 1;
900 let raw_line = &self.raw_lines[line_idx];
901 if is_section_header_with_inline_entry(raw_line) {
902 let header_only = extract_section_header(raw_line);
904 self.raw_lines[line_idx] = header_only;
905 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
906 self.raw_lines.insert(line_idx + 1, new_line);
907 let content = self.raw_lines.join("\n");
909 let reparsed = Self::parse(&self.path, &content, self.scope)?;
910 self.entries = reparsed.entries;
911 self.raw_lines = reparsed.raw_lines;
912 } else {
913 self.raw_lines[line_idx] =
914 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
915 self.entries[idx].value = Some(value.to_owned());
916 }
917 } else {
918 let (section, subsection, _var) = split_key(&canon)?;
920 let (raw_sec, raw_sub) = raw_section_parts(key);
921 let section_line = self.find_or_create_section_preserving_case(
922 §ion,
923 subsection.as_deref(),
924 &raw_sec,
925 raw_sub.as_deref(),
926 );
927 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
928
929 let insert_at = self.last_line_in_section(section_line) + 1;
931 self.raw_lines.insert(insert_at, new_line);
932
933 let content = self.raw_lines.join("\n");
935 let reparsed = Self::parse(&self.path, &content, self.scope)?;
936 self.entries = reparsed.entries;
937 self.raw_lines = reparsed.raw_lines;
938 }
939
940 Ok(())
941 }
942
943 pub fn replace_all(
948 &mut self,
949 key: &str,
950 value: &str,
951 value_pattern: Option<&str>,
952 ) -> Result<()> {
953 self.replace_all_with_comment(key, value, value_pattern, None)
954 }
955
956 pub fn replace_all_with_comment(
961 &mut self,
962 key: &str,
963 value: &str,
964 value_pattern: Option<&str>,
965 comment: Option<&str>,
966 ) -> Result<()> {
967 let canon = canonical_key(key)?;
968 let comment_suffix = format_comment_suffix(comment);
969
970 let (re, negated) = match value_pattern {
972 Some(pat) => {
973 let (neg, actual_pat) = if let Some(rest) = pat.strip_prefix('!') {
974 (true, rest)
975 } else {
976 (false, pat)
977 };
978 let compiled = regex::Regex::new(actual_pat)
979 .map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?;
980 (Some(compiled), neg)
981 }
982 None => (None, false),
983 };
984
985 let matching_indices: Vec<usize> = self
987 .entries
988 .iter()
989 .enumerate()
990 .filter(|(_, e)| {
991 if e.key != canon {
992 return false;
993 }
994 if let Some(ref re) = re {
995 let v = e.value.as_deref().unwrap_or("");
996 let matched = re.is_match(v);
997 if negated {
998 !matched
999 } else {
1000 matched
1001 }
1002 } else {
1003 true
1004 }
1005 })
1006 .map(|(i, _)| i)
1007 .collect();
1008
1009 if matching_indices.is_empty() {
1010 return self.add_value_with_comment(key, value, comment);
1012 }
1013
1014 let raw_var = raw_variable_name(key);
1015
1016 if matching_indices.len() == 1 {
1017 let match_idx = matching_indices[0];
1019 let line_idx = self.entries[match_idx].line - 1;
1020 let raw_line = &self.raw_lines[line_idx];
1021 if is_section_header_with_inline_entry(raw_line) {
1022 let header = extract_section_header(raw_line);
1023 self.raw_lines[line_idx] = header;
1024 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1025 self.raw_lines.insert(line_idx + 1, new_line);
1026 } else {
1027 self.raw_lines[line_idx] =
1028 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1029 }
1030 } else {
1031 for &idx in matching_indices.iter().rev() {
1033 let line_idx = self.entries[idx].line - 1;
1034 self.remove_entry_line(line_idx);
1035 }
1036
1037 let content = self.raw_lines.join("\n");
1039 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1040 self.entries = reparsed.entries;
1041 self.raw_lines = reparsed.raw_lines;
1042
1043 let (section, subsection, _var) = split_key(&canon)?;
1045 let (raw_sec, raw_sub) = raw_section_parts(key);
1046 let section_line = self.find_or_create_section_preserving_case(
1047 §ion,
1048 subsection.as_deref(),
1049 &raw_sec,
1050 raw_sub.as_deref(),
1051 );
1052 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1053 let insert_at = self.last_line_in_section(section_line) + 1;
1054 self.raw_lines.insert(insert_at, new_line);
1055 }
1056
1057 let content = self.raw_lines.join("\n");
1059 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1060 self.entries = reparsed.entries;
1061 self.raw_lines = reparsed.raw_lines;
1062
1063 Ok(())
1064 }
1065
1066 pub fn count(&self, key: &str) -> Result<usize> {
1068 let canon = canonical_key(key)?;
1069 Ok(self.entries.iter().filter(|e| e.key == canon).count())
1070 }
1071
1072 fn remove_entry_line(&mut self, line_idx: usize) {
1083 if is_section_header_with_inline_entry(&self.raw_lines[line_idx]) {
1084 let header = extract_section_header(&self.raw_lines[line_idx]);
1086 self.raw_lines[line_idx] = header;
1087 } else {
1088 let mut lines_to_remove = 1;
1090 let mut check_line = self.raw_lines[line_idx].clone();
1091 while value_line_continues(&check_line)
1092 && (line_idx + lines_to_remove) < self.raw_lines.len()
1093 {
1094 check_line = self.raw_lines[line_idx + lines_to_remove].clone();
1095 lines_to_remove += 1;
1096 }
1097 for _ in 0..lines_to_remove {
1098 self.raw_lines.remove(line_idx);
1099 }
1100 }
1101 }
1102
1103 pub fn unset_last(&mut self, key: &str) -> Result<usize> {
1107 let canon = canonical_key(key)?;
1108 let last_idx = self.entries.iter().rposition(|e| e.key == canon);
1109
1110 if let Some(idx) = last_idx {
1111 let line_idx = self.entries[idx].line - 1;
1112 self.remove_entry_line(line_idx);
1113 let content = self.raw_lines.join("\n");
1114 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1115 self.entries = reparsed.entries;
1116 self.raw_lines = reparsed.raw_lines;
1117 Ok(1)
1118 } else {
1119 Ok(0)
1120 }
1121 }
1122
1123 pub fn unset(&mut self, key: &str) -> Result<usize> {
1133 let canon = canonical_key(key)?;
1134 let line_indices: Vec<usize> = self
1135 .entries
1136 .iter()
1137 .filter(|e| e.key == canon)
1138 .map(|e| e.line - 1)
1139 .collect();
1140
1141 let count = line_indices.len();
1142 for &idx in line_indices.iter().rev() {
1144 self.remove_entry_line(idx);
1145 }
1146
1147 if count > 0 {
1148 let content = self.raw_lines.join("\n");
1149 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1150 self.entries = reparsed.entries;
1151 self.raw_lines = reparsed.raw_lines;
1152 }
1153
1154 Ok(count)
1155 }
1156
1157 pub fn unset_matching(
1166 &mut self,
1167 key: &str,
1168 value_pattern: Option<&str>,
1169 preserve_empty_section_header: bool,
1170 ) -> Result<usize> {
1171 let canon = canonical_key(key)?;
1172 let re = match value_pattern {
1173 Some(pat) => Some(
1174 regex::Regex::new(pat)
1175 .map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?,
1176 ),
1177 None => None,
1178 };
1179
1180 let line_indices: Vec<usize> = self
1181 .entries
1182 .iter()
1183 .filter(|e| {
1184 if e.key != canon {
1185 return false;
1186 }
1187 if let Some(ref re) = re {
1188 let v = e.value.as_deref().unwrap_or("");
1189 re.is_match(v)
1190 } else {
1191 true
1192 }
1193 })
1194 .map(|e| e.line - 1)
1195 .collect();
1196
1197 let count = line_indices.len();
1198 for &idx in line_indices.iter().rev() {
1199 self.remove_entry_line(idx);
1200 }
1201
1202 if count > 0 {
1203 if !preserve_empty_section_header {
1204 self.remove_empty_section_headers();
1206 }
1207
1208 let content = self.raw_lines.join("\n");
1209 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1210 self.entries = reparsed.entries;
1211 self.raw_lines = reparsed.raw_lines;
1212 }
1213
1214 Ok(count)
1215 }
1216
1217 pub fn remove_section(&mut self, section: &str) -> Result<bool> {
1223 let (sec_name, sub_name) = parse_section_name(section);
1224 let sec_lower = sec_name.to_lowercase();
1225
1226 let mut start = None;
1228 let mut end = 0;
1229 let mut parser = Parser::new();
1230
1231 for (idx, line) in self.raw_lines.iter().enumerate() {
1232 if parser.try_parse_section(line) {
1233 if parser.section.to_lowercase() == sec_lower
1234 && parser.subsection.as_deref() == sub_name
1235 {
1236 start = Some(idx);
1237 end = idx;
1238 } else if start.is_some() {
1239 break;
1240 }
1241 } else if start.is_some() {
1242 end = idx;
1243 }
1244 }
1245
1246 if let Some(s) = start {
1247 self.raw_lines.drain(s..=end);
1248 let content = self.raw_lines.join("\n");
1249 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1250 self.entries = reparsed.entries;
1251 self.raw_lines = reparsed.raw_lines;
1252 Ok(true)
1253 } else {
1254 Ok(false)
1255 }
1256 }
1257
1258 pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
1265 let (old_sec, old_sub) = parse_section_name(old_name);
1266 let (new_sec, new_sub) = parse_section_name(new_name);
1267 let old_lower = old_sec.to_lowercase();
1268
1269 let mut found = false;
1270 let mut parser = Parser::new();
1271
1272 for idx in 0..self.raw_lines.len() {
1273 let line = &self.raw_lines[idx];
1274 if parser.try_parse_section(line)
1275 && parser.section.to_lowercase() == old_lower
1276 && parser.subsection.as_deref() == old_sub
1277 {
1278 let header = match new_sub {
1280 Some(sub) => format!("[{} \"{}\"]", new_sec, sub),
1281 None => format!("[{}]", new_sec),
1282 };
1283 self.raw_lines[idx] = header;
1284 found = true;
1285 }
1286 }
1287
1288 if found {
1289 let content = self.raw_lines.join("\n");
1290 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1291 self.entries = reparsed.entries;
1292 self.raw_lines = reparsed.raw_lines;
1293 }
1294
1295 Ok(found)
1296 }
1297
1298 pub fn add_value(&mut self, key: &str, value: &str) -> Result<()> {
1303 self.add_value_with_comment(key, value, None)
1304 }
1305
1306 pub fn add_value_with_comment(
1308 &mut self,
1309 key: &str,
1310 value: &str,
1311 comment: Option<&str>,
1312 ) -> Result<()> {
1313 let canon = canonical_key(key)?;
1314 let raw_var = raw_variable_name(key);
1315 let comment_suffix = format_comment_suffix(comment);
1316 let (section, subsection, _var) = split_key(&canon)?;
1317 let (raw_sec, raw_sub) = raw_section_parts(key);
1318
1319 let section_line = self.find_or_create_section_preserving_case(
1320 §ion,
1321 subsection.as_deref(),
1322 &raw_sec,
1323 raw_sub.as_deref(),
1324 );
1325 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1326 let insert_at = self.last_line_in_section(section_line) + 1;
1327 self.raw_lines.insert(insert_at, new_line);
1328
1329 let content = self.raw_lines.join("\n");
1331 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1332 self.entries = reparsed.entries;
1333 self.raw_lines = reparsed.raw_lines;
1334
1335 Ok(())
1336 }
1337
1338 fn remove_empty_section_headers(&mut self) {
1341 let section_re = regex::Regex::new(r"^\s*\[").unwrap();
1342 let comment_re = regex::Regex::new(r"^\s*(#|;)").unwrap();
1343
1344 let mut to_remove: Vec<usize> = Vec::new();
1345 let len = self.raw_lines.len();
1346
1347 for i in 0..len {
1348 let line = &self.raw_lines[i];
1349 if !section_re.is_match(line) {
1350 continue;
1351 }
1352 if is_section_header_with_inline_entry(line) {
1354 continue;
1355 }
1356 let mut has_entries = false;
1359 for j in (i + 1)..len {
1360 let next = self.raw_lines[j].trim();
1361 if next.is_empty() {
1362 continue;
1363 }
1364 if section_re.is_match(&self.raw_lines[j]) {
1365 break;
1366 }
1367 if comment_re.is_match(&self.raw_lines[j]) {
1368 has_entries = true;
1370 break;
1371 }
1372 has_entries = true;
1374 break;
1375 }
1376 if !has_entries {
1377 to_remove.push(i);
1378 }
1379 }
1380
1381 for &idx in to_remove.iter().rev() {
1383 self.raw_lines.remove(idx);
1384 }
1385
1386 while self.raw_lines.last().is_some_and(|l| l.trim().is_empty()) {
1388 self.raw_lines.pop();
1389 }
1390 }
1391
1392 pub fn write(&self) -> Result<()> {
1397 let content = self.raw_lines.join("\n");
1398 let trimmed = content.trim();
1399 if trimmed.is_empty() {
1400 fs::write(&self.path, "")?;
1402 } else {
1403 let content = if content.ends_with('\n') {
1405 content
1406 } else {
1407 format!("{content}\n")
1408 };
1409 fs::write(&self.path, content)?;
1410 }
1411 Ok(())
1412 }
1413
1414 #[allow(dead_code)]
1416 fn find_or_create_section(&mut self, section: &str, subsection: Option<&str>) -> usize {
1417 let sec_lower = section.to_lowercase();
1418 let mut parser = Parser::new();
1419
1420 for (idx, line) in self.raw_lines.iter().enumerate() {
1421 if parser.try_parse_section(line)
1422 && parser.section.to_lowercase() == sec_lower
1423 && parser.subsection.as_deref() == subsection
1424 {
1425 return idx;
1426 }
1427 }
1428
1429 let header = match subsection {
1431 Some(sub) => {
1432 let escaped = escape_subsection(sub);
1433 format!("[{} \"{}\"]", section, escaped)
1434 }
1435 None => format!("[{}]", section),
1436 };
1437 self.raw_lines.push(header);
1438 self.raw_lines.len() - 1
1439 }
1440
1441 fn find_or_create_section_preserving_case(
1444 &mut self,
1445 section: &str,
1446 subsection: Option<&str>,
1447 raw_section: &str,
1448 raw_subsection: Option<&str>,
1449 ) -> usize {
1450 let sec_lower = section.to_lowercase();
1451 let mut parser = Parser::new();
1452
1453 for (idx, line) in self.raw_lines.iter().enumerate() {
1454 if parser.try_parse_section(line)
1455 && parser.section.to_lowercase() == sec_lower
1456 && parser.subsection.as_deref() == subsection
1457 {
1458 return idx;
1459 }
1460 }
1461
1462 let header = match raw_subsection {
1464 Some(sub) => {
1465 let escaped = escape_subsection(sub);
1466 format!("[{} \"{}\"]", raw_section, escaped)
1467 }
1468 None => format!("[{}]", raw_section),
1469 };
1470 self.raw_lines.push(header);
1471 self.raw_lines.len() - 1
1472 }
1473
1474 fn last_line_in_section(&self, section_line: usize) -> usize {
1476 let mut last = section_line;
1477 for idx in (section_line + 1)..self.raw_lines.len() {
1478 let trimmed = self.raw_lines[idx].trim();
1479 if trimmed.starts_with('[') {
1480 break;
1481 }
1482 last = idx;
1483 }
1484 last
1485 }
1486}
1487
1488impl ConfigSet {
1491 #[must_use]
1493 pub fn new() -> Self {
1494 Self {
1495 entries: Vec::new(),
1496 }
1497 }
1498
1499 #[must_use]
1501 pub fn entries(&self) -> &[ConfigEntry] {
1502 &self.entries
1503 }
1504
1505 pub fn merge(&mut self, file: &ConfigFile) {
1510 self.entries.extend(file.entries.iter().cloned());
1511 }
1512
1513 pub fn merge_set(&mut self, other: &ConfigSet) {
1515 self.entries.extend(other.entries.iter().cloned());
1516 }
1517
1518 pub fn add_command_override(&mut self, key: &str, value: &str) -> Result<()> {
1520 let canon = canonical_key(key)?;
1521 self.entries.push(ConfigEntry {
1522 key: canon,
1523 value: Some(value.to_owned()),
1524 scope: ConfigScope::Command,
1525 file: None,
1526 line: 0,
1527 });
1528 Ok(())
1529 }
1530
1531 #[must_use]
1542 pub fn get(&self, key: &str) -> Option<String> {
1543 let canon = canonical_key(key).ok()?;
1544 self.entries
1545 .iter()
1546 .rev()
1547 .find(|e| e.key == canon)
1548 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
1549 }
1550
1551 #[must_use]
1556 pub fn get_last_entry(&self, key: &str) -> Option<ConfigEntry> {
1557 let canon = canonical_key(key).ok()?;
1558 self.entries.iter().rev().find(|e| e.key == canon).cloned()
1559 }
1560
1561 #[must_use]
1563 pub fn get_all(&self, key: &str) -> Vec<String> {
1564 let canon = match canonical_key(key) {
1565 Ok(c) => c,
1566 Err(_) => return Vec::new(),
1567 };
1568 self.entries
1569 .iter()
1570 .filter(|e| e.key == canon)
1571 .map(|e| e.value.clone().unwrap_or_default())
1572 .collect()
1573 }
1574
1575 #[must_use]
1579 pub fn get_all_raw(&self, key: &str) -> Vec<Option<String>> {
1580 let canon = match canonical_key(key) {
1581 Ok(c) => c,
1582 Err(_) => return Vec::new(),
1583 };
1584 self.entries
1585 .iter()
1586 .filter(|e| e.key == canon)
1587 .map(|e| e.value.clone())
1588 .collect()
1589 }
1590
1591 #[must_use]
1596 pub fn has_key(&self, key: &str) -> bool {
1597 let Ok(canon) = canonical_key(key) else {
1598 return false;
1599 };
1600 self.entries.iter().any(|e| e.key == canon)
1601 }
1602
1603 pub fn get_bool(&self, key: &str) -> Option<std::result::Result<bool, String>> {
1609 let v = self.get(key)?;
1610 if canonical_key(key).ok().as_deref() == Some("pack.allowpackreuse") {
1611 let lower = v.trim().to_ascii_lowercase();
1612 if lower == "single" || lower == "multi" {
1613 return None;
1614 }
1615 }
1616 Some(parse_bool(&v))
1617 }
1618
1619 #[must_use]
1625 pub fn quote_path_fully(&self) -> bool {
1626 let from_key = |key: &str| self.get_bool(key).and_then(|r| r.ok());
1627 from_key("core.quotepath")
1628 .or_else(|| from_key("core.quotePath"))
1629 .unwrap_or(true)
1630 }
1631
1632 #[must_use]
1636 pub fn pack_write_reverse_index_default(&self) -> bool {
1637 if std::env::var("GIT_TEST_NO_WRITE_REV_INDEX")
1638 .ok()
1639 .as_deref()
1640 .is_some_and(|v| {
1641 let s = v.trim().to_ascii_lowercase();
1642 matches!(s.as_str(), "1" | "true" | "yes" | "on")
1643 })
1644 {
1645 return false;
1646 }
1647 self.get_bool("pack.writereverseindex")
1648 .or_else(|| self.get_bool("pack.writeReverseIndex"))
1649 .and_then(|r| r.ok())
1650 .unwrap_or(true)
1651 }
1652
1653 #[must_use]
1655 pub fn pack_read_reverse_index_default(&self) -> bool {
1656 self.get_bool("pack.readreverseindex")
1657 .or_else(|| self.get_bool("pack.readReverseIndex"))
1658 .and_then(|r| r.ok())
1659 .unwrap_or(true)
1660 }
1661
1662 #[must_use]
1665 pub fn effective_log_refs_config(&self, git_dir: &Path) -> refs::LogRefsConfig {
1666 if let Some(v) = self.get("core.logAllRefUpdates") {
1667 let lower = v.trim().to_ascii_lowercase();
1668 let parsed = match lower.as_str() {
1669 "always" => Some(refs::LogRefsConfig::Always),
1670 "1" | "true" | "yes" | "on" => Some(refs::LogRefsConfig::Normal),
1671 "0" | "false" | "no" | "off" | "never" => Some(refs::LogRefsConfig::None),
1672 _ => None,
1673 };
1674 if let Some(c) = parsed {
1675 return c;
1676 }
1677 }
1678 refs::effective_log_refs_config(git_dir)
1679 }
1680
1681 pub fn get_i64(&self, key: &str) -> Option<std::result::Result<i64, String>> {
1683 self.get(key).map(|v| parse_i64(&v))
1684 }
1685
1686 pub fn pack_objects_zlib_level(&self) -> Result<i32> {
1694 const Z_DEFAULT_COMPRESSION: i32 = 6;
1695 const Z_BEST_COMPRESSION: i32 = 9;
1696
1697 let parse_compression = |raw: &str| -> Result<i32> {
1698 let v = parse_git_config_int_strict(raw.trim()).map_err(|_| {
1699 Error::ConfigError(format!("bad numeric config value '{raw}' for compression"))
1700 })?;
1701 if v == -1 {
1702 return Ok(Z_DEFAULT_COMPRESSION);
1703 }
1704 if v < 0 || v > i64::from(Z_BEST_COMPRESSION) {
1705 return Err(Error::ConfigError(format!(
1706 "bad zlib compression level {v}"
1707 )));
1708 }
1709 Ok(v as i32)
1710 };
1711
1712 let mut pack_level = Z_DEFAULT_COMPRESSION;
1714 let mut pack_compression_seen = false;
1715
1716 for e in self.entries() {
1717 match e.key.as_str() {
1718 "core.compression" => {
1719 let Some(val) = e.value.as_deref() else {
1720 continue;
1721 };
1722 let level = parse_compression(val)?;
1723 if !pack_compression_seen {
1724 pack_level = level;
1725 }
1726 }
1727 "pack.compression" => {
1728 let Some(val) = e.value.as_deref() else {
1729 continue;
1730 };
1731 pack_level = parse_compression(val)?;
1732 pack_compression_seen = true;
1733 }
1734 _ => {}
1735 }
1736 }
1737
1738 Ok(pack_level)
1739 }
1740
1741 pub fn get_regexp(&self, pattern: &str) -> std::result::Result<Vec<&ConfigEntry>, String> {
1746 let re = regex::Regex::new(pattern).map_err(|e| format!("invalid key pattern: {e}"))?;
1747 Ok(self
1748 .entries
1749 .iter()
1750 .filter(|e| re.is_match(&e.key))
1751 .collect())
1752 }
1753
1754 pub fn load(git_dir: Option<&Path>, include_system: bool) -> Result<Self> {
1765 let mut opts = LoadConfigOptions::default();
1766 opts.include_system = include_system;
1767 opts.include_ctx.git_dir = git_dir.map(PathBuf::from);
1768 Self::load_with_options(git_dir, &opts)
1769 }
1770
1771 pub fn load_with_options(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Result<Self> {
1775 let mut set = Self::new();
1776 let proc = opts.process_includes;
1777 let ctx = opts.include_ctx.clone();
1778
1779 if opts.include_system && std::env::var("GIT_CONFIG_NOSYSTEM").is_err() {
1781 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
1782 .map(std::path::PathBuf::from)
1783 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
1784 match ConfigFile::from_path(&system_path, ConfigScope::System) {
1785 Ok(Some(f)) => Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?,
1786 Ok(None) => {}
1787 Err(e) => return Err(e),
1788 }
1789 }
1790
1791 for path in global_config_paths() {
1793 match ConfigFile::from_path(&path, ConfigScope::Global) {
1794 Ok(Some(f)) => Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?,
1795 Ok(None) => {}
1796 Err(e) => return Err(e),
1797 }
1798 }
1799
1800 if let Some(gd) = git_dir {
1802 let local_path = gd.join("config");
1803 match ConfigFile::from_path(&local_path, ConfigScope::Local) {
1804 Ok(Some(f)) => Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?,
1805 Ok(None) => {}
1806 Err(e) => return Err(e),
1807 }
1808
1809 let common_dir = crate::repo::common_git_dir_for_config(gd);
1812 let wt_path = gd.join("config.worktree");
1813 if crate::repo::worktree_config_enabled(&common_dir) {
1814 match ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
1815 Ok(Some(f)) => Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?,
1816 Ok(None) => {}
1817 Err(e) => return Err(e),
1818 }
1819 }
1820 }
1821
1822 if let Ok(path) = std::env::var("GIT_CONFIG") {
1824 match ConfigFile::from_path(Path::new(&path), ConfigScope::Command) {
1825 Ok(Some(f)) => {
1826 if proc {
1827 Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?;
1828 } else {
1829 set.merge(&f);
1830 }
1831 }
1832 Ok(None) => {}
1833 Err(e) => return Err(e),
1834 }
1835 }
1836
1837 if let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") {
1839 if let Ok(count) = count_str.parse::<usize>() {
1840 for i in 0..count {
1841 let key_var = format!("GIT_CONFIG_KEY_{i}");
1842 let val_var = format!("GIT_CONFIG_VALUE_{i}");
1843 if let (Ok(key), Ok(val)) = (std::env::var(&key_var), std::env::var(&val_var)) {
1844 let _ = set.add_command_override(&key, &val);
1845 }
1846 }
1847 }
1848 }
1849
1850 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
1852 if proc && opts.command_includes && !params.trim().is_empty() {
1853 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
1854 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, ¶ms)?;
1855 Self::merge_with_includes(&mut set, &cmd_file, proc, 0, &ctx)?;
1856 } else if !params.trim().is_empty() {
1857 for entry in parse_config_parameters(¶ms) {
1858 if let Some((key, val)) = entry.split_once('=') {
1859 let _ = set.add_command_override(key.trim(), val);
1860 } else {
1861 let _ = set.add_command_override(entry.trim(), "true");
1862 }
1863 }
1864 }
1865 }
1866
1867 Ok(set)
1868 }
1869
1870 pub fn read_early_config(git_dir: Option<&Path>, key: &str) -> Result<Vec<String>> {
1882 let mut set = Self::new();
1883 let ctx = IncludeContext {
1884 git_dir: git_dir.map(PathBuf::from),
1885 command_line_relative_include_is_error: false,
1886 };
1887
1888 if std::env::var("GIT_CONFIG_NOSYSTEM").is_err() {
1890 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
1891 .map(std::path::PathBuf::from)
1892 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
1893 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
1894 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1895 }
1896 }
1897
1898 for path in global_config_paths() {
1900 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
1901 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1902 }
1903 }
1904
1905 if let Some(gd) = git_dir {
1906 let common_dir = crate::repo::common_git_dir_for_config(gd);
1907 let local_path = common_dir.join("config");
1909 if let Some(msg) = crate::repo::early_config_ignore_repo_reason(&common_dir) {
1910 eprintln!("warning: ignoring git dir '{}': {}", gd.display(), msg);
1911 } else if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
1912 set.merge_file_with_includes(&f, true, &ctx)?;
1913 }
1914
1915 let wt_path = gd.join("config.worktree");
1917 if crate::repo::worktree_config_enabled(&common_dir) {
1918 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
1919 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1920 }
1921 }
1922 }
1923
1924 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
1926 if !params.trim().is_empty() {
1927 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
1928 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, ¶ms)?;
1929 Self::merge_with_includes(&mut set, &cmd_file, true, 0, &ctx)?;
1930 }
1931 }
1932
1933 Ok(set.get_all(key))
1934 }
1935
1936 pub fn merge_file_with_includes(
1941 &mut self,
1942 file: &ConfigFile,
1943 process_includes: bool,
1944 ctx: &IncludeContext,
1945 ) -> Result<()> {
1946 Self::merge_with_includes(self, file, process_includes, 0, ctx)
1947 }
1948
1949 pub fn load_repo_local_only(git_dir: &Path) -> Result<Self> {
1955 let mut set = Self::new();
1956 let local_path = git_dir.join("config");
1957 let ctx = IncludeContext {
1958 git_dir: Some(git_dir.to_path_buf()),
1959 command_line_relative_include_is_error: false,
1960 };
1961 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
1962 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1963 }
1964 Ok(set)
1965 }
1966
1967 pub fn load_protected(include_system: bool) -> Result<Self> {
1978 let mut set = Self::new();
1979 let ctx = IncludeContext {
1980 git_dir: None,
1981 command_line_relative_include_is_error: false,
1982 };
1983
1984 if include_system && std::env::var("GIT_CONFIG_NOSYSTEM").is_err() {
1985 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
1986 .map(std::path::PathBuf::from)
1987 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
1988 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
1989 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1990 }
1991 }
1992
1993 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
1994 let path = PathBuf::from(p);
1995 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
1996 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1997 }
1998 } else {
1999 let mut global_paths = Vec::new();
2000 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
2001 global_paths.push(PathBuf::from(xdg).join("git/config"));
2002 } else if let Some(home) = home_dir() {
2003 global_paths.push(home.join(".config/git/config"));
2004 }
2005 if let Some(home) = home_dir() {
2006 global_paths.push(home.join(".gitconfig"));
2007 }
2008 for path in global_paths {
2009 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2010 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2011 }
2012 }
2013 }
2014
2015 if let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") {
2016 if let Ok(count) = count_str.parse::<usize>() {
2017 for i in 0..count {
2018 let key_var = format!("GIT_CONFIG_KEY_{i}");
2019 let val_var = format!("GIT_CONFIG_VALUE_{i}");
2020 if let (Ok(key), Ok(val)) = (std::env::var(&key_var), std::env::var(&val_var)) {
2021 let _ = set.add_command_override(&key, &val);
2022 }
2023 }
2024 }
2025 }
2026
2027 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
2028 for entry in parse_config_parameters(¶ms) {
2029 if let Some((key, val)) = entry.split_once('=') {
2030 let _ = set.add_command_override(key.trim(), val);
2031 } else {
2032 let _ = set.add_command_override(entry.trim(), "true");
2033 }
2034 }
2035 }
2036
2037 Ok(set)
2038 }
2039
2040 fn merge_with_includes(
2042 set: &mut Self,
2043 file: &ConfigFile,
2044 process_includes: bool,
2045 depth: usize,
2046 ctx: &IncludeContext,
2047 ) -> Result<()> {
2048 const MAX_INCLUDE_DEPTH: usize = 10;
2051 if depth > MAX_INCLUDE_DEPTH {
2052 return Err(Error::ConfigError(
2053 "exceeded maximum include depth".to_owned(),
2054 ));
2055 }
2056 let mut includes: Vec<(String, Option<String>)> = Vec::new();
2058
2059 for entry in &file.entries {
2060 if entry.key == "include.path" {
2061 if let Some(ref val) = entry.value {
2062 includes.push((val.clone(), None));
2063 }
2064 } else if entry.key.starts_with("includeif.") && entry.key.ends_with(".path") {
2065 let mid = &entry.key["includeif.".len()..entry.key.len() - ".path".len()];
2067 if let Some(ref val) = entry.value {
2068 includes.push((val.clone(), Some(mid.to_owned())));
2069 }
2070 }
2071 }
2072
2073 set.merge(file);
2075
2076 if process_includes {
2078 for (inc_path, condition) in includes {
2079 if let Some(ref cond) = condition {
2080 if !evaluate_include_condition(cond, file, ctx) {
2081 continue;
2082 }
2083 }
2084
2085 let resolved = match resolve_include_file_path(&inc_path, file, ctx) {
2086 Ok(p) => p,
2087 Err(Error::ConfigError(msg)) if msg.is_empty() => continue,
2088 Err(e) => return Err(e),
2089 };
2090 if let Ok(Some(inc_file)) = ConfigFile::from_path(&resolved, file.scope) {
2091 Self::merge_with_includes(set, &inc_file, process_includes, depth + 1, ctx)?;
2092 }
2093 }
2094 }
2095
2096 Ok(())
2097 }
2098}
2099
2100pub fn parse_bool(s: &str) -> std::result::Result<bool, String> {
2113 match s.to_lowercase().as_str() {
2114 "true" | "yes" | "on" => Ok(true),
2115 "" => Ok(true),
2116 "false" | "no" | "off" => Ok(false),
2117 _ => {
2118 if let Ok(n) = s.parse::<i64>() {
2120 return Ok(n != 0);
2121 }
2122 Err(format!("bad boolean config value '{s}'"))
2123 }
2124 }
2125}
2126
2127pub fn parse_i64(s: &str) -> std::result::Result<i64, String> {
2129 let s = s.trim();
2130 if s.is_empty() {
2131 return Err("empty integer value".to_owned());
2132 }
2133
2134 let (num_str, multiplier) = match s.as_bytes().last() {
2135 Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
2136 Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
2137 Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
2138 _ => (s, 1_i64),
2139 };
2140
2141 let base: i64 = num_str
2142 .parse()
2143 .map_err(|_| format!("invalid integer: '{s}'"))?;
2144 base.checked_mul(multiplier)
2145 .ok_or_else(|| format!("integer overflow: '{s}'"))
2146}
2147
2148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2150pub enum GitConfigIntStrictError {
2151 InvalidUnit,
2153 OutOfRange,
2155}
2156
2157pub fn parse_git_config_int_strict(raw: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
2161 let s = raw.trim();
2162 if s.is_empty() {
2163 return Err(GitConfigIntStrictError::InvalidUnit);
2164 }
2165
2166 let bytes = s.as_bytes();
2167 let mut idx = 0usize;
2168 if matches!(bytes.first(), Some(b'+') | Some(b'-')) {
2169 idx = 1;
2170 }
2171 if idx >= bytes.len() {
2172 return Err(GitConfigIntStrictError::InvalidUnit);
2173 }
2174 let digit_start = idx;
2175 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
2176 idx += 1;
2177 }
2178 if idx == digit_start {
2179 return Err(GitConfigIntStrictError::InvalidUnit);
2180 }
2181
2182 let num_part =
2183 std::str::from_utf8(&bytes[..idx]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2184 let suffix =
2185 std::str::from_utf8(&bytes[idx..]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2186 let mult: i64 = match suffix {
2187 "" => 1,
2188 "k" | "K" => 1024,
2189 "m" | "M" => 1024 * 1024,
2190 "g" | "G" => 1024_i64
2191 .checked_mul(1024)
2192 .and_then(|x| x.checked_mul(1024))
2193 .ok_or(GitConfigIntStrictError::OutOfRange)?,
2194 _ => return Err(GitConfigIntStrictError::InvalidUnit),
2195 };
2196
2197 let val: i64 = num_part
2198 .parse()
2199 .map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2200 val.checked_mul(mult)
2201 .ok_or(GitConfigIntStrictError::OutOfRange)
2202}
2203
2204const DIFF_CONTEXT_KEY: &str = "diff.context";
2205
2206fn format_bad_numeric_diff_context(
2207 value: &str,
2208 err: GitConfigIntStrictError,
2209 entry: &ConfigEntry,
2210) -> String {
2211 let detail = match err {
2212 GitConfigIntStrictError::InvalidUnit => "invalid unit",
2213 GitConfigIntStrictError::OutOfRange => "out of range",
2214 };
2215 if entry.scope == ConfigScope::Command || entry.file.is_none() {
2216 return format!(
2217 "fatal: bad numeric config value '{value}' for '{DIFF_CONTEXT_KEY}': {detail}"
2218 );
2219 }
2220 let path = entry
2221 .file
2222 .as_deref()
2223 .map(config_error_path_display)
2224 .unwrap_or_default();
2225 format!("fatal: bad numeric config value '{value}' for '{DIFF_CONTEXT_KEY}' in file {path}: {detail}")
2226}
2227
2228fn format_bad_diff_context_variable(entry: &ConfigEntry) -> String {
2229 if entry.scope == ConfigScope::Command || entry.file.is_none() {
2230 return format!("fatal: unable to parse '{DIFF_CONTEXT_KEY}' from command-line config");
2231 }
2232 let path = entry
2233 .file
2234 .as_deref()
2235 .map(config_error_path_display)
2236 .unwrap_or_default();
2237 format!(
2238 "fatal: bad config variable '{DIFF_CONTEXT_KEY}' in file '{path}' at line {}",
2239 entry.line
2240 )
2241}
2242
2243pub fn resolve_diff_context_lines(cfg: &ConfigSet) -> std::result::Result<Option<usize>, String> {
2248 let Some(entry) = cfg.get_last_entry(DIFF_CONTEXT_KEY) else {
2249 return Ok(None);
2250 };
2251 let value_src = entry.value.as_deref().unwrap_or("").trim();
2252 match parse_git_config_int_strict(value_src) {
2253 Ok(n) if n < 0 => Err(format_bad_diff_context_variable(&entry)),
2254 Ok(n) => Ok(Some(usize::try_from(n).map_err(|_| {
2255 format_bad_numeric_diff_context(value_src, GitConfigIntStrictError::OutOfRange, &entry)
2256 })?)),
2257 Err(e) => Err(format_bad_numeric_diff_context(value_src, e, &entry)),
2258 }
2259}
2260
2261pub fn parse_color(s: &str) -> std::result::Result<String, String> {
2268 const COLOR_BACKGROUND_OFFSET: i32 = 10;
2269 const COLOR_FOREGROUND_ANSI: i32 = 30;
2270 const COLOR_FOREGROUND_RGB: i32 = 38;
2271 const COLOR_FOREGROUND_256: i32 = 38;
2272 const COLOR_FOREGROUND_BRIGHT_ANSI: i32 = 90;
2273
2274 #[derive(Clone, Copy, Default)]
2275 struct Color {
2276 kind: u8,
2277 value: u8,
2278 red: u8,
2279 green: u8,
2280 blue: u8,
2281 }
2282
2283 const COLOR_UNSPECIFIED: u8 = 0;
2284 const COLOR_NORMAL: u8 = 1;
2285 const COLOR_ANSI: u8 = 2;
2286 const COLOR_256: u8 = 3;
2287 const COLOR_RGB: u8 = 4;
2288
2289 fn color_empty(c: &Color) -> bool {
2290 c.kind == COLOR_UNSPECIFIED || c.kind == COLOR_NORMAL
2291 }
2292
2293 fn parse_ansi_color(name: &str) -> Option<Color> {
2294 let color_names = [
2295 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
2296 ];
2297 let color_offset = COLOR_FOREGROUND_ANSI;
2298
2299 if name.eq_ignore_ascii_case("default") {
2300 return Some(Color {
2301 kind: COLOR_ANSI,
2302 value: (9 + color_offset) as u8,
2303 ..Default::default()
2304 });
2305 }
2306
2307 let (name, color_offset) = if name.len() >= 6 && name[..6].eq_ignore_ascii_case("bright") {
2308 (&name[6..], COLOR_FOREGROUND_BRIGHT_ANSI)
2309 } else {
2310 (name, COLOR_FOREGROUND_ANSI)
2311 };
2312
2313 for (i, cn) in color_names.iter().enumerate() {
2314 if name.eq_ignore_ascii_case(cn) {
2315 return Some(Color {
2316 kind: COLOR_ANSI,
2317 value: (i as i32 + color_offset) as u8,
2318 ..Default::default()
2319 });
2320 }
2321 }
2322 None
2323 }
2324
2325 fn hex_val(b: u8) -> Option<u8> {
2326 match b {
2327 b'0'..=b'9' => Some(b - b'0'),
2328 b'a'..=b'f' => Some(b - b'a' + 10),
2329 b'A'..=b'F' => Some(b - b'A' + 10),
2330 _ => None,
2331 }
2332 }
2333
2334 fn get_hex_color(chars: &[u8], width: usize) -> Option<(u8, usize)> {
2335 assert!(width == 1 || width == 2);
2336 if chars.len() < width {
2337 return None;
2338 }
2339 let v = if width == 2 {
2340 let hi = hex_val(chars[0])?;
2341 let lo = hex_val(chars[1])?;
2342 (hi << 4) | lo
2343 } else {
2344 let n = hex_val(chars[0])?;
2345 (n << 4) | n
2346 };
2347 Some((v, width))
2348 }
2349
2350 fn parse_single_color(word: &str) -> Option<Color> {
2351 if word.eq_ignore_ascii_case("normal") {
2352 return Some(Color {
2353 kind: COLOR_NORMAL,
2354 ..Default::default()
2355 });
2356 }
2357
2358 let bytes = word.as_bytes();
2359 if (bytes.len() == 7 || bytes.len() == 4) && bytes.first() == Some(&b'#') {
2360 let width = if bytes.len() == 7 { 2 } else { 1 };
2361 let mut idx = 1;
2362 let (r, n1) = get_hex_color(&bytes[idx..], width)?;
2363 idx += n1;
2364 let (g, n2) = get_hex_color(&bytes[idx..], width)?;
2365 idx += n2;
2366 let (b, n3) = get_hex_color(&bytes[idx..], width)?;
2367 idx += n3;
2368 if idx != bytes.len() {
2369 return None;
2370 }
2371 return Some(Color {
2372 kind: COLOR_RGB,
2373 red: r,
2374 green: g,
2375 blue: b,
2376 ..Default::default()
2377 });
2378 }
2379
2380 if let Some(c) = parse_ansi_color(word) {
2381 return Some(c);
2382 }
2383
2384 let Ok(val) = word.parse::<i64>() else {
2385 return None;
2386 };
2387 if val < -1 {
2388 return None;
2389 }
2390 if val < 0 {
2391 return Some(Color {
2392 kind: COLOR_NORMAL,
2393 ..Default::default()
2394 });
2395 }
2396 if val < 8 {
2397 return Some(Color {
2398 kind: COLOR_ANSI,
2399 value: (val as i32 + COLOR_FOREGROUND_ANSI) as u8,
2400 ..Default::default()
2401 });
2402 }
2403 if val < 16 {
2404 return Some(Color {
2405 kind: COLOR_ANSI,
2406 value: (val as i32 - 8 + COLOR_FOREGROUND_BRIGHT_ANSI) as u8,
2407 ..Default::default()
2408 });
2409 }
2410 if val < 256 {
2411 return Some(Color {
2412 kind: COLOR_256,
2413 value: val as u8,
2414 ..Default::default()
2415 });
2416 }
2417 None
2418 }
2419
2420 fn parse_attr(word: &str) -> Option<u8> {
2421 const ATTRS: [(&str, u8, u8); 8] = [
2422 ("bold", 1, 22),
2423 ("dim", 2, 22),
2424 ("italic", 3, 23),
2425 ("ul", 4, 24),
2426 ("underline", 4, 24),
2427 ("blink", 5, 25),
2428 ("reverse", 7, 27),
2429 ("strike", 9, 29),
2430 ];
2431
2432 let mut negate = false;
2433 let mut rest = word;
2434 if let Some(stripped) = rest.strip_prefix("no") {
2435 negate = true;
2436 rest = stripped;
2437 if let Some(s) = rest.strip_prefix('-') {
2438 rest = s;
2439 }
2440 }
2441
2442 for (name, val, neg) in ATTRS {
2443 if rest == name {
2444 return Some(if negate { neg } else { val });
2445 }
2446 }
2447 None
2448 }
2449
2450 fn append_color_output(out: &mut String, c: &Color, background: bool) {
2451 let offset = if background {
2452 COLOR_BACKGROUND_OFFSET
2453 } else {
2454 0
2455 };
2456 match c.kind {
2457 COLOR_UNSPECIFIED | COLOR_NORMAL => {}
2458 COLOR_ANSI => {
2459 use std::fmt::Write;
2460 let _ = write!(out, "{}", i32::from(c.value) + offset);
2461 }
2462 COLOR_256 => {
2463 use std::fmt::Write;
2464 let _ = write!(out, "{};5;{}", COLOR_FOREGROUND_256 + offset, c.value);
2465 }
2466 COLOR_RGB => {
2467 use std::fmt::Write;
2468 let _ = write!(
2469 out,
2470 "{};2;{};{};{}",
2471 COLOR_FOREGROUND_RGB + offset,
2472 c.red,
2473 c.green,
2474 c.blue
2475 );
2476 }
2477 _ => {}
2478 }
2479 }
2480
2481 let s = s.trim();
2482 if s.is_empty() {
2483 return Ok(String::new());
2484 }
2485
2486 let mut has_reset = false;
2487 let mut attr: u64 = 0;
2488 let mut fg = Color::default();
2489 let mut bg = Color::default();
2490 fg.kind = COLOR_UNSPECIFIED;
2491 bg.kind = COLOR_UNSPECIFIED;
2492
2493 for word in s.split_whitespace() {
2494 if word.eq_ignore_ascii_case("reset") {
2495 has_reset = true;
2496 continue;
2497 }
2498
2499 if let Some(c) = parse_single_color(word) {
2500 if fg.kind == COLOR_UNSPECIFIED {
2501 fg = c;
2502 continue;
2503 }
2504 if bg.kind == COLOR_UNSPECIFIED {
2505 bg = c;
2506 continue;
2507 }
2508 return Err(format!("bad color value '{s}'"));
2509 }
2510
2511 if let Some(code) = parse_attr(word) {
2512 attr |= 1u64 << u64::from(code);
2513 continue;
2514 }
2515
2516 return Err(format!("bad color value '{s}'"));
2517 }
2518
2519 if !has_reset && attr == 0 && color_empty(&fg) && color_empty(&bg) {
2520 return Err(format!("bad color value '{s}'"));
2521 }
2522
2523 let mut out = String::from("\x1b[");
2524 let mut sep = if has_reset { 1u32 } else { 0u32 };
2525
2526 let mut attr_bits = attr;
2527 let mut i = 0u32;
2528 while attr_bits != 0 {
2529 let bit = 1u64 << i;
2530 if attr_bits & bit == 0 {
2531 i += 1;
2532 continue;
2533 }
2534 attr_bits &= !bit;
2535 if sep > 0 {
2536 out.push(';');
2537 }
2538 sep += 1;
2539 use std::fmt::Write;
2540 let _ = write!(out, "{i}");
2541 i += 1;
2542 }
2543
2544 if !color_empty(&fg) {
2545 if sep > 0 {
2546 out.push(';');
2547 }
2548 sep += 1;
2549 append_color_output(&mut out, &fg, false);
2550 }
2551 if !color_empty(&bg) {
2552 if sep > 0 {
2553 out.push(';');
2554 }
2555 append_color_output(&mut out, &bg, true);
2556 }
2557 out.push('m');
2558 Ok(out)
2559}
2560
2561pub fn url_matches(pattern_url: &str, target_url: &str) -> bool {
2563 let pattern = pattern_url.trim_end_matches('/');
2564 let target = target_url.trim_end_matches('/');
2565 if target == pattern {
2566 return true;
2567 }
2568 if let Some(rest) = target.strip_prefix(pattern) {
2569 return rest.starts_with('/') || rest.is_empty();
2570 }
2571 let pattern_slash = format!("{}/", pattern);
2572 target.starts_with(&pattern_slash)
2573}
2574
2575pub fn get_urlmatch_entries<'a>(
2577 entries: &'a [ConfigEntry],
2578 section: &str,
2579 variable: &str,
2580 url: &str,
2581) -> Vec<&'a ConfigEntry> {
2582 let section_lower = section.to_lowercase();
2583 let variable_lower = variable.to_lowercase();
2584 let mut matches: Vec<(usize, &'a ConfigEntry)> = Vec::new();
2585
2586 for entry in entries {
2587 let key = &entry.key;
2588 let first_dot = match key.find('.') {
2589 Some(i) => i,
2590 None => continue,
2591 };
2592 let last_dot = match key.rfind('.') {
2593 Some(i) => i,
2594 None => continue,
2595 };
2596 let entry_section = &key[..first_dot];
2597 let entry_variable = &key[last_dot + 1..];
2598 if entry_section.to_lowercase() != section_lower
2599 || entry_variable.to_lowercase() != variable_lower
2600 {
2601 continue;
2602 }
2603 if first_dot == last_dot {
2604 matches.push((0, entry));
2605 } else {
2606 let subsection = &key[first_dot + 1..last_dot];
2607 if url_matches(subsection, url) {
2608 matches.push((subsection.len(), entry));
2609 }
2610 }
2611 }
2612 matches.sort_by(|a, b| a.0.cmp(&b.0));
2613 matches.into_iter().map(|(_, e)| e).collect()
2614}
2615
2616pub fn get_urlmatch_all_in_section(
2618 entries: &[ConfigEntry],
2619 section: &str,
2620 url: &str,
2621) -> Vec<(String, String, ConfigScope)> {
2622 let section_lower = section.to_lowercase();
2623 let mut matches: Vec<(String, usize, String, String, ConfigScope)> = Vec::new();
2624
2625 for entry in entries {
2626 let key = &entry.key;
2627 let first_dot = match key.find('.') {
2628 Some(i) => i,
2629 None => continue,
2630 };
2631 let last_dot = match key.rfind('.') {
2632 Some(i) => i,
2633 None => continue,
2634 };
2635 let entry_section = &key[..first_dot];
2636 if entry_section.to_lowercase() != section_lower {
2637 continue;
2638 }
2639 let entry_variable = &key[last_dot + 1..];
2640 let val = entry.value.as_deref().unwrap_or("true");
2641 if first_dot == last_dot {
2642 let canonical = format!("{}.{}", section_lower, entry_variable);
2643 matches.push((
2644 entry_variable.to_lowercase(),
2645 0,
2646 val.to_owned(),
2647 canonical,
2648 entry.scope,
2649 ));
2650 } else {
2651 let subsection = &key[first_dot + 1..last_dot];
2652 if url_matches(subsection, url) {
2653 let canonical = format!("{}.{}", section_lower, entry_variable);
2654 matches.push((
2655 entry_variable.to_lowercase(),
2656 subsection.len(),
2657 val.to_owned(),
2658 canonical,
2659 entry.scope,
2660 ));
2661 }
2662 }
2663 }
2664
2665 let mut best: std::collections::BTreeMap<String, (usize, String, String, ConfigScope)> =
2666 std::collections::BTreeMap::new();
2667 for (var, specificity, val, canonical, scope) in matches {
2668 let entry = best
2669 .entry(var)
2670 .or_insert((0, String::new(), String::new(), scope));
2671 if specificity >= entry.0 {
2672 *entry = (specificity, val, canonical, scope);
2673 }
2674 }
2675 best.into_values()
2676 .map(|(_, val, canonical, scope)| (canonical, val, scope))
2677 .collect()
2678}
2679
2680pub fn parse_path(s: &str) -> String {
2684 if let Some(rest) = s.strip_prefix("~/") {
2685 if let Some(home) = home_dir() {
2686 return home.join(rest).to_string_lossy().to_string();
2687 }
2688 }
2689 s.to_owned()
2690}
2691
2692pub fn parse_path_optional(s: &str) -> Option<String> {
2697 if let Some(rest) = s.strip_prefix(":(optional)") {
2698 let resolved = parse_path(rest);
2699 if std::path::Path::new(&resolved).exists() {
2700 Some(resolved)
2701 } else {
2702 None }
2704 } else {
2705 Some(parse_path(s))
2706 }
2707}
2708
2709#[must_use]
2725pub fn git_config_parameters_last_value(raw: &str, key: &str) -> Option<String> {
2726 let Ok(canon) = canonical_key(key) else {
2727 return None;
2728 };
2729 let mut last: Option<String> = None;
2730 for entry in parse_config_parameters(raw) {
2731 if let Some((k, v)) = entry.split_once('=') {
2732 if canonical_key(k.trim()).ok().as_ref() == Some(&canon) {
2733 last = Some(v.to_owned());
2734 }
2735 } else if canonical_key(entry.trim()).ok().as_ref() == Some(&canon) {
2736 last = Some("true".to_owned());
2737 }
2738 }
2739 last
2740}
2741
2742pub fn parse_config_parameters(raw: &str) -> Vec<String> {
2743 let mut out: Vec<String> = Vec::new();
2744 let mut buf = String::new();
2745 let mut in_single = false;
2746 let mut in_double = false;
2747
2748 let mut chars = raw.chars().peekable();
2749 while let Some(ch) = chars.next() {
2750 if in_single {
2751 if ch == '\'' {
2752 in_single = false;
2753 } else {
2754 buf.push(ch);
2755 }
2756 continue;
2757 }
2758 if in_double {
2759 if ch == '"' {
2760 in_double = false;
2761 continue;
2762 }
2763 if ch == '\\' {
2764 if let Some(next) = chars.next() {
2765 let mapped = match next {
2766 'n' => '\n',
2767 't' => '\t',
2768 'r' => '\r',
2769 '"' => '"',
2770 '\\' => '\\',
2771 other => other,
2772 };
2773 buf.push(mapped);
2774 }
2775 continue;
2776 }
2777 buf.push(ch);
2778 continue;
2779 }
2780
2781 if ch == '\'' {
2782 in_single = true;
2783 continue;
2784 }
2785 if ch == '"' {
2786 in_double = true;
2787 continue;
2788 }
2789
2790 if ch.is_whitespace() {
2791 if !buf.is_empty() {
2792 out.push(std::mem::take(&mut buf));
2793 }
2794 continue;
2795 }
2796
2797 buf.push(ch);
2798 }
2799
2800 if !buf.is_empty() {
2801 out.push(buf);
2802 }
2803
2804 out
2805}
2806
2807pub fn global_config_paths_pub() -> Vec<PathBuf> {
2810 global_config_paths()
2811}
2812
2813fn global_config_paths() -> Vec<PathBuf> {
2814 let mut paths = Vec::new();
2815
2816 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
2818 paths.push(PathBuf::from(p));
2819 return paths;
2820 }
2821
2822 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
2824 paths.push(PathBuf::from(xdg).join("git/config"));
2825 } else if let Some(home) = home_dir() {
2826 paths.push(home.join(".config/git/config"));
2827 }
2828 if let Some(home) = home_dir() {
2829 paths.push(home.join(".gitconfig"));
2830 }
2831
2832 paths
2833}
2834
2835fn home_dir() -> Option<PathBuf> {
2837 std::env::var("HOME").ok().map(PathBuf::from)
2838}
2839
2840fn include_source_is_disk_file(file: &ConfigFile) -> bool {
2842 file.include_origin == ConfigIncludeOrigin::Disk
2843}
2844
2845fn resolve_include_file_path(
2849 path: &str,
2850 file: &ConfigFile,
2851 ctx: &IncludeContext,
2852) -> Result<PathBuf> {
2853 let expanded = parse_path(path);
2854 let p = Path::new(&expanded);
2855 if p.is_absolute() {
2856 return Ok(p.to_path_buf());
2857 }
2858 if !include_source_is_disk_file(file) {
2859 if file.include_origin == ConfigIncludeOrigin::CommandLine {
2860 if ctx.command_line_relative_include_is_error {
2861 return Err(Error::ConfigError(
2862 "relative config includes must come from files".to_owned(),
2863 ));
2864 }
2865 return Err(Error::ConfigError(String::new()));
2866 }
2867 return Err(Error::ConfigError(
2868 "relative config includes must come from files".to_owned(),
2869 ));
2870 }
2871 let base = match file.path.parent() {
2872 Some(p) if !p.as_os_str().is_empty() => p,
2873 Some(_) | None => Path::new("."),
2874 };
2875 Ok(base.join(p))
2876}
2877
2878fn is_dir_sep(b: u8) -> bool {
2879 b == b'/' || b == b'\\'
2880}
2881
2882fn add_trailing_starstar_for_dir(pat: &mut String) {
2883 let bytes = pat.as_bytes();
2884 if !bytes.is_empty() && is_dir_sep(*bytes.last().unwrap()) {
2885 pat.push_str("**");
2886 }
2887}
2888
2889fn prepare_gitdir_pattern(condition: &str, file: &ConfigFile) -> Result<(String, usize)> {
2891 let mut pat = parse_path(condition);
2893 if pat.starts_with("./") || pat.starts_with(".\\") {
2894 if !include_source_is_disk_file(file) {
2895 return Err(Error::ConfigError(
2896 "relative config include conditionals must come from files".to_owned(),
2897 ));
2898 }
2899 let parent = file.path.parent().ok_or_else(|| {
2900 Error::ConfigError(
2901 "relative config include conditionals must come from files".to_owned(),
2902 )
2903 })?;
2904 let real = parent.canonicalize().map_err(Error::Io)?;
2905 let mut dir = real.to_string_lossy().into_owned();
2906 if !dir.ends_with('/') && !dir.ends_with('\\') {
2907 dir.push('/');
2908 }
2909 let rest = &pat[2..];
2910 pat = format!("{dir}{rest}");
2911 let prefix_len = dir.len();
2912 add_trailing_starstar_for_dir(&mut pat);
2913 return Ok((pat, prefix_len));
2914 }
2915 let p = Path::new(&pat);
2916 if !p.is_absolute() {
2917 pat.insert_str(0, "**/");
2918 }
2919 add_trailing_starstar_for_dir(&mut pat);
2920 Ok((pat, 0))
2921}
2922
2923fn git_dir_match_texts(git_dir: &Path) -> (String, String) {
2925 let real = git_dir
2926 .canonicalize()
2927 .map(|p| p.to_string_lossy().into_owned())
2928 .unwrap_or_else(|_| git_dir.to_string_lossy().into_owned());
2929 let abs = if git_dir.is_absolute() {
2930 git_dir.to_string_lossy().into_owned()
2931 } else if let Ok(cwd) = std::env::current_dir() {
2932 cwd.join(git_dir).to_string_lossy().into_owned()
2933 } else {
2934 git_dir.to_string_lossy().into_owned()
2935 };
2936 (real, abs)
2937}
2938
2939fn include_by_gitdir(
2940 condition: &str,
2941 file: &ConfigFile,
2942 ctx: &IncludeContext,
2943 icase: bool,
2944) -> bool {
2945 let Some(git_dir) = ctx.git_dir.as_ref() else {
2946 return false;
2947 };
2948 let (pattern, prefix) = match prepare_gitdir_pattern(condition, file) {
2949 Ok(x) => x,
2950 Err(_) => return false,
2951 };
2952 let flags = WM_PATHNAME | if icase { WM_CASEFOLD } else { 0 };
2953 let (text_real, text_abs) = git_dir_match_texts(git_dir);
2954 let try_match = |text: &str| -> bool {
2955 let t = text.as_bytes();
2956 let p = pattern.as_bytes();
2957 if prefix > 0 {
2958 if t.len() < prefix {
2959 return false;
2960 }
2961 let pre = &p[..prefix];
2962 let te = &t[..prefix];
2963 let ok = if icase {
2964 pre.eq_ignore_ascii_case(te)
2965 } else {
2966 pre == te
2967 };
2968 if !ok {
2969 return false;
2970 }
2971 return wildmatch(&p[prefix..], &t[prefix..], flags);
2972 }
2973 wildmatch(p, t, flags)
2974 };
2975 if try_match(&text_real) {
2976 return true;
2977 }
2978 text_real != text_abs && try_match(&text_abs)
2979}
2980
2981fn current_branch_short_name(git_dir: Option<&Path>) -> Option<String> {
2982 let gd = git_dir?;
2983 let target = refs::read_symbolic_ref(gd, "HEAD").ok()??;
2984 let rest = target.strip_prefix("refs/heads/")?;
2985 Some(rest.to_owned())
2986}
2987
2988fn include_by_onbranch(condition: &str, ctx: &IncludeContext) -> bool {
2989 let Some(short) = current_branch_short_name(ctx.git_dir.as_deref()) else {
2990 return false;
2991 };
2992 let mut pattern = condition.to_owned();
2993 add_trailing_starstar_for_dir(&mut pattern);
2994 wildmatch(pattern.as_bytes(), short.as_bytes(), WM_PATHNAME)
2995}
2996
2997fn evaluate_include_condition(condition: &str, file: &ConfigFile, ctx: &IncludeContext) -> bool {
3001 if let Some(rest) = condition.strip_prefix("gitdir/i:") {
3002 return include_by_gitdir(rest, file, ctx, true);
3003 }
3004 if let Some(rest) = condition.strip_prefix("gitdir:") {
3005 return include_by_gitdir(rest, file, ctx, false);
3006 }
3007 if let Some(rest) = condition.strip_prefix("onbranch:") {
3008 return include_by_onbranch(rest, ctx);
3009 }
3010 false
3011}
3012
3013fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
3015 let first_dot = key
3016 .find('.')
3017 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
3018 let last_dot = key
3019 .rfind('.')
3020 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
3021
3022 let section = key[..first_dot].to_owned();
3023 let variable = key[last_dot + 1..].to_owned();
3024
3025 let subsection = if first_dot == last_dot {
3026 None
3027 } else {
3028 Some(key[first_dot + 1..last_dot].to_owned())
3029 };
3030
3031 Ok((section, subsection, variable))
3032}
3033
3034#[allow(dead_code)]
3036fn variable_name_from_key(key: &str) -> &str {
3037 match key.rfind('.') {
3038 Some(i) => &key[i + 1..],
3039 None => key,
3040 }
3041}
3042
3043fn parse_section_name(name: &str) -> (&str, Option<&str>) {
3047 match name.find('.') {
3048 Some(i) => (&name[..i], Some(&name[i + 1..])),
3049 None => (name, None),
3050 }
3051}
3052
3053fn raw_variable_name(raw_key: &str) -> &str {
3057 match raw_key.rfind('.') {
3058 Some(i) => &raw_key[i + 1..],
3059 None => raw_key,
3060 }
3061}
3062
3063fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
3068 let first_dot = match raw_key.find('.') {
3069 Some(i) => i,
3070 None => return (raw_key.to_owned(), None),
3071 };
3072 let last_dot = match raw_key.rfind('.') {
3074 Some(i) => i,
3075 None => return (raw_key[..first_dot].to_owned(), None),
3076 };
3077 let section = raw_key[..first_dot].to_owned();
3078 if first_dot == last_dot {
3079 (section, None)
3080 } else {
3081 let subsection = raw_key[first_dot + 1..last_dot].to_owned();
3082 (section, Some(subsection))
3083 }
3084}
3085
3086fn is_section_header_with_inline_entry(line: &str) -> bool {
3088 let trimmed = line.trim();
3089 if !trimmed.starts_with('[') {
3090 return false;
3091 }
3092 let end = match trimmed.find(']') {
3093 Some(i) => i,
3094 None => return false,
3095 };
3096 let after = trimmed[end + 1..].trim();
3097 !after.is_empty() && !after.starts_with('#') && !after.starts_with(';')
3099}
3100
3101fn extract_section_header(line: &str) -> String {
3104 let trimmed = line.trim();
3105 let end = match trimmed.find(']') {
3106 Some(i) => i,
3107 None => return line.to_owned(),
3108 };
3109 trimmed[..=end].to_owned()
3112}
3113
3114#[cfg(test)]
3115mod get_regexp_tests {
3116 use super::{ConfigFile, ConfigScope, ConfigSet};
3117 use std::path::Path;
3118
3119 fn set_from_snippet(text: &str) -> ConfigSet {
3120 let path = Path::new(".git/config");
3121 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
3122 let mut set = ConfigSet::new();
3123 set.merge(&file);
3124 set
3125 }
3126
3127 #[test]
3128 fn get_regexp_matches_section_prefix_like_git_config() {
3129 let text = r#"
3130[user]
3131 email = alice@example.com
3132 name = Alice
3133[core]
3134 bare = false
3135"#;
3136 let set = set_from_snippet(text);
3137 let keys: Vec<_> = set
3138 .get_regexp("user")
3139 .expect("valid pattern")
3140 .into_iter()
3141 .map(|e| e.key.as_str())
3142 .collect();
3143 assert!(keys.contains(&"user.email"));
3144 assert!(keys.contains(&"user.name"));
3145 assert!(!keys.iter().any(|k| k.starts_with("core.")));
3146 }
3147
3148 #[test]
3149 fn get_regexp_returns_all_multi_value_entries_in_order() {
3150 let text = r#"
3151[remote "origin"]
3152 url = https://example.com/repo.git
3153 fetch = +refs/heads/*:refs/remotes/origin/*
3154 push = +refs/heads/main:refs/heads/main
3155 push = +refs/heads/develop:refs/heads/develop
3156"#;
3157 let set = set_from_snippet(text);
3158 let matches = set.get_regexp("remote.origin").expect("valid pattern");
3159 let push_vals: Vec<_> = matches
3160 .iter()
3161 .filter(|e| e.key == "remote.origin.push")
3162 .map(|e| e.value.as_deref().unwrap_or(""))
3163 .collect();
3164 assert_eq!(push_vals.len(), 2);
3165 assert_eq!(push_vals[0], "+refs/heads/main:refs/heads/main");
3166 assert_eq!(push_vals[1], "+refs/heads/develop:refs/heads/develop");
3167 }
3168
3169 #[test]
3170 fn get_regexp_dot_matches_any_key() {
3171 let text = r#"
3172[a]
3173 x = 1
3174[b]
3175 y = 2
3176"#;
3177 let set = set_from_snippet(text);
3178 let m = set.get_regexp(".").expect("valid pattern");
3179 assert_eq!(m.len(), 2);
3180 }
3181
3182 #[test]
3183 fn get_regexp_no_match_returns_empty_vec() {
3184 let set = set_from_snippet("[user]\n\tname = x\n");
3185 let m = set.get_regexp("zzz").expect("valid pattern");
3186 assert!(m.is_empty());
3187 }
3188
3189 #[test]
3190 fn get_regexp_invalid_pattern_is_error() {
3191 let set = set_from_snippet("[user]\n\tname = x\n");
3192 let err = set.get_regexp("(").expect_err("unclosed group");
3193 assert!(err.contains("invalid key pattern"), "got: {err}");
3194 }
3195}
3196
3197#[cfg(test)]
3198mod pack_compression_tests {
3199 use super::{ConfigFile, ConfigScope, ConfigSet};
3200 use std::path::Path;
3201
3202 fn set_from_snippet(text: &str) -> ConfigSet {
3203 let path = Path::new(".git/config");
3204 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
3205 let mut set = ConfigSet::new();
3206 set.merge(&file);
3207 set
3208 }
3209
3210 #[test]
3211 fn pack_objects_zlib_level_defaults_to_six() {
3212 let set = ConfigSet::new();
3213 assert_eq!(set.pack_objects_zlib_level().unwrap(), 6);
3214 }
3215
3216 #[test]
3217 fn pack_objects_zlib_level_core_compression() {
3218 let set = set_from_snippet("[core]\n\tcompression = 0\n");
3219 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3220 let set = set_from_snippet("[core]\n\tcompression = 9\n");
3221 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3222 }
3223
3224 #[test]
3225 fn pack_objects_zlib_level_pack_overrides_core() {
3226 let set = set_from_snippet("[core]\n\tcompression = 9\n[pack]\n\tcompression = 0\n");
3227 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3228 let set = set_from_snippet("[core]\n\tcompression = 0\n[pack]\n\tcompression = 9\n");
3229 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3230 }
3231
3232 #[test]
3233 fn pack_objects_zlib_level_later_core_does_not_override_earlier_pack() {
3234 let mut set = ConfigSet::new();
3235 set.merge(
3236 &ConfigFile::parse(
3237 Path::new("a"),
3238 "[pack]\n\tcompression = 9\n",
3239 ConfigScope::Local,
3240 )
3241 .unwrap(),
3242 );
3243 set.merge(
3244 &ConfigFile::parse(
3245 Path::new("b"),
3246 "[core]\n\tcompression = 0\n",
3247 ConfigScope::Local,
3248 )
3249 .unwrap(),
3250 );
3251 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3252 }
3253
3254 #[test]
3255 fn pack_objects_zlib_level_loosecompression_does_not_block_core_pack_level() {
3256 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 0\n");
3257 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3258 }
3259
3260 #[test]
3261 fn pack_objects_zlib_level_pack_wins_after_loose_and_core() {
3262 let set = set_from_snippet(
3263 "[core]\n\tloosecompression = 1\n\tcompression = 0\n[pack]\n\tcompression = 9\n",
3264 );
3265 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3266 }
3267}