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)) => Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?,
1877 Ok(None) => {}
1878 Err(e) => return Err(e),
1879 }
1880 }
1881
1882 for path in global_config_paths() {
1884 match ConfigFile::from_path(&path, ConfigScope::Global) {
1885 Ok(Some(f)) => Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?,
1886 Ok(None) => {}
1887 Err(e) => return Err(e),
1888 }
1889 }
1890
1891 if let Some(gd) = git_dir {
1893 let common_dir = crate::repo::common_git_dir_for_config(gd);
1894 let local_path = common_dir.join("config");
1895 match ConfigFile::from_path(&local_path, ConfigScope::Local) {
1896 Ok(Some(f)) => Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?,
1897 Ok(None) => {}
1898 Err(e) => return Err(e),
1899 }
1900
1901 let wt_path = gd.join("config.worktree");
1904 if crate::repo::worktree_config_enabled(&common_dir) {
1905 match ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
1906 Ok(Some(f)) => Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?,
1907 Ok(None) => {}
1908 Err(e) => return Err(e),
1909 }
1910 }
1911 }
1912
1913 if let Ok(path) = std::env::var("GIT_CONFIG") {
1915 match ConfigFile::from_path(Path::new(&path), ConfigScope::Command) {
1916 Ok(Some(f)) => {
1917 if proc {
1918 Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?;
1919 } else {
1920 set.merge(&f);
1921 }
1922 }
1923 Ok(None) => {}
1924 Err(e) => return Err(e),
1925 }
1926 }
1927
1928 add_environment_config_pairs(&mut set)?;
1929
1930 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
1932 if proc && opts.command_includes && !params.trim().is_empty() {
1933 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
1934 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, ¶ms)?;
1935 Self::merge_with_includes_collect(&mut set, &cmd_file, proc, 0, &ctx, included_files)?;
1936 } else if !params.trim().is_empty() {
1937 for entry in parse_config_parameters(¶ms) {
1938 if let Some((key, val)) =
1939 entry.split_once('\u{1}').or_else(|| entry.split_once('='))
1940 {
1941 let _ = set.add_command_override(key.trim(), val);
1942 } else {
1943 let _ = set.add_command_override(entry.trim(), "true");
1944 }
1945 }
1946 }
1947 }
1948
1949 Ok(set)
1950 }
1951
1952 pub fn read_early_config(git_dir: Option<&Path>, key: &str) -> Result<Vec<String>> {
1964 let mut set = Self::new();
1965 let ctx = IncludeContext {
1966 git_dir: git_dir.map(PathBuf::from),
1967 command_line_relative_include_is_error: false,
1968 };
1969
1970 if !git_config_nosystem_enabled() {
1972 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
1973 .map(std::path::PathBuf::from)
1974 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
1975 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
1976 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1977 }
1978 }
1979
1980 for path in global_config_paths() {
1982 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
1983 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1984 }
1985 }
1986
1987 if let Some(gd) = git_dir {
1988 let common_dir = crate::repo::common_git_dir_for_config(gd);
1989 let local_path = common_dir.join("config");
1991 if let Some(msg) = crate::repo::early_config_ignore_repo_reason(&common_dir) {
1992 eprintln!("warning: ignoring git dir '{}': {}", gd.display(), msg);
1993 } else if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
1994 set.merge_file_with_includes(&f, true, &ctx)?;
1995 }
1996
1997 let wt_path = gd.join("config.worktree");
1999 if crate::repo::worktree_config_enabled(&common_dir) {
2000 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
2001 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2002 }
2003 }
2004 }
2005
2006 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
2008 if !params.trim().is_empty() {
2009 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
2010 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, ¶ms)?;
2011 Self::merge_with_includes(&mut set, &cmd_file, true, 0, &ctx)?;
2012 }
2013 }
2014
2015 Ok(set.get_all(key))
2016 }
2017
2018 pub fn merge_file_with_includes(
2023 &mut self,
2024 file: &ConfigFile,
2025 process_includes: bool,
2026 ctx: &IncludeContext,
2027 ) -> Result<()> {
2028 Self::merge_with_includes(self, file, process_includes, 0, ctx)
2029 }
2030
2031 pub fn load_repo_local_only(git_dir: &Path) -> Result<Self> {
2037 let mut set = Self::new();
2038 let local_path = git_dir.join("config");
2039 let ctx = IncludeContext {
2040 git_dir: Some(git_dir.to_path_buf()),
2041 command_line_relative_include_is_error: false,
2042 };
2043 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2044 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2045 }
2046 Ok(set)
2047 }
2048
2049 pub fn load_protected(include_system: bool) -> Result<Self> {
2060 let mut set = Self::new();
2061 let ctx = IncludeContext {
2062 git_dir: None,
2063 command_line_relative_include_is_error: false,
2064 };
2065
2066 if include_system && !git_config_nosystem_enabled() {
2067 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
2068 .map(std::path::PathBuf::from)
2069 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
2070 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2071 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2072 }
2073 }
2074
2075 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
2076 let path = PathBuf::from(p);
2077 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2078 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2079 }
2080 } else {
2081 let mut global_paths = Vec::new();
2082 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
2083 global_paths.push(PathBuf::from(xdg).join("git/config"));
2084 } else if let Some(home) = home_dir() {
2085 global_paths.push(home.join(".config/git/config"));
2086 }
2087 if let Some(home) = home_dir() {
2088 global_paths.push(home.join(".gitconfig"));
2089 }
2090 for path in global_paths {
2091 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2092 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2093 }
2094 }
2095 }
2096
2097 add_environment_config_pairs(&mut set)?;
2098
2099 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
2100 for entry in parse_config_parameters(¶ms) {
2101 if let Some((key, val)) =
2102 entry.split_once('\u{1}').or_else(|| entry.split_once('='))
2103 {
2104 let _ = set.add_command_override(key.trim(), val);
2105 } else {
2106 let _ = set.add_command_override(entry.trim(), "true");
2107 }
2108 }
2109 }
2110
2111 Ok(set)
2112 }
2113
2114 fn merge_with_includes(
2116 set: &mut Self,
2117 file: &ConfigFile,
2118 process_includes: bool,
2119 depth: usize,
2120 ctx: &IncludeContext,
2121 ) -> Result<()> {
2122 let mut included_files = Vec::new();
2123 Self::merge_with_includes_collect(
2124 set,
2125 file,
2126 process_includes,
2127 depth,
2128 ctx,
2129 &mut included_files,
2130 )
2131 }
2132
2133 fn merge_with_includes_collect(
2137 set: &mut Self,
2138 file: &ConfigFile,
2139 process_includes: bool,
2140 depth: usize,
2141 ctx: &IncludeContext,
2142 included_files: &mut Vec<PathBuf>,
2143 ) -> Result<()> {
2144 const MAX_INCLUDE_DEPTH: usize = 10;
2147 if depth > MAX_INCLUDE_DEPTH {
2148 return Err(Error::ConfigError(
2149 "exceeded maximum include depth".to_owned(),
2150 ));
2151 }
2152 if !process_includes {
2153 set.merge(file);
2154 return Ok(());
2155 }
2156
2157 for entry in &file.entries {
2158 set.entries.push(entry.clone());
2159
2160 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
2161 continue;
2162 };
2163 let included_by_hasconfig = condition.as_deref().is_some_and(is_hasconfig_remote_url);
2164 if condition.is_some() && !included_by_hasconfig {
2165 let cond = condition.as_deref().unwrap_or_default();
2166 if !evaluate_include_condition(cond, set, file, ctx) {
2167 continue;
2168 }
2169 }
2170
2171 let resolved = match resolve_include_file_path(&inc_path, file, ctx) {
2172 Ok(p) => p,
2173 Err(Error::ConfigError(msg)) if msg.is_empty() => continue,
2174 Err(e) => return Err(e),
2175 };
2176 included_files.push(resolved.clone());
2177 let Some(inc_file) = ConfigFile::from_path(&resolved, file.scope)? else {
2181 continue;
2182 };
2183
2184 if included_by_hasconfig {
2185 validate_hasconfig_remote_url_include(&inc_file, process_includes, depth + 1, ctx)?;
2186 let cond = condition.as_deref().unwrap_or_default();
2187 if !evaluate_include_condition(cond, set, file, ctx) {
2188 continue;
2189 }
2190 }
2191
2192 Self::merge_with_includes_collect(
2193 set,
2194 &inc_file,
2195 process_includes,
2196 depth + 1,
2197 ctx,
2198 included_files,
2199 )?;
2200 }
2201
2202 Ok(())
2203 }
2204}
2205
2206fn include_directive_for_entry(entry: &ConfigEntry) -> Option<(String, Option<String>)> {
2207 let val = entry.value.as_ref()?;
2208 if entry.key == "include.path" {
2209 return Some((val.clone(), None));
2210 }
2211 if entry.key.starts_with("includeif.") && entry.key.ends_with(".path") {
2212 let mid = &entry.key["includeif.".len()..entry.key.len() - ".path".len()];
2213 return Some((val.clone(), Some(mid.to_owned())));
2214 }
2215 None
2216}
2217
2218type ConfigFileStamp = (PathBuf, Option<(SystemTime, u64)>);
2245
2246#[derive(PartialEq, Eq, Hash)]
2247struct ConfigCacheKey {
2248 git_dir: Option<PathBuf>,
2249 include_system: bool,
2250 process_includes: bool,
2251 command_includes: bool,
2252 ctx_git_dir: Option<PathBuf>,
2253 ctx_relative_include_is_error: bool,
2254}
2255
2256impl ConfigCacheKey {
2257 fn new(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Self {
2258 Self {
2259 git_dir: git_dir.map(Path::to_path_buf),
2260 include_system: opts.include_system,
2261 process_includes: opts.process_includes,
2262 command_includes: opts.command_includes,
2263 ctx_git_dir: opts.include_ctx.git_dir.clone(),
2264 ctx_relative_include_is_error: opts.include_ctx.command_line_relative_include_is_error,
2265 }
2266 }
2267}
2268
2269struct ConfigCacheEntry {
2270 env_fingerprint: Vec<(String, Option<String>)>,
2271 base_stamps: Vec<ConfigFileStamp>,
2273 extra_stamps: Vec<ConfigFileStamp>,
2276 set: Arc<ConfigSet>,
2277}
2278
2279fn config_cache() -> &'static Mutex<HashMap<ConfigCacheKey, ConfigCacheEntry>> {
2280 static CACHE: OnceLock<Mutex<HashMap<ConfigCacheKey, ConfigCacheEntry>>> = OnceLock::new();
2281 CACHE.get_or_init(|| Mutex::new(HashMap::new()))
2282}
2283
2284fn config_env_fingerprint() -> Option<Vec<(String, Option<String>)>> {
2287 const VARS: [&str; 8] = [
2288 "GIT_CONFIG_NOSYSTEM",
2289 "GIT_CONFIG_SYSTEM",
2290 "GIT_CONFIG_GLOBAL",
2291 "XDG_CONFIG_HOME",
2292 "HOME",
2293 "GIT_CONFIG",
2294 "GIT_CONFIG_PARAMETERS",
2295 "GIT_CONFIG_COUNT",
2296 ];
2297 let mut fp: Vec<(String, Option<String>)> = VARS
2298 .iter()
2299 .map(|name| ((*name).to_owned(), std::env::var(name).ok()))
2300 .collect();
2301 if let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") {
2302 const MAX_TRACKED: usize = 256;
2304 match count_str.parse::<usize>() {
2305 Ok(n) if n <= MAX_TRACKED => {
2306 for i in 0..n {
2307 for var in [format!("GIT_CONFIG_KEY_{i}"), format!("GIT_CONFIG_VALUE_{i}")] {
2308 let val = std::env::var(&var).ok();
2309 fp.push((var, val));
2310 }
2311 }
2312 }
2313 Ok(_) => return None,
2314 Err(_) => {}
2315 }
2316 }
2317 Some(fp)
2318}
2319
2320fn config_cascade_file_paths(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Vec<PathBuf> {
2323 let mut paths = Vec::new();
2324 if opts.include_system && !git_config_nosystem_enabled() {
2325 paths.push(
2326 std::env::var("GIT_CONFIG_SYSTEM")
2327 .map(PathBuf::from)
2328 .unwrap_or_else(|_| PathBuf::from("/etc/gitconfig")),
2329 );
2330 }
2331 paths.extend(global_config_paths());
2332 if let Some(gd) = git_dir {
2333 let common_dir = crate::repo::common_git_dir_for_config(gd);
2334 paths.push(common_dir.join("config"));
2335 paths.push(gd.join("config.worktree"));
2338 paths.push(gd.join("HEAD"));
2341 }
2342 if let Some(cgd) = &opts.include_ctx.git_dir {
2343 if Some(cgd.as_path()) != git_dir {
2344 paths.push(cgd.join("HEAD"));
2345 }
2346 }
2347 if let Ok(p) = std::env::var("GIT_CONFIG") {
2348 paths.push(PathBuf::from(p));
2349 }
2350 paths
2351}
2352
2353fn stamp_for_path(path: &Path) -> Option<(SystemTime, u64)> {
2354 fs::metadata(path)
2355 .ok()
2356 .and_then(|m| Some((m.modified().ok()?, m.len())))
2357}
2358
2359fn stamp_paths(paths: Vec<PathBuf>) -> Vec<ConfigFileStamp> {
2360 paths
2361 .into_iter()
2362 .map(|path| {
2363 let stamp = stamp_for_path(&path);
2364 (path, stamp)
2365 })
2366 .collect()
2367}
2368
2369fn config_file_stamps(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Vec<ConfigFileStamp> {
2370 stamp_paths(config_cascade_file_paths(git_dir, opts))
2371}
2372
2373fn config_cache_lookup(
2374 key: &ConfigCacheKey,
2375 env_fp: &[(String, Option<String>)],
2376 base_stamps: &[ConfigFileStamp],
2377) -> Option<ConfigSet> {
2378 let cache = config_cache()
2379 .lock()
2380 .unwrap_or_else(std::sync::PoisonError::into_inner);
2381 let entry = cache.get(key)?;
2382 if entry.env_fingerprint.as_slice() != env_fp || entry.base_stamps.as_slice() != base_stamps {
2383 return None;
2384 }
2385 for (path, stamp) in &entry.extra_stamps {
2386 if stamp_for_path(path) != *stamp {
2387 return None;
2388 }
2389 }
2390 Some((*entry.set).clone())
2391}
2392
2393fn evict_config_cache_for_path(path: &Path) {
2395 let mut cache = config_cache()
2396 .lock()
2397 .unwrap_or_else(std::sync::PoisonError::into_inner);
2398 cache.retain(|_, entry| {
2399 !entry
2400 .base_stamps
2401 .iter()
2402 .chain(&entry.extra_stamps)
2403 .any(|(p, _)| p == path)
2404 });
2405}
2406
2407fn git_config_nosystem_enabled() -> bool {
2408 std::env::var("GIT_CONFIG_NOSYSTEM")
2409 .ok()
2410 .map(|value| parse_bool(&value).unwrap_or(true))
2411 .unwrap_or(false)
2412}
2413
2414fn add_environment_config_pairs(set: &mut ConfigSet) -> Result<()> {
2415 let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") else {
2416 return Ok(());
2417 };
2418 if count_str.is_empty() {
2419 return Ok(());
2420 }
2421
2422 let count = count_str
2423 .parse::<usize>()
2424 .map_err(|_| Error::ConfigError("bogus count in GIT_CONFIG_COUNT".to_owned()))?;
2425 if count > i32::MAX as usize {
2426 return Err(Error::ConfigError(
2427 "too many entries in GIT_CONFIG_COUNT".to_owned(),
2428 ));
2429 }
2430
2431 for i in 0..count {
2432 let key_var = format!("GIT_CONFIG_KEY_{i}");
2433 let value_var = format!("GIT_CONFIG_VALUE_{i}");
2434 let key = std::env::var(&key_var)
2435 .map_err(|_| Error::ConfigError(format!("missing config key {key_var}")))?;
2436 let value = std::env::var(&value_var)
2437 .map_err(|_| Error::ConfigError(format!("missing config value {value_var}")))?;
2438 set.add_command_override(&key, &value)?;
2439 }
2440
2441 Ok(())
2442}
2443
2444pub fn parse_bool(s: &str) -> std::result::Result<bool, String> {
2457 match s.to_lowercase().as_str() {
2458 "true" | "yes" | "on" => Ok(true),
2459 "" => Ok(false),
2460 "false" | "no" | "off" => Ok(false),
2461 _ => {
2462 if let Ok(n) = parse_i64(s) {
2464 return Ok(n != 0);
2465 }
2466 Err(format!("bad boolean config value '{s}'"))
2467 }
2468 }
2469}
2470
2471pub fn parse_i64(s: &str) -> std::result::Result<i64, String> {
2473 let s = s.trim();
2474 if s.is_empty() {
2475 return Err("empty integer value".to_owned());
2476 }
2477
2478 let (num_str, multiplier) = match s.as_bytes().last() {
2479 Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
2480 Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
2481 Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
2482 _ => (s, 1_i64),
2483 };
2484
2485 let base: i64 = num_str
2486 .parse()
2487 .map_err(|_| format!("invalid integer: '{s}'"))?;
2488 base.checked_mul(multiplier)
2489 .ok_or_else(|| format!("integer overflow: '{s}'"))
2490}
2491
2492#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2494pub enum GitConfigIntStrictError {
2495 InvalidUnit,
2497 OutOfRange,
2499}
2500
2501pub fn parse_git_config_int_strict(raw: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
2505 let s = raw.trim();
2506 if s.is_empty() {
2507 return Err(GitConfigIntStrictError::InvalidUnit);
2508 }
2509
2510 let bytes = s.as_bytes();
2511 let mut idx = 0usize;
2512 if matches!(bytes.first(), Some(b'+') | Some(b'-')) {
2513 idx = 1;
2514 }
2515 if idx >= bytes.len() {
2516 return Err(GitConfigIntStrictError::InvalidUnit);
2517 }
2518 let digit_start = idx;
2519 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
2520 idx += 1;
2521 }
2522 if idx == digit_start {
2523 return Err(GitConfigIntStrictError::InvalidUnit);
2524 }
2525
2526 let num_part =
2527 std::str::from_utf8(&bytes[..idx]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2528 let suffix =
2529 std::str::from_utf8(&bytes[idx..]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2530 let mult: i64 = match suffix {
2531 "" => 1,
2532 "k" | "K" => 1024,
2533 "m" | "M" => 1024 * 1024,
2534 "g" | "G" => 1024_i64
2535 .checked_mul(1024)
2536 .and_then(|x| x.checked_mul(1024))
2537 .ok_or(GitConfigIntStrictError::OutOfRange)?,
2538 _ => return Err(GitConfigIntStrictError::InvalidUnit),
2539 };
2540
2541 let val: i64 = num_part
2542 .parse()
2543 .map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2544 val.checked_mul(mult)
2545 .ok_or(GitConfigIntStrictError::OutOfRange)
2546}
2547
2548const DIFF_CONTEXT_KEY: &str = "diff.context";
2549
2550fn format_bad_numeric_diff_context(
2551 value: &str,
2552 err: GitConfigIntStrictError,
2553 entry: &ConfigEntry,
2554) -> String {
2555 let detail = match err {
2556 GitConfigIntStrictError::InvalidUnit => "invalid unit",
2557 GitConfigIntStrictError::OutOfRange => "out of range",
2558 };
2559 if entry.scope == ConfigScope::Command || entry.file.is_none() {
2560 return format!(
2561 "fatal: bad numeric config value '{value}' for '{DIFF_CONTEXT_KEY}': {detail}"
2562 );
2563 }
2564 let path = entry
2565 .file
2566 .as_deref()
2567 .map(config_error_path_display)
2568 .unwrap_or_default();
2569 format!("fatal: bad numeric config value '{value}' for '{DIFF_CONTEXT_KEY}' in file {path}: {detail}")
2570}
2571
2572fn format_bad_diff_context_variable(entry: &ConfigEntry) -> String {
2573 if entry.scope == ConfigScope::Command || entry.file.is_none() {
2574 return format!("fatal: unable to parse '{DIFF_CONTEXT_KEY}' from command-line config");
2575 }
2576 let path = entry
2577 .file
2578 .as_deref()
2579 .map(config_error_path_display)
2580 .unwrap_or_default();
2581 format!(
2582 "fatal: bad config variable '{DIFF_CONTEXT_KEY}' in file '{path}' at line {}",
2583 entry.line
2584 )
2585}
2586
2587pub fn resolve_diff_context_lines(cfg: &ConfigSet) -> std::result::Result<Option<usize>, String> {
2592 let Some(entry) = cfg.get_last_entry(DIFF_CONTEXT_KEY) else {
2593 return Ok(None);
2594 };
2595 let value_src = entry.value.as_deref().unwrap_or("").trim();
2596 match parse_git_config_int_strict(value_src) {
2597 Ok(n) if n < 0 => Err(format_bad_diff_context_variable(&entry)),
2598 Ok(n) => Ok(Some(usize::try_from(n).map_err(|_| {
2599 format_bad_numeric_diff_context(value_src, GitConfigIntStrictError::OutOfRange, &entry)
2600 })?)),
2601 Err(e) => Err(format_bad_numeric_diff_context(value_src, e, &entry)),
2602 }
2603}
2604
2605pub fn parse_color(s: &str) -> std::result::Result<String, String> {
2612 const COLOR_BACKGROUND_OFFSET: i32 = 10;
2613 const COLOR_FOREGROUND_ANSI: i32 = 30;
2614 const COLOR_FOREGROUND_RGB: i32 = 38;
2615 const COLOR_FOREGROUND_256: i32 = 38;
2616 const COLOR_FOREGROUND_BRIGHT_ANSI: i32 = 90;
2617
2618 #[derive(Clone, Copy, Default)]
2619 struct Color {
2620 kind: u8,
2621 value: u8,
2622 red: u8,
2623 green: u8,
2624 blue: u8,
2625 }
2626
2627 const COLOR_UNSPECIFIED: u8 = 0;
2628 const COLOR_NORMAL: u8 = 1;
2629 const COLOR_ANSI: u8 = 2;
2630 const COLOR_256: u8 = 3;
2631 const COLOR_RGB: u8 = 4;
2632
2633 fn color_empty(c: &Color) -> bool {
2634 c.kind == COLOR_UNSPECIFIED || c.kind == COLOR_NORMAL
2635 }
2636
2637 fn parse_ansi_color(name: &str) -> Option<Color> {
2638 let color_names = [
2639 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
2640 ];
2641 let color_offset = COLOR_FOREGROUND_ANSI;
2642
2643 if name.eq_ignore_ascii_case("default") {
2644 return Some(Color {
2645 kind: COLOR_ANSI,
2646 value: (9 + color_offset) as u8,
2647 ..Default::default()
2648 });
2649 }
2650
2651 let (name, color_offset) = if name.len() >= 6 && name[..6].eq_ignore_ascii_case("bright") {
2652 (&name[6..], COLOR_FOREGROUND_BRIGHT_ANSI)
2653 } else {
2654 (name, COLOR_FOREGROUND_ANSI)
2655 };
2656
2657 for (i, cn) in color_names.iter().enumerate() {
2658 if name.eq_ignore_ascii_case(cn) {
2659 return Some(Color {
2660 kind: COLOR_ANSI,
2661 value: (i as i32 + color_offset) as u8,
2662 ..Default::default()
2663 });
2664 }
2665 }
2666 None
2667 }
2668
2669 fn hex_val(b: u8) -> Option<u8> {
2670 match b {
2671 b'0'..=b'9' => Some(b - b'0'),
2672 b'a'..=b'f' => Some(b - b'a' + 10),
2673 b'A'..=b'F' => Some(b - b'A' + 10),
2674 _ => None,
2675 }
2676 }
2677
2678 fn get_hex_color(chars: &[u8], width: usize) -> Option<(u8, usize)> {
2679 assert!(width == 1 || width == 2);
2680 if chars.len() < width {
2681 return None;
2682 }
2683 let v = if width == 2 {
2684 let hi = hex_val(chars[0])?;
2685 let lo = hex_val(chars[1])?;
2686 (hi << 4) | lo
2687 } else {
2688 let n = hex_val(chars[0])?;
2689 (n << 4) | n
2690 };
2691 Some((v, width))
2692 }
2693
2694 fn parse_single_color(word: &str) -> Option<Color> {
2695 if word.eq_ignore_ascii_case("normal") {
2696 return Some(Color {
2697 kind: COLOR_NORMAL,
2698 ..Default::default()
2699 });
2700 }
2701
2702 let bytes = word.as_bytes();
2703 if (bytes.len() == 7 || bytes.len() == 4) && bytes.first() == Some(&b'#') {
2704 let width = if bytes.len() == 7 { 2 } else { 1 };
2705 let mut idx = 1;
2706 let (r, n1) = get_hex_color(&bytes[idx..], width)?;
2707 idx += n1;
2708 let (g, n2) = get_hex_color(&bytes[idx..], width)?;
2709 idx += n2;
2710 let (b, n3) = get_hex_color(&bytes[idx..], width)?;
2711 idx += n3;
2712 if idx != bytes.len() {
2713 return None;
2714 }
2715 return Some(Color {
2716 kind: COLOR_RGB,
2717 red: r,
2718 green: g,
2719 blue: b,
2720 ..Default::default()
2721 });
2722 }
2723
2724 if let Some(c) = parse_ansi_color(word) {
2725 return Some(c);
2726 }
2727
2728 let Ok(val) = word.parse::<i64>() else {
2729 return None;
2730 };
2731 if val < -1 {
2732 return None;
2733 }
2734 if val < 0 {
2735 return Some(Color {
2736 kind: COLOR_NORMAL,
2737 ..Default::default()
2738 });
2739 }
2740 if val < 8 {
2741 return Some(Color {
2742 kind: COLOR_ANSI,
2743 value: (val as i32 + COLOR_FOREGROUND_ANSI) as u8,
2744 ..Default::default()
2745 });
2746 }
2747 if val < 16 {
2748 return Some(Color {
2749 kind: COLOR_ANSI,
2750 value: (val as i32 - 8 + COLOR_FOREGROUND_BRIGHT_ANSI) as u8,
2751 ..Default::default()
2752 });
2753 }
2754 if val < 256 {
2755 return Some(Color {
2756 kind: COLOR_256,
2757 value: val as u8,
2758 ..Default::default()
2759 });
2760 }
2761 None
2762 }
2763
2764 fn parse_attr(word: &str) -> Option<u8> {
2765 const ATTRS: [(&str, u8, u8); 8] = [
2766 ("bold", 1, 22),
2767 ("dim", 2, 22),
2768 ("italic", 3, 23),
2769 ("ul", 4, 24),
2770 ("underline", 4, 24),
2771 ("blink", 5, 25),
2772 ("reverse", 7, 27),
2773 ("strike", 9, 29),
2774 ];
2775
2776 let mut negate = false;
2777 let mut rest = word;
2778 if let Some(stripped) = rest.strip_prefix("no") {
2779 negate = true;
2780 rest = stripped;
2781 if let Some(s) = rest.strip_prefix('-') {
2782 rest = s;
2783 }
2784 }
2785
2786 for (name, val, neg) in ATTRS {
2787 if rest == name {
2788 return Some(if negate { neg } else { val });
2789 }
2790 }
2791 None
2792 }
2793
2794 fn append_color_output(out: &mut String, c: &Color, background: bool) {
2795 let offset = if background {
2796 COLOR_BACKGROUND_OFFSET
2797 } else {
2798 0
2799 };
2800 match c.kind {
2801 COLOR_UNSPECIFIED | COLOR_NORMAL => {}
2802 COLOR_ANSI => {
2803 use std::fmt::Write;
2804 let _ = write!(out, "{}", i32::from(c.value) + offset);
2805 }
2806 COLOR_256 => {
2807 use std::fmt::Write;
2808 let _ = write!(out, "{};5;{}", COLOR_FOREGROUND_256 + offset, c.value);
2809 }
2810 COLOR_RGB => {
2811 use std::fmt::Write;
2812 let _ = write!(
2813 out,
2814 "{};2;{};{};{}",
2815 COLOR_FOREGROUND_RGB + offset,
2816 c.red,
2817 c.green,
2818 c.blue
2819 );
2820 }
2821 _ => {}
2822 }
2823 }
2824
2825 let s = s.trim();
2826 if s.is_empty() {
2827 return Ok(String::new());
2828 }
2829
2830 let mut has_reset = false;
2831 let mut attr: u64 = 0;
2832 let mut fg = Color::default();
2833 let mut bg = Color::default();
2834 fg.kind = COLOR_UNSPECIFIED;
2835 bg.kind = COLOR_UNSPECIFIED;
2836
2837 for word in s.split_whitespace() {
2838 if word.eq_ignore_ascii_case("reset") {
2839 has_reset = true;
2840 continue;
2841 }
2842
2843 if let Some(c) = parse_single_color(word) {
2844 if fg.kind == COLOR_UNSPECIFIED {
2845 fg = c;
2846 continue;
2847 }
2848 if bg.kind == COLOR_UNSPECIFIED {
2849 bg = c;
2850 continue;
2851 }
2852 return Err(format!("bad color value '{s}'"));
2853 }
2854
2855 if let Some(code) = parse_attr(word) {
2856 attr |= 1u64 << u64::from(code);
2857 continue;
2858 }
2859
2860 return Err(format!("bad color value '{s}'"));
2861 }
2862
2863 if !has_reset && attr == 0 && color_empty(&fg) && color_empty(&bg) {
2864 return Err(format!("bad color value '{s}'"));
2865 }
2866
2867 let mut out = String::from("\x1b[");
2868 let mut sep = if has_reset { 1u32 } else { 0u32 };
2869
2870 let mut attr_bits = attr;
2871 let mut i = 0u32;
2872 while attr_bits != 0 {
2873 let bit = 1u64 << i;
2874 if attr_bits & bit == 0 {
2875 i += 1;
2876 continue;
2877 }
2878 attr_bits &= !bit;
2879 if sep > 0 {
2880 out.push(';');
2881 }
2882 sep += 1;
2883 use std::fmt::Write;
2884 let _ = write!(out, "{i}");
2885 i += 1;
2886 }
2887
2888 if !color_empty(&fg) {
2889 if sep > 0 {
2890 out.push(';');
2891 }
2892 sep += 1;
2893 append_color_output(&mut out, &fg, false);
2894 }
2895 if !color_empty(&bg) {
2896 if sep > 0 {
2897 out.push(';');
2898 }
2899 append_color_output(&mut out, &bg, true);
2900 }
2901 out.push('m');
2902 Ok(out)
2903}
2904
2905#[derive(Debug, Clone)]
2906struct UrlParts {
2907 scheme: String,
2908 user: Option<String>,
2909 host: String,
2910 port: Option<String>,
2911 path: String,
2912}
2913
2914#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2915struct UrlMatchScore {
2916 host_len: usize,
2917 path_len: usize,
2918 user_matched: bool,
2919}
2920
2921fn parse_config_url(url: &str) -> Option<UrlParts> {
2922 let (scheme, rest) = url.split_once("://")?;
2923 let (authority, path) = match rest.find('/') {
2924 Some(idx) => (&rest[..idx], &rest[idx..]),
2925 None => (rest, "/"),
2926 };
2927 let (user, host_port) = match authority.rsplit_once('@') {
2928 Some((user, host)) => (Some(user.to_owned()), host),
2929 None => (None, authority),
2930 };
2931 let (host, port) = match host_port.rsplit_once(':') {
2932 Some((host, port)) if !host.contains(']') => (host, Some(port.to_owned())),
2933 _ => (host_port, None),
2934 };
2935 Some(UrlParts {
2936 scheme: scheme.to_lowercase(),
2937 user,
2938 host: host.to_lowercase(),
2939 port,
2940 path: if path.is_empty() {
2941 "/".to_owned()
2942 } else {
2943 path.trim_end_matches('/').to_owned()
2944 },
2945 })
2946}
2947
2948fn host_matches(pattern: &str, target: &str) -> bool {
2949 let pattern_parts: Vec<&str> = pattern.split('.').collect();
2950 let target_parts: Vec<&str> = target.split('.').collect();
2951 pattern_parts.len() == target_parts.len()
2952 && pattern_parts
2953 .iter()
2954 .zip(target_parts)
2955 .all(|(pattern, target)| *pattern == "*" || *pattern == target)
2956}
2957
2958fn path_match_len(pattern: &str, target: &str) -> Option<usize> {
2959 let pattern = if pattern.is_empty() { "/" } else { pattern };
2960 let target = if target.is_empty() { "/" } else { target };
2961 if pattern == "/" {
2962 return Some(1);
2963 }
2964 let pattern = pattern.trim_end_matches('/');
2965 if target == pattern
2966 || target
2967 .strip_prefix(pattern)
2968 .is_some_and(|rest| rest.starts_with('/'))
2969 {
2970 Some(pattern.len() + 1)
2971 } else {
2972 None
2973 }
2974}
2975
2976fn url_match_score(pattern_url: &str, target_url: &str) -> Option<UrlMatchScore> {
2977 let pattern = parse_config_url(pattern_url)?;
2978 let target = parse_config_url(target_url)?;
2979 if pattern.scheme != target.scheme {
2980 return None;
2981 }
2982 let user_matched = match pattern.user.as_deref() {
2983 Some(user) if target.user.as_deref() == Some(user) => true,
2984 Some(_) => return None,
2985 None => false,
2986 };
2987 if !host_matches(&pattern.host, &target.host) || pattern.port != target.port {
2988 return None;
2989 }
2990 let path_len = path_match_len(&pattern.path, &target.path)?;
2991 Some(UrlMatchScore {
2992 host_len: pattern.host.len(),
2993 path_len,
2994 user_matched,
2995 })
2996}
2997
2998pub fn url_matches(pattern_url: &str, target_url: &str) -> bool {
3000 url_match_score(pattern_url, target_url).is_some()
3001}
3002
3003pub fn get_urlmatch_entries<'a>(
3005 entries: &'a [ConfigEntry],
3006 section: &str,
3007 variable: &str,
3008 url: &str,
3009) -> Vec<&'a ConfigEntry> {
3010 let section_lower = section.to_lowercase();
3011 let variable_lower = variable.to_lowercase();
3012 let mut matches: Vec<(UrlMatchScore, &'a ConfigEntry)> = Vec::new();
3013
3014 for entry in entries {
3015 let key = &entry.key;
3016 let first_dot = match key.find('.') {
3017 Some(i) => i,
3018 None => continue,
3019 };
3020 let last_dot = match key.rfind('.') {
3021 Some(i) => i,
3022 None => continue,
3023 };
3024 let entry_section = &key[..first_dot];
3025 let entry_variable = &key[last_dot + 1..];
3026 if entry_section.to_lowercase() != section_lower
3027 || entry_variable.to_lowercase() != variable_lower
3028 {
3029 continue;
3030 }
3031 if first_dot == last_dot {
3032 matches.push((
3033 UrlMatchScore {
3034 host_len: 0,
3035 path_len: 0,
3036 user_matched: false,
3037 },
3038 entry,
3039 ));
3040 } else {
3041 let subsection = &key[first_dot + 1..last_dot];
3042 if let Some(score) = url_match_score(subsection, url) {
3043 matches.push((score, entry));
3044 }
3045 }
3046 }
3047 matches.sort_by_key(|a| a.0);
3048 matches.into_iter().map(|(_, e)| e).collect()
3049}
3050
3051pub fn get_urlmatch_all_in_section(
3053 entries: &[ConfigEntry],
3054 section: &str,
3055 url: &str,
3056) -> Vec<(String, String, ConfigScope)> {
3057 let section_lower = section.to_lowercase();
3058 let mut matches: Vec<(String, UrlMatchScore, String, String, ConfigScope)> = Vec::new();
3059
3060 for entry in entries {
3061 let key = &entry.key;
3062 let first_dot = match key.find('.') {
3063 Some(i) => i,
3064 None => continue,
3065 };
3066 let last_dot = match key.rfind('.') {
3067 Some(i) => i,
3068 None => continue,
3069 };
3070 let entry_section = &key[..first_dot];
3071 if entry_section.to_lowercase() != section_lower {
3072 continue;
3073 }
3074 let entry_variable = &key[last_dot + 1..];
3075 let val = entry.value.as_deref().unwrap_or("");
3076 if first_dot == last_dot {
3077 let canonical = format!("{}.{}", section_lower, entry_variable);
3078 matches.push((
3079 entry_variable.to_lowercase(),
3080 UrlMatchScore {
3081 host_len: 0,
3082 path_len: 0,
3083 user_matched: false,
3084 },
3085 val.to_owned(),
3086 canonical,
3087 entry.scope,
3088 ));
3089 } else {
3090 let subsection = &key[first_dot + 1..last_dot];
3091 if let Some(score) = url_match_score(subsection, url) {
3092 let canonical = format!("{}.{}", section_lower, entry_variable);
3093 matches.push((
3094 entry_variable.to_lowercase(),
3095 score,
3096 val.to_owned(),
3097 canonical,
3098 entry.scope,
3099 ));
3100 }
3101 }
3102 }
3103
3104 let mut best: std::collections::BTreeMap<String, (UrlMatchScore, String, String, ConfigScope)> =
3105 std::collections::BTreeMap::new();
3106 for (var, specificity, val, canonical, scope) in matches {
3107 let entry = best.entry(var).or_insert((
3108 UrlMatchScore {
3109 host_len: 0,
3110 path_len: 0,
3111 user_matched: false,
3112 },
3113 String::new(),
3114 String::new(),
3115 scope,
3116 ));
3117 if specificity >= entry.0 {
3118 *entry = (specificity, val, canonical, scope);
3119 }
3120 }
3121 best.into_values()
3122 .map(|(_, val, canonical, scope)| (canonical, val, scope))
3123 .collect()
3124}
3125
3126pub fn parse_path(s: &str) -> String {
3130 if let Some(rest) = s.strip_prefix("~/") {
3131 if let Some(home) = home_dir() {
3132 return home.join(rest).to_string_lossy().to_string();
3133 }
3134 }
3135 s.to_owned()
3136}
3137
3138pub fn parse_path_optional(s: &str) -> Option<String> {
3143 if let Some(rest) = s.strip_prefix(":(optional)") {
3144 let resolved = parse_path(rest);
3145 if std::path::Path::new(&resolved).exists() {
3146 Some(resolved)
3147 } else {
3148 None }
3150 } else {
3151 Some(parse_path(s))
3152 }
3153}
3154
3155#[must_use]
3171pub fn git_config_parameters_last_value(raw: &str, key: &str) -> Option<String> {
3172 let Ok(canon) = canonical_key(key) else {
3173 return None;
3174 };
3175 let mut last: Option<String> = None;
3176 for entry in parse_config_parameters_strict(raw).ok()? {
3177 match entry {
3178 ConfigParameter::Pair { key, value } => {
3179 if canonical_key(key.trim()).ok().as_ref() == Some(&canon) {
3180 last = Some(value.unwrap_or_else(|| "true".to_owned()));
3181 }
3182 }
3183 ConfigParameter::OldStyle(entry) => {
3184 if let Some((k, v)) = entry.split_once('=') {
3185 if canonical_key(k.trim()).ok().as_ref() == Some(&canon) {
3186 last = Some(v.to_owned());
3187 }
3188 } else if canonical_key(entry.trim()).ok().as_ref() == Some(&canon) {
3189 last = Some("true".to_owned());
3190 }
3191 }
3192 }
3193 }
3194 last
3195}
3196
3197#[derive(Debug, Clone, PartialEq, Eq)]
3198enum ConfigParameter {
3199 OldStyle(String),
3200 Pair { key: String, value: Option<String> },
3201}
3202
3203pub fn parse_config_parameters(raw: &str) -> Vec<String> {
3204 parse_config_parameters_strict(raw)
3205 .map(|entries| {
3206 entries
3207 .into_iter()
3208 .map(|entry| match entry {
3209 ConfigParameter::OldStyle(entry) => entry,
3210 ConfigParameter::Pair {
3211 key,
3212 value: Some(value),
3213 } => format!("{key}\u{1}{value}"),
3214 ConfigParameter::Pair { key, value: None } => format!("{key}\u{1}"),
3215 })
3216 .collect()
3217 })
3218 .unwrap_or_default()
3219}
3220
3221fn parse_config_parameters_strict(raw: &str) -> Result<Vec<ConfigParameter>> {
3222 let mut out: Vec<ConfigParameter> = Vec::new();
3223 let chars: Vec<char> = raw.chars().collect();
3224 let mut idx = skip_config_parameter_spaces(&chars, 0);
3225
3226 while idx < chars.len() {
3227 let (key, next) = sq_dequote_step_chars(&chars, idx)?;
3228 let Some(next_idx) = next else {
3229 out.push(ConfigParameter::OldStyle(key));
3230 break;
3231 };
3232
3233 if chars[next_idx].is_whitespace() {
3234 out.push(ConfigParameter::OldStyle(key));
3235 idx = skip_config_parameter_spaces(&chars, next_idx);
3236 continue;
3237 }
3238
3239 if chars[next_idx] != '=' {
3240 return Err(Error::ConfigError(
3241 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3242 ));
3243 }
3244
3245 let value_start = next_idx + 1;
3246 if value_start >= chars.len() || chars[value_start].is_whitespace() {
3247 out.push(ConfigParameter::Pair { key, value: None });
3248 idx = skip_config_parameter_spaces(&chars, value_start);
3249 continue;
3250 }
3251
3252 if chars[value_start] != '\'' {
3253 return Err(Error::ConfigError(
3254 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3255 ));
3256 }
3257 let (value, value_next) = sq_dequote_step_chars(&chars, value_start)?;
3258 if let Some(value_next) = value_next {
3259 if !chars[value_next].is_whitespace() {
3260 return Err(Error::ConfigError(
3261 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3262 ));
3263 }
3264 idx = skip_config_parameter_spaces(&chars, value_next);
3265 } else {
3266 idx = chars.len();
3267 }
3268 out.push(ConfigParameter::Pair {
3269 key,
3270 value: Some(value),
3271 });
3272 }
3273
3274 Ok(out)
3275}
3276
3277fn skip_config_parameter_spaces(chars: &[char], mut idx: usize) -> usize {
3278 while idx < chars.len() && chars[idx].is_whitespace() {
3279 idx += 1;
3280 }
3281 idx
3282}
3283
3284fn sq_dequote_step_chars(chars: &[char], start: usize) -> Result<(String, Option<usize>)> {
3285 if chars.get(start) != Some(&'\'') {
3286 return Err(Error::ConfigError(
3287 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3288 ));
3289 }
3290
3291 let mut out = String::new();
3292 let mut idx = start + 1;
3293 loop {
3294 let Some(&ch) = chars.get(idx) else {
3295 return Err(Error::ConfigError(
3296 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3297 ));
3298 };
3299 if ch != '\'' {
3300 out.push(ch);
3301 idx += 1;
3302 continue;
3303 }
3304
3305 idx += 1;
3306 match chars.get(idx).copied() {
3307 None => return Ok((out, None)),
3308 Some('\\')
3309 if chars
3310 .get(idx + 1)
3311 .copied()
3312 .is_some_and(needs_sq_backslash_quote)
3313 && chars.get(idx + 2) == Some(&'\'') =>
3314 {
3315 if let Some(escaped) = chars.get(idx + 1) {
3316 out.push(*escaped);
3317 }
3318 idx += 3;
3319 }
3320 _ => return Ok((out, Some(idx))),
3321 }
3322 }
3323}
3324
3325fn needs_sq_backslash_quote(ch: char) -> bool {
3326 ch == '\'' || ch == '!'
3327}
3328
3329pub fn global_config_paths_pub() -> Vec<PathBuf> {
3332 global_config_paths()
3333}
3334
3335fn global_config_paths() -> Vec<PathBuf> {
3336 let mut paths = Vec::new();
3337
3338 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
3340 paths.push(PathBuf::from(p));
3341 return paths;
3342 }
3343
3344 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
3346 paths.push(PathBuf::from(xdg).join("git/config"));
3347 } else if let Some(home) = home_dir() {
3348 paths.push(home.join(".config/git/config"));
3349 }
3350 if let Some(home) = home_dir() {
3351 paths.push(home.join(".gitconfig"));
3352 }
3353
3354 paths
3355}
3356
3357fn home_dir() -> Option<PathBuf> {
3359 std::env::var("HOME").ok().map(PathBuf::from)
3360}
3361
3362fn include_source_is_disk_file(file: &ConfigFile) -> bool {
3364 file.include_origin == ConfigIncludeOrigin::Disk
3365}
3366
3367fn resolve_include_file_path(
3371 path: &str,
3372 file: &ConfigFile,
3373 ctx: &IncludeContext,
3374) -> Result<PathBuf> {
3375 let expanded = parse_path(path);
3376 let p = Path::new(&expanded);
3377 if p.is_absolute() {
3378 return Ok(p.to_path_buf());
3379 }
3380 if !include_source_is_disk_file(file) {
3381 if file.include_origin == ConfigIncludeOrigin::CommandLine {
3382 if ctx.command_line_relative_include_is_error {
3383 return Err(Error::ConfigError(
3384 "relative config includes must come from files".to_owned(),
3385 ));
3386 }
3387 return Err(Error::ConfigError(String::new()));
3388 }
3389 return Err(Error::ConfigError(
3390 "relative config includes must come from files".to_owned(),
3391 ));
3392 }
3393 let base = match file.path.parent() {
3394 Some(p) if !p.as_os_str().is_empty() => p,
3395 Some(_) | None => Path::new("."),
3396 };
3397 Ok(base.join(p))
3398}
3399
3400fn is_dir_sep(b: u8) -> bool {
3401 b == b'/' || b == b'\\'
3402}
3403
3404fn add_trailing_starstar_for_dir(pat: &mut String) {
3405 let bytes = pat.as_bytes();
3406 if bytes.last().is_some_and(|&b| is_dir_sep(b)) {
3407 pat.push_str("**");
3408 }
3409}
3410
3411fn prepare_gitdir_pattern(condition: &str, file: &ConfigFile) -> Result<(String, usize)> {
3413 let mut pat = parse_path(condition);
3415 if pat.starts_with("./") || pat.starts_with(".\\") {
3416 if !include_source_is_disk_file(file) {
3417 return Err(Error::ConfigError(
3418 "relative config include conditionals must come from files".to_owned(),
3419 ));
3420 }
3421 let parent = file.path.parent().ok_or_else(|| {
3422 Error::ConfigError(
3423 "relative config include conditionals must come from files".to_owned(),
3424 )
3425 })?;
3426 let real = parent.canonicalize().map_err(Error::Io)?;
3427 let mut dir = real.to_string_lossy().into_owned();
3428 if !dir.ends_with('/') && !dir.ends_with('\\') {
3429 dir.push('/');
3430 }
3431 let rest = &pat[2..];
3432 pat = format!("{dir}{rest}");
3433 let prefix_len = dir.len();
3434 add_trailing_starstar_for_dir(&mut pat);
3435 return Ok((pat, prefix_len));
3436 }
3437 let p = Path::new(&pat);
3438 if !p.is_absolute() {
3439 pat.insert_str(0, "**/");
3440 }
3441 add_trailing_starstar_for_dir(&mut pat);
3442 Ok((pat, 0))
3443}
3444
3445fn git_dir_match_texts(git_dir: &Path) -> (String, String) {
3450 let real = git_dir
3451 .canonicalize()
3452 .map(|p| p.to_string_lossy().into_owned())
3453 .unwrap_or_else(|_| git_dir.to_string_lossy().into_owned());
3454 let abs = if git_dir.is_absolute() {
3457 let pwd_abs = std::env::var("PWD").ok().and_then(|pwd| {
3460 let pwd_path = std::path::Path::new(&pwd);
3461 if !pwd_path.is_absolute() {
3462 return None;
3463 }
3464 let pwd_canon = pwd_path.canonicalize().ok()?;
3465 let git_dir_str = git_dir.to_string_lossy();
3466 let pwd_canon_str = pwd_canon.to_string_lossy();
3467 let suffix = git_dir_str.strip_prefix(pwd_canon_str.as_ref())?;
3469 Some(format!("{pwd}{suffix}"))
3470 });
3471 pwd_abs.unwrap_or_else(|| git_dir.to_string_lossy().into_owned())
3472 } else if let Ok(cwd) = std::env::current_dir() {
3473 cwd.join(git_dir).to_string_lossy().into_owned()
3474 } else {
3475 git_dir.to_string_lossy().into_owned()
3476 };
3477 (real, abs)
3478}
3479
3480fn include_by_gitdir(
3481 condition: &str,
3482 file: &ConfigFile,
3483 ctx: &IncludeContext,
3484 icase: bool,
3485) -> bool {
3486 let Some(git_dir) = ctx.git_dir.as_ref() else {
3487 return false;
3488 };
3489 let (pattern, prefix) = match prepare_gitdir_pattern(condition, file) {
3490 Ok(x) => x,
3491 Err(_) => return false,
3492 };
3493 let flags = WM_PATHNAME | if icase { WM_CASEFOLD } else { 0 };
3494 let (text_real, text_abs) = git_dir_match_texts(git_dir);
3495 let try_match = |text: &str| -> bool {
3496 let t = text.as_bytes();
3497 let p = pattern.as_bytes();
3498 if prefix > 0 {
3499 if t.len() < prefix {
3500 return false;
3501 }
3502 let pre = &p[..prefix];
3503 let te = &t[..prefix];
3504 let ok = if icase {
3505 pre.eq_ignore_ascii_case(te)
3506 } else {
3507 pre == te
3508 };
3509 if !ok {
3510 return false;
3511 }
3512 return wildmatch(&p[prefix..], &t[prefix..], flags);
3513 }
3514 wildmatch(p, t, flags)
3515 };
3516 if try_match(&text_real) {
3517 return true;
3518 }
3519 text_real != text_abs && try_match(&text_abs)
3520}
3521
3522fn current_branch_short_name(git_dir: Option<&Path>) -> Option<String> {
3523 let gd = git_dir?;
3524 let target = refs::read_symbolic_ref(gd, "HEAD").ok()??;
3525 let rest = target.strip_prefix("refs/heads/")?;
3526 Some(rest.to_owned())
3527}
3528
3529fn include_by_onbranch(condition: &str, ctx: &IncludeContext) -> bool {
3530 let Some(short) = current_branch_short_name(ctx.git_dir.as_deref()) else {
3531 return false;
3532 };
3533 let mut pattern = condition.to_owned();
3534 add_trailing_starstar_for_dir(&mut pattern);
3535 wildmatch(pattern.as_bytes(), short.as_bytes(), WM_PATHNAME)
3536}
3537
3538fn is_remote_url_entry(entry: &ConfigEntry) -> bool {
3539 let Ok((section, subsection, variable)) = split_key(&entry.key) else {
3540 return false;
3541 };
3542 section == "remote" && subsection.is_some() && variable == "url"
3543}
3544
3545fn is_hasconfig_remote_url(condition: &str) -> bool {
3546 condition
3547 .strip_prefix("hasconfig:")
3548 .is_some_and(|rest| rest.starts_with("remote.*.url:"))
3549}
3550
3551fn include_by_hasconfig_remote_url(condition: &str, set: &ConfigSet, file: &ConfigFile) -> bool {
3552 let Some(pattern) = condition.strip_prefix("remote.*.url:") else {
3553 return false;
3554 };
3555 set.entries
3556 .iter()
3557 .chain(file.entries.iter())
3558 .filter(|entry| is_remote_url_entry(entry))
3559 .filter_map(|entry| entry.value.as_deref())
3560 .any(|value| wildmatch(pattern.as_bytes(), value.as_bytes(), WM_PATHNAME))
3561}
3562
3563fn validate_hasconfig_remote_url_include(
3564 file: &ConfigFile,
3565 process_includes: bool,
3566 depth: usize,
3567 ctx: &IncludeContext,
3568) -> Result<()> {
3569 const MAX_INCLUDE_DEPTH: usize = 10;
3570 if depth > MAX_INCLUDE_DEPTH {
3571 return Err(Error::ConfigError(
3572 "exceeded maximum include depth".to_owned(),
3573 ));
3574 }
3575 if file.entries.iter().any(is_remote_url_entry) {
3576 return Err(Error::Message(
3577 "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url"
3578 .to_owned(),
3579 ));
3580 }
3581 if !process_includes {
3582 return Ok(());
3583 }
3584 for entry in &file.entries {
3585 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
3586 continue;
3587 };
3588 if let Some(ref cond) = condition {
3589 if !evaluate_include_condition(cond, &ConfigSet::new(), file, ctx) {
3590 continue;
3591 }
3592 }
3593 let resolved = match resolve_include_file_path(&inc_path, file, ctx) {
3594 Ok(p) => p,
3595 Err(Error::ConfigError(msg)) if msg.is_empty() => continue,
3596 Err(e) => return Err(e),
3597 };
3598 if let Some(inc_file) = ConfigFile::from_path(&resolved, file.scope)? {
3599 validate_hasconfig_remote_url_include(&inc_file, process_includes, depth + 1, ctx)?;
3600 }
3601 }
3602 Ok(())
3603}
3604
3605fn evaluate_include_condition(
3610 condition: &str,
3611 set: &ConfigSet,
3612 file: &ConfigFile,
3613 ctx: &IncludeContext,
3614) -> bool {
3615 if let Some(rest) = condition.strip_prefix("gitdir/i:") {
3616 return include_by_gitdir(rest, file, ctx, true);
3617 }
3618 if let Some(rest) = condition.strip_prefix("gitdir:") {
3619 return include_by_gitdir(rest, file, ctx, false);
3620 }
3621 if let Some(rest) = condition.strip_prefix("onbranch:") {
3622 return include_by_onbranch(rest, ctx);
3623 }
3624 if let Some(rest) = condition.strip_prefix("hasconfig:") {
3625 return include_by_hasconfig_remote_url(rest, set, file);
3626 }
3627 false
3628}
3629
3630fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
3632 let first_dot = key
3633 .find('.')
3634 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
3635 let last_dot = key
3636 .rfind('.')
3637 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
3638
3639 let section = key[..first_dot].to_owned();
3640 let variable = key[last_dot + 1..].to_owned();
3641
3642 let subsection = if first_dot == last_dot {
3643 None
3644 } else {
3645 Some(key[first_dot + 1..last_dot].to_owned())
3646 };
3647
3648 Ok((section, subsection, variable))
3649}
3650
3651#[allow(dead_code)]
3653fn variable_name_from_key(key: &str) -> &str {
3654 match key.rfind('.') {
3655 Some(i) => &key[i + 1..],
3656 None => key,
3657 }
3658}
3659
3660fn parse_section_name(name: &str) -> (&str, Option<&str>) {
3664 match name.find('.') {
3665 Some(i) => (&name[..i], Some(&name[i + 1..])),
3666 None => (name, None),
3667 }
3668}
3669
3670fn section_matches(parser: &Parser, section_lower: &str, subsection: Option<&str>) -> bool {
3671 if parser.section.to_lowercase() == section_lower && parser.subsection.as_deref() == subsection
3672 {
3673 return true;
3674 }
3675 let Some(subsection) = subsection else {
3676 return false;
3677 };
3678 parser.subsection.is_none()
3679 && parser.section.to_lowercase() == format!("{section_lower}.{}", subsection.to_lowercase())
3680}
3681
3682fn validate_section_name(section: &str, subsection: Option<&str>) -> Result<()> {
3683 if section.is_empty()
3684 || !section
3685 .chars()
3686 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
3687 || subsection.is_some_and(str::is_empty)
3688 {
3689 return Err(Error::ConfigError(format!(
3690 "invalid section name: {section}"
3691 )));
3692 }
3693 Ok(())
3694}
3695
3696fn raw_variable_name(raw_key: &str) -> &str {
3700 match raw_key.rfind('.') {
3701 Some(i) => &raw_key[i + 1..],
3702 None => raw_key,
3703 }
3704}
3705
3706fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
3711 let first_dot = match raw_key.find('.') {
3712 Some(i) => i,
3713 None => return (raw_key.to_owned(), None),
3714 };
3715 let last_dot = match raw_key.rfind('.') {
3717 Some(i) => i,
3718 None => return (raw_key[..first_dot].to_owned(), None),
3719 };
3720 let section = raw_key[..first_dot].to_owned();
3721 if first_dot == last_dot {
3722 (section, None)
3723 } else {
3724 let subsection = raw_key[first_dot + 1..last_dot].to_owned();
3725 (section, Some(subsection))
3726 }
3727}
3728
3729fn is_section_header_with_inline_entry(line: &str) -> bool {
3731 let trimmed = line.trim();
3732 if !trimmed.starts_with('[') {
3733 return false;
3734 }
3735 let end = match trimmed.find(']') {
3736 Some(i) => i,
3737 None => return false,
3738 };
3739 let after = trimmed[end + 1..].trim();
3740 !after.is_empty() && !after.starts_with('#') && !after.starts_with(';')
3742}
3743
3744fn extract_section_header(line: &str) -> String {
3747 let trimmed = line.trim();
3748 let end = match trimmed.find(']') {
3749 Some(i) => i,
3750 None => return line.to_owned(),
3751 };
3752 trimmed[..=end].to_owned()
3755}
3756
3757#[cfg(test)]
3758mod get_regexp_tests {
3759 use super::{ConfigFile, ConfigScope, ConfigSet};
3760 use std::path::Path;
3761
3762 fn set_from_snippet(text: &str) -> ConfigSet {
3763 let path = Path::new(".git/config");
3764 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
3765 let mut set = ConfigSet::new();
3766 set.merge(&file);
3767 set
3768 }
3769
3770 #[test]
3771 fn get_regexp_matches_section_prefix_like_git_config() {
3772 let text = r#"
3773[user]
3774 email = alice@example.com
3775 name = Alice
3776[core]
3777 bare = false
3778"#;
3779 let set = set_from_snippet(text);
3780 let keys: Vec<_> = set
3781 .get_regexp("user")
3782 .expect("valid pattern")
3783 .into_iter()
3784 .map(|e| e.key.as_str())
3785 .collect();
3786 assert!(keys.contains(&"user.email"));
3787 assert!(keys.contains(&"user.name"));
3788 assert!(!keys.iter().any(|k| k.starts_with("core.")));
3789 }
3790
3791 #[test]
3792 fn get_regexp_returns_all_multi_value_entries_in_order() {
3793 let text = r#"
3794[remote "origin"]
3795 url = https://example.com/repo.git
3796 fetch = +refs/heads/*:refs/remotes/origin/*
3797 push = +refs/heads/main:refs/heads/main
3798 push = +refs/heads/develop:refs/heads/develop
3799"#;
3800 let set = set_from_snippet(text);
3801 let matches = set.get_regexp("remote.origin").expect("valid pattern");
3802 let push_vals: Vec<_> = matches
3803 .iter()
3804 .filter(|e| e.key == "remote.origin.push")
3805 .map(|e| e.value.as_deref().unwrap_or(""))
3806 .collect();
3807 assert_eq!(push_vals.len(), 2);
3808 assert_eq!(push_vals[0], "+refs/heads/main:refs/heads/main");
3809 assert_eq!(push_vals[1], "+refs/heads/develop:refs/heads/develop");
3810 }
3811
3812 #[test]
3813 fn get_regexp_dot_matches_any_key() {
3814 let text = r#"
3815[a]
3816 x = 1
3817[b]
3818 y = 2
3819"#;
3820 let set = set_from_snippet(text);
3821 let m = set.get_regexp(".").expect("valid pattern");
3822 assert_eq!(m.len(), 2);
3823 }
3824
3825 #[test]
3826 fn get_regexp_no_match_returns_empty_vec() {
3827 let set = set_from_snippet("[user]\n\tname = x\n");
3828 let m = set.get_regexp("zzz").expect("valid pattern");
3829 assert!(m.is_empty());
3830 }
3831
3832 #[test]
3833 fn get_regexp_invalid_pattern_is_error() {
3834 let set = set_from_snippet("[user]\n\tname = x\n");
3835 let err = set.get_regexp("(").expect_err("unclosed group");
3836 assert!(err.contains("invalid key pattern"), "got: {err}");
3837 }
3838}
3839
3840#[cfg(test)]
3841mod pack_compression_tests {
3842 use super::{ConfigFile, ConfigScope, ConfigSet};
3843 use std::path::Path;
3844
3845 fn set_from_snippet(text: &str) -> ConfigSet {
3846 let path = Path::new(".git/config");
3847 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
3848 let mut set = ConfigSet::new();
3849 set.merge(&file);
3850 set
3851 }
3852
3853 #[test]
3854 fn pack_objects_zlib_level_defaults_to_six() {
3855 let set = ConfigSet::new();
3856 assert_eq!(set.pack_objects_zlib_level().unwrap(), 6);
3857 }
3858
3859 #[test]
3860 fn pack_objects_zlib_level_core_compression() {
3861 let set = set_from_snippet("[core]\n\tcompression = 0\n");
3862 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3863 let set = set_from_snippet("[core]\n\tcompression = 9\n");
3864 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3865 }
3866
3867 #[test]
3868 fn pack_objects_zlib_level_pack_overrides_core() {
3869 let set = set_from_snippet("[core]\n\tcompression = 9\n[pack]\n\tcompression = 0\n");
3870 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3871 let set = set_from_snippet("[core]\n\tcompression = 0\n[pack]\n\tcompression = 9\n");
3872 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3873 }
3874
3875 #[test]
3876 fn pack_objects_zlib_level_later_core_does_not_override_earlier_pack() {
3877 let mut set = ConfigSet::new();
3878 set.merge(
3879 &ConfigFile::parse(
3880 Path::new("a"),
3881 "[pack]\n\tcompression = 9\n",
3882 ConfigScope::Local,
3883 )
3884 .unwrap(),
3885 );
3886 set.merge(
3887 &ConfigFile::parse(
3888 Path::new("b"),
3889 "[core]\n\tcompression = 0\n",
3890 ConfigScope::Local,
3891 )
3892 .unwrap(),
3893 );
3894 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3895 }
3896
3897 #[test]
3898 fn pack_objects_zlib_level_loosecompression_does_not_block_core_pack_level() {
3899 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 0\n");
3900 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3901 }
3902
3903 #[test]
3904 fn pack_objects_zlib_level_pack_wins_after_loose_and_core() {
3905 let set = set_from_snippet(
3906 "[core]\n\tloosecompression = 1\n\tcompression = 0\n[pack]\n\tcompression = 9\n",
3907 );
3908 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3909 }
3910}
3911
3912#[cfg(test)]
3913mod config_cache_tests {
3914 use super::*;
3915 use filetime::FileTime;
3916
3917 fn local_opts(git_dir: &Path) -> LoadConfigOptions {
3918 let mut opts = LoadConfigOptions::default();
3919 opts.include_system = false;
3920 opts.include_ctx.git_dir = Some(git_dir.to_path_buf());
3921 opts
3922 }
3923
3924 fn load_value(git_dir: &Path) -> Option<String> {
3925 let opts = local_opts(git_dir);
3926 let set = ConfigSet::load_with_options(Some(git_dir), &opts).expect("load cascade");
3927 set.get("gritcachetest.value")
3928 }
3929
3930 fn mtime_of(path: &Path) -> FileTime {
3931 FileTime::from_last_modification_time(&fs::metadata(path).expect("stat config"))
3932 }
3933
3934 fn restore_mtime(path: &Path, stamp: FileTime) {
3935 filetime::set_file_mtime(path, stamp).expect("restore mtime");
3936 }
3937
3938 #[test]
3939 fn cache_serves_same_stamp_and_config_write_evicts() {
3940 let td = tempfile::tempdir().expect("tempdir");
3941 let gd = td.path();
3942 let cfg = gd.join("config");
3943 fs::write(&cfg, "[gritcachetest]\n\tvalue = aaa\n").expect("write v1");
3944 let t0 = mtime_of(&cfg);
3945 assert_eq!(load_value(gd).as_deref(), Some("aaa"));
3946
3947 fs::write(&cfg, "[gritcachetest]\n\tvalue = bbb\n").expect("write v2");
3952 restore_mtime(&cfg, t0);
3953 assert_eq!(load_value(gd).as_deref(), Some("aaa"));
3954
3955 let mut file = ConfigFile::from_path(&cfg, ConfigScope::Local)
3957 .expect("read config")
3958 .expect("config exists");
3959 file.set("gritcachetest.value", "ccc").expect("set value");
3960 file.write().expect("persist");
3961 restore_mtime(&cfg, t0);
3962 assert_eq!(load_value(gd).as_deref(), Some("ccc"));
3963 }
3964
3965 #[test]
3966 fn cache_invalidates_on_size_or_existence_change() {
3967 let td = tempfile::tempdir().expect("tempdir");
3968 let gd = td.path();
3969 let cfg = gd.join("config");
3970 assert_eq!(load_value(gd), None);
3972 fs::write(&cfg, "[gritcachetest]\n\tvalue = first\n").expect("create");
3974 assert_eq!(load_value(gd).as_deref(), Some("first"));
3975 let t0 = mtime_of(&cfg);
3977 fs::write(&cfg, "[gritcachetest]\n\tvalue = second-longer\n").expect("rewrite");
3978 restore_mtime(&cfg, t0);
3979 assert_eq!(load_value(gd).as_deref(), Some("second-longer"));
3980 }
3981
3982 #[test]
3983 fn include_targets_are_stamped_and_invalidate() {
3984 let td = tempfile::tempdir().expect("tempdir");
3985 let gd = td.path();
3986 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
3987 let inc = gd.join("extra.conf");
3988 fs::write(&inc, "[gritcachetest]\n\tvalue = one\n").expect("write include v1");
3989 assert_eq!(load_value(gd).as_deref(), Some("one"));
3990
3991 let t0 = mtime_of(&inc);
3995 fs::write(&inc, "[gritcachetest]\n\tvalue = two\n").expect("write include v2");
3996 restore_mtime(&inc, t0);
3997 assert_eq!(load_value(gd).as_deref(), Some("one"));
3998
3999 fs::write(&inc, "[gritcachetest]\n\tvalue = two-longer\n").expect("write include v3");
4001 assert_eq!(load_value(gd).as_deref(), Some("two-longer"));
4002 }
4003
4004 #[test]
4005 fn missing_include_target_is_watched() {
4006 let td = tempfile::tempdir().expect("tempdir");
4007 let gd = td.path();
4008 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
4009 assert_eq!(load_value(gd), None);
4010
4011 fs::write(gd.join("extra.conf"), "[gritcachetest]\n\tvalue = born\n").expect("create");
4014 assert_eq!(load_value(gd).as_deref(), Some("born"));
4015 }
4016
4017 #[test]
4018 fn onbranch_condition_follows_head() {
4019 let td = tempfile::tempdir().expect("tempdir");
4020 let gd = td.path();
4021 fs::write(gd.join("HEAD"), "ref: refs/heads/main\n").expect("write HEAD");
4022 fs::write(
4023 gd.join("config"),
4024 "[includeIf \"onbranch:main\"]\n\tpath = branch.conf\n",
4025 )
4026 .expect("write config");
4027 fs::write(gd.join("branch.conf"), "[gritcachetest]\n\tvalue = onmain\n")
4028 .expect("write branch config");
4029 assert_eq!(load_value(gd).as_deref(), Some("onmain"));
4030
4031 fs::write(gd.join("HEAD"), "ref: refs/heads/dev\n").expect("rewrite HEAD");
4034 assert_eq!(load_value(gd), None);
4035 }
4036}