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
227fn config_error_path_display(path: &Path) -> String {
230 if path.file_name().and_then(|s| s.to_str()) == Some("config")
231 && path
232 .parent()
233 .and_then(|p| p.file_name())
234 .and_then(|s| s.to_str())
235 == Some(".git")
236 {
237 return ".git/config".to_owned();
238 }
239 path.display().to_string()
240}
241
242struct Parser {
244 section: String,
245 subsection: Option<String>,
246}
247
248impl Parser {
249 fn new() -> Self {
250 Self {
251 section: String::new(),
252 subsection: None,
253 }
254 }
255
256 fn make_key(&self, name: &str) -> String {
258 let sec = self.section.to_lowercase();
259 let var = name.to_lowercase();
260 match &self.subsection {
261 Some(sub) => format!("{sec}.{sub}.{var}"),
262 None => format!("{sec}.{var}"),
263 }
264 }
265
266 fn try_parse_section_with_remainder<'a>(
272 &mut self,
273 line: &'a str,
274 inline_remainder: &mut Option<&'a str>,
275 ) -> bool {
276 let trimmed = line.trim();
277 if !trimmed.starts_with('[') {
278 return false;
279 }
280 let end = {
284 let bytes = trimmed.as_bytes();
285 let mut i = 1; let mut in_quotes = false;
287 let mut found = None;
288 while i < bytes.len() {
289 if in_quotes {
290 if bytes[i] == b'\\' {
291 i += 2; continue;
293 }
294 if bytes[i] == b'"' {
295 in_quotes = false;
296 }
297 } else {
298 if bytes[i] == b'"' {
299 in_quotes = true;
300 }
301 if bytes[i] == b']' {
302 found = Some(i);
303 break;
304 }
305 }
306 i += 1;
307 }
308 match found {
309 Some(i) => i,
310 None => return false,
311 }
312 };
313 let inside = &trimmed[1..end];
314 if let Some(quote_start) = inside.find('"') {
316 self.section = inside[..quote_start].trim().to_owned();
317 let rest = &inside[quote_start + 1..];
318 let mut sub = String::new();
320 let mut chars = rest.chars();
321 while let Some(ch) = chars.next() {
322 if ch == '\\' {
323 if let Some(escaped) = chars.next() {
324 sub.push(escaped);
325 }
326 } else if ch == '"' {
327 break;
328 } else {
329 sub.push(ch);
330 }
331 }
332 self.subsection = Some(sub);
333 } else {
334 self.section = inside.trim().to_owned();
335 self.subsection = None;
336 }
337 let after = trimmed[end + 1..].trim();
339 if !after.is_empty() && !after.starts_with('#') && !after.starts_with(';') {
340 *inline_remainder = Some(after);
341 } else {
342 *inline_remainder = None;
343 }
344 true
345 }
346
347 fn try_parse_section(&mut self, line: &str) -> bool {
349 let mut _remainder = None;
350 self.try_parse_section_with_remainder(line, &mut _remainder)
351 }
352
353 fn try_parse_entry(&self, line: &str) -> Option<(String, Option<String>)> {
357 let trimmed = line.trim();
358 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
359 return None;
360 }
361 if trimmed.starts_with('[') {
362 return None;
363 }
364 if self.section.is_empty() {
365 return None;
366 }
367
368 if let Some(eq_pos) = trimmed.find('=') {
369 let raw_name = trimmed[..eq_pos].trim();
370 let raw_value = trimmed[eq_pos + 1..].trim();
371 let value = strip_inline_comment(raw_value);
373 let value = unescape_value(&value);
374 let key = self.make_key(raw_name);
375 Some((key, Some(value)))
376 } else {
377 let raw_name = strip_inline_comment(trimmed);
379 let key = self.make_key(raw_name.trim());
380 Some((key, None))
381 }
382 }
383}
384
385fn value_line_continues(line: &str) -> bool {
391 let trimmed = line.trim();
392 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
393 return false;
394 }
395 let value_part = match trimmed.find('=') {
398 Some(pos) => &trimmed[pos + 1..],
399 None => return false,
400 };
401 let mut in_quote = false;
403 let mut last_was_backslash = false;
404 let mut in_comment = false;
405 for ch in value_part.chars() {
406 if in_comment {
407 last_was_backslash = false;
409 continue;
410 }
411 match ch {
412 '"' if !last_was_backslash => {
413 in_quote = !in_quote;
414 last_was_backslash = false;
415 }
416 '\\' if !last_was_backslash => {
417 last_was_backslash = true;
418 continue;
419 }
420 '#' | ';' if !in_quote && !last_was_backslash => {
421 in_comment = true;
422 last_was_backslash = false;
423 }
424 _ => {
425 last_was_backslash = false;
426 }
427 }
428 }
429 last_was_backslash && !in_comment
431}
432
433fn strip_inline_comment(s: &str) -> String {
435 let mut in_quote = false;
436 let mut result = String::with_capacity(s.len());
437 let mut chars = s.chars().peekable();
438 while let Some(ch) = chars.next() {
439 match ch {
440 '"' => {
441 in_quote = !in_quote;
442 result.push(ch);
443 }
444 '\\' if in_quote => {
445 result.push(ch);
446 if let Some(&next) = chars.peek() {
447 result.push(next);
448 chars.next();
449 }
450 }
451 '#' | ';' if !in_quote => break,
452 _ => result.push(ch),
453 }
454 }
455 let trimmed = result.trim_end();
457 trimmed.to_owned()
458}
459
460fn unescape_value(s: &str) -> String {
463 let mut result = String::with_capacity(s.len());
464 let mut chars = s.chars();
465 while let Some(ch) = chars.next() {
466 match ch {
467 '"' => { }
468 '\\' => match chars.next() {
469 Some('n') => result.push('\n'),
470 Some('t') => result.push('\t'),
471 Some('\\') => result.push('\\'),
472 Some('"') => result.push('"'),
473 Some(other) => {
474 result.push('\\');
475 result.push(other);
476 }
477 None => result.push('\\'),
478 },
479 _ => result.push(ch),
480 }
481 }
482 result
483}
484
485fn escape_subsection(s: &str) -> String {
492 let mut out = String::with_capacity(s.len());
493 for ch in s.chars() {
494 match ch {
495 '"' => out.push_str("\\\""),
496 '\\' => out.push_str("\\\\"),
497 other => out.push(other),
498 }
499 }
500 out
501}
502
503fn escape_value(s: &str) -> String {
504 let needs_quoting = s.starts_with('-')
507 || s.starts_with(' ')
508 || s.starts_with('\t')
509 || s.ends_with(' ')
510 || s.ends_with('\t')
511 || s.contains('"')
512 || s.contains('\\')
513 || s.contains('\n')
514 || s.contains('#')
515 || s.contains(';');
516
517 if !needs_quoting {
518 return s.to_owned();
519 }
520
521 let mut out = String::with_capacity(s.len() + 4);
522 out.push('"');
523 for ch in s.chars() {
524 match ch {
525 '"' => out.push_str("\\\""),
526 '\\' => out.push_str("\\\\"),
527 '\n' => out.push_str("\\n"),
528 '\t' => out.push_str("\\t"),
529 other => out.push(other),
530 }
531 }
532 out.push('"');
533 out
534}
535
536fn format_comment_suffix(comment: Option<&str>) -> String {
543 match comment {
544 None => String::new(),
545 Some(c) => {
546 if c.starts_with(' ') || c.starts_with('\t') {
547 c.to_owned()
549 } else if c.starts_with('#') {
550 format!(" {c}")
552 } else {
553 format!(" # {c}")
555 }
556 }
557 }
558}
559
560impl ConfigFile {
561 pub fn parse(path: &Path, content: &str, scope: ConfigScope) -> Result<Self> {
573 let raw_lines: Vec<String> = content
574 .lines()
575 .map(|l| l.strip_suffix('\r').unwrap_or(l))
576 .map(String::from)
577 .collect();
578 let mut entries = Vec::new();
579 let mut parser = Parser::new();
580
581 let mut idx = 0;
582 while idx < raw_lines.len() {
583 let start_idx = idx;
584 let line = &raw_lines[idx];
585 idx += 1;
586
587 let trimmed = line.trim();
589 if trimmed.starts_with('#') || trimmed.starts_with(';') {
590 continue;
591 }
592
593 let mut inline_remainder = None;
594 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
595 if let Some(remainder) = inline_remainder {
597 if let Some((key, value)) = parser.try_parse_entry(remainder) {
598 if key == "fetch.negotiationalgorithm" && value.is_none() {
599 let file_disp = config_error_path_display(path);
600 return Err(Error::Message(format!(
601 "error: missing value for 'fetch.negotiationalgorithm'\n\
602fatal: bad config variable 'fetch.negotiationalgorithm' in file '{file_disp}' at line {}",
603 start_idx + 1
604 )));
605 }
606 entries.push(ConfigEntry {
607 key,
608 value,
609 scope,
610 file: Some(path.to_path_buf()),
611 line: start_idx + 1,
612 });
613 }
614 }
615 continue;
616 }
617
618 let mut logical_line = line.clone();
621 while value_line_continues(&logical_line) && idx < raw_lines.len() {
622 let t = logical_line.trim_end();
624 logical_line = t[..t.len() - 1].to_string();
625 let next = raw_lines[idx].trim_start();
627 logical_line.push_str(next);
628 idx += 1;
629 }
630
631 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
632 if key == "fetch.negotiationalgorithm" && value.is_none() {
633 let file_disp = config_error_path_display(path);
634 return Err(Error::Message(format!(
635 "error: missing value for 'fetch.negotiationalgorithm'\n\
636fatal: bad config variable 'fetch.negotiationalgorithm' in file '{file_disp}' at line {}",
637 start_idx + 1
638 )));
639 }
640 entries.push(ConfigEntry {
641 key,
642 value,
643 scope,
644 file: Some(path.to_path_buf()),
645 line: start_idx + 1,
646 });
647 }
648 }
649
650 Ok(Self {
651 path: path.to_path_buf(),
652 scope,
653 entries,
654 raw_lines,
655 include_origin: ConfigIncludeOrigin::Disk,
656 })
657 }
658
659 pub fn parse_with_origin(
661 path: &Path,
662 content: &str,
663 scope: ConfigScope,
664 include_origin: ConfigIncludeOrigin,
665 ) -> Result<Self> {
666 let mut f = Self::parse(path, content, scope)?;
667 f.include_origin = include_origin;
668 Ok(f)
669 }
670
671 pub fn from_git_config_parameters(path: &Path, raw: &str) -> Result<Self> {
676 let mut entries = Vec::new();
677 for entry in parse_config_parameters(raw) {
678 if let Some((key, val)) = entry.split_once('=') {
679 let canon = canonical_key(key.trim())?;
680 entries.push(ConfigEntry {
681 key: canon,
682 value: Some(val.to_owned()),
683 scope: ConfigScope::Command,
684 file: None,
685 line: 0,
686 });
687 } else {
688 let canon = canonical_key(entry.trim())?;
689 entries.push(ConfigEntry {
690 key: canon,
691 value: None,
692 scope: ConfigScope::Command,
693 file: None,
694 line: 0,
695 });
696 }
697 }
698 Ok(Self {
699 path: path.to_path_buf(),
700 scope: ConfigScope::Command,
701 entries,
702 raw_lines: Vec::new(),
703 include_origin: ConfigIncludeOrigin::CommandLine,
704 })
705 }
706
707 pub fn from_path(path: &Path, scope: ConfigScope) -> Result<Option<Self>> {
716 match fs::read_to_string(path) {
717 Ok(content) => Ok(Some(Self::parse(path, &content, scope)?)),
718 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
719 Err(e) => Err(Error::Io(e)),
720 }
721 }
722
723 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
734 self.set_with_comment(key, value, None)
735 }
736
737 pub fn set_with_comment(
739 &mut self,
740 key: &str,
741 value: &str,
742 comment: Option<&str>,
743 ) -> Result<()> {
744 let canon = canonical_key(key)?;
745 let raw_var = raw_variable_name(key);
746 let comment_suffix = format_comment_suffix(comment);
747
748 let existing_idx = self.entries.iter().rposition(|e| e.key == canon);
750
751 if let Some(idx) = existing_idx {
752 let line_idx = self.entries[idx].line - 1;
753 let raw_line = &self.raw_lines[line_idx];
754 if is_section_header_with_inline_entry(raw_line) {
755 let header_only = extract_section_header(raw_line);
757 self.raw_lines[line_idx] = header_only;
758 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
759 self.raw_lines.insert(line_idx + 1, new_line);
760 let content = self.raw_lines.join("\n");
762 let reparsed = Self::parse(&self.path, &content, self.scope)?;
763 self.entries = reparsed.entries;
764 self.raw_lines = reparsed.raw_lines;
765 } else {
766 self.raw_lines[line_idx] =
767 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
768 self.entries[idx].value = Some(value.to_owned());
769 }
770 } else {
771 let (section, subsection, _var) = split_key(&canon)?;
773 let (raw_sec, raw_sub) = raw_section_parts(key);
774 let section_line = self.find_or_create_section_preserving_case(
775 §ion,
776 subsection.as_deref(),
777 &raw_sec,
778 raw_sub.as_deref(),
779 );
780 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
781
782 let insert_at = self.last_line_in_section(section_line) + 1;
784 self.raw_lines.insert(insert_at, new_line);
785
786 let content = self.raw_lines.join("\n");
788 let reparsed = Self::parse(&self.path, &content, self.scope)?;
789 self.entries = reparsed.entries;
790 self.raw_lines = reparsed.raw_lines;
791 }
792
793 Ok(())
794 }
795
796 pub fn replace_all(
801 &mut self,
802 key: &str,
803 value: &str,
804 value_pattern: Option<&str>,
805 ) -> Result<()> {
806 self.replace_all_with_comment(key, value, value_pattern, None)
807 }
808
809 pub fn replace_all_with_comment(
814 &mut self,
815 key: &str,
816 value: &str,
817 value_pattern: Option<&str>,
818 comment: Option<&str>,
819 ) -> Result<()> {
820 let canon = canonical_key(key)?;
821 let comment_suffix = format_comment_suffix(comment);
822
823 let (re, negated) = match value_pattern {
825 Some(pat) => {
826 let (neg, actual_pat) = if let Some(rest) = pat.strip_prefix('!') {
827 (true, rest)
828 } else {
829 (false, pat)
830 };
831 let compiled = regex::Regex::new(actual_pat)
832 .map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?;
833 (Some(compiled), neg)
834 }
835 None => (None, false),
836 };
837
838 let matching_indices: Vec<usize> = self
840 .entries
841 .iter()
842 .enumerate()
843 .filter(|(_, e)| {
844 if e.key != canon {
845 return false;
846 }
847 if let Some(ref re) = re {
848 let v = e.value.as_deref().unwrap_or("");
849 let matched = re.is_match(v);
850 if negated {
851 !matched
852 } else {
853 matched
854 }
855 } else {
856 true
857 }
858 })
859 .map(|(i, _)| i)
860 .collect();
861
862 if matching_indices.is_empty() {
863 return self.add_value_with_comment(key, value, comment);
865 }
866
867 let raw_var = raw_variable_name(key);
868
869 if matching_indices.len() == 1 {
870 let match_idx = matching_indices[0];
872 let line_idx = self.entries[match_idx].line - 1;
873 let raw_line = &self.raw_lines[line_idx];
874 if is_section_header_with_inline_entry(raw_line) {
875 let header = extract_section_header(raw_line);
876 self.raw_lines[line_idx] = header;
877 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
878 self.raw_lines.insert(line_idx + 1, new_line);
879 } else {
880 self.raw_lines[line_idx] =
881 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
882 }
883 } else {
884 for &idx in matching_indices.iter().rev() {
886 let line_idx = self.entries[idx].line - 1;
887 self.remove_entry_line(line_idx);
888 }
889
890 let content = self.raw_lines.join("\n");
892 let reparsed = Self::parse(&self.path, &content, self.scope)?;
893 self.entries = reparsed.entries;
894 self.raw_lines = reparsed.raw_lines;
895
896 let (section, subsection, _var) = split_key(&canon)?;
898 let (raw_sec, raw_sub) = raw_section_parts(key);
899 let section_line = self.find_or_create_section_preserving_case(
900 §ion,
901 subsection.as_deref(),
902 &raw_sec,
903 raw_sub.as_deref(),
904 );
905 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
906 let insert_at = self.last_line_in_section(section_line) + 1;
907 self.raw_lines.insert(insert_at, new_line);
908 }
909
910 let content = self.raw_lines.join("\n");
912 let reparsed = Self::parse(&self.path, &content, self.scope)?;
913 self.entries = reparsed.entries;
914 self.raw_lines = reparsed.raw_lines;
915
916 Ok(())
917 }
918
919 pub fn count(&self, key: &str) -> Result<usize> {
921 let canon = canonical_key(key)?;
922 Ok(self.entries.iter().filter(|e| e.key == canon).count())
923 }
924
925 fn remove_entry_line(&mut self, line_idx: usize) {
936 if is_section_header_with_inline_entry(&self.raw_lines[line_idx]) {
937 let header = extract_section_header(&self.raw_lines[line_idx]);
939 self.raw_lines[line_idx] = header;
940 } else {
941 let mut lines_to_remove = 1;
943 let mut check_line = self.raw_lines[line_idx].clone();
944 while value_line_continues(&check_line)
945 && (line_idx + lines_to_remove) < self.raw_lines.len()
946 {
947 check_line = self.raw_lines[line_idx + lines_to_remove].clone();
948 lines_to_remove += 1;
949 }
950 for _ in 0..lines_to_remove {
951 self.raw_lines.remove(line_idx);
952 }
953 }
954 }
955
956 pub fn unset_last(&mut self, key: &str) -> Result<usize> {
960 let canon = canonical_key(key)?;
961 let last_idx = self.entries.iter().rposition(|e| e.key == canon);
962
963 if let Some(idx) = last_idx {
964 let line_idx = self.entries[idx].line - 1;
965 self.remove_entry_line(line_idx);
966 let content = self.raw_lines.join("\n");
967 let reparsed = Self::parse(&self.path, &content, self.scope)?;
968 self.entries = reparsed.entries;
969 self.raw_lines = reparsed.raw_lines;
970 Ok(1)
971 } else {
972 Ok(0)
973 }
974 }
975
976 pub fn unset(&mut self, key: &str) -> Result<usize> {
986 let canon = canonical_key(key)?;
987 let line_indices: Vec<usize> = self
988 .entries
989 .iter()
990 .filter(|e| e.key == canon)
991 .map(|e| e.line - 1)
992 .collect();
993
994 let count = line_indices.len();
995 for &idx in line_indices.iter().rev() {
997 self.remove_entry_line(idx);
998 }
999
1000 if count > 0 {
1001 let content = self.raw_lines.join("\n");
1002 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1003 self.entries = reparsed.entries;
1004 self.raw_lines = reparsed.raw_lines;
1005 }
1006
1007 Ok(count)
1008 }
1009
1010 pub fn unset_matching(
1019 &mut self,
1020 key: &str,
1021 value_pattern: Option<&str>,
1022 preserve_empty_section_header: bool,
1023 ) -> Result<usize> {
1024 let canon = canonical_key(key)?;
1025 let re = match value_pattern {
1026 Some(pat) => Some(
1027 regex::Regex::new(pat)
1028 .map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?,
1029 ),
1030 None => None,
1031 };
1032
1033 let line_indices: Vec<usize> = self
1034 .entries
1035 .iter()
1036 .filter(|e| {
1037 if e.key != canon {
1038 return false;
1039 }
1040 if let Some(ref re) = re {
1041 let v = e.value.as_deref().unwrap_or("");
1042 re.is_match(v)
1043 } else {
1044 true
1045 }
1046 })
1047 .map(|e| e.line - 1)
1048 .collect();
1049
1050 let count = line_indices.len();
1051 for &idx in line_indices.iter().rev() {
1052 self.remove_entry_line(idx);
1053 }
1054
1055 if count > 0 {
1056 if !preserve_empty_section_header {
1057 self.remove_empty_section_headers();
1059 }
1060
1061 let content = self.raw_lines.join("\n");
1062 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1063 self.entries = reparsed.entries;
1064 self.raw_lines = reparsed.raw_lines;
1065 }
1066
1067 Ok(count)
1068 }
1069
1070 pub fn remove_section(&mut self, section: &str) -> Result<bool> {
1076 let (sec_name, sub_name) = parse_section_name(section);
1077 let sec_lower = sec_name.to_lowercase();
1078
1079 let mut start = None;
1081 let mut end = 0;
1082 let mut parser = Parser::new();
1083
1084 for (idx, line) in self.raw_lines.iter().enumerate() {
1085 if parser.try_parse_section(line) {
1086 if parser.section.to_lowercase() == sec_lower
1087 && parser.subsection.as_deref() == sub_name
1088 {
1089 start = Some(idx);
1090 end = idx;
1091 } else if start.is_some() {
1092 break;
1093 }
1094 } else if start.is_some() {
1095 end = idx;
1096 }
1097 }
1098
1099 if let Some(s) = start {
1100 self.raw_lines.drain(s..=end);
1101 let content = self.raw_lines.join("\n");
1102 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1103 self.entries = reparsed.entries;
1104 self.raw_lines = reparsed.raw_lines;
1105 Ok(true)
1106 } else {
1107 Ok(false)
1108 }
1109 }
1110
1111 pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
1118 let (old_sec, old_sub) = parse_section_name(old_name);
1119 let (new_sec, new_sub) = parse_section_name(new_name);
1120 let old_lower = old_sec.to_lowercase();
1121
1122 let mut found = false;
1123 let mut parser = Parser::new();
1124
1125 for idx in 0..self.raw_lines.len() {
1126 let line = &self.raw_lines[idx];
1127 if parser.try_parse_section(line)
1128 && parser.section.to_lowercase() == old_lower
1129 && parser.subsection.as_deref() == old_sub
1130 {
1131 let header = match new_sub {
1133 Some(sub) => format!("[{} \"{}\"]", new_sec, sub),
1134 None => format!("[{}]", new_sec),
1135 };
1136 self.raw_lines[idx] = header;
1137 found = true;
1138 }
1139 }
1140
1141 if found {
1142 let content = self.raw_lines.join("\n");
1143 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1144 self.entries = reparsed.entries;
1145 self.raw_lines = reparsed.raw_lines;
1146 }
1147
1148 Ok(found)
1149 }
1150
1151 pub fn add_value(&mut self, key: &str, value: &str) -> Result<()> {
1156 self.add_value_with_comment(key, value, None)
1157 }
1158
1159 pub fn add_value_with_comment(
1161 &mut self,
1162 key: &str,
1163 value: &str,
1164 comment: Option<&str>,
1165 ) -> Result<()> {
1166 let canon = canonical_key(key)?;
1167 let raw_var = raw_variable_name(key);
1168 let comment_suffix = format_comment_suffix(comment);
1169 let (section, subsection, _var) = split_key(&canon)?;
1170 let (raw_sec, raw_sub) = raw_section_parts(key);
1171
1172 let section_line = self.find_or_create_section_preserving_case(
1173 §ion,
1174 subsection.as_deref(),
1175 &raw_sec,
1176 raw_sub.as_deref(),
1177 );
1178 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1179 let insert_at = self.last_line_in_section(section_line) + 1;
1180 self.raw_lines.insert(insert_at, new_line);
1181
1182 let content = self.raw_lines.join("\n");
1184 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1185 self.entries = reparsed.entries;
1186 self.raw_lines = reparsed.raw_lines;
1187
1188 Ok(())
1189 }
1190
1191 fn remove_empty_section_headers(&mut self) {
1194 let section_re = regex::Regex::new(r"^\s*\[").unwrap();
1195 let comment_re = regex::Regex::new(r"^\s*(#|;)").unwrap();
1196
1197 let mut to_remove: Vec<usize> = Vec::new();
1198 let len = self.raw_lines.len();
1199
1200 for i in 0..len {
1201 let line = &self.raw_lines[i];
1202 if !section_re.is_match(line) {
1203 continue;
1204 }
1205 if is_section_header_with_inline_entry(line) {
1207 continue;
1208 }
1209 let mut has_entries = false;
1212 for j in (i + 1)..len {
1213 let next = self.raw_lines[j].trim();
1214 if next.is_empty() {
1215 continue;
1216 }
1217 if section_re.is_match(&self.raw_lines[j]) {
1218 break;
1219 }
1220 if comment_re.is_match(&self.raw_lines[j]) {
1221 has_entries = true;
1223 break;
1224 }
1225 has_entries = true;
1227 break;
1228 }
1229 if !has_entries {
1230 to_remove.push(i);
1231 }
1232 }
1233
1234 for &idx in to_remove.iter().rev() {
1236 self.raw_lines.remove(idx);
1237 }
1238
1239 while self.raw_lines.last().is_some_and(|l| l.trim().is_empty()) {
1241 self.raw_lines.pop();
1242 }
1243 }
1244
1245 pub fn write(&self) -> Result<()> {
1250 let content = self.raw_lines.join("\n");
1251 let trimmed = content.trim();
1252 if trimmed.is_empty() {
1253 fs::write(&self.path, "")?;
1255 } else {
1256 let content = if content.ends_with('\n') {
1258 content
1259 } else {
1260 format!("{content}\n")
1261 };
1262 fs::write(&self.path, content)?;
1263 }
1264 Ok(())
1265 }
1266
1267 #[allow(dead_code)]
1269 fn find_or_create_section(&mut self, section: &str, subsection: Option<&str>) -> usize {
1270 let sec_lower = section.to_lowercase();
1271 let mut parser = Parser::new();
1272
1273 for (idx, line) in self.raw_lines.iter().enumerate() {
1274 if parser.try_parse_section(line)
1275 && parser.section.to_lowercase() == sec_lower
1276 && parser.subsection.as_deref() == subsection
1277 {
1278 return idx;
1279 }
1280 }
1281
1282 let header = match subsection {
1284 Some(sub) => {
1285 let escaped = escape_subsection(sub);
1286 format!("[{} \"{}\"]", section, escaped)
1287 }
1288 None => format!("[{}]", section),
1289 };
1290 self.raw_lines.push(header);
1291 self.raw_lines.len() - 1
1292 }
1293
1294 fn find_or_create_section_preserving_case(
1297 &mut self,
1298 section: &str,
1299 subsection: Option<&str>,
1300 raw_section: &str,
1301 raw_subsection: Option<&str>,
1302 ) -> usize {
1303 let sec_lower = section.to_lowercase();
1304 let mut parser = Parser::new();
1305
1306 for (idx, line) in self.raw_lines.iter().enumerate() {
1307 if parser.try_parse_section(line)
1308 && parser.section.to_lowercase() == sec_lower
1309 && parser.subsection.as_deref() == subsection
1310 {
1311 return idx;
1312 }
1313 }
1314
1315 let header = match raw_subsection {
1317 Some(sub) => {
1318 let escaped = escape_subsection(sub);
1319 format!("[{} \"{}\"]", raw_section, escaped)
1320 }
1321 None => format!("[{}]", raw_section),
1322 };
1323 self.raw_lines.push(header);
1324 self.raw_lines.len() - 1
1325 }
1326
1327 fn last_line_in_section(&self, section_line: usize) -> usize {
1329 let mut last = section_line;
1330 for idx in (section_line + 1)..self.raw_lines.len() {
1331 let trimmed = self.raw_lines[idx].trim();
1332 if trimmed.starts_with('[') {
1333 break;
1334 }
1335 last = idx;
1336 }
1337 last
1338 }
1339}
1340
1341impl ConfigSet {
1344 #[must_use]
1346 pub fn new() -> Self {
1347 Self {
1348 entries: Vec::new(),
1349 }
1350 }
1351
1352 #[must_use]
1354 pub fn entries(&self) -> &[ConfigEntry] {
1355 &self.entries
1356 }
1357
1358 pub fn merge(&mut self, file: &ConfigFile) {
1363 self.entries.extend(file.entries.iter().cloned());
1364 }
1365
1366 pub fn add_command_override(&mut self, key: &str, value: &str) -> Result<()> {
1368 let canon = canonical_key(key)?;
1369 self.entries.push(ConfigEntry {
1370 key: canon,
1371 value: Some(value.to_owned()),
1372 scope: ConfigScope::Command,
1373 file: None,
1374 line: 0,
1375 });
1376 Ok(())
1377 }
1378
1379 #[must_use]
1390 pub fn get(&self, key: &str) -> Option<String> {
1391 let canon = canonical_key(key).ok()?;
1392 self.entries
1393 .iter()
1394 .rev()
1395 .find(|e| e.key == canon)
1396 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
1397 }
1398
1399 #[must_use]
1404 pub fn get_last_entry(&self, key: &str) -> Option<ConfigEntry> {
1405 let canon = canonical_key(key).ok()?;
1406 self.entries.iter().rev().find(|e| e.key == canon).cloned()
1407 }
1408
1409 #[must_use]
1411 pub fn get_all(&self, key: &str) -> Vec<String> {
1412 let canon = match canonical_key(key) {
1413 Ok(c) => c,
1414 Err(_) => return Vec::new(),
1415 };
1416 self.entries
1417 .iter()
1418 .filter(|e| e.key == canon)
1419 .map(|e| e.value.clone().unwrap_or_default())
1420 .collect()
1421 }
1422
1423 pub fn get_bool(&self, key: &str) -> Option<std::result::Result<bool, String>> {
1426 self.get(key).map(|v| parse_bool(&v))
1427 }
1428
1429 #[must_use]
1435 pub fn quote_path_fully(&self) -> bool {
1436 let from_key = |key: &str| self.get_bool(key).and_then(|r| r.ok());
1437 from_key("core.quotepath")
1438 .or_else(|| from_key("core.quotePath"))
1439 .unwrap_or(true)
1440 }
1441
1442 #[must_use]
1445 pub fn effective_log_refs_config(&self, git_dir: &Path) -> refs::LogRefsConfig {
1446 if let Some(v) = self.get("core.logAllRefUpdates") {
1447 let lower = v.trim().to_ascii_lowercase();
1448 let parsed = match lower.as_str() {
1449 "always" => Some(refs::LogRefsConfig::Always),
1450 "1" | "true" | "yes" | "on" => Some(refs::LogRefsConfig::Normal),
1451 "0" | "false" | "no" | "off" | "never" => Some(refs::LogRefsConfig::None),
1452 _ => None,
1453 };
1454 if let Some(c) = parsed {
1455 return c;
1456 }
1457 }
1458 refs::effective_log_refs_config(git_dir)
1459 }
1460
1461 pub fn get_i64(&self, key: &str) -> Option<std::result::Result<i64, String>> {
1463 self.get(key).map(|v| parse_i64(&v))
1464 }
1465
1466 pub fn get_regexp(&self, pattern: &str) -> std::result::Result<Vec<&ConfigEntry>, String> {
1471 let re = regex::Regex::new(pattern).map_err(|e| format!("invalid key pattern: {e}"))?;
1472 Ok(self
1473 .entries
1474 .iter()
1475 .filter(|e| re.is_match(&e.key))
1476 .collect())
1477 }
1478
1479 pub fn load(git_dir: Option<&Path>, include_system: bool) -> Result<Self> {
1490 let mut opts = LoadConfigOptions::default();
1491 opts.include_system = include_system;
1492 opts.include_ctx.git_dir = git_dir.map(PathBuf::from);
1493 Self::load_with_options(git_dir, &opts)
1494 }
1495
1496 pub fn load_with_options(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Result<Self> {
1500 let mut set = Self::new();
1501 let proc = opts.process_includes;
1502 let ctx = opts.include_ctx.clone();
1503
1504 if opts.include_system && std::env::var("GIT_CONFIG_NOSYSTEM").is_err() {
1506 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
1507 .map(std::path::PathBuf::from)
1508 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
1509 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
1510 Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?;
1511 }
1512 }
1513
1514 for path in global_config_paths() {
1516 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
1517 Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?;
1518 }
1519 }
1520
1521 if let Some(gd) = git_dir {
1523 let local_path = gd.join("config");
1524 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
1525 Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?;
1526 }
1527
1528 let wt_path = gd.join("config.worktree");
1530 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
1531 Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?;
1532 }
1533 }
1534
1535 if let Ok(path) = std::env::var("GIT_CONFIG") {
1537 if let Ok(Some(f)) = ConfigFile::from_path(Path::new(&path), ConfigScope::Command) {
1538 if proc {
1539 Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?;
1540 } else {
1541 set.merge(&f);
1542 }
1543 }
1544 }
1545
1546 if let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") {
1548 if let Ok(count) = count_str.parse::<usize>() {
1549 for i in 0..count {
1550 let key_var = format!("GIT_CONFIG_KEY_{i}");
1551 let val_var = format!("GIT_CONFIG_VALUE_{i}");
1552 if let (Ok(key), Ok(val)) = (std::env::var(&key_var), std::env::var(&val_var)) {
1553 let _ = set.add_command_override(&key, &val);
1554 }
1555 }
1556 }
1557 }
1558
1559 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
1561 if proc && opts.command_includes && !params.trim().is_empty() {
1562 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
1563 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, ¶ms)?;
1564 Self::merge_with_includes(&mut set, &cmd_file, proc, 0, &ctx)?;
1565 } else if !params.trim().is_empty() {
1566 for entry in parse_config_parameters(¶ms) {
1567 if let Some((key, val)) = entry.split_once('=') {
1568 let _ = set.add_command_override(key.trim(), val);
1569 } else {
1570 let _ = set.add_command_override(entry.trim(), "true");
1571 }
1572 }
1573 }
1574 }
1575
1576 Ok(set)
1577 }
1578
1579 pub fn read_early_config(git_dir: Option<&Path>, key: &str) -> Result<Vec<String>> {
1591 let mut set = Self::new();
1592 let ctx = IncludeContext {
1593 git_dir: git_dir.map(PathBuf::from),
1594 command_line_relative_include_is_error: false,
1595 };
1596
1597 if std::env::var("GIT_CONFIG_NOSYSTEM").is_err() {
1599 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
1600 .map(std::path::PathBuf::from)
1601 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
1602 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
1603 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1604 }
1605 }
1606
1607 for path in global_config_paths() {
1609 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
1610 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1611 }
1612 }
1613
1614 if let Some(gd) = git_dir {
1615 let common_dir = crate::repo::common_git_dir_for_config(gd);
1616 let local_path = common_dir.join("config");
1618 if let Some(msg) = crate::repo::early_config_ignore_repo_reason(&common_dir) {
1619 eprintln!("warning: ignoring git dir '{}': {}", gd.display(), msg);
1620 } else if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
1621 set.merge_file_with_includes(&f, true, &ctx)?;
1622 }
1623
1624 let wt_path = gd.join("config.worktree");
1626 if crate::repo::worktree_config_enabled(&common_dir) {
1627 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
1628 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1629 }
1630 }
1631 }
1632
1633 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
1635 if !params.trim().is_empty() {
1636 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
1637 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, ¶ms)?;
1638 Self::merge_with_includes(&mut set, &cmd_file, true, 0, &ctx)?;
1639 }
1640 }
1641
1642 Ok(set.get_all(key))
1643 }
1644
1645 pub fn merge_file_with_includes(
1650 &mut self,
1651 file: &ConfigFile,
1652 process_includes: bool,
1653 ctx: &IncludeContext,
1654 ) -> Result<()> {
1655 Self::merge_with_includes(self, file, process_includes, 0, ctx)
1656 }
1657
1658 pub fn load_repo_local_only(git_dir: &Path) -> Result<Self> {
1664 let mut set = Self::new();
1665 let local_path = git_dir.join("config");
1666 let ctx = IncludeContext {
1667 git_dir: Some(git_dir.to_path_buf()),
1668 command_line_relative_include_is_error: false,
1669 };
1670 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
1671 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1672 }
1673 Ok(set)
1674 }
1675
1676 pub fn load_protected(include_system: bool) -> Result<Self> {
1687 let mut set = Self::new();
1688 let ctx = IncludeContext {
1689 git_dir: None,
1690 command_line_relative_include_is_error: false,
1691 };
1692
1693 if include_system && std::env::var("GIT_CONFIG_NOSYSTEM").is_err() {
1694 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
1695 .map(std::path::PathBuf::from)
1696 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
1697 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
1698 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1699 }
1700 }
1701
1702 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
1703 let path = PathBuf::from(p);
1704 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
1705 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1706 }
1707 } else {
1708 let mut global_paths = Vec::new();
1709 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
1710 global_paths.push(PathBuf::from(xdg).join("git/config"));
1711 } else if let Some(home) = home_dir() {
1712 global_paths.push(home.join(".config/git/config"));
1713 }
1714 if let Some(home) = home_dir() {
1715 global_paths.push(home.join(".gitconfig"));
1716 }
1717 for path in global_paths {
1718 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
1719 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1720 }
1721 }
1722 }
1723
1724 if let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") {
1725 if let Ok(count) = count_str.parse::<usize>() {
1726 for i in 0..count {
1727 let key_var = format!("GIT_CONFIG_KEY_{i}");
1728 let val_var = format!("GIT_CONFIG_VALUE_{i}");
1729 if let (Ok(key), Ok(val)) = (std::env::var(&key_var), std::env::var(&val_var)) {
1730 let _ = set.add_command_override(&key, &val);
1731 }
1732 }
1733 }
1734 }
1735
1736 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
1737 for entry in parse_config_parameters(¶ms) {
1738 if let Some((key, val)) = entry.split_once('=') {
1739 let _ = set.add_command_override(key.trim(), val);
1740 } else {
1741 let _ = set.add_command_override(entry.trim(), "true");
1742 }
1743 }
1744 }
1745
1746 Ok(set)
1747 }
1748
1749 fn merge_with_includes(
1751 set: &mut Self,
1752 file: &ConfigFile,
1753 process_includes: bool,
1754 depth: usize,
1755 ctx: &IncludeContext,
1756 ) -> Result<()> {
1757 const MAX_INCLUDE_DEPTH: usize = 10;
1760 if depth > MAX_INCLUDE_DEPTH {
1761 return Err(Error::ConfigError(
1762 "exceeded maximum include depth".to_owned(),
1763 ));
1764 }
1765 let mut includes: Vec<(String, Option<String>)> = Vec::new();
1767
1768 for entry in &file.entries {
1769 if entry.key == "include.path" {
1770 if let Some(ref val) = entry.value {
1771 includes.push((val.clone(), None));
1772 }
1773 } else if entry.key.starts_with("includeif.") && entry.key.ends_with(".path") {
1774 let mid = &entry.key["includeif.".len()..entry.key.len() - ".path".len()];
1776 if let Some(ref val) = entry.value {
1777 includes.push((val.clone(), Some(mid.to_owned())));
1778 }
1779 }
1780 }
1781
1782 set.merge(file);
1784
1785 if process_includes {
1787 for (inc_path, condition) in includes {
1788 if let Some(ref cond) = condition {
1789 if !evaluate_include_condition(cond, file, ctx) {
1790 continue;
1791 }
1792 }
1793
1794 let resolved = match resolve_include_file_path(&inc_path, file, ctx) {
1795 Ok(p) => p,
1796 Err(Error::ConfigError(msg)) if msg.is_empty() => continue,
1797 Err(e) => return Err(e),
1798 };
1799 if let Ok(Some(inc_file)) = ConfigFile::from_path(&resolved, file.scope) {
1800 Self::merge_with_includes(set, &inc_file, process_includes, depth + 1, ctx)?;
1801 }
1802 }
1803 }
1804
1805 Ok(())
1806 }
1807}
1808
1809pub fn parse_bool(s: &str) -> std::result::Result<bool, String> {
1821 match s.to_lowercase().as_str() {
1822 "true" | "yes" | "on" => Ok(true),
1823 "" => Ok(false),
1824 "false" | "no" | "off" => Ok(false),
1825 _ => {
1826 if let Ok(n) = s.parse::<i64>() {
1828 return Ok(n != 0);
1829 }
1830 Err(format!("bad boolean config value '{s}'"))
1831 }
1832 }
1833}
1834
1835pub fn parse_i64(s: &str) -> std::result::Result<i64, String> {
1837 let s = s.trim();
1838 if s.is_empty() {
1839 return Err("empty integer value".to_owned());
1840 }
1841
1842 let (num_str, multiplier) = match s.as_bytes().last() {
1843 Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
1844 Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
1845 Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
1846 _ => (s, 1_i64),
1847 };
1848
1849 let base: i64 = num_str
1850 .parse()
1851 .map_err(|_| format!("invalid integer: '{s}'"))?;
1852 base.checked_mul(multiplier)
1853 .ok_or_else(|| format!("integer overflow: '{s}'"))
1854}
1855
1856#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1858pub enum GitConfigIntStrictError {
1859 InvalidUnit,
1861 OutOfRange,
1863}
1864
1865pub fn parse_git_config_int_strict(raw: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
1869 let s = raw.trim();
1870 if s.is_empty() {
1871 return Err(GitConfigIntStrictError::InvalidUnit);
1872 }
1873
1874 let bytes = s.as_bytes();
1875 let mut idx = 0usize;
1876 if matches!(bytes.first(), Some(b'+') | Some(b'-')) {
1877 idx = 1;
1878 }
1879 if idx >= bytes.len() {
1880 return Err(GitConfigIntStrictError::InvalidUnit);
1881 }
1882 let digit_start = idx;
1883 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
1884 idx += 1;
1885 }
1886 if idx == digit_start {
1887 return Err(GitConfigIntStrictError::InvalidUnit);
1888 }
1889
1890 let num_part =
1891 std::str::from_utf8(&bytes[..idx]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
1892 let suffix =
1893 std::str::from_utf8(&bytes[idx..]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
1894 let mult: i64 = match suffix {
1895 "" => 1,
1896 "k" | "K" => 1024,
1897 "m" | "M" => 1024 * 1024,
1898 "g" | "G" => 1024_i64
1899 .checked_mul(1024)
1900 .and_then(|x| x.checked_mul(1024))
1901 .ok_or(GitConfigIntStrictError::OutOfRange)?,
1902 _ => return Err(GitConfigIntStrictError::InvalidUnit),
1903 };
1904
1905 let val: i64 = num_part
1906 .parse()
1907 .map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
1908 val.checked_mul(mult)
1909 .ok_or(GitConfigIntStrictError::OutOfRange)
1910}
1911
1912const DIFF_CONTEXT_KEY: &str = "diff.context";
1913
1914fn format_bad_numeric_diff_context(
1915 value: &str,
1916 err: GitConfigIntStrictError,
1917 entry: &ConfigEntry,
1918) -> String {
1919 let detail = match err {
1920 GitConfigIntStrictError::InvalidUnit => "invalid unit",
1921 GitConfigIntStrictError::OutOfRange => "out of range",
1922 };
1923 if entry.scope == ConfigScope::Command || entry.file.is_none() {
1924 return format!(
1925 "fatal: bad numeric config value '{value}' for '{DIFF_CONTEXT_KEY}': {detail}"
1926 );
1927 }
1928 let path = entry
1929 .file
1930 .as_deref()
1931 .map(config_error_path_display)
1932 .unwrap_or_default();
1933 format!("fatal: bad numeric config value '{value}' for '{DIFF_CONTEXT_KEY}' in file {path}: {detail}")
1934}
1935
1936fn format_bad_diff_context_variable(entry: &ConfigEntry) -> String {
1937 if entry.scope == ConfigScope::Command || entry.file.is_none() {
1938 return format!("fatal: unable to parse '{DIFF_CONTEXT_KEY}' from command-line config");
1939 }
1940 let path = entry
1941 .file
1942 .as_deref()
1943 .map(config_error_path_display)
1944 .unwrap_or_default();
1945 format!(
1946 "fatal: bad config variable '{DIFF_CONTEXT_KEY}' in file '{path}' at line {}",
1947 entry.line
1948 )
1949}
1950
1951pub fn resolve_diff_context_lines(cfg: &ConfigSet) -> std::result::Result<Option<usize>, String> {
1956 let Some(entry) = cfg.get_last_entry(DIFF_CONTEXT_KEY) else {
1957 return Ok(None);
1958 };
1959 let value_src = entry.value.as_deref().unwrap_or("").trim();
1960 match parse_git_config_int_strict(value_src) {
1961 Ok(n) if n < 0 => Err(format_bad_diff_context_variable(&entry)),
1962 Ok(n) => Ok(Some(usize::try_from(n).map_err(|_| {
1963 format_bad_numeric_diff_context(value_src, GitConfigIntStrictError::OutOfRange, &entry)
1964 })?)),
1965 Err(e) => Err(format_bad_numeric_diff_context(value_src, e, &entry)),
1966 }
1967}
1968
1969pub fn parse_color(s: &str) -> std::result::Result<String, String> {
1976 const COLOR_BACKGROUND_OFFSET: i32 = 10;
1977 const COLOR_FOREGROUND_ANSI: i32 = 30;
1978 const COLOR_FOREGROUND_RGB: i32 = 38;
1979 const COLOR_FOREGROUND_256: i32 = 38;
1980 const COLOR_FOREGROUND_BRIGHT_ANSI: i32 = 90;
1981
1982 #[derive(Clone, Copy, Default)]
1983 struct Color {
1984 kind: u8,
1985 value: u8,
1986 red: u8,
1987 green: u8,
1988 blue: u8,
1989 }
1990
1991 const COLOR_UNSPECIFIED: u8 = 0;
1992 const COLOR_NORMAL: u8 = 1;
1993 const COLOR_ANSI: u8 = 2;
1994 const COLOR_256: u8 = 3;
1995 const COLOR_RGB: u8 = 4;
1996
1997 fn color_empty(c: &Color) -> bool {
1998 c.kind == COLOR_UNSPECIFIED || c.kind == COLOR_NORMAL
1999 }
2000
2001 fn parse_ansi_color(name: &str) -> Option<Color> {
2002 let color_names = [
2003 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
2004 ];
2005 let color_offset = COLOR_FOREGROUND_ANSI;
2006
2007 if name.eq_ignore_ascii_case("default") {
2008 return Some(Color {
2009 kind: COLOR_ANSI,
2010 value: (9 + color_offset) as u8,
2011 ..Default::default()
2012 });
2013 }
2014
2015 let (name, color_offset) = if name.len() >= 6 && name[..6].eq_ignore_ascii_case("bright") {
2016 (&name[6..], COLOR_FOREGROUND_BRIGHT_ANSI)
2017 } else {
2018 (name, COLOR_FOREGROUND_ANSI)
2019 };
2020
2021 for (i, cn) in color_names.iter().enumerate() {
2022 if name.eq_ignore_ascii_case(cn) {
2023 return Some(Color {
2024 kind: COLOR_ANSI,
2025 value: (i as i32 + color_offset) as u8,
2026 ..Default::default()
2027 });
2028 }
2029 }
2030 None
2031 }
2032
2033 fn hex_val(b: u8) -> Option<u8> {
2034 match b {
2035 b'0'..=b'9' => Some(b - b'0'),
2036 b'a'..=b'f' => Some(b - b'a' + 10),
2037 b'A'..=b'F' => Some(b - b'A' + 10),
2038 _ => None,
2039 }
2040 }
2041
2042 fn get_hex_color(chars: &[u8], width: usize) -> Option<(u8, usize)> {
2043 assert!(width == 1 || width == 2);
2044 if chars.len() < width {
2045 return None;
2046 }
2047 let v = if width == 2 {
2048 let hi = hex_val(chars[0])?;
2049 let lo = hex_val(chars[1])?;
2050 (hi << 4) | lo
2051 } else {
2052 let n = hex_val(chars[0])?;
2053 (n << 4) | n
2054 };
2055 Some((v, width))
2056 }
2057
2058 fn parse_single_color(word: &str) -> Option<Color> {
2059 if word.eq_ignore_ascii_case("normal") {
2060 return Some(Color {
2061 kind: COLOR_NORMAL,
2062 ..Default::default()
2063 });
2064 }
2065
2066 let bytes = word.as_bytes();
2067 if (bytes.len() == 7 || bytes.len() == 4) && bytes.first() == Some(&b'#') {
2068 let width = if bytes.len() == 7 { 2 } else { 1 };
2069 let mut idx = 1;
2070 let (r, n1) = get_hex_color(&bytes[idx..], width)?;
2071 idx += n1;
2072 let (g, n2) = get_hex_color(&bytes[idx..], width)?;
2073 idx += n2;
2074 let (b, n3) = get_hex_color(&bytes[idx..], width)?;
2075 idx += n3;
2076 if idx != bytes.len() {
2077 return None;
2078 }
2079 return Some(Color {
2080 kind: COLOR_RGB,
2081 red: r,
2082 green: g,
2083 blue: b,
2084 ..Default::default()
2085 });
2086 }
2087
2088 if let Some(c) = parse_ansi_color(word) {
2089 return Some(c);
2090 }
2091
2092 let Ok(val) = word.parse::<i64>() else {
2093 return None;
2094 };
2095 if val < -1 {
2096 return None;
2097 }
2098 if val < 0 {
2099 return Some(Color {
2100 kind: COLOR_NORMAL,
2101 ..Default::default()
2102 });
2103 }
2104 if val < 8 {
2105 return Some(Color {
2106 kind: COLOR_ANSI,
2107 value: (val as i32 + COLOR_FOREGROUND_ANSI) as u8,
2108 ..Default::default()
2109 });
2110 }
2111 if val < 16 {
2112 return Some(Color {
2113 kind: COLOR_ANSI,
2114 value: (val as i32 - 8 + COLOR_FOREGROUND_BRIGHT_ANSI) as u8,
2115 ..Default::default()
2116 });
2117 }
2118 if val < 256 {
2119 return Some(Color {
2120 kind: COLOR_256,
2121 value: val as u8,
2122 ..Default::default()
2123 });
2124 }
2125 None
2126 }
2127
2128 fn parse_attr(word: &str) -> Option<u8> {
2129 const ATTRS: [(&str, u8, u8); 8] = [
2130 ("bold", 1, 22),
2131 ("dim", 2, 22),
2132 ("italic", 3, 23),
2133 ("ul", 4, 24),
2134 ("underline", 4, 24),
2135 ("blink", 5, 25),
2136 ("reverse", 7, 27),
2137 ("strike", 9, 29),
2138 ];
2139
2140 let mut negate = false;
2141 let mut rest = word;
2142 if let Some(stripped) = rest.strip_prefix("no") {
2143 negate = true;
2144 rest = stripped;
2145 if let Some(s) = rest.strip_prefix('-') {
2146 rest = s;
2147 }
2148 }
2149
2150 for (name, val, neg) in ATTRS {
2151 if rest == name {
2152 return Some(if negate { neg } else { val });
2153 }
2154 }
2155 None
2156 }
2157
2158 fn append_color_output(out: &mut String, c: &Color, background: bool) {
2159 let offset = if background {
2160 COLOR_BACKGROUND_OFFSET
2161 } else {
2162 0
2163 };
2164 match c.kind {
2165 COLOR_UNSPECIFIED | COLOR_NORMAL => {}
2166 COLOR_ANSI => {
2167 use std::fmt::Write;
2168 let _ = write!(out, "{}", i32::from(c.value) + offset);
2169 }
2170 COLOR_256 => {
2171 use std::fmt::Write;
2172 let _ = write!(out, "{};5;{}", COLOR_FOREGROUND_256 + offset, c.value);
2173 }
2174 COLOR_RGB => {
2175 use std::fmt::Write;
2176 let _ = write!(
2177 out,
2178 "{};2;{};{};{}",
2179 COLOR_FOREGROUND_RGB + offset,
2180 c.red,
2181 c.green,
2182 c.blue
2183 );
2184 }
2185 _ => {}
2186 }
2187 }
2188
2189 let s = s.trim();
2190 if s.is_empty() {
2191 return Ok(String::new());
2192 }
2193
2194 let mut has_reset = false;
2195 let mut attr: u64 = 0;
2196 let mut fg = Color::default();
2197 let mut bg = Color::default();
2198 fg.kind = COLOR_UNSPECIFIED;
2199 bg.kind = COLOR_UNSPECIFIED;
2200
2201 for word in s.split_whitespace() {
2202 if word.eq_ignore_ascii_case("reset") {
2203 has_reset = true;
2204 continue;
2205 }
2206
2207 if let Some(c) = parse_single_color(word) {
2208 if fg.kind == COLOR_UNSPECIFIED {
2209 fg = c;
2210 continue;
2211 }
2212 if bg.kind == COLOR_UNSPECIFIED {
2213 bg = c;
2214 continue;
2215 }
2216 return Err(format!("bad color value '{s}'"));
2217 }
2218
2219 if let Some(code) = parse_attr(word) {
2220 attr |= 1u64 << u64::from(code);
2221 continue;
2222 }
2223
2224 return Err(format!("bad color value '{s}'"));
2225 }
2226
2227 if !has_reset && attr == 0 && color_empty(&fg) && color_empty(&bg) {
2228 return Err(format!("bad color value '{s}'"));
2229 }
2230
2231 let mut out = String::from("\x1b[");
2232 let mut sep = if has_reset { 1u32 } else { 0u32 };
2233
2234 let mut attr_bits = attr;
2235 let mut i = 0u32;
2236 while attr_bits != 0 {
2237 let bit = 1u64 << i;
2238 if attr_bits & bit == 0 {
2239 i += 1;
2240 continue;
2241 }
2242 attr_bits &= !bit;
2243 if sep > 0 {
2244 out.push(';');
2245 }
2246 sep += 1;
2247 use std::fmt::Write;
2248 let _ = write!(out, "{i}");
2249 i += 1;
2250 }
2251
2252 if !color_empty(&fg) {
2253 if sep > 0 {
2254 out.push(';');
2255 }
2256 sep += 1;
2257 append_color_output(&mut out, &fg, false);
2258 }
2259 if !color_empty(&bg) {
2260 if sep > 0 {
2261 out.push(';');
2262 }
2263 append_color_output(&mut out, &bg, true);
2264 }
2265 out.push('m');
2266 Ok(out)
2267}
2268
2269pub fn url_matches(pattern_url: &str, target_url: &str) -> bool {
2271 let pattern = pattern_url.trim_end_matches('/');
2272 let target = target_url.trim_end_matches('/');
2273 if target == pattern {
2274 return true;
2275 }
2276 if let Some(rest) = target.strip_prefix(pattern) {
2277 return rest.starts_with('/') || rest.is_empty();
2278 }
2279 let pattern_slash = format!("{}/", pattern);
2280 target.starts_with(&pattern_slash)
2281}
2282
2283pub fn get_urlmatch_entries<'a>(
2285 entries: &'a [ConfigEntry],
2286 section: &str,
2287 variable: &str,
2288 url: &str,
2289) -> Vec<&'a ConfigEntry> {
2290 let section_lower = section.to_lowercase();
2291 let variable_lower = variable.to_lowercase();
2292 let mut matches: Vec<(usize, &'a ConfigEntry)> = Vec::new();
2293
2294 for entry in entries {
2295 let key = &entry.key;
2296 let first_dot = match key.find('.') {
2297 Some(i) => i,
2298 None => continue,
2299 };
2300 let last_dot = match key.rfind('.') {
2301 Some(i) => i,
2302 None => continue,
2303 };
2304 let entry_section = &key[..first_dot];
2305 let entry_variable = &key[last_dot + 1..];
2306 if entry_section.to_lowercase() != section_lower
2307 || entry_variable.to_lowercase() != variable_lower
2308 {
2309 continue;
2310 }
2311 if first_dot == last_dot {
2312 matches.push((0, entry));
2313 } else {
2314 let subsection = &key[first_dot + 1..last_dot];
2315 if url_matches(subsection, url) {
2316 matches.push((subsection.len(), entry));
2317 }
2318 }
2319 }
2320 matches.sort_by(|a, b| a.0.cmp(&b.0));
2321 matches.into_iter().map(|(_, e)| e).collect()
2322}
2323
2324pub fn get_urlmatch_all_in_section(
2326 entries: &[ConfigEntry],
2327 section: &str,
2328 url: &str,
2329) -> Vec<(String, String, ConfigScope)> {
2330 let section_lower = section.to_lowercase();
2331 let mut matches: Vec<(String, usize, String, String, ConfigScope)> = Vec::new();
2332
2333 for entry in entries {
2334 let key = &entry.key;
2335 let first_dot = match key.find('.') {
2336 Some(i) => i,
2337 None => continue,
2338 };
2339 let last_dot = match key.rfind('.') {
2340 Some(i) => i,
2341 None => continue,
2342 };
2343 let entry_section = &key[..first_dot];
2344 if entry_section.to_lowercase() != section_lower {
2345 continue;
2346 }
2347 let entry_variable = &key[last_dot + 1..];
2348 let val = entry.value.as_deref().unwrap_or("true");
2349 if first_dot == last_dot {
2350 let canonical = format!("{}.{}", section_lower, entry_variable);
2351 matches.push((
2352 entry_variable.to_lowercase(),
2353 0,
2354 val.to_owned(),
2355 canonical,
2356 entry.scope,
2357 ));
2358 } else {
2359 let subsection = &key[first_dot + 1..last_dot];
2360 if url_matches(subsection, url) {
2361 let canonical = format!("{}.{}", section_lower, entry_variable);
2362 matches.push((
2363 entry_variable.to_lowercase(),
2364 subsection.len(),
2365 val.to_owned(),
2366 canonical,
2367 entry.scope,
2368 ));
2369 }
2370 }
2371 }
2372
2373 let mut best: std::collections::BTreeMap<String, (usize, String, String, ConfigScope)> =
2374 std::collections::BTreeMap::new();
2375 for (var, specificity, val, canonical, scope) in matches {
2376 let entry = best
2377 .entry(var)
2378 .or_insert((0, String::new(), String::new(), scope));
2379 if specificity >= entry.0 {
2380 *entry = (specificity, val, canonical, scope);
2381 }
2382 }
2383 best.into_values()
2384 .map(|(_, val, canonical, scope)| (canonical, val, scope))
2385 .collect()
2386}
2387
2388pub fn parse_path(s: &str) -> String {
2392 if let Some(rest) = s.strip_prefix("~/") {
2393 if let Some(home) = home_dir() {
2394 return home.join(rest).to_string_lossy().to_string();
2395 }
2396 }
2397 s.to_owned()
2398}
2399
2400pub fn parse_path_optional(s: &str) -> Option<String> {
2405 if let Some(rest) = s.strip_prefix(":(optional)") {
2406 let resolved = parse_path(rest);
2407 if std::path::Path::new(&resolved).exists() {
2408 Some(resolved)
2409 } else {
2410 None }
2412 } else {
2413 Some(parse_path(s))
2414 }
2415}
2416
2417fn parse_config_parameters(raw: &str) -> Vec<String> {
2428 let mut out: Vec<String> = Vec::new();
2429 let mut buf = String::new();
2430 let mut in_single = false;
2431 let mut in_double = false;
2432
2433 let mut chars = raw.chars().peekable();
2434 while let Some(ch) = chars.next() {
2435 if in_single {
2436 if ch == '\'' {
2437 in_single = false;
2438 } else {
2439 buf.push(ch);
2440 }
2441 continue;
2442 }
2443 if in_double {
2444 if ch == '"' {
2445 in_double = false;
2446 continue;
2447 }
2448 if ch == '\\' {
2449 if let Some(next) = chars.next() {
2450 let mapped = match next {
2451 'n' => '\n',
2452 't' => '\t',
2453 'r' => '\r',
2454 '"' => '"',
2455 '\\' => '\\',
2456 other => other,
2457 };
2458 buf.push(mapped);
2459 }
2460 continue;
2461 }
2462 buf.push(ch);
2463 continue;
2464 }
2465
2466 if ch == '\'' {
2467 in_single = true;
2468 continue;
2469 }
2470 if ch == '"' {
2471 in_double = true;
2472 continue;
2473 }
2474
2475 if ch.is_whitespace() {
2476 if !buf.is_empty() {
2477 out.push(std::mem::take(&mut buf));
2478 }
2479 continue;
2480 }
2481
2482 buf.push(ch);
2483 }
2484
2485 if !buf.is_empty() {
2486 out.push(buf);
2487 }
2488
2489 out
2490}
2491
2492pub fn global_config_paths_pub() -> Vec<PathBuf> {
2495 global_config_paths()
2496}
2497
2498fn global_config_paths() -> Vec<PathBuf> {
2499 let mut paths = Vec::new();
2500
2501 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
2503 paths.push(PathBuf::from(p));
2504 return paths;
2505 }
2506
2507 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
2509 paths.push(PathBuf::from(xdg).join("git/config"));
2510 } else if let Some(home) = home_dir() {
2511 paths.push(home.join(".config/git/config"));
2512 }
2513 if let Some(home) = home_dir() {
2514 paths.push(home.join(".gitconfig"));
2515 }
2516
2517 paths
2518}
2519
2520fn home_dir() -> Option<PathBuf> {
2522 std::env::var("HOME").ok().map(PathBuf::from)
2523}
2524
2525fn include_source_is_disk_file(file: &ConfigFile) -> bool {
2527 file.include_origin == ConfigIncludeOrigin::Disk
2528}
2529
2530fn resolve_include_file_path(
2534 path: &str,
2535 file: &ConfigFile,
2536 ctx: &IncludeContext,
2537) -> Result<PathBuf> {
2538 let expanded = parse_path(path);
2539 let p = Path::new(&expanded);
2540 if p.is_absolute() {
2541 return Ok(p.to_path_buf());
2542 }
2543 if !include_source_is_disk_file(file) {
2544 if file.include_origin == ConfigIncludeOrigin::CommandLine {
2545 if ctx.command_line_relative_include_is_error {
2546 return Err(Error::ConfigError(
2547 "relative config includes must come from files".to_owned(),
2548 ));
2549 }
2550 return Err(Error::ConfigError(String::new()));
2551 }
2552 return Err(Error::ConfigError(
2553 "relative config includes must come from files".to_owned(),
2554 ));
2555 }
2556 let base = match file.path.parent() {
2557 Some(p) if !p.as_os_str().is_empty() => p,
2558 Some(_) | None => Path::new("."),
2559 };
2560 Ok(base.join(p))
2561}
2562
2563fn is_dir_sep(b: u8) -> bool {
2564 b == b'/' || b == b'\\'
2565}
2566
2567fn add_trailing_starstar_for_dir(pat: &mut String) {
2568 let bytes = pat.as_bytes();
2569 if !bytes.is_empty() && is_dir_sep(*bytes.last().unwrap()) {
2570 pat.push_str("**");
2571 }
2572}
2573
2574fn prepare_gitdir_pattern(condition: &str, file: &ConfigFile) -> Result<(String, usize)> {
2576 let mut pat = parse_path(condition);
2578 if pat.starts_with("./") || pat.starts_with(".\\") {
2579 if !include_source_is_disk_file(file) {
2580 return Err(Error::ConfigError(
2581 "relative config include conditionals must come from files".to_owned(),
2582 ));
2583 }
2584 let parent = file.path.parent().ok_or_else(|| {
2585 Error::ConfigError(
2586 "relative config include conditionals must come from files".to_owned(),
2587 )
2588 })?;
2589 let real = parent.canonicalize().map_err(Error::Io)?;
2590 let mut dir = real.to_string_lossy().into_owned();
2591 if !dir.ends_with('/') && !dir.ends_with('\\') {
2592 dir.push('/');
2593 }
2594 let rest = &pat[2..];
2595 pat = format!("{dir}{rest}");
2596 let prefix_len = dir.len();
2597 add_trailing_starstar_for_dir(&mut pat);
2598 return Ok((pat, prefix_len));
2599 }
2600 let p = Path::new(&pat);
2601 if !p.is_absolute() {
2602 pat.insert_str(0, "**/");
2603 }
2604 add_trailing_starstar_for_dir(&mut pat);
2605 Ok((pat, 0))
2606}
2607
2608fn git_dir_match_texts(git_dir: &Path) -> (String, String) {
2610 let real = git_dir
2611 .canonicalize()
2612 .map(|p| p.to_string_lossy().into_owned())
2613 .unwrap_or_else(|_| git_dir.to_string_lossy().into_owned());
2614 let abs = if git_dir.is_absolute() {
2615 git_dir.to_string_lossy().into_owned()
2616 } else if let Ok(cwd) = std::env::current_dir() {
2617 cwd.join(git_dir).to_string_lossy().into_owned()
2618 } else {
2619 git_dir.to_string_lossy().into_owned()
2620 };
2621 (real, abs)
2622}
2623
2624fn include_by_gitdir(
2625 condition: &str,
2626 file: &ConfigFile,
2627 ctx: &IncludeContext,
2628 icase: bool,
2629) -> bool {
2630 let Some(git_dir) = ctx.git_dir.as_ref() else {
2631 return false;
2632 };
2633 let (pattern, prefix) = match prepare_gitdir_pattern(condition, file) {
2634 Ok(x) => x,
2635 Err(_) => return false,
2636 };
2637 let flags = WM_PATHNAME | if icase { WM_CASEFOLD } else { 0 };
2638 let (text_real, text_abs) = git_dir_match_texts(git_dir);
2639 let try_match = |text: &str| -> bool {
2640 let t = text.as_bytes();
2641 let p = pattern.as_bytes();
2642 if prefix > 0 {
2643 if t.len() < prefix {
2644 return false;
2645 }
2646 let pre = &p[..prefix];
2647 let te = &t[..prefix];
2648 let ok = if icase {
2649 pre.eq_ignore_ascii_case(te)
2650 } else {
2651 pre == te
2652 };
2653 if !ok {
2654 return false;
2655 }
2656 return wildmatch(&p[prefix..], &t[prefix..], flags);
2657 }
2658 wildmatch(p, t, flags)
2659 };
2660 if try_match(&text_real) {
2661 return true;
2662 }
2663 text_real != text_abs && try_match(&text_abs)
2664}
2665
2666fn current_branch_short_name(git_dir: Option<&Path>) -> Option<String> {
2667 let gd = git_dir?;
2668 let target = refs::read_symbolic_ref(gd, "HEAD").ok()??;
2669 let rest = target.strip_prefix("refs/heads/")?;
2670 Some(rest.to_owned())
2671}
2672
2673fn include_by_onbranch(condition: &str, ctx: &IncludeContext) -> bool {
2674 let Some(short) = current_branch_short_name(ctx.git_dir.as_deref()) else {
2675 return false;
2676 };
2677 let mut pattern = condition.to_owned();
2678 add_trailing_starstar_for_dir(&mut pattern);
2679 wildmatch(pattern.as_bytes(), short.as_bytes(), WM_PATHNAME)
2680}
2681
2682fn evaluate_include_condition(condition: &str, file: &ConfigFile, ctx: &IncludeContext) -> bool {
2686 if let Some(rest) = condition.strip_prefix("gitdir/i:") {
2687 return include_by_gitdir(rest, file, ctx, true);
2688 }
2689 if let Some(rest) = condition.strip_prefix("gitdir:") {
2690 return include_by_gitdir(rest, file, ctx, false);
2691 }
2692 if let Some(rest) = condition.strip_prefix("onbranch:") {
2693 return include_by_onbranch(rest, ctx);
2694 }
2695 false
2696}
2697
2698fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
2700 let first_dot = key
2701 .find('.')
2702 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
2703 let last_dot = key
2704 .rfind('.')
2705 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
2706
2707 let section = key[..first_dot].to_owned();
2708 let variable = key[last_dot + 1..].to_owned();
2709
2710 let subsection = if first_dot == last_dot {
2711 None
2712 } else {
2713 Some(key[first_dot + 1..last_dot].to_owned())
2714 };
2715
2716 Ok((section, subsection, variable))
2717}
2718
2719#[allow(dead_code)]
2721fn variable_name_from_key(key: &str) -> &str {
2722 match key.rfind('.') {
2723 Some(i) => &key[i + 1..],
2724 None => key,
2725 }
2726}
2727
2728fn parse_section_name(name: &str) -> (&str, Option<&str>) {
2732 match name.find('.') {
2733 Some(i) => (&name[..i], Some(&name[i + 1..])),
2734 None => (name, None),
2735 }
2736}
2737
2738fn raw_variable_name(raw_key: &str) -> &str {
2742 match raw_key.rfind('.') {
2743 Some(i) => &raw_key[i + 1..],
2744 None => raw_key,
2745 }
2746}
2747
2748fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
2753 let first_dot = match raw_key.find('.') {
2754 Some(i) => i,
2755 None => return (raw_key.to_owned(), None),
2756 };
2757 let last_dot = match raw_key.rfind('.') {
2759 Some(i) => i,
2760 None => return (raw_key[..first_dot].to_owned(), None),
2761 };
2762 let section = raw_key[..first_dot].to_owned();
2763 if first_dot == last_dot {
2764 (section, None)
2765 } else {
2766 let subsection = raw_key[first_dot + 1..last_dot].to_owned();
2767 (section, Some(subsection))
2768 }
2769}
2770
2771fn is_section_header_with_inline_entry(line: &str) -> bool {
2773 let trimmed = line.trim();
2774 if !trimmed.starts_with('[') {
2775 return false;
2776 }
2777 let end = match trimmed.find(']') {
2778 Some(i) => i,
2779 None => return false,
2780 };
2781 let after = trimmed[end + 1..].trim();
2782 !after.is_empty() && !after.starts_with('#') && !after.starts_with(';')
2784}
2785
2786fn extract_section_header(line: &str) -> String {
2789 let trimmed = line.trim();
2790 let end = match trimmed.find(']') {
2791 Some(i) => i,
2792 None => return line.to_owned(),
2793 };
2794 trimmed[..=end].to_owned()
2797}
2798
2799#[cfg(test)]
2800mod get_regexp_tests {
2801 use super::{ConfigFile, ConfigScope, ConfigSet};
2802 use std::path::Path;
2803
2804 fn set_from_snippet(text: &str) -> ConfigSet {
2805 let path = Path::new(".git/config");
2806 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
2807 let mut set = ConfigSet::new();
2808 set.merge(&file);
2809 set
2810 }
2811
2812 #[test]
2813 fn get_regexp_matches_section_prefix_like_git_config() {
2814 let text = r#"
2815[user]
2816 email = alice@example.com
2817 name = Alice
2818[core]
2819 bare = false
2820"#;
2821 let set = set_from_snippet(text);
2822 let keys: Vec<_> = set
2823 .get_regexp("user")
2824 .expect("valid pattern")
2825 .into_iter()
2826 .map(|e| e.key.as_str())
2827 .collect();
2828 assert!(keys.contains(&"user.email"));
2829 assert!(keys.contains(&"user.name"));
2830 assert!(!keys.iter().any(|k| k.starts_with("core.")));
2831 }
2832
2833 #[test]
2834 fn get_regexp_returns_all_multi_value_entries_in_order() {
2835 let text = r#"
2836[remote "origin"]
2837 url = https://example.com/repo.git
2838 fetch = +refs/heads/*:refs/remotes/origin/*
2839 push = +refs/heads/main:refs/heads/main
2840 push = +refs/heads/develop:refs/heads/develop
2841"#;
2842 let set = set_from_snippet(text);
2843 let matches = set.get_regexp("remote.origin").expect("valid pattern");
2844 let push_vals: Vec<_> = matches
2845 .iter()
2846 .filter(|e| e.key == "remote.origin.push")
2847 .map(|e| e.value.as_deref().unwrap_or(""))
2848 .collect();
2849 assert_eq!(push_vals.len(), 2);
2850 assert_eq!(push_vals[0], "+refs/heads/main:refs/heads/main");
2851 assert_eq!(push_vals[1], "+refs/heads/develop:refs/heads/develop");
2852 }
2853
2854 #[test]
2855 fn get_regexp_dot_matches_any_key() {
2856 let text = r#"
2857[a]
2858 x = 1
2859[b]
2860 y = 2
2861"#;
2862 let set = set_from_snippet(text);
2863 let m = set.get_regexp(".").expect("valid pattern");
2864 assert_eq!(m.len(), 2);
2865 }
2866
2867 #[test]
2868 fn get_regexp_no_match_returns_empty_vec() {
2869 let set = set_from_snippet("[user]\n\tname = x\n");
2870 let m = set.get_regexp("zzz").expect("valid pattern");
2871 assert!(m.is_empty());
2872 }
2873
2874 #[test]
2875 fn get_regexp_invalid_pattern_is_error() {
2876 let set = set_from_snippet("[user]\n\tname = x\n");
2877 let err = set.get_regexp("(").expect_err("unclosed group");
2878 assert!(err.contains("invalid key pattern"), "got: {err}");
2879 }
2880}