1use crate::git_path::CanonicalizeExt;
39use std::borrow::Cow;
40use std::collections::HashMap;
41
42use crate::environment::Environment;
43use std::fmt;
44use std::fs;
45use std::path::{Path, PathBuf};
46use std::sync::Arc;
47use std::time::SystemTime;
48
49use crate::error::{
50 BadNumericSource, ConfigError, ConfigValueError, Error, InvalidKeyReason, Result,
51};
52
53#[cfg(test)]
55pub mod cascade_load_counters {
56 use std::cell::Cell;
57
58 thread_local! {
61 static MEASURING: Cell<bool> = const { Cell::new(false) };
62 static UNCACHED: Cell<usize> = const { Cell::new(0) };
63 static CACHE_VALIDATED: Cell<usize> = const { Cell::new(0) };
64 }
65
66 pub fn measure<R>(f: impl FnOnce() -> R) -> R {
68 MEASURING.with(|m| {
69 m.set(true);
70 reset();
71 let out = f();
72 m.set(false);
73 out
74 })
75 }
76
77 pub fn reset() {
79 UNCACHED.with(|c| c.set(0));
80 CACHE_VALIDATED.with(|c| c.set(0));
81 }
82
83 #[must_use]
85 pub fn uncached_loads() -> usize {
86 UNCACHED.with(Cell::get)
87 }
88
89 #[must_use]
91 pub fn cache_validated_loads() -> usize {
92 CACHE_VALIDATED.with(Cell::get)
93 }
94
95 #[must_use]
97 pub fn total_loads() -> usize {
98 uncached_loads().saturating_add(cache_validated_loads())
99 }
100
101 pub(crate) fn record_uncached() {
102 if MEASURING.with(Cell::get) {
103 UNCACHED.with(|c| c.set(c.get() + 1));
104 }
105 }
106}
107use crate::refs;
108use crate::wildmatch::{wildmatch, WM_CASEFOLD, WM_PATHNAME};
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
112pub enum ConfigScope {
113 System,
115 Global,
117 Local,
119 Worktree,
121 Command,
123}
124
125impl fmt::Display for ConfigScope {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 match self {
128 Self::System => write!(f, "system"),
129 Self::Global => write!(f, "global"),
130 Self::Local => write!(f, "local"),
131 Self::Worktree => write!(f, "worktree"),
132 Self::Command => write!(f, "command"),
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ConfigValueOrigin {
140 pub value: Option<String>,
143 pub scope: ConfigScope,
145 pub file: Option<PathBuf>,
147 pub line: Option<usize>,
149}
150
151#[derive(Debug, Clone)]
153pub struct ConfigEntry {
154 pub key: String,
157 pub value: Option<String>,
159 pub scope: ConfigScope,
161 pub file: Option<PathBuf>,
163 pub line: usize,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum ConfigIncludeOrigin {
170 Disk,
172 Stdin,
174 CommandLine,
176 Blob,
178}
179
180#[derive(Debug, Clone)]
183pub struct ConfigFile {
184 pub path: PathBuf,
186 pub scope: ConfigScope,
188 pub entries: Vec<ConfigEntry>,
190 raw_lines: Vec<String>,
192 pub include_origin: ConfigIncludeOrigin,
194}
195
196#[derive(Debug, Clone, Default)]
215pub struct ConfigSet {
216 entries: Vec<ConfigEntry>,
218}
219
220#[derive(Debug, Clone)]
222pub struct IncludeContext {
223 pub git_dir: Option<PathBuf>,
225 pub command_line_relative_include_is_error: bool,
227 pub cwd: PathBuf,
229 pub pwd: Option<String>,
231 pub env: std::sync::Arc<Environment>,
233}
234
235impl Default for IncludeContext {
236 fn default() -> Self {
237 Self {
238 git_dir: None,
239 command_line_relative_include_is_error: false,
240 cwd: PathBuf::from("."),
241 pwd: None,
242 env: std::sync::Arc::new(Environment::empty()),
243 }
244 }
245}
246
247#[derive(Clone)]
249pub struct LoadConfigOptions {
250 pub include_system: bool,
252 pub process_includes: bool,
254 pub command_includes: bool,
256 pub include_ctx: IncludeContext,
258 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
260 pub repo_caches: Option<std::sync::Arc<crate::repo_caches::RepoCaches>>,
262}
263
264impl std::fmt::Debug for LoadConfigOptions {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 f.debug_struct("LoadConfigOptions")
267 .field("include_system", &self.include_system)
268 .field("process_includes", &self.process_includes)
269 .field("command_includes", &self.command_includes)
270 .field("include_ctx", &self.include_ctx)
271 .finish_non_exhaustive()
272 }
273}
274
275impl Default for LoadConfigOptions {
276 fn default() -> Self {
277 Self {
278 include_system: true,
279 process_includes: true,
280 command_includes: true,
281 include_ctx: IncludeContext::default(),
282 diagnostics: None,
283 repo_caches: None,
284 }
285 }
286}
287
288impl LoadConfigOptions {
289 pub(crate) fn for_repository(
296 env: &Environment,
297 git_dir: Option<&Path>,
298 repo_caches: Option<Arc<crate::repo_caches::RepoCaches>>,
299 ) -> Self {
300 Self {
301 include_ctx: IncludeContext {
302 git_dir: git_dir.map(Path::to_path_buf),
303 cwd: env.cwd.clone(),
304 pwd: env.pwd.clone(),
305 ..IncludeContext::default()
306 },
307 repo_caches,
308 ..Self::default()
309 }
310 }
311}
312
313#[must_use]
318pub fn config_entry_origin_file(entry: &ConfigEntry) -> Option<&Path> {
319 if entry.scope == ConfigScope::Command && entry.file.is_none() {
320 None
321 } else {
322 entry.file.as_deref()
323 }
324}
325
326#[must_use]
328pub fn config_entry_origin_line(entry: &ConfigEntry) -> Option<usize> {
329 if entry.scope == ConfigScope::Command && entry.file.is_none() {
330 None
331 } else {
332 (entry.line > 0).then_some(entry.line)
333 }
334}
335
336pub fn canonical_key(raw: &str) -> Result<String> {
352 let invalid = |reason| {
353 Error::Config(ConfigError::InvalidKey {
354 key: raw.to_owned(),
355 reason,
356 })
357 };
358 if raw.contains('\n') || raw.contains('\r') {
359 return Err(invalid(InvalidKeyReason::LineBreak));
360 }
361
362 let (Some(first_dot), Some(last_dot)) = (raw.find('.'), raw.rfind('.')) else {
363 return Err(invalid(InvalidKeyReason::MissingSection));
364 };
365
366 if last_dot == raw.len() - 1 {
367 return Err(invalid(InvalidKeyReason::MissingVariable));
368 }
369
370 let section = &raw[..first_dot];
371 let name = &raw[last_dot + 1..];
372
373 if section.is_empty() || !section.chars().all(|c| c.is_alphanumeric() || c == '-') {
375 return Err(invalid(InvalidKeyReason::InvalidSection));
376 }
377
378 if !name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
380 || !name.chars().all(|c| c.is_alphanumeric() || c == '-')
381 {
382 return Err(invalid(InvalidKeyReason::InvalidVariable));
383 }
384
385 let mut key = String::with_capacity(raw.len());
386 push_lowercase(&mut key, section);
387 key.push('.');
388 if first_dot != last_dot {
389 key.push_str(&raw[first_dot + 1..last_dot]);
390 key.push('.');
391 }
392 push_lowercase(&mut key, name);
393 Ok(key)
394}
395
396fn push_lowercase(out: &mut String, s: &str) {
398 if s.is_ascii() {
399 out.extend(s.bytes().map(|b| char::from(b.to_ascii_lowercase())));
400 } else {
401 out.push_str(&s.to_lowercase());
402 }
403}
404
405#[must_use]
409pub fn config_file_display_for_error(path: &Path) -> String {
410 config_error_path_display(path)
411}
412
413fn config_error_path_display(path: &Path) -> String {
414 if path == Path::new("-") {
415 return "standard input".to_owned();
416 }
417 if path.file_name().and_then(|s| s.to_str()) == Some("config")
418 && path
419 .parent()
420 .and_then(|p| p.file_name())
421 .and_then(|s| s.to_str())
422 == Some(".git")
423 {
424 return ".git/config".to_owned();
425 }
426 path.display().to_string()
427}
428
429fn reject_valueless_negotiation_algorithm(
431 key: &str,
432 value: Option<&str>,
433 path: &Path,
434 line: usize,
435) -> Result<()> {
436 if key != "fetch.negotiationalgorithm" || value.is_some() {
437 return Ok(());
438 }
439 Err(ConfigError::BadConfigVariable {
440 key: key.to_owned(),
441 file: config_error_path_display(path),
442 line,
443 }
444 .into())
445}
446
447struct Parser {
449 section: String,
450 subsection: Option<String>,
451 key_prefix: String,
454}
455
456impl Parser {
457 fn new() -> Self {
458 Self {
459 section: String::new(),
460 subsection: None,
461 key_prefix: String::new(),
462 }
463 }
464
465 fn valid_variable_name(name: &str) -> bool {
466 let name = name.trim();
467 name.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
468 && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
469 }
470
471 fn set_section(&mut self, section: String, subsection: Option<String>) {
472 self.key_prefix.clear();
473 push_lowercase(&mut self.key_prefix, §ion);
474 self.key_prefix.push('.');
475 if let Some(sub) = &subsection {
476 self.key_prefix.push_str(sub);
477 self.key_prefix.push('.');
478 }
479 self.section = section;
480 self.subsection = subsection;
481 }
482
483 fn make_key(&self, name: &str) -> String {
487 let mut key = String::with_capacity(self.key_prefix.len() + name.len());
488 key.push_str(&self.key_prefix);
489 push_lowercase(&mut key, name);
490 key
491 }
492
493 fn try_parse_section_with_remainder<'a>(
499 &mut self,
500 line: &'a str,
501 inline_remainder: &mut Option<&'a str>,
502 ) -> bool {
503 let trimmed = line.trim();
504 if !trimmed.starts_with('[') {
505 return false;
506 }
507 let end = {
511 let bytes = trimmed.as_bytes();
512 let mut i = 1; let mut in_quotes = false;
514 let mut found = None;
515 while i < bytes.len() {
516 if in_quotes {
517 if bytes[i] == b'\\' {
518 i += 2; continue;
520 }
521 if bytes[i] == b'"' {
522 in_quotes = false;
523 }
524 } else {
525 if bytes[i] == b'"' {
526 in_quotes = true;
527 }
528 if bytes[i] == b']' {
529 found = Some(i);
530 break;
531 }
532 }
533 i += 1;
534 }
535 match found {
536 Some(i) => i,
537 None => return false,
538 }
539 };
540 let inside = &trimmed[1..end];
541 if let Some(quote_start) = inside.find('"') {
543 let section = inside[..quote_start].trim().to_owned();
544 let rest = &inside[quote_start + 1..];
545 let mut sub = String::new();
547 let mut chars = rest.chars();
548 while let Some(ch) = chars.next() {
549 if ch == '\\' {
550 if let Some(escaped) = chars.next() {
551 sub.push(escaped);
552 }
553 } else if ch == '"' {
554 break;
555 } else {
556 sub.push(ch);
557 }
558 }
559 self.set_section(section, Some(sub));
560 } else {
561 self.set_section(inside.trim().to_owned(), None);
562 }
563 let after = trimmed[end + 1..].trim();
565 if !after.is_empty() && !after.starts_with('#') && !after.starts_with(';') {
566 *inline_remainder = Some(after);
567 } else {
568 *inline_remainder = None;
569 }
570 true
571 }
572
573 fn try_parse_section(&mut self, line: &str) -> bool {
575 let mut _remainder = None;
576 self.try_parse_section_with_remainder(line, &mut _remainder)
577 }
578
579 fn try_parse_entry(&self, line: &str) -> Option<(String, Option<String>)> {
583 let trimmed = line.trim();
584 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
585 return None;
586 }
587 if trimmed.starts_with('[') {
588 return None;
589 }
590 if self.section.is_empty() {
591 return None;
592 }
593
594 if let Some(eq_pos) = trimmed.find('=') {
595 let raw_name = trimmed[..eq_pos].trim();
596 if !Self::valid_variable_name(raw_name) {
597 return None;
598 }
599 let raw_value = trimmed[eq_pos + 1..].trim();
600 let value = parse_config_value(strip_inline_comment(raw_value));
602 let key = self.make_key(raw_name);
603 Some((key, Some(value)))
604 } else {
605 let raw_name = strip_inline_comment(trimmed);
607 if raw_name.split_whitespace().count() > 1 {
608 return None;
609 }
610 let raw_name = raw_name.trim();
611 if !Self::valid_variable_name(raw_name) {
612 return None;
613 }
614 let key = self.make_key(raw_name);
615 Some((key, None))
616 }
617 }
618}
619
620fn entry_line_value_has_unclosed_quote(line: &str) -> bool {
630 let trimmed = line.trim();
631 let Some(eq_pos) = trimmed.find('=') else {
632 return false;
633 };
634 let raw_value = trimmed[eq_pos + 1..].trim_start();
635 let mut in_quote = false;
636 let mut last_was_backslash = false;
637 for b in raw_value.bytes() {
640 match b {
641 b'"' if !last_was_backslash => {
642 in_quote = !in_quote;
643 last_was_backslash = false;
644 }
645 b'\\' if !last_was_backslash => {
646 last_was_backslash = true;
647 continue;
648 }
649 b'#' | b';' if !in_quote && !last_was_backslash => return false,
650 _ => {
651 last_was_backslash = false;
652 }
653 }
654 }
655 in_quote
656}
657
658fn value_line_continues(line: &str) -> bool {
659 let trimmed = line.trim();
660 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
661 return false;
662 }
663 let value_part = match trimmed.find('=') {
666 Some(pos) => &trimmed[pos + 1..],
667 None => return false,
668 };
669 let mut in_quote = false;
671 let mut last_was_backslash = false;
672 let mut in_comment = false;
673 for b in value_part.bytes() {
674 if in_comment {
675 last_was_backslash = false;
677 continue;
678 }
679 match b {
680 b'"' if !last_was_backslash => {
681 in_quote = !in_quote;
682 last_was_backslash = false;
683 }
684 b'\\' if !last_was_backslash => {
685 last_was_backslash = true;
686 continue;
687 }
688 b'#' | b';' if !in_quote && !last_was_backslash => {
689 in_comment = true;
690 last_was_backslash = false;
691 }
692 _ => {
693 last_was_backslash = false;
694 }
695 }
696 }
697 last_was_backslash && !in_comment
699}
700
701fn strip_inline_comment(s: &str) -> &str {
704 let bytes = s.as_bytes();
705 let mut in_quote = false;
706 let mut last_was_backslash = false;
707 let mut i = 0;
708 while i < bytes.len() {
709 match bytes[i] {
710 b'"' if !last_was_backslash => {
711 in_quote = !in_quote;
712 last_was_backslash = false;
713 }
714 b'\\' if !last_was_backslash => {
715 last_was_backslash = true;
716 if in_quote && i + 1 < bytes.len() {
717 i += 1;
718 last_was_backslash = false;
719 }
720 }
721 b'#' | b';' if !in_quote && !last_was_backslash => break,
722 _ => last_was_backslash = false,
723 }
724 i += 1;
725 }
726 s[..i].trim_end()
728}
729
730fn parse_config_value(s: &str) -> String {
732 if s.starts_with('"') {
733 unescape_value(s)
734 } else {
735 unescape_unquoted_value(s)
736 }
737}
738
739fn unescape_unquoted_value(s: &str) -> String {
743 let mut result = String::with_capacity(s.len());
744 let mut chars = s.chars();
745 while let Some(ch) = chars.next() {
746 if ch == '\\' {
747 match chars.next() {
748 Some('n') => result.push('\n'),
749 Some('r') => result.push('\r'),
750 Some('t') => result.push('\t'),
751 Some('b') => result.push('\x08'),
752 Some('\\') => result.push('\\'),
753 Some('"') => result.push('"'),
754 Some(other) => {
755 result.push('\\');
756 result.push(other);
757 }
758 None => result.push('\\'),
759 }
760 } else {
761 result.push(ch);
762 }
763 }
764 result
765}
766
767fn unescape_value(s: &str) -> String {
770 let mut result = String::with_capacity(s.len());
771 let mut chars = s.chars();
772 while let Some(ch) = chars.next() {
773 match ch {
774 '"' => { }
775 '\\' => match chars.next() {
776 Some('n') => result.push('\n'),
777 Some('r') => result.push('\r'),
778 Some('t') => result.push('\t'),
779 Some('b') => result.push('\x08'),
780 Some('\\') => result.push('\\'),
781 Some('"') => result.push('"'),
782 Some(other) => {
783 result.push('\\');
784 result.push(other);
785 }
786 None => result.push('\\'),
787 },
788 _ => result.push(ch),
789 }
790 }
791 result
792}
793
794fn escape_subsection(s: &str) -> String {
801 let mut out = String::with_capacity(s.len());
802 for ch in s.chars() {
803 match ch {
804 '"' => out.push_str("\\\""),
805 '\\' => out.push_str("\\\\"),
806 other => out.push(other),
807 }
808 }
809 out
810}
811
812fn escape_value(s: &str) -> String {
813 let leading_dash_needs_quoting = s.starts_with('-') && parse_i64(s).is_err();
816 let must_quote = leading_dash_needs_quoting
817 || s.starts_with(' ')
818 || s.starts_with('\t')
819 || s.ends_with(' ')
820 || s.ends_with('\t')
821 || s.contains('"')
822 || s.contains('#')
823 || s.contains(';');
824
825 if must_quote {
826 return escape_value_quoted(s);
827 }
828
829 if s.contains('\n') || s.contains('\r') || s.contains('\t') || s.contains('\\') {
830 let mut out = String::with_capacity(s.len() + 4);
831 for ch in s.chars() {
832 match ch {
833 '\n' => out.push_str("\\n"),
834 '\r' => out.push_str("\\r"),
835 '\t' => out.push_str("\\t"),
836 '\\' => out.push_str("\\\\"),
837 '"' => out.push_str("\\\""),
838 other => out.push(other),
839 }
840 }
841 return out;
842 }
843
844 s.to_owned()
845}
846
847fn compile_config_value_pattern(pat: &str) -> std::result::Result<(regex::Regex, bool), Error> {
849 let (negated, actual_pat) = if let Some(rest) = pat.strip_prefix('!') {
850 (true, rest)
851 } else {
852 (false, pat)
853 };
854 let re = regex::Regex::new(actual_pat).map_err(|e| {
855 Error::Config(ConfigError::InvalidValuePattern {
856 pattern: pat.to_owned(),
857 reason: e.to_string(),
858 })
859 })?;
860 Ok((re, negated))
861}
862
863fn value_matches_config_pattern(re: ®ex::Regex, negated: bool, value: &str) -> bool {
864 let matched = re.is_match(value);
865 if negated {
866 !matched
867 } else {
868 matched
869 }
870}
871
872fn escape_value_quoted(s: &str) -> String {
873 let mut out = String::with_capacity(s.len() + 4);
874 out.push('"');
875 for ch in s.chars() {
876 match ch {
877 '"' => out.push_str("\\\""),
878 '\\' => out.push_str("\\\\"),
879 '\n' => out.push_str("\\n"),
880 '\r' => out.push_str("\\r"),
881 '\t' => out.push_str("\\t"),
882 other => out.push(other),
883 }
884 }
885 out.push('"');
886 out
887}
888
889fn format_comment_suffix(comment: Option<&str>) -> Result<String> {
898 match comment {
899 None => Ok(String::new()),
900 Some(c) => {
901 if c.contains('\n') || c.contains('\r') {
902 return Err(Error::Config(ConfigError::MultilineCommentNotAllowed));
903 }
904 let leading_blanks = c.find(|ch| ch != ' ' && ch != '\t').unwrap_or(c.len());
905 let after_blanks = &c[leading_blanks..];
906 if leading_blanks > 0 && after_blanks.starts_with('#') {
907 Ok(c.to_owned())
908 } else if c.starts_with('#') {
909 Ok(format!(" {c}"))
910 } else {
911 Ok(format!(" # {c}"))
912 }
913 }
914 }
915}
916
917impl ConfigFile {
918 pub fn parse(path: &Path, content: &str, scope: ConfigScope) -> Result<Self> {
930 let content = content.strip_prefix('\u{feff}').unwrap_or(content);
931 let raw_lines: Vec<String> = content
932 .lines()
933 .map(|l| l.strip_suffix('\r').unwrap_or(l))
934 .map(String::from)
935 .collect();
936 let mut entries = Vec::new();
937 let mut parser = Parser::new();
938
939 let mut idx = 0;
940 while idx < raw_lines.len() {
941 let start_idx = idx;
942 let line = &raw_lines[idx];
943 idx += 1;
944
945 let trimmed = line.trim();
947 if trimmed.starts_with('#') || trimmed.starts_with(';') {
948 continue;
949 }
950
951 let mut inline_remainder = None;
952 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
953 if let Some(remainder) = inline_remainder {
955 if let Some((key, value)) = parser.try_parse_entry(remainder) {
956 reject_valueless_negotiation_algorithm(
957 &key,
958 value.as_deref(),
959 path,
960 start_idx + 1,
961 )?;
962 entries.push(ConfigEntry {
963 key,
964 value,
965 scope,
966 file: Some(path.to_path_buf()),
967 line: start_idx + 1,
968 });
969 }
970 }
971 continue;
972 }
973
974 let mut logical_line = Cow::Borrowed(line.as_str());
977 while value_line_continues(&logical_line) && idx < raw_lines.len() {
978 let t = logical_line.trim_end();
980 let mut joined = t[..t.len() - 1].to_string();
981 joined.push_str(raw_lines[idx].trim_start());
983 logical_line = Cow::Owned(joined);
984 idx += 1;
985 }
986
987 let mut unclosed_quote = entry_line_value_has_unclosed_quote(&logical_line);
988 while unclosed_quote && idx < raw_lines.len() {
989 let next = raw_lines[idx].trim_start();
990 logical_line.to_mut().push_str(next);
991 idx += 1;
992 unclosed_quote = entry_line_value_has_unclosed_quote(&logical_line);
993 }
994 if unclosed_quote {
995 let file_disp = config_error_path_display(path);
996 return Err(ConfigError::BadConfigLineInFile {
997 file: file_disp,
998 line: start_idx + 1,
999 }
1000 .into());
1001 }
1002
1003 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
1004 reject_valueless_negotiation_algorithm(
1005 &key,
1006 value.as_deref(),
1007 path,
1008 start_idx + 1,
1009 )?;
1010 entries.push(ConfigEntry {
1011 key,
1012 value,
1013 scope,
1014 file: Some(path.to_path_buf()),
1015 line: start_idx + 1,
1016 });
1017 } else if logical_line.trim().is_empty() {
1018 continue;
1019 } else {
1020 let file_disp = config_error_path_display(path);
1021 let location = if path == Path::new("-") {
1022 file_disp
1023 } else {
1024 format!("file {file_disp}")
1025 };
1026 return Err(ConfigError::BadConfigLine {
1027 location,
1028 line: start_idx + 1,
1029 }
1030 .into());
1031 }
1032 }
1033
1034 Ok(Self {
1035 path: path.to_path_buf(),
1036 scope,
1037 entries,
1038 raw_lines,
1039 include_origin: ConfigIncludeOrigin::Disk,
1040 })
1041 }
1042
1043 pub fn parse_gitmodules_best_effort(
1049 path: &Path,
1050 content: &str,
1051 scope: ConfigScope,
1052 ) -> (Vec<ConfigEntry>, Option<usize>) {
1053 let raw_lines: Vec<String> = content
1054 .lines()
1055 .map(|l| l.strip_suffix('\r').unwrap_or(l))
1056 .map(String::from)
1057 .collect();
1058 let mut entries = Vec::new();
1059 let mut parser = Parser::new();
1060
1061 let mut idx = 0;
1062 while idx < raw_lines.len() {
1063 let start_idx = idx;
1064 let line = &raw_lines[idx];
1065 idx += 1;
1066
1067 let trimmed = line.trim();
1068 if trimmed.starts_with('#') || trimmed.starts_with(';') {
1069 continue;
1070 }
1071
1072 let mut inline_remainder = None;
1073 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
1074 if let Some(remainder) = inline_remainder {
1075 if let Some((key, value)) = parser.try_parse_entry(remainder) {
1076 entries.push(ConfigEntry {
1077 key,
1078 value,
1079 scope,
1080 file: Some(path.to_path_buf()),
1081 line: start_idx + 1,
1082 });
1083 }
1084 }
1085 continue;
1086 }
1087
1088 let mut logical_line = line.clone();
1089 while value_line_continues(&logical_line) && idx < raw_lines.len() {
1090 let t = logical_line.trim_end();
1091 logical_line = t[..t.len() - 1].to_string();
1092 let next = raw_lines[idx].trim_start();
1093 logical_line.push_str(next);
1094 idx += 1;
1095 }
1096
1097 while entry_line_value_has_unclosed_quote(&logical_line) && idx < raw_lines.len() {
1098 let next = raw_lines[idx].trim_start();
1099 logical_line.push_str(next);
1100 idx += 1;
1101 }
1102 if entry_line_value_has_unclosed_quote(&logical_line) {
1103 return (entries, Some(start_idx + 1));
1104 }
1105
1106 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
1107 entries.push(ConfigEntry {
1108 key,
1109 value,
1110 scope,
1111 file: Some(path.to_path_buf()),
1112 line: start_idx + 1,
1113 });
1114 }
1115 }
1116
1117 (entries, None)
1118 }
1119
1120 #[must_use]
1122 pub fn get(&self, key: &str) -> Option<String> {
1123 let canon = canonical_key(key).ok()?;
1124 self.entries
1125 .iter()
1126 .rev()
1127 .find(|e| e.key == canon)
1128 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
1129 }
1130
1131 pub fn parse_with_origin(
1133 path: &Path,
1134 content: &str,
1135 scope: ConfigScope,
1136 include_origin: ConfigIncludeOrigin,
1137 ) -> Result<Self> {
1138 let mut f = Self::parse(path, content, scope)?;
1139 f.include_origin = include_origin;
1140 Ok(f)
1141 }
1142
1143 pub fn from_git_config_parameters(path: &Path, raw: &str) -> Result<Self> {
1148 let mut entries = Vec::new();
1149 for entry in parse_config_parameters_strict(raw)? {
1150 match entry {
1151 ConfigParameter::Pair { key, value } => {
1152 let canon = canonical_key(key.trim())?;
1153 entries.push(ConfigEntry {
1154 key: canon,
1155 value,
1156 scope: ConfigScope::Command,
1157 file: None,
1158 line: 0,
1159 });
1160 }
1161 ConfigParameter::OldStyle(entry) => {
1162 if let Some((key, val)) = entry.split_once('=') {
1163 let canon = canonical_key(key.trim())?;
1164 entries.push(ConfigEntry {
1165 key: canon,
1166 value: Some(val.to_owned()),
1167 scope: ConfigScope::Command,
1168 file: None,
1169 line: 0,
1170 });
1171 } else {
1172 let canon = canonical_key(entry.trim())?;
1173 entries.push(ConfigEntry {
1174 key: canon,
1175 value: None,
1176 scope: ConfigScope::Command,
1177 file: None,
1178 line: 0,
1179 });
1180 }
1181 }
1182 }
1183 }
1184 Ok(Self {
1185 path: path.to_path_buf(),
1186 scope: ConfigScope::Command,
1187 entries,
1188 raw_lines: Vec::new(),
1189 include_origin: ConfigIncludeOrigin::CommandLine,
1190 })
1191 }
1192
1193 pub fn from_path(path: &Path, scope: ConfigScope) -> Result<Option<Self>> {
1202 match fs::read_to_string(path) {
1203 Ok(content) => Ok(Some(Self::parse(path, &content, scope)?)),
1204 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
1205 Err(e) => Err(Error::Io(e)),
1206 }
1207 }
1208
1209 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
1220 self.set_with_comment(key, value, None)
1221 }
1222
1223 pub fn set_with_comment(
1225 &mut self,
1226 key: &str,
1227 value: &str,
1228 comment: Option<&str>,
1229 ) -> Result<()> {
1230 let canon = canonical_key(key)?;
1231 if self.entries.iter().filter(|e| e.key == canon).count() > 1 {
1232 return Err(Error::Config(ConfigError::MultipleValues {
1233 key: key.to_owned(),
1234 }));
1235 }
1236 let raw_var = raw_variable_name(key);
1237 let comment_suffix = format_comment_suffix(comment)?;
1238
1239 let existing_idx = self.entries.iter().rposition(|e| e.key == canon);
1241
1242 if let Some(idx) = existing_idx {
1243 let line_idx = self.entries[idx].line - 1;
1244 let raw_line = &self.raw_lines[line_idx];
1245 if is_section_header_with_inline_entry(raw_line) {
1246 let header_only = extract_section_header(raw_line);
1248 self.raw_lines[line_idx] = header_only;
1249 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1250 self.raw_lines.insert(line_idx + 1, new_line);
1251 let content = self.raw_lines.join("\n");
1253 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1254 self.entries = reparsed.entries;
1255 self.raw_lines = reparsed.raw_lines;
1256 } else {
1257 self.raw_lines[line_idx] =
1258 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1259 self.entries[idx].value = Some(value.to_owned());
1260 }
1261 } else {
1262 let (section, subsection, _var) = split_key(&canon)?;
1264 let (raw_sec, raw_sub) = raw_section_parts(key);
1265 let section_line = self.find_or_create_section_preserving_case(
1266 §ion,
1267 subsection.as_deref(),
1268 &raw_sec,
1269 raw_sub.as_deref(),
1270 );
1271 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1272
1273 let insert_at = self.last_line_in_section(section_line) + 1;
1275 self.raw_lines.insert(insert_at, new_line);
1276
1277 let content = self.raw_lines.join("\n");
1279 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1280 self.entries = reparsed.entries;
1281 self.raw_lines = reparsed.raw_lines;
1282 }
1283
1284 Ok(())
1285 }
1286
1287 pub fn replace_all(
1292 &mut self,
1293 key: &str,
1294 value: &str,
1295 value_pattern: Option<&str>,
1296 ) -> Result<()> {
1297 self.replace_all_with_comment(key, value, value_pattern, None)
1298 }
1299
1300 pub fn replace_all_with_comment(
1305 &mut self,
1306 key: &str,
1307 value: &str,
1308 value_pattern: Option<&str>,
1309 comment: Option<&str>,
1310 ) -> Result<()> {
1311 let canon = canonical_key(key)?;
1312 let comment_suffix = format_comment_suffix(comment)?;
1313
1314 let (re, negated) = match value_pattern {
1315 Some(pat) => {
1316 let (compiled, neg) = compile_config_value_pattern(pat)?;
1317 (Some(compiled), neg)
1318 }
1319 None => (None, false),
1320 };
1321
1322 let matching_indices: Vec<usize> = self
1324 .entries
1325 .iter()
1326 .enumerate()
1327 .filter(|(_, e)| {
1328 if e.key != canon {
1329 return false;
1330 }
1331 if let Some(ref re) = re {
1332 let v = e.value.as_deref().unwrap_or("");
1333 value_matches_config_pattern(re, negated, v)
1334 } else {
1335 true
1336 }
1337 })
1338 .map(|(i, _)| i)
1339 .collect();
1340
1341 let Some(&target_idx) = matching_indices.last() else {
1342 return self.add_value_with_comment(key, value, comment);
1344 };
1345
1346 let raw_var = raw_variable_name(key);
1347
1348 let target_line_idx = self.entries[target_idx].line - 1;
1349 let raw_line = &self.raw_lines[target_line_idx];
1350 if is_section_header_with_inline_entry(raw_line) {
1351 let header = extract_section_header(raw_line);
1352 self.raw_lines[target_line_idx] = header;
1353 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1354 self.raw_lines.insert(target_line_idx + 1, new_line);
1355 } else {
1356 self.raw_lines[target_line_idx] =
1357 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1358 }
1359
1360 for &idx in matching_indices.iter().rev() {
1361 if idx == target_idx {
1362 continue;
1363 }
1364 let line_idx = self.entries[idx].line - 1;
1365 self.remove_entry_line(line_idx);
1366 }
1367
1368 let content = self.raw_lines.join("\n");
1370 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1371 self.entries = reparsed.entries;
1372 self.raw_lines = reparsed.raw_lines;
1373
1374 Ok(())
1375 }
1376
1377 pub fn count(&self, key: &str) -> Result<usize> {
1379 let canon = canonical_key(key)?;
1380 Ok(self.entries.iter().filter(|e| e.key == canon).count())
1381 }
1382
1383 fn remove_entry_line(&mut self, line_idx: usize) {
1394 if is_section_header_with_inline_entry(&self.raw_lines[line_idx]) {
1395 let header = extract_section_header(&self.raw_lines[line_idx]);
1397 self.raw_lines[line_idx] = header;
1398 } else {
1399 let mut lines_to_remove = 1;
1401 let mut check_line = self.raw_lines[line_idx].clone();
1402 while value_line_continues(&check_line)
1403 && (line_idx + lines_to_remove) < self.raw_lines.len()
1404 {
1405 check_line = self.raw_lines[line_idx + lines_to_remove].clone();
1406 lines_to_remove += 1;
1407 }
1408 for _ in 0..lines_to_remove {
1409 self.raw_lines.remove(line_idx);
1410 }
1411 }
1412 }
1413
1414 pub fn unset_last(&mut self, key: &str) -> Result<usize> {
1418 let canon = canonical_key(key)?;
1419 let last_idx = self.entries.iter().rposition(|e| e.key == canon);
1420
1421 if let Some(idx) = last_idx {
1422 let line_idx = self.entries[idx].line - 1;
1423 self.remove_entry_line(line_idx);
1424 let (section, subsection, _) = split_key(&canon)?;
1425 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
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 Ok(1)
1431 } else {
1432 Ok(0)
1433 }
1434 }
1435
1436 pub fn unset(&mut self, key: &str) -> Result<usize> {
1446 let canon = canonical_key(key)?;
1447 let line_indices: Vec<usize> = self
1448 .entries
1449 .iter()
1450 .filter(|e| e.key == canon)
1451 .map(|e| e.line - 1)
1452 .collect();
1453
1454 let count = line_indices.len();
1455 for &idx in line_indices.iter().rev() {
1457 self.remove_entry_line(idx);
1458 }
1459
1460 if count > 0 {
1461 if let Ok((section, subsection, _)) = split_key(&canon) {
1462 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
1463 }
1464 let content = self.raw_lines.join("\n");
1465 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1466 self.entries = reparsed.entries;
1467 self.raw_lines = reparsed.raw_lines;
1468 }
1469
1470 Ok(count)
1471 }
1472
1473 pub fn unset_matching(
1482 &mut self,
1483 key: &str,
1484 value_pattern: Option<&str>,
1485 preserve_empty_section_header: bool,
1486 ) -> Result<usize> {
1487 let canon = canonical_key(key)?;
1488 let pattern = match value_pattern {
1489 Some(pat) => {
1490 let (re, negated) = compile_config_value_pattern(pat)?;
1491 Some((re, negated))
1492 }
1493 None => None,
1494 };
1495
1496 let line_indices: Vec<usize> = self
1497 .entries
1498 .iter()
1499 .filter(|e| {
1500 if e.key != canon {
1501 return false;
1502 }
1503 if let Some((ref re, negated)) = pattern {
1504 let v = e.value.as_deref().unwrap_or("");
1505 value_matches_config_pattern(re, negated, v)
1506 } else {
1507 true
1508 }
1509 })
1510 .map(|e| e.line - 1)
1511 .collect();
1512
1513 let count = line_indices.len();
1514 for &idx in line_indices.iter().rev() {
1515 self.remove_entry_line(idx);
1516 }
1517
1518 if count > 0 {
1519 if !preserve_empty_section_header {
1520 let (section, subsection, _) = split_key(&canon)?;
1521 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
1522 }
1523
1524 let content = self.raw_lines.join("\n");
1525 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1526 self.entries = reparsed.entries;
1527 self.raw_lines = reparsed.raw_lines;
1528 }
1529
1530 Ok(count)
1531 }
1532
1533 pub fn remove_section(&mut self, section: &str) -> Result<bool> {
1539 let (sec_name, sub_name) = parse_section_name(section);
1540 let sec_lower = sec_name.to_lowercase();
1541
1542 let mut remove = vec![false; self.raw_lines.len()];
1543 let mut removing = false;
1544 let mut found = false;
1545 let mut parser = Parser::new();
1546
1547 for (idx, line) in self.raw_lines.iter().enumerate() {
1548 if parser.try_parse_section(line) {
1549 removing = section_matches(&parser, &sec_lower, sub_name);
1550 found |= removing;
1551 }
1552 if removing {
1553 remove[idx] = true;
1554 }
1555 }
1556
1557 if found {
1558 self.raw_lines = self
1559 .raw_lines
1560 .iter()
1561 .enumerate()
1562 .filter_map(|(idx, line)| (!remove[idx]).then_some(line.clone()))
1563 .collect();
1564 let content = self.raw_lines.join("\n");
1565 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1566 self.entries = reparsed.entries;
1567 self.raw_lines = reparsed.raw_lines;
1568 Ok(true)
1569 } else {
1570 Ok(false)
1571 }
1572 }
1573
1574 pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
1581 let (old_sec, old_sub) = parse_section_name(old_name);
1582 let (new_sec, new_sub) = parse_section_name(new_name);
1583 validate_section_name(new_name, new_sec, new_sub)?;
1584 let old_lower = old_sec.to_lowercase();
1585
1586 let mut found = false;
1587 let mut parser = Parser::new();
1588
1589 let mut idx = 0usize;
1590 while idx < self.raw_lines.len() {
1591 let line = self.raw_lines[idx].clone();
1592 let mut inline_remainder = None;
1593 if parser.try_parse_section_with_remainder(&line, &mut inline_remainder)
1594 && section_matches(&parser, &old_lower, old_sub)
1595 {
1596 let header = match new_sub {
1598 Some(sub) => format!("[{} \"{}\"]", new_sec, sub),
1599 None => format!("[{}]", new_sec),
1600 };
1601 self.raw_lines[idx] = header;
1602 if let Some(remainder) = inline_remainder {
1603 self.raw_lines
1604 .insert(idx + 1, format!("\t{}", remainder.trim()));
1605 idx += 1;
1606 }
1607 found = true;
1608 }
1609 idx += 1;
1610 }
1611
1612 if found {
1613 let content = self.raw_lines.join("\n");
1614 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1615 self.entries = reparsed.entries;
1616 self.raw_lines = reparsed.raw_lines;
1617 }
1618
1619 Ok(found)
1620 }
1621
1622 pub fn add_value(&mut self, key: &str, value: &str) -> Result<()> {
1627 self.add_value_with_comment(key, value, None)
1628 }
1629
1630 pub fn add_value_with_comment(
1632 &mut self,
1633 key: &str,
1634 value: &str,
1635 comment: Option<&str>,
1636 ) -> Result<()> {
1637 let canon = canonical_key(key)?;
1638 let raw_var = raw_variable_name(key);
1639 let comment_suffix = format_comment_suffix(comment)?;
1640 let (section, subsection, _var) = split_key(&canon)?;
1641 let (raw_sec, raw_sub) = raw_section_parts(key);
1642
1643 let section_line = self.find_or_create_section_preserving_case(
1644 §ion,
1645 subsection.as_deref(),
1646 &raw_sec,
1647 raw_sub.as_deref(),
1648 );
1649 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1650 let insert_at = self.last_line_in_section(section_line) + 1;
1651 self.raw_lines.insert(insert_at, new_line);
1652
1653 let content = self.raw_lines.join("\n");
1655 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1656 self.entries = reparsed.entries;
1657 self.raw_lines = reparsed.raw_lines;
1658
1659 Ok(())
1660 }
1661
1662 fn remove_empty_section_headers_matching(&mut self, section: &str, subsection: Option<&str>) {
1665 let (Ok(section_re), Ok(comment_re)) = (
1666 regex::Regex::new(r"^\s*\["),
1667 regex::Regex::new(r"^\s*(#|;)"),
1668 ) else {
1669 return;
1671 };
1672
1673 let mut to_remove: Vec<usize> = Vec::new();
1674 let len = self.raw_lines.len();
1675 let section_lower = section.to_lowercase();
1676 let mut parser = Parser::new();
1677
1678 for i in 0..len {
1679 let line = &self.raw_lines[i];
1680 if !section_re.is_match(line) {
1681 continue;
1682 }
1683 if !parser.try_parse_section(line)
1684 || !section_matches(&parser, §ion_lower, subsection)
1685 {
1686 continue;
1687 }
1688 if is_section_header_with_inline_entry(line) {
1690 continue;
1691 }
1692 let has_attached_leading_comment = self.raw_lines[..i]
1693 .iter()
1694 .enumerate()
1695 .rev()
1696 .find(|(_, line)| !line.trim().is_empty())
1697 .is_some_and(|(idx, line)| {
1698 comment_re.is_match(line)
1699 && idx
1700 .checked_sub(1)
1701 .is_none_or(|prev| !value_line_continues(&self.raw_lines[prev]))
1702 });
1703 if has_attached_leading_comment {
1704 continue;
1705 }
1706 let mut has_entries = false;
1709 for j in (i + 1)..len {
1710 let next = self.raw_lines[j].trim();
1711 if next.is_empty() {
1712 continue;
1713 }
1714 if section_re.is_match(&self.raw_lines[j]) {
1715 break;
1716 }
1717 if comment_re.is_match(&self.raw_lines[j]) {
1718 has_entries = true;
1720 break;
1721 }
1722 has_entries = true;
1724 break;
1725 }
1726 if !has_entries {
1727 to_remove.push(i);
1728 }
1729 }
1730
1731 for &idx in to_remove.iter().rev() {
1733 self.raw_lines.remove(idx);
1734 }
1735
1736 while self.raw_lines.last().is_some_and(|l| l.trim().is_empty()) {
1738 self.raw_lines.pop();
1739 }
1740 }
1741
1742 pub fn write(&self) -> Result<()> {
1753 let lock_path = config_lock_path(&self.path);
1754 let content = self.raw_lines.join("\n");
1755 let bytes: Vec<u8> = if content.trim().is_empty() {
1756 Vec::new()
1757 } else if content.ends_with('\n') {
1758 content.into_bytes()
1759 } else {
1760 format!("{content}\n").into_bytes()
1761 };
1762
1763 let mut lock_file = fs::OpenOptions::new()
1764 .write(true)
1765 .create_new(true)
1766 .open(&lock_path)
1767 .map_err(|e| {
1768 if e.kind() == std::io::ErrorKind::AlreadyExists {
1769 Error::Config(ConfigError::ConfigFileLocked {
1770 path: lock_path.display().to_string(),
1771 })
1772 } else {
1773 Error::Io(e)
1774 }
1775 })?;
1776
1777 #[cfg(unix)]
1778 if let Ok(meta) = fs::metadata(&self.path) {
1779 let _ = lock_file.set_permissions(meta.permissions());
1780 }
1781
1782 use std::io::Write as _;
1783 lock_file.write_all(&bytes).map_err(|e| {
1784 let _ = fs::remove_file(&lock_path);
1785 Error::Io(e)
1786 })?;
1787 drop(lock_file);
1788
1789 fs::rename(&lock_path, &self.path).map_err(|e| {
1790 let _ = fs::remove_file(&lock_path);
1791 Error::Io(e)
1792 })?;
1793 Ok(())
1794 }
1795
1796 fn find_or_create_section_preserving_case(
1799 &mut self,
1800 section: &str,
1801 subsection: Option<&str>,
1802 raw_section: &str,
1803 raw_subsection: Option<&str>,
1804 ) -> usize {
1805 let sec_lower = section.to_lowercase();
1806 let mut parser = Parser::new();
1807 let mut last_match = None;
1808
1809 for (idx, line) in self.raw_lines.iter().enumerate() {
1810 if parser.try_parse_section(line) && section_matches(&parser, &sec_lower, subsection) {
1811 last_match = Some(idx);
1812 }
1813 }
1814 if let Some(idx) = last_match {
1815 return idx;
1816 }
1817
1818 let header = match raw_subsection {
1820 Some(sub) => {
1821 let escaped = escape_subsection(sub);
1822 format!("[{} \"{}\"]", raw_section, escaped)
1823 }
1824 None => format!("[{}]", raw_section),
1825 };
1826 self.raw_lines.push(header);
1827 self.raw_lines.len() - 1
1828 }
1829
1830 fn last_line_in_section(&self, section_line: usize) -> usize {
1832 let mut last = section_line;
1833 for idx in (section_line + 1)..self.raw_lines.len() {
1834 let trimmed = self.raw_lines[idx].trim();
1835 if trimmed.starts_with('[') {
1836 break;
1837 }
1838 last = idx;
1839 }
1840 last
1841 }
1842}
1843
1844impl ConfigSet {
1847 #[must_use]
1849 pub fn new() -> Self {
1850 Self {
1851 entries: Vec::new(),
1852 }
1853 }
1854
1855 #[must_use]
1857 pub fn entries(&self) -> &[ConfigEntry] {
1858 &self.entries
1859 }
1860
1861 pub fn merge(&mut self, file: &ConfigFile) {
1866 self.entries.extend(file.entries.iter().cloned());
1867 }
1868
1869 pub fn merge_set(&mut self, other: &ConfigSet) {
1871 self.entries.extend(other.entries.iter().cloned());
1872 }
1873
1874 pub fn add_command_override(&mut self, key: &str, value: &str) -> Result<()> {
1876 let canon = canonical_key(key)?;
1877 self.entries.push(ConfigEntry {
1878 key: canon,
1879 value: Some(value.to_owned()),
1880 scope: ConfigScope::Command,
1881 file: None,
1882 line: 0,
1883 });
1884 Ok(())
1885 }
1886
1887 #[must_use]
1898 pub fn get(&self, key: &str) -> Option<String> {
1899 let canon = canonical_key(key).ok()?;
1900 self.entries
1901 .iter()
1902 .rev()
1903 .find(|e| e.key == canon)
1904 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
1905 }
1906
1907 #[must_use]
1912 pub fn get_last_entry(&self, key: &str) -> Option<ConfigEntry> {
1913 let canon = canonical_key(key).ok()?;
1914 self.entries.iter().rev().find(|e| e.key == canon).cloned()
1915 }
1916
1917 pub fn get_with_origin(&self, key: &str) -> Result<Option<ConfigValueOrigin>> {
1923 let _canon = canonical_key(key)?;
1924 Ok(self.get_last_entry(key).map(|e| ConfigValueOrigin {
1925 value: e.value.clone().or_else(|| Some("true".to_owned())),
1926 scope: e.scope,
1927 file: config_entry_origin_file(&e).map(Path::to_path_buf),
1928 line: config_entry_origin_line(&e),
1929 }))
1930 }
1931
1932 pub fn get_all_with_origin(&self, key: &str) -> Result<Vec<ConfigValueOrigin>> {
1938 let canon = canonical_key(key)?;
1939 Ok(self
1940 .entries
1941 .iter()
1942 .filter(|e| e.key == canon)
1943 .map(|e| ConfigValueOrigin {
1944 value: e.value.clone().or_else(|| Some("true".to_owned())),
1945 scope: e.scope,
1946 file: config_entry_origin_file(e).map(Path::to_path_buf),
1947 line: config_entry_origin_line(e),
1948 })
1949 .collect())
1950 }
1951
1952 #[must_use]
1954 pub fn get_all(&self, key: &str) -> Vec<String> {
1955 let canon = match canonical_key(key) {
1956 Ok(c) => c,
1957 Err(_) => return Vec::new(),
1958 };
1959 self.entries
1960 .iter()
1961 .filter(|e| e.key == canon)
1962 .map(|e| e.value.clone().unwrap_or_default())
1963 .collect()
1964 }
1965
1966 #[must_use]
1970 pub fn get_all_raw(&self, key: &str) -> Vec<Option<String>> {
1971 let canon = match canonical_key(key) {
1972 Ok(c) => c,
1973 Err(_) => return Vec::new(),
1974 };
1975 self.entries
1976 .iter()
1977 .filter(|e| e.key == canon)
1978 .map(|e| e.value.clone())
1979 .collect()
1980 }
1981
1982 #[must_use]
1987 pub fn has_key(&self, key: &str) -> bool {
1988 let Ok(canon) = canonical_key(key) else {
1989 return false;
1990 };
1991 self.entries.iter().any(|e| e.key == canon)
1992 }
1993
1994 pub fn get_bool(&self, key: &str) -> Option<std::result::Result<bool, ConfigError>> {
2008 let v = self.get(key)?;
2009 if canonical_key(key).ok().as_deref() == Some("pack.allowpackreuse") {
2010 let lower = v.trim().to_ascii_lowercase();
2011 if lower == "single" || lower == "multi" {
2012 return None;
2013 }
2014 }
2015 Some(parse_bool(&v).map_err(|e| e.for_key(key)))
2016 }
2017
2018 #[must_use]
2029 pub fn bool_or(&self, key: &str, default: bool) -> bool {
2030 self.get_bool(key)
2031 .and_then(std::result::Result::ok)
2032 .unwrap_or(default)
2033 }
2034
2035 #[must_use]
2037 pub fn ignore_case(&self) -> bool {
2038 self.bool_or("core.ignoreCase", false)
2039 }
2040
2041 #[must_use]
2046 pub fn precompose_unicode(&self) -> bool {
2047 self.bool_or("core.precomposeUnicode", false)
2048 }
2049
2050 #[must_use]
2052 pub fn preload_index(&self) -> bool {
2053 self.bool_or("core.preloadIndex", true)
2054 }
2055
2056 #[must_use]
2058 pub fn file_mode(&self) -> bool {
2059 self.bool_or("core.fileMode", true)
2060 }
2061
2062 #[must_use]
2067 pub fn quote_path_fully(&self) -> bool {
2068 self.bool_or("core.quotePath", true)
2069 }
2070
2071 #[must_use]
2073 pub fn pack_write_bitmap_hash_cache(&self) -> bool {
2074 self.bool_or("pack.writeBitmapHashCache", true)
2075 }
2076
2077 #[must_use]
2079 pub fn pack_write_bitmap_lookup_table(&self) -> bool {
2080 self.bool_or("pack.writeBitmapLookupTable", false)
2081 }
2082
2083 #[must_use]
2085 pub fn pack_prefer_bitmap_tips(&self) -> Vec<String> {
2086 let mut out = self.get_all("pack.preferbitmaptips");
2087 if out.is_empty() {
2088 out = self.get_all("pack.preferBitmapTips");
2089 }
2090 out
2091 }
2092
2093 #[must_use]
2095 pub fn pack_bitmap_write_options(&self) -> crate::pack_bitmap::PackBitmapWriteOptions {
2096 crate::pack_bitmap::PackBitmapWriteOptions {
2097 full_dag: true,
2098 hash_cache: self.pack_write_bitmap_hash_cache(),
2099 lookup_table: self.pack_write_bitmap_lookup_table(),
2100 prefer_bitmap_tips: self.pack_prefer_bitmap_tips(),
2101 verify_reachability_closure: true,
2102 reachability_roots: None,
2103 }
2104 }
2105
2106 #[must_use]
2108 pub fn pack_write_reverse_index_default(&self) -> bool {
2109 if self
2110 .get("pack.writeReverseIndex")
2111 .is_some_and(|v| v.trim().is_empty())
2112 {
2113 return false;
2114 }
2115 self.bool_or("pack.writeReverseIndex", true)
2116 }
2117
2118 #[must_use]
2120 pub fn pack_index_threads(&self) -> Option<usize> {
2121 self.get_i64("pack.threads")
2122 .and_then(|r| r.ok())
2123 .and_then(|n| usize::try_from(n).ok())
2124 }
2125
2126 #[must_use]
2130 pub fn pack_object_window(&self) -> usize {
2131 match self.get_i64("pack.window") {
2132 None => 10,
2133 Some(Ok(n)) if n >= 0 => usize::try_from(n).unwrap_or(10),
2134 Some(Ok(_)) | Some(Err(_)) => 10,
2135 }
2136 }
2137
2138 #[must_use]
2142 pub fn pack_window_memory_bytes(&self) -> u64 {
2144 match self.get_i64("pack.windowmemory") {
2145 None => 0,
2146 Some(Ok(n)) if n >= 0 => u64::try_from(n).unwrap_or(0),
2147 Some(Ok(_)) | Some(Err(_)) => 0,
2148 }
2149 }
2150
2151 pub fn pack_object_depth(&self) -> usize {
2153 match self.get_i64("pack.depth") {
2154 None => crate::pack::DEFAULT_PACK_DEPTH,
2155 Some(Ok(n)) if n >= 0 => usize::try_from(n).unwrap_or(crate::pack::DEFAULT_PACK_DEPTH),
2156 Some(Ok(_)) | Some(Err(_)) => crate::pack::DEFAULT_PACK_DEPTH,
2157 }
2158 }
2159
2160 #[must_use]
2167 pub fn gc_auto_threshold(&self) -> Option<usize> {
2168 match self.get_i64("gc.auto") {
2169 Some(Ok(n)) if n <= 0 => None,
2170 Some(Ok(n)) => Some(usize::try_from(n).unwrap_or(usize::MAX)),
2171 None | Some(Err(_)) => Some(DEFAULT_GC_AUTO_THRESHOLD),
2172 }
2173 }
2174
2175 #[must_use]
2181 pub fn gc_auto_pack_limit(&self) -> Option<usize> {
2182 match self.get_i64("gc.autoPackLimit") {
2183 Some(Ok(n)) if n <= 0 => None,
2184 Some(Ok(n)) => Some(usize::try_from(n).unwrap_or(usize::MAX)),
2185 None | Some(Err(_)) => Some(DEFAULT_GC_AUTO_PACK_LIMIT),
2186 }
2187 }
2188
2189 #[must_use]
2192 pub fn gc_auto_detach(&self) -> bool {
2193 self.bool_or("gc.autoDetach", true)
2194 }
2195
2196 #[must_use]
2199 pub fn maintenance_auto(&self) -> bool {
2200 self.bool_or("maintenance.auto", true)
2201 }
2202
2203 pub fn try_pack_index_parallelism(
2209 &self,
2210 ) -> std::result::Result<crate::parallel::Parallelism, ConfigError> {
2211 Ok(crate::parallel::ThreadPolicy::default()
2212 .try_resolve(crate::parallel::ParallelOp::IndexPack, self)?
2213 .parallelism())
2214 }
2215
2216 #[must_use]
2219 pub fn pack_index_parallelism(&self) -> crate::parallel::Parallelism {
2220 self.try_pack_index_parallelism()
2221 .unwrap_or_else(|_| crate::parallel::Parallelism::resolve(None))
2222 }
2223
2224 pub fn try_pack_index_parallelism_for_git_dir(
2230 env: &Environment,
2231 git_dir: Option<&std::path::Path>,
2232 ) -> Result<crate::parallel::Parallelism> {
2233 match git_dir {
2234 Some(d) => {
2235 let cfg = Self::load(env, Some(d), true)?;
2236 Ok(cfg.try_pack_index_parallelism()?)
2237 }
2238 None => Ok(crate::parallel::Parallelism::resolve(None)),
2239 }
2240 }
2241
2242 #[must_use]
2244 pub fn pack_index_parallelism_for_git_dir(
2245 env: &Environment,
2246 git_dir: Option<&std::path::Path>,
2247 ) -> crate::parallel::Parallelism {
2248 Self::try_pack_index_parallelism_for_git_dir(env, git_dir)
2249 .unwrap_or_else(|_| crate::parallel::Parallelism::resolve(None))
2250 }
2251
2252 #[must_use]
2254 pub fn pack_read_reverse_index_default(&self) -> bool {
2255 self.bool_or("pack.readReverseIndex", true)
2256 }
2257
2258 #[must_use]
2261 pub(crate) fn log_refs_config(&self) -> refs::LogRefsConfig {
2262 self.get("core.logAllRefUpdates")
2263 .and_then(|v| refs::LogRefsConfig::parse(&v))
2264 .unwrap_or(refs::LogRefsConfig::Unset)
2265 }
2266
2267 #[must_use]
2274 pub fn effective_log_refs_config(&self) -> refs::LogRefsConfig {
2275 match self.log_refs_config() {
2276 refs::LogRefsConfig::Unset if self.bool_or("core.bare", false) => {
2277 refs::LogRefsConfig::None
2278 }
2279 refs::LogRefsConfig::Unset => refs::LogRefsConfig::Normal,
2280 mode => mode,
2281 }
2282 }
2283
2284 pub(crate) fn from_entries(entries: Vec<ConfigEntry>) -> Self {
2286 Self { entries }
2287 }
2288
2289 pub fn get_i64(&self, key: &str) -> Option<std::result::Result<i64, ConfigError>> {
2300 self.get(key)
2301 .map(|v| parse_i64(&v).map_err(|e| e.for_key(key)))
2302 }
2303
2304 fn parse_zlib_compression_level(key: &str, raw: &str) -> Result<i32> {
2309 const Z_DEFAULT_COMPRESSION: i32 = 6;
2310 const Z_BEST_COMPRESSION: i32 = 9;
2311
2312 let bad = |reason| {
2313 Error::Config(ConfigError::BadNumericValue {
2314 key: key.to_owned(),
2315 value: raw.to_owned(),
2316 reason,
2317 })
2318 };
2319 let v = parse_git_config_int_strict(raw.trim()).map_err(|e| bad(e.into()))?;
2320 if v == -1 {
2321 return Ok(Z_DEFAULT_COMPRESSION);
2322 }
2323 if v < 0 || v > i64::from(Z_BEST_COMPRESSION) {
2324 return Err(bad(BadNumericSource::OutOfRange));
2325 }
2326 Ok(v as i32)
2327 }
2328
2329 pub fn loose_objects_zlib_level(&self) -> Result<i32> {
2340 let mut loose_level = Self::LOOSE_OBJECTS_ZLIB_DEFAULT;
2341 let mut loose_compression_seen = false;
2342 let missing_value = |key: &str| {
2343 Error::Config(ConfigError::MissingValue {
2344 key: key.to_owned(),
2345 })
2346 };
2347
2348 for e in self.entries() {
2349 match e.key.as_str() {
2350 "core.compression" => {
2351 let val = e.value.as_deref().ok_or_else(|| missing_value(&e.key))?;
2352 let level = Self::parse_zlib_compression_level(&e.key, val)?;
2353 if !loose_compression_seen {
2354 loose_level = level;
2355 }
2356 }
2357 "core.loosecompression" => {
2358 let val = e.value.as_deref().ok_or_else(|| missing_value(&e.key))?;
2359 let level = Self::parse_zlib_compression_level(&e.key, val)?;
2360 loose_level = level;
2361 loose_compression_seen = true;
2362 }
2363 _ => {}
2364 }
2365 }
2366
2367 Ok(loose_level)
2368 }
2369
2370 pub const LOOSE_OBJECTS_ZLIB_DEFAULT: i32 = 1;
2373
2374 pub const BIG_FILE_THRESHOLD_DEFAULT: u64 = 512 * 1024 * 1024;
2376
2377 pub fn big_file_threshold(&self) -> Result<u64> {
2388 const KEY: &str = "core.bigfilethreshold";
2389 let Some(entry) = self.get_last_entry(KEY) else {
2390 return Ok(Self::BIG_FILE_THRESHOLD_DEFAULT);
2391 };
2392 let raw = entry.value.as_deref().unwrap_or_default();
2393 let bad = |reason| {
2394 Error::Config(ConfigError::BadNumericValue {
2395 key: KEY.to_owned(),
2396 value: raw.to_owned(),
2397 reason,
2398 })
2399 };
2400 let value = parse_git_config_int_strict(raw.trim()).map_err(|e| bad(e.into()))?;
2401 u64::try_from(value).map_err(|_| bad(BadNumericSource::OutOfRange))
2402 }
2403
2404 pub fn pack_objects_zlib_level(&self) -> Result<i32> {
2412 const Z_DEFAULT_COMPRESSION: i32 = 6;
2413
2414 let mut pack_level = Z_DEFAULT_COMPRESSION;
2416 let mut pack_compression_seen = false;
2417
2418 for e in self.entries() {
2419 match e.key.as_str() {
2420 "core.compression" => {
2421 let Some(val) = e.value.as_deref() else {
2422 continue;
2423 };
2424 let level = Self::parse_zlib_compression_level(&e.key, val)?;
2425 if !pack_compression_seen {
2426 pack_level = level;
2427 }
2428 }
2429 "pack.compression" => {
2430 let Some(val) = e.value.as_deref() else {
2431 continue;
2432 };
2433 pack_level = Self::parse_zlib_compression_level(&e.key, val)?;
2434 pack_compression_seen = true;
2435 }
2436 _ => {}
2437 }
2438 }
2439
2440 Ok(pack_level)
2441 }
2442
2443 pub fn get_regexp(&self, pattern: &str) -> std::result::Result<Vec<&ConfigEntry>, String> {
2448 let re = regex::Regex::new(pattern).map_err(|e| format!("invalid key pattern: {e}"))?;
2449 Ok(self
2450 .entries
2451 .iter()
2452 .filter(|e| re.is_match(&e.key))
2453 .collect())
2454 }
2455
2456 pub fn load(env: &Environment, git_dir: Option<&Path>, include_system: bool) -> Result<Self> {
2467 let opts = LoadConfigOptions {
2468 include_system,
2469 include_ctx: IncludeContext {
2470 git_dir: git_dir.map(PathBuf::from),
2471 cwd: env.cwd.clone(),
2472 pwd: env.pwd.clone(),
2473 env: std::sync::Arc::new(env.clone()),
2474 ..Default::default()
2475 },
2476 ..Default::default()
2477 };
2478 Self::load_with_options(env, git_dir, &opts)
2479 }
2480
2481 pub fn load_with_options(
2488 env: &Environment,
2489 git_dir: Option<&Path>,
2490 opts: &LoadConfigOptions,
2491 ) -> Result<Self> {
2492 Self::load_with_options_uncached(env, git_dir, opts, &mut Vec::new())
2493 }
2494
2495 fn read_local_config_text_optional(
2496 path: &Path,
2497 caches: Option<&std::sync::Arc<crate::repo_caches::RepoCaches>>,
2498 ) -> Result<Option<String>> {
2499 let read = |p: &Path| -> Result<Option<String>> {
2500 match fs::read_to_string(p) {
2501 Ok(text) => Ok(Some(text)),
2502 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
2503 Err(e) => Err(Error::Io(e)),
2504 }
2505 };
2506 let Some(c) = caches else {
2507 return read(path);
2508 };
2509 match c.local_config_text(path) {
2510 Ok(text) => Ok(Some(text)),
2511 Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
2512 Err(e) => Err(e),
2513 }
2514 }
2515
2516 pub(crate) fn load_with_options_uncached(
2517 env: &Environment,
2518 git_dir: Option<&Path>,
2519 opts: &LoadConfigOptions,
2520 included_files: &mut Vec<PathBuf>,
2521 ) -> Result<Self> {
2522 let proc = opts.process_includes;
2523 let mut ctx = opts.include_ctx.clone();
2524 if ctx.cwd.as_os_str().is_empty() {
2525 ctx.cwd = env.cwd.clone();
2526 }
2527 ctx.pwd = env.pwd.clone();
2528 ctx.env = std::sync::Arc::new(env.clone());
2529 let mut layers = Vec::new();
2530
2531 if opts.include_system && !git_config_nosystem_enabled(env) {
2532 let system_path = system_config_path(env);
2533 if let Some(f) = ConfigFile::from_path(&system_path, ConfigScope::System)? {
2534 layers.push(ConfigLayer::file(f));
2535 }
2536 }
2537
2538 for path in global_config_paths(env) {
2540 if let Some(f) = ConfigFile::from_path(&path, ConfigScope::Global)? {
2541 layers.push(ConfigLayer::file(f));
2542 }
2543 }
2544
2545 if let Some(gd) = git_dir {
2547 let common_dir = crate::repo::common_git_dir_for_config(gd);
2548 let local_path = common_dir.join("config");
2549 let local_text =
2550 Self::read_local_config_text_optional(&local_path, opts.repo_caches.as_ref())?;
2551 if let Some(msg) =
2552 crate::repo::early_config_ignore_repo_reason_for_text(local_text.as_deref())
2553 {
2554 if let Some(sink) = opts.diagnostics.as_ref() {
2555 sink.warn(crate::diagnostics::Warning::IgnoredGitDir {
2556 path: gd.to_path_buf(),
2557 reason: msg,
2558 });
2559 }
2560 } else if let Some(text) = local_text.as_deref() {
2561 let f = ConfigFile::parse(&local_path, text, ConfigScope::Local)?;
2562 layers.push(ConfigLayer::file(f));
2563 }
2564
2565 let wt_path = gd.join("config.worktree");
2568 if crate::repo::worktree_config_enabled_for_text(local_text.as_deref()) {
2569 if let Some(f) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree)? {
2570 layers.push(ConfigLayer::file(f));
2571 }
2572 }
2573 }
2574
2575 if let Some(path) = env.git_config.as_deref() {
2576 let path = resolve_config_override_path(env, path);
2577 if let Some(f) = ConfigFile::from_path(&path, ConfigScope::Command)? {
2578 layers.push(ConfigLayer::file(f));
2579 }
2580 }
2581
2582 layers.push(ConfigLayer::Entries(environment_config_pair_entries(env)?));
2583
2584 if let Some(params) = env
2586 .git_config_parameters
2587 .as_deref()
2588 .filter(|p| !p.trim().is_empty())
2589 {
2590 if proc && opts.command_includes {
2591 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
2592 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, params)?;
2593 layers.push(ConfigLayer::file(cmd_file));
2594 } else {
2595 layers.push(ConfigLayer::Entries(command_override_entries(params)));
2596 }
2597 }
2598
2599 let mut set = Self::new();
2600 set.merge_layers(&layers, proc, &ctx, included_files)?;
2601 Ok(set)
2602 }
2603
2604 pub fn read_early_config(
2616 env: &Environment,
2617 git_dir: Option<&Path>,
2618 key: &str,
2619 ) -> Result<Vec<String>> {
2620 let ctx = IncludeContext {
2621 git_dir: git_dir.map(PathBuf::from),
2622 command_line_relative_include_is_error: false,
2623 cwd: env.cwd.clone(),
2624 pwd: env.pwd.clone(),
2625 env: std::sync::Arc::new(env.clone()),
2626 };
2627 let mut layers = Vec::new();
2628
2629 if !git_config_nosystem_enabled(env) {
2630 let system_path = system_config_path(env);
2631 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2632 layers.push(ConfigLayer::file(f));
2633 }
2634 }
2635
2636 for path in global_config_paths(env) {
2638 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2639 layers.push(ConfigLayer::file(f));
2640 }
2641 }
2642
2643 if let Some(gd) = git_dir {
2644 let common_dir = crate::repo::common_git_dir_for_config(gd);
2645 let local_path = common_dir.join("config");
2647 if crate::repo::early_config_ignore_repo_reason(&common_dir).is_some() {
2648 } else if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2650 layers.push(ConfigLayer::file(f));
2651 }
2652
2653 let wt_path = gd.join("config.worktree");
2655 if crate::repo::worktree_config_enabled(&common_dir) {
2656 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
2657 layers.push(ConfigLayer::file(f));
2658 }
2659 }
2660 }
2661
2662 if let Some(params) = env
2664 .git_config_parameters
2665 .as_deref()
2666 .filter(|p| !p.trim().is_empty())
2667 {
2668 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
2669 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, params)?;
2670 layers.push(ConfigLayer::file(cmd_file));
2671 }
2672
2673 let mut set = Self::new();
2674 set.merge_layers(&layers, true, &ctx, &mut Vec::new())?;
2675 Ok(set.get_all(key))
2676 }
2677
2678 pub fn merge_file_with_includes(
2691 &mut self,
2692 file: &ConfigFile,
2693 process_includes: bool,
2694 ctx: &IncludeContext,
2695 ) -> Result<()> {
2696 self.merge_layers(
2697 &[ConfigLayer::File(Cow::Borrowed(file))],
2698 process_includes,
2699 ctx,
2700 &mut Vec::new(),
2701 )
2702 }
2703
2704 pub fn load_repo_local_only(git_dir: &Path) -> Result<Self> {
2710 let local_path = git_dir.join("config");
2711 let ctx = IncludeContext {
2712 git_dir: Some(git_dir.to_path_buf()),
2713 command_line_relative_include_is_error: false,
2714 cwd: PathBuf::from("."),
2715 pwd: None,
2716 env: std::sync::Arc::new(Environment::empty()),
2717 };
2718 let mut set = Self::new();
2719 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2720 set.merge_layers(&[ConfigLayer::file(f)], true, &ctx, &mut Vec::new())?;
2721 }
2722 Ok(set)
2723 }
2724
2725 pub fn load_protected(env: &Environment, include_system: bool) -> Result<Self> {
2736 let ctx = IncludeContext {
2737 git_dir: None,
2738 command_line_relative_include_is_error: false,
2739 cwd: env.cwd.clone(),
2740 pwd: env.pwd.clone(),
2741 env: std::sync::Arc::new(env.clone()),
2742 };
2743 let mut layers = Vec::new();
2744
2745 if include_system && !git_config_nosystem_enabled(env) {
2746 let system_path = system_config_path(env);
2747 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2748 layers.push(ConfigLayer::file(f));
2749 }
2750 }
2751
2752 for path in global_config_paths(env) {
2753 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2754 layers.push(ConfigLayer::file(f));
2755 }
2756 }
2757
2758 layers.push(ConfigLayer::Entries(environment_config_pair_entries(env)?));
2759 if let Some(params) = env.git_config_parameters.as_deref() {
2760 layers.push(ConfigLayer::Entries(command_override_entries(params)));
2761 }
2762
2763 let mut set = Self::new();
2764 set.merge_layers(&layers, true, &ctx, &mut Vec::new())?;
2765 Ok(set)
2766 }
2767
2768 fn merge_layers(
2775 &mut self,
2776 layers: &[ConfigLayer<'_>],
2777 process_includes: bool,
2778 ctx: &IncludeContext,
2779 included_files: &mut Vec<PathBuf>,
2780 ) -> Result<()> {
2781 const INCLUDE_HEADROOM: usize = 64;
2784 let mut remote_urls = RemoteUrls::new(layers);
2785 let top_level: usize = layers
2786 .iter()
2787 .map(|layer| match layer {
2788 ConfigLayer::File(file) => file.entries.len(),
2789 ConfigLayer::Entries(entries) => entries.len(),
2790 })
2791 .sum();
2792 self.entries.reserve(top_level + INCLUDE_HEADROOM);
2793 for layer in layers {
2794 match layer {
2795 ConfigLayer::File(file) => Self::merge_with_includes(
2796 self,
2797 file,
2798 process_includes,
2799 0,
2800 ctx,
2801 &mut remote_urls,
2802 included_files,
2803 )?,
2804 ConfigLayer::Entries(entries) => self.entries.extend(entries.iter().cloned()),
2805 }
2806 }
2807 Ok(())
2808 }
2809
2810 fn merge_with_includes(
2812 set: &mut Self,
2813 file: &ConfigFile,
2814 process_includes: bool,
2815 depth: usize,
2816 ctx: &IncludeContext,
2817 remote_urls: &mut RemoteUrls<'_>,
2818 included_files: &mut Vec<PathBuf>,
2819 ) -> Result<()> {
2820 check_include_depth(depth)?;
2821 if !process_includes {
2822 set.merge(file);
2823 return Ok(());
2824 }
2825
2826 for entry in &file.entries {
2827 set.entries.push(entry.clone());
2828
2829 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
2830 continue;
2831 };
2832 if let Some(cond) = condition.as_deref() {
2833 let holds = match hasconfig_remote_url_pattern(cond) {
2834 Some(pattern) => remote_urls.any_matches(pattern, ctx, included_files)?,
2835 None => evaluate_include_condition(cond, file, ctx),
2836 };
2837 if !holds {
2838 continue;
2839 }
2840 }
2841 let Some(inc_file) = open_include_target(&inc_path, file, ctx, included_files)? else {
2842 continue;
2843 };
2844 Self::merge_with_includes(
2845 set,
2846 &inc_file,
2847 process_includes,
2848 depth + 1,
2849 ctx,
2850 remote_urls,
2851 included_files,
2852 )?;
2853 }
2854
2855 Ok(())
2856 }
2857}
2858
2859enum ConfigLayer<'a> {
2861 File(Cow<'a, ConfigFile>),
2863 Entries(Vec<ConfigEntry>),
2865}
2866
2867impl ConfigLayer<'_> {
2868 fn file(file: ConfigFile) -> Self {
2869 Self::File(Cow::Owned(file))
2870 }
2871}
2872
2873struct RemoteUrls<'a> {
2876 layers: &'a [ConfigLayer<'a>],
2877 urls: Option<Vec<String>>,
2878}
2879
2880impl<'a> RemoteUrls<'a> {
2881 fn new(layers: &'a [ConfigLayer<'a>]) -> Self {
2882 Self { layers, urls: None }
2883 }
2884
2885 fn any_matches(
2887 &mut self,
2888 pattern: &str,
2889 ctx: &IncludeContext,
2890 included_files: &mut Vec<PathBuf>,
2891 ) -> Result<bool> {
2892 let urls = match self.urls.take() {
2893 Some(urls) => urls,
2894 None => self.collect(ctx, included_files)?,
2895 };
2896 let hit = urls
2897 .iter()
2898 .any(|url| wildmatch(pattern.as_bytes(), url.as_bytes(), WM_PATHNAME));
2899 self.urls = Some(urls);
2900 Ok(hit)
2901 }
2902
2903 fn collect(
2904 &self,
2905 ctx: &IncludeContext,
2906 included_files: &mut Vec<PathBuf>,
2907 ) -> Result<Vec<String>> {
2908 let mut urls = Vec::new();
2909 for layer in self.layers {
2910 match layer {
2911 ConfigLayer::File(file) => {
2912 collect_remote_urls(file, false, 0, ctx, &mut urls, included_files)?;
2913 }
2914 ConfigLayer::Entries(entries) => urls.extend(
2915 entries
2916 .iter()
2917 .filter(|entry| is_remote_url_entry(entry))
2918 .filter_map(|entry| entry.value.clone()),
2919 ),
2920 }
2921 }
2922 Ok(urls)
2923 }
2924}
2925
2926fn collect_remote_urls(
2933 file: &ConfigFile,
2934 in_conditional_include: bool,
2935 depth: usize,
2936 ctx: &IncludeContext,
2937 urls: &mut Vec<String>,
2938 included_files: &mut Vec<PathBuf>,
2939) -> Result<()> {
2940 check_include_depth(depth)?;
2941 for entry in &file.entries {
2942 if is_remote_url_entry(entry) {
2943 if in_conditional_include {
2944 return Err(ConfigError::RemoteUrlInHasconfigInclude.into());
2945 }
2946 urls.extend(entry.value.clone());
2947 continue;
2948 }
2949 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
2950 continue;
2951 };
2952 let conditional = match condition.as_deref() {
2953 None => in_conditional_include,
2954 Some(cond) => {
2955 if hasconfig_remote_url_pattern(cond).is_none()
2956 && !evaluate_include_condition(cond, file, ctx)
2957 {
2958 continue;
2959 }
2960 true
2961 }
2962 };
2963 let Some(inc_file) = open_include_target(&inc_path, file, ctx, included_files)? else {
2964 continue;
2965 };
2966 collect_remote_urls(&inc_file, conditional, depth + 1, ctx, urls, included_files)?;
2967 }
2968 Ok(())
2969}
2970
2971fn check_include_depth(depth: usize) -> Result<()> {
2973 const MAX_INCLUDE_DEPTH: usize = 10;
2974 if depth > MAX_INCLUDE_DEPTH {
2975 return Err(ConfigError::IncludeDepthExceeded {
2976 depth,
2977 limit: MAX_INCLUDE_DEPTH,
2978 }
2979 .into());
2980 }
2981 Ok(())
2982}
2983
2984fn open_include_target(
2990 inc_path: &str,
2991 file: &ConfigFile,
2992 ctx: &IncludeContext,
2993 included_files: &mut Vec<PathBuf>,
2994) -> Result<Option<ConfigFile>> {
2995 let Some(resolved) = resolve_include_file_path(inc_path, file, ctx)? else {
2996 return Ok(None);
2997 };
2998 included_files.push(resolved.clone());
2999 ConfigFile::from_path(&resolved, file.scope)
3000}
3001
3002fn include_directive_for_entry(entry: &ConfigEntry) -> Option<(String, Option<String>)> {
3003 let val = entry.value.as_ref()?;
3004 if entry.key == "include.path" {
3005 return Some((val.clone(), None));
3006 }
3007 if entry.key.starts_with("includeif.") && entry.key.ends_with(".path") {
3008 let mid = &entry.key["includeif.".len()..entry.key.len() - ".path".len()];
3009 return Some((val.clone(), Some(mid.to_owned())));
3010 }
3011 None
3012}
3013
3014pub(crate) type ConfigFileStamp = (PathBuf, Option<(SystemTime, u64)>);
3043
3044pub(crate) type ConfigCacheEntryStamps = (
3046 Vec<(String, Option<String>)>,
3047 Vec<ConfigFileStamp>,
3048 Vec<ConfigFileStamp>,
3049);
3050
3051#[derive(Clone, PartialEq, Eq, Hash)]
3052pub(crate) struct ConfigCacheKey {
3053 pub(crate) git_dir: Option<PathBuf>,
3054 include_system: bool,
3055 process_includes: bool,
3056 command_includes: bool,
3057 ctx_git_dir: Option<PathBuf>,
3058 ctx_relative_include_is_error: bool,
3059}
3060
3061impl ConfigCacheKey {
3062 pub fn new(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Self {
3063 Self {
3064 git_dir: git_dir.map(Path::to_path_buf),
3065 include_system: opts.include_system,
3066 process_includes: opts.process_includes,
3067 command_includes: opts.command_includes,
3068 ctx_git_dir: opts.include_ctx.git_dir.clone(),
3069 ctx_relative_include_is_error: opts.include_ctx.command_line_relative_include_is_error,
3070 }
3071 }
3072}
3073
3074pub(crate) struct ConfigCacheEntry {
3075 pub(crate) env_fingerprint: Vec<(String, Option<String>)>,
3076 pub(crate) base_stamps: Vec<ConfigFileStamp>,
3078 pub(crate) extra_stamps: Vec<ConfigFileStamp>,
3081 pub(crate) set: Arc<ConfigSet>,
3082}
3083
3084fn config_cascade_file_paths(
3087 env: &Environment,
3088 git_dir: Option<&Path>,
3089 opts: &LoadConfigOptions,
3090) -> Vec<PathBuf> {
3091 let mut paths = Vec::new();
3092 if opts.include_system && !git_config_nosystem_enabled(env) {
3093 paths.push(system_config_path(env));
3094 }
3095 paths.extend(global_config_paths(env));
3096 if let Some(gd) = git_dir {
3097 let common_dir = crate::repo::common_git_dir_for_config(gd);
3098 paths.push(common_dir.join("config"));
3099 paths.push(gd.join("config.worktree"));
3102 paths.push(gd.join("HEAD"));
3105 }
3106 if let Some(cgd) = &opts.include_ctx.git_dir {
3107 if Some(cgd.as_path()) != git_dir {
3108 paths.push(cgd.join("HEAD"));
3109 }
3110 }
3111 if let Some(p) = env.git_config.as_deref() {
3112 paths.push(resolve_config_override_path(env, p));
3113 }
3114 paths
3115}
3116
3117pub(crate) fn stamp_for_path(path: &Path) -> Option<(SystemTime, u64)> {
3118 fs::metadata(path)
3119 .ok()
3120 .and_then(|m| Some((m.modified().ok()?, m.len())))
3121}
3122
3123pub(crate) fn stamp_paths(paths: Vec<PathBuf>) -> Vec<ConfigFileStamp> {
3124 paths
3125 .into_iter()
3126 .map(|path| {
3127 let stamp = stamp_for_path(&path);
3128 (path, stamp)
3129 })
3130 .collect()
3131}
3132
3133pub(crate) fn config_file_stamps(
3134 env: &Environment,
3135 git_dir: Option<&Path>,
3136 opts: &LoadConfigOptions,
3137) -> Vec<ConfigFileStamp> {
3138 stamp_paths(config_cascade_file_paths(env, git_dir, opts))
3139}
3140
3141pub(crate) fn config_cache_lookup(
3142 cache: &HashMap<ConfigCacheKey, ConfigCacheEntry>,
3143 key: &ConfigCacheKey,
3144 env_fp: &[(String, Option<String>)],
3145 base_stamps: &[ConfigFileStamp],
3146) -> Option<Arc<ConfigSet>> {
3147 let entry = cache.get(key)?;
3148 if entry.env_fingerprint.as_slice() != env_fp || entry.base_stamps.as_slice() != base_stamps {
3149 return None;
3150 }
3151 for (path, stamp) in &entry.extra_stamps {
3152 if stamp_for_path(path) != *stamp {
3153 return None;
3154 }
3155 }
3156 Some(Arc::clone(&entry.set))
3157}
3158
3159pub(crate) fn config_cache_store(
3160 cache: &mut HashMap<ConfigCacheKey, ConfigCacheEntry>,
3161 key: ConfigCacheKey,
3162 env_fingerprint: Vec<(String, Option<String>)>,
3163 base_stamps: Vec<ConfigFileStamp>,
3164 extra_stamps: Vec<ConfigFileStamp>,
3165 set: Arc<ConfigSet>,
3166) {
3167 cache.insert(
3168 key,
3169 ConfigCacheEntry {
3170 env_fingerprint,
3171 base_stamps,
3172 extra_stamps,
3173 set,
3174 },
3175 );
3176}
3177
3178fn git_config_nosystem_enabled(env: &Environment) -> bool {
3179 env.git_config_nosystem
3180 .as_deref()
3181 .map(|value| parse_bool(value).unwrap_or(true))
3182 .unwrap_or(false)
3183}
3184
3185fn environment_config_pair_entries(env: &Environment) -> Result<Vec<ConfigEntry>> {
3187 let Some(count_str) = env.git_config_count.as_deref() else {
3188 return Ok(Vec::new());
3189 };
3190 if count_str.is_empty() {
3191 return Ok(Vec::new());
3192 }
3193
3194 let count = count_str
3195 .parse::<usize>()
3196 .ok()
3197 .filter(|&n| n <= i32::MAX as usize)
3198 .ok_or_else(|| {
3199 Error::Config(ConfigError::InvalidEnvironmentCount {
3200 value: count_str.to_owned(),
3201 })
3202 })?;
3203
3204 let mut set = ConfigSet::new();
3205 for i in 0..count {
3206 let (key, value) = env.git_config_pairs.get(i).ok_or(Error::Config(
3207 ConfigError::MissingEnvironmentKey { index: i },
3208 ))?;
3209 set.add_command_override(key, value)?;
3210 }
3211 Ok(set.entries)
3212}
3213
3214fn command_override_entries(params: &str) -> Vec<ConfigEntry> {
3217 let mut set = ConfigSet::new();
3218 for entry in parse_config_parameters(params) {
3219 let (key, val) = entry
3220 .split_once('\u{1}')
3221 .or_else(|| entry.split_once('='))
3222 .unwrap_or((entry.as_str(), "true"));
3223 let _ = set.add_command_override(key.trim(), val);
3224 }
3225 set.entries
3226}
3227
3228pub fn parse_bool(s: &str) -> std::result::Result<bool, ConfigValueError> {
3245 match s.to_lowercase().as_str() {
3246 "true" | "yes" | "on" => Ok(true),
3247 "" => Ok(false),
3248 "false" | "no" | "off" => Ok(false),
3249 _ => parse_git_config_maybe_bool(s).ok_or_else(|| ConfigValueError::InvalidBool {
3250 value: s.to_owned(),
3251 }),
3252 }
3253}
3254
3255pub fn parse_i64(s: &str) -> std::result::Result<i64, ConfigValueError> {
3263 let s = s.trim();
3264 if s.is_empty() {
3265 return Err(ConfigValueError::EmptyInteger);
3266 }
3267
3268 let (num_str, multiplier) = match s.as_bytes().last() {
3269 Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
3270 Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
3271 Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
3272 _ => (s, 1_i64),
3273 };
3274
3275 let base: i64 = num_str
3276 .parse()
3277 .map_err(|_| ConfigValueError::InvalidInteger {
3278 value: s.to_owned(),
3279 })?;
3280 base.checked_mul(multiplier)
3281 .ok_or_else(|| ConfigValueError::IntegerOverflow {
3282 value: s.to_owned(),
3283 })
3284}
3285
3286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3288pub enum GitConfigIntStrictError {
3289 InvalidUnit,
3291 OutOfRange,
3293}
3294
3295pub fn parse_git_config_int_strict(raw: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
3299 let s = raw.trim();
3300 if s.is_empty() {
3301 return Err(GitConfigIntStrictError::InvalidUnit);
3302 }
3303
3304 let bytes = s.as_bytes();
3305 let mut idx = 0usize;
3306 if matches!(bytes.first(), Some(b'+') | Some(b'-')) {
3307 idx = 1;
3308 }
3309 if idx >= bytes.len() {
3310 return Err(GitConfigIntStrictError::InvalidUnit);
3311 }
3312 let digit_start = idx;
3313 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
3314 idx += 1;
3315 }
3316 if idx == digit_start {
3317 return Err(GitConfigIntStrictError::InvalidUnit);
3318 }
3319
3320 let num_part =
3321 std::str::from_utf8(&bytes[..idx]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3322 let suffix =
3323 std::str::from_utf8(&bytes[idx..]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3324 let mult: i64 = match suffix {
3325 "" => 1,
3326 "k" | "K" => 1024,
3327 "m" | "M" => 1024 * 1024,
3328 "g" | "G" => 1024_i64
3329 .checked_mul(1024)
3330 .and_then(|x| x.checked_mul(1024))
3331 .ok_or(GitConfigIntStrictError::OutOfRange)?,
3332 _ => return Err(GitConfigIntStrictError::InvalidUnit),
3333 };
3334
3335 let val: i64 = num_part
3336 .parse()
3337 .map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3338 val.checked_mul(mult)
3339 .ok_or(GitConfigIntStrictError::OutOfRange)
3340}
3341
3342fn is_c_locale_space(b: u8) -> bool {
3343 matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
3344}
3345
3346fn skip_c_locale_leading_whitespace(s: &str) -> &str {
3347 let bytes = s.as_bytes();
3348 let mut i = 0usize;
3349 while i < bytes.len() && is_c_locale_space(bytes[i]) {
3350 i += 1;
3351 }
3352 &s[i..]
3353}
3354
3355fn git_unit_factor(suffix: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
3356 match suffix {
3357 "" => Ok(1),
3358 "k" | "K" => Ok(1024),
3359 "m" | "M" => Ok(1024 * 1024),
3360 "g" | "G" => Ok(1024_i64 * 1024 * 1024),
3361 _ => Err(GitConfigIntStrictError::InvalidUnit),
3362 }
3363}
3364
3365fn parse_base0_signed_prefix(s: &str) -> std::result::Result<(i64, &str), GitConfigIntStrictError> {
3367 let bytes = s.as_bytes();
3368 if bytes.is_empty() {
3369 return Err(GitConfigIntStrictError::InvalidUnit);
3370 }
3371 let mut i = 0usize;
3372 let mut sign: i64 = 1;
3373 if bytes[i] == b'+' {
3374 i += 1;
3375 } else if bytes[i] == b'-' {
3376 sign = -1;
3377 i += 1;
3378 }
3379 if i >= bytes.len() {
3380 return Err(GitConfigIntStrictError::InvalidUnit);
3381 }
3382
3383 let base = if bytes[i] == b'0' {
3384 if i + 1 < bytes.len() && matches!(bytes[i + 1], b'x' | b'X') {
3385 i += 2;
3386 if i >= bytes.len() {
3387 return Err(GitConfigIntStrictError::InvalidUnit);
3388 }
3389 16
3390 } else {
3391 8
3392 }
3393 } else {
3394 10
3395 };
3396
3397 let digit_start = i;
3398 let mut acc: i128 = 0;
3399 while i < bytes.len() {
3400 let ch = char::from(bytes[i]);
3401 let digit = match base {
3402 10 => ch.to_digit(10),
3403 8 => ch.to_digit(8),
3404 16 => ch.to_digit(16),
3405 _ => None,
3406 };
3407 let Some(d) = digit else {
3408 break;
3409 };
3410 acc = acc
3411 .checked_mul(base as i128)
3412 .and_then(|v| v.checked_add(i128::from(d)))
3413 .ok_or(GitConfigIntStrictError::OutOfRange)?;
3414 i += 1;
3415 }
3416 if i == digit_start {
3417 return Err(GitConfigIntStrictError::InvalidUnit);
3418 }
3419
3420 let signed = if sign < 0 { -acc } else { acc };
3421 let val = i64::try_from(signed).map_err(|_| GitConfigIntStrictError::OutOfRange)?;
3422 Ok((val, &s[i..]))
3423}
3424
3425pub fn parse_git_signed_config(
3427 raw: &str,
3428 max: i64,
3429) -> std::result::Result<i64, GitConfigIntStrictError> {
3430 if max < 0 {
3431 return Err(GitConfigIntStrictError::InvalidUnit);
3432 }
3433 let s = skip_c_locale_leading_whitespace(raw);
3434 if s.is_empty() {
3435 return Err(GitConfigIntStrictError::InvalidUnit);
3436 }
3437
3438 let (val, rest) = parse_base0_signed_prefix(s)?;
3439 let factor = git_unit_factor(rest)?;
3440
3441 if val < 0 {
3442 let min = -max - 1;
3443 if min / factor > val {
3444 return Err(GitConfigIntStrictError::OutOfRange);
3445 }
3446 } else if val > 0 && max / factor < val {
3447 return Err(GitConfigIntStrictError::OutOfRange);
3448 }
3449
3450 val.checked_mul(factor)
3451 .ok_or(GitConfigIntStrictError::OutOfRange)
3452}
3453
3454pub fn parse_git_config_int(raw: &str) -> std::result::Result<i32, GitConfigIntStrictError> {
3456 let max = i32::MAX as i64;
3457 let n = parse_git_signed_config(raw, max)?;
3458 i32::try_from(n).map_err(|_| GitConfigIntStrictError::OutOfRange)
3459}
3460
3461pub fn parse_git_config_maybe_bool(raw: &str) -> Option<bool> {
3463 if raw.is_empty() {
3464 return Some(false);
3465 }
3466 let lower = raw.to_ascii_lowercase();
3467 match lower.as_str() {
3468 "true" | "yes" | "on" => Some(true),
3469 "false" | "no" | "off" => Some(false),
3470 _ => parse_git_config_int(raw).ok().map(|n| n != 0),
3471 }
3472}
3473
3474const DIFF_CONTEXT_KEY: &str = "diff.context";
3475
3476const DEFAULT_GC_AUTO_THRESHOLD: usize = 6700;
3478const DEFAULT_GC_AUTO_PACK_LIMIT: usize = 50;
3480
3481fn bad_numeric_diff_context(
3482 value: &str,
3483 err: GitConfigIntStrictError,
3484 entry: &ConfigEntry,
3485) -> ConfigError {
3486 let reason = match err {
3487 GitConfigIntStrictError::InvalidUnit => crate::error::BadNumericSource::InvalidUnit,
3488 GitConfigIntStrictError::OutOfRange => crate::error::BadNumericSource::OutOfRange,
3489 };
3490 if entry.scope == ConfigScope::Command || entry.file.is_none() {
3491 ConfigError::BadNumericValue {
3492 key: DIFF_CONTEXT_KEY.to_owned(),
3493 value: value.to_owned(),
3494 reason,
3495 }
3496 } else {
3497 let path = entry
3498 .file
3499 .as_deref()
3500 .map(config_error_path_display)
3501 .unwrap_or_default();
3502 ConfigError::BadNumericValueInFile {
3503 key: DIFF_CONTEXT_KEY.to_owned(),
3504 value: value.to_owned(),
3505 file: path,
3506 reason,
3507 }
3508 }
3509}
3510
3511fn bad_diff_context_variable(entry: &ConfigEntry) -> ConfigError {
3512 if entry.scope == ConfigScope::Command || entry.file.is_none() {
3513 ConfigError::UnableToParseFromCommandLine {
3514 key: DIFF_CONTEXT_KEY.to_owned(),
3515 }
3516 } else {
3517 let path = entry
3518 .file
3519 .as_deref()
3520 .map(config_error_path_display)
3521 .unwrap_or_default();
3522 ConfigError::BadConfigVariableAtLine {
3523 key: DIFF_CONTEXT_KEY.to_owned(),
3524 file: path,
3525 line: entry.line,
3526 }
3527 }
3528}
3529
3530pub fn resolve_diff_context_lines(cfg: &ConfigSet) -> Result<Option<usize>> {
3535 let Some(entry) = cfg.get_last_entry(DIFF_CONTEXT_KEY) else {
3536 return Ok(None);
3537 };
3538 let value_src = entry.value.as_deref().unwrap_or("").trim();
3539 match parse_git_config_int_strict(value_src) {
3540 Ok(n) if n < 0 => Err(bad_diff_context_variable(&entry).into()),
3541 Ok(n) => Ok(Some(usize::try_from(n).map_err(|_| {
3542 bad_numeric_diff_context(value_src, GitConfigIntStrictError::OutOfRange, &entry)
3543 })?)),
3544 Err(e) => Err(bad_numeric_diff_context(value_src, e, &entry).into()),
3545 }
3546}
3547
3548pub fn parse_color(s: &str) -> std::result::Result<String, ConfigValueError> {
3560 const COLOR_BACKGROUND_OFFSET: i32 = 10;
3561 const COLOR_FOREGROUND_ANSI: i32 = 30;
3562 const COLOR_FOREGROUND_RGB: i32 = 38;
3563 const COLOR_FOREGROUND_256: i32 = 38;
3564 const COLOR_FOREGROUND_BRIGHT_ANSI: i32 = 90;
3565
3566 #[derive(Clone, Copy, Default)]
3567 struct Color {
3568 kind: u8,
3569 value: u8,
3570 red: u8,
3571 green: u8,
3572 blue: u8,
3573 }
3574
3575 const COLOR_UNSPECIFIED: u8 = 0;
3576 const COLOR_NORMAL: u8 = 1;
3577 const COLOR_ANSI: u8 = 2;
3578 const COLOR_256: u8 = 3;
3579 const COLOR_RGB: u8 = 4;
3580
3581 fn color_empty(c: &Color) -> bool {
3582 c.kind == COLOR_UNSPECIFIED || c.kind == COLOR_NORMAL
3583 }
3584
3585 fn parse_ansi_color(name: &str) -> Option<Color> {
3586 let color_names = [
3587 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
3588 ];
3589 let color_offset = COLOR_FOREGROUND_ANSI;
3590
3591 if name.eq_ignore_ascii_case("default") {
3592 return Some(Color {
3593 kind: COLOR_ANSI,
3594 value: (9 + color_offset) as u8,
3595 ..Default::default()
3596 });
3597 }
3598
3599 let (name, color_offset) = if name.len() >= 6 && name[..6].eq_ignore_ascii_case("bright") {
3600 (&name[6..], COLOR_FOREGROUND_BRIGHT_ANSI)
3601 } else {
3602 (name, COLOR_FOREGROUND_ANSI)
3603 };
3604
3605 for (i, cn) in color_names.iter().enumerate() {
3606 if name.eq_ignore_ascii_case(cn) {
3607 return Some(Color {
3608 kind: COLOR_ANSI,
3609 value: (i as i32 + color_offset) as u8,
3610 ..Default::default()
3611 });
3612 }
3613 }
3614 None
3615 }
3616
3617 fn hex_val(b: u8) -> Option<u8> {
3618 match b {
3619 b'0'..=b'9' => Some(b - b'0'),
3620 b'a'..=b'f' => Some(b - b'a' + 10),
3621 b'A'..=b'F' => Some(b - b'A' + 10),
3622 _ => None,
3623 }
3624 }
3625
3626 fn get_hex_color(chars: &[u8], width: usize) -> Option<(u8, usize)> {
3627 assert!(width == 1 || width == 2);
3628 if chars.len() < width {
3629 return None;
3630 }
3631 let v = if width == 2 {
3632 let hi = hex_val(chars[0])?;
3633 let lo = hex_val(chars[1])?;
3634 (hi << 4) | lo
3635 } else {
3636 let n = hex_val(chars[0])?;
3637 (n << 4) | n
3638 };
3639 Some((v, width))
3640 }
3641
3642 fn parse_single_color(word: &str) -> Option<Color> {
3643 if word.eq_ignore_ascii_case("normal") {
3644 return Some(Color {
3645 kind: COLOR_NORMAL,
3646 ..Default::default()
3647 });
3648 }
3649
3650 let bytes = word.as_bytes();
3651 if (bytes.len() == 7 || bytes.len() == 4) && bytes.first() == Some(&b'#') {
3652 let width = if bytes.len() == 7 { 2 } else { 1 };
3653 let mut idx = 1;
3654 let (r, n1) = get_hex_color(&bytes[idx..], width)?;
3655 idx += n1;
3656 let (g, n2) = get_hex_color(&bytes[idx..], width)?;
3657 idx += n2;
3658 let (b, n3) = get_hex_color(&bytes[idx..], width)?;
3659 idx += n3;
3660 if idx != bytes.len() {
3661 return None;
3662 }
3663 return Some(Color {
3664 kind: COLOR_RGB,
3665 red: r,
3666 green: g,
3667 blue: b,
3668 ..Default::default()
3669 });
3670 }
3671
3672 if let Some(c) = parse_ansi_color(word) {
3673 return Some(c);
3674 }
3675
3676 let Ok(val) = word.parse::<i64>() else {
3677 return None;
3678 };
3679 if val < -1 {
3680 return None;
3681 }
3682 if val < 0 {
3683 return Some(Color {
3684 kind: COLOR_NORMAL,
3685 ..Default::default()
3686 });
3687 }
3688 if val < 8 {
3689 return Some(Color {
3690 kind: COLOR_ANSI,
3691 value: (val as i32 + COLOR_FOREGROUND_ANSI) as u8,
3692 ..Default::default()
3693 });
3694 }
3695 if val < 16 {
3696 return Some(Color {
3697 kind: COLOR_ANSI,
3698 value: (val as i32 - 8 + COLOR_FOREGROUND_BRIGHT_ANSI) as u8,
3699 ..Default::default()
3700 });
3701 }
3702 if val < 256 {
3703 return Some(Color {
3704 kind: COLOR_256,
3705 value: val as u8,
3706 ..Default::default()
3707 });
3708 }
3709 None
3710 }
3711
3712 fn parse_attr(word: &str) -> Option<u8> {
3713 const ATTRS: [(&str, u8, u8); 8] = [
3714 ("bold", 1, 22),
3715 ("dim", 2, 22),
3716 ("italic", 3, 23),
3717 ("ul", 4, 24),
3718 ("underline", 4, 24),
3719 ("blink", 5, 25),
3720 ("reverse", 7, 27),
3721 ("strike", 9, 29),
3722 ];
3723
3724 let mut negate = false;
3725 let mut rest = word;
3726 if let Some(stripped) = rest.strip_prefix("no") {
3727 negate = true;
3728 rest = stripped;
3729 if let Some(s) = rest.strip_prefix('-') {
3730 rest = s;
3731 }
3732 }
3733
3734 for (name, val, neg) in ATTRS {
3735 if rest == name {
3736 return Some(if negate { neg } else { val });
3737 }
3738 }
3739 None
3740 }
3741
3742 fn append_color_output(out: &mut String, c: &Color, background: bool) {
3743 let offset = if background {
3744 COLOR_BACKGROUND_OFFSET
3745 } else {
3746 0
3747 };
3748 match c.kind {
3749 COLOR_UNSPECIFIED | COLOR_NORMAL => {}
3750 COLOR_ANSI => {
3751 use std::fmt::Write;
3752 let _ = write!(out, "{}", i32::from(c.value) + offset);
3753 }
3754 COLOR_256 => {
3755 use std::fmt::Write;
3756 let _ = write!(out, "{};5;{}", COLOR_FOREGROUND_256 + offset, c.value);
3757 }
3758 COLOR_RGB => {
3759 use std::fmt::Write;
3760 let _ = write!(
3761 out,
3762 "{};2;{};{};{}",
3763 COLOR_FOREGROUND_RGB + offset,
3764 c.red,
3765 c.green,
3766 c.blue
3767 );
3768 }
3769 _ => {}
3770 }
3771 }
3772
3773 let s = s.trim();
3774 let bad_color = || ConfigValueError::InvalidColor {
3775 value: s.to_owned(),
3776 };
3777 if s.is_empty() {
3778 return Ok(String::new());
3779 }
3780
3781 let mut has_reset = false;
3782 let mut attr: u64 = 0;
3783 let mut fg = Color::default();
3784 let mut bg = Color::default();
3785 fg.kind = COLOR_UNSPECIFIED;
3786 bg.kind = COLOR_UNSPECIFIED;
3787
3788 for word in s.split_whitespace() {
3789 if word.eq_ignore_ascii_case("reset") {
3790 has_reset = true;
3791 continue;
3792 }
3793
3794 if let Some(c) = parse_single_color(word) {
3795 if fg.kind == COLOR_UNSPECIFIED {
3796 fg = c;
3797 continue;
3798 }
3799 if bg.kind == COLOR_UNSPECIFIED {
3800 bg = c;
3801 continue;
3802 }
3803 return Err(bad_color());
3804 }
3805
3806 if let Some(code) = parse_attr(word) {
3807 attr |= 1u64 << u64::from(code);
3808 continue;
3809 }
3810
3811 return Err(bad_color());
3812 }
3813
3814 if !has_reset && attr == 0 && color_empty(&fg) && color_empty(&bg) {
3815 return Err(bad_color());
3816 }
3817
3818 let mut out = String::from("\x1b[");
3819 let mut sep = if has_reset { 1u32 } else { 0u32 };
3820
3821 let mut attr_bits = attr;
3822 let mut i = 0u32;
3823 while attr_bits != 0 {
3824 let bit = 1u64 << i;
3825 if attr_bits & bit == 0 {
3826 i += 1;
3827 continue;
3828 }
3829 attr_bits &= !bit;
3830 if sep > 0 {
3831 out.push(';');
3832 }
3833 sep += 1;
3834 use std::fmt::Write;
3835 let _ = write!(out, "{i}");
3836 i += 1;
3837 }
3838
3839 if !color_empty(&fg) {
3840 if sep > 0 {
3841 out.push(';');
3842 }
3843 sep += 1;
3844 append_color_output(&mut out, &fg, false);
3845 }
3846 if !color_empty(&bg) {
3847 if sep > 0 {
3848 out.push(';');
3849 }
3850 append_color_output(&mut out, &bg, true);
3851 }
3852 out.push('m');
3853 Ok(out)
3854}
3855
3856#[derive(Debug, Clone)]
3857struct UrlParts {
3858 scheme: String,
3859 user: Option<String>,
3860 host: String,
3861 port: Option<String>,
3862 path: String,
3863}
3864
3865#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3866struct UrlMatchScore {
3867 host_len: usize,
3868 path_len: usize,
3869 user_matched: bool,
3870}
3871
3872fn parse_config_url(url: &str) -> Option<UrlParts> {
3873 let (scheme, rest) = url.split_once("://")?;
3874 let (authority, path) = match rest.find('/') {
3875 Some(idx) => (&rest[..idx], &rest[idx..]),
3876 None => (rest, "/"),
3877 };
3878 let (user, host_port) = match authority.rsplit_once('@') {
3879 Some((user, host)) => (Some(user.to_owned()), host),
3880 None => (None, authority),
3881 };
3882 let (host, port) = match host_port.rsplit_once(':') {
3883 Some((host, port)) if !host.contains(']') => (host, Some(port.to_owned())),
3884 _ => (host_port, None),
3885 };
3886 Some(UrlParts {
3887 scheme: scheme.to_lowercase(),
3888 user,
3889 host: host.to_lowercase(),
3890 port,
3891 path: if path.is_empty() {
3892 "/".to_owned()
3893 } else {
3894 path.trim_end_matches('/').to_owned()
3895 },
3896 })
3897}
3898
3899fn host_matches(pattern: &str, target: &str) -> bool {
3900 let pattern_parts: Vec<&str> = pattern.split('.').collect();
3901 let target_parts: Vec<&str> = target.split('.').collect();
3902 pattern_parts.len() == target_parts.len()
3903 && pattern_parts
3904 .iter()
3905 .zip(target_parts)
3906 .all(|(pattern, target)| *pattern == "*" || *pattern == target)
3907}
3908
3909fn path_match_len(pattern: &str, target: &str) -> Option<usize> {
3910 let pattern = if pattern.is_empty() { "/" } else { pattern };
3911 let target = if target.is_empty() { "/" } else { target };
3912 if pattern == "/" {
3913 return Some(1);
3914 }
3915 let pattern = pattern.trim_end_matches('/');
3916 if target == pattern
3917 || target
3918 .strip_prefix(pattern)
3919 .is_some_and(|rest| rest.starts_with('/'))
3920 {
3921 Some(pattern.len() + 1)
3922 } else {
3923 None
3924 }
3925}
3926
3927fn url_match_score(pattern_url: &str, target_url: &str) -> Option<UrlMatchScore> {
3928 let pattern = parse_config_url(pattern_url)?;
3929 let target = parse_config_url(target_url)?;
3930 if pattern.scheme != target.scheme {
3931 return None;
3932 }
3933 let user_matched = match pattern.user.as_deref() {
3934 Some(user) if target.user.as_deref() == Some(user) => true,
3935 Some(_) => return None,
3936 None => false,
3937 };
3938 if !host_matches(&pattern.host, &target.host) || pattern.port != target.port {
3939 return None;
3940 }
3941 let path_len = path_match_len(&pattern.path, &target.path)?;
3942 Some(UrlMatchScore {
3943 host_len: pattern.host.len(),
3944 path_len,
3945 user_matched,
3946 })
3947}
3948
3949pub fn url_matches(pattern_url: &str, target_url: &str) -> bool {
3951 url_match_score(pattern_url, target_url).is_some()
3952}
3953
3954pub fn get_urlmatch_entries<'a>(
3956 entries: &'a [ConfigEntry],
3957 section: &str,
3958 variable: &str,
3959 url: &str,
3960) -> Vec<&'a ConfigEntry> {
3961 let section_lower = section.to_lowercase();
3962 let variable_lower = variable.to_lowercase();
3963 let mut matches: Vec<(UrlMatchScore, &'a ConfigEntry)> = Vec::new();
3964
3965 for entry in entries {
3966 let key = &entry.key;
3967 let first_dot = match key.find('.') {
3968 Some(i) => i,
3969 None => continue,
3970 };
3971 let last_dot = match key.rfind('.') {
3972 Some(i) => i,
3973 None => continue,
3974 };
3975 let entry_section = &key[..first_dot];
3976 let entry_variable = &key[last_dot + 1..];
3977 if entry_section.to_lowercase() != section_lower
3978 || entry_variable.to_lowercase() != variable_lower
3979 {
3980 continue;
3981 }
3982 if first_dot == last_dot {
3983 matches.push((
3984 UrlMatchScore {
3985 host_len: 0,
3986 path_len: 0,
3987 user_matched: false,
3988 },
3989 entry,
3990 ));
3991 } else {
3992 let subsection = &key[first_dot + 1..last_dot];
3993 if let Some(score) = url_match_score(subsection, url) {
3994 matches.push((score, entry));
3995 }
3996 }
3997 }
3998 matches.sort_by_key(|a| a.0);
3999 matches.into_iter().map(|(_, e)| e).collect()
4000}
4001
4002pub fn get_urlmatch_all_in_section(
4004 entries: &[ConfigEntry],
4005 section: &str,
4006 url: &str,
4007) -> Vec<(String, String, ConfigScope)> {
4008 let section_lower = section.to_lowercase();
4009 let mut matches: Vec<(String, UrlMatchScore, String, String, ConfigScope)> = Vec::new();
4010
4011 for entry in entries {
4012 let key = &entry.key;
4013 let first_dot = match key.find('.') {
4014 Some(i) => i,
4015 None => continue,
4016 };
4017 let last_dot = match key.rfind('.') {
4018 Some(i) => i,
4019 None => continue,
4020 };
4021 let entry_section = &key[..first_dot];
4022 if entry_section.to_lowercase() != section_lower {
4023 continue;
4024 }
4025 let entry_variable = &key[last_dot + 1..];
4026 let val = entry.value.as_deref().unwrap_or("");
4027 if first_dot == last_dot {
4028 let canonical = format!("{}.{}", section_lower, entry_variable);
4029 matches.push((
4030 entry_variable.to_lowercase(),
4031 UrlMatchScore {
4032 host_len: 0,
4033 path_len: 0,
4034 user_matched: false,
4035 },
4036 val.to_owned(),
4037 canonical,
4038 entry.scope,
4039 ));
4040 } else {
4041 let subsection = &key[first_dot + 1..last_dot];
4042 if let Some(score) = url_match_score(subsection, url) {
4043 let canonical = format!("{}.{}", section_lower, entry_variable);
4044 matches.push((
4045 entry_variable.to_lowercase(),
4046 score,
4047 val.to_owned(),
4048 canonical,
4049 entry.scope,
4050 ));
4051 }
4052 }
4053 }
4054
4055 let mut best: std::collections::BTreeMap<String, (UrlMatchScore, String, String, ConfigScope)> =
4056 std::collections::BTreeMap::new();
4057 for (var, specificity, val, canonical, scope) in matches {
4058 let entry = best.entry(var).or_insert((
4059 UrlMatchScore {
4060 host_len: 0,
4061 path_len: 0,
4062 user_matched: false,
4063 },
4064 String::new(),
4065 String::new(),
4066 scope,
4067 ));
4068 if specificity >= entry.0 {
4069 *entry = (specificity, val, canonical, scope);
4070 }
4071 }
4072 best.into_values()
4073 .map(|(_, val, canonical, scope)| (canonical, val, scope))
4074 .collect()
4075}
4076
4077pub fn parse_path(env: &Environment, s: &str) -> String {
4081 if let Some(rest) = s.strip_prefix("~/") {
4082 if let Some(home) = home_dir(env) {
4083 return home.join(rest).to_string_lossy().to_string();
4084 }
4085 }
4086 s.to_owned()
4087}
4088
4089pub fn parse_path_optional(env: &Environment, s: &str) -> Option<String> {
4094 if let Some(rest) = s.strip_prefix(":(optional)") {
4095 let resolved = parse_path(env, rest);
4096 if std::path::Path::new(&resolved).exists() {
4097 Some(resolved)
4098 } else {
4099 None }
4101 } else {
4102 Some(parse_path(env, s))
4103 }
4104}
4105
4106#[must_use]
4122pub fn git_config_parameters_last_value(raw: &str, key: &str) -> Option<String> {
4123 let Ok(canon) = canonical_key(key) else {
4124 return None;
4125 };
4126 let mut last: Option<String> = None;
4127 for entry in parse_config_parameters_strict(raw).ok()? {
4128 match entry {
4129 ConfigParameter::Pair { key, value } => {
4130 if canonical_key(key.trim()).ok().as_ref() == Some(&canon) {
4131 last = Some(value.unwrap_or_else(|| "true".to_owned()));
4132 }
4133 }
4134 ConfigParameter::OldStyle(entry) => {
4135 if let Some((k, v)) = entry.split_once('=') {
4136 if canonical_key(k.trim()).ok().as_ref() == Some(&canon) {
4137 last = Some(v.to_owned());
4138 }
4139 } else if canonical_key(entry.trim()).ok().as_ref() == Some(&canon) {
4140 last = Some("true".to_owned());
4141 }
4142 }
4143 }
4144 }
4145 last
4146}
4147
4148#[derive(Debug, Clone, PartialEq, Eq)]
4149enum ConfigParameter {
4150 OldStyle(String),
4151 Pair { key: String, value: Option<String> },
4152}
4153
4154pub fn parse_config_parameters(raw: &str) -> Vec<String> {
4158 parse_config_parameters_strict(raw)
4159 .map(|entries| {
4160 entries
4161 .into_iter()
4162 .map(|entry| match entry {
4163 ConfigParameter::OldStyle(entry) => entry,
4164 ConfigParameter::Pair {
4165 key,
4166 value: Some(value),
4167 } => format!("{key}\u{1}{value}"),
4168 ConfigParameter::Pair { key, value: None } => format!("{key}\u{1}"),
4169 })
4170 .collect()
4171 })
4172 .unwrap_or_default()
4173}
4174
4175fn parse_config_parameters_strict(raw: &str) -> Result<Vec<ConfigParameter>> {
4176 let mut out: Vec<ConfigParameter> = Vec::new();
4177 let chars: Vec<char> = raw.chars().collect();
4178 let mut idx = skip_config_parameter_spaces(&chars, 0);
4179
4180 while idx < chars.len() {
4181 let (key, next) = sq_dequote_step_chars(&chars, idx)?;
4182 let Some(next_idx) = next else {
4183 out.push(ConfigParameter::OldStyle(key));
4184 break;
4185 };
4186
4187 if chars[next_idx].is_whitespace() {
4188 out.push(ConfigParameter::OldStyle(key));
4189 idx = skip_config_parameter_spaces(&chars, next_idx);
4190 continue;
4191 }
4192
4193 if chars[next_idx] != '=' {
4194 return Err(Error::Config(ConfigError::MalformedConfigParameters));
4195 }
4196
4197 let value_start = next_idx + 1;
4198 if value_start >= chars.len() || chars[value_start].is_whitespace() {
4199 out.push(ConfigParameter::Pair { key, value: None });
4200 idx = skip_config_parameter_spaces(&chars, value_start);
4201 continue;
4202 }
4203
4204 if chars[value_start] != '\'' {
4205 return Err(Error::Config(ConfigError::MalformedConfigParameters));
4206 }
4207 let (value, value_next) = sq_dequote_step_chars(&chars, value_start)?;
4208 if let Some(value_next) = value_next {
4209 if !chars[value_next].is_whitespace() {
4210 return Err(Error::Config(ConfigError::MalformedConfigParameters));
4211 }
4212 idx = skip_config_parameter_spaces(&chars, value_next);
4213 } else {
4214 idx = chars.len();
4215 }
4216 out.push(ConfigParameter::Pair {
4217 key,
4218 value: Some(value),
4219 });
4220 }
4221
4222 Ok(out)
4223}
4224
4225fn skip_config_parameter_spaces(chars: &[char], mut idx: usize) -> usize {
4226 while idx < chars.len() && chars[idx].is_whitespace() {
4227 idx += 1;
4228 }
4229 idx
4230}
4231
4232fn sq_dequote_step_chars(chars: &[char], start: usize) -> Result<(String, Option<usize>)> {
4233 if chars.get(start) != Some(&'\'') {
4234 return Err(Error::Config(ConfigError::MalformedConfigParameters));
4235 }
4236
4237 let mut out = String::new();
4238 let mut idx = start + 1;
4239 loop {
4240 let Some(&ch) = chars.get(idx) else {
4241 return Err(Error::Config(ConfigError::MalformedConfigParameters));
4242 };
4243 if ch != '\'' {
4244 out.push(ch);
4245 idx += 1;
4246 continue;
4247 }
4248
4249 idx += 1;
4250 match chars.get(idx).copied() {
4251 None => return Ok((out, None)),
4252 Some('\\')
4253 if chars
4254 .get(idx + 1)
4255 .copied()
4256 .is_some_and(needs_sq_backslash_quote)
4257 && chars.get(idx + 2) == Some(&'\'') =>
4258 {
4259 if let Some(escaped) = chars.get(idx + 1) {
4260 out.push(*escaped);
4261 }
4262 idx += 3;
4263 }
4264 _ => return Ok((out, Some(idx))),
4265 }
4266 }
4267}
4268
4269fn needs_sq_backslash_quote(ch: char) -> bool {
4270 ch == '\'' || ch == '!'
4271}
4272
4273pub(crate) fn resolve_config_override_path(env: &Environment, path: &str) -> PathBuf {
4275 let p = Path::new(path);
4276 if p.is_absolute() {
4277 p.to_path_buf()
4278 } else {
4279 env.cwd.join(p)
4280 }
4281}
4282
4283pub fn global_config_paths_pub(env: &Environment) -> Vec<PathBuf> {
4286 global_config_paths(env)
4287}
4288
4289fn global_config_paths(env: &Environment) -> Vec<PathBuf> {
4290 let mut paths = Vec::new();
4291
4292 if let Some(p) = env.git_config_global.as_deref() {
4294 paths.push(resolve_config_override_path(env, p));
4295 return paths;
4296 }
4297
4298 if let Some(xdg) = env.xdg_config_home.as_deref() {
4300 paths.push(PathBuf::from(xdg).join("git/config"));
4301 } else if let Some(home) = home_dir(env) {
4302 paths.push(home.join(".config/git/config"));
4303 }
4304 if let Some(home) = home_dir(env) {
4305 paths.push(home.join(".gitconfig"));
4306 }
4307
4308 paths
4309}
4310
4311#[must_use]
4316pub fn system_config_path(env: &Environment) -> PathBuf {
4317 if let Some(p) = env.git_config_system.as_deref() {
4318 return resolve_config_override_path(env, p);
4319 }
4320 let unix_default = PathBuf::from("/etc/gitconfig");
4321 #[cfg(windows)]
4322 {
4323 if unix_default.is_file() {
4324 return unix_default;
4325 }
4326 if let Some(root) = env.git_install_root.as_ref() {
4327 let candidate = PathBuf::from(root).join("etc").join("gitconfig");
4328 if candidate.is_file() {
4329 return candidate;
4330 }
4331 }
4332 for pf in [
4333 env.program_files.as_deref(),
4334 env.program_files_x86.as_deref(),
4335 ]
4336 .into_iter()
4337 .flatten()
4338 {
4339 let candidate = PathBuf::from(pf).join("Git").join("etc").join("gitconfig");
4340 if candidate.is_file() {
4341 return candidate;
4342 }
4343 }
4344 }
4345 unix_default
4346}
4347
4348fn home_dir(env: &Environment) -> Option<PathBuf> {
4355 if let Some(home) = env.home.as_ref().filter(|h| !h.is_empty()) {
4356 return Some(PathBuf::from(home));
4357 }
4358 #[cfg(windows)]
4359 {
4360 if let (Some(drive), Some(path)) = (
4361 env.homedrive.as_ref().filter(|d| !d.is_empty()),
4362 env.homepath.as_ref().filter(|p| !p.is_empty()),
4363 ) {
4364 let mut combined = drive.clone();
4365 combined.push(path);
4366 return Some(PathBuf::from(combined));
4367 }
4368 if let Some(profile) = env.userprofile.as_ref().filter(|p| !p.is_empty()) {
4369 return Some(PathBuf::from(profile));
4370 }
4371 }
4372 None
4373}
4374
4375fn include_source_is_disk_file(file: &ConfigFile) -> bool {
4377 file.include_origin == ConfigIncludeOrigin::Disk
4378}
4379
4380fn resolve_include_file_path(
4386 path: &str,
4387 file: &ConfigFile,
4388 ctx: &IncludeContext,
4389) -> Result<Option<PathBuf>> {
4390 let expanded = parse_path(ctx.env.as_ref(), path);
4391 let p = Path::new(&expanded);
4392 if p.is_absolute() {
4393 return Ok(Some(p.to_path_buf()));
4394 }
4395 if !include_source_is_disk_file(file) {
4396 if file.include_origin == ConfigIncludeOrigin::CommandLine
4397 && !ctx.command_line_relative_include_is_error
4398 {
4399 return Ok(None);
4400 }
4401 return Err(Error::Config(ConfigError::RelativeIncludeOutsideFile));
4402 }
4403 let base = match file.path.parent() {
4404 Some(p) if !p.as_os_str().is_empty() => p,
4405 Some(_) | None => Path::new("."),
4406 };
4407 Ok(Some(base.join(p)))
4408}
4409
4410fn is_dir_sep(b: u8) -> bool {
4411 b == b'/' || b == b'\\'
4412}
4413
4414fn add_trailing_starstar_for_dir(pat: &mut String) {
4415 let bytes = pat.as_bytes();
4416 if bytes.last().is_some_and(|&b| is_dir_sep(b)) {
4417 pat.push_str("**");
4418 }
4419}
4420
4421fn prepare_gitdir_pattern(
4426 condition: &str,
4427 file: &ConfigFile,
4428 ctx: &IncludeContext,
4429) -> Option<(String, usize)> {
4430 let mut pat = parse_path(ctx.env.as_ref(), condition);
4432 if pat.starts_with("./") || pat.starts_with(".\\") {
4433 let parent = file
4434 .path
4435 .parent()
4436 .filter(|_| include_source_is_disk_file(file))?;
4437 let real = parent.canonicalize_simplified().ok()?;
4438 let mut dir = real.to_string_lossy().into_owned();
4439 if !dir.ends_with('/') && !dir.ends_with('\\') {
4440 dir.push('/');
4441 }
4442 let rest = &pat[2..];
4443 pat = format!("{dir}{rest}");
4444 let prefix_len = dir.len();
4445 add_trailing_starstar_for_dir(&mut pat);
4446 return Some((pat, prefix_len));
4447 }
4448 let p = Path::new(&pat);
4449 if !p.is_absolute() {
4450 pat.insert_str(0, "**/");
4451 }
4452 add_trailing_starstar_for_dir(&mut pat);
4453 Some((pat, 0))
4454}
4455
4456fn git_dir_match_texts(ctx: &IncludeContext, git_dir: &Path) -> (String, String) {
4461 let real = git_dir
4462 .canonicalize_simplified()
4463 .map(|p| p.to_string_lossy().into_owned())
4464 .unwrap_or_else(|_| git_dir.to_string_lossy().into_owned());
4465 let abs = if git_dir.is_absolute() {
4468 let pwd_abs = ctx.pwd.as_deref().and_then(|pwd| {
4471 let pwd_path = std::path::Path::new(pwd);
4472 if !pwd_path.is_absolute() {
4473 return None;
4474 }
4475 let pwd_canon = pwd_path.canonicalize_simplified().ok()?;
4476 let git_dir_str = git_dir.to_string_lossy();
4477 let pwd_canon_str = pwd_canon.to_string_lossy();
4478 let suffix = git_dir_str.strip_prefix(pwd_canon_str.as_ref())?;
4480 Some(format!("{pwd}{suffix}"))
4481 });
4482 pwd_abs.unwrap_or_else(|| git_dir.to_string_lossy().into_owned())
4483 } else {
4484 ctx.cwd.join(git_dir).to_string_lossy().into_owned()
4485 };
4486 (real, abs)
4487}
4488
4489fn include_by_gitdir(
4490 condition: &str,
4491 file: &ConfigFile,
4492 ctx: &IncludeContext,
4493 icase: bool,
4494) -> bool {
4495 let Some(git_dir) = ctx.git_dir.as_ref() else {
4496 return false;
4497 };
4498 let Some((pattern, prefix)) = prepare_gitdir_pattern(condition, file, ctx) else {
4499 return false;
4500 };
4501 let flags = WM_PATHNAME | if icase { WM_CASEFOLD } else { 0 };
4502 let (text_real, text_abs) = git_dir_match_texts(ctx, git_dir);
4503 let try_match = |text: &str| -> bool {
4504 let t = text.as_bytes();
4505 let p = pattern.as_bytes();
4506 if prefix > 0 {
4507 if t.len() < prefix {
4508 return false;
4509 }
4510 let pre = &p[..prefix];
4511 let te = &t[..prefix];
4512 let ok = if icase {
4513 pre.eq_ignore_ascii_case(te)
4514 } else {
4515 pre == te
4516 };
4517 if !ok {
4518 return false;
4519 }
4520 return wildmatch(&p[prefix..], &t[prefix..], flags);
4521 }
4522 wildmatch(p, t, flags)
4523 };
4524 if try_match(&text_real) {
4525 return true;
4526 }
4527 text_real != text_abs && try_match(&text_abs)
4528}
4529
4530fn current_branch_short_name(git_dir: Option<&Path>) -> Option<String> {
4531 let gd = git_dir?;
4532 let target = refs::read_symbolic_ref(gd, "HEAD").ok()??;
4533 let rest = target.strip_prefix("refs/heads/")?;
4534 Some(rest.to_owned())
4535}
4536
4537fn include_by_onbranch(condition: &str, ctx: &IncludeContext) -> bool {
4538 let Some(short) = current_branch_short_name(ctx.git_dir.as_deref()) else {
4539 return false;
4540 };
4541 let mut pattern = condition.to_owned();
4542 add_trailing_starstar_for_dir(&mut pattern);
4543 wildmatch(pattern.as_bytes(), short.as_bytes(), WM_PATHNAME)
4544}
4545
4546fn is_remote_url_entry(entry: &ConfigEntry) -> bool {
4548 entry
4549 .key
4550 .strip_prefix("remote.")
4551 .is_some_and(|rest| rest.ends_with(".url"))
4552}
4553
4554fn hasconfig_remote_url_pattern(condition: &str) -> Option<&str> {
4556 condition.strip_prefix("hasconfig:remote.*.url:")
4557}
4558
4559fn evaluate_include_condition(condition: &str, file: &ConfigFile, ctx: &IncludeContext) -> bool {
4565 if let Some(rest) = condition.strip_prefix("gitdir/i:") {
4566 return include_by_gitdir(rest, file, ctx, true);
4567 }
4568 if let Some(rest) = condition.strip_prefix("gitdir:") {
4569 return include_by_gitdir(rest, file, ctx, false);
4570 }
4571 if let Some(rest) = condition.strip_prefix("onbranch:") {
4572 return include_by_onbranch(rest, ctx);
4573 }
4574 false
4575}
4576
4577fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
4579 let (Some(first_dot), Some(last_dot)) = (key.find('.'), key.rfind('.')) else {
4580 return Err(Error::Config(ConfigError::InvalidKey {
4581 key: key.to_owned(),
4582 reason: InvalidKeyReason::MissingSection,
4583 }));
4584 };
4585
4586 let section = key[..first_dot].to_owned();
4587 let variable = key[last_dot + 1..].to_owned();
4588
4589 let subsection = if first_dot == last_dot {
4590 None
4591 } else {
4592 Some(key[first_dot + 1..last_dot].to_owned())
4593 };
4594
4595 Ok((section, subsection, variable))
4596}
4597
4598fn parse_section_name(name: &str) -> (&str, Option<&str>) {
4602 match name.find('.') {
4603 Some(i) => (&name[..i], Some(&name[i + 1..])),
4604 None => (name, None),
4605 }
4606}
4607
4608fn section_matches(parser: &Parser, section_lower: &str, subsection: Option<&str>) -> bool {
4609 if parser.section.to_lowercase() == section_lower && parser.subsection.as_deref() == subsection
4610 {
4611 return true;
4612 }
4613 let Some(subsection) = subsection else {
4614 return false;
4615 };
4616 parser.subsection.is_none()
4617 && parser.section.to_lowercase() == format!("{section_lower}.{}", subsection.to_lowercase())
4618}
4619
4620#[must_use]
4622pub fn config_lock_path(path: &Path) -> PathBuf {
4623 let file_name = path
4624 .file_name()
4625 .and_then(|s| s.to_str())
4626 .unwrap_or("config");
4627 path.with_file_name(format!("{file_name}.lock"))
4628}
4629
4630fn validate_section_name(name: &str, section: &str, subsection: Option<&str>) -> Result<()> {
4633 if section.is_empty()
4634 || !section
4635 .chars()
4636 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
4637 || subsection.is_some_and(str::is_empty)
4638 {
4639 return Err(Error::Config(ConfigError::InvalidSectionName {
4640 section: name.to_owned(),
4641 }));
4642 }
4643 Ok(())
4644}
4645
4646fn raw_variable_name(raw_key: &str) -> &str {
4650 match raw_key.rfind('.') {
4651 Some(i) => &raw_key[i + 1..],
4652 None => raw_key,
4653 }
4654}
4655
4656fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
4661 let first_dot = match raw_key.find('.') {
4662 Some(i) => i,
4663 None => return (raw_key.to_owned(), None),
4664 };
4665 let last_dot = match raw_key.rfind('.') {
4667 Some(i) => i,
4668 None => return (raw_key[..first_dot].to_owned(), None),
4669 };
4670 let section = raw_key[..first_dot].to_owned();
4671 if first_dot == last_dot {
4672 (section, None)
4673 } else {
4674 let subsection = raw_key[first_dot + 1..last_dot].to_owned();
4675 (section, Some(subsection))
4676 }
4677}
4678
4679fn is_section_header_with_inline_entry(line: &str) -> bool {
4681 let trimmed = line.trim();
4682 if !trimmed.starts_with('[') {
4683 return false;
4684 }
4685 let end = match trimmed.find(']') {
4686 Some(i) => i,
4687 None => return false,
4688 };
4689 let after = trimmed[end + 1..].trim();
4690 !after.is_empty() && !after.starts_with('#') && !after.starts_with(';')
4692}
4693
4694fn extract_section_header(line: &str) -> String {
4697 let trimmed = line.trim();
4698 let end = match trimmed.find(']') {
4699 Some(i) => i,
4700 None => return line.to_owned(),
4701 };
4702 trimmed[..=end].to_owned()
4705}
4706
4707#[cfg(test)]
4708mod pack_object_config_tests {
4709 use super::{ConfigFile, ConfigScope, ConfigSet};
4710 use std::path::Path;
4711
4712 fn set_from_snippet(text: &str) -> ConfigSet {
4713 let path = Path::new(".git/config");
4714 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4715 let mut set = ConfigSet::new();
4716 set.merge(&file);
4717 set
4718 }
4719
4720 #[test]
4721 fn pack_object_window_and_depth_defaults() {
4722 let set = ConfigSet::new();
4723 assert_eq!(set.pack_object_window(), 10);
4724 assert_eq!(set.pack_object_depth(), crate::pack::DEFAULT_PACK_DEPTH);
4725 }
4726
4727 #[test]
4728 fn pack_object_window_and_depth_preserve_explicit_zero() {
4729 let set = set_from_snippet("[pack]\n\twindow = 0\n\tdepth = 0\n");
4730 assert_eq!(set.pack_object_window(), 0);
4731 assert_eq!(set.pack_object_depth(), 0);
4732 }
4733
4734 #[test]
4735 fn gc_auto_limits_from_config() {
4736 let set = set_from_snippet("[gc]\n\tauto = 100\n\tautoPackLimit = 3\n");
4737 assert_eq!(set.gc_auto_threshold(), Some(100));
4738 assert_eq!(set.gc_auto_pack_limit(), Some(3));
4739 let defaults = ConfigSet::new();
4740 assert_eq!(defaults.gc_auto_threshold(), Some(6700));
4741 assert_eq!(defaults.gc_auto_pack_limit(), Some(50));
4742 assert!(defaults.gc_auto_detach());
4743 assert!(defaults.maintenance_auto());
4744 let disabled = set_from_snippet("[gc]\n\tautoPackLimit = 0\n");
4745 assert_eq!(disabled.gc_auto_pack_limit(), None);
4746 assert_eq!(disabled.gc_auto_threshold(), Some(6700));
4747 let auto_off = set_from_snippet("[gc]\n\tauto = 0\n");
4748 assert_eq!(auto_off.gc_auto_threshold(), None);
4749 let negative = set_from_snippet("[gc]\n\tauto = -1\n\tautoPackLimit = -1\n");
4750 assert_eq!(negative.gc_auto_threshold(), None);
4751 assert_eq!(negative.gc_auto_pack_limit(), None);
4752 let unparsable = set_from_snippet("[gc]\n\tauto = lots\n");
4753 assert_eq!(unparsable.gc_auto_threshold(), Some(6700));
4754 let foreground =
4755 set_from_snippet("[gc]\n\tautoDetach = false\n[maintenance]\n\tauto = false\n");
4756 assert!(!foreground.gc_auto_detach());
4757 assert!(!foreground.maintenance_auto());
4758 }
4759}
4760
4761#[cfg(test)]
4762mod value_error_tests {
4763 use super::{parse_bool, parse_color, parse_i64, ConfigFile, ConfigScope, ConfigSet};
4764 use crate::error::{ConfigError, ConfigValueError, Error};
4765 use std::path::Path;
4766
4767 fn set_from_snippet(text: &str) -> ConfigSet {
4768 let file = ConfigFile::parse(Path::new(".git/config"), text, ConfigScope::Local)
4769 .expect("parse config snippet");
4770 let mut set = ConfigSet::new();
4771 set.merge(&file);
4772 set
4773 }
4774
4775 #[test]
4776 fn parse_bool_rejects_non_boolean_with_the_value() {
4777 assert_eq!(
4778 parse_bool("maybe"),
4779 Err(ConfigValueError::InvalidBool {
4780 value: "maybe".to_owned()
4781 })
4782 );
4783 assert_eq!(parse_bool("2"), Ok(true));
4784 assert_eq!(parse_bool(""), Ok(false));
4785 }
4786
4787 #[test]
4788 fn parse_i64_distinguishes_empty_invalid_and_overflow() {
4789 assert_eq!(parse_i64(" "), Err(ConfigValueError::EmptyInteger));
4790 assert_eq!(
4791 parse_i64(" 12x "),
4792 Err(ConfigValueError::InvalidInteger {
4793 value: "12x".to_owned()
4794 })
4795 );
4796 assert_eq!(
4797 parse_i64("9223372036854775807k"),
4798 Err(ConfigValueError::IntegerOverflow {
4799 value: "9223372036854775807k".to_owned()
4800 })
4801 );
4802 assert_eq!(parse_i64("2m"), Ok(2 * 1024 * 1024));
4803 }
4804
4805 #[test]
4806 fn parse_color_reports_the_trimmed_value() {
4807 let bad = ConfigValueError::InvalidColor {
4808 value: "red green blue".to_owned(),
4809 };
4810 assert_eq!(parse_color(" red green blue "), Err(bad));
4811 assert_eq!(
4812 parse_color("nonsense").map_err(|e| e.to_string()),
4813 Err("bad color value 'nonsense'".to_owned())
4814 );
4815 assert_eq!(parse_color(" "), Ok(String::new()));
4816 }
4817
4818 #[test]
4819 fn get_bool_and_get_i64_name_the_key() {
4820 let set = set_from_snippet("[core]\n\tbare = sometimes\n[pack]\n\tthreads = many\n");
4821 let bool_err = set
4822 .get_bool("core.bare")
4823 .and_then(Result::err)
4824 .expect("invalid bool");
4825 assert_eq!(
4826 bool_err,
4827 ConfigError::InvalidValue {
4828 key: "core.bare".to_owned(),
4829 source: ConfigValueError::InvalidBool {
4830 value: "sometimes".to_owned()
4831 },
4832 }
4833 );
4834 assert_eq!(
4835 bool_err.to_string(),
4836 "bad boolean config value 'sometimes' for 'core.bare'"
4837 );
4838 let int_err = set
4839 .get_i64("pack.threads")
4840 .and_then(Result::err)
4841 .expect("invalid int");
4842 assert!(matches!(
4843 &int_err,
4844 ConfigError::InvalidValue { key, source: ConfigValueError::InvalidInteger { value } }
4845 if key == "pack.threads" && value == "many"
4846 ));
4847 assert!(matches!(Error::from(int_err), Error::Config(_)));
4848 assert!(set.get_bool("core.missing").is_none());
4849 }
4850
4851 #[test]
4852 fn typed_readers_match_any_casing_of_the_key() {
4853 let set = set_from_snippet(
4854 "[CORE]\n\tQUOTEPATH = false\n[pack]\n\twritebitmaphashcache = false\n\tREADREVERSEINDEX = false\n",
4855 );
4856 assert!(!set.quote_path_fully());
4857 assert!(!set.pack_write_bitmap_hash_cache());
4858 assert!(!set.pack_read_reverse_index_default());
4859 assert_eq!(set.get_bool("Core.QuotePath"), Some(Ok(false)));
4860 }
4861
4862 #[test]
4863 fn bool_or_uses_the_default_only_when_unset_or_invalid() {
4864 let set = set_from_snippet(
4865 "[core]\n\tbare = sometimes\n\tlogAllRefUpdates\n[feature]\n\tmanyFiles = off\n",
4866 );
4867 for default in [false, true] {
4868 assert_eq!(set.bool_or("core.missing", default), default);
4869 assert_eq!(set.bool_or("core.bare", default), default);
4870 assert!(set.bool_or("core.logallrefupdates", default));
4871 assert!(!set.bool_or("FEATURE.MANYFILES", default));
4872 }
4873 }
4874
4875 #[test]
4876 fn core_bool_accessors_use_git_defaults() {
4877 let unset = set_from_snippet("");
4878 assert!(!unset.ignore_case());
4879 assert!(!unset.precompose_unicode());
4880 assert!(unset.preload_index());
4881 assert!(unset.file_mode());
4882
4883 let flipped = set_from_snippet(
4884 "[core]\n\tignorecase = true\n\tprecomposeunicode = yes\n\tpreloadindex = 0\n\tfilemode = off\n",
4885 );
4886 assert!(flipped.ignore_case());
4887 assert!(flipped.precompose_unicode());
4888 assert!(!flipped.preload_index());
4889 assert!(!flipped.file_mode());
4890
4891 let invalid = set_from_snippet(
4892 "[core]\n\tignoreCase = maybe\n\tprecomposeUnicode = maybe\n\tpreloadIndex = maybe\n\tfileMode = maybe\n",
4893 );
4894 assert!(!invalid.ignore_case());
4895 assert!(!invalid.precompose_unicode());
4896 assert!(invalid.preload_index());
4897 assert!(invalid.file_mode());
4898 }
4899
4900 #[test]
4901 fn for_key_keeps_the_source_error() {
4902 let err = ConfigValueError::EmptyInteger.for_key("gc.auto");
4903 assert_eq!(err.to_string(), "empty integer value for 'gc.auto'");
4904 let source = std::error::Error::source(&err).map(ToString::to_string);
4905 assert_eq!(source.as_deref(), Some("empty integer value"));
4906 }
4907}
4908
4909#[cfg(test)]
4910mod get_regexp_tests {
4911 use super::{ConfigFile, ConfigScope, ConfigSet};
4912 use std::path::Path;
4913
4914 fn set_from_snippet(text: &str) -> ConfigSet {
4915 let path = Path::new(".git/config");
4916 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4917 let mut set = ConfigSet::new();
4918 set.merge(&file);
4919 set
4920 }
4921
4922 #[test]
4923 fn get_regexp_matches_section_prefix_like_git_config() {
4924 let text = r#"
4925[user]
4926 email = alice@example.com
4927 name = Alice
4928[core]
4929 bare = false
4930"#;
4931 let set = set_from_snippet(text);
4932 let keys: Vec<_> = set
4933 .get_regexp("user")
4934 .expect("valid pattern")
4935 .into_iter()
4936 .map(|e| e.key.as_str())
4937 .collect();
4938 assert!(keys.contains(&"user.email"));
4939 assert!(keys.contains(&"user.name"));
4940 assert!(!keys.iter().any(|k| k.starts_with("core.")));
4941 }
4942
4943 #[test]
4944 fn get_regexp_returns_all_multi_value_entries_in_order() {
4945 let text = r#"
4946[remote "origin"]
4947 url = https://example.com/repo.git
4948 fetch = +refs/heads/*:refs/remotes/origin/*
4949 push = +refs/heads/main:refs/heads/main
4950 push = +refs/heads/develop:refs/heads/develop
4951"#;
4952 let set = set_from_snippet(text);
4953 let matches = set.get_regexp("remote.origin").expect("valid pattern");
4954 let push_vals: Vec<_> = matches
4955 .iter()
4956 .filter(|e| e.key == "remote.origin.push")
4957 .map(|e| e.value.as_deref().unwrap_or(""))
4958 .collect();
4959 assert_eq!(push_vals.len(), 2);
4960 assert_eq!(push_vals[0], "+refs/heads/main:refs/heads/main");
4961 assert_eq!(push_vals[1], "+refs/heads/develop:refs/heads/develop");
4962 }
4963
4964 #[test]
4965 fn get_regexp_dot_matches_any_key() {
4966 let text = r#"
4967[a]
4968 x = 1
4969[b]
4970 y = 2
4971"#;
4972 let set = set_from_snippet(text);
4973 let m = set.get_regexp(".").expect("valid pattern");
4974 assert_eq!(m.len(), 2);
4975 }
4976
4977 #[test]
4978 fn get_regexp_no_match_returns_empty_vec() {
4979 let set = set_from_snippet("[user]\n\tname = x\n");
4980 let m = set.get_regexp("zzz").expect("valid pattern");
4981 assert!(m.is_empty());
4982 }
4983
4984 #[test]
4985 fn get_regexp_invalid_pattern_is_error() {
4986 let set = set_from_snippet("[user]\n\tname = x\n");
4987 let err = set.get_regexp("(").expect_err("unclosed group");
4988 assert!(err.contains("invalid key pattern"), "got: {err}");
4989 }
4990
4991 #[test]
4992 fn valueless_negotiation_algorithm_is_a_typed_config_error() {
4993 use crate::error::{ConfigError, Error};
4994
4995 let path = Path::new("/repo/.git/config");
4996 for (text, line) in [
4997 (
4998 "[core]\n\tbare = false\n[fetch]\n\tnegotiationAlgorithm\n",
4999 4,
5000 ),
5001 ("[fetch] negotiationAlgorithm\n", 1),
5002 ] {
5003 let err = ConfigFile::parse(path, text, ConfigScope::Local).expect_err(text);
5004 let Error::Config(config_err) = err else {
5005 panic!("expected a config error for {text:?}, got {err:?}");
5006 };
5007 assert_eq!(
5008 config_err,
5009 ConfigError::BadConfigVariable {
5010 key: "fetch.negotiationalgorithm".to_owned(),
5011 file: ".git/config".to_owned(),
5012 line,
5013 },
5014 "{text:?}"
5015 );
5016 }
5017
5018 let ok = ConfigFile::parse(
5019 path,
5020 "[fetch]\n\tnegotiationAlgorithm = skipping\n",
5021 ConfigScope::Local,
5022 )
5023 .expect("valued key parses");
5024 assert_eq!(ok.entries[0].value.as_deref(), Some("skipping"));
5025 }
5026}
5027
5028#[cfg(test)]
5029mod loose_compression_tests {
5030 use std::path::Path;
5031
5032 use super::{ConfigFile, ConfigScope, ConfigSet, Error};
5033
5034 fn set_from_snippet(text: &str) -> ConfigSet {
5035 let path = Path::new(".git/config");
5036 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
5037 let mut set = ConfigSet::new();
5038 set.merge(&file);
5039 set
5040 }
5041
5042 #[test]
5043 fn big_file_threshold_default_suffixes_and_errors() {
5044 assert_eq!(
5045 ConfigSet::new().big_file_threshold().unwrap(),
5046 512 * 1024 * 1024
5047 );
5048 let cases = [
5049 ("1000", 1000),
5050 ("0", 0),
5051 ("4k", 4096),
5052 ("2m", 2 * 1024 * 1024),
5053 ("1g", 1024 * 1024 * 1024),
5054 ("8G", 8 * 1024 * 1024 * 1024),
5055 ];
5056 for (raw, want) in cases {
5057 let set = set_from_snippet(&format!("[core]\n\tbigFileThreshold = {raw}\n"));
5058 assert_eq!(set.big_file_threshold().unwrap(), want, "value {raw}");
5059 }
5060 let set = set_from_snippet("[core]\n\tbigfilethreshold = 1k\n\tbigFileThreshold = 3k\n");
5061 assert_eq!(set.big_file_threshold().unwrap(), 3 * 1024);
5062
5063 for raw in ["-1", "12x", "lots", ""] {
5064 let snippet = if raw.is_empty() {
5065 "[core]\n\tbigFileThreshold\n".to_owned()
5066 } else {
5067 format!("[core]\n\tbigFileThreshold = {raw}\n")
5068 };
5069 let err = set_from_snippet(&snippet).big_file_threshold().unwrap_err();
5070 assert!(
5071 matches!(
5072 err,
5073 Error::Config(super::ConfigError::BadNumericValue { ref key, .. })
5074 if key == "core.bigfilethreshold"
5075 ),
5076 "value {raw:?}: {err:?}"
5077 );
5078 }
5079 }
5080
5081 #[test]
5082 fn loose_compression_cascade() {
5083 let set = ConfigSet::new();
5084 assert_eq!(
5085 set.loose_objects_zlib_level().unwrap(),
5086 ConfigSet::LOOSE_OBJECTS_ZLIB_DEFAULT
5087 );
5088
5089 let set = set_from_snippet("[core]\n\tcompression = 0\n");
5090 assert_eq!(set.loose_objects_zlib_level().unwrap(), 0);
5091
5092 let set = set_from_snippet("[core]\n\tcompression = 9\n");
5093 assert_eq!(set.loose_objects_zlib_level().unwrap(), 9);
5094
5095 let set = set_from_snippet("[core]\n\tcompression = 0\n\tloosecompression = 9\n");
5096 assert_eq!(set.loose_objects_zlib_level().unwrap(), 9);
5097
5098 let set = set_from_snippet("[core]\n\tloosecompression = 3\n");
5099 assert_eq!(set.loose_objects_zlib_level().unwrap(), 3);
5100
5101 let set = set_from_snippet("[core]\n\tcompression = -1\n");
5102 assert_eq!(set.loose_objects_zlib_level().unwrap(), 6);
5103
5104 for level in 0..=9 {
5105 let set = set_from_snippet(&format!("[core]\n\tloosecompression = {level}\n"));
5106 assert_eq!(set.loose_objects_zlib_level().unwrap(), level);
5107 }
5108
5109 let set = set_from_snippet("[core]\n\tloosecompression = -1\n");
5110 assert_eq!(set.loose_objects_zlib_level().unwrap(), 6);
5111
5112 let err = set_from_snippet("[core]\n\tloosecompression = 10\n")
5113 .loose_objects_zlib_level()
5114 .unwrap_err();
5115 assert!(matches!(err, Error::Config(_)));
5116
5117 let err = set_from_snippet("[core]\n\tloosecompression = not-a-number\n")
5118 .loose_objects_zlib_level()
5119 .unwrap_err();
5120 assert!(matches!(err, Error::Config(_)));
5121
5122 let err = set_from_snippet("[core]\n\tcompression = 42\n")
5123 .loose_objects_zlib_level()
5124 .unwrap_err();
5125 assert!(matches!(err, Error::Config(_)));
5126
5127 let err = set_from_snippet("[core]\n\tcompression = nope\n\tloosecompression = 1\n")
5128 .loose_objects_zlib_level()
5129 .unwrap_err();
5130 assert!(matches!(err, Error::Config(_)));
5131
5132 let err = set_from_snippet("[core]\n\tloosecompression = nope\n\tloosecompression = 1\n")
5133 .loose_objects_zlib_level()
5134 .unwrap_err();
5135 assert!(matches!(err, Error::Config(_)));
5136
5137 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 9\n");
5138 assert_eq!(set.loose_objects_zlib_level().unwrap(), 1);
5139
5140 let err = set_from_snippet("[core]\n\tloosecompression\n")
5141 .loose_objects_zlib_level()
5142 .unwrap_err();
5143 assert!(matches!(err, Error::Config(_)));
5144 }
5145}
5146
5147#[cfg(test)]
5148mod pack_compression_tests {
5149 use super::{ConfigFile, ConfigScope, ConfigSet};
5150 use std::path::Path;
5151
5152 fn set_from_snippet(text: &str) -> ConfigSet {
5153 let path = Path::new(".git/config");
5154 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
5155 let mut set = ConfigSet::new();
5156 set.merge(&file);
5157 set
5158 }
5159
5160 #[test]
5161 fn pack_objects_zlib_level_defaults_to_six() {
5162 let set = ConfigSet::new();
5163 assert_eq!(set.pack_objects_zlib_level().unwrap(), 6);
5164 }
5165
5166 #[test]
5167 fn pack_objects_zlib_level_core_compression() {
5168 let set = set_from_snippet("[core]\n\tcompression = 0\n");
5169 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
5170 let set = set_from_snippet("[core]\n\tcompression = 9\n");
5171 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5172 }
5173
5174 #[test]
5175 fn pack_objects_zlib_level_pack_overrides_core() {
5176 let set = set_from_snippet("[core]\n\tcompression = 9\n[pack]\n\tcompression = 0\n");
5177 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
5178 let set = set_from_snippet("[core]\n\tcompression = 0\n[pack]\n\tcompression = 9\n");
5179 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5180 }
5181
5182 #[test]
5183 fn pack_objects_zlib_level_later_core_does_not_override_earlier_pack() {
5184 let mut set = ConfigSet::new();
5185 set.merge(
5186 &ConfigFile::parse(
5187 Path::new("a"),
5188 "[pack]\n\tcompression = 9\n",
5189 ConfigScope::Local,
5190 )
5191 .unwrap(),
5192 );
5193 set.merge(
5194 &ConfigFile::parse(
5195 Path::new("b"),
5196 "[core]\n\tcompression = 0\n",
5197 ConfigScope::Local,
5198 )
5199 .unwrap(),
5200 );
5201 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5202 }
5203
5204 #[test]
5205 fn pack_objects_zlib_level_loosecompression_does_not_block_core_pack_level() {
5206 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 0\n");
5207 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
5208 }
5209
5210 #[test]
5211 fn pack_objects_zlib_level_pack_wins_after_loose_and_core() {
5212 let set = set_from_snippet(
5213 "[core]\n\tloosecompression = 1\n\tcompression = 0\n[pack]\n\tcompression = 9\n",
5214 );
5215 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5216 }
5217}
5218
5219#[cfg(test)]
5220mod config_cache_tests {
5221 use super::*;
5222 use filetime::FileTime;
5223
5224 fn local_opts(git_dir: &Path) -> LoadConfigOptions {
5225 LoadConfigOptions {
5226 include_system: false,
5227 include_ctx: IncludeContext {
5228 git_dir: Some(git_dir.to_path_buf()),
5229 ..Default::default()
5230 },
5231 ..Default::default()
5232 }
5233 }
5234
5235 fn load_value(caches: &crate::repo_caches::RepoCaches, git_dir: &Path) -> Option<String> {
5236 let opts = local_opts(git_dir);
5237 let set = caches
5238 .load_config_with_options(&Environment::empty(), Some(git_dir), &opts)
5239 .expect("load cascade");
5240 set.get("gritcachetest.value")
5241 }
5242
5243 fn mtime_of(path: &Path) -> FileTime {
5244 FileTime::from_last_modification_time(&fs::metadata(path).expect("stat config"))
5245 }
5246
5247 fn restore_mtime(path: &Path, stamp: FileTime) {
5248 filetime::set_file_mtime(path, stamp).expect("restore mtime");
5249 }
5250
5251 #[test]
5252 fn cache_serves_same_stamp_and_config_write_evicts() {
5253 let caches =
5254 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5255 let td = tempfile::tempdir().expect("tempdir");
5256 let gd = td.path();
5257 let cfg = gd.join("config");
5258 fs::write(&cfg, "[gritcachetest]\n\tvalue = aaa\n").expect("write v1");
5259 let t0 = mtime_of(&cfg);
5260 assert_eq!(load_value(&caches, gd).as_deref(), Some("aaa"));
5261
5262 fs::write(&cfg, "[gritcachetest]\n\tvalue = bbb\n").expect("write v2");
5266 restore_mtime(&cfg, t0);
5267 assert_eq!(load_value(&caches, gd).as_deref(), Some("aaa"));
5268
5269 let mut file = ConfigFile::from_path(&cfg, ConfigScope::Local)
5271 .expect("read config")
5272 .expect("config exists");
5273 file.set("gritcachetest.value", "ccc").expect("set value");
5274 file.write().expect("persist");
5275 caches.evict_config_for_path(&cfg);
5276 restore_mtime(&cfg, t0);
5277 assert_eq!(load_value(&caches, gd).as_deref(), Some("ccc"));
5278 }
5279
5280 #[test]
5281 fn cache_invalidates_on_size_or_existence_change() {
5282 let caches =
5283 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5284 let td = tempfile::tempdir().expect("tempdir");
5285 let gd = td.path();
5286 let cfg = gd.join("config");
5287 assert_eq!(load_value(&caches, gd), None);
5289 fs::write(&cfg, "[gritcachetest]\n\tvalue = first\n").expect("create");
5291 assert_eq!(load_value(&caches, gd).as_deref(), Some("first"));
5292 let t0 = mtime_of(&cfg);
5294 fs::write(&cfg, "[gritcachetest]\n\tvalue = second-longer\n").expect("rewrite");
5295 restore_mtime(&cfg, t0);
5296 assert_eq!(load_value(&caches, gd).as_deref(), Some("second-longer"));
5297 }
5298
5299 #[test]
5300 fn include_targets_are_stamped_and_invalidate() {
5301 let caches =
5302 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5303 let td = tempfile::tempdir().expect("tempdir");
5304 let gd = td.path();
5305 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
5306 let inc = gd.join("extra.conf");
5307 fs::write(&inc, "[gritcachetest]\n\tvalue = one\n").expect("write include v1");
5308 assert_eq!(load_value(&caches, gd).as_deref(), Some("one"));
5309
5310 let t0 = mtime_of(&inc);
5314 fs::write(&inc, "[gritcachetest]\n\tvalue = two\n").expect("write include v2");
5315 restore_mtime(&inc, t0);
5316 assert_eq!(load_value(&caches, gd).as_deref(), Some("one"));
5317
5318 fs::write(&inc, "[gritcachetest]\n\tvalue = two-longer\n").expect("write include v3");
5320 assert_eq!(load_value(&caches, gd).as_deref(), Some("two-longer"));
5321 }
5322
5323 #[test]
5324 fn missing_include_target_is_watched() {
5325 let caches =
5326 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5327 let td = tempfile::tempdir().expect("tempdir");
5328 let gd = td.path();
5329 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
5330 assert_eq!(load_value(&caches, gd), None);
5331
5332 fs::write(gd.join("extra.conf"), "[gritcachetest]\n\tvalue = born\n").expect("create");
5335 assert_eq!(load_value(&caches, gd).as_deref(), Some("born"));
5336 }
5337
5338 #[test]
5339 fn onbranch_condition_follows_head() {
5340 let caches =
5341 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5342 let td = tempfile::tempdir().expect("tempdir");
5343 let gd = td.path();
5344 fs::write(gd.join("HEAD"), "ref: refs/heads/main\n").expect("write HEAD");
5345 fs::write(
5346 gd.join("config"),
5347 "[includeIf \"onbranch:main\"]\n\tpath = branch.conf\n",
5348 )
5349 .expect("write config");
5350 fs::write(
5351 gd.join("branch.conf"),
5352 "[gritcachetest]\n\tvalue = onmain\n",
5353 )
5354 .expect("write branch config");
5355 assert_eq!(load_value(&caches, gd).as_deref(), Some("onmain"));
5356
5357 fs::write(gd.join("HEAD"), "ref: refs/heads/dev\n").expect("rewrite HEAD");
5360 assert_eq!(load_value(&caches, gd), None);
5361 }
5362
5363 #[test]
5364 #[cfg(unix)]
5365 fn load_propagates_local_config_io_errors() {
5366 use std::os::unix::fs::symlink;
5367 let td = tempfile::tempdir().expect("tempdir");
5368 let git_dir = td.path();
5369 let config = git_dir.join("config");
5370 symlink("config", &config).expect("self-referential symlink");
5371 let err = ConfigSet::load(&Environment::empty(), Some(git_dir), false).unwrap_err();
5372 assert!(
5373 matches!(err, Error::Io(_)),
5374 "expected I/O error for symlink loop, got {err:?}"
5375 );
5376 }
5377}
5378
5379#[cfg(test)]
5380mod line_scan_tests {
5381 use super::*;
5382
5383 fn strip_inline_comment_by_chars(s: &str) -> String {
5385 let mut in_quote = false;
5386 let mut last_was_backslash = false;
5387 let mut result = String::with_capacity(s.len());
5388 let mut chars = s.chars().peekable();
5389 while let Some(ch) = chars.next() {
5390 match ch {
5391 '"' if !last_was_backslash => {
5392 in_quote = !in_quote;
5393 result.push(ch);
5394 last_was_backslash = false;
5395 }
5396 '\\' if !last_was_backslash => {
5397 last_was_backslash = true;
5398 result.push(ch);
5399 if in_quote {
5400 if let Some(&next) = chars.peek() {
5401 result.push(next);
5402 chars.next();
5403 last_was_backslash = false;
5404 }
5405 }
5406 }
5407 '#' | ';' if !in_quote && !last_was_backslash => break,
5408 _ => {
5409 last_was_backslash = false;
5410 result.push(ch);
5411 }
5412 }
5413 }
5414 result.trim_end().to_owned()
5415 }
5416
5417 fn unclosed_quote_by_chars(line: &str) -> bool {
5418 let trimmed = line.trim();
5419 let Some(eq_pos) = trimmed.find('=') else {
5420 return false;
5421 };
5422 let mut in_quote = false;
5423 let mut last_was_backslash = false;
5424 for ch in trimmed[eq_pos + 1..].trim_start().chars() {
5425 match ch {
5426 '"' if !last_was_backslash => {
5427 in_quote = !in_quote;
5428 last_was_backslash = false;
5429 }
5430 '\\' if !last_was_backslash => {
5431 last_was_backslash = true;
5432 continue;
5433 }
5434 '#' | ';' if !in_quote && !last_was_backslash => return false,
5435 _ => last_was_backslash = false,
5436 }
5437 }
5438 in_quote
5439 }
5440
5441 fn continues_by_chars(line: &str) -> bool {
5442 let trimmed = line.trim();
5443 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
5444 return false;
5445 }
5446 let Some(pos) = trimmed.find('=') else {
5447 return false;
5448 };
5449 let mut in_quote = false;
5450 let mut last_was_backslash = false;
5451 let mut in_comment = false;
5452 for ch in trimmed[pos + 1..].chars() {
5453 if in_comment {
5454 last_was_backslash = false;
5455 continue;
5456 }
5457 match ch {
5458 '"' if !last_was_backslash => {
5459 in_quote = !in_quote;
5460 last_was_backslash = false;
5461 }
5462 '\\' if !last_was_backslash => {
5463 last_was_backslash = true;
5464 continue;
5465 }
5466 '#' | ';' if !in_quote && !last_was_backslash => {
5467 in_comment = true;
5468 last_was_backslash = false;
5469 }
5470 _ => last_was_backslash = false,
5471 }
5472 }
5473 last_was_backslash && !in_comment
5474 }
5475
5476 fn corpus() -> Vec<String> {
5479 const TOKENS: [&str; 11] = [
5480 "a", "\"", "\\", "#", ";", " ", "=", "\t", "é", "日", "\u{2003}",
5481 ];
5482 let mut out = vec![String::new()];
5483 let mut frontier = vec![String::new()];
5484 for _ in 0..5 {
5485 let mut next = Vec::with_capacity(frontier.len() * TOKENS.len());
5486 for prefix in &frontier {
5487 for token in TOKENS {
5488 next.push(format!("{prefix}{token}"));
5489 }
5490 }
5491 out.extend(next.iter().cloned());
5492 frontier = next;
5493 }
5494 out
5495 }
5496
5497 #[test]
5498 fn byte_scanners_match_char_scanners_on_every_short_line() {
5499 for s in corpus() {
5500 assert_eq!(
5501 strip_inline_comment(&s),
5502 strip_inline_comment_by_chars(&s),
5503 "strip_inline_comment({s:?})"
5504 );
5505 let line = format!("k = {s}");
5506 assert_eq!(
5507 entry_line_value_has_unclosed_quote(&line),
5508 unclosed_quote_by_chars(&line),
5509 "unclosed quote in {line:?}"
5510 );
5511 assert_eq!(
5512 value_line_continues(&line),
5513 continues_by_chars(&line),
5514 "continuation of {line:?}"
5515 );
5516 assert_eq!(
5517 entry_line_value_has_unclosed_quote(&s),
5518 unclosed_quote_by_chars(&s),
5519 "unclosed quote in {s:?}"
5520 );
5521 assert_eq!(
5522 value_line_continues(&s),
5523 continues_by_chars(&s),
5524 "continuation of {s:?}"
5525 );
5526 }
5527 }
5528
5529 #[test]
5530 fn strip_inline_comment_borrows_a_prefix() {
5531 let s = "\"a # b\" tail ; comment";
5532 let stripped = strip_inline_comment(s);
5533 assert_eq!(stripped, "\"a # b\" tail");
5534 assert_eq!(stripped.as_ptr(), s.as_ptr());
5535 assert_eq!(strip_inline_comment("\"\\\"#\" x"), "\"\\\"#\" x");
5536 assert_eq!(strip_inline_comment("v\\#kept # gone"), "v\\#kept");
5537 }
5538
5539 #[test]
5540 fn canonical_key_lowercases_section_and_name_only() {
5541 assert_eq!(canonical_key("core.bare").unwrap(), "core.bare");
5542 assert_eq!(canonical_key("CORE.BARE").unwrap(), "core.bare");
5543 assert_eq!(
5544 canonical_key("Remote.Origin.URL").unwrap(),
5545 "remote.Origin.url"
5546 );
5547 assert_eq!(canonical_key("a.B.C.D").unwrap(), "a.B.C.d");
5548 assert!(canonical_key("nodot").is_err());
5549 assert!(canonical_key("core.").is_err());
5550 assert!(canonical_key("core.1bad").is_err());
5551 assert!(canonical_key("core.ba\nre").is_err());
5552 }
5553
5554 #[test]
5555 fn parsed_keys_reuse_the_section_prefix_per_header() {
5556 let text = "[Core]\n\tBare = false\n\tFileMode\n\
5557 [Remote \"Origin\"]\n\tURL = /x\n\
5558 [branch \"Main\"] Remote = origin\n\
5559 [core]\n\tEditor = vi ; trailing\n";
5560 let file = ConfigFile::parse(Path::new("cfg"), text, ConfigScope::Local).unwrap();
5561 let keys: Vec<(&str, Option<&str>)> = file
5562 .entries
5563 .iter()
5564 .map(|e| (e.key.as_str(), e.value.as_deref()))
5565 .collect();
5566 assert_eq!(
5567 keys,
5568 [
5569 ("core.bare", Some("false")),
5570 ("core.filemode", None),
5571 ("remote.Origin.url", Some("/x")),
5572 ("branch.Main.remote", Some("origin")),
5573 ("core.editor", Some("vi")),
5574 ]
5575 );
5576 }
5577
5578 #[test]
5579 fn push_lowercase_matches_to_lowercase() {
5580 for s in ["", "core", "MiXeD-09", "ÄBC", "İx", "ΣΑΣ"] {
5581 let mut out = String::from("p.");
5582 push_lowercase(&mut out, s);
5583 assert_eq!(out, format!("p.{}", s.to_lowercase()));
5584 }
5585 }
5586}