1use std::collections::HashMap;
31
32use crate::environment::Environment;
33use std::fmt;
34use std::fs;
35use std::path::{Path, PathBuf};
36use std::sync::Arc;
37use std::time::SystemTime;
38
39#[cfg(test)]
40use std::sync::atomic::{AtomicUsize, Ordering};
41
42use crate::error::{BadNumericSource, ConfigError, Error, Result};
43
44#[cfg(test)]
46pub mod cascade_load_counters {
47 use std::cell::Cell;
48
49 use super::{AtomicUsize, Ordering};
50
51 thread_local! {
52 static MEASURING: Cell<bool> = const { Cell::new(false) };
53 }
54
55 static UNCACHED: AtomicUsize = AtomicUsize::new(0);
56 static CACHE_VALIDATED: AtomicUsize = AtomicUsize::new(0);
57
58 pub fn measure<R>(f: impl FnOnce() -> R) -> R {
60 MEASURING.with(|m| {
61 m.set(true);
62 reset();
63 let out = f();
64 m.set(false);
65 out
66 })
67 }
68
69 pub fn reset() {
71 UNCACHED.store(0, Ordering::SeqCst);
72 CACHE_VALIDATED.store(0, Ordering::SeqCst);
73 }
74
75 #[must_use]
77 pub fn uncached_loads() -> usize {
78 UNCACHED.load(Ordering::SeqCst)
79 }
80
81 #[must_use]
83 pub fn cache_validated_loads() -> usize {
84 CACHE_VALIDATED.load(Ordering::SeqCst)
85 }
86
87 #[must_use]
89 pub fn total_loads() -> usize {
90 uncached_loads().saturating_add(cache_validated_loads())
91 }
92
93 pub(crate) fn record_uncached() {
94 MEASURING.with(|m| {
95 if m.get() {
96 UNCACHED.fetch_add(1, Ordering::SeqCst);
97 }
98 });
99 }
100}
101use crate::refs;
102use crate::wildmatch::{wildmatch, WM_CASEFOLD, WM_PATHNAME};
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
106pub enum ConfigScope {
107 System,
109 Global,
111 Local,
113 Worktree,
115 Command,
117}
118
119impl fmt::Display for ConfigScope {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 match self {
122 Self::System => write!(f, "system"),
123 Self::Global => write!(f, "global"),
124 Self::Local => write!(f, "local"),
125 Self::Worktree => write!(f, "worktree"),
126 Self::Command => write!(f, "command"),
127 }
128 }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct ConfigValueOrigin {
134 pub value: Option<String>,
137 pub scope: ConfigScope,
139 pub file: Option<PathBuf>,
141 pub line: Option<usize>,
143}
144
145#[derive(Debug, Clone)]
147pub struct ConfigEntry {
148 pub key: String,
151 pub value: Option<String>,
153 pub scope: ConfigScope,
155 pub file: Option<PathBuf>,
157 pub line: usize,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum ConfigIncludeOrigin {
164 Disk,
166 Stdin,
168 CommandLine,
170 Blob,
172}
173
174#[derive(Debug, Clone)]
177pub struct ConfigFile {
178 pub path: PathBuf,
180 pub scope: ConfigScope,
182 pub entries: Vec<ConfigEntry>,
184 raw_lines: Vec<String>,
186 pub include_origin: ConfigIncludeOrigin,
188}
189
190#[derive(Debug, Clone, Default)]
209pub struct ConfigSet {
210 entries: Vec<ConfigEntry>,
212}
213
214#[derive(Debug, Clone)]
216pub struct IncludeContext {
217 pub git_dir: Option<PathBuf>,
219 pub command_line_relative_include_is_error: bool,
221 pub cwd: PathBuf,
223 pub pwd: Option<String>,
225 pub env: std::sync::Arc<Environment>,
227}
228
229impl Default for IncludeContext {
230 fn default() -> Self {
231 Self {
232 git_dir: None,
233 command_line_relative_include_is_error: false,
234 cwd: PathBuf::from("."),
235 pwd: None,
236 env: std::sync::Arc::new(Environment::empty()),
237 }
238 }
239}
240
241#[derive(Clone)]
243pub struct LoadConfigOptions {
244 pub include_system: bool,
246 pub process_includes: bool,
248 pub command_includes: bool,
250 pub include_ctx: IncludeContext,
252 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
254 pub repo_caches: Option<std::sync::Arc<crate::repo_caches::RepoCaches>>,
256}
257
258impl std::fmt::Debug for LoadConfigOptions {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 f.debug_struct("LoadConfigOptions")
261 .field("include_system", &self.include_system)
262 .field("process_includes", &self.process_includes)
263 .field("command_includes", &self.command_includes)
264 .field("include_ctx", &self.include_ctx)
265 .finish_non_exhaustive()
266 }
267}
268
269impl Default for LoadConfigOptions {
270 fn default() -> Self {
271 Self {
272 include_system: true,
273 process_includes: true,
274 command_includes: true,
275 include_ctx: IncludeContext::default(),
276 diagnostics: None,
277 repo_caches: None,
278 }
279 }
280}
281
282#[must_use]
287pub fn config_entry_origin_file(entry: &ConfigEntry) -> Option<&Path> {
288 if entry.scope == ConfigScope::Command && entry.file.is_none() {
289 None
290 } else {
291 entry.file.as_deref()
292 }
293}
294
295#[must_use]
297pub fn config_entry_origin_line(entry: &ConfigEntry) -> Option<usize> {
298 if entry.scope == ConfigScope::Command && entry.file.is_none() {
299 None
300 } else {
301 (entry.line > 0).then_some(entry.line)
302 }
303}
304
305pub fn canonical_key(raw: &str) -> Result<String> {
321 if raw.contains('\n') || raw.contains('\r') {
323 return Err(Error::Config(
324 format!("invalid key: '{}'", raw.replace('\n', "\\n")).into(),
325 ));
326 }
327
328 let first_dot = raw
329 .find('.')
330 .ok_or_else(|| Error::Config(format!("key does not contain a section: '{raw}'").into()))?;
331 let last_dot = raw
332 .rfind('.')
333 .ok_or_else(|| Error::Config(format!("key does not contain a section: '{raw}'").into()))?;
334
335 if last_dot == raw.len() - 1 {
336 return Err(Error::Config(
337 format!("key does not contain variable name: '{raw}'").into(),
338 ));
339 }
340
341 let section = &raw[..first_dot];
342 let name = &raw[last_dot + 1..];
343
344 if section.is_empty() || !section.chars().all(|c| c.is_alphanumeric() || c == '-') {
346 return Err(Error::Config(
347 format!("invalid key (bad section): '{raw}'").into(),
348 ));
349 }
350
351 if !name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
353 || !name.chars().all(|c| c.is_alphanumeric() || c == '-')
354 {
355 return Err(Error::Config(
356 format!("invalid key (bad variable name): '{raw}'").into(),
357 ));
358 }
359
360 if first_dot == last_dot {
361 Ok(format!(
363 "{}.{}",
364 section.to_lowercase(),
365 name.to_lowercase()
366 ))
367 } else {
368 let subsection = &raw[first_dot + 1..last_dot];
370 Ok(format!(
371 "{}.{}.{}",
372 section.to_lowercase(),
373 subsection,
374 name.to_lowercase()
375 ))
376 }
377}
378
379#[must_use]
383pub fn config_file_display_for_error(path: &Path) -> String {
384 config_error_path_display(path)
385}
386
387fn config_error_path_display(path: &Path) -> String {
388 if path == Path::new("-") {
389 return "standard input".to_owned();
390 }
391 if path.file_name().and_then(|s| s.to_str()) == Some("config")
392 && path
393 .parent()
394 .and_then(|p| p.file_name())
395 .and_then(|s| s.to_str())
396 == Some(".git")
397 {
398 return ".git/config".to_owned();
399 }
400 path.display().to_string()
401}
402
403fn reject_valueless_negotiation_algorithm(
405 key: &str,
406 value: Option<&str>,
407 path: &Path,
408 line: usize,
409) -> Result<()> {
410 if key != "fetch.negotiationalgorithm" || value.is_some() {
411 return Ok(());
412 }
413 Err(ConfigError::BadConfigVariable {
414 key: key.to_owned(),
415 file: config_error_path_display(path),
416 line,
417 }
418 .into())
419}
420
421struct Parser {
423 section: String,
424 subsection: Option<String>,
425}
426
427impl Parser {
428 fn new() -> Self {
429 Self {
430 section: String::new(),
431 subsection: None,
432 }
433 }
434
435 fn valid_variable_name(name: &str) -> bool {
436 let name = name.trim();
437 name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
438 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
439 }
440
441 fn make_key(&self, name: &str) -> String {
443 let sec = self.section.to_lowercase();
444 let var = name.to_lowercase();
445 match &self.subsection {
446 Some(sub) => format!("{sec}.{sub}.{var}"),
447 None => format!("{sec}.{var}"),
448 }
449 }
450
451 fn try_parse_section_with_remainder<'a>(
457 &mut self,
458 line: &'a str,
459 inline_remainder: &mut Option<&'a str>,
460 ) -> bool {
461 let trimmed = line.trim();
462 if !trimmed.starts_with('[') {
463 return false;
464 }
465 let end = {
469 let bytes = trimmed.as_bytes();
470 let mut i = 1; let mut in_quotes = false;
472 let mut found = None;
473 while i < bytes.len() {
474 if in_quotes {
475 if bytes[i] == b'\\' {
476 i += 2; continue;
478 }
479 if bytes[i] == b'"' {
480 in_quotes = false;
481 }
482 } else {
483 if bytes[i] == b'"' {
484 in_quotes = true;
485 }
486 if bytes[i] == b']' {
487 found = Some(i);
488 break;
489 }
490 }
491 i += 1;
492 }
493 match found {
494 Some(i) => i,
495 None => return false,
496 }
497 };
498 let inside = &trimmed[1..end];
499 if let Some(quote_start) = inside.find('"') {
501 self.section = inside[..quote_start].trim().to_owned();
502 let rest = &inside[quote_start + 1..];
503 let mut sub = String::new();
505 let mut chars = rest.chars();
506 while let Some(ch) = chars.next() {
507 if ch == '\\' {
508 if let Some(escaped) = chars.next() {
509 sub.push(escaped);
510 }
511 } else if ch == '"' {
512 break;
513 } else {
514 sub.push(ch);
515 }
516 }
517 self.subsection = Some(sub);
518 } else {
519 self.section = inside.trim().to_owned();
520 self.subsection = None;
521 }
522 let after = trimmed[end + 1..].trim();
524 if !after.is_empty() && !after.starts_with('#') && !after.starts_with(';') {
525 *inline_remainder = Some(after);
526 } else {
527 *inline_remainder = None;
528 }
529 true
530 }
531
532 fn try_parse_section(&mut self, line: &str) -> bool {
534 let mut _remainder = None;
535 self.try_parse_section_with_remainder(line, &mut _remainder)
536 }
537
538 fn try_parse_entry(&self, line: &str) -> Option<(String, Option<String>)> {
542 let trimmed = line.trim();
543 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
544 return None;
545 }
546 if trimmed.starts_with('[') {
547 return None;
548 }
549 if self.section.is_empty() {
550 return None;
551 }
552
553 if let Some(eq_pos) = trimmed.find('=') {
554 let raw_name = trimmed[..eq_pos].trim();
555 if !Self::valid_variable_name(raw_name) {
556 return None;
557 }
558 let raw_value = trimmed[eq_pos + 1..].trim();
559 let value = strip_inline_comment(raw_value);
561 let value = parse_config_value(&value);
562 let key = self.make_key(raw_name);
563 Some((key, Some(value)))
564 } else {
565 let raw_name = strip_inline_comment(trimmed);
567 if raw_name.split_whitespace().count() > 1 {
568 return None;
569 }
570 let raw_name = raw_name.trim();
571 if !Self::valid_variable_name(raw_name) {
572 return None;
573 }
574 let key = self.make_key(raw_name);
575 Some((key, None))
576 }
577 }
578}
579
580fn entry_line_value_has_unclosed_quote(line: &str) -> bool {
590 let trimmed = line.trim();
591 let Some(eq_pos) = trimmed.find('=') else {
592 return false;
593 };
594 let raw_value = trimmed[eq_pos + 1..].trim_start();
595 let mut in_quote = false;
596 let mut last_was_backslash = false;
597 for ch in raw_value.chars() {
598 match ch {
599 '"' if !last_was_backslash => {
600 in_quote = !in_quote;
601 last_was_backslash = false;
602 }
603 '\\' if !last_was_backslash => {
604 last_was_backslash = true;
605 continue;
606 }
607 '#' | ';' if !in_quote && !last_was_backslash => return false,
608 _ => {
609 last_was_backslash = false;
610 }
611 }
612 }
613 in_quote
614}
615
616fn value_line_continues(line: &str) -> bool {
617 let trimmed = line.trim();
618 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
619 return false;
620 }
621 let value_part = match trimmed.find('=') {
624 Some(pos) => &trimmed[pos + 1..],
625 None => return false,
626 };
627 let mut in_quote = false;
629 let mut last_was_backslash = false;
630 let mut in_comment = false;
631 for ch in value_part.chars() {
632 if in_comment {
633 last_was_backslash = false;
635 continue;
636 }
637 match ch {
638 '"' if !last_was_backslash => {
639 in_quote = !in_quote;
640 last_was_backslash = false;
641 }
642 '\\' if !last_was_backslash => {
643 last_was_backslash = true;
644 continue;
645 }
646 '#' | ';' if !in_quote && !last_was_backslash => {
647 in_comment = true;
648 last_was_backslash = false;
649 }
650 _ => {
651 last_was_backslash = false;
652 }
653 }
654 }
655 last_was_backslash && !in_comment
657}
658
659fn strip_inline_comment(s: &str) -> String {
661 let mut in_quote = false;
662 let mut last_was_backslash = false;
663 let mut result = String::with_capacity(s.len());
664 let mut chars = s.chars().peekable();
665 while let Some(ch) = chars.next() {
666 match ch {
667 '"' if !last_was_backslash => {
668 in_quote = !in_quote;
669 result.push(ch);
670 last_was_backslash = false;
671 }
672 '\\' if !last_was_backslash => {
673 last_was_backslash = true;
674 result.push(ch);
675 if in_quote {
676 if let Some(&next) = chars.peek() {
677 result.push(next);
678 chars.next();
679 last_was_backslash = false;
680 }
681 }
682 }
683 '#' | ';' if !in_quote && !last_was_backslash => break,
684 _ => {
685 last_was_backslash = false;
686 result.push(ch);
687 }
688 }
689 }
690 let trimmed = result.trim_end();
692 trimmed.to_owned()
693}
694
695fn parse_config_value(s: &str) -> String {
697 if s.starts_with('"') {
698 unescape_value(s)
699 } else {
700 unescape_unquoted_value(s)
701 }
702}
703
704fn unescape_unquoted_value(s: &str) -> String {
708 let mut result = String::with_capacity(s.len());
709 let mut chars = s.chars();
710 while let Some(ch) = chars.next() {
711 if ch == '\\' {
712 match chars.next() {
713 Some('n') => result.push('\n'),
714 Some('r') => result.push('\r'),
715 Some('t') => result.push('\t'),
716 Some('b') => result.push('\x08'),
717 Some('\\') => result.push('\\'),
718 Some('"') => result.push('"'),
719 Some(other) => {
720 result.push('\\');
721 result.push(other);
722 }
723 None => result.push('\\'),
724 }
725 } else {
726 result.push(ch);
727 }
728 }
729 result
730}
731
732fn unescape_value(s: &str) -> String {
735 let mut result = String::with_capacity(s.len());
736 let mut chars = s.chars();
737 while let Some(ch) = chars.next() {
738 match ch {
739 '"' => { }
740 '\\' => match chars.next() {
741 Some('n') => result.push('\n'),
742 Some('r') => result.push('\r'),
743 Some('t') => result.push('\t'),
744 Some('b') => result.push('\x08'),
745 Some('\\') => result.push('\\'),
746 Some('"') => result.push('"'),
747 Some(other) => {
748 result.push('\\');
749 result.push(other);
750 }
751 None => result.push('\\'),
752 },
753 _ => result.push(ch),
754 }
755 }
756 result
757}
758
759fn escape_subsection(s: &str) -> String {
766 let mut out = String::with_capacity(s.len());
767 for ch in s.chars() {
768 match ch {
769 '"' => out.push_str("\\\""),
770 '\\' => out.push_str("\\\\"),
771 other => out.push(other),
772 }
773 }
774 out
775}
776
777fn escape_value(s: &str) -> String {
778 let leading_dash_needs_quoting = s.starts_with('-') && parse_i64(s).is_err();
781 let must_quote = leading_dash_needs_quoting
782 || s.starts_with(' ')
783 || s.starts_with('\t')
784 || s.ends_with(' ')
785 || s.ends_with('\t')
786 || s.contains('"')
787 || s.contains('#')
788 || s.contains(';');
789
790 if must_quote {
791 return escape_value_quoted(s);
792 }
793
794 if s.contains('\n') || s.contains('\r') || s.contains('\t') || s.contains('\\') {
795 let mut out = String::with_capacity(s.len() + 4);
796 for ch in s.chars() {
797 match ch {
798 '\n' => out.push_str("\\n"),
799 '\r' => out.push_str("\\r"),
800 '\t' => out.push_str("\\t"),
801 '\\' => out.push_str("\\\\"),
802 '"' => out.push_str("\\\""),
803 other => out.push(other),
804 }
805 }
806 return out;
807 }
808
809 s.to_owned()
810}
811
812fn compile_config_value_pattern(pat: &str) -> std::result::Result<(regex::Regex, bool), Error> {
814 let (negated, actual_pat) = if let Some(rest) = pat.strip_prefix('!') {
815 (true, rest)
816 } else {
817 (false, pat)
818 };
819 let re = regex::Regex::new(actual_pat)
820 .map_err(|e| Error::Config(format!("invalid value-pattern regex: {e}").into()))?;
821 Ok((re, negated))
822}
823
824fn value_matches_config_pattern(re: ®ex::Regex, negated: bool, value: &str) -> bool {
825 let matched = re.is_match(value);
826 if negated {
827 !matched
828 } else {
829 matched
830 }
831}
832
833fn escape_value_quoted(s: &str) -> String {
834 let mut out = String::with_capacity(s.len() + 4);
835 out.push('"');
836 for ch in s.chars() {
837 match ch {
838 '"' => out.push_str("\\\""),
839 '\\' => out.push_str("\\\\"),
840 '\n' => out.push_str("\\n"),
841 '\r' => out.push_str("\\r"),
842 '\t' => out.push_str("\\t"),
843 other => out.push(other),
844 }
845 }
846 out.push('"');
847 out
848}
849
850fn format_comment_suffix(comment: Option<&str>) -> Result<String> {
859 match comment {
860 None => Ok(String::new()),
861 Some(c) => {
862 if c.contains('\n') || c.contains('\r') {
863 return Err(Error::Config(ConfigError::MultilineCommentNotAllowed));
864 }
865 let leading_blanks = c.find(|ch| ch != ' ' && ch != '\t').unwrap_or(c.len());
866 let after_blanks = &c[leading_blanks..];
867 if leading_blanks > 0 && after_blanks.starts_with('#') {
868 Ok(c.to_owned())
869 } else if c.starts_with('#') {
870 Ok(format!(" {c}"))
871 } else {
872 Ok(format!(" # {c}"))
873 }
874 }
875 }
876}
877
878impl ConfigFile {
879 pub fn parse(path: &Path, content: &str, scope: ConfigScope) -> Result<Self> {
891 let content = content.strip_prefix('\u{feff}').unwrap_or(content);
892 let raw_lines: Vec<String> = content
893 .lines()
894 .map(|l| l.strip_suffix('\r').unwrap_or(l))
895 .map(String::from)
896 .collect();
897 let mut entries = Vec::new();
898 let mut parser = Parser::new();
899
900 let mut idx = 0;
901 while idx < raw_lines.len() {
902 let start_idx = idx;
903 let line = &raw_lines[idx];
904 idx += 1;
905
906 let trimmed = line.trim();
908 if trimmed.starts_with('#') || trimmed.starts_with(';') {
909 continue;
910 }
911
912 let mut inline_remainder = None;
913 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
914 if let Some(remainder) = inline_remainder {
916 if let Some((key, value)) = parser.try_parse_entry(remainder) {
917 reject_valueless_negotiation_algorithm(
918 &key,
919 value.as_deref(),
920 path,
921 start_idx + 1,
922 )?;
923 entries.push(ConfigEntry {
924 key,
925 value,
926 scope,
927 file: Some(path.to_path_buf()),
928 line: start_idx + 1,
929 });
930 }
931 }
932 continue;
933 }
934
935 let mut logical_line = line.clone();
938 while value_line_continues(&logical_line) && idx < raw_lines.len() {
939 let t = logical_line.trim_end();
941 logical_line = t[..t.len() - 1].to_string();
942 let next = raw_lines[idx].trim_start();
944 logical_line.push_str(next);
945 idx += 1;
946 }
947
948 while entry_line_value_has_unclosed_quote(&logical_line) && idx < raw_lines.len() {
949 let next = raw_lines[idx].trim_start();
950 logical_line.push_str(next);
951 idx += 1;
952 }
953 if entry_line_value_has_unclosed_quote(&logical_line) {
954 let file_disp = config_error_path_display(path);
955 return Err(ConfigError::BadConfigLineInFile {
956 file: file_disp,
957 line: start_idx + 1,
958 }
959 .into());
960 }
961
962 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
963 reject_valueless_negotiation_algorithm(
964 &key,
965 value.as_deref(),
966 path,
967 start_idx + 1,
968 )?;
969 entries.push(ConfigEntry {
970 key,
971 value,
972 scope,
973 file: Some(path.to_path_buf()),
974 line: start_idx + 1,
975 });
976 } else if logical_line.trim().is_empty() {
977 continue;
978 } else {
979 let file_disp = config_error_path_display(path);
980 let location = if path == Path::new("-") {
981 file_disp
982 } else {
983 format!("file {file_disp}")
984 };
985 return Err(ConfigError::BadConfigLine {
986 location,
987 line: start_idx + 1,
988 }
989 .into());
990 }
991 }
992
993 Ok(Self {
994 path: path.to_path_buf(),
995 scope,
996 entries,
997 raw_lines,
998 include_origin: ConfigIncludeOrigin::Disk,
999 })
1000 }
1001
1002 pub fn parse_gitmodules_best_effort(
1008 path: &Path,
1009 content: &str,
1010 scope: ConfigScope,
1011 ) -> (Vec<ConfigEntry>, Option<usize>) {
1012 let raw_lines: Vec<String> = content
1013 .lines()
1014 .map(|l| l.strip_suffix('\r').unwrap_or(l))
1015 .map(String::from)
1016 .collect();
1017 let mut entries = Vec::new();
1018 let mut parser = Parser::new();
1019
1020 let mut idx = 0;
1021 while idx < raw_lines.len() {
1022 let start_idx = idx;
1023 let line = &raw_lines[idx];
1024 idx += 1;
1025
1026 let trimmed = line.trim();
1027 if trimmed.starts_with('#') || trimmed.starts_with(';') {
1028 continue;
1029 }
1030
1031 let mut inline_remainder = None;
1032 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
1033 if let Some(remainder) = inline_remainder {
1034 if let Some((key, value)) = parser.try_parse_entry(remainder) {
1035 entries.push(ConfigEntry {
1036 key,
1037 value,
1038 scope,
1039 file: Some(path.to_path_buf()),
1040 line: start_idx + 1,
1041 });
1042 }
1043 }
1044 continue;
1045 }
1046
1047 let mut logical_line = line.clone();
1048 while value_line_continues(&logical_line) && idx < raw_lines.len() {
1049 let t = logical_line.trim_end();
1050 logical_line = t[..t.len() - 1].to_string();
1051 let next = raw_lines[idx].trim_start();
1052 logical_line.push_str(next);
1053 idx += 1;
1054 }
1055
1056 while entry_line_value_has_unclosed_quote(&logical_line) && idx < raw_lines.len() {
1057 let next = raw_lines[idx].trim_start();
1058 logical_line.push_str(next);
1059 idx += 1;
1060 }
1061 if entry_line_value_has_unclosed_quote(&logical_line) {
1062 return (entries, Some(start_idx + 1));
1063 }
1064
1065 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
1066 entries.push(ConfigEntry {
1067 key,
1068 value,
1069 scope,
1070 file: Some(path.to_path_buf()),
1071 line: start_idx + 1,
1072 });
1073 }
1074 }
1075
1076 (entries, None)
1077 }
1078
1079 #[must_use]
1081 pub fn get(&self, key: &str) -> Option<String> {
1082 let canon = canonical_key(key).ok()?;
1083 self.entries
1084 .iter()
1085 .rev()
1086 .find(|e| e.key == canon)
1087 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
1088 }
1089
1090 pub fn parse_with_origin(
1092 path: &Path,
1093 content: &str,
1094 scope: ConfigScope,
1095 include_origin: ConfigIncludeOrigin,
1096 ) -> Result<Self> {
1097 let mut f = Self::parse(path, content, scope)?;
1098 f.include_origin = include_origin;
1099 Ok(f)
1100 }
1101
1102 pub fn from_git_config_parameters(path: &Path, raw: &str) -> Result<Self> {
1107 let mut entries = Vec::new();
1108 for entry in parse_config_parameters_strict(raw)? {
1109 match entry {
1110 ConfigParameter::Pair { key, value } => {
1111 let canon = canonical_key(key.trim())?;
1112 entries.push(ConfigEntry {
1113 key: canon,
1114 value,
1115 scope: ConfigScope::Command,
1116 file: None,
1117 line: 0,
1118 });
1119 }
1120 ConfigParameter::OldStyle(entry) => {
1121 if let Some((key, val)) = entry.split_once('=') {
1122 let canon = canonical_key(key.trim())?;
1123 entries.push(ConfigEntry {
1124 key: canon,
1125 value: Some(val.to_owned()),
1126 scope: ConfigScope::Command,
1127 file: None,
1128 line: 0,
1129 });
1130 } else {
1131 let canon = canonical_key(entry.trim())?;
1132 entries.push(ConfigEntry {
1133 key: canon,
1134 value: None,
1135 scope: ConfigScope::Command,
1136 file: None,
1137 line: 0,
1138 });
1139 }
1140 }
1141 }
1142 }
1143 Ok(Self {
1144 path: path.to_path_buf(),
1145 scope: ConfigScope::Command,
1146 entries,
1147 raw_lines: Vec::new(),
1148 include_origin: ConfigIncludeOrigin::CommandLine,
1149 })
1150 }
1151
1152 pub fn from_path(path: &Path, scope: ConfigScope) -> Result<Option<Self>> {
1161 match fs::read_to_string(path) {
1162 Ok(content) => Ok(Some(Self::parse(path, &content, scope)?)),
1163 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
1164 Err(e) => Err(Error::Io(e)),
1165 }
1166 }
1167
1168 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
1179 self.set_with_comment(key, value, None)
1180 }
1181
1182 pub fn set_with_comment(
1184 &mut self,
1185 key: &str,
1186 value: &str,
1187 comment: Option<&str>,
1188 ) -> Result<()> {
1189 let canon = canonical_key(key)?;
1190 if self.entries.iter().filter(|e| e.key == canon).count() > 1 {
1191 return Err(Error::Config(ConfigError::MultipleValues {
1192 key: key.to_owned(),
1193 }));
1194 }
1195 let raw_var = raw_variable_name(key);
1196 let comment_suffix = format_comment_suffix(comment)?;
1197
1198 let existing_idx = self.entries.iter().rposition(|e| e.key == canon);
1200
1201 if let Some(idx) = existing_idx {
1202 let line_idx = self.entries[idx].line - 1;
1203 let raw_line = &self.raw_lines[line_idx];
1204 if is_section_header_with_inline_entry(raw_line) {
1205 let header_only = extract_section_header(raw_line);
1207 self.raw_lines[line_idx] = header_only;
1208 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1209 self.raw_lines.insert(line_idx + 1, new_line);
1210 let content = self.raw_lines.join("\n");
1212 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1213 self.entries = reparsed.entries;
1214 self.raw_lines = reparsed.raw_lines;
1215 } else {
1216 self.raw_lines[line_idx] =
1217 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1218 self.entries[idx].value = Some(value.to_owned());
1219 }
1220 } else {
1221 let (section, subsection, _var) = split_key(&canon)?;
1223 let (raw_sec, raw_sub) = raw_section_parts(key);
1224 let section_line = self.find_or_create_section_preserving_case(
1225 §ion,
1226 subsection.as_deref(),
1227 &raw_sec,
1228 raw_sub.as_deref(),
1229 );
1230 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1231
1232 let insert_at = self.last_line_in_section(section_line) + 1;
1234 self.raw_lines.insert(insert_at, new_line);
1235
1236 let content = self.raw_lines.join("\n");
1238 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1239 self.entries = reparsed.entries;
1240 self.raw_lines = reparsed.raw_lines;
1241 }
1242
1243 Ok(())
1244 }
1245
1246 pub fn replace_all(
1251 &mut self,
1252 key: &str,
1253 value: &str,
1254 value_pattern: Option<&str>,
1255 ) -> Result<()> {
1256 self.replace_all_with_comment(key, value, value_pattern, None)
1257 }
1258
1259 pub fn replace_all_with_comment(
1264 &mut self,
1265 key: &str,
1266 value: &str,
1267 value_pattern: Option<&str>,
1268 comment: Option<&str>,
1269 ) -> Result<()> {
1270 let canon = canonical_key(key)?;
1271 let comment_suffix = format_comment_suffix(comment)?;
1272
1273 let (re, negated) = match value_pattern {
1274 Some(pat) => {
1275 let (compiled, neg) = compile_config_value_pattern(pat)?;
1276 (Some(compiled), neg)
1277 }
1278 None => (None, false),
1279 };
1280
1281 let matching_indices: Vec<usize> = self
1283 .entries
1284 .iter()
1285 .enumerate()
1286 .filter(|(_, e)| {
1287 if e.key != canon {
1288 return false;
1289 }
1290 if let Some(ref re) = re {
1291 let v = e.value.as_deref().unwrap_or("");
1292 value_matches_config_pattern(re, negated, v)
1293 } else {
1294 true
1295 }
1296 })
1297 .map(|(i, _)| i)
1298 .collect();
1299
1300 if matching_indices.is_empty() {
1301 return self.add_value_with_comment(key, value, comment);
1303 }
1304
1305 let raw_var = raw_variable_name(key);
1306
1307 let target_idx = *matching_indices
1308 .last()
1309 .ok_or_else(|| Error::Config("missing config match".to_owned().into()))?;
1310 let target_line_idx = self.entries[target_idx].line - 1;
1311 let raw_line = &self.raw_lines[target_line_idx];
1312 if is_section_header_with_inline_entry(raw_line) {
1313 let header = extract_section_header(raw_line);
1314 self.raw_lines[target_line_idx] = header;
1315 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1316 self.raw_lines.insert(target_line_idx + 1, new_line);
1317 } else {
1318 self.raw_lines[target_line_idx] =
1319 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1320 }
1321
1322 for &idx in matching_indices.iter().rev() {
1323 if idx == target_idx {
1324 continue;
1325 }
1326 let line_idx = self.entries[idx].line - 1;
1327 self.remove_entry_line(line_idx);
1328 }
1329
1330 let content = self.raw_lines.join("\n");
1332 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1333 self.entries = reparsed.entries;
1334 self.raw_lines = reparsed.raw_lines;
1335
1336 Ok(())
1337 }
1338
1339 pub fn count(&self, key: &str) -> Result<usize> {
1341 let canon = canonical_key(key)?;
1342 Ok(self.entries.iter().filter(|e| e.key == canon).count())
1343 }
1344
1345 fn remove_entry_line(&mut self, line_idx: usize) {
1356 if is_section_header_with_inline_entry(&self.raw_lines[line_idx]) {
1357 let header = extract_section_header(&self.raw_lines[line_idx]);
1359 self.raw_lines[line_idx] = header;
1360 } else {
1361 let mut lines_to_remove = 1;
1363 let mut check_line = self.raw_lines[line_idx].clone();
1364 while value_line_continues(&check_line)
1365 && (line_idx + lines_to_remove) < self.raw_lines.len()
1366 {
1367 check_line = self.raw_lines[line_idx + lines_to_remove].clone();
1368 lines_to_remove += 1;
1369 }
1370 for _ in 0..lines_to_remove {
1371 self.raw_lines.remove(line_idx);
1372 }
1373 }
1374 }
1375
1376 pub fn unset_last(&mut self, key: &str) -> Result<usize> {
1380 let canon = canonical_key(key)?;
1381 let last_idx = self.entries.iter().rposition(|e| e.key == canon);
1382
1383 if let Some(idx) = last_idx {
1384 let line_idx = self.entries[idx].line - 1;
1385 self.remove_entry_line(line_idx);
1386 let (section, subsection, _) = split_key(&canon)?;
1387 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
1388 let content = self.raw_lines.join("\n");
1389 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1390 self.entries = reparsed.entries;
1391 self.raw_lines = reparsed.raw_lines;
1392 Ok(1)
1393 } else {
1394 Ok(0)
1395 }
1396 }
1397
1398 pub fn unset(&mut self, key: &str) -> Result<usize> {
1408 let canon = canonical_key(key)?;
1409 let line_indices: Vec<usize> = self
1410 .entries
1411 .iter()
1412 .filter(|e| e.key == canon)
1413 .map(|e| e.line - 1)
1414 .collect();
1415
1416 let count = line_indices.len();
1417 for &idx in line_indices.iter().rev() {
1419 self.remove_entry_line(idx);
1420 }
1421
1422 if count > 0 {
1423 if let Ok((section, subsection, _)) = split_key(&canon) {
1424 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
1425 }
1426 let content = self.raw_lines.join("\n");
1427 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1428 self.entries = reparsed.entries;
1429 self.raw_lines = reparsed.raw_lines;
1430 }
1431
1432 Ok(count)
1433 }
1434
1435 pub fn unset_matching(
1444 &mut self,
1445 key: &str,
1446 value_pattern: Option<&str>,
1447 preserve_empty_section_header: bool,
1448 ) -> Result<usize> {
1449 let canon = canonical_key(key)?;
1450 let pattern = match value_pattern {
1451 Some(pat) => {
1452 let (re, negated) = compile_config_value_pattern(pat)?;
1453 Some((re, negated))
1454 }
1455 None => None,
1456 };
1457
1458 let line_indices: Vec<usize> = self
1459 .entries
1460 .iter()
1461 .filter(|e| {
1462 if e.key != canon {
1463 return false;
1464 }
1465 if let Some((ref re, negated)) = pattern {
1466 let v = e.value.as_deref().unwrap_or("");
1467 value_matches_config_pattern(re, negated, v)
1468 } else {
1469 true
1470 }
1471 })
1472 .map(|e| e.line - 1)
1473 .collect();
1474
1475 let count = line_indices.len();
1476 for &idx in line_indices.iter().rev() {
1477 self.remove_entry_line(idx);
1478 }
1479
1480 if count > 0 {
1481 if !preserve_empty_section_header {
1482 let (section, subsection, _) = split_key(&canon)?;
1483 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
1484 }
1485
1486 let content = self.raw_lines.join("\n");
1487 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1488 self.entries = reparsed.entries;
1489 self.raw_lines = reparsed.raw_lines;
1490 }
1491
1492 Ok(count)
1493 }
1494
1495 pub fn remove_section(&mut self, section: &str) -> Result<bool> {
1501 let (sec_name, sub_name) = parse_section_name(section);
1502 let sec_lower = sec_name.to_lowercase();
1503
1504 let mut remove = vec![false; self.raw_lines.len()];
1505 let mut removing = false;
1506 let mut found = false;
1507 let mut parser = Parser::new();
1508
1509 for (idx, line) in self.raw_lines.iter().enumerate() {
1510 if parser.try_parse_section(line) {
1511 removing = section_matches(&parser, &sec_lower, sub_name);
1512 found |= removing;
1513 }
1514 if removing {
1515 remove[idx] = true;
1516 }
1517 }
1518
1519 if found {
1520 self.raw_lines = self
1521 .raw_lines
1522 .iter()
1523 .enumerate()
1524 .filter_map(|(idx, line)| (!remove[idx]).then_some(line.clone()))
1525 .collect();
1526 let content = self.raw_lines.join("\n");
1527 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1528 self.entries = reparsed.entries;
1529 self.raw_lines = reparsed.raw_lines;
1530 Ok(true)
1531 } else {
1532 Ok(false)
1533 }
1534 }
1535
1536 pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
1543 let (old_sec, old_sub) = parse_section_name(old_name);
1544 let (new_sec, new_sub) = parse_section_name(new_name);
1545 validate_section_name(new_sec, new_sub)?;
1546 let old_lower = old_sec.to_lowercase();
1547
1548 let mut found = false;
1549 let mut parser = Parser::new();
1550
1551 let mut idx = 0usize;
1552 while idx < self.raw_lines.len() {
1553 let line = self.raw_lines[idx].clone();
1554 let mut inline_remainder = None;
1555 if parser.try_parse_section_with_remainder(&line, &mut inline_remainder)
1556 && section_matches(&parser, &old_lower, old_sub)
1557 {
1558 let header = match new_sub {
1560 Some(sub) => format!("[{} \"{}\"]", new_sec, sub),
1561 None => format!("[{}]", new_sec),
1562 };
1563 self.raw_lines[idx] = header;
1564 if let Some(remainder) = inline_remainder {
1565 self.raw_lines
1566 .insert(idx + 1, format!("\t{}", remainder.trim()));
1567 idx += 1;
1568 }
1569 found = true;
1570 }
1571 idx += 1;
1572 }
1573
1574 if found {
1575 let content = self.raw_lines.join("\n");
1576 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1577 self.entries = reparsed.entries;
1578 self.raw_lines = reparsed.raw_lines;
1579 }
1580
1581 Ok(found)
1582 }
1583
1584 pub fn add_value(&mut self, key: &str, value: &str) -> Result<()> {
1589 self.add_value_with_comment(key, value, None)
1590 }
1591
1592 pub fn add_value_with_comment(
1594 &mut self,
1595 key: &str,
1596 value: &str,
1597 comment: Option<&str>,
1598 ) -> Result<()> {
1599 let canon = canonical_key(key)?;
1600 let raw_var = raw_variable_name(key);
1601 let comment_suffix = format_comment_suffix(comment)?;
1602 let (section, subsection, _var) = split_key(&canon)?;
1603 let (raw_sec, raw_sub) = raw_section_parts(key);
1604
1605 let section_line = self.find_or_create_section_preserving_case(
1606 §ion,
1607 subsection.as_deref(),
1608 &raw_sec,
1609 raw_sub.as_deref(),
1610 );
1611 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1612 let insert_at = self.last_line_in_section(section_line) + 1;
1613 self.raw_lines.insert(insert_at, new_line);
1614
1615 let content = self.raw_lines.join("\n");
1617 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1618 self.entries = reparsed.entries;
1619 self.raw_lines = reparsed.raw_lines;
1620
1621 Ok(())
1622 }
1623
1624 fn remove_empty_section_headers_matching(&mut self, section: &str, subsection: Option<&str>) {
1627 let (Ok(section_re), Ok(comment_re)) = (
1628 regex::Regex::new(r"^\s*\["),
1629 regex::Regex::new(r"^\s*(#|;)"),
1630 ) else {
1631 return;
1633 };
1634
1635 let mut to_remove: Vec<usize> = Vec::new();
1636 let len = self.raw_lines.len();
1637 let section_lower = section.to_lowercase();
1638 let mut parser = Parser::new();
1639
1640 for i in 0..len {
1641 let line = &self.raw_lines[i];
1642 if !section_re.is_match(line) {
1643 continue;
1644 }
1645 if !parser.try_parse_section(line)
1646 || !section_matches(&parser, §ion_lower, subsection)
1647 {
1648 continue;
1649 }
1650 if is_section_header_with_inline_entry(line) {
1652 continue;
1653 }
1654 let has_attached_leading_comment = self.raw_lines[..i]
1655 .iter()
1656 .enumerate()
1657 .rev()
1658 .find(|(_, line)| !line.trim().is_empty())
1659 .is_some_and(|(idx, line)| {
1660 comment_re.is_match(line)
1661 && idx
1662 .checked_sub(1)
1663 .is_none_or(|prev| !value_line_continues(&self.raw_lines[prev]))
1664 });
1665 if has_attached_leading_comment {
1666 continue;
1667 }
1668 let mut has_entries = false;
1671 for j in (i + 1)..len {
1672 let next = self.raw_lines[j].trim();
1673 if next.is_empty() {
1674 continue;
1675 }
1676 if section_re.is_match(&self.raw_lines[j]) {
1677 break;
1678 }
1679 if comment_re.is_match(&self.raw_lines[j]) {
1680 has_entries = true;
1682 break;
1683 }
1684 has_entries = true;
1686 break;
1687 }
1688 if !has_entries {
1689 to_remove.push(i);
1690 }
1691 }
1692
1693 for &idx in to_remove.iter().rev() {
1695 self.raw_lines.remove(idx);
1696 }
1697
1698 while self.raw_lines.last().is_some_and(|l| l.trim().is_empty()) {
1700 self.raw_lines.pop();
1701 }
1702 }
1703
1704 pub fn write(&self) -> Result<()> {
1710 let lock_path = config_lock_path(&self.path);
1711 if lock_path.exists() {
1712 return Err(Error::Config(ConfigError::ConfigFileLocked {
1713 path: lock_path.display().to_string(),
1714 }));
1715 }
1716
1717 let content = self.raw_lines.join("\n");
1718 let bytes: Vec<u8> = if content.trim().is_empty() {
1719 Vec::new()
1720 } else if content.ends_with('\n') {
1721 content.into_bytes()
1722 } else {
1723 format!("{content}\n").into_bytes()
1724 };
1725
1726 let mut lock_file = fs::OpenOptions::new()
1727 .write(true)
1728 .create_new(true)
1729 .open(&lock_path)
1730 .map_err(|e| {
1731 if e.kind() == std::io::ErrorKind::AlreadyExists {
1732 Error::Config(ConfigError::ConfigFileLocked {
1733 path: lock_path.display().to_string(),
1734 })
1735 } else {
1736 Error::Io(e)
1737 }
1738 })?;
1739
1740 if let Ok(meta) = fs::metadata(&self.path) {
1741 #[cfg(unix)]
1742 {
1743 use std::os::unix::fs::PermissionsExt;
1744 let mode = meta.permissions().mode();
1745 let _ = fs::set_permissions(&lock_path, fs::Permissions::from_mode(mode));
1746 }
1747 }
1748
1749 use std::io::Write as _;
1750 lock_file.write_all(&bytes).map_err(|e| {
1751 let _ = fs::remove_file(&lock_path);
1752 Error::Io(e)
1753 })?;
1754 lock_file.sync_all().map_err(|e| {
1755 let _ = fs::remove_file(&lock_path);
1756 Error::Io(e)
1757 })?;
1758 drop(lock_file);
1759
1760 fs::rename(&lock_path, &self.path).map_err(|e| {
1761 let _ = fs::remove_file(&lock_path);
1762 Error::Io(e)
1763 })?;
1764 Ok(())
1765 }
1766
1767 fn find_or_create_section_preserving_case(
1770 &mut self,
1771 section: &str,
1772 subsection: Option<&str>,
1773 raw_section: &str,
1774 raw_subsection: Option<&str>,
1775 ) -> usize {
1776 let sec_lower = section.to_lowercase();
1777 let mut parser = Parser::new();
1778 let mut last_match = None;
1779
1780 for (idx, line) in self.raw_lines.iter().enumerate() {
1781 if parser.try_parse_section(line) && section_matches(&parser, &sec_lower, subsection) {
1782 last_match = Some(idx);
1783 }
1784 }
1785 if let Some(idx) = last_match {
1786 return idx;
1787 }
1788
1789 let header = match raw_subsection {
1791 Some(sub) => {
1792 let escaped = escape_subsection(sub);
1793 format!("[{} \"{}\"]", raw_section, escaped)
1794 }
1795 None => format!("[{}]", raw_section),
1796 };
1797 self.raw_lines.push(header);
1798 self.raw_lines.len() - 1
1799 }
1800
1801 fn last_line_in_section(&self, section_line: usize) -> usize {
1803 let mut last = section_line;
1804 for idx in (section_line + 1)..self.raw_lines.len() {
1805 let trimmed = self.raw_lines[idx].trim();
1806 if trimmed.starts_with('[') {
1807 break;
1808 }
1809 last = idx;
1810 }
1811 last
1812 }
1813}
1814
1815impl ConfigSet {
1818 #[must_use]
1820 pub fn new() -> Self {
1821 Self {
1822 entries: Vec::new(),
1823 }
1824 }
1825
1826 #[must_use]
1828 pub fn entries(&self) -> &[ConfigEntry] {
1829 &self.entries
1830 }
1831
1832 pub fn merge(&mut self, file: &ConfigFile) {
1837 self.entries.extend(file.entries.iter().cloned());
1838 }
1839
1840 pub fn merge_set(&mut self, other: &ConfigSet) {
1842 self.entries.extend(other.entries.iter().cloned());
1843 }
1844
1845 pub fn add_command_override(&mut self, key: &str, value: &str) -> Result<()> {
1847 let canon = canonical_key(key)?;
1848 self.entries.push(ConfigEntry {
1849 key: canon,
1850 value: Some(value.to_owned()),
1851 scope: ConfigScope::Command,
1852 file: None,
1853 line: 0,
1854 });
1855 Ok(())
1856 }
1857
1858 #[must_use]
1869 pub fn get(&self, key: &str) -> Option<String> {
1870 let canon = canonical_key(key).ok()?;
1871 self.entries
1872 .iter()
1873 .rev()
1874 .find(|e| e.key == canon)
1875 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
1876 }
1877
1878 #[must_use]
1883 pub fn get_last_entry(&self, key: &str) -> Option<ConfigEntry> {
1884 let canon = canonical_key(key).ok()?;
1885 self.entries.iter().rev().find(|e| e.key == canon).cloned()
1886 }
1887
1888 pub fn get_with_origin(&self, key: &str) -> Result<Option<ConfigValueOrigin>> {
1894 let _canon = canonical_key(key)?;
1895 Ok(self.get_last_entry(key).map(|e| ConfigValueOrigin {
1896 value: e.value.clone().or_else(|| Some("true".to_owned())),
1897 scope: e.scope,
1898 file: config_entry_origin_file(&e).map(Path::to_path_buf),
1899 line: config_entry_origin_line(&e),
1900 }))
1901 }
1902
1903 pub fn get_all_with_origin(&self, key: &str) -> Result<Vec<ConfigValueOrigin>> {
1909 let canon = canonical_key(key)?;
1910 Ok(self
1911 .entries
1912 .iter()
1913 .filter(|e| e.key == canon)
1914 .map(|e| ConfigValueOrigin {
1915 value: e.value.clone().or_else(|| Some("true".to_owned())),
1916 scope: e.scope,
1917 file: config_entry_origin_file(e).map(Path::to_path_buf),
1918 line: config_entry_origin_line(e),
1919 })
1920 .collect())
1921 }
1922
1923 #[must_use]
1925 pub fn get_all(&self, key: &str) -> Vec<String> {
1926 let canon = match canonical_key(key) {
1927 Ok(c) => c,
1928 Err(_) => return Vec::new(),
1929 };
1930 self.entries
1931 .iter()
1932 .filter(|e| e.key == canon)
1933 .map(|e| e.value.clone().unwrap_or_default())
1934 .collect()
1935 }
1936
1937 #[must_use]
1941 pub fn get_all_raw(&self, key: &str) -> Vec<Option<String>> {
1942 let canon = match canonical_key(key) {
1943 Ok(c) => c,
1944 Err(_) => return Vec::new(),
1945 };
1946 self.entries
1947 .iter()
1948 .filter(|e| e.key == canon)
1949 .map(|e| e.value.clone())
1950 .collect()
1951 }
1952
1953 #[must_use]
1958 pub fn has_key(&self, key: &str) -> bool {
1959 let Ok(canon) = canonical_key(key) else {
1960 return false;
1961 };
1962 self.entries.iter().any(|e| e.key == canon)
1963 }
1964
1965 pub fn get_bool(&self, key: &str) -> Option<std::result::Result<bool, String>> {
1971 let v = self.get(key)?;
1972 if canonical_key(key).ok().as_deref() == Some("pack.allowpackreuse") {
1973 let lower = v.trim().to_ascii_lowercase();
1974 if lower == "single" || lower == "multi" {
1975 return None;
1976 }
1977 }
1978 Some(parse_bool(&v))
1979 }
1980
1981 #[must_use]
1987 pub fn quote_path_fully(&self) -> bool {
1988 let from_key = |key: &str| self.get_bool(key).and_then(|r| r.ok());
1989 from_key("core.quotepath")
1990 .or_else(|| from_key("core.quotePath"))
1991 .unwrap_or(true)
1992 }
1993
1994 #[must_use]
1996 pub fn pack_write_bitmap_hash_cache(&self) -> bool {
1997 self.get_bool("pack.writebitmaphashcache")
1998 .or_else(|| self.get_bool("pack.writeBitmapHashCache"))
1999 .and_then(|r| r.ok())
2000 .unwrap_or(true)
2001 }
2002
2003 #[must_use]
2005 pub fn pack_write_bitmap_lookup_table(&self) -> bool {
2006 self.get_bool("pack.writebitmaplookuptable")
2007 .or_else(|| self.get_bool("pack.writeBitmapLookupTable"))
2008 .and_then(|r| r.ok())
2009 .unwrap_or(false)
2010 }
2011
2012 #[must_use]
2014 pub fn pack_prefer_bitmap_tips(&self) -> Vec<String> {
2015 let mut out = self.get_all("pack.preferbitmaptips");
2016 if out.is_empty() {
2017 out = self.get_all("pack.preferBitmapTips");
2018 }
2019 out
2020 }
2021
2022 #[must_use]
2024 pub fn pack_bitmap_write_options(&self) -> crate::pack_bitmap::PackBitmapWriteOptions {
2025 crate::pack_bitmap::PackBitmapWriteOptions {
2026 full_dag: true,
2027 hash_cache: self.pack_write_bitmap_hash_cache(),
2028 lookup_table: self.pack_write_bitmap_lookup_table(),
2029 prefer_bitmap_tips: self.pack_prefer_bitmap_tips(),
2030 verify_reachability_closure: true,
2031 }
2032 }
2033
2034 #[must_use]
2038 pub fn pack_write_reverse_index_default(&self, env: &Environment) -> bool {
2039 if env.git_test_no_write_rev_index.as_deref().is_some_and(|v| {
2040 let s = v.trim().to_ascii_lowercase();
2041 matches!(s.as_str(), "1" | "true" | "yes" | "on")
2042 }) {
2043 return false;
2044 }
2045 if self
2046 .get("pack.writereverseindex")
2047 .or_else(|| self.get("pack.writeReverseIndex"))
2048 .is_some_and(|v| v.trim().is_empty())
2049 {
2050 return false;
2051 }
2052 self.get_bool("pack.writereverseindex")
2053 .or_else(|| self.get_bool("pack.writeReverseIndex"))
2054 .and_then(|r| r.ok())
2055 .unwrap_or(true)
2056 }
2057
2058 #[must_use]
2060 pub fn pack_index_threads(&self) -> Option<usize> {
2061 self.get_i64("pack.threads")
2062 .and_then(|r| r.ok())
2063 .and_then(|n| usize::try_from(n).ok())
2064 }
2065
2066 #[must_use]
2070 pub fn pack_object_window(&self) -> usize {
2071 match self.get_i64("pack.window") {
2072 None => 10,
2073 Some(Ok(n)) if n >= 0 => usize::try_from(n).unwrap_or(10),
2074 Some(Ok(_)) | Some(Err(_)) => 10,
2075 }
2076 }
2077
2078 #[must_use]
2082 pub fn pack_window_memory_bytes(&self) -> u64 {
2084 match self.get_i64("pack.windowmemory") {
2085 None => 0,
2086 Some(Ok(n)) if n >= 0 => u64::try_from(n).unwrap_or(0),
2087 Some(Ok(_)) | Some(Err(_)) => 0,
2088 }
2089 }
2090
2091 pub fn pack_object_depth(&self) -> usize {
2093 match self.get_i64("pack.depth") {
2094 None => crate::pack::DEFAULT_PACK_DEPTH,
2095 Some(Ok(n)) if n >= 0 => usize::try_from(n).unwrap_or(crate::pack::DEFAULT_PACK_DEPTH),
2096 Some(Ok(_)) | Some(Err(_)) => crate::pack::DEFAULT_PACK_DEPTH,
2097 }
2098 }
2099
2100 #[must_use]
2107 pub fn gc_auto_threshold(&self) -> Option<usize> {
2108 match self.get_i64("gc.auto") {
2109 Some(Ok(n)) if n <= 0 => None,
2110 Some(Ok(n)) => Some(usize::try_from(n).unwrap_or(usize::MAX)),
2111 None | Some(Err(_)) => Some(DEFAULT_GC_AUTO_THRESHOLD),
2112 }
2113 }
2114
2115 #[must_use]
2121 pub fn gc_auto_pack_limit(&self) -> Option<usize> {
2122 match self.get_i64("gc.autoPackLimit") {
2123 Some(Ok(n)) if n <= 0 => None,
2124 Some(Ok(n)) => Some(usize::try_from(n).unwrap_or(usize::MAX)),
2125 None | Some(Err(_)) => Some(DEFAULT_GC_AUTO_PACK_LIMIT),
2126 }
2127 }
2128
2129 #[must_use]
2132 pub fn gc_auto_detach(&self) -> bool {
2133 self.get_bool("gc.autoDetach")
2134 .and_then(|v| v.ok())
2135 .unwrap_or(true)
2136 }
2137
2138 #[must_use]
2141 pub fn maintenance_auto(&self) -> bool {
2142 self.get_bool("maintenance.auto")
2143 .and_then(|v| v.ok())
2144 .unwrap_or(true)
2145 }
2146
2147 pub fn try_pack_index_parallelism(
2153 &self,
2154 ) -> std::result::Result<crate::parallel::Parallelism, ConfigError> {
2155 Ok(crate::parallel::ThreadPolicy::default()
2156 .try_resolve(crate::parallel::ParallelOp::IndexPack, self)?
2157 .parallelism())
2158 }
2159
2160 #[must_use]
2163 pub fn pack_index_parallelism(&self) -> crate::parallel::Parallelism {
2164 self.try_pack_index_parallelism()
2165 .unwrap_or_else(|_| crate::parallel::Parallelism::resolve(None))
2166 }
2167
2168 pub fn try_pack_index_parallelism_for_git_dir(
2174 env: &Environment,
2175 git_dir: Option<&std::path::Path>,
2176 ) -> Result<crate::parallel::Parallelism> {
2177 match git_dir {
2178 Some(d) => {
2179 let cfg = Self::load(env, Some(d), true)?;
2180 Ok(cfg.try_pack_index_parallelism()?)
2181 }
2182 None => Ok(crate::parallel::Parallelism::resolve(None)),
2183 }
2184 }
2185
2186 #[must_use]
2188 pub fn pack_index_parallelism_for_git_dir(
2189 env: &Environment,
2190 git_dir: Option<&std::path::Path>,
2191 ) -> crate::parallel::Parallelism {
2192 Self::try_pack_index_parallelism_for_git_dir(env, git_dir)
2193 .unwrap_or_else(|_| crate::parallel::Parallelism::resolve(None))
2194 }
2195
2196 #[must_use]
2198 pub fn pack_read_reverse_index_default(&self) -> bool {
2199 self.get_bool("pack.readreverseindex")
2200 .or_else(|| self.get_bool("pack.readReverseIndex"))
2201 .and_then(|r| r.ok())
2202 .unwrap_or(true)
2203 }
2204
2205 #[must_use]
2208 pub(crate) fn log_refs_config(&self) -> refs::LogRefsConfig {
2209 self.get("core.logAllRefUpdates")
2210 .and_then(|v| refs::LogRefsConfig::parse(&v))
2211 .unwrap_or(refs::LogRefsConfig::Unset)
2212 }
2213
2214 #[must_use]
2221 pub fn effective_log_refs_config(&self) -> refs::LogRefsConfig {
2222 match self.log_refs_config() {
2223 refs::LogRefsConfig::Unset
2224 if self
2225 .get_bool("core.bare")
2226 .and_then(|r| r.ok())
2227 .unwrap_or(false) =>
2228 {
2229 refs::LogRefsConfig::None
2230 }
2231 refs::LogRefsConfig::Unset => refs::LogRefsConfig::Normal,
2232 mode => mode,
2233 }
2234 }
2235
2236 pub(crate) fn from_entries(entries: Vec<ConfigEntry>) -> Self {
2238 Self { entries }
2239 }
2240
2241 pub fn get_i64(&self, key: &str) -> Option<std::result::Result<i64, String>> {
2243 self.get(key).map(|v| parse_i64(&v))
2244 }
2245
2246 fn parse_zlib_compression_level(raw: &str) -> Result<i32> {
2251 const Z_DEFAULT_COMPRESSION: i32 = 6;
2252 const Z_BEST_COMPRESSION: i32 = 9;
2253
2254 let v = parse_git_config_int_strict(raw.trim()).map_err(|_| {
2255 Error::Config(format!("bad numeric config value '{raw}' for compression").into())
2256 })?;
2257 if v == -1 {
2258 return Ok(Z_DEFAULT_COMPRESSION);
2259 }
2260 if v < 0 || v > i64::from(Z_BEST_COMPRESSION) {
2261 return Err(Error::Config(
2262 format!("bad zlib compression level {v}").into(),
2263 ));
2264 }
2265 Ok(v as i32)
2266 }
2267
2268 pub fn loose_objects_zlib_level(&self) -> Result<i32> {
2279 let mut loose_level = Self::LOOSE_OBJECTS_ZLIB_DEFAULT;
2280 let mut loose_compression_seen = false;
2281
2282 for e in self.entries() {
2283 match e.key.as_str() {
2284 "core.compression" => {
2285 let val = e.value.as_deref().ok_or_else(|| {
2286 Error::Config(
2287 "bad numeric config value '' for compression"
2288 .to_owned()
2289 .into(),
2290 )
2291 })?;
2292 let level = Self::parse_zlib_compression_level(val)?;
2293 if !loose_compression_seen {
2294 loose_level = level;
2295 }
2296 }
2297 "core.loosecompression" => {
2298 let val = e.value.as_deref().ok_or_else(|| {
2299 Error::Config(
2300 "bad numeric config value '' for compression"
2301 .to_owned()
2302 .into(),
2303 )
2304 })?;
2305 let level = Self::parse_zlib_compression_level(val)?;
2306 loose_level = level;
2307 loose_compression_seen = true;
2308 }
2309 _ => {}
2310 }
2311 }
2312
2313 Ok(loose_level)
2314 }
2315
2316 pub const LOOSE_OBJECTS_ZLIB_DEFAULT: i32 = 1;
2319
2320 pub const BIG_FILE_THRESHOLD_DEFAULT: u64 = 512 * 1024 * 1024;
2322
2323 pub fn big_file_threshold(&self) -> Result<u64> {
2334 const KEY: &str = "core.bigfilethreshold";
2335 let Some(entry) = self.get_last_entry(KEY) else {
2336 return Ok(Self::BIG_FILE_THRESHOLD_DEFAULT);
2337 };
2338 let raw = entry.value.as_deref().unwrap_or_default();
2339 let bad = |reason| {
2340 Error::Config(ConfigError::BadNumericValue {
2341 key: KEY.to_owned(),
2342 value: raw.to_owned(),
2343 reason,
2344 })
2345 };
2346 let value = parse_git_config_int_strict(raw.trim()).map_err(|e| {
2347 bad(match e {
2348 GitConfigIntStrictError::InvalidUnit => BadNumericSource::InvalidUnit,
2349 GitConfigIntStrictError::OutOfRange => BadNumericSource::OutOfRange,
2350 })
2351 })?;
2352 u64::try_from(value).map_err(|_| bad(BadNumericSource::OutOfRange))
2353 }
2354
2355 pub fn pack_objects_zlib_level(&self) -> Result<i32> {
2363 const Z_DEFAULT_COMPRESSION: i32 = 6;
2364
2365 let parse_compression =
2366 |raw: &str| -> Result<i32> { Self::parse_zlib_compression_level(raw) };
2367
2368 let mut pack_level = Z_DEFAULT_COMPRESSION;
2370 let mut pack_compression_seen = false;
2371
2372 for e in self.entries() {
2373 match e.key.as_str() {
2374 "core.compression" => {
2375 let Some(val) = e.value.as_deref() else {
2376 continue;
2377 };
2378 let level = parse_compression(val)?;
2379 if !pack_compression_seen {
2380 pack_level = level;
2381 }
2382 }
2383 "pack.compression" => {
2384 let Some(val) = e.value.as_deref() else {
2385 continue;
2386 };
2387 pack_level = parse_compression(val)?;
2388 pack_compression_seen = true;
2389 }
2390 _ => {}
2391 }
2392 }
2393
2394 Ok(pack_level)
2395 }
2396
2397 pub fn get_regexp(&self, pattern: &str) -> std::result::Result<Vec<&ConfigEntry>, String> {
2402 let re = regex::Regex::new(pattern).map_err(|e| format!("invalid key pattern: {e}"))?;
2403 Ok(self
2404 .entries
2405 .iter()
2406 .filter(|e| re.is_match(&e.key))
2407 .collect())
2408 }
2409
2410 pub fn load(env: &Environment, git_dir: Option<&Path>, include_system: bool) -> Result<Self> {
2421 let opts = LoadConfigOptions {
2422 include_system,
2423 include_ctx: IncludeContext {
2424 git_dir: git_dir.map(PathBuf::from),
2425 cwd: env.cwd.clone(),
2426 pwd: env.pwd.clone(),
2427 env: std::sync::Arc::new(env.clone()),
2428 ..Default::default()
2429 },
2430 ..Default::default()
2431 };
2432 Self::load_with_options(env, git_dir, &opts)
2433 }
2434
2435 pub fn load_with_options(
2442 env: &Environment,
2443 git_dir: Option<&Path>,
2444 opts: &LoadConfigOptions,
2445 ) -> Result<Self> {
2446 Self::load_with_options_uncached(env, git_dir, opts, &mut Vec::new())
2447 }
2448
2449 fn read_local_config_text_optional(
2450 path: &Path,
2451 caches: Option<&std::sync::Arc<crate::repo_caches::RepoCaches>>,
2452 ) -> Result<Option<String>> {
2453 let read = |p: &Path| -> Result<Option<String>> {
2454 match fs::read_to_string(p) {
2455 Ok(text) => Ok(Some(text)),
2456 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
2457 Err(e) => Err(Error::Io(e)),
2458 }
2459 };
2460 let Some(c) = caches else {
2461 return read(path);
2462 };
2463 match c.local_config_text(path) {
2464 Ok(text) => Ok(Some(text)),
2465 Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
2466 Err(e) => Err(e),
2467 }
2468 }
2469
2470 pub(crate) fn load_with_options_uncached(
2471 env: &Environment,
2472 git_dir: Option<&Path>,
2473 opts: &LoadConfigOptions,
2474 included_files: &mut Vec<PathBuf>,
2475 ) -> Result<Self> {
2476 let mut set = Self::new();
2477 let proc = opts.process_includes;
2478 let mut ctx = opts.include_ctx.clone();
2479 if ctx.cwd.as_os_str().is_empty() {
2480 ctx.cwd = env.cwd.clone();
2481 }
2482 ctx.pwd = env.pwd.clone();
2483 ctx.env = std::sync::Arc::new(env.clone());
2484
2485 if opts.include_system && !git_config_nosystem_enabled(env) {
2487 let system_path = system_config_path(env);
2488 match ConfigFile::from_path(&system_path, ConfigScope::System) {
2489 Ok(Some(f)) => {
2490 Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?
2491 }
2492 Ok(None) => {}
2493 Err(e) => return Err(e),
2494 }
2495 }
2496
2497 for path in global_config_paths(env) {
2499 match ConfigFile::from_path(&path, ConfigScope::Global) {
2500 Ok(Some(f)) => {
2501 Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?
2502 }
2503 Ok(None) => {}
2504 Err(e) => return Err(e),
2505 }
2506 }
2507
2508 if let Some(gd) = git_dir {
2510 let common_dir = crate::repo::common_git_dir_for_config(gd);
2511 let local_path = common_dir.join("config");
2512 let local_text =
2513 Self::read_local_config_text_optional(&local_path, opts.repo_caches.as_ref())?;
2514 if let Some(msg) =
2515 crate::repo::early_config_ignore_repo_reason_for_text(local_text.as_deref())
2516 {
2517 if let Some(sink) = opts.diagnostics.as_ref() {
2518 sink.warn(crate::diagnostics::Warning::IgnoredGitDir {
2519 path: gd.to_path_buf(),
2520 reason: msg,
2521 });
2522 }
2523 } else if let Some(text) = local_text.as_deref() {
2524 let f = ConfigFile::parse(&local_path, text, ConfigScope::Local)?;
2525 Self::merge_with_includes_collect(&mut set, &f, proc, 0, &ctx, included_files)?;
2526 }
2527
2528 let wt_path = gd.join("config.worktree");
2531 if crate::repo::worktree_config_enabled_for_text(local_text.as_deref()) {
2532 match ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
2533 Ok(Some(f)) => Self::merge_with_includes_collect(
2534 &mut set,
2535 &f,
2536 proc,
2537 0,
2538 &ctx,
2539 included_files,
2540 )?,
2541 Ok(None) => {}
2542 Err(e) => return Err(e),
2543 }
2544 }
2545 }
2546
2547 if let Some(path) = env.git_config.as_deref() {
2549 let path = resolve_config_override_path(env, path);
2550 match ConfigFile::from_path(&path, ConfigScope::Command) {
2551 Ok(Some(f)) => {
2552 if proc {
2553 Self::merge_with_includes_collect(
2554 &mut set,
2555 &f,
2556 proc,
2557 0,
2558 &ctx,
2559 included_files,
2560 )?;
2561 } else {
2562 set.merge(&f);
2563 }
2564 }
2565 Ok(None) => {}
2566 Err(e) => return Err(e),
2567 }
2568 }
2569
2570 add_environment_config_pairs(&mut set, env)?;
2571
2572 if let Some(params) = env.git_config_parameters.as_deref() {
2574 if proc && opts.command_includes && !params.trim().is_empty() {
2575 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
2576 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, params)?;
2577 Self::merge_with_includes_collect(
2578 &mut set,
2579 &cmd_file,
2580 proc,
2581 0,
2582 &ctx,
2583 included_files,
2584 )?;
2585 } else if !params.trim().is_empty() {
2586 for entry in parse_config_parameters(params) {
2587 if let Some((key, val)) =
2588 entry.split_once('\u{1}').or_else(|| entry.split_once('='))
2589 {
2590 let _ = set.add_command_override(key.trim(), val);
2591 } else {
2592 let _ = set.add_command_override(entry.trim(), "true");
2593 }
2594 }
2595 }
2596 }
2597
2598 Ok(set)
2599 }
2600
2601 pub fn read_early_config(
2613 env: &Environment,
2614 git_dir: Option<&Path>,
2615 key: &str,
2616 ) -> Result<Vec<String>> {
2617 let mut set = Self::new();
2618 let ctx = IncludeContext {
2619 git_dir: git_dir.map(PathBuf::from),
2620 command_line_relative_include_is_error: false,
2621 cwd: env.cwd.clone(),
2622 pwd: env.pwd.clone(),
2623 env: std::sync::Arc::new(env.clone()),
2624 };
2625
2626 if !git_config_nosystem_enabled(env) {
2628 let system_path = system_config_path(env);
2629 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2630 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2631 }
2632 }
2633
2634 for path in global_config_paths(env) {
2636 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2637 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2638 }
2639 }
2640
2641 if let Some(gd) = git_dir {
2642 let common_dir = crate::repo::common_git_dir_for_config(gd);
2643 let local_path = common_dir.join("config");
2645 if crate::repo::early_config_ignore_repo_reason(&common_dir).is_some() {
2646 } else if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2648 set.merge_file_with_includes(&f, true, &ctx)?;
2649 }
2650
2651 let wt_path = gd.join("config.worktree");
2653 if crate::repo::worktree_config_enabled(&common_dir) {
2654 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
2655 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2656 }
2657 }
2658 }
2659
2660 if let Some(params) = env.git_config_parameters.as_deref() {
2662 if !params.trim().is_empty() {
2663 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
2664 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, params)?;
2665 Self::merge_with_includes(&mut set, &cmd_file, true, 0, &ctx)?;
2666 }
2667 }
2668
2669 Ok(set.get_all(key))
2670 }
2671
2672 pub fn merge_file_with_includes(
2677 &mut self,
2678 file: &ConfigFile,
2679 process_includes: bool,
2680 ctx: &IncludeContext,
2681 ) -> Result<()> {
2682 Self::merge_with_includes(self, file, process_includes, 0, ctx)
2683 }
2684
2685 pub fn load_repo_local_only(git_dir: &Path) -> Result<Self> {
2691 let mut set = Self::new();
2692 let local_path = git_dir.join("config");
2693 let ctx = IncludeContext {
2694 git_dir: Some(git_dir.to_path_buf()),
2695 command_line_relative_include_is_error: false,
2696 cwd: PathBuf::from("."),
2697 pwd: None,
2698 env: std::sync::Arc::new(Environment::empty()),
2699 };
2700 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2701 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2702 }
2703 Ok(set)
2704 }
2705
2706 pub fn load_protected(env: &Environment, include_system: bool) -> Result<Self> {
2717 let mut set = Self::new();
2718 let ctx = IncludeContext {
2719 git_dir: None,
2720 command_line_relative_include_is_error: false,
2721 cwd: env.cwd.clone(),
2722 pwd: env.pwd.clone(),
2723 env: std::sync::Arc::new(env.clone()),
2724 };
2725
2726 if include_system && !git_config_nosystem_enabled(env) {
2727 let system_path = system_config_path(env);
2728 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2729 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2730 }
2731 }
2732
2733 for path in global_config_paths(env) {
2734 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2735 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2736 }
2737 }
2738
2739 add_environment_config_pairs(&mut set, env)?;
2740
2741 if let Some(params) = env.git_config_parameters.as_deref() {
2742 for entry in parse_config_parameters(params) {
2743 if let Some((key, val)) =
2744 entry.split_once('\u{1}').or_else(|| entry.split_once('='))
2745 {
2746 let _ = set.add_command_override(key.trim(), val);
2747 } else {
2748 let _ = set.add_command_override(entry.trim(), "true");
2749 }
2750 }
2751 }
2752
2753 Ok(set)
2754 }
2755
2756 fn merge_with_includes(
2758 set: &mut Self,
2759 file: &ConfigFile,
2760 process_includes: bool,
2761 depth: usize,
2762 ctx: &IncludeContext,
2763 ) -> Result<()> {
2764 let mut included_files = Vec::new();
2765 Self::merge_with_includes_collect(
2766 set,
2767 file,
2768 process_includes,
2769 depth,
2770 ctx,
2771 &mut included_files,
2772 )
2773 }
2774
2775 fn merge_with_includes_collect(
2779 set: &mut Self,
2780 file: &ConfigFile,
2781 process_includes: bool,
2782 depth: usize,
2783 ctx: &IncludeContext,
2784 included_files: &mut Vec<PathBuf>,
2785 ) -> Result<()> {
2786 const MAX_INCLUDE_DEPTH: usize = 10;
2788 if depth > MAX_INCLUDE_DEPTH {
2789 return Err(ConfigError::IncludeDepthExceeded {
2790 depth,
2791 limit: MAX_INCLUDE_DEPTH,
2792 }
2793 .into());
2794 }
2795 if !process_includes {
2796 set.merge(file);
2797 return Ok(());
2798 }
2799
2800 for entry in &file.entries {
2801 set.entries.push(entry.clone());
2802
2803 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
2804 continue;
2805 };
2806 let included_by_hasconfig = condition.as_deref().is_some_and(is_hasconfig_remote_url);
2807 if condition.is_some() && !included_by_hasconfig {
2808 let cond = condition.as_deref().unwrap_or_default();
2809 if !evaluate_include_condition(cond, set, file, ctx) {
2810 continue;
2811 }
2812 }
2813
2814 let resolved = match resolve_include_file_path(&inc_path, file, ctx) {
2815 Ok(p) => p,
2816 Err(Error::Config(ConfigError::Other(msg))) if msg.is_empty() => continue,
2817 Err(e) => return Err(e),
2818 };
2819 included_files.push(resolved.clone());
2820 let Some(inc_file) = ConfigFile::from_path(&resolved, file.scope)? else {
2824 continue;
2825 };
2826
2827 if included_by_hasconfig {
2828 validate_hasconfig_remote_url_include(&inc_file, process_includes, depth + 1, ctx)?;
2829 let cond = condition.as_deref().unwrap_or_default();
2830 if !evaluate_include_condition(cond, set, file, ctx) {
2831 continue;
2832 }
2833 }
2834
2835 Self::merge_with_includes_collect(
2836 set,
2837 &inc_file,
2838 process_includes,
2839 depth + 1,
2840 ctx,
2841 included_files,
2842 )?;
2843 }
2844
2845 Ok(())
2846 }
2847}
2848
2849fn include_directive_for_entry(entry: &ConfigEntry) -> Option<(String, Option<String>)> {
2850 let val = entry.value.as_ref()?;
2851 if entry.key == "include.path" {
2852 return Some((val.clone(), None));
2853 }
2854 if entry.key.starts_with("includeif.") && entry.key.ends_with(".path") {
2855 let mid = &entry.key["includeif.".len()..entry.key.len() - ".path".len()];
2856 return Some((val.clone(), Some(mid.to_owned())));
2857 }
2858 None
2859}
2860
2861pub(crate) type ConfigFileStamp = (PathBuf, Option<(SystemTime, u64)>);
2888
2889pub(crate) type ConfigCacheEntryStamps = (
2891 Vec<(String, Option<String>)>,
2892 Vec<ConfigFileStamp>,
2893 Vec<ConfigFileStamp>,
2894);
2895
2896#[derive(Clone, PartialEq, Eq, Hash)]
2897pub(crate) struct ConfigCacheKey {
2898 pub(crate) git_dir: Option<PathBuf>,
2899 include_system: bool,
2900 process_includes: bool,
2901 command_includes: bool,
2902 ctx_git_dir: Option<PathBuf>,
2903 ctx_relative_include_is_error: bool,
2904}
2905
2906impl ConfigCacheKey {
2907 pub(crate) fn new(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Self {
2908 Self {
2909 git_dir: git_dir.map(Path::to_path_buf),
2910 include_system: opts.include_system,
2911 process_includes: opts.process_includes,
2912 command_includes: opts.command_includes,
2913 ctx_git_dir: opts.include_ctx.git_dir.clone(),
2914 ctx_relative_include_is_error: opts.include_ctx.command_line_relative_include_is_error,
2915 }
2916 }
2917}
2918
2919pub(crate) struct ConfigCacheEntry {
2920 pub(crate) env_fingerprint: Vec<(String, Option<String>)>,
2921 pub(crate) base_stamps: Vec<ConfigFileStamp>,
2923 pub(crate) extra_stamps: Vec<ConfigFileStamp>,
2926 pub(crate) set: Arc<ConfigSet>,
2927}
2928
2929fn config_cascade_file_paths(
2932 env: &Environment,
2933 git_dir: Option<&Path>,
2934 opts: &LoadConfigOptions,
2935) -> Vec<PathBuf> {
2936 let mut paths = Vec::new();
2937 if opts.include_system && !git_config_nosystem_enabled(env) {
2938 paths.push(system_config_path(env));
2939 }
2940 paths.extend(global_config_paths(env));
2941 if let Some(gd) = git_dir {
2942 let common_dir = crate::repo::common_git_dir_for_config(gd);
2943 paths.push(common_dir.join("config"));
2944 paths.push(gd.join("config.worktree"));
2947 paths.push(gd.join("HEAD"));
2950 }
2951 if let Some(cgd) = &opts.include_ctx.git_dir {
2952 if Some(cgd.as_path()) != git_dir {
2953 paths.push(cgd.join("HEAD"));
2954 }
2955 }
2956 if let Some(p) = env.git_config.as_deref() {
2957 paths.push(resolve_config_override_path(env, p));
2958 }
2959 paths
2960}
2961
2962pub(crate) fn stamp_for_path(path: &Path) -> Option<(SystemTime, u64)> {
2963 fs::metadata(path)
2964 .ok()
2965 .and_then(|m| Some((m.modified().ok()?, m.len())))
2966}
2967
2968pub(crate) fn stamp_paths(paths: Vec<PathBuf>) -> Vec<ConfigFileStamp> {
2969 paths
2970 .into_iter()
2971 .map(|path| {
2972 let stamp = stamp_for_path(&path);
2973 (path, stamp)
2974 })
2975 .collect()
2976}
2977
2978pub(crate) fn config_file_stamps(
2979 env: &Environment,
2980 git_dir: Option<&Path>,
2981 opts: &LoadConfigOptions,
2982) -> Vec<ConfigFileStamp> {
2983 stamp_paths(config_cascade_file_paths(env, git_dir, opts))
2984}
2985
2986pub(crate) fn config_cache_lookup(
2987 cache: &HashMap<ConfigCacheKey, ConfigCacheEntry>,
2988 key: &ConfigCacheKey,
2989 env_fp: &[(String, Option<String>)],
2990 base_stamps: &[ConfigFileStamp],
2991) -> Option<ConfigSet> {
2992 let entry = cache.get(key)?;
2993 if entry.env_fingerprint.as_slice() != env_fp || entry.base_stamps.as_slice() != base_stamps {
2994 return None;
2995 }
2996 for (path, stamp) in &entry.extra_stamps {
2997 if stamp_for_path(path) != *stamp {
2998 return None;
2999 }
3000 }
3001 Some((*entry.set).clone())
3002}
3003
3004pub(crate) fn config_cache_store(
3005 cache: &mut HashMap<ConfigCacheKey, ConfigCacheEntry>,
3006 key: ConfigCacheKey,
3007 env_fingerprint: Vec<(String, Option<String>)>,
3008 base_stamps: Vec<ConfigFileStamp>,
3009 extra_stamps: Vec<ConfigFileStamp>,
3010 set: Arc<ConfigSet>,
3011) {
3012 cache.insert(
3013 key,
3014 ConfigCacheEntry {
3015 env_fingerprint,
3016 base_stamps,
3017 extra_stamps,
3018 set,
3019 },
3020 );
3021}
3022
3023fn git_config_nosystem_enabled(env: &Environment) -> bool {
3024 env.git_config_nosystem
3025 .as_deref()
3026 .map(|value| parse_bool(value).unwrap_or(true))
3027 .unwrap_or(false)
3028}
3029
3030fn add_environment_config_pairs(set: &mut ConfigSet, env: &Environment) -> Result<()> {
3031 let Some(count_str) = env.git_config_count.as_deref() else {
3032 return Ok(());
3033 };
3034 if count_str.is_empty() {
3035 return Ok(());
3036 }
3037
3038 let count = count_str
3039 .parse::<usize>()
3040 .map_err(|_| Error::Config("bogus count in GIT_CONFIG_COUNT".to_owned().into()))?;
3041 if count > i32::MAX as usize {
3042 return Err(Error::Config(
3043 "too many entries in GIT_CONFIG_COUNT".to_owned().into(),
3044 ));
3045 }
3046
3047 for i in 0..count {
3048 let (key, value) = env.git_config_pairs.get(i).ok_or_else(|| {
3049 Error::Config(format!("missing config key GIT_CONFIG_KEY_{i}").into())
3050 })?;
3051 set.add_command_override(key, value)?;
3052 }
3053
3054 Ok(())
3055}
3056
3057pub fn parse_bool(s: &str) -> std::result::Result<bool, String> {
3070 match s.to_lowercase().as_str() {
3071 "true" | "yes" | "on" => Ok(true),
3072 "" => Ok(false),
3073 "false" | "no" | "off" => Ok(false),
3074 _ => {
3075 parse_git_config_maybe_bool(s).ok_or_else(|| format!("bad boolean config value '{s}'"))
3076 }
3077 }
3078}
3079
3080pub fn parse_i64(s: &str) -> std::result::Result<i64, String> {
3082 let s = s.trim();
3083 if s.is_empty() {
3084 return Err("empty integer value".to_owned());
3085 }
3086
3087 let (num_str, multiplier) = match s.as_bytes().last() {
3088 Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
3089 Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
3090 Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
3091 _ => (s, 1_i64),
3092 };
3093
3094 let base: i64 = num_str
3095 .parse()
3096 .map_err(|_| format!("invalid integer: '{s}'"))?;
3097 base.checked_mul(multiplier)
3098 .ok_or_else(|| format!("integer overflow: '{s}'"))
3099}
3100
3101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3103pub enum GitConfigIntStrictError {
3104 InvalidUnit,
3106 OutOfRange,
3108}
3109
3110pub fn parse_git_config_int_strict(raw: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
3114 let s = raw.trim();
3115 if s.is_empty() {
3116 return Err(GitConfigIntStrictError::InvalidUnit);
3117 }
3118
3119 let bytes = s.as_bytes();
3120 let mut idx = 0usize;
3121 if matches!(bytes.first(), Some(b'+') | Some(b'-')) {
3122 idx = 1;
3123 }
3124 if idx >= bytes.len() {
3125 return Err(GitConfigIntStrictError::InvalidUnit);
3126 }
3127 let digit_start = idx;
3128 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
3129 idx += 1;
3130 }
3131 if idx == digit_start {
3132 return Err(GitConfigIntStrictError::InvalidUnit);
3133 }
3134
3135 let num_part =
3136 std::str::from_utf8(&bytes[..idx]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3137 let suffix =
3138 std::str::from_utf8(&bytes[idx..]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3139 let mult: i64 = match suffix {
3140 "" => 1,
3141 "k" | "K" => 1024,
3142 "m" | "M" => 1024 * 1024,
3143 "g" | "G" => 1024_i64
3144 .checked_mul(1024)
3145 .and_then(|x| x.checked_mul(1024))
3146 .ok_or(GitConfigIntStrictError::OutOfRange)?,
3147 _ => return Err(GitConfigIntStrictError::InvalidUnit),
3148 };
3149
3150 let val: i64 = num_part
3151 .parse()
3152 .map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3153 val.checked_mul(mult)
3154 .ok_or(GitConfigIntStrictError::OutOfRange)
3155}
3156
3157fn is_c_locale_space(b: u8) -> bool {
3158 matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
3159}
3160
3161fn skip_c_locale_leading_whitespace(s: &str) -> &str {
3162 let bytes = s.as_bytes();
3163 let mut i = 0usize;
3164 while i < bytes.len() && is_c_locale_space(bytes[i]) {
3165 i += 1;
3166 }
3167 &s[i..]
3168}
3169
3170fn git_unit_factor(suffix: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
3171 match suffix {
3172 "" => Ok(1),
3173 "k" | "K" => Ok(1024),
3174 "m" | "M" => Ok(1024 * 1024),
3175 "g" | "G" => Ok(1024_i64 * 1024 * 1024),
3176 _ => Err(GitConfigIntStrictError::InvalidUnit),
3177 }
3178}
3179
3180fn parse_base0_signed_prefix(s: &str) -> std::result::Result<(i64, &str), GitConfigIntStrictError> {
3182 let bytes = s.as_bytes();
3183 if bytes.is_empty() {
3184 return Err(GitConfigIntStrictError::InvalidUnit);
3185 }
3186 let mut i = 0usize;
3187 let mut sign: i64 = 1;
3188 if bytes[i] == b'+' {
3189 i += 1;
3190 } else if bytes[i] == b'-' {
3191 sign = -1;
3192 i += 1;
3193 }
3194 if i >= bytes.len() {
3195 return Err(GitConfigIntStrictError::InvalidUnit);
3196 }
3197
3198 let base = if bytes[i] == b'0' {
3199 if i + 1 < bytes.len() && matches!(bytes[i + 1], b'x' | b'X') {
3200 i += 2;
3201 if i >= bytes.len() {
3202 return Err(GitConfigIntStrictError::InvalidUnit);
3203 }
3204 16
3205 } else {
3206 8
3207 }
3208 } else {
3209 10
3210 };
3211
3212 let digit_start = i;
3213 let mut acc: i128 = 0;
3214 while i < bytes.len() {
3215 let ch = char::from(bytes[i]);
3216 let digit = match base {
3217 10 => ch.to_digit(10),
3218 8 => ch.to_digit(8),
3219 16 => ch.to_digit(16),
3220 _ => None,
3221 };
3222 let Some(d) = digit else {
3223 break;
3224 };
3225 acc = acc
3226 .checked_mul(base as i128)
3227 .and_then(|v| v.checked_add(i128::from(d)))
3228 .ok_or(GitConfigIntStrictError::OutOfRange)?;
3229 i += 1;
3230 }
3231 if i == digit_start {
3232 return Err(GitConfigIntStrictError::InvalidUnit);
3233 }
3234
3235 let signed = if sign < 0 { -acc } else { acc };
3236 let val = i64::try_from(signed).map_err(|_| GitConfigIntStrictError::OutOfRange)?;
3237 Ok((val, &s[i..]))
3238}
3239
3240pub fn parse_git_signed_config(
3242 raw: &str,
3243 max: i64,
3244) -> std::result::Result<i64, GitConfigIntStrictError> {
3245 if max < 0 {
3246 return Err(GitConfigIntStrictError::InvalidUnit);
3247 }
3248 let s = skip_c_locale_leading_whitespace(raw);
3249 if s.is_empty() {
3250 return Err(GitConfigIntStrictError::InvalidUnit);
3251 }
3252
3253 let (val, rest) = parse_base0_signed_prefix(s)?;
3254 let factor = git_unit_factor(rest)?;
3255
3256 if val < 0 {
3257 let min = -max - 1;
3258 if min / factor > val {
3259 return Err(GitConfigIntStrictError::OutOfRange);
3260 }
3261 } else if val > 0 && max / factor < val {
3262 return Err(GitConfigIntStrictError::OutOfRange);
3263 }
3264
3265 val.checked_mul(factor)
3266 .ok_or(GitConfigIntStrictError::OutOfRange)
3267}
3268
3269pub fn parse_git_config_int(raw: &str) -> std::result::Result<i32, GitConfigIntStrictError> {
3271 let max = i32::MAX as i64;
3272 let n = parse_git_signed_config(raw, max)?;
3273 i32::try_from(n).map_err(|_| GitConfigIntStrictError::OutOfRange)
3274}
3275
3276pub fn parse_git_config_maybe_bool(raw: &str) -> Option<bool> {
3278 if raw.is_empty() {
3279 return Some(false);
3280 }
3281 let lower = raw.to_ascii_lowercase();
3282 match lower.as_str() {
3283 "true" | "yes" | "on" => Some(true),
3284 "false" | "no" | "off" => Some(false),
3285 _ => parse_git_config_int(raw).ok().map(|n| n != 0),
3286 }
3287}
3288
3289const DIFF_CONTEXT_KEY: &str = "diff.context";
3290
3291const DEFAULT_GC_AUTO_THRESHOLD: usize = 6700;
3293const DEFAULT_GC_AUTO_PACK_LIMIT: usize = 50;
3295
3296fn bad_numeric_diff_context(
3297 value: &str,
3298 err: GitConfigIntStrictError,
3299 entry: &ConfigEntry,
3300) -> ConfigError {
3301 let reason = match err {
3302 GitConfigIntStrictError::InvalidUnit => crate::error::BadNumericSource::InvalidUnit,
3303 GitConfigIntStrictError::OutOfRange => crate::error::BadNumericSource::OutOfRange,
3304 };
3305 if entry.scope == ConfigScope::Command || entry.file.is_none() {
3306 ConfigError::BadNumericValue {
3307 key: DIFF_CONTEXT_KEY.to_owned(),
3308 value: value.to_owned(),
3309 reason,
3310 }
3311 } else {
3312 let path = entry
3313 .file
3314 .as_deref()
3315 .map(config_error_path_display)
3316 .unwrap_or_default();
3317 ConfigError::BadNumericValueInFile {
3318 key: DIFF_CONTEXT_KEY.to_owned(),
3319 value: value.to_owned(),
3320 file: path,
3321 reason,
3322 }
3323 }
3324}
3325
3326fn bad_diff_context_variable(entry: &ConfigEntry) -> ConfigError {
3327 if entry.scope == ConfigScope::Command || entry.file.is_none() {
3328 ConfigError::UnableToParseFromCommandLine {
3329 key: DIFF_CONTEXT_KEY.to_owned(),
3330 }
3331 } else {
3332 let path = entry
3333 .file
3334 .as_deref()
3335 .map(config_error_path_display)
3336 .unwrap_or_default();
3337 ConfigError::BadConfigVariableAtLine {
3338 key: DIFF_CONTEXT_KEY.to_owned(),
3339 file: path,
3340 line: entry.line,
3341 }
3342 }
3343}
3344
3345pub fn resolve_diff_context_lines(cfg: &ConfigSet) -> Result<Option<usize>> {
3350 let Some(entry) = cfg.get_last_entry(DIFF_CONTEXT_KEY) else {
3351 return Ok(None);
3352 };
3353 let value_src = entry.value.as_deref().unwrap_or("").trim();
3354 match parse_git_config_int_strict(value_src) {
3355 Ok(n) if n < 0 => Err(bad_diff_context_variable(&entry).into()),
3356 Ok(n) => Ok(Some(usize::try_from(n).map_err(|_| {
3357 bad_numeric_diff_context(value_src, GitConfigIntStrictError::OutOfRange, &entry)
3358 })?)),
3359 Err(e) => Err(bad_numeric_diff_context(value_src, e, &entry).into()),
3360 }
3361}
3362
3363pub fn parse_color(s: &str) -> std::result::Result<String, String> {
3370 const COLOR_BACKGROUND_OFFSET: i32 = 10;
3371 const COLOR_FOREGROUND_ANSI: i32 = 30;
3372 const COLOR_FOREGROUND_RGB: i32 = 38;
3373 const COLOR_FOREGROUND_256: i32 = 38;
3374 const COLOR_FOREGROUND_BRIGHT_ANSI: i32 = 90;
3375
3376 #[derive(Clone, Copy, Default)]
3377 struct Color {
3378 kind: u8,
3379 value: u8,
3380 red: u8,
3381 green: u8,
3382 blue: u8,
3383 }
3384
3385 const COLOR_UNSPECIFIED: u8 = 0;
3386 const COLOR_NORMAL: u8 = 1;
3387 const COLOR_ANSI: u8 = 2;
3388 const COLOR_256: u8 = 3;
3389 const COLOR_RGB: u8 = 4;
3390
3391 fn color_empty(c: &Color) -> bool {
3392 c.kind == COLOR_UNSPECIFIED || c.kind == COLOR_NORMAL
3393 }
3394
3395 fn parse_ansi_color(name: &str) -> Option<Color> {
3396 let color_names = [
3397 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
3398 ];
3399 let color_offset = COLOR_FOREGROUND_ANSI;
3400
3401 if name.eq_ignore_ascii_case("default") {
3402 return Some(Color {
3403 kind: COLOR_ANSI,
3404 value: (9 + color_offset) as u8,
3405 ..Default::default()
3406 });
3407 }
3408
3409 let (name, color_offset) = if name.len() >= 6 && name[..6].eq_ignore_ascii_case("bright") {
3410 (&name[6..], COLOR_FOREGROUND_BRIGHT_ANSI)
3411 } else {
3412 (name, COLOR_FOREGROUND_ANSI)
3413 };
3414
3415 for (i, cn) in color_names.iter().enumerate() {
3416 if name.eq_ignore_ascii_case(cn) {
3417 return Some(Color {
3418 kind: COLOR_ANSI,
3419 value: (i as i32 + color_offset) as u8,
3420 ..Default::default()
3421 });
3422 }
3423 }
3424 None
3425 }
3426
3427 fn hex_val(b: u8) -> Option<u8> {
3428 match b {
3429 b'0'..=b'9' => Some(b - b'0'),
3430 b'a'..=b'f' => Some(b - b'a' + 10),
3431 b'A'..=b'F' => Some(b - b'A' + 10),
3432 _ => None,
3433 }
3434 }
3435
3436 fn get_hex_color(chars: &[u8], width: usize) -> Option<(u8, usize)> {
3437 assert!(width == 1 || width == 2);
3438 if chars.len() < width {
3439 return None;
3440 }
3441 let v = if width == 2 {
3442 let hi = hex_val(chars[0])?;
3443 let lo = hex_val(chars[1])?;
3444 (hi << 4) | lo
3445 } else {
3446 let n = hex_val(chars[0])?;
3447 (n << 4) | n
3448 };
3449 Some((v, width))
3450 }
3451
3452 fn parse_single_color(word: &str) -> Option<Color> {
3453 if word.eq_ignore_ascii_case("normal") {
3454 return Some(Color {
3455 kind: COLOR_NORMAL,
3456 ..Default::default()
3457 });
3458 }
3459
3460 let bytes = word.as_bytes();
3461 if (bytes.len() == 7 || bytes.len() == 4) && bytes.first() == Some(&b'#') {
3462 let width = if bytes.len() == 7 { 2 } else { 1 };
3463 let mut idx = 1;
3464 let (r, n1) = get_hex_color(&bytes[idx..], width)?;
3465 idx += n1;
3466 let (g, n2) = get_hex_color(&bytes[idx..], width)?;
3467 idx += n2;
3468 let (b, n3) = get_hex_color(&bytes[idx..], width)?;
3469 idx += n3;
3470 if idx != bytes.len() {
3471 return None;
3472 }
3473 return Some(Color {
3474 kind: COLOR_RGB,
3475 red: r,
3476 green: g,
3477 blue: b,
3478 ..Default::default()
3479 });
3480 }
3481
3482 if let Some(c) = parse_ansi_color(word) {
3483 return Some(c);
3484 }
3485
3486 let Ok(val) = word.parse::<i64>() else {
3487 return None;
3488 };
3489 if val < -1 {
3490 return None;
3491 }
3492 if val < 0 {
3493 return Some(Color {
3494 kind: COLOR_NORMAL,
3495 ..Default::default()
3496 });
3497 }
3498 if val < 8 {
3499 return Some(Color {
3500 kind: COLOR_ANSI,
3501 value: (val as i32 + COLOR_FOREGROUND_ANSI) as u8,
3502 ..Default::default()
3503 });
3504 }
3505 if val < 16 {
3506 return Some(Color {
3507 kind: COLOR_ANSI,
3508 value: (val as i32 - 8 + COLOR_FOREGROUND_BRIGHT_ANSI) as u8,
3509 ..Default::default()
3510 });
3511 }
3512 if val < 256 {
3513 return Some(Color {
3514 kind: COLOR_256,
3515 value: val as u8,
3516 ..Default::default()
3517 });
3518 }
3519 None
3520 }
3521
3522 fn parse_attr(word: &str) -> Option<u8> {
3523 const ATTRS: [(&str, u8, u8); 8] = [
3524 ("bold", 1, 22),
3525 ("dim", 2, 22),
3526 ("italic", 3, 23),
3527 ("ul", 4, 24),
3528 ("underline", 4, 24),
3529 ("blink", 5, 25),
3530 ("reverse", 7, 27),
3531 ("strike", 9, 29),
3532 ];
3533
3534 let mut negate = false;
3535 let mut rest = word;
3536 if let Some(stripped) = rest.strip_prefix("no") {
3537 negate = true;
3538 rest = stripped;
3539 if let Some(s) = rest.strip_prefix('-') {
3540 rest = s;
3541 }
3542 }
3543
3544 for (name, val, neg) in ATTRS {
3545 if rest == name {
3546 return Some(if negate { neg } else { val });
3547 }
3548 }
3549 None
3550 }
3551
3552 fn append_color_output(out: &mut String, c: &Color, background: bool) {
3553 let offset = if background {
3554 COLOR_BACKGROUND_OFFSET
3555 } else {
3556 0
3557 };
3558 match c.kind {
3559 COLOR_UNSPECIFIED | COLOR_NORMAL => {}
3560 COLOR_ANSI => {
3561 use std::fmt::Write;
3562 let _ = write!(out, "{}", i32::from(c.value) + offset);
3563 }
3564 COLOR_256 => {
3565 use std::fmt::Write;
3566 let _ = write!(out, "{};5;{}", COLOR_FOREGROUND_256 + offset, c.value);
3567 }
3568 COLOR_RGB => {
3569 use std::fmt::Write;
3570 let _ = write!(
3571 out,
3572 "{};2;{};{};{}",
3573 COLOR_FOREGROUND_RGB + offset,
3574 c.red,
3575 c.green,
3576 c.blue
3577 );
3578 }
3579 _ => {}
3580 }
3581 }
3582
3583 let s = s.trim();
3584 if s.is_empty() {
3585 return Ok(String::new());
3586 }
3587
3588 let mut has_reset = false;
3589 let mut attr: u64 = 0;
3590 let mut fg = Color::default();
3591 let mut bg = Color::default();
3592 fg.kind = COLOR_UNSPECIFIED;
3593 bg.kind = COLOR_UNSPECIFIED;
3594
3595 for word in s.split_whitespace() {
3596 if word.eq_ignore_ascii_case("reset") {
3597 has_reset = true;
3598 continue;
3599 }
3600
3601 if let Some(c) = parse_single_color(word) {
3602 if fg.kind == COLOR_UNSPECIFIED {
3603 fg = c;
3604 continue;
3605 }
3606 if bg.kind == COLOR_UNSPECIFIED {
3607 bg = c;
3608 continue;
3609 }
3610 return Err(format!("bad color value '{s}'"));
3611 }
3612
3613 if let Some(code) = parse_attr(word) {
3614 attr |= 1u64 << u64::from(code);
3615 continue;
3616 }
3617
3618 return Err(format!("bad color value '{s}'"));
3619 }
3620
3621 if !has_reset && attr == 0 && color_empty(&fg) && color_empty(&bg) {
3622 return Err(format!("bad color value '{s}'"));
3623 }
3624
3625 let mut out = String::from("\x1b[");
3626 let mut sep = if has_reset { 1u32 } else { 0u32 };
3627
3628 let mut attr_bits = attr;
3629 let mut i = 0u32;
3630 while attr_bits != 0 {
3631 let bit = 1u64 << i;
3632 if attr_bits & bit == 0 {
3633 i += 1;
3634 continue;
3635 }
3636 attr_bits &= !bit;
3637 if sep > 0 {
3638 out.push(';');
3639 }
3640 sep += 1;
3641 use std::fmt::Write;
3642 let _ = write!(out, "{i}");
3643 i += 1;
3644 }
3645
3646 if !color_empty(&fg) {
3647 if sep > 0 {
3648 out.push(';');
3649 }
3650 sep += 1;
3651 append_color_output(&mut out, &fg, false);
3652 }
3653 if !color_empty(&bg) {
3654 if sep > 0 {
3655 out.push(';');
3656 }
3657 append_color_output(&mut out, &bg, true);
3658 }
3659 out.push('m');
3660 Ok(out)
3661}
3662
3663#[derive(Debug, Clone)]
3664struct UrlParts {
3665 scheme: String,
3666 user: Option<String>,
3667 host: String,
3668 port: Option<String>,
3669 path: String,
3670}
3671
3672#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3673struct UrlMatchScore {
3674 host_len: usize,
3675 path_len: usize,
3676 user_matched: bool,
3677}
3678
3679fn parse_config_url(url: &str) -> Option<UrlParts> {
3680 let (scheme, rest) = url.split_once("://")?;
3681 let (authority, path) = match rest.find('/') {
3682 Some(idx) => (&rest[..idx], &rest[idx..]),
3683 None => (rest, "/"),
3684 };
3685 let (user, host_port) = match authority.rsplit_once('@') {
3686 Some((user, host)) => (Some(user.to_owned()), host),
3687 None => (None, authority),
3688 };
3689 let (host, port) = match host_port.rsplit_once(':') {
3690 Some((host, port)) if !host.contains(']') => (host, Some(port.to_owned())),
3691 _ => (host_port, None),
3692 };
3693 Some(UrlParts {
3694 scheme: scheme.to_lowercase(),
3695 user,
3696 host: host.to_lowercase(),
3697 port,
3698 path: if path.is_empty() {
3699 "/".to_owned()
3700 } else {
3701 path.trim_end_matches('/').to_owned()
3702 },
3703 })
3704}
3705
3706fn host_matches(pattern: &str, target: &str) -> bool {
3707 let pattern_parts: Vec<&str> = pattern.split('.').collect();
3708 let target_parts: Vec<&str> = target.split('.').collect();
3709 pattern_parts.len() == target_parts.len()
3710 && pattern_parts
3711 .iter()
3712 .zip(target_parts)
3713 .all(|(pattern, target)| *pattern == "*" || *pattern == target)
3714}
3715
3716fn path_match_len(pattern: &str, target: &str) -> Option<usize> {
3717 let pattern = if pattern.is_empty() { "/" } else { pattern };
3718 let target = if target.is_empty() { "/" } else { target };
3719 if pattern == "/" {
3720 return Some(1);
3721 }
3722 let pattern = pattern.trim_end_matches('/');
3723 if target == pattern
3724 || target
3725 .strip_prefix(pattern)
3726 .is_some_and(|rest| rest.starts_with('/'))
3727 {
3728 Some(pattern.len() + 1)
3729 } else {
3730 None
3731 }
3732}
3733
3734fn url_match_score(pattern_url: &str, target_url: &str) -> Option<UrlMatchScore> {
3735 let pattern = parse_config_url(pattern_url)?;
3736 let target = parse_config_url(target_url)?;
3737 if pattern.scheme != target.scheme {
3738 return None;
3739 }
3740 let user_matched = match pattern.user.as_deref() {
3741 Some(user) if target.user.as_deref() == Some(user) => true,
3742 Some(_) => return None,
3743 None => false,
3744 };
3745 if !host_matches(&pattern.host, &target.host) || pattern.port != target.port {
3746 return None;
3747 }
3748 let path_len = path_match_len(&pattern.path, &target.path)?;
3749 Some(UrlMatchScore {
3750 host_len: pattern.host.len(),
3751 path_len,
3752 user_matched,
3753 })
3754}
3755
3756pub fn url_matches(pattern_url: &str, target_url: &str) -> bool {
3758 url_match_score(pattern_url, target_url).is_some()
3759}
3760
3761pub fn get_urlmatch_entries<'a>(
3763 entries: &'a [ConfigEntry],
3764 section: &str,
3765 variable: &str,
3766 url: &str,
3767) -> Vec<&'a ConfigEntry> {
3768 let section_lower = section.to_lowercase();
3769 let variable_lower = variable.to_lowercase();
3770 let mut matches: Vec<(UrlMatchScore, &'a ConfigEntry)> = Vec::new();
3771
3772 for entry in entries {
3773 let key = &entry.key;
3774 let first_dot = match key.find('.') {
3775 Some(i) => i,
3776 None => continue,
3777 };
3778 let last_dot = match key.rfind('.') {
3779 Some(i) => i,
3780 None => continue,
3781 };
3782 let entry_section = &key[..first_dot];
3783 let entry_variable = &key[last_dot + 1..];
3784 if entry_section.to_lowercase() != section_lower
3785 || entry_variable.to_lowercase() != variable_lower
3786 {
3787 continue;
3788 }
3789 if first_dot == last_dot {
3790 matches.push((
3791 UrlMatchScore {
3792 host_len: 0,
3793 path_len: 0,
3794 user_matched: false,
3795 },
3796 entry,
3797 ));
3798 } else {
3799 let subsection = &key[first_dot + 1..last_dot];
3800 if let Some(score) = url_match_score(subsection, url) {
3801 matches.push((score, entry));
3802 }
3803 }
3804 }
3805 matches.sort_by_key(|a| a.0);
3806 matches.into_iter().map(|(_, e)| e).collect()
3807}
3808
3809pub fn get_urlmatch_all_in_section(
3811 entries: &[ConfigEntry],
3812 section: &str,
3813 url: &str,
3814) -> Vec<(String, String, ConfigScope)> {
3815 let section_lower = section.to_lowercase();
3816 let mut matches: Vec<(String, UrlMatchScore, String, String, ConfigScope)> = Vec::new();
3817
3818 for entry in entries {
3819 let key = &entry.key;
3820 let first_dot = match key.find('.') {
3821 Some(i) => i,
3822 None => continue,
3823 };
3824 let last_dot = match key.rfind('.') {
3825 Some(i) => i,
3826 None => continue,
3827 };
3828 let entry_section = &key[..first_dot];
3829 if entry_section.to_lowercase() != section_lower {
3830 continue;
3831 }
3832 let entry_variable = &key[last_dot + 1..];
3833 let val = entry.value.as_deref().unwrap_or("");
3834 if first_dot == last_dot {
3835 let canonical = format!("{}.{}", section_lower, entry_variable);
3836 matches.push((
3837 entry_variable.to_lowercase(),
3838 UrlMatchScore {
3839 host_len: 0,
3840 path_len: 0,
3841 user_matched: false,
3842 },
3843 val.to_owned(),
3844 canonical,
3845 entry.scope,
3846 ));
3847 } else {
3848 let subsection = &key[first_dot + 1..last_dot];
3849 if let Some(score) = url_match_score(subsection, url) {
3850 let canonical = format!("{}.{}", section_lower, entry_variable);
3851 matches.push((
3852 entry_variable.to_lowercase(),
3853 score,
3854 val.to_owned(),
3855 canonical,
3856 entry.scope,
3857 ));
3858 }
3859 }
3860 }
3861
3862 let mut best: std::collections::BTreeMap<String, (UrlMatchScore, String, String, ConfigScope)> =
3863 std::collections::BTreeMap::new();
3864 for (var, specificity, val, canonical, scope) in matches {
3865 let entry = best.entry(var).or_insert((
3866 UrlMatchScore {
3867 host_len: 0,
3868 path_len: 0,
3869 user_matched: false,
3870 },
3871 String::new(),
3872 String::new(),
3873 scope,
3874 ));
3875 if specificity >= entry.0 {
3876 *entry = (specificity, val, canonical, scope);
3877 }
3878 }
3879 best.into_values()
3880 .map(|(_, val, canonical, scope)| (canonical, val, scope))
3881 .collect()
3882}
3883
3884pub fn parse_path(env: &Environment, s: &str) -> String {
3888 if let Some(rest) = s.strip_prefix("~/") {
3889 if let Some(home) = home_dir(env) {
3890 return home.join(rest).to_string_lossy().to_string();
3891 }
3892 }
3893 s.to_owned()
3894}
3895
3896pub fn parse_path_optional(env: &Environment, s: &str) -> Option<String> {
3901 if let Some(rest) = s.strip_prefix(":(optional)") {
3902 let resolved = parse_path(env, rest);
3903 if std::path::Path::new(&resolved).exists() {
3904 Some(resolved)
3905 } else {
3906 None }
3908 } else {
3909 Some(parse_path(env, s))
3910 }
3911}
3912
3913#[must_use]
3929pub fn git_config_parameters_last_value(raw: &str, key: &str) -> Option<String> {
3930 let Ok(canon) = canonical_key(key) else {
3931 return None;
3932 };
3933 let mut last: Option<String> = None;
3934 for entry in parse_config_parameters_strict(raw).ok()? {
3935 match entry {
3936 ConfigParameter::Pair { key, value } => {
3937 if canonical_key(key.trim()).ok().as_ref() == Some(&canon) {
3938 last = Some(value.unwrap_or_else(|| "true".to_owned()));
3939 }
3940 }
3941 ConfigParameter::OldStyle(entry) => {
3942 if let Some((k, v)) = entry.split_once('=') {
3943 if canonical_key(k.trim()).ok().as_ref() == Some(&canon) {
3944 last = Some(v.to_owned());
3945 }
3946 } else if canonical_key(entry.trim()).ok().as_ref() == Some(&canon) {
3947 last = Some("true".to_owned());
3948 }
3949 }
3950 }
3951 }
3952 last
3953}
3954
3955#[derive(Debug, Clone, PartialEq, Eq)]
3956enum ConfigParameter {
3957 OldStyle(String),
3958 Pair { key: String, value: Option<String> },
3959}
3960
3961pub fn parse_config_parameters(raw: &str) -> Vec<String> {
3965 parse_config_parameters_strict(raw)
3966 .map(|entries| {
3967 entries
3968 .into_iter()
3969 .map(|entry| match entry {
3970 ConfigParameter::OldStyle(entry) => entry,
3971 ConfigParameter::Pair {
3972 key,
3973 value: Some(value),
3974 } => format!("{key}\u{1}{value}"),
3975 ConfigParameter::Pair { key, value: None } => format!("{key}\u{1}"),
3976 })
3977 .collect()
3978 })
3979 .unwrap_or_default()
3980}
3981
3982fn parse_config_parameters_strict(raw: &str) -> Result<Vec<ConfigParameter>> {
3983 let mut out: Vec<ConfigParameter> = Vec::new();
3984 let chars: Vec<char> = raw.chars().collect();
3985 let mut idx = skip_config_parameter_spaces(&chars, 0);
3986
3987 while idx < chars.len() {
3988 let (key, next) = sq_dequote_step_chars(&chars, idx)?;
3989 let Some(next_idx) = next else {
3990 out.push(ConfigParameter::OldStyle(key));
3991 break;
3992 };
3993
3994 if chars[next_idx].is_whitespace() {
3995 out.push(ConfigParameter::OldStyle(key));
3996 idx = skip_config_parameter_spaces(&chars, next_idx);
3997 continue;
3998 }
3999
4000 if chars[next_idx] != '=' {
4001 return Err(Error::Config(
4002 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4003 ));
4004 }
4005
4006 let value_start = next_idx + 1;
4007 if value_start >= chars.len() || chars[value_start].is_whitespace() {
4008 out.push(ConfigParameter::Pair { key, value: None });
4009 idx = skip_config_parameter_spaces(&chars, value_start);
4010 continue;
4011 }
4012
4013 if chars[value_start] != '\'' {
4014 return Err(Error::Config(
4015 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4016 ));
4017 }
4018 let (value, value_next) = sq_dequote_step_chars(&chars, value_start)?;
4019 if let Some(value_next) = value_next {
4020 if !chars[value_next].is_whitespace() {
4021 return Err(Error::Config(
4022 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4023 ));
4024 }
4025 idx = skip_config_parameter_spaces(&chars, value_next);
4026 } else {
4027 idx = chars.len();
4028 }
4029 out.push(ConfigParameter::Pair {
4030 key,
4031 value: Some(value),
4032 });
4033 }
4034
4035 Ok(out)
4036}
4037
4038fn skip_config_parameter_spaces(chars: &[char], mut idx: usize) -> usize {
4039 while idx < chars.len() && chars[idx].is_whitespace() {
4040 idx += 1;
4041 }
4042 idx
4043}
4044
4045fn sq_dequote_step_chars(chars: &[char], start: usize) -> Result<(String, Option<usize>)> {
4046 if chars.get(start) != Some(&'\'') {
4047 return Err(Error::Config(
4048 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4049 ));
4050 }
4051
4052 let mut out = String::new();
4053 let mut idx = start + 1;
4054 loop {
4055 let Some(&ch) = chars.get(idx) else {
4056 return Err(Error::Config(
4057 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4058 ));
4059 };
4060 if ch != '\'' {
4061 out.push(ch);
4062 idx += 1;
4063 continue;
4064 }
4065
4066 idx += 1;
4067 match chars.get(idx).copied() {
4068 None => return Ok((out, None)),
4069 Some('\\')
4070 if chars
4071 .get(idx + 1)
4072 .copied()
4073 .is_some_and(needs_sq_backslash_quote)
4074 && chars.get(idx + 2) == Some(&'\'') =>
4075 {
4076 if let Some(escaped) = chars.get(idx + 1) {
4077 out.push(*escaped);
4078 }
4079 idx += 3;
4080 }
4081 _ => return Ok((out, Some(idx))),
4082 }
4083 }
4084}
4085
4086fn needs_sq_backslash_quote(ch: char) -> bool {
4087 ch == '\'' || ch == '!'
4088}
4089
4090pub(crate) fn resolve_config_override_path(env: &Environment, path: &str) -> PathBuf {
4092 let p = Path::new(path);
4093 if p.is_absolute() {
4094 p.to_path_buf()
4095 } else {
4096 env.cwd.join(p)
4097 }
4098}
4099
4100pub fn global_config_paths_pub(env: &Environment) -> Vec<PathBuf> {
4103 global_config_paths(env)
4104}
4105
4106fn global_config_paths(env: &Environment) -> Vec<PathBuf> {
4107 let mut paths = Vec::new();
4108
4109 if let Some(p) = env.git_config_global.as_deref() {
4111 paths.push(resolve_config_override_path(env, p));
4112 return paths;
4113 }
4114
4115 if let Some(xdg) = env.xdg_config_home.as_deref() {
4117 paths.push(PathBuf::from(xdg).join("git/config"));
4118 } else if let Some(home) = home_dir(env) {
4119 paths.push(home.join(".config/git/config"));
4120 }
4121 if let Some(home) = home_dir(env) {
4122 paths.push(home.join(".gitconfig"));
4123 }
4124
4125 paths
4126}
4127
4128#[must_use]
4133pub fn system_config_path(env: &Environment) -> PathBuf {
4134 if let Some(p) = env.git_config_system.as_deref() {
4135 return resolve_config_override_path(env, p);
4136 }
4137 let unix_default = PathBuf::from("/etc/gitconfig");
4138 #[cfg(windows)]
4139 {
4140 if unix_default.is_file() {
4141 return unix_default;
4142 }
4143 if let Some(root) = env.git_install_root.as_ref() {
4144 let candidate = PathBuf::from(root).join("etc").join("gitconfig");
4145 if candidate.is_file() {
4146 return candidate;
4147 }
4148 }
4149 for pf in [
4150 env.program_files.as_deref(),
4151 env.program_files_x86.as_deref(),
4152 ]
4153 .into_iter()
4154 .flatten()
4155 {
4156 let candidate = PathBuf::from(pf).join("Git").join("etc").join("gitconfig");
4157 if candidate.is_file() {
4158 return candidate;
4159 }
4160 }
4161 }
4162 unix_default
4163}
4164
4165fn home_dir(env: &Environment) -> Option<PathBuf> {
4172 if let Some(home) = env.home.as_ref().filter(|h| !h.is_empty()) {
4173 return Some(PathBuf::from(home));
4174 }
4175 #[cfg(windows)]
4176 {
4177 if let (Some(drive), Some(path)) = (
4178 env.homedrive.as_ref().filter(|d| !d.is_empty()),
4179 env.homepath.as_ref().filter(|p| !p.is_empty()),
4180 ) {
4181 let mut combined = drive.clone();
4182 combined.push(path);
4183 return Some(PathBuf::from(combined));
4184 }
4185 if let Some(profile) = env.userprofile.as_ref().filter(|p| !p.is_empty()) {
4186 return Some(PathBuf::from(profile));
4187 }
4188 }
4189 None
4190}
4191
4192fn include_source_is_disk_file(file: &ConfigFile) -> bool {
4194 file.include_origin == ConfigIncludeOrigin::Disk
4195}
4196
4197fn resolve_include_file_path(
4201 path: &str,
4202 file: &ConfigFile,
4203 ctx: &IncludeContext,
4204) -> Result<PathBuf> {
4205 let expanded = parse_path(ctx.env.as_ref(), path);
4206 let p = Path::new(&expanded);
4207 if p.is_absolute() {
4208 return Ok(p.to_path_buf());
4209 }
4210 if !include_source_is_disk_file(file) {
4211 if file.include_origin == ConfigIncludeOrigin::CommandLine {
4212 if ctx.command_line_relative_include_is_error {
4213 return Err(Error::Config(
4214 "relative config includes must come from files"
4215 .to_owned()
4216 .into(),
4217 ));
4218 }
4219 return Err(Error::Config(String::new().into()));
4220 }
4221 return Err(Error::Config(
4222 "relative config includes must come from files"
4223 .to_owned()
4224 .into(),
4225 ));
4226 }
4227 let base = match file.path.parent() {
4228 Some(p) if !p.as_os_str().is_empty() => p,
4229 Some(_) | None => Path::new("."),
4230 };
4231 Ok(base.join(p))
4232}
4233
4234fn is_dir_sep(b: u8) -> bool {
4235 b == b'/' || b == b'\\'
4236}
4237
4238fn add_trailing_starstar_for_dir(pat: &mut String) {
4239 let bytes = pat.as_bytes();
4240 if bytes.last().is_some_and(|&b| is_dir_sep(b)) {
4241 pat.push_str("**");
4242 }
4243}
4244
4245fn prepare_gitdir_pattern(
4247 condition: &str,
4248 file: &ConfigFile,
4249 ctx: &IncludeContext,
4250) -> Result<(String, usize)> {
4251 let mut pat = parse_path(ctx.env.as_ref(), condition);
4253 if pat.starts_with("./") || pat.starts_with(".\\") {
4254 if !include_source_is_disk_file(file) {
4255 return Err(Error::Config(
4256 "relative config include conditionals must come from files"
4257 .to_owned()
4258 .into(),
4259 ));
4260 }
4261 let parent = file.path.parent().ok_or_else(|| {
4262 Error::Config(
4263 "relative config include conditionals must come from files"
4264 .to_owned()
4265 .into(),
4266 )
4267 })?;
4268 let real = parent.canonicalize().map_err(Error::Io)?;
4269 let mut dir = real.to_string_lossy().into_owned();
4270 if !dir.ends_with('/') && !dir.ends_with('\\') {
4271 dir.push('/');
4272 }
4273 let rest = &pat[2..];
4274 pat = format!("{dir}{rest}");
4275 let prefix_len = dir.len();
4276 add_trailing_starstar_for_dir(&mut pat);
4277 return Ok((pat, prefix_len));
4278 }
4279 let p = Path::new(&pat);
4280 if !p.is_absolute() {
4281 pat.insert_str(0, "**/");
4282 }
4283 add_trailing_starstar_for_dir(&mut pat);
4284 Ok((pat, 0))
4285}
4286
4287fn git_dir_match_texts(ctx: &IncludeContext, git_dir: &Path) -> (String, String) {
4292 let real = git_dir
4293 .canonicalize()
4294 .map(|p| p.to_string_lossy().into_owned())
4295 .unwrap_or_else(|_| git_dir.to_string_lossy().into_owned());
4296 let abs = if git_dir.is_absolute() {
4299 let pwd_abs = ctx.pwd.as_deref().and_then(|pwd| {
4302 let pwd_path = std::path::Path::new(pwd);
4303 if !pwd_path.is_absolute() {
4304 return None;
4305 }
4306 let pwd_canon = pwd_path.canonicalize().ok()?;
4307 let git_dir_str = git_dir.to_string_lossy();
4308 let pwd_canon_str = pwd_canon.to_string_lossy();
4309 let suffix = git_dir_str.strip_prefix(pwd_canon_str.as_ref())?;
4311 Some(format!("{pwd}{suffix}"))
4312 });
4313 pwd_abs.unwrap_or_else(|| git_dir.to_string_lossy().into_owned())
4314 } else {
4315 ctx.cwd.join(git_dir).to_string_lossy().into_owned()
4316 };
4317 (real, abs)
4318}
4319
4320fn include_by_gitdir(
4321 condition: &str,
4322 file: &ConfigFile,
4323 ctx: &IncludeContext,
4324 icase: bool,
4325) -> bool {
4326 let Some(git_dir) = ctx.git_dir.as_ref() else {
4327 return false;
4328 };
4329 let (pattern, prefix) = match prepare_gitdir_pattern(condition, file, ctx) {
4330 Ok(x) => x,
4331 Err(_) => return false,
4332 };
4333 let flags = WM_PATHNAME | if icase { WM_CASEFOLD } else { 0 };
4334 let (text_real, text_abs) = git_dir_match_texts(ctx, git_dir);
4335 let try_match = |text: &str| -> bool {
4336 let t = text.as_bytes();
4337 let p = pattern.as_bytes();
4338 if prefix > 0 {
4339 if t.len() < prefix {
4340 return false;
4341 }
4342 let pre = &p[..prefix];
4343 let te = &t[..prefix];
4344 let ok = if icase {
4345 pre.eq_ignore_ascii_case(te)
4346 } else {
4347 pre == te
4348 };
4349 if !ok {
4350 return false;
4351 }
4352 return wildmatch(&p[prefix..], &t[prefix..], flags);
4353 }
4354 wildmatch(p, t, flags)
4355 };
4356 if try_match(&text_real) {
4357 return true;
4358 }
4359 text_real != text_abs && try_match(&text_abs)
4360}
4361
4362fn current_branch_short_name(git_dir: Option<&Path>) -> Option<String> {
4363 let gd = git_dir?;
4364 let target = refs::read_symbolic_ref(gd, "HEAD").ok()??;
4365 let rest = target.strip_prefix("refs/heads/")?;
4366 Some(rest.to_owned())
4367}
4368
4369fn include_by_onbranch(condition: &str, ctx: &IncludeContext) -> bool {
4370 let Some(short) = current_branch_short_name(ctx.git_dir.as_deref()) else {
4371 return false;
4372 };
4373 let mut pattern = condition.to_owned();
4374 add_trailing_starstar_for_dir(&mut pattern);
4375 wildmatch(pattern.as_bytes(), short.as_bytes(), WM_PATHNAME)
4376}
4377
4378fn is_remote_url_entry(entry: &ConfigEntry) -> bool {
4379 let Ok((section, subsection, variable)) = split_key(&entry.key) else {
4380 return false;
4381 };
4382 section == "remote" && subsection.is_some() && variable == "url"
4383}
4384
4385fn is_hasconfig_remote_url(condition: &str) -> bool {
4386 condition
4387 .strip_prefix("hasconfig:")
4388 .is_some_and(|rest| rest.starts_with("remote.*.url:"))
4389}
4390
4391fn include_by_hasconfig_remote_url(condition: &str, set: &ConfigSet, file: &ConfigFile) -> bool {
4392 let Some(pattern) = condition.strip_prefix("remote.*.url:") else {
4393 return false;
4394 };
4395 set.entries
4396 .iter()
4397 .chain(file.entries.iter())
4398 .filter(|entry| is_remote_url_entry(entry))
4399 .filter_map(|entry| entry.value.as_deref())
4400 .any(|value| wildmatch(pattern.as_bytes(), value.as_bytes(), WM_PATHNAME))
4401}
4402
4403fn validate_hasconfig_remote_url_include(
4404 file: &ConfigFile,
4405 process_includes: bool,
4406 depth: usize,
4407 ctx: &IncludeContext,
4408) -> Result<()> {
4409 const MAX_INCLUDE_DEPTH: usize = 10;
4410 if depth > MAX_INCLUDE_DEPTH {
4411 return Err(ConfigError::IncludeDepthExceeded {
4412 depth,
4413 limit: MAX_INCLUDE_DEPTH,
4414 }
4415 .into());
4416 }
4417 if file.entries.iter().any(is_remote_url_entry) {
4418 return Err(ConfigError::RemoteUrlInHasconfigInclude.into());
4419 }
4420 if !process_includes {
4421 return Ok(());
4422 }
4423 for entry in &file.entries {
4424 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
4425 continue;
4426 };
4427 if let Some(ref cond) = condition {
4428 if !evaluate_include_condition(cond, &ConfigSet::new(), file, ctx) {
4429 continue;
4430 }
4431 }
4432 let resolved = match resolve_include_file_path(&inc_path, file, ctx) {
4433 Ok(p) => p,
4434 Err(Error::Config(ConfigError::Other(msg))) if msg.is_empty() => continue,
4435 Err(e) => return Err(e),
4436 };
4437 if let Some(inc_file) = ConfigFile::from_path(&resolved, file.scope)? {
4438 validate_hasconfig_remote_url_include(&inc_file, process_includes, depth + 1, ctx)?;
4439 }
4440 }
4441 Ok(())
4442}
4443
4444fn evaluate_include_condition(
4449 condition: &str,
4450 set: &ConfigSet,
4451 file: &ConfigFile,
4452 ctx: &IncludeContext,
4453) -> bool {
4454 if let Some(rest) = condition.strip_prefix("gitdir/i:") {
4455 return include_by_gitdir(rest, file, ctx, true);
4456 }
4457 if let Some(rest) = condition.strip_prefix("gitdir:") {
4458 return include_by_gitdir(rest, file, ctx, false);
4459 }
4460 if let Some(rest) = condition.strip_prefix("onbranch:") {
4461 return include_by_onbranch(rest, ctx);
4462 }
4463 if let Some(rest) = condition.strip_prefix("hasconfig:") {
4464 return include_by_hasconfig_remote_url(rest, set, file);
4465 }
4466 false
4467}
4468
4469fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
4471 let first_dot = key
4472 .find('.')
4473 .ok_or_else(|| Error::Config(format!("invalid key: '{key}'").into()))?;
4474 let last_dot = key
4475 .rfind('.')
4476 .ok_or_else(|| Error::Config(format!("invalid key: '{key}'").into()))?;
4477
4478 let section = key[..first_dot].to_owned();
4479 let variable = key[last_dot + 1..].to_owned();
4480
4481 let subsection = if first_dot == last_dot {
4482 None
4483 } else {
4484 Some(key[first_dot + 1..last_dot].to_owned())
4485 };
4486
4487 Ok((section, subsection, variable))
4488}
4489
4490fn parse_section_name(name: &str) -> (&str, Option<&str>) {
4494 match name.find('.') {
4495 Some(i) => (&name[..i], Some(&name[i + 1..])),
4496 None => (name, None),
4497 }
4498}
4499
4500fn section_matches(parser: &Parser, section_lower: &str, subsection: Option<&str>) -> bool {
4501 if parser.section.to_lowercase() == section_lower && parser.subsection.as_deref() == subsection
4502 {
4503 return true;
4504 }
4505 let Some(subsection) = subsection else {
4506 return false;
4507 };
4508 parser.subsection.is_none()
4509 && parser.section.to_lowercase() == format!("{section_lower}.{}", subsection.to_lowercase())
4510}
4511
4512#[must_use]
4514pub fn config_lock_path(path: &Path) -> PathBuf {
4515 let file_name = path
4516 .file_name()
4517 .and_then(|s| s.to_str())
4518 .unwrap_or("config");
4519 path.with_file_name(format!("{file_name}.lock"))
4520}
4521
4522fn validate_section_name(section: &str, subsection: Option<&str>) -> Result<()> {
4523 if section.is_empty()
4524 || !section
4525 .chars()
4526 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
4527 || subsection.is_some_and(str::is_empty)
4528 {
4529 return Err(Error::Config(
4530 format!("invalid section name: {section}").into(),
4531 ));
4532 }
4533 Ok(())
4534}
4535
4536fn raw_variable_name(raw_key: &str) -> &str {
4540 match raw_key.rfind('.') {
4541 Some(i) => &raw_key[i + 1..],
4542 None => raw_key,
4543 }
4544}
4545
4546fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
4551 let first_dot = match raw_key.find('.') {
4552 Some(i) => i,
4553 None => return (raw_key.to_owned(), None),
4554 };
4555 let last_dot = match raw_key.rfind('.') {
4557 Some(i) => i,
4558 None => return (raw_key[..first_dot].to_owned(), None),
4559 };
4560 let section = raw_key[..first_dot].to_owned();
4561 if first_dot == last_dot {
4562 (section, None)
4563 } else {
4564 let subsection = raw_key[first_dot + 1..last_dot].to_owned();
4565 (section, Some(subsection))
4566 }
4567}
4568
4569fn is_section_header_with_inline_entry(line: &str) -> bool {
4571 let trimmed = line.trim();
4572 if !trimmed.starts_with('[') {
4573 return false;
4574 }
4575 let end = match trimmed.find(']') {
4576 Some(i) => i,
4577 None => return false,
4578 };
4579 let after = trimmed[end + 1..].trim();
4580 !after.is_empty() && !after.starts_with('#') && !after.starts_with(';')
4582}
4583
4584fn extract_section_header(line: &str) -> String {
4587 let trimmed = line.trim();
4588 let end = match trimmed.find(']') {
4589 Some(i) => i,
4590 None => return line.to_owned(),
4591 };
4592 trimmed[..=end].to_owned()
4595}
4596
4597#[cfg(test)]
4598mod pack_object_config_tests {
4599 use super::{ConfigFile, ConfigScope, ConfigSet};
4600 use std::path::Path;
4601
4602 fn set_from_snippet(text: &str) -> ConfigSet {
4603 let path = Path::new(".git/config");
4604 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4605 let mut set = ConfigSet::new();
4606 set.merge(&file);
4607 set
4608 }
4609
4610 #[test]
4611 fn pack_object_window_and_depth_defaults() {
4612 let set = ConfigSet::new();
4613 assert_eq!(set.pack_object_window(), 10);
4614 assert_eq!(set.pack_object_depth(), crate::pack::DEFAULT_PACK_DEPTH);
4615 }
4616
4617 #[test]
4618 fn pack_object_window_and_depth_preserve_explicit_zero() {
4619 let set = set_from_snippet("[pack]\n\twindow = 0\n\tdepth = 0\n");
4620 assert_eq!(set.pack_object_window(), 0);
4621 assert_eq!(set.pack_object_depth(), 0);
4622 }
4623
4624 #[test]
4625 fn gc_auto_limits_from_config() {
4626 let set = set_from_snippet("[gc]\n\tauto = 100\n\tautoPackLimit = 3\n");
4627 assert_eq!(set.gc_auto_threshold(), Some(100));
4628 assert_eq!(set.gc_auto_pack_limit(), Some(3));
4629 let defaults = ConfigSet::new();
4630 assert_eq!(defaults.gc_auto_threshold(), Some(6700));
4631 assert_eq!(defaults.gc_auto_pack_limit(), Some(50));
4632 assert!(defaults.gc_auto_detach());
4633 assert!(defaults.maintenance_auto());
4634 let disabled = set_from_snippet("[gc]\n\tautoPackLimit = 0\n");
4635 assert_eq!(disabled.gc_auto_pack_limit(), None);
4636 assert_eq!(disabled.gc_auto_threshold(), Some(6700));
4637 let auto_off = set_from_snippet("[gc]\n\tauto = 0\n");
4638 assert_eq!(auto_off.gc_auto_threshold(), None);
4639 let negative = set_from_snippet("[gc]\n\tauto = -1\n\tautoPackLimit = -1\n");
4640 assert_eq!(negative.gc_auto_threshold(), None);
4641 assert_eq!(negative.gc_auto_pack_limit(), None);
4642 let unparsable = set_from_snippet("[gc]\n\tauto = lots\n");
4643 assert_eq!(unparsable.gc_auto_threshold(), Some(6700));
4644 let foreground =
4645 set_from_snippet("[gc]\n\tautoDetach = false\n[maintenance]\n\tauto = false\n");
4646 assert!(!foreground.gc_auto_detach());
4647 assert!(!foreground.maintenance_auto());
4648 }
4649}
4650
4651#[cfg(test)]
4652mod get_regexp_tests {
4653 use super::{ConfigFile, ConfigScope, ConfigSet};
4654 use std::path::Path;
4655
4656 fn set_from_snippet(text: &str) -> ConfigSet {
4657 let path = Path::new(".git/config");
4658 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4659 let mut set = ConfigSet::new();
4660 set.merge(&file);
4661 set
4662 }
4663
4664 #[test]
4665 fn get_regexp_matches_section_prefix_like_git_config() {
4666 let text = r#"
4667[user]
4668 email = alice@example.com
4669 name = Alice
4670[core]
4671 bare = false
4672"#;
4673 let set = set_from_snippet(text);
4674 let keys: Vec<_> = set
4675 .get_regexp("user")
4676 .expect("valid pattern")
4677 .into_iter()
4678 .map(|e| e.key.as_str())
4679 .collect();
4680 assert!(keys.contains(&"user.email"));
4681 assert!(keys.contains(&"user.name"));
4682 assert!(!keys.iter().any(|k| k.starts_with("core.")));
4683 }
4684
4685 #[test]
4686 fn get_regexp_returns_all_multi_value_entries_in_order() {
4687 let text = r#"
4688[remote "origin"]
4689 url = https://example.com/repo.git
4690 fetch = +refs/heads/*:refs/remotes/origin/*
4691 push = +refs/heads/main:refs/heads/main
4692 push = +refs/heads/develop:refs/heads/develop
4693"#;
4694 let set = set_from_snippet(text);
4695 let matches = set.get_regexp("remote.origin").expect("valid pattern");
4696 let push_vals: Vec<_> = matches
4697 .iter()
4698 .filter(|e| e.key == "remote.origin.push")
4699 .map(|e| e.value.as_deref().unwrap_or(""))
4700 .collect();
4701 assert_eq!(push_vals.len(), 2);
4702 assert_eq!(push_vals[0], "+refs/heads/main:refs/heads/main");
4703 assert_eq!(push_vals[1], "+refs/heads/develop:refs/heads/develop");
4704 }
4705
4706 #[test]
4707 fn get_regexp_dot_matches_any_key() {
4708 let text = r#"
4709[a]
4710 x = 1
4711[b]
4712 y = 2
4713"#;
4714 let set = set_from_snippet(text);
4715 let m = set.get_regexp(".").expect("valid pattern");
4716 assert_eq!(m.len(), 2);
4717 }
4718
4719 #[test]
4720 fn get_regexp_no_match_returns_empty_vec() {
4721 let set = set_from_snippet("[user]\n\tname = x\n");
4722 let m = set.get_regexp("zzz").expect("valid pattern");
4723 assert!(m.is_empty());
4724 }
4725
4726 #[test]
4727 fn get_regexp_invalid_pattern_is_error() {
4728 let set = set_from_snippet("[user]\n\tname = x\n");
4729 let err = set.get_regexp("(").expect_err("unclosed group");
4730 assert!(err.contains("invalid key pattern"), "got: {err}");
4731 }
4732
4733 #[test]
4734 fn valueless_negotiation_algorithm_is_a_typed_config_error() {
4735 use crate::error::{ConfigError, Error};
4736
4737 let path = Path::new("/repo/.git/config");
4738 for (text, line) in [
4739 (
4740 "[core]\n\tbare = false\n[fetch]\n\tnegotiationAlgorithm\n",
4741 4,
4742 ),
4743 ("[fetch] negotiationAlgorithm\n", 1),
4744 ] {
4745 let err = ConfigFile::parse(path, text, ConfigScope::Local).expect_err(text);
4746 let Error::Config(config_err) = err else {
4747 panic!("expected a config error for {text:?}, got {err:?}");
4748 };
4749 assert_eq!(
4750 config_err,
4751 ConfigError::BadConfigVariable {
4752 key: "fetch.negotiationalgorithm".to_owned(),
4753 file: ".git/config".to_owned(),
4754 line,
4755 },
4756 "{text:?}"
4757 );
4758 }
4759
4760 let ok = ConfigFile::parse(
4761 path,
4762 "[fetch]\n\tnegotiationAlgorithm = skipping\n",
4763 ConfigScope::Local,
4764 )
4765 .expect("valued key parses");
4766 assert_eq!(ok.entries[0].value.as_deref(), Some("skipping"));
4767 }
4768}
4769
4770#[cfg(test)]
4771mod loose_compression_tests {
4772 use std::path::Path;
4773
4774 use super::{ConfigFile, ConfigScope, ConfigSet, Error};
4775
4776 fn set_from_snippet(text: &str) -> ConfigSet {
4777 let path = Path::new(".git/config");
4778 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4779 let mut set = ConfigSet::new();
4780 set.merge(&file);
4781 set
4782 }
4783
4784 #[test]
4785 fn big_file_threshold_default_suffixes_and_errors() {
4786 assert_eq!(
4787 ConfigSet::new().big_file_threshold().unwrap(),
4788 512 * 1024 * 1024
4789 );
4790 let cases = [
4791 ("1000", 1000),
4792 ("0", 0),
4793 ("4k", 4096),
4794 ("2m", 2 * 1024 * 1024),
4795 ("1g", 1024 * 1024 * 1024),
4796 ("8G", 8 * 1024 * 1024 * 1024),
4797 ];
4798 for (raw, want) in cases {
4799 let set = set_from_snippet(&format!("[core]\n\tbigFileThreshold = {raw}\n"));
4800 assert_eq!(set.big_file_threshold().unwrap(), want, "value {raw}");
4801 }
4802 let set = set_from_snippet("[core]\n\tbigfilethreshold = 1k\n\tbigFileThreshold = 3k\n");
4803 assert_eq!(set.big_file_threshold().unwrap(), 3 * 1024);
4804
4805 for raw in ["-1", "12x", "lots", ""] {
4806 let snippet = if raw.is_empty() {
4807 "[core]\n\tbigFileThreshold\n".to_owned()
4808 } else {
4809 format!("[core]\n\tbigFileThreshold = {raw}\n")
4810 };
4811 let err = set_from_snippet(&snippet).big_file_threshold().unwrap_err();
4812 assert!(
4813 matches!(
4814 err,
4815 Error::Config(super::ConfigError::BadNumericValue { ref key, .. })
4816 if key == "core.bigfilethreshold"
4817 ),
4818 "value {raw:?}: {err:?}"
4819 );
4820 }
4821 }
4822
4823 #[test]
4824 fn loose_compression_cascade() {
4825 let set = ConfigSet::new();
4826 assert_eq!(
4827 set.loose_objects_zlib_level().unwrap(),
4828 ConfigSet::LOOSE_OBJECTS_ZLIB_DEFAULT
4829 );
4830
4831 let set = set_from_snippet("[core]\n\tcompression = 0\n");
4832 assert_eq!(set.loose_objects_zlib_level().unwrap(), 0);
4833
4834 let set = set_from_snippet("[core]\n\tcompression = 9\n");
4835 assert_eq!(set.loose_objects_zlib_level().unwrap(), 9);
4836
4837 let set = set_from_snippet("[core]\n\tcompression = 0\n\tloosecompression = 9\n");
4838 assert_eq!(set.loose_objects_zlib_level().unwrap(), 9);
4839
4840 let set = set_from_snippet("[core]\n\tloosecompression = 3\n");
4841 assert_eq!(set.loose_objects_zlib_level().unwrap(), 3);
4842
4843 let set = set_from_snippet("[core]\n\tcompression = -1\n");
4844 assert_eq!(set.loose_objects_zlib_level().unwrap(), 6);
4845
4846 for level in 0..=9 {
4847 let set = set_from_snippet(&format!("[core]\n\tloosecompression = {level}\n"));
4848 assert_eq!(set.loose_objects_zlib_level().unwrap(), level);
4849 }
4850
4851 let set = set_from_snippet("[core]\n\tloosecompression = -1\n");
4852 assert_eq!(set.loose_objects_zlib_level().unwrap(), 6);
4853
4854 let err = set_from_snippet("[core]\n\tloosecompression = 10\n")
4855 .loose_objects_zlib_level()
4856 .unwrap_err();
4857 assert!(matches!(err, Error::Config(_)));
4858
4859 let err = set_from_snippet("[core]\n\tloosecompression = not-a-number\n")
4860 .loose_objects_zlib_level()
4861 .unwrap_err();
4862 assert!(matches!(err, Error::Config(_)));
4863
4864 let err = set_from_snippet("[core]\n\tcompression = 42\n")
4865 .loose_objects_zlib_level()
4866 .unwrap_err();
4867 assert!(matches!(err, Error::Config(_)));
4868
4869 let err = set_from_snippet("[core]\n\tcompression = nope\n\tloosecompression = 1\n")
4870 .loose_objects_zlib_level()
4871 .unwrap_err();
4872 assert!(matches!(err, Error::Config(_)));
4873
4874 let err = set_from_snippet("[core]\n\tloosecompression = nope\n\tloosecompression = 1\n")
4875 .loose_objects_zlib_level()
4876 .unwrap_err();
4877 assert!(matches!(err, Error::Config(_)));
4878
4879 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 9\n");
4880 assert_eq!(set.loose_objects_zlib_level().unwrap(), 1);
4881
4882 let err = set_from_snippet("[core]\n\tloosecompression\n")
4883 .loose_objects_zlib_level()
4884 .unwrap_err();
4885 assert!(matches!(err, Error::Config(_)));
4886 }
4887}
4888
4889#[cfg(test)]
4890mod pack_compression_tests {
4891 use super::{ConfigFile, ConfigScope, ConfigSet};
4892 use std::path::Path;
4893
4894 fn set_from_snippet(text: &str) -> ConfigSet {
4895 let path = Path::new(".git/config");
4896 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4897 let mut set = ConfigSet::new();
4898 set.merge(&file);
4899 set
4900 }
4901
4902 #[test]
4903 fn pack_objects_zlib_level_defaults_to_six() {
4904 let set = ConfigSet::new();
4905 assert_eq!(set.pack_objects_zlib_level().unwrap(), 6);
4906 }
4907
4908 #[test]
4909 fn pack_objects_zlib_level_core_compression() {
4910 let set = set_from_snippet("[core]\n\tcompression = 0\n");
4911 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
4912 let set = set_from_snippet("[core]\n\tcompression = 9\n");
4913 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
4914 }
4915
4916 #[test]
4917 fn pack_objects_zlib_level_pack_overrides_core() {
4918 let set = set_from_snippet("[core]\n\tcompression = 9\n[pack]\n\tcompression = 0\n");
4919 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
4920 let set = set_from_snippet("[core]\n\tcompression = 0\n[pack]\n\tcompression = 9\n");
4921 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
4922 }
4923
4924 #[test]
4925 fn pack_objects_zlib_level_later_core_does_not_override_earlier_pack() {
4926 let mut set = ConfigSet::new();
4927 set.merge(
4928 &ConfigFile::parse(
4929 Path::new("a"),
4930 "[pack]\n\tcompression = 9\n",
4931 ConfigScope::Local,
4932 )
4933 .unwrap(),
4934 );
4935 set.merge(
4936 &ConfigFile::parse(
4937 Path::new("b"),
4938 "[core]\n\tcompression = 0\n",
4939 ConfigScope::Local,
4940 )
4941 .unwrap(),
4942 );
4943 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
4944 }
4945
4946 #[test]
4947 fn pack_objects_zlib_level_loosecompression_does_not_block_core_pack_level() {
4948 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 0\n");
4949 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
4950 }
4951
4952 #[test]
4953 fn pack_objects_zlib_level_pack_wins_after_loose_and_core() {
4954 let set = set_from_snippet(
4955 "[core]\n\tloosecompression = 1\n\tcompression = 0\n[pack]\n\tcompression = 9\n",
4956 );
4957 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
4958 }
4959}
4960
4961#[cfg(test)]
4962mod config_cache_tests {
4963 use super::*;
4964 use filetime::FileTime;
4965
4966 fn local_opts(git_dir: &Path) -> LoadConfigOptions {
4967 LoadConfigOptions {
4968 include_system: false,
4969 include_ctx: IncludeContext {
4970 git_dir: Some(git_dir.to_path_buf()),
4971 ..Default::default()
4972 },
4973 ..Default::default()
4974 }
4975 }
4976
4977 fn load_value(caches: &crate::repo_caches::RepoCaches, git_dir: &Path) -> Option<String> {
4978 let opts = local_opts(git_dir);
4979 let set = caches
4980 .load_config_with_options(&Environment::empty(), Some(git_dir), &opts)
4981 .expect("load cascade");
4982 set.get("gritcachetest.value")
4983 }
4984
4985 fn mtime_of(path: &Path) -> FileTime {
4986 FileTime::from_last_modification_time(&fs::metadata(path).expect("stat config"))
4987 }
4988
4989 fn restore_mtime(path: &Path, stamp: FileTime) {
4990 filetime::set_file_mtime(path, stamp).expect("restore mtime");
4991 }
4992
4993 #[test]
4994 fn cache_serves_same_stamp_and_config_write_evicts() {
4995 let caches =
4996 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
4997 let td = tempfile::tempdir().expect("tempdir");
4998 let gd = td.path();
4999 let cfg = gd.join("config");
5000 fs::write(&cfg, "[gritcachetest]\n\tvalue = aaa\n").expect("write v1");
5001 let t0 = mtime_of(&cfg);
5002 assert_eq!(load_value(&caches, gd).as_deref(), Some("aaa"));
5003
5004 fs::write(&cfg, "[gritcachetest]\n\tvalue = bbb\n").expect("write v2");
5008 restore_mtime(&cfg, t0);
5009 assert_eq!(load_value(&caches, gd).as_deref(), Some("aaa"));
5010
5011 let mut file = ConfigFile::from_path(&cfg, ConfigScope::Local)
5013 .expect("read config")
5014 .expect("config exists");
5015 file.set("gritcachetest.value", "ccc").expect("set value");
5016 file.write().expect("persist");
5017 caches.evict_config_for_path(&cfg);
5018 restore_mtime(&cfg, t0);
5019 assert_eq!(load_value(&caches, gd).as_deref(), Some("ccc"));
5020 }
5021
5022 #[test]
5023 fn cache_invalidates_on_size_or_existence_change() {
5024 let caches =
5025 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5026 let td = tempfile::tempdir().expect("tempdir");
5027 let gd = td.path();
5028 let cfg = gd.join("config");
5029 assert_eq!(load_value(&caches, gd), None);
5031 fs::write(&cfg, "[gritcachetest]\n\tvalue = first\n").expect("create");
5033 assert_eq!(load_value(&caches, gd).as_deref(), Some("first"));
5034 let t0 = mtime_of(&cfg);
5036 fs::write(&cfg, "[gritcachetest]\n\tvalue = second-longer\n").expect("rewrite");
5037 restore_mtime(&cfg, t0);
5038 assert_eq!(load_value(&caches, gd).as_deref(), Some("second-longer"));
5039 }
5040
5041 #[test]
5042 fn include_targets_are_stamped_and_invalidate() {
5043 let caches =
5044 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5045 let td = tempfile::tempdir().expect("tempdir");
5046 let gd = td.path();
5047 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
5048 let inc = gd.join("extra.conf");
5049 fs::write(&inc, "[gritcachetest]\n\tvalue = one\n").expect("write include v1");
5050 assert_eq!(load_value(&caches, gd).as_deref(), Some("one"));
5051
5052 let t0 = mtime_of(&inc);
5056 fs::write(&inc, "[gritcachetest]\n\tvalue = two\n").expect("write include v2");
5057 restore_mtime(&inc, t0);
5058 assert_eq!(load_value(&caches, gd).as_deref(), Some("one"));
5059
5060 fs::write(&inc, "[gritcachetest]\n\tvalue = two-longer\n").expect("write include v3");
5062 assert_eq!(load_value(&caches, gd).as_deref(), Some("two-longer"));
5063 }
5064
5065 #[test]
5066 fn missing_include_target_is_watched() {
5067 let caches =
5068 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5069 let td = tempfile::tempdir().expect("tempdir");
5070 let gd = td.path();
5071 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
5072 assert_eq!(load_value(&caches, gd), None);
5073
5074 fs::write(gd.join("extra.conf"), "[gritcachetest]\n\tvalue = born\n").expect("create");
5077 assert_eq!(load_value(&caches, gd).as_deref(), Some("born"));
5078 }
5079
5080 #[test]
5081 fn onbranch_condition_follows_head() {
5082 let caches =
5083 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5084 let td = tempfile::tempdir().expect("tempdir");
5085 let gd = td.path();
5086 fs::write(gd.join("HEAD"), "ref: refs/heads/main\n").expect("write HEAD");
5087 fs::write(
5088 gd.join("config"),
5089 "[includeIf \"onbranch:main\"]\n\tpath = branch.conf\n",
5090 )
5091 .expect("write config");
5092 fs::write(
5093 gd.join("branch.conf"),
5094 "[gritcachetest]\n\tvalue = onmain\n",
5095 )
5096 .expect("write branch config");
5097 assert_eq!(load_value(&caches, gd).as_deref(), Some("onmain"));
5098
5099 fs::write(gd.join("HEAD"), "ref: refs/heads/dev\n").expect("rewrite HEAD");
5102 assert_eq!(load_value(&caches, gd), None);
5103 }
5104
5105 #[test]
5106 #[cfg(unix)]
5107 fn load_propagates_local_config_io_errors() {
5108 use std::os::unix::fs::symlink;
5109 let td = tempfile::tempdir().expect("tempdir");
5110 let git_dir = td.path();
5111 let config = git_dir.join("config");
5112 symlink("config", &config).expect("self-referential symlink");
5113 let err = ConfigSet::load(&Environment::empty(), Some(git_dir), false).unwrap_err();
5114 assert!(
5115 matches!(err, Error::Io(_)),
5116 "expected I/O error for symlink loop, got {err:?}"
5117 );
5118 }
5119}