1use std::collections::HashMap;
31use std::fmt;
32use std::fs;
33use std::path::{Path, PathBuf};
34use std::sync::{Arc, Mutex, OnceLock};
35use std::time::SystemTime;
36
37use crate::error::{Error, Result};
38use crate::refs;
39use crate::wildmatch::{wildmatch, WM_CASEFOLD, WM_PATHNAME};
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub enum ConfigScope {
44 System,
46 Global,
48 Local,
50 Worktree,
52 Command,
54}
55
56impl fmt::Display for ConfigScope {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 match self {
59 Self::System => write!(f, "system"),
60 Self::Global => write!(f, "global"),
61 Self::Local => write!(f, "local"),
62 Self::Worktree => write!(f, "worktree"),
63 Self::Command => write!(f, "command"),
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct ConfigEntry {
71 pub key: String,
74 pub value: Option<String>,
76 pub scope: ConfigScope,
78 pub file: Option<PathBuf>,
80 pub line: usize,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum ConfigIncludeOrigin {
87 Disk,
89 Stdin,
91 CommandLine,
93 Blob,
95}
96
97#[derive(Debug, Clone)]
100pub struct ConfigFile {
101 pub path: PathBuf,
103 pub scope: ConfigScope,
105 pub entries: Vec<ConfigEntry>,
107 raw_lines: Vec<String>,
109 pub include_origin: ConfigIncludeOrigin,
111}
112
113#[derive(Debug, Clone, Default)]
118pub struct ConfigSet {
119 entries: Vec<ConfigEntry>,
121}
122
123#[derive(Debug, Clone, Default)]
125pub struct IncludeContext {
126 pub git_dir: Option<PathBuf>,
128 pub command_line_relative_include_is_error: bool,
130}
131
132#[derive(Debug, Clone)]
134pub struct LoadConfigOptions {
135 pub include_system: bool,
137 pub process_includes: bool,
139 pub command_includes: bool,
141 pub include_ctx: IncludeContext,
142}
143
144impl Default for LoadConfigOptions {
145 fn default() -> Self {
146 Self {
147 include_system: true,
148 process_includes: true,
149 command_includes: true,
150 include_ctx: IncludeContext::default(),
151 }
152 }
153}
154
155pub fn canonical_key(raw: &str) -> Result<String> {
171 if raw.contains('\n') || raw.contains('\r') {
173 return Err(Error::ConfigError(format!(
174 "invalid key: '{}'",
175 raw.replace('\n', "\\n")
176 )));
177 }
178
179 let first_dot = raw
180 .find('.')
181 .ok_or_else(|| Error::ConfigError(format!("key does not contain a section: '{raw}'")))?;
182 let last_dot = raw
183 .rfind('.')
184 .ok_or_else(|| Error::ConfigError(format!("key does not contain a section: '{raw}'")))?;
185
186 if last_dot == raw.len() - 1 {
187 return Err(Error::ConfigError(format!(
188 "key does not contain variable name: '{raw}'"
189 )));
190 }
191
192 let section = &raw[..first_dot];
193 let name = &raw[last_dot + 1..];
194
195 if section.is_empty() || !section.chars().all(|c| c.is_alphanumeric() || c == '-') {
197 return Err(Error::ConfigError(format!(
198 "invalid key (bad section): '{raw}'"
199 )));
200 }
201
202 if !name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
204 || !name.chars().all(|c| c.is_alphanumeric() || c == '-')
205 {
206 return Err(Error::ConfigError(format!(
207 "invalid key (bad variable name): '{raw}'"
208 )));
209 }
210
211 if first_dot == last_dot {
212 Ok(format!(
214 "{}.{}",
215 section.to_lowercase(),
216 name.to_lowercase()
217 ))
218 } else {
219 let subsection = &raw[first_dot + 1..last_dot];
221 Ok(format!(
222 "{}.{}.{}",
223 section.to_lowercase(),
224 subsection,
225 name.to_lowercase()
226 ))
227 }
228}
229
230#[must_use]
234pub fn config_file_display_for_error(path: &Path) -> String {
235 config_error_path_display(path)
236}
237
238fn config_error_path_display(path: &Path) -> String {
239 if path == Path::new("-") {
240 return "standard input".to_owned();
241 }
242 if path.file_name().and_then(|s| s.to_str()) == Some("config")
243 && path
244 .parent()
245 .and_then(|p| p.file_name())
246 .and_then(|s| s.to_str())
247 == Some(".git")
248 {
249 return ".git/config".to_owned();
250 }
251 path.display().to_string()
252}
253
254struct Parser {
256 section: String,
257 subsection: Option<String>,
258}
259
260impl Parser {
261 fn new() -> Self {
262 Self {
263 section: String::new(),
264 subsection: None,
265 }
266 }
267
268 fn make_key(&self, name: &str) -> String {
270 let sec = self.section.to_lowercase();
271 let var = name.to_lowercase();
272 match &self.subsection {
273 Some(sub) => format!("{sec}.{sub}.{var}"),
274 None => format!("{sec}.{var}"),
275 }
276 }
277
278 fn try_parse_section_with_remainder<'a>(
284 &mut self,
285 line: &'a str,
286 inline_remainder: &mut Option<&'a str>,
287 ) -> bool {
288 let trimmed = line.trim();
289 if !trimmed.starts_with('[') {
290 return false;
291 }
292 let end = {
296 let bytes = trimmed.as_bytes();
297 let mut i = 1; let mut in_quotes = false;
299 let mut found = None;
300 while i < bytes.len() {
301 if in_quotes {
302 if bytes[i] == b'\\' {
303 i += 2; continue;
305 }
306 if bytes[i] == b'"' {
307 in_quotes = false;
308 }
309 } else {
310 if bytes[i] == b'"' {
311 in_quotes = true;
312 }
313 if bytes[i] == b']' {
314 found = Some(i);
315 break;
316 }
317 }
318 i += 1;
319 }
320 match found {
321 Some(i) => i,
322 None => return false,
323 }
324 };
325 let inside = &trimmed[1..end];
326 if let Some(quote_start) = inside.find('"') {
328 self.section = inside[..quote_start].trim().to_owned();
329 let rest = &inside[quote_start + 1..];
330 let mut sub = String::new();
332 let mut chars = rest.chars();
333 while let Some(ch) = chars.next() {
334 if ch == '\\' {
335 if let Some(escaped) = chars.next() {
336 sub.push(escaped);
337 }
338 } else if ch == '"' {
339 break;
340 } else {
341 sub.push(ch);
342 }
343 }
344 self.subsection = Some(sub);
345 } else {
346 self.section = inside.trim().to_owned();
347 self.subsection = None;
348 }
349 let after = trimmed[end + 1..].trim();
351 if !after.is_empty() && !after.starts_with('#') && !after.starts_with(';') {
352 *inline_remainder = Some(after);
353 } else {
354 *inline_remainder = None;
355 }
356 true
357 }
358
359 fn try_parse_section(&mut self, line: &str) -> bool {
361 let mut _remainder = None;
362 self.try_parse_section_with_remainder(line, &mut _remainder)
363 }
364
365 fn try_parse_entry(&self, line: &str) -> Option<(String, Option<String>)> {
369 let trimmed = line.trim();
370 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
371 return None;
372 }
373 if trimmed.starts_with('[') {
374 return None;
375 }
376 if self.section.is_empty() {
377 return None;
378 }
379
380 if let Some(eq_pos) = trimmed.find('=') {
381 let raw_name = trimmed[..eq_pos].trim();
382 let raw_value = trimmed[eq_pos + 1..].trim();
383 let value = strip_inline_comment(raw_value);
385 let value = unescape_value(&value);
386 let key = self.make_key(raw_name);
387 Some((key, Some(value)))
388 } else {
389 let raw_name = strip_inline_comment(trimmed);
391 if raw_name.split_whitespace().count() > 1 {
392 return None;
393 }
394 let key = self.make_key(raw_name.trim());
395 Some((key, None))
396 }
397 }
398}
399
400fn entry_line_value_has_unclosed_quote(line: &str) -> bool {
410 let trimmed = line.trim();
411 let Some(eq_pos) = trimmed.find('=') else {
412 return false;
413 };
414 let raw_value = trimmed[eq_pos + 1..].trim_start();
415 let mut in_quote = false;
416 let mut last_was_backslash = false;
417 for ch in raw_value.chars() {
418 match ch {
419 '"' if !last_was_backslash => {
420 in_quote = !in_quote;
421 last_was_backslash = false;
422 }
423 '\\' if in_quote && !last_was_backslash => {
424 last_was_backslash = true;
425 continue;
426 }
427 '#' | ';' if !in_quote && !last_was_backslash => return false,
428 _ => {
429 last_was_backslash = false;
430 }
431 }
432 }
433 in_quote
434}
435
436fn value_line_continues(line: &str) -> bool {
437 let trimmed = line.trim();
438 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
439 return false;
440 }
441 let value_part = match trimmed.find('=') {
444 Some(pos) => &trimmed[pos + 1..],
445 None => return false,
446 };
447 let mut in_quote = false;
449 let mut last_was_backslash = false;
450 let mut in_comment = false;
451 for ch in value_part.chars() {
452 if in_comment {
453 last_was_backslash = false;
455 continue;
456 }
457 match ch {
458 '"' if !last_was_backslash => {
459 in_quote = !in_quote;
460 last_was_backslash = false;
461 }
462 '\\' if !last_was_backslash => {
463 last_was_backslash = true;
464 continue;
465 }
466 '#' | ';' if !in_quote && !last_was_backslash => {
467 in_comment = true;
468 last_was_backslash = false;
469 }
470 _ => {
471 last_was_backslash = false;
472 }
473 }
474 }
475 last_was_backslash && !in_comment
477}
478
479fn strip_inline_comment(s: &str) -> String {
481 let mut in_quote = false;
482 let mut result = String::with_capacity(s.len());
483 let mut chars = s.chars().peekable();
484 while let Some(ch) = chars.next() {
485 match ch {
486 '"' => {
487 in_quote = !in_quote;
488 result.push(ch);
489 }
490 '\\' if in_quote => {
491 result.push(ch);
492 if let Some(&next) = chars.peek() {
493 result.push(next);
494 chars.next();
495 }
496 }
497 '#' | ';' if !in_quote => break,
498 _ => result.push(ch),
499 }
500 }
501 let trimmed = result.trim_end();
503 trimmed.to_owned()
504}
505
506fn unescape_value(s: &str) -> String {
509 let mut result = String::with_capacity(s.len());
510 let mut chars = s.chars();
511 while let Some(ch) = chars.next() {
512 match ch {
513 '"' => { }
514 '\\' => match chars.next() {
515 Some('n') => result.push('\n'),
516 Some('r') => result.push('\r'),
517 Some('t') => result.push('\t'),
518 Some('\\') => result.push('\\'),
519 Some('"') => result.push('"'),
520 Some(other) => {
521 result.push('\\');
522 result.push(other);
523 }
524 None => result.push('\\'),
525 },
526 _ => result.push(ch),
527 }
528 }
529 result
530}
531
532fn escape_subsection(s: &str) -> String {
539 let mut out = String::with_capacity(s.len());
540 for ch in s.chars() {
541 match ch {
542 '"' => out.push_str("\\\""),
543 '\\' => out.push_str("\\\\"),
544 other => out.push(other),
545 }
546 }
547 out
548}
549
550fn escape_value(s: &str) -> String {
551 let leading_dash_needs_quoting = s.starts_with('-') && parse_i64(s).is_err();
554 let needs_quoting = leading_dash_needs_quoting
555 || s.starts_with(' ')
556 || s.starts_with('\t')
557 || s.ends_with(' ')
558 || s.ends_with('\t')
559 || s.contains('"')
560 || s.contains('\\')
561 || s.contains('\n')
562 || s.contains('\r')
563 || s.contains('#')
564 || s.contains(';');
565
566 if !needs_quoting {
567 return s.to_owned();
568 }
569
570 let mut out = String::with_capacity(s.len() + 4);
571 out.push('"');
572 for ch in s.chars() {
573 match ch {
574 '"' => out.push_str("\\\""),
575 '\\' => out.push_str("\\\\"),
576 '\n' => out.push_str("\\n"),
577 '\r' => out.push_str("\\r"),
578 '\t' => out.push_str("\\t"),
579 other => out.push(other),
580 }
581 }
582 out.push('"');
583 out
584}
585
586fn format_comment_suffix(comment: Option<&str>) -> String {
593 match comment {
594 None => String::new(),
595 Some(c) => {
596 if c.starts_with(' ') || c.starts_with('\t') {
597 c.to_owned()
599 } else if c.starts_with('#') {
600 format!(" {c}")
602 } else {
603 format!(" # {c}")
605 }
606 }
607 }
608}
609
610impl ConfigFile {
611 pub fn parse(path: &Path, content: &str, scope: ConfigScope) -> Result<Self> {
623 let raw_lines: Vec<String> = content
624 .lines()
625 .map(|l| l.strip_suffix('\r').unwrap_or(l))
626 .map(String::from)
627 .collect();
628 let mut entries = Vec::new();
629 let mut parser = Parser::new();
630
631 let mut idx = 0;
632 while idx < raw_lines.len() {
633 let start_idx = idx;
634 let line = &raw_lines[idx];
635 idx += 1;
636
637 let trimmed = line.trim();
639 if trimmed.starts_with('#') || trimmed.starts_with(';') {
640 continue;
641 }
642
643 let mut inline_remainder = None;
644 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
645 if let Some(remainder) = inline_remainder {
647 if let Some((key, value)) = parser.try_parse_entry(remainder) {
648 if key == "fetch.negotiationalgorithm" && value.is_none() {
649 let file_disp = config_error_path_display(path);
650 return Err(Error::Message(format!(
651 "error: missing value for 'fetch.negotiationalgorithm'\n\
652fatal: bad config variable 'fetch.negotiationalgorithm' in file '{file_disp}' at line {}",
653 start_idx + 1
654 )));
655 }
656 entries.push(ConfigEntry {
657 key,
658 value,
659 scope,
660 file: Some(path.to_path_buf()),
661 line: start_idx + 1,
662 });
663 }
664 }
665 continue;
666 }
667
668 let mut logical_line = line.clone();
671 while value_line_continues(&logical_line) && idx < raw_lines.len() {
672 let t = logical_line.trim_end();
674 logical_line = t[..t.len() - 1].to_string();
675 let next = raw_lines[idx].trim_start();
677 logical_line.push_str(next);
678 idx += 1;
679 }
680
681 while entry_line_value_has_unclosed_quote(&logical_line) && idx < raw_lines.len() {
682 let next = raw_lines[idx].trim_start();
683 logical_line.push_str(next);
684 idx += 1;
685 }
686 if entry_line_value_has_unclosed_quote(&logical_line) {
687 let file_disp = config_error_path_display(path);
688 return Err(Error::ConfigError(format!(
689 "bad config line {} in file '{file_disp}'",
690 start_idx + 1
691 )));
692 }
693
694 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
695 if key == "fetch.negotiationalgorithm" && value.is_none() {
696 let file_disp = config_error_path_display(path);
697 return Err(Error::Message(format!(
698 "error: missing value for 'fetch.negotiationalgorithm'\n\
699fatal: bad config variable 'fetch.negotiationalgorithm' in file '{file_disp}' at line {}",
700 start_idx + 1
701 )));
702 }
703 entries.push(ConfigEntry {
704 key,
705 value,
706 scope,
707 file: Some(path.to_path_buf()),
708 line: start_idx + 1,
709 });
710 } else if logical_line.trim().is_empty() {
711 continue;
712 } else {
713 let file_disp = config_error_path_display(path);
714 let location = if path == Path::new("-") {
715 file_disp
716 } else {
717 format!("file {file_disp}")
718 };
719 return Err(Error::Message(format!(
720 "fatal: bad config line {} in {location}",
721 start_idx + 1
722 )));
723 }
724 }
725
726 Ok(Self {
727 path: path.to_path_buf(),
728 scope,
729 entries,
730 raw_lines,
731 include_origin: ConfigIncludeOrigin::Disk,
732 })
733 }
734
735 pub fn parse_gitmodules_best_effort(
741 path: &Path,
742 content: &str,
743 scope: ConfigScope,
744 ) -> (Vec<ConfigEntry>, Option<usize>) {
745 let raw_lines: Vec<String> = content
746 .lines()
747 .map(|l| l.strip_suffix('\r').unwrap_or(l))
748 .map(String::from)
749 .collect();
750 let mut entries = Vec::new();
751 let mut parser = Parser::new();
752
753 let mut idx = 0;
754 while idx < raw_lines.len() {
755 let start_idx = idx;
756 let line = &raw_lines[idx];
757 idx += 1;
758
759 let trimmed = line.trim();
760 if trimmed.starts_with('#') || trimmed.starts_with(';') {
761 continue;
762 }
763
764 let mut inline_remainder = None;
765 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
766 if let Some(remainder) = inline_remainder {
767 if let Some((key, value)) = parser.try_parse_entry(remainder) {
768 entries.push(ConfigEntry {
769 key,
770 value,
771 scope,
772 file: Some(path.to_path_buf()),
773 line: start_idx + 1,
774 });
775 }
776 }
777 continue;
778 }
779
780 let mut logical_line = line.clone();
781 while value_line_continues(&logical_line) && idx < raw_lines.len() {
782 let t = logical_line.trim_end();
783 logical_line = t[..t.len() - 1].to_string();
784 let next = raw_lines[idx].trim_start();
785 logical_line.push_str(next);
786 idx += 1;
787 }
788
789 while entry_line_value_has_unclosed_quote(&logical_line) && idx < raw_lines.len() {
790 let next = raw_lines[idx].trim_start();
791 logical_line.push_str(next);
792 idx += 1;
793 }
794 if entry_line_value_has_unclosed_quote(&logical_line) {
795 return (entries, Some(start_idx + 1));
796 }
797
798 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
799 entries.push(ConfigEntry {
800 key,
801 value,
802 scope,
803 file: Some(path.to_path_buf()),
804 line: start_idx + 1,
805 });
806 }
807 }
808
809 (entries, None)
810 }
811
812 #[must_use]
814 pub fn get(&self, key: &str) -> Option<String> {
815 let canon = canonical_key(key).ok()?;
816 self.entries
817 .iter()
818 .rev()
819 .find(|e| e.key == canon)
820 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
821 }
822
823 pub fn parse_with_origin(
825 path: &Path,
826 content: &str,
827 scope: ConfigScope,
828 include_origin: ConfigIncludeOrigin,
829 ) -> Result<Self> {
830 let mut f = Self::parse(path, content, scope)?;
831 f.include_origin = include_origin;
832 Ok(f)
833 }
834
835 pub fn from_git_config_parameters(path: &Path, raw: &str) -> Result<Self> {
840 let mut entries = Vec::new();
841 let pseudo_path = path.to_path_buf();
842 for entry in parse_config_parameters_strict(raw)? {
843 match entry {
844 ConfigParameter::Pair { key, value } => {
845 let canon = canonical_key(key.trim())?;
846 entries.push(ConfigEntry {
847 key: canon,
848 value,
849 scope: ConfigScope::Command,
850 file: Some(pseudo_path.clone()),
851 line: 0,
852 });
853 }
854 ConfigParameter::OldStyle(entry) => {
855 if let Some((key, val)) = entry.split_once('=') {
856 let canon = canonical_key(key.trim())?;
857 entries.push(ConfigEntry {
858 key: canon,
859 value: Some(val.to_owned()),
860 scope: ConfigScope::Command,
861 file: Some(pseudo_path.clone()),
862 line: 0,
863 });
864 } else {
865 let canon = canonical_key(entry.trim())?;
866 entries.push(ConfigEntry {
867 key: canon,
868 value: None,
869 scope: ConfigScope::Command,
870 file: Some(pseudo_path.clone()),
871 line: 0,
872 });
873 }
874 }
875 }
876 }
877 Ok(Self {
878 path: path.to_path_buf(),
879 scope: ConfigScope::Command,
880 entries,
881 raw_lines: Vec::new(),
882 include_origin: ConfigIncludeOrigin::CommandLine,
883 })
884 }
885
886 pub fn from_path(path: &Path, scope: ConfigScope) -> Result<Option<Self>> {
895 match fs::read_to_string(path) {
896 Ok(content) => Ok(Some(Self::parse(path, &content, scope)?)),
897 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
898 Err(e) => Err(Error::Io(e)),
899 }
900 }
901
902 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
913 self.set_with_comment(key, value, None)
914 }
915
916 pub fn set_with_comment(
918 &mut self,
919 key: &str,
920 value: &str,
921 comment: Option<&str>,
922 ) -> Result<()> {
923 let canon = canonical_key(key)?;
924 let raw_var = raw_variable_name(key);
925 let comment_suffix = format_comment_suffix(comment);
926
927 let existing_idx = self.entries.iter().rposition(|e| e.key == canon);
929
930 if let Some(idx) = existing_idx {
931 let line_idx = self.entries[idx].line - 1;
932 let raw_line = &self.raw_lines[line_idx];
933 if is_section_header_with_inline_entry(raw_line) {
934 let header_only = extract_section_header(raw_line);
936 self.raw_lines[line_idx] = header_only;
937 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
938 self.raw_lines.insert(line_idx + 1, new_line);
939 let content = self.raw_lines.join("\n");
941 let reparsed = Self::parse(&self.path, &content, self.scope)?;
942 self.entries = reparsed.entries;
943 self.raw_lines = reparsed.raw_lines;
944 } else {
945 self.raw_lines[line_idx] =
946 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
947 self.entries[idx].value = Some(value.to_owned());
948 }
949 } else {
950 let (section, subsection, _var) = split_key(&canon)?;
952 let (raw_sec, raw_sub) = raw_section_parts(key);
953 let section_line = self.find_or_create_section_preserving_case(
954 §ion,
955 subsection.as_deref(),
956 &raw_sec,
957 raw_sub.as_deref(),
958 );
959 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
960
961 let insert_at = self.last_line_in_section(section_line) + 1;
963 self.raw_lines.insert(insert_at, new_line);
964
965 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 }
971
972 Ok(())
973 }
974
975 pub fn replace_all(
980 &mut self,
981 key: &str,
982 value: &str,
983 value_pattern: Option<&str>,
984 ) -> Result<()> {
985 self.replace_all_with_comment(key, value, value_pattern, None)
986 }
987
988 pub fn replace_all_with_comment(
993 &mut self,
994 key: &str,
995 value: &str,
996 value_pattern: Option<&str>,
997 comment: Option<&str>,
998 ) -> Result<()> {
999 let canon = canonical_key(key)?;
1000 let comment_suffix = format_comment_suffix(comment);
1001
1002 let (re, negated) = match value_pattern {
1004 Some(pat) => {
1005 let (neg, actual_pat) = if let Some(rest) = pat.strip_prefix('!') {
1006 (true, rest)
1007 } else {
1008 (false, pat)
1009 };
1010 let compiled = regex::Regex::new(actual_pat)
1011 .map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?;
1012 (Some(compiled), neg)
1013 }
1014 None => (None, false),
1015 };
1016
1017 let matching_indices: Vec<usize> = self
1019 .entries
1020 .iter()
1021 .enumerate()
1022 .filter(|(_, e)| {
1023 if e.key != canon {
1024 return false;
1025 }
1026 if let Some(ref re) = re {
1027 let v = e.value.as_deref().unwrap_or("");
1028 let matched = re.is_match(v);
1029 if negated {
1030 !matched
1031 } else {
1032 matched
1033 }
1034 } else {
1035 true
1036 }
1037 })
1038 .map(|(i, _)| i)
1039 .collect();
1040
1041 if matching_indices.is_empty() {
1042 return self.add_value_with_comment(key, value, comment);
1044 }
1045
1046 let raw_var = raw_variable_name(key);
1047
1048 let target_idx = if value_pattern.is_some() {
1049 matching_indices[0]
1050 } else {
1051 *matching_indices
1052 .last()
1053 .ok_or_else(|| Error::ConfigError("missing config match".to_owned()))?
1054 };
1055 let target_line_idx = self.entries[target_idx].line - 1;
1056 let raw_line = &self.raw_lines[target_line_idx];
1057 if is_section_header_with_inline_entry(raw_line) {
1058 let header = extract_section_header(raw_line);
1059 self.raw_lines[target_line_idx] = header;
1060 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1061 self.raw_lines.insert(target_line_idx + 1, new_line);
1062 } else {
1063 self.raw_lines[target_line_idx] =
1064 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1065 }
1066
1067 for &idx in matching_indices.iter().rev() {
1068 if idx == target_idx {
1069 continue;
1070 }
1071 let line_idx = self.entries[idx].line - 1;
1072 self.remove_entry_line(line_idx);
1073 }
1074
1075 let content = self.raw_lines.join("\n");
1077 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1078 self.entries = reparsed.entries;
1079 self.raw_lines = reparsed.raw_lines;
1080
1081 Ok(())
1082 }
1083
1084 pub fn count(&self, key: &str) -> Result<usize> {
1086 let canon = canonical_key(key)?;
1087 Ok(self.entries.iter().filter(|e| e.key == canon).count())
1088 }
1089
1090 fn remove_entry_line(&mut self, line_idx: usize) {
1101 if is_section_header_with_inline_entry(&self.raw_lines[line_idx]) {
1102 let header = extract_section_header(&self.raw_lines[line_idx]);
1104 self.raw_lines[line_idx] = header;
1105 } else {
1106 let mut lines_to_remove = 1;
1108 let mut check_line = self.raw_lines[line_idx].clone();
1109 while value_line_continues(&check_line)
1110 && (line_idx + lines_to_remove) < self.raw_lines.len()
1111 {
1112 check_line = self.raw_lines[line_idx + lines_to_remove].clone();
1113 lines_to_remove += 1;
1114 }
1115 for _ in 0..lines_to_remove {
1116 self.raw_lines.remove(line_idx);
1117 }
1118 }
1119 }
1120
1121 pub fn unset_last(&mut self, key: &str) -> Result<usize> {
1125 let canon = canonical_key(key)?;
1126 let last_idx = self.entries.iter().rposition(|e| e.key == canon);
1127
1128 if let Some(idx) = last_idx {
1129 let line_idx = self.entries[idx].line - 1;
1130 self.remove_entry_line(line_idx);
1131 let content = self.raw_lines.join("\n");
1132 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1133 self.entries = reparsed.entries;
1134 self.raw_lines = reparsed.raw_lines;
1135 Ok(1)
1136 } else {
1137 Ok(0)
1138 }
1139 }
1140
1141 pub fn unset(&mut self, key: &str) -> Result<usize> {
1151 let canon = canonical_key(key)?;
1152 let line_indices: Vec<usize> = self
1153 .entries
1154 .iter()
1155 .filter(|e| e.key == canon)
1156 .map(|e| e.line - 1)
1157 .collect();
1158
1159 let count = line_indices.len();
1160 for &idx in line_indices.iter().rev() {
1162 self.remove_entry_line(idx);
1163 }
1164
1165 if count > 0 {
1166 let content = self.raw_lines.join("\n");
1167 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1168 self.entries = reparsed.entries;
1169 self.raw_lines = reparsed.raw_lines;
1170 }
1171
1172 Ok(count)
1173 }
1174
1175 pub fn unset_matching(
1184 &mut self,
1185 key: &str,
1186 value_pattern: Option<&str>,
1187 preserve_empty_section_header: bool,
1188 ) -> Result<usize> {
1189 let canon = canonical_key(key)?;
1190 let re = match value_pattern {
1191 Some(pat) => Some(
1192 regex::Regex::new(pat)
1193 .map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?,
1194 ),
1195 None => None,
1196 };
1197
1198 let line_indices: Vec<usize> = self
1199 .entries
1200 .iter()
1201 .filter(|e| {
1202 if e.key != canon {
1203 return false;
1204 }
1205 if let Some(ref re) = re {
1206 let v = e.value.as_deref().unwrap_or("");
1207 re.is_match(v)
1208 } else {
1209 true
1210 }
1211 })
1212 .map(|e| e.line - 1)
1213 .collect();
1214
1215 let count = line_indices.len();
1216 for &idx in line_indices.iter().rev() {
1217 self.remove_entry_line(idx);
1218 }
1219
1220 if count > 0 {
1221 if !preserve_empty_section_header {
1222 let (section, subsection, _) = split_key(&canon)?;
1223 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
1224 }
1225
1226 let content = self.raw_lines.join("\n");
1227 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1228 self.entries = reparsed.entries;
1229 self.raw_lines = reparsed.raw_lines;
1230 }
1231
1232 Ok(count)
1233 }
1234
1235 pub fn remove_section(&mut self, section: &str) -> Result<bool> {
1241 let (sec_name, sub_name) = parse_section_name(section);
1242 let sec_lower = sec_name.to_lowercase();
1243
1244 let mut remove = vec![false; self.raw_lines.len()];
1245 let mut removing = false;
1246 let mut found = false;
1247 let mut parser = Parser::new();
1248
1249 for (idx, line) in self.raw_lines.iter().enumerate() {
1250 if parser.try_parse_section(line) {
1251 removing = section_matches(&parser, &sec_lower, sub_name);
1252 found |= removing;
1253 }
1254 if removing {
1255 remove[idx] = true;
1256 }
1257 }
1258
1259 if found {
1260 self.raw_lines = self
1261 .raw_lines
1262 .iter()
1263 .enumerate()
1264 .filter_map(|(idx, line)| (!remove[idx]).then_some(line.clone()))
1265 .collect();
1266 let content = self.raw_lines.join("\n");
1267 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1268 self.entries = reparsed.entries;
1269 self.raw_lines = reparsed.raw_lines;
1270 Ok(true)
1271 } else {
1272 Ok(false)
1273 }
1274 }
1275
1276 pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
1283 let (old_sec, old_sub) = parse_section_name(old_name);
1284 let (new_sec, new_sub) = parse_section_name(new_name);
1285 validate_section_name(new_sec, new_sub)?;
1286 let old_lower = old_sec.to_lowercase();
1287
1288 let mut found = false;
1289 let mut parser = Parser::new();
1290
1291 let mut idx = 0usize;
1292 while idx < self.raw_lines.len() {
1293 let line = self.raw_lines[idx].clone();
1294 let mut inline_remainder = None;
1295 if parser.try_parse_section_with_remainder(&line, &mut inline_remainder)
1296 && section_matches(&parser, &old_lower, old_sub)
1297 {
1298 let header = match new_sub {
1300 Some(sub) => format!("[{} \"{}\"]", new_sec, sub),
1301 None => format!("[{}]", new_sec),
1302 };
1303 self.raw_lines[idx] = header;
1304 if let Some(remainder) = inline_remainder {
1305 self.raw_lines
1306 .insert(idx + 1, format!("\t{}", remainder.trim()));
1307 idx += 1;
1308 }
1309 found = true;
1310 }
1311 idx += 1;
1312 }
1313
1314 if found {
1315 let content = self.raw_lines.join("\n");
1316 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1317 self.entries = reparsed.entries;
1318 self.raw_lines = reparsed.raw_lines;
1319 }
1320
1321 Ok(found)
1322 }
1323
1324 pub fn add_value(&mut self, key: &str, value: &str) -> Result<()> {
1329 self.add_value_with_comment(key, value, None)
1330 }
1331
1332 pub fn add_value_with_comment(
1334 &mut self,
1335 key: &str,
1336 value: &str,
1337 comment: Option<&str>,
1338 ) -> Result<()> {
1339 let canon = canonical_key(key)?;
1340 let raw_var = raw_variable_name(key);
1341 let comment_suffix = format_comment_suffix(comment);
1342 let (section, subsection, _var) = split_key(&canon)?;
1343 let (raw_sec, raw_sub) = raw_section_parts(key);
1344
1345 let section_line = self.find_or_create_section_preserving_case(
1346 §ion,
1347 subsection.as_deref(),
1348 &raw_sec,
1349 raw_sub.as_deref(),
1350 );
1351 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1352 let insert_at = self.last_line_in_section(section_line) + 1;
1353 self.raw_lines.insert(insert_at, new_line);
1354
1355 let content = self.raw_lines.join("\n");
1357 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1358 self.entries = reparsed.entries;
1359 self.raw_lines = reparsed.raw_lines;
1360
1361 Ok(())
1362 }
1363
1364 fn remove_empty_section_headers_matching(&mut self, section: &str, subsection: Option<&str>) {
1367 let (Ok(section_re), Ok(comment_re)) = (
1368 regex::Regex::new(r"^\s*\["),
1369 regex::Regex::new(r"^\s*(#|;)"),
1370 ) else {
1371 return;
1373 };
1374
1375 let mut to_remove: Vec<usize> = Vec::new();
1376 let len = self.raw_lines.len();
1377 let section_lower = section.to_lowercase();
1378 let mut parser = Parser::new();
1379
1380 for i in 0..len {
1381 let line = &self.raw_lines[i];
1382 if !section_re.is_match(line) {
1383 continue;
1384 }
1385 if !parser.try_parse_section(line)
1386 || !section_matches(&parser, §ion_lower, subsection)
1387 {
1388 continue;
1389 }
1390 if is_section_header_with_inline_entry(line) {
1392 continue;
1393 }
1394 let has_attached_leading_comment = self.raw_lines[..i]
1395 .iter()
1396 .enumerate()
1397 .rev()
1398 .find(|(_, line)| !line.trim().is_empty())
1399 .is_some_and(|(idx, line)| {
1400 comment_re.is_match(line)
1401 && idx
1402 .checked_sub(1)
1403 .is_none_or(|prev| !value_line_continues(&self.raw_lines[prev]))
1404 });
1405 if has_attached_leading_comment {
1406 continue;
1407 }
1408 let mut has_entries = false;
1411 for j in (i + 1)..len {
1412 let next = self.raw_lines[j].trim();
1413 if next.is_empty() {
1414 continue;
1415 }
1416 if section_re.is_match(&self.raw_lines[j]) {
1417 break;
1418 }
1419 if comment_re.is_match(&self.raw_lines[j]) {
1420 has_entries = true;
1422 break;
1423 }
1424 has_entries = true;
1426 break;
1427 }
1428 if !has_entries {
1429 to_remove.push(i);
1430 }
1431 }
1432
1433 for &idx in to_remove.iter().rev() {
1435 self.raw_lines.remove(idx);
1436 }
1437
1438 while self.raw_lines.last().is_some_and(|l| l.trim().is_empty()) {
1440 self.raw_lines.pop();
1441 }
1442 }
1443
1444 pub fn write(&self) -> Result<()> {
1449 let content = self.raw_lines.join("\n");
1450 let trimmed = content.trim();
1451 if trimmed.is_empty() {
1452 fs::write(&self.path, "")?;
1454 } else {
1455 let content = if content.ends_with('\n') {
1457 content
1458 } else {
1459 format!("{content}\n")
1460 };
1461 fs::write(&self.path, content)?;
1462 }
1463 evict_config_cache_for_path(&self.path);
1464 Ok(())
1465 }
1466
1467 #[allow(dead_code)]
1469 fn find_or_create_section(&mut self, section: &str, subsection: Option<&str>) -> usize {
1470 let sec_lower = section.to_lowercase();
1471 let mut parser = Parser::new();
1472
1473 for (idx, line) in self.raw_lines.iter().enumerate() {
1474 if parser.try_parse_section(line) && section_matches(&parser, &sec_lower, subsection) {
1475 return idx;
1476 }
1477 }
1478
1479 let header = match subsection {
1481 Some(sub) => {
1482 let escaped = escape_subsection(sub);
1483 format!("[{} \"{}\"]", section, escaped)
1484 }
1485 None => format!("[{}]", section),
1486 };
1487 self.raw_lines.push(header);
1488 self.raw_lines.len() - 1
1489 }
1490
1491 fn find_or_create_section_preserving_case(
1494 &mut self,
1495 section: &str,
1496 subsection: Option<&str>,
1497 raw_section: &str,
1498 raw_subsection: Option<&str>,
1499 ) -> usize {
1500 let sec_lower = section.to_lowercase();
1501 let mut parser = Parser::new();
1502
1503 for (idx, line) in self.raw_lines.iter().enumerate() {
1504 if parser.try_parse_section(line) && section_matches(&parser, &sec_lower, subsection) {
1505 return idx;
1506 }
1507 }
1508
1509 let header = match raw_subsection {
1511 Some(sub) => {
1512 let escaped = escape_subsection(sub);
1513 format!("[{} \"{}\"]", raw_section, escaped)
1514 }
1515 None => format!("[{}]", raw_section),
1516 };
1517 self.raw_lines.push(header);
1518 self.raw_lines.len() - 1
1519 }
1520
1521 fn last_line_in_section(&self, section_line: usize) -> usize {
1523 let mut last = section_line;
1524 for idx in (section_line + 1)..self.raw_lines.len() {
1525 let trimmed = self.raw_lines[idx].trim();
1526 if trimmed.starts_with('[') {
1527 break;
1528 }
1529 last = idx;
1530 }
1531 last
1532 }
1533}
1534
1535impl ConfigSet {
1538 #[must_use]
1540 pub fn new() -> Self {
1541 Self {
1542 entries: Vec::new(),
1543 }
1544 }
1545
1546 #[must_use]
1548 pub fn entries(&self) -> &[ConfigEntry] {
1549 &self.entries
1550 }
1551
1552 pub fn merge(&mut self, file: &ConfigFile) {
1557 self.entries.extend(file.entries.iter().cloned());
1558 }
1559
1560 pub fn merge_set(&mut self, other: &ConfigSet) {
1562 self.entries.extend(other.entries.iter().cloned());
1563 }
1564
1565 pub fn add_command_override(&mut self, key: &str, value: &str) -> Result<()> {
1567 let canon = canonical_key(key)?;
1568 self.entries.push(ConfigEntry {
1569 key: canon,
1570 value: Some(value.to_owned()),
1571 scope: ConfigScope::Command,
1572 file: None,
1573 line: 0,
1574 });
1575 Ok(())
1576 }
1577
1578 #[must_use]
1589 pub fn get(&self, key: &str) -> Option<String> {
1590 let canon = canonical_key(key).ok()?;
1591 self.entries
1592 .iter()
1593 .rev()
1594 .find(|e| e.key == canon)
1595 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
1596 }
1597
1598 #[must_use]
1603 pub fn get_last_entry(&self, key: &str) -> Option<ConfigEntry> {
1604 let canon = canonical_key(key).ok()?;
1605 self.entries.iter().rev().find(|e| e.key == canon).cloned()
1606 }
1607
1608 #[must_use]
1610 pub fn get_all(&self, key: &str) -> Vec<String> {
1611 let canon = match canonical_key(key) {
1612 Ok(c) => c,
1613 Err(_) => return Vec::new(),
1614 };
1615 self.entries
1616 .iter()
1617 .filter(|e| e.key == canon)
1618 .map(|e| e.value.clone().unwrap_or_default())
1619 .collect()
1620 }
1621
1622 #[must_use]
1626 pub fn get_all_raw(&self, key: &str) -> Vec<Option<String>> {
1627 let canon = match canonical_key(key) {
1628 Ok(c) => c,
1629 Err(_) => return Vec::new(),
1630 };
1631 self.entries
1632 .iter()
1633 .filter(|e| e.key == canon)
1634 .map(|e| e.value.clone())
1635 .collect()
1636 }
1637
1638 #[must_use]
1643 pub fn has_key(&self, key: &str) -> bool {
1644 let Ok(canon) = canonical_key(key) else {
1645 return false;
1646 };
1647 self.entries.iter().any(|e| e.key == canon)
1648 }
1649
1650 pub fn get_bool(&self, key: &str) -> Option<std::result::Result<bool, String>> {
1656 let v = self.get(key)?;
1657 if canonical_key(key).ok().as_deref() == Some("pack.allowpackreuse") {
1658 let lower = v.trim().to_ascii_lowercase();
1659 if lower == "single" || lower == "multi" {
1660 return None;
1661 }
1662 }
1663 Some(parse_bool(&v))
1664 }
1665
1666 #[must_use]
1672 pub fn quote_path_fully(&self) -> bool {
1673 let from_key = |key: &str| self.get_bool(key).and_then(|r| r.ok());
1674 from_key("core.quotepath")
1675 .or_else(|| from_key("core.quotePath"))
1676 .unwrap_or(true)
1677 }
1678
1679 #[must_use]
1683 pub fn pack_write_reverse_index_default(&self) -> bool {
1684 if std::env::var("GIT_TEST_NO_WRITE_REV_INDEX")
1685 .ok()
1686 .as_deref()
1687 .is_some_and(|v| {
1688 let s = v.trim().to_ascii_lowercase();
1689 matches!(s.as_str(), "1" | "true" | "yes" | "on")
1690 })
1691 {
1692 return false;
1693 }
1694 if self
1695 .get("pack.writereverseindex")
1696 .or_else(|| self.get("pack.writeReverseIndex"))
1697 .is_some_and(|v| v.trim().is_empty())
1698 {
1699 return false;
1700 }
1701 self.get_bool("pack.writereverseindex")
1702 .or_else(|| self.get_bool("pack.writeReverseIndex"))
1703 .and_then(|r| r.ok())
1704 .unwrap_or(true)
1705 }
1706
1707 #[must_use]
1709 pub fn pack_read_reverse_index_default(&self) -> bool {
1710 self.get_bool("pack.readreverseindex")
1711 .or_else(|| self.get_bool("pack.readReverseIndex"))
1712 .and_then(|r| r.ok())
1713 .unwrap_or(true)
1714 }
1715
1716 #[must_use]
1719 pub fn effective_log_refs_config(&self, git_dir: &Path) -> refs::LogRefsConfig {
1720 if let Some(v) = self.get("core.logAllRefUpdates") {
1721 let lower = v.trim().to_ascii_lowercase();
1722 let parsed = match lower.as_str() {
1723 "always" => Some(refs::LogRefsConfig::Always),
1724 "1" | "true" | "yes" | "on" => Some(refs::LogRefsConfig::Normal),
1725 "0" | "false" | "no" | "off" | "never" => Some(refs::LogRefsConfig::None),
1726 _ => None,
1727 };
1728 if let Some(c) = parsed {
1729 return c;
1730 }
1731 }
1732 refs::effective_log_refs_config(git_dir)
1733 }
1734
1735 pub fn get_i64(&self, key: &str) -> Option<std::result::Result<i64, String>> {
1737 self.get(key).map(|v| parse_i64(&v))
1738 }
1739
1740 pub fn pack_objects_zlib_level(&self) -> Result<i32> {
1748 const Z_DEFAULT_COMPRESSION: i32 = 6;
1749 const Z_BEST_COMPRESSION: i32 = 9;
1750
1751 let parse_compression = |raw: &str| -> Result<i32> {
1752 let v = parse_git_config_int_strict(raw.trim()).map_err(|_| {
1753 Error::ConfigError(format!("bad numeric config value '{raw}' for compression"))
1754 })?;
1755 if v == -1 {
1756 return Ok(Z_DEFAULT_COMPRESSION);
1757 }
1758 if v < 0 || v > i64::from(Z_BEST_COMPRESSION) {
1759 return Err(Error::ConfigError(format!(
1760 "bad zlib compression level {v}"
1761 )));
1762 }
1763 Ok(v as i32)
1764 };
1765
1766 let mut pack_level = Z_DEFAULT_COMPRESSION;
1768 let mut pack_compression_seen = false;
1769
1770 for e in self.entries() {
1771 match e.key.as_str() {
1772 "core.compression" => {
1773 let Some(val) = e.value.as_deref() else {
1774 continue;
1775 };
1776 let level = parse_compression(val)?;
1777 if !pack_compression_seen {
1778 pack_level = level;
1779 }
1780 }
1781 "pack.compression" => {
1782 let Some(val) = e.value.as_deref() else {
1783 continue;
1784 };
1785 pack_level = parse_compression(val)?;
1786 pack_compression_seen = true;
1787 }
1788 _ => {}
1789 }
1790 }
1791
1792 Ok(pack_level)
1793 }
1794
1795 pub fn get_regexp(&self, pattern: &str) -> std::result::Result<Vec<&ConfigEntry>, String> {
1800 let re = regex::Regex::new(pattern).map_err(|e| format!("invalid key pattern: {e}"))?;
1801 Ok(self
1802 .entries
1803 .iter()
1804 .filter(|e| re.is_match(&e.key))
1805 .collect())
1806 }
1807
1808 pub fn load(git_dir: Option<&Path>, include_system: bool) -> Result<Self> {
1819 let mut opts = LoadConfigOptions::default();
1820 opts.include_system = include_system;
1821 opts.include_ctx.git_dir = git_dir.map(PathBuf::from);
1822 Self::load_with_options(git_dir, &opts)
1823 }
1824
1825 pub fn load_with_options(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Result<Self> {
1833 let Some(env_fp) = config_env_fingerprint() else {
1834 return Self::load_with_options_uncached(git_dir, opts, &mut Vec::new());
1835 };
1836 let key = ConfigCacheKey::new(git_dir, opts);
1837 let base_stamps = config_file_stamps(git_dir, opts);
1838 if let Some(cached) = config_cache_lookup(&key, &env_fp, &base_stamps) {
1839 return Ok(cached);
1840 }
1841 let mut included_files = Vec::new();
1842 let set = Self::load_with_options_uncached(git_dir, opts, &mut included_files)?;
1843 included_files.sort_unstable();
1844 included_files.dedup();
1845 let extra_stamps = stamp_paths(included_files);
1846 let mut cache = config_cache()
1847 .lock()
1848 .unwrap_or_else(std::sync::PoisonError::into_inner);
1849 cache.insert(
1850 key,
1851 ConfigCacheEntry {
1852 env_fingerprint: env_fp,
1853 base_stamps,
1854 extra_stamps,
1855 set: Arc::new(set.clone()),
1856 },
1857 );
1858 Ok(set)
1859 }
1860
1861 fn load_with_options_uncached(
1862 git_dir: Option<&Path>,
1863 opts: &LoadConfigOptions,
1864 included_files: &mut Vec<PathBuf>,
1865 ) -> Result<Self> {
1866 let mut set = Self::new();
1867 let proc = opts.process_includes;
1868 let ctx = opts.include_ctx.clone();
1869
1870 if opts.include_system && !git_config_nosystem_enabled() {
1872 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
1873 .map(std::path::PathBuf::from)
1874 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
1875 match ConfigFile::from_path(&system_path, ConfigScope::System) {
1876 Ok(Some(f)) => {
1877 Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?
1878 }
1879 Ok(None) => {}
1880 Err(e) => return Err(e),
1881 }
1882 }
1883
1884 for path in global_config_paths() {
1886 match ConfigFile::from_path(&path, ConfigScope::Global) {
1887 Ok(Some(f)) => {
1888 Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?
1889 }
1890 Ok(None) => {}
1891 Err(e) => return Err(e),
1892 }
1893 }
1894
1895 if let Some(gd) = git_dir {
1897 let common_dir = crate::repo::common_git_dir_for_config(gd);
1898 let local_path = common_dir.join("config");
1899 match ConfigFile::from_path(&local_path, ConfigScope::Local) {
1900 Ok(Some(f)) => {
1901 Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?
1902 }
1903 Ok(None) => {}
1904 Err(e) => return Err(e),
1905 }
1906
1907 let wt_path = gd.join("config.worktree");
1910 if crate::repo::worktree_config_enabled(&common_dir) {
1911 match ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
1912 Ok(Some(f)) => Self::merge_with_includes_collect(
1913 &mut set,
1914 &f,
1915 proc,
1916 0,
1917 &ctx,
1918 included_files,
1919 )?,
1920 Ok(None) => {}
1921 Err(e) => return Err(e),
1922 }
1923 }
1924 }
1925
1926 if let Ok(path) = std::env::var("GIT_CONFIG") {
1928 match ConfigFile::from_path(Path::new(&path), ConfigScope::Command) {
1929 Ok(Some(f)) => {
1930 if proc {
1931 Self::merge_with_includes_collect(
1932 &mut set,
1933 &f,
1934 proc,
1935 0,
1936 &ctx,
1937 included_files,
1938 )?;
1939 } else {
1940 set.merge(&f);
1941 }
1942 }
1943 Ok(None) => {}
1944 Err(e) => return Err(e),
1945 }
1946 }
1947
1948 add_environment_config_pairs(&mut set)?;
1949
1950 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
1952 if proc && opts.command_includes && !params.trim().is_empty() {
1953 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
1954 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, ¶ms)?;
1955 Self::merge_with_includes_collect(
1956 &mut set,
1957 &cmd_file,
1958 proc,
1959 0,
1960 &ctx,
1961 included_files,
1962 )?;
1963 } else if !params.trim().is_empty() {
1964 for entry in parse_config_parameters(¶ms) {
1965 if let Some((key, val)) =
1966 entry.split_once('\u{1}').or_else(|| entry.split_once('='))
1967 {
1968 let _ = set.add_command_override(key.trim(), val);
1969 } else {
1970 let _ = set.add_command_override(entry.trim(), "true");
1971 }
1972 }
1973 }
1974 }
1975
1976 Ok(set)
1977 }
1978
1979 pub fn read_early_config(git_dir: Option<&Path>, key: &str) -> Result<Vec<String>> {
1991 let mut set = Self::new();
1992 let ctx = IncludeContext {
1993 git_dir: git_dir.map(PathBuf::from),
1994 command_line_relative_include_is_error: false,
1995 };
1996
1997 if !git_config_nosystem_enabled() {
1999 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
2000 .map(std::path::PathBuf::from)
2001 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
2002 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2003 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2004 }
2005 }
2006
2007 for path in global_config_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 if let Some(gd) = git_dir {
2015 let common_dir = crate::repo::common_git_dir_for_config(gd);
2016 let local_path = common_dir.join("config");
2018 if let Some(msg) = crate::repo::early_config_ignore_repo_reason(&common_dir) {
2019 eprintln!("warning: ignoring git dir '{}': {}", gd.display(), msg);
2020 } else if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2021 set.merge_file_with_includes(&f, true, &ctx)?;
2022 }
2023
2024 let wt_path = gd.join("config.worktree");
2026 if crate::repo::worktree_config_enabled(&common_dir) {
2027 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
2028 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2029 }
2030 }
2031 }
2032
2033 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
2035 if !params.trim().is_empty() {
2036 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
2037 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, ¶ms)?;
2038 Self::merge_with_includes(&mut set, &cmd_file, true, 0, &ctx)?;
2039 }
2040 }
2041
2042 Ok(set.get_all(key))
2043 }
2044
2045 pub fn merge_file_with_includes(
2050 &mut self,
2051 file: &ConfigFile,
2052 process_includes: bool,
2053 ctx: &IncludeContext,
2054 ) -> Result<()> {
2055 Self::merge_with_includes(self, file, process_includes, 0, ctx)
2056 }
2057
2058 pub fn load_repo_local_only(git_dir: &Path) -> Result<Self> {
2064 let mut set = Self::new();
2065 let local_path = git_dir.join("config");
2066 let ctx = IncludeContext {
2067 git_dir: Some(git_dir.to_path_buf()),
2068 command_line_relative_include_is_error: false,
2069 };
2070 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2071 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2072 }
2073 Ok(set)
2074 }
2075
2076 pub fn load_protected(include_system: bool) -> Result<Self> {
2087 let mut set = Self::new();
2088 let ctx = IncludeContext {
2089 git_dir: None,
2090 command_line_relative_include_is_error: false,
2091 };
2092
2093 if include_system && !git_config_nosystem_enabled() {
2094 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
2095 .map(std::path::PathBuf::from)
2096 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
2097 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2098 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2099 }
2100 }
2101
2102 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
2103 let path = PathBuf::from(p);
2104 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2105 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2106 }
2107 } else {
2108 let mut global_paths = Vec::new();
2109 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
2110 global_paths.push(PathBuf::from(xdg).join("git/config"));
2111 } else if let Some(home) = home_dir() {
2112 global_paths.push(home.join(".config/git/config"));
2113 }
2114 if let Some(home) = home_dir() {
2115 global_paths.push(home.join(".gitconfig"));
2116 }
2117 for path in global_paths {
2118 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2119 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2120 }
2121 }
2122 }
2123
2124 add_environment_config_pairs(&mut set)?;
2125
2126 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
2127 for entry in parse_config_parameters(¶ms) {
2128 if let Some((key, val)) =
2129 entry.split_once('\u{1}').or_else(|| entry.split_once('='))
2130 {
2131 let _ = set.add_command_override(key.trim(), val);
2132 } else {
2133 let _ = set.add_command_override(entry.trim(), "true");
2134 }
2135 }
2136 }
2137
2138 Ok(set)
2139 }
2140
2141 fn merge_with_includes(
2143 set: &mut Self,
2144 file: &ConfigFile,
2145 process_includes: bool,
2146 depth: usize,
2147 ctx: &IncludeContext,
2148 ) -> Result<()> {
2149 let mut included_files = Vec::new();
2150 Self::merge_with_includes_collect(
2151 set,
2152 file,
2153 process_includes,
2154 depth,
2155 ctx,
2156 &mut included_files,
2157 )
2158 }
2159
2160 fn merge_with_includes_collect(
2164 set: &mut Self,
2165 file: &ConfigFile,
2166 process_includes: bool,
2167 depth: usize,
2168 ctx: &IncludeContext,
2169 included_files: &mut Vec<PathBuf>,
2170 ) -> Result<()> {
2171 const MAX_INCLUDE_DEPTH: usize = 10;
2174 if depth > MAX_INCLUDE_DEPTH {
2175 return Err(Error::ConfigError(
2176 "exceeded maximum include depth".to_owned(),
2177 ));
2178 }
2179 if !process_includes {
2180 set.merge(file);
2181 return Ok(());
2182 }
2183
2184 for entry in &file.entries {
2185 set.entries.push(entry.clone());
2186
2187 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
2188 continue;
2189 };
2190 let included_by_hasconfig = condition.as_deref().is_some_and(is_hasconfig_remote_url);
2191 if condition.is_some() && !included_by_hasconfig {
2192 let cond = condition.as_deref().unwrap_or_default();
2193 if !evaluate_include_condition(cond, set, file, ctx) {
2194 continue;
2195 }
2196 }
2197
2198 let resolved = match resolve_include_file_path(&inc_path, file, ctx) {
2199 Ok(p) => p,
2200 Err(Error::ConfigError(msg)) if msg.is_empty() => continue,
2201 Err(e) => return Err(e),
2202 };
2203 included_files.push(resolved.clone());
2204 let Some(inc_file) = ConfigFile::from_path(&resolved, file.scope)? else {
2208 continue;
2209 };
2210
2211 if included_by_hasconfig {
2212 validate_hasconfig_remote_url_include(&inc_file, process_includes, depth + 1, ctx)?;
2213 let cond = condition.as_deref().unwrap_or_default();
2214 if !evaluate_include_condition(cond, set, file, ctx) {
2215 continue;
2216 }
2217 }
2218
2219 Self::merge_with_includes_collect(
2220 set,
2221 &inc_file,
2222 process_includes,
2223 depth + 1,
2224 ctx,
2225 included_files,
2226 )?;
2227 }
2228
2229 Ok(())
2230 }
2231}
2232
2233fn include_directive_for_entry(entry: &ConfigEntry) -> Option<(String, Option<String>)> {
2234 let val = entry.value.as_ref()?;
2235 if entry.key == "include.path" {
2236 return Some((val.clone(), None));
2237 }
2238 if entry.key.starts_with("includeif.") && entry.key.ends_with(".path") {
2239 let mid = &entry.key["includeif.".len()..entry.key.len() - ".path".len()];
2240 return Some((val.clone(), Some(mid.to_owned())));
2241 }
2242 None
2243}
2244
2245type ConfigFileStamp = (PathBuf, Option<(SystemTime, u64)>);
2272
2273#[derive(PartialEq, Eq, Hash)]
2274struct ConfigCacheKey {
2275 git_dir: Option<PathBuf>,
2276 include_system: bool,
2277 process_includes: bool,
2278 command_includes: bool,
2279 ctx_git_dir: Option<PathBuf>,
2280 ctx_relative_include_is_error: bool,
2281}
2282
2283impl ConfigCacheKey {
2284 fn new(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Self {
2285 Self {
2286 git_dir: git_dir.map(Path::to_path_buf),
2287 include_system: opts.include_system,
2288 process_includes: opts.process_includes,
2289 command_includes: opts.command_includes,
2290 ctx_git_dir: opts.include_ctx.git_dir.clone(),
2291 ctx_relative_include_is_error: opts.include_ctx.command_line_relative_include_is_error,
2292 }
2293 }
2294}
2295
2296struct ConfigCacheEntry {
2297 env_fingerprint: Vec<(String, Option<String>)>,
2298 base_stamps: Vec<ConfigFileStamp>,
2300 extra_stamps: Vec<ConfigFileStamp>,
2303 set: Arc<ConfigSet>,
2304}
2305
2306fn config_cache() -> &'static Mutex<HashMap<ConfigCacheKey, ConfigCacheEntry>> {
2307 static CACHE: OnceLock<Mutex<HashMap<ConfigCacheKey, ConfigCacheEntry>>> = OnceLock::new();
2308 CACHE.get_or_init(|| Mutex::new(HashMap::new()))
2309}
2310
2311fn config_env_fingerprint() -> Option<Vec<(String, Option<String>)>> {
2314 const VARS: [&str; 8] = [
2315 "GIT_CONFIG_NOSYSTEM",
2316 "GIT_CONFIG_SYSTEM",
2317 "GIT_CONFIG_GLOBAL",
2318 "XDG_CONFIG_HOME",
2319 "HOME",
2320 "GIT_CONFIG",
2321 "GIT_CONFIG_PARAMETERS",
2322 "GIT_CONFIG_COUNT",
2323 ];
2324 let mut fp: Vec<(String, Option<String>)> = VARS
2325 .iter()
2326 .map(|name| ((*name).to_owned(), std::env::var(name).ok()))
2327 .collect();
2328 if let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") {
2329 const MAX_TRACKED: usize = 256;
2331 match count_str.parse::<usize>() {
2332 Ok(n) if n <= MAX_TRACKED => {
2333 for i in 0..n {
2334 for var in [
2335 format!("GIT_CONFIG_KEY_{i}"),
2336 format!("GIT_CONFIG_VALUE_{i}"),
2337 ] {
2338 let val = std::env::var(&var).ok();
2339 fp.push((var, val));
2340 }
2341 }
2342 }
2343 Ok(_) => return None,
2344 Err(_) => {}
2345 }
2346 }
2347 Some(fp)
2348}
2349
2350fn config_cascade_file_paths(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Vec<PathBuf> {
2353 let mut paths = Vec::new();
2354 if opts.include_system && !git_config_nosystem_enabled() {
2355 paths.push(
2356 std::env::var("GIT_CONFIG_SYSTEM")
2357 .map(PathBuf::from)
2358 .unwrap_or_else(|_| PathBuf::from("/etc/gitconfig")),
2359 );
2360 }
2361 paths.extend(global_config_paths());
2362 if let Some(gd) = git_dir {
2363 let common_dir = crate::repo::common_git_dir_for_config(gd);
2364 paths.push(common_dir.join("config"));
2365 paths.push(gd.join("config.worktree"));
2368 paths.push(gd.join("HEAD"));
2371 }
2372 if let Some(cgd) = &opts.include_ctx.git_dir {
2373 if Some(cgd.as_path()) != git_dir {
2374 paths.push(cgd.join("HEAD"));
2375 }
2376 }
2377 if let Ok(p) = std::env::var("GIT_CONFIG") {
2378 paths.push(PathBuf::from(p));
2379 }
2380 paths
2381}
2382
2383fn stamp_for_path(path: &Path) -> Option<(SystemTime, u64)> {
2384 fs::metadata(path)
2385 .ok()
2386 .and_then(|m| Some((m.modified().ok()?, m.len())))
2387}
2388
2389fn stamp_paths(paths: Vec<PathBuf>) -> Vec<ConfigFileStamp> {
2390 paths
2391 .into_iter()
2392 .map(|path| {
2393 let stamp = stamp_for_path(&path);
2394 (path, stamp)
2395 })
2396 .collect()
2397}
2398
2399fn config_file_stamps(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Vec<ConfigFileStamp> {
2400 stamp_paths(config_cascade_file_paths(git_dir, opts))
2401}
2402
2403fn config_cache_lookup(
2404 key: &ConfigCacheKey,
2405 env_fp: &[(String, Option<String>)],
2406 base_stamps: &[ConfigFileStamp],
2407) -> Option<ConfigSet> {
2408 let cache = config_cache()
2409 .lock()
2410 .unwrap_or_else(std::sync::PoisonError::into_inner);
2411 let entry = cache.get(key)?;
2412 if entry.env_fingerprint.as_slice() != env_fp || entry.base_stamps.as_slice() != base_stamps {
2413 return None;
2414 }
2415 for (path, stamp) in &entry.extra_stamps {
2416 if stamp_for_path(path) != *stamp {
2417 return None;
2418 }
2419 }
2420 Some((*entry.set).clone())
2421}
2422
2423fn evict_config_cache_for_path(path: &Path) {
2425 let mut cache = config_cache()
2426 .lock()
2427 .unwrap_or_else(std::sync::PoisonError::into_inner);
2428 cache.retain(|_, entry| {
2429 !entry
2430 .base_stamps
2431 .iter()
2432 .chain(&entry.extra_stamps)
2433 .any(|(p, _)| p == path)
2434 });
2435}
2436
2437fn git_config_nosystem_enabled() -> bool {
2438 std::env::var("GIT_CONFIG_NOSYSTEM")
2439 .ok()
2440 .map(|value| parse_bool(&value).unwrap_or(true))
2441 .unwrap_or(false)
2442}
2443
2444fn add_environment_config_pairs(set: &mut ConfigSet) -> Result<()> {
2445 let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") else {
2446 return Ok(());
2447 };
2448 if count_str.is_empty() {
2449 return Ok(());
2450 }
2451
2452 let count = count_str
2453 .parse::<usize>()
2454 .map_err(|_| Error::ConfigError("bogus count in GIT_CONFIG_COUNT".to_owned()))?;
2455 if count > i32::MAX as usize {
2456 return Err(Error::ConfigError(
2457 "too many entries in GIT_CONFIG_COUNT".to_owned(),
2458 ));
2459 }
2460
2461 for i in 0..count {
2462 let key_var = format!("GIT_CONFIG_KEY_{i}");
2463 let value_var = format!("GIT_CONFIG_VALUE_{i}");
2464 let key = std::env::var(&key_var)
2465 .map_err(|_| Error::ConfigError(format!("missing config key {key_var}")))?;
2466 let value = std::env::var(&value_var)
2467 .map_err(|_| Error::ConfigError(format!("missing config value {value_var}")))?;
2468 set.add_command_override(&key, &value)?;
2469 }
2470
2471 Ok(())
2472}
2473
2474pub fn parse_bool(s: &str) -> std::result::Result<bool, String> {
2487 match s.to_lowercase().as_str() {
2488 "true" | "yes" | "on" => Ok(true),
2489 "" => Ok(false),
2490 "false" | "no" | "off" => Ok(false),
2491 _ => {
2492 if let Ok(n) = parse_i64(s) {
2494 return Ok(n != 0);
2495 }
2496 Err(format!("bad boolean config value '{s}'"))
2497 }
2498 }
2499}
2500
2501pub fn parse_i64(s: &str) -> std::result::Result<i64, String> {
2503 let s = s.trim();
2504 if s.is_empty() {
2505 return Err("empty integer value".to_owned());
2506 }
2507
2508 let (num_str, multiplier) = match s.as_bytes().last() {
2509 Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
2510 Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
2511 Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
2512 _ => (s, 1_i64),
2513 };
2514
2515 let base: i64 = num_str
2516 .parse()
2517 .map_err(|_| format!("invalid integer: '{s}'"))?;
2518 base.checked_mul(multiplier)
2519 .ok_or_else(|| format!("integer overflow: '{s}'"))
2520}
2521
2522#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2524pub enum GitConfigIntStrictError {
2525 InvalidUnit,
2527 OutOfRange,
2529}
2530
2531pub fn parse_git_config_int_strict(raw: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
2535 let s = raw.trim();
2536 if s.is_empty() {
2537 return Err(GitConfigIntStrictError::InvalidUnit);
2538 }
2539
2540 let bytes = s.as_bytes();
2541 let mut idx = 0usize;
2542 if matches!(bytes.first(), Some(b'+') | Some(b'-')) {
2543 idx = 1;
2544 }
2545 if idx >= bytes.len() {
2546 return Err(GitConfigIntStrictError::InvalidUnit);
2547 }
2548 let digit_start = idx;
2549 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
2550 idx += 1;
2551 }
2552 if idx == digit_start {
2553 return Err(GitConfigIntStrictError::InvalidUnit);
2554 }
2555
2556 let num_part =
2557 std::str::from_utf8(&bytes[..idx]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2558 let suffix =
2559 std::str::from_utf8(&bytes[idx..]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2560 let mult: i64 = match suffix {
2561 "" => 1,
2562 "k" | "K" => 1024,
2563 "m" | "M" => 1024 * 1024,
2564 "g" | "G" => 1024_i64
2565 .checked_mul(1024)
2566 .and_then(|x| x.checked_mul(1024))
2567 .ok_or(GitConfigIntStrictError::OutOfRange)?,
2568 _ => return Err(GitConfigIntStrictError::InvalidUnit),
2569 };
2570
2571 let val: i64 = num_part
2572 .parse()
2573 .map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2574 val.checked_mul(mult)
2575 .ok_or(GitConfigIntStrictError::OutOfRange)
2576}
2577
2578const DIFF_CONTEXT_KEY: &str = "diff.context";
2579
2580fn format_bad_numeric_diff_context(
2581 value: &str,
2582 err: GitConfigIntStrictError,
2583 entry: &ConfigEntry,
2584) -> String {
2585 let detail = match err {
2586 GitConfigIntStrictError::InvalidUnit => "invalid unit",
2587 GitConfigIntStrictError::OutOfRange => "out of range",
2588 };
2589 if entry.scope == ConfigScope::Command || entry.file.is_none() {
2590 return format!(
2591 "fatal: bad numeric config value '{value}' for '{DIFF_CONTEXT_KEY}': {detail}"
2592 );
2593 }
2594 let path = entry
2595 .file
2596 .as_deref()
2597 .map(config_error_path_display)
2598 .unwrap_or_default();
2599 format!("fatal: bad numeric config value '{value}' for '{DIFF_CONTEXT_KEY}' in file {path}: {detail}")
2600}
2601
2602fn format_bad_diff_context_variable(entry: &ConfigEntry) -> String {
2603 if entry.scope == ConfigScope::Command || entry.file.is_none() {
2604 return format!("fatal: unable to parse '{DIFF_CONTEXT_KEY}' from command-line config");
2605 }
2606 let path = entry
2607 .file
2608 .as_deref()
2609 .map(config_error_path_display)
2610 .unwrap_or_default();
2611 format!(
2612 "fatal: bad config variable '{DIFF_CONTEXT_KEY}' in file '{path}' at line {}",
2613 entry.line
2614 )
2615}
2616
2617pub fn resolve_diff_context_lines(cfg: &ConfigSet) -> std::result::Result<Option<usize>, String> {
2622 let Some(entry) = cfg.get_last_entry(DIFF_CONTEXT_KEY) else {
2623 return Ok(None);
2624 };
2625 let value_src = entry.value.as_deref().unwrap_or("").trim();
2626 match parse_git_config_int_strict(value_src) {
2627 Ok(n) if n < 0 => Err(format_bad_diff_context_variable(&entry)),
2628 Ok(n) => Ok(Some(usize::try_from(n).map_err(|_| {
2629 format_bad_numeric_diff_context(value_src, GitConfigIntStrictError::OutOfRange, &entry)
2630 })?)),
2631 Err(e) => Err(format_bad_numeric_diff_context(value_src, e, &entry)),
2632 }
2633}
2634
2635pub fn parse_color(s: &str) -> std::result::Result<String, String> {
2642 const COLOR_BACKGROUND_OFFSET: i32 = 10;
2643 const COLOR_FOREGROUND_ANSI: i32 = 30;
2644 const COLOR_FOREGROUND_RGB: i32 = 38;
2645 const COLOR_FOREGROUND_256: i32 = 38;
2646 const COLOR_FOREGROUND_BRIGHT_ANSI: i32 = 90;
2647
2648 #[derive(Clone, Copy, Default)]
2649 struct Color {
2650 kind: u8,
2651 value: u8,
2652 red: u8,
2653 green: u8,
2654 blue: u8,
2655 }
2656
2657 const COLOR_UNSPECIFIED: u8 = 0;
2658 const COLOR_NORMAL: u8 = 1;
2659 const COLOR_ANSI: u8 = 2;
2660 const COLOR_256: u8 = 3;
2661 const COLOR_RGB: u8 = 4;
2662
2663 fn color_empty(c: &Color) -> bool {
2664 c.kind == COLOR_UNSPECIFIED || c.kind == COLOR_NORMAL
2665 }
2666
2667 fn parse_ansi_color(name: &str) -> Option<Color> {
2668 let color_names = [
2669 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
2670 ];
2671 let color_offset = COLOR_FOREGROUND_ANSI;
2672
2673 if name.eq_ignore_ascii_case("default") {
2674 return Some(Color {
2675 kind: COLOR_ANSI,
2676 value: (9 + color_offset) as u8,
2677 ..Default::default()
2678 });
2679 }
2680
2681 let (name, color_offset) = if name.len() >= 6 && name[..6].eq_ignore_ascii_case("bright") {
2682 (&name[6..], COLOR_FOREGROUND_BRIGHT_ANSI)
2683 } else {
2684 (name, COLOR_FOREGROUND_ANSI)
2685 };
2686
2687 for (i, cn) in color_names.iter().enumerate() {
2688 if name.eq_ignore_ascii_case(cn) {
2689 return Some(Color {
2690 kind: COLOR_ANSI,
2691 value: (i as i32 + color_offset) as u8,
2692 ..Default::default()
2693 });
2694 }
2695 }
2696 None
2697 }
2698
2699 fn hex_val(b: u8) -> Option<u8> {
2700 match b {
2701 b'0'..=b'9' => Some(b - b'0'),
2702 b'a'..=b'f' => Some(b - b'a' + 10),
2703 b'A'..=b'F' => Some(b - b'A' + 10),
2704 _ => None,
2705 }
2706 }
2707
2708 fn get_hex_color(chars: &[u8], width: usize) -> Option<(u8, usize)> {
2709 assert!(width == 1 || width == 2);
2710 if chars.len() < width {
2711 return None;
2712 }
2713 let v = if width == 2 {
2714 let hi = hex_val(chars[0])?;
2715 let lo = hex_val(chars[1])?;
2716 (hi << 4) | lo
2717 } else {
2718 let n = hex_val(chars[0])?;
2719 (n << 4) | n
2720 };
2721 Some((v, width))
2722 }
2723
2724 fn parse_single_color(word: &str) -> Option<Color> {
2725 if word.eq_ignore_ascii_case("normal") {
2726 return Some(Color {
2727 kind: COLOR_NORMAL,
2728 ..Default::default()
2729 });
2730 }
2731
2732 let bytes = word.as_bytes();
2733 if (bytes.len() == 7 || bytes.len() == 4) && bytes.first() == Some(&b'#') {
2734 let width = if bytes.len() == 7 { 2 } else { 1 };
2735 let mut idx = 1;
2736 let (r, n1) = get_hex_color(&bytes[idx..], width)?;
2737 idx += n1;
2738 let (g, n2) = get_hex_color(&bytes[idx..], width)?;
2739 idx += n2;
2740 let (b, n3) = get_hex_color(&bytes[idx..], width)?;
2741 idx += n3;
2742 if idx != bytes.len() {
2743 return None;
2744 }
2745 return Some(Color {
2746 kind: COLOR_RGB,
2747 red: r,
2748 green: g,
2749 blue: b,
2750 ..Default::default()
2751 });
2752 }
2753
2754 if let Some(c) = parse_ansi_color(word) {
2755 return Some(c);
2756 }
2757
2758 let Ok(val) = word.parse::<i64>() else {
2759 return None;
2760 };
2761 if val < -1 {
2762 return None;
2763 }
2764 if val < 0 {
2765 return Some(Color {
2766 kind: COLOR_NORMAL,
2767 ..Default::default()
2768 });
2769 }
2770 if val < 8 {
2771 return Some(Color {
2772 kind: COLOR_ANSI,
2773 value: (val as i32 + COLOR_FOREGROUND_ANSI) as u8,
2774 ..Default::default()
2775 });
2776 }
2777 if val < 16 {
2778 return Some(Color {
2779 kind: COLOR_ANSI,
2780 value: (val as i32 - 8 + COLOR_FOREGROUND_BRIGHT_ANSI) as u8,
2781 ..Default::default()
2782 });
2783 }
2784 if val < 256 {
2785 return Some(Color {
2786 kind: COLOR_256,
2787 value: val as u8,
2788 ..Default::default()
2789 });
2790 }
2791 None
2792 }
2793
2794 fn parse_attr(word: &str) -> Option<u8> {
2795 const ATTRS: [(&str, u8, u8); 8] = [
2796 ("bold", 1, 22),
2797 ("dim", 2, 22),
2798 ("italic", 3, 23),
2799 ("ul", 4, 24),
2800 ("underline", 4, 24),
2801 ("blink", 5, 25),
2802 ("reverse", 7, 27),
2803 ("strike", 9, 29),
2804 ];
2805
2806 let mut negate = false;
2807 let mut rest = word;
2808 if let Some(stripped) = rest.strip_prefix("no") {
2809 negate = true;
2810 rest = stripped;
2811 if let Some(s) = rest.strip_prefix('-') {
2812 rest = s;
2813 }
2814 }
2815
2816 for (name, val, neg) in ATTRS {
2817 if rest == name {
2818 return Some(if negate { neg } else { val });
2819 }
2820 }
2821 None
2822 }
2823
2824 fn append_color_output(out: &mut String, c: &Color, background: bool) {
2825 let offset = if background {
2826 COLOR_BACKGROUND_OFFSET
2827 } else {
2828 0
2829 };
2830 match c.kind {
2831 COLOR_UNSPECIFIED | COLOR_NORMAL => {}
2832 COLOR_ANSI => {
2833 use std::fmt::Write;
2834 let _ = write!(out, "{}", i32::from(c.value) + offset);
2835 }
2836 COLOR_256 => {
2837 use std::fmt::Write;
2838 let _ = write!(out, "{};5;{}", COLOR_FOREGROUND_256 + offset, c.value);
2839 }
2840 COLOR_RGB => {
2841 use std::fmt::Write;
2842 let _ = write!(
2843 out,
2844 "{};2;{};{};{}",
2845 COLOR_FOREGROUND_RGB + offset,
2846 c.red,
2847 c.green,
2848 c.blue
2849 );
2850 }
2851 _ => {}
2852 }
2853 }
2854
2855 let s = s.trim();
2856 if s.is_empty() {
2857 return Ok(String::new());
2858 }
2859
2860 let mut has_reset = false;
2861 let mut attr: u64 = 0;
2862 let mut fg = Color::default();
2863 let mut bg = Color::default();
2864 fg.kind = COLOR_UNSPECIFIED;
2865 bg.kind = COLOR_UNSPECIFIED;
2866
2867 for word in s.split_whitespace() {
2868 if word.eq_ignore_ascii_case("reset") {
2869 has_reset = true;
2870 continue;
2871 }
2872
2873 if let Some(c) = parse_single_color(word) {
2874 if fg.kind == COLOR_UNSPECIFIED {
2875 fg = c;
2876 continue;
2877 }
2878 if bg.kind == COLOR_UNSPECIFIED {
2879 bg = c;
2880 continue;
2881 }
2882 return Err(format!("bad color value '{s}'"));
2883 }
2884
2885 if let Some(code) = parse_attr(word) {
2886 attr |= 1u64 << u64::from(code);
2887 continue;
2888 }
2889
2890 return Err(format!("bad color value '{s}'"));
2891 }
2892
2893 if !has_reset && attr == 0 && color_empty(&fg) && color_empty(&bg) {
2894 return Err(format!("bad color value '{s}'"));
2895 }
2896
2897 let mut out = String::from("\x1b[");
2898 let mut sep = if has_reset { 1u32 } else { 0u32 };
2899
2900 let mut attr_bits = attr;
2901 let mut i = 0u32;
2902 while attr_bits != 0 {
2903 let bit = 1u64 << i;
2904 if attr_bits & bit == 0 {
2905 i += 1;
2906 continue;
2907 }
2908 attr_bits &= !bit;
2909 if sep > 0 {
2910 out.push(';');
2911 }
2912 sep += 1;
2913 use std::fmt::Write;
2914 let _ = write!(out, "{i}");
2915 i += 1;
2916 }
2917
2918 if !color_empty(&fg) {
2919 if sep > 0 {
2920 out.push(';');
2921 }
2922 sep += 1;
2923 append_color_output(&mut out, &fg, false);
2924 }
2925 if !color_empty(&bg) {
2926 if sep > 0 {
2927 out.push(';');
2928 }
2929 append_color_output(&mut out, &bg, true);
2930 }
2931 out.push('m');
2932 Ok(out)
2933}
2934
2935#[derive(Debug, Clone)]
2936struct UrlParts {
2937 scheme: String,
2938 user: Option<String>,
2939 host: String,
2940 port: Option<String>,
2941 path: String,
2942}
2943
2944#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2945struct UrlMatchScore {
2946 host_len: usize,
2947 path_len: usize,
2948 user_matched: bool,
2949}
2950
2951fn parse_config_url(url: &str) -> Option<UrlParts> {
2952 let (scheme, rest) = url.split_once("://")?;
2953 let (authority, path) = match rest.find('/') {
2954 Some(idx) => (&rest[..idx], &rest[idx..]),
2955 None => (rest, "/"),
2956 };
2957 let (user, host_port) = match authority.rsplit_once('@') {
2958 Some((user, host)) => (Some(user.to_owned()), host),
2959 None => (None, authority),
2960 };
2961 let (host, port) = match host_port.rsplit_once(':') {
2962 Some((host, port)) if !host.contains(']') => (host, Some(port.to_owned())),
2963 _ => (host_port, None),
2964 };
2965 Some(UrlParts {
2966 scheme: scheme.to_lowercase(),
2967 user,
2968 host: host.to_lowercase(),
2969 port,
2970 path: if path.is_empty() {
2971 "/".to_owned()
2972 } else {
2973 path.trim_end_matches('/').to_owned()
2974 },
2975 })
2976}
2977
2978fn host_matches(pattern: &str, target: &str) -> bool {
2979 let pattern_parts: Vec<&str> = pattern.split('.').collect();
2980 let target_parts: Vec<&str> = target.split('.').collect();
2981 pattern_parts.len() == target_parts.len()
2982 && pattern_parts
2983 .iter()
2984 .zip(target_parts)
2985 .all(|(pattern, target)| *pattern == "*" || *pattern == target)
2986}
2987
2988fn path_match_len(pattern: &str, target: &str) -> Option<usize> {
2989 let pattern = if pattern.is_empty() { "/" } else { pattern };
2990 let target = if target.is_empty() { "/" } else { target };
2991 if pattern == "/" {
2992 return Some(1);
2993 }
2994 let pattern = pattern.trim_end_matches('/');
2995 if target == pattern
2996 || target
2997 .strip_prefix(pattern)
2998 .is_some_and(|rest| rest.starts_with('/'))
2999 {
3000 Some(pattern.len() + 1)
3001 } else {
3002 None
3003 }
3004}
3005
3006fn url_match_score(pattern_url: &str, target_url: &str) -> Option<UrlMatchScore> {
3007 let pattern = parse_config_url(pattern_url)?;
3008 let target = parse_config_url(target_url)?;
3009 if pattern.scheme != target.scheme {
3010 return None;
3011 }
3012 let user_matched = match pattern.user.as_deref() {
3013 Some(user) if target.user.as_deref() == Some(user) => true,
3014 Some(_) => return None,
3015 None => false,
3016 };
3017 if !host_matches(&pattern.host, &target.host) || pattern.port != target.port {
3018 return None;
3019 }
3020 let path_len = path_match_len(&pattern.path, &target.path)?;
3021 Some(UrlMatchScore {
3022 host_len: pattern.host.len(),
3023 path_len,
3024 user_matched,
3025 })
3026}
3027
3028pub fn url_matches(pattern_url: &str, target_url: &str) -> bool {
3030 url_match_score(pattern_url, target_url).is_some()
3031}
3032
3033pub fn get_urlmatch_entries<'a>(
3035 entries: &'a [ConfigEntry],
3036 section: &str,
3037 variable: &str,
3038 url: &str,
3039) -> Vec<&'a ConfigEntry> {
3040 let section_lower = section.to_lowercase();
3041 let variable_lower = variable.to_lowercase();
3042 let mut matches: Vec<(UrlMatchScore, &'a ConfigEntry)> = Vec::new();
3043
3044 for entry in entries {
3045 let key = &entry.key;
3046 let first_dot = match key.find('.') {
3047 Some(i) => i,
3048 None => continue,
3049 };
3050 let last_dot = match key.rfind('.') {
3051 Some(i) => i,
3052 None => continue,
3053 };
3054 let entry_section = &key[..first_dot];
3055 let entry_variable = &key[last_dot + 1..];
3056 if entry_section.to_lowercase() != section_lower
3057 || entry_variable.to_lowercase() != variable_lower
3058 {
3059 continue;
3060 }
3061 if first_dot == last_dot {
3062 matches.push((
3063 UrlMatchScore {
3064 host_len: 0,
3065 path_len: 0,
3066 user_matched: false,
3067 },
3068 entry,
3069 ));
3070 } else {
3071 let subsection = &key[first_dot + 1..last_dot];
3072 if let Some(score) = url_match_score(subsection, url) {
3073 matches.push((score, entry));
3074 }
3075 }
3076 }
3077 matches.sort_by_key(|a| a.0);
3078 matches.into_iter().map(|(_, e)| e).collect()
3079}
3080
3081pub fn get_urlmatch_all_in_section(
3083 entries: &[ConfigEntry],
3084 section: &str,
3085 url: &str,
3086) -> Vec<(String, String, ConfigScope)> {
3087 let section_lower = section.to_lowercase();
3088 let mut matches: Vec<(String, UrlMatchScore, String, String, ConfigScope)> = Vec::new();
3089
3090 for entry in entries {
3091 let key = &entry.key;
3092 let first_dot = match key.find('.') {
3093 Some(i) => i,
3094 None => continue,
3095 };
3096 let last_dot = match key.rfind('.') {
3097 Some(i) => i,
3098 None => continue,
3099 };
3100 let entry_section = &key[..first_dot];
3101 if entry_section.to_lowercase() != section_lower {
3102 continue;
3103 }
3104 let entry_variable = &key[last_dot + 1..];
3105 let val = entry.value.as_deref().unwrap_or("");
3106 if first_dot == last_dot {
3107 let canonical = format!("{}.{}", section_lower, entry_variable);
3108 matches.push((
3109 entry_variable.to_lowercase(),
3110 UrlMatchScore {
3111 host_len: 0,
3112 path_len: 0,
3113 user_matched: false,
3114 },
3115 val.to_owned(),
3116 canonical,
3117 entry.scope,
3118 ));
3119 } else {
3120 let subsection = &key[first_dot + 1..last_dot];
3121 if let Some(score) = url_match_score(subsection, url) {
3122 let canonical = format!("{}.{}", section_lower, entry_variable);
3123 matches.push((
3124 entry_variable.to_lowercase(),
3125 score,
3126 val.to_owned(),
3127 canonical,
3128 entry.scope,
3129 ));
3130 }
3131 }
3132 }
3133
3134 let mut best: std::collections::BTreeMap<String, (UrlMatchScore, String, String, ConfigScope)> =
3135 std::collections::BTreeMap::new();
3136 for (var, specificity, val, canonical, scope) in matches {
3137 let entry = best.entry(var).or_insert((
3138 UrlMatchScore {
3139 host_len: 0,
3140 path_len: 0,
3141 user_matched: false,
3142 },
3143 String::new(),
3144 String::new(),
3145 scope,
3146 ));
3147 if specificity >= entry.0 {
3148 *entry = (specificity, val, canonical, scope);
3149 }
3150 }
3151 best.into_values()
3152 .map(|(_, val, canonical, scope)| (canonical, val, scope))
3153 .collect()
3154}
3155
3156pub fn parse_path(s: &str) -> String {
3160 if let Some(rest) = s.strip_prefix("~/") {
3161 if let Some(home) = home_dir() {
3162 return home.join(rest).to_string_lossy().to_string();
3163 }
3164 }
3165 s.to_owned()
3166}
3167
3168pub fn parse_path_optional(s: &str) -> Option<String> {
3173 if let Some(rest) = s.strip_prefix(":(optional)") {
3174 let resolved = parse_path(rest);
3175 if std::path::Path::new(&resolved).exists() {
3176 Some(resolved)
3177 } else {
3178 None }
3180 } else {
3181 Some(parse_path(s))
3182 }
3183}
3184
3185#[must_use]
3201pub fn git_config_parameters_last_value(raw: &str, key: &str) -> Option<String> {
3202 let Ok(canon) = canonical_key(key) else {
3203 return None;
3204 };
3205 let mut last: Option<String> = None;
3206 for entry in parse_config_parameters_strict(raw).ok()? {
3207 match entry {
3208 ConfigParameter::Pair { key, value } => {
3209 if canonical_key(key.trim()).ok().as_ref() == Some(&canon) {
3210 last = Some(value.unwrap_or_else(|| "true".to_owned()));
3211 }
3212 }
3213 ConfigParameter::OldStyle(entry) => {
3214 if let Some((k, v)) = entry.split_once('=') {
3215 if canonical_key(k.trim()).ok().as_ref() == Some(&canon) {
3216 last = Some(v.to_owned());
3217 }
3218 } else if canonical_key(entry.trim()).ok().as_ref() == Some(&canon) {
3219 last = Some("true".to_owned());
3220 }
3221 }
3222 }
3223 }
3224 last
3225}
3226
3227#[derive(Debug, Clone, PartialEq, Eq)]
3228enum ConfigParameter {
3229 OldStyle(String),
3230 Pair { key: String, value: Option<String> },
3231}
3232
3233pub fn parse_config_parameters(raw: &str) -> Vec<String> {
3234 parse_config_parameters_strict(raw)
3235 .map(|entries| {
3236 entries
3237 .into_iter()
3238 .map(|entry| match entry {
3239 ConfigParameter::OldStyle(entry) => entry,
3240 ConfigParameter::Pair {
3241 key,
3242 value: Some(value),
3243 } => format!("{key}\u{1}{value}"),
3244 ConfigParameter::Pair { key, value: None } => format!("{key}\u{1}"),
3245 })
3246 .collect()
3247 })
3248 .unwrap_or_default()
3249}
3250
3251fn parse_config_parameters_strict(raw: &str) -> Result<Vec<ConfigParameter>> {
3252 let mut out: Vec<ConfigParameter> = Vec::new();
3253 let chars: Vec<char> = raw.chars().collect();
3254 let mut idx = skip_config_parameter_spaces(&chars, 0);
3255
3256 while idx < chars.len() {
3257 let (key, next) = sq_dequote_step_chars(&chars, idx)?;
3258 let Some(next_idx) = next else {
3259 out.push(ConfigParameter::OldStyle(key));
3260 break;
3261 };
3262
3263 if chars[next_idx].is_whitespace() {
3264 out.push(ConfigParameter::OldStyle(key));
3265 idx = skip_config_parameter_spaces(&chars, next_idx);
3266 continue;
3267 }
3268
3269 if chars[next_idx] != '=' {
3270 return Err(Error::ConfigError(
3271 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3272 ));
3273 }
3274
3275 let value_start = next_idx + 1;
3276 if value_start >= chars.len() || chars[value_start].is_whitespace() {
3277 out.push(ConfigParameter::Pair { key, value: None });
3278 idx = skip_config_parameter_spaces(&chars, value_start);
3279 continue;
3280 }
3281
3282 if chars[value_start] != '\'' {
3283 return Err(Error::ConfigError(
3284 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3285 ));
3286 }
3287 let (value, value_next) = sq_dequote_step_chars(&chars, value_start)?;
3288 if let Some(value_next) = value_next {
3289 if !chars[value_next].is_whitespace() {
3290 return Err(Error::ConfigError(
3291 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3292 ));
3293 }
3294 idx = skip_config_parameter_spaces(&chars, value_next);
3295 } else {
3296 idx = chars.len();
3297 }
3298 out.push(ConfigParameter::Pair {
3299 key,
3300 value: Some(value),
3301 });
3302 }
3303
3304 Ok(out)
3305}
3306
3307fn skip_config_parameter_spaces(chars: &[char], mut idx: usize) -> usize {
3308 while idx < chars.len() && chars[idx].is_whitespace() {
3309 idx += 1;
3310 }
3311 idx
3312}
3313
3314fn sq_dequote_step_chars(chars: &[char], start: usize) -> Result<(String, Option<usize>)> {
3315 if chars.get(start) != Some(&'\'') {
3316 return Err(Error::ConfigError(
3317 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3318 ));
3319 }
3320
3321 let mut out = String::new();
3322 let mut idx = start + 1;
3323 loop {
3324 let Some(&ch) = chars.get(idx) else {
3325 return Err(Error::ConfigError(
3326 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3327 ));
3328 };
3329 if ch != '\'' {
3330 out.push(ch);
3331 idx += 1;
3332 continue;
3333 }
3334
3335 idx += 1;
3336 match chars.get(idx).copied() {
3337 None => return Ok((out, None)),
3338 Some('\\')
3339 if chars
3340 .get(idx + 1)
3341 .copied()
3342 .is_some_and(needs_sq_backslash_quote)
3343 && chars.get(idx + 2) == Some(&'\'') =>
3344 {
3345 if let Some(escaped) = chars.get(idx + 1) {
3346 out.push(*escaped);
3347 }
3348 idx += 3;
3349 }
3350 _ => return Ok((out, Some(idx))),
3351 }
3352 }
3353}
3354
3355fn needs_sq_backslash_quote(ch: char) -> bool {
3356 ch == '\'' || ch == '!'
3357}
3358
3359pub fn global_config_paths_pub() -> Vec<PathBuf> {
3362 global_config_paths()
3363}
3364
3365fn global_config_paths() -> Vec<PathBuf> {
3366 let mut paths = Vec::new();
3367
3368 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
3370 paths.push(PathBuf::from(p));
3371 return paths;
3372 }
3373
3374 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
3376 paths.push(PathBuf::from(xdg).join("git/config"));
3377 } else if let Some(home) = home_dir() {
3378 paths.push(home.join(".config/git/config"));
3379 }
3380 if let Some(home) = home_dir() {
3381 paths.push(home.join(".gitconfig"));
3382 }
3383
3384 paths
3385}
3386
3387fn home_dir() -> Option<PathBuf> {
3394 if let Some(home) = std::env::var_os("HOME").filter(|h| !h.is_empty()) {
3395 return Some(PathBuf::from(home));
3396 }
3397 #[cfg(windows)]
3398 {
3399 if let (Some(drive), Some(path)) = (
3400 std::env::var_os("HOMEDRIVE").filter(|d| !d.is_empty()),
3401 std::env::var_os("HOMEPATH").filter(|p| !p.is_empty()),
3402 ) {
3403 let mut combined = drive;
3404 combined.push(path);
3405 return Some(PathBuf::from(combined));
3406 }
3407 if let Some(profile) = std::env::var_os("USERPROFILE").filter(|p| !p.is_empty()) {
3408 return Some(PathBuf::from(profile));
3409 }
3410 }
3411 None
3412}
3413
3414fn include_source_is_disk_file(file: &ConfigFile) -> bool {
3416 file.include_origin == ConfigIncludeOrigin::Disk
3417}
3418
3419fn resolve_include_file_path(
3423 path: &str,
3424 file: &ConfigFile,
3425 ctx: &IncludeContext,
3426) -> Result<PathBuf> {
3427 let expanded = parse_path(path);
3428 let p = Path::new(&expanded);
3429 if p.is_absolute() {
3430 return Ok(p.to_path_buf());
3431 }
3432 if !include_source_is_disk_file(file) {
3433 if file.include_origin == ConfigIncludeOrigin::CommandLine {
3434 if ctx.command_line_relative_include_is_error {
3435 return Err(Error::ConfigError(
3436 "relative config includes must come from files".to_owned(),
3437 ));
3438 }
3439 return Err(Error::ConfigError(String::new()));
3440 }
3441 return Err(Error::ConfigError(
3442 "relative config includes must come from files".to_owned(),
3443 ));
3444 }
3445 let base = match file.path.parent() {
3446 Some(p) if !p.as_os_str().is_empty() => p,
3447 Some(_) | None => Path::new("."),
3448 };
3449 Ok(base.join(p))
3450}
3451
3452fn is_dir_sep(b: u8) -> bool {
3453 b == b'/' || b == b'\\'
3454}
3455
3456fn add_trailing_starstar_for_dir(pat: &mut String) {
3457 let bytes = pat.as_bytes();
3458 if bytes.last().is_some_and(|&b| is_dir_sep(b)) {
3459 pat.push_str("**");
3460 }
3461}
3462
3463fn prepare_gitdir_pattern(condition: &str, file: &ConfigFile) -> Result<(String, usize)> {
3465 let mut pat = parse_path(condition);
3467 if pat.starts_with("./") || pat.starts_with(".\\") {
3468 if !include_source_is_disk_file(file) {
3469 return Err(Error::ConfigError(
3470 "relative config include conditionals must come from files".to_owned(),
3471 ));
3472 }
3473 let parent = file.path.parent().ok_or_else(|| {
3474 Error::ConfigError(
3475 "relative config include conditionals must come from files".to_owned(),
3476 )
3477 })?;
3478 let real = parent.canonicalize().map_err(Error::Io)?;
3479 let mut dir = real.to_string_lossy().into_owned();
3480 if !dir.ends_with('/') && !dir.ends_with('\\') {
3481 dir.push('/');
3482 }
3483 let rest = &pat[2..];
3484 pat = format!("{dir}{rest}");
3485 let prefix_len = dir.len();
3486 add_trailing_starstar_for_dir(&mut pat);
3487 return Ok((pat, prefix_len));
3488 }
3489 let p = Path::new(&pat);
3490 if !p.is_absolute() {
3491 pat.insert_str(0, "**/");
3492 }
3493 add_trailing_starstar_for_dir(&mut pat);
3494 Ok((pat, 0))
3495}
3496
3497fn git_dir_match_texts(git_dir: &Path) -> (String, String) {
3502 let real = git_dir
3503 .canonicalize()
3504 .map(|p| p.to_string_lossy().into_owned())
3505 .unwrap_or_else(|_| git_dir.to_string_lossy().into_owned());
3506 let abs = if git_dir.is_absolute() {
3509 let pwd_abs = std::env::var("PWD").ok().and_then(|pwd| {
3512 let pwd_path = std::path::Path::new(&pwd);
3513 if !pwd_path.is_absolute() {
3514 return None;
3515 }
3516 let pwd_canon = pwd_path.canonicalize().ok()?;
3517 let git_dir_str = git_dir.to_string_lossy();
3518 let pwd_canon_str = pwd_canon.to_string_lossy();
3519 let suffix = git_dir_str.strip_prefix(pwd_canon_str.as_ref())?;
3521 Some(format!("{pwd}{suffix}"))
3522 });
3523 pwd_abs.unwrap_or_else(|| git_dir.to_string_lossy().into_owned())
3524 } else if let Ok(cwd) = std::env::current_dir() {
3525 cwd.join(git_dir).to_string_lossy().into_owned()
3526 } else {
3527 git_dir.to_string_lossy().into_owned()
3528 };
3529 (real, abs)
3530}
3531
3532fn include_by_gitdir(
3533 condition: &str,
3534 file: &ConfigFile,
3535 ctx: &IncludeContext,
3536 icase: bool,
3537) -> bool {
3538 let Some(git_dir) = ctx.git_dir.as_ref() else {
3539 return false;
3540 };
3541 let (pattern, prefix) = match prepare_gitdir_pattern(condition, file) {
3542 Ok(x) => x,
3543 Err(_) => return false,
3544 };
3545 let flags = WM_PATHNAME | if icase { WM_CASEFOLD } else { 0 };
3546 let (text_real, text_abs) = git_dir_match_texts(git_dir);
3547 let try_match = |text: &str| -> bool {
3548 let t = text.as_bytes();
3549 let p = pattern.as_bytes();
3550 if prefix > 0 {
3551 if t.len() < prefix {
3552 return false;
3553 }
3554 let pre = &p[..prefix];
3555 let te = &t[..prefix];
3556 let ok = if icase {
3557 pre.eq_ignore_ascii_case(te)
3558 } else {
3559 pre == te
3560 };
3561 if !ok {
3562 return false;
3563 }
3564 return wildmatch(&p[prefix..], &t[prefix..], flags);
3565 }
3566 wildmatch(p, t, flags)
3567 };
3568 if try_match(&text_real) {
3569 return true;
3570 }
3571 text_real != text_abs && try_match(&text_abs)
3572}
3573
3574fn current_branch_short_name(git_dir: Option<&Path>) -> Option<String> {
3575 let gd = git_dir?;
3576 let target = refs::read_symbolic_ref(gd, "HEAD").ok()??;
3577 let rest = target.strip_prefix("refs/heads/")?;
3578 Some(rest.to_owned())
3579}
3580
3581fn include_by_onbranch(condition: &str, ctx: &IncludeContext) -> bool {
3582 let Some(short) = current_branch_short_name(ctx.git_dir.as_deref()) else {
3583 return false;
3584 };
3585 let mut pattern = condition.to_owned();
3586 add_trailing_starstar_for_dir(&mut pattern);
3587 wildmatch(pattern.as_bytes(), short.as_bytes(), WM_PATHNAME)
3588}
3589
3590fn is_remote_url_entry(entry: &ConfigEntry) -> bool {
3591 let Ok((section, subsection, variable)) = split_key(&entry.key) else {
3592 return false;
3593 };
3594 section == "remote" && subsection.is_some() && variable == "url"
3595}
3596
3597fn is_hasconfig_remote_url(condition: &str) -> bool {
3598 condition
3599 .strip_prefix("hasconfig:")
3600 .is_some_and(|rest| rest.starts_with("remote.*.url:"))
3601}
3602
3603fn include_by_hasconfig_remote_url(condition: &str, set: &ConfigSet, file: &ConfigFile) -> bool {
3604 let Some(pattern) = condition.strip_prefix("remote.*.url:") else {
3605 return false;
3606 };
3607 set.entries
3608 .iter()
3609 .chain(file.entries.iter())
3610 .filter(|entry| is_remote_url_entry(entry))
3611 .filter_map(|entry| entry.value.as_deref())
3612 .any(|value| wildmatch(pattern.as_bytes(), value.as_bytes(), WM_PATHNAME))
3613}
3614
3615fn validate_hasconfig_remote_url_include(
3616 file: &ConfigFile,
3617 process_includes: bool,
3618 depth: usize,
3619 ctx: &IncludeContext,
3620) -> Result<()> {
3621 const MAX_INCLUDE_DEPTH: usize = 10;
3622 if depth > MAX_INCLUDE_DEPTH {
3623 return Err(Error::ConfigError(
3624 "exceeded maximum include depth".to_owned(),
3625 ));
3626 }
3627 if file.entries.iter().any(is_remote_url_entry) {
3628 return Err(Error::Message(
3629 "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url"
3630 .to_owned(),
3631 ));
3632 }
3633 if !process_includes {
3634 return Ok(());
3635 }
3636 for entry in &file.entries {
3637 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
3638 continue;
3639 };
3640 if let Some(ref cond) = condition {
3641 if !evaluate_include_condition(cond, &ConfigSet::new(), file, ctx) {
3642 continue;
3643 }
3644 }
3645 let resolved = match resolve_include_file_path(&inc_path, file, ctx) {
3646 Ok(p) => p,
3647 Err(Error::ConfigError(msg)) if msg.is_empty() => continue,
3648 Err(e) => return Err(e),
3649 };
3650 if let Some(inc_file) = ConfigFile::from_path(&resolved, file.scope)? {
3651 validate_hasconfig_remote_url_include(&inc_file, process_includes, depth + 1, ctx)?;
3652 }
3653 }
3654 Ok(())
3655}
3656
3657fn evaluate_include_condition(
3662 condition: &str,
3663 set: &ConfigSet,
3664 file: &ConfigFile,
3665 ctx: &IncludeContext,
3666) -> bool {
3667 if let Some(rest) = condition.strip_prefix("gitdir/i:") {
3668 return include_by_gitdir(rest, file, ctx, true);
3669 }
3670 if let Some(rest) = condition.strip_prefix("gitdir:") {
3671 return include_by_gitdir(rest, file, ctx, false);
3672 }
3673 if let Some(rest) = condition.strip_prefix("onbranch:") {
3674 return include_by_onbranch(rest, ctx);
3675 }
3676 if let Some(rest) = condition.strip_prefix("hasconfig:") {
3677 return include_by_hasconfig_remote_url(rest, set, file);
3678 }
3679 false
3680}
3681
3682fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
3684 let first_dot = key
3685 .find('.')
3686 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
3687 let last_dot = key
3688 .rfind('.')
3689 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
3690
3691 let section = key[..first_dot].to_owned();
3692 let variable = key[last_dot + 1..].to_owned();
3693
3694 let subsection = if first_dot == last_dot {
3695 None
3696 } else {
3697 Some(key[first_dot + 1..last_dot].to_owned())
3698 };
3699
3700 Ok((section, subsection, variable))
3701}
3702
3703#[allow(dead_code)]
3705fn variable_name_from_key(key: &str) -> &str {
3706 match key.rfind('.') {
3707 Some(i) => &key[i + 1..],
3708 None => key,
3709 }
3710}
3711
3712fn parse_section_name(name: &str) -> (&str, Option<&str>) {
3716 match name.find('.') {
3717 Some(i) => (&name[..i], Some(&name[i + 1..])),
3718 None => (name, None),
3719 }
3720}
3721
3722fn section_matches(parser: &Parser, section_lower: &str, subsection: Option<&str>) -> bool {
3723 if parser.section.to_lowercase() == section_lower && parser.subsection.as_deref() == subsection
3724 {
3725 return true;
3726 }
3727 let Some(subsection) = subsection else {
3728 return false;
3729 };
3730 parser.subsection.is_none()
3731 && parser.section.to_lowercase() == format!("{section_lower}.{}", subsection.to_lowercase())
3732}
3733
3734fn validate_section_name(section: &str, subsection: Option<&str>) -> Result<()> {
3735 if section.is_empty()
3736 || !section
3737 .chars()
3738 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
3739 || subsection.is_some_and(str::is_empty)
3740 {
3741 return Err(Error::ConfigError(format!(
3742 "invalid section name: {section}"
3743 )));
3744 }
3745 Ok(())
3746}
3747
3748fn raw_variable_name(raw_key: &str) -> &str {
3752 match raw_key.rfind('.') {
3753 Some(i) => &raw_key[i + 1..],
3754 None => raw_key,
3755 }
3756}
3757
3758fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
3763 let first_dot = match raw_key.find('.') {
3764 Some(i) => i,
3765 None => return (raw_key.to_owned(), None),
3766 };
3767 let last_dot = match raw_key.rfind('.') {
3769 Some(i) => i,
3770 None => return (raw_key[..first_dot].to_owned(), None),
3771 };
3772 let section = raw_key[..first_dot].to_owned();
3773 if first_dot == last_dot {
3774 (section, None)
3775 } else {
3776 let subsection = raw_key[first_dot + 1..last_dot].to_owned();
3777 (section, Some(subsection))
3778 }
3779}
3780
3781fn is_section_header_with_inline_entry(line: &str) -> bool {
3783 let trimmed = line.trim();
3784 if !trimmed.starts_with('[') {
3785 return false;
3786 }
3787 let end = match trimmed.find(']') {
3788 Some(i) => i,
3789 None => return false,
3790 };
3791 let after = trimmed[end + 1..].trim();
3792 !after.is_empty() && !after.starts_with('#') && !after.starts_with(';')
3794}
3795
3796fn extract_section_header(line: &str) -> String {
3799 let trimmed = line.trim();
3800 let end = match trimmed.find(']') {
3801 Some(i) => i,
3802 None => return line.to_owned(),
3803 };
3804 trimmed[..=end].to_owned()
3807}
3808
3809#[cfg(test)]
3810mod get_regexp_tests {
3811 use super::{ConfigFile, ConfigScope, ConfigSet};
3812 use std::path::Path;
3813
3814 fn set_from_snippet(text: &str) -> ConfigSet {
3815 let path = Path::new(".git/config");
3816 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
3817 let mut set = ConfigSet::new();
3818 set.merge(&file);
3819 set
3820 }
3821
3822 #[test]
3823 fn get_regexp_matches_section_prefix_like_git_config() {
3824 let text = r#"
3825[user]
3826 email = alice@example.com
3827 name = Alice
3828[core]
3829 bare = false
3830"#;
3831 let set = set_from_snippet(text);
3832 let keys: Vec<_> = set
3833 .get_regexp("user")
3834 .expect("valid pattern")
3835 .into_iter()
3836 .map(|e| e.key.as_str())
3837 .collect();
3838 assert!(keys.contains(&"user.email"));
3839 assert!(keys.contains(&"user.name"));
3840 assert!(!keys.iter().any(|k| k.starts_with("core.")));
3841 }
3842
3843 #[test]
3844 fn get_regexp_returns_all_multi_value_entries_in_order() {
3845 let text = r#"
3846[remote "origin"]
3847 url = https://example.com/repo.git
3848 fetch = +refs/heads/*:refs/remotes/origin/*
3849 push = +refs/heads/main:refs/heads/main
3850 push = +refs/heads/develop:refs/heads/develop
3851"#;
3852 let set = set_from_snippet(text);
3853 let matches = set.get_regexp("remote.origin").expect("valid pattern");
3854 let push_vals: Vec<_> = matches
3855 .iter()
3856 .filter(|e| e.key == "remote.origin.push")
3857 .map(|e| e.value.as_deref().unwrap_or(""))
3858 .collect();
3859 assert_eq!(push_vals.len(), 2);
3860 assert_eq!(push_vals[0], "+refs/heads/main:refs/heads/main");
3861 assert_eq!(push_vals[1], "+refs/heads/develop:refs/heads/develop");
3862 }
3863
3864 #[test]
3865 fn get_regexp_dot_matches_any_key() {
3866 let text = r#"
3867[a]
3868 x = 1
3869[b]
3870 y = 2
3871"#;
3872 let set = set_from_snippet(text);
3873 let m = set.get_regexp(".").expect("valid pattern");
3874 assert_eq!(m.len(), 2);
3875 }
3876
3877 #[test]
3878 fn get_regexp_no_match_returns_empty_vec() {
3879 let set = set_from_snippet("[user]\n\tname = x\n");
3880 let m = set.get_regexp("zzz").expect("valid pattern");
3881 assert!(m.is_empty());
3882 }
3883
3884 #[test]
3885 fn get_regexp_invalid_pattern_is_error() {
3886 let set = set_from_snippet("[user]\n\tname = x\n");
3887 let err = set.get_regexp("(").expect_err("unclosed group");
3888 assert!(err.contains("invalid key pattern"), "got: {err}");
3889 }
3890}
3891
3892#[cfg(test)]
3893mod pack_compression_tests {
3894 use super::{ConfigFile, ConfigScope, ConfigSet};
3895 use std::path::Path;
3896
3897 fn set_from_snippet(text: &str) -> ConfigSet {
3898 let path = Path::new(".git/config");
3899 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
3900 let mut set = ConfigSet::new();
3901 set.merge(&file);
3902 set
3903 }
3904
3905 #[test]
3906 fn pack_objects_zlib_level_defaults_to_six() {
3907 let set = ConfigSet::new();
3908 assert_eq!(set.pack_objects_zlib_level().unwrap(), 6);
3909 }
3910
3911 #[test]
3912 fn pack_objects_zlib_level_core_compression() {
3913 let set = set_from_snippet("[core]\n\tcompression = 0\n");
3914 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3915 let set = set_from_snippet("[core]\n\tcompression = 9\n");
3916 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3917 }
3918
3919 #[test]
3920 fn pack_objects_zlib_level_pack_overrides_core() {
3921 let set = set_from_snippet("[core]\n\tcompression = 9\n[pack]\n\tcompression = 0\n");
3922 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3923 let set = set_from_snippet("[core]\n\tcompression = 0\n[pack]\n\tcompression = 9\n");
3924 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3925 }
3926
3927 #[test]
3928 fn pack_objects_zlib_level_later_core_does_not_override_earlier_pack() {
3929 let mut set = ConfigSet::new();
3930 set.merge(
3931 &ConfigFile::parse(
3932 Path::new("a"),
3933 "[pack]\n\tcompression = 9\n",
3934 ConfigScope::Local,
3935 )
3936 .unwrap(),
3937 );
3938 set.merge(
3939 &ConfigFile::parse(
3940 Path::new("b"),
3941 "[core]\n\tcompression = 0\n",
3942 ConfigScope::Local,
3943 )
3944 .unwrap(),
3945 );
3946 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3947 }
3948
3949 #[test]
3950 fn pack_objects_zlib_level_loosecompression_does_not_block_core_pack_level() {
3951 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 0\n");
3952 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3953 }
3954
3955 #[test]
3956 fn pack_objects_zlib_level_pack_wins_after_loose_and_core() {
3957 let set = set_from_snippet(
3958 "[core]\n\tloosecompression = 1\n\tcompression = 0\n[pack]\n\tcompression = 9\n",
3959 );
3960 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3961 }
3962}
3963
3964#[cfg(test)]
3965mod config_cache_tests {
3966 use super::*;
3967 use filetime::FileTime;
3968
3969 fn local_opts(git_dir: &Path) -> LoadConfigOptions {
3970 let mut opts = LoadConfigOptions::default();
3971 opts.include_system = false;
3972 opts.include_ctx.git_dir = Some(git_dir.to_path_buf());
3973 opts
3974 }
3975
3976 fn load_value(git_dir: &Path) -> Option<String> {
3977 let opts = local_opts(git_dir);
3978 let set = ConfigSet::load_with_options(Some(git_dir), &opts).expect("load cascade");
3979 set.get("gritcachetest.value")
3980 }
3981
3982 fn mtime_of(path: &Path) -> FileTime {
3983 FileTime::from_last_modification_time(&fs::metadata(path).expect("stat config"))
3984 }
3985
3986 fn restore_mtime(path: &Path, stamp: FileTime) {
3987 filetime::set_file_mtime(path, stamp).expect("restore mtime");
3988 }
3989
3990 #[test]
3991 fn cache_serves_same_stamp_and_config_write_evicts() {
3992 let td = tempfile::tempdir().expect("tempdir");
3993 let gd = td.path();
3994 let cfg = gd.join("config");
3995 fs::write(&cfg, "[gritcachetest]\n\tvalue = aaa\n").expect("write v1");
3996 let t0 = mtime_of(&cfg);
3997 assert_eq!(load_value(gd).as_deref(), Some("aaa"));
3998
3999 fs::write(&cfg, "[gritcachetest]\n\tvalue = bbb\n").expect("write v2");
4004 restore_mtime(&cfg, t0);
4005 assert_eq!(load_value(gd).as_deref(), Some("aaa"));
4006
4007 let mut file = ConfigFile::from_path(&cfg, ConfigScope::Local)
4009 .expect("read config")
4010 .expect("config exists");
4011 file.set("gritcachetest.value", "ccc").expect("set value");
4012 file.write().expect("persist");
4013 restore_mtime(&cfg, t0);
4014 assert_eq!(load_value(gd).as_deref(), Some("ccc"));
4015 }
4016
4017 #[test]
4018 fn cache_invalidates_on_size_or_existence_change() {
4019 let td = tempfile::tempdir().expect("tempdir");
4020 let gd = td.path();
4021 let cfg = gd.join("config");
4022 assert_eq!(load_value(gd), None);
4024 fs::write(&cfg, "[gritcachetest]\n\tvalue = first\n").expect("create");
4026 assert_eq!(load_value(gd).as_deref(), Some("first"));
4027 let t0 = mtime_of(&cfg);
4029 fs::write(&cfg, "[gritcachetest]\n\tvalue = second-longer\n").expect("rewrite");
4030 restore_mtime(&cfg, t0);
4031 assert_eq!(load_value(gd).as_deref(), Some("second-longer"));
4032 }
4033
4034 #[test]
4035 fn include_targets_are_stamped_and_invalidate() {
4036 let td = tempfile::tempdir().expect("tempdir");
4037 let gd = td.path();
4038 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
4039 let inc = gd.join("extra.conf");
4040 fs::write(&inc, "[gritcachetest]\n\tvalue = one\n").expect("write include v1");
4041 assert_eq!(load_value(gd).as_deref(), Some("one"));
4042
4043 let t0 = mtime_of(&inc);
4047 fs::write(&inc, "[gritcachetest]\n\tvalue = two\n").expect("write include v2");
4048 restore_mtime(&inc, t0);
4049 assert_eq!(load_value(gd).as_deref(), Some("one"));
4050
4051 fs::write(&inc, "[gritcachetest]\n\tvalue = two-longer\n").expect("write include v3");
4053 assert_eq!(load_value(gd).as_deref(), Some("two-longer"));
4054 }
4055
4056 #[test]
4057 fn missing_include_target_is_watched() {
4058 let td = tempfile::tempdir().expect("tempdir");
4059 let gd = td.path();
4060 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
4061 assert_eq!(load_value(gd), None);
4062
4063 fs::write(gd.join("extra.conf"), "[gritcachetest]\n\tvalue = born\n").expect("create");
4066 assert_eq!(load_value(gd).as_deref(), Some("born"));
4067 }
4068
4069 #[test]
4070 fn onbranch_condition_follows_head() {
4071 let td = tempfile::tempdir().expect("tempdir");
4072 let gd = td.path();
4073 fs::write(gd.join("HEAD"), "ref: refs/heads/main\n").expect("write HEAD");
4074 fs::write(
4075 gd.join("config"),
4076 "[includeIf \"onbranch:main\"]\n\tpath = branch.conf\n",
4077 )
4078 .expect("write config");
4079 fs::write(
4080 gd.join("branch.conf"),
4081 "[gritcachetest]\n\tvalue = onmain\n",
4082 )
4083 .expect("write branch config");
4084 assert_eq!(load_value(gd).as_deref(), Some("onmain"));
4085
4086 fs::write(gd.join("HEAD"), "ref: refs/heads/dev\n").expect("rewrite HEAD");
4089 assert_eq!(load_value(gd), None);
4090 }
4091}