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::{BadNumericSource, ConfigError, ConfigValueError, Error, Result};
50
51#[cfg(test)]
53pub mod cascade_load_counters {
54 use std::cell::Cell;
55
56 thread_local! {
59 static MEASURING: Cell<bool> = const { Cell::new(false) };
60 static UNCACHED: Cell<usize> = const { Cell::new(0) };
61 static CACHE_VALIDATED: Cell<usize> = const { Cell::new(0) };
62 }
63
64 pub fn measure<R>(f: impl FnOnce() -> R) -> R {
66 MEASURING.with(|m| {
67 m.set(true);
68 reset();
69 let out = f();
70 m.set(false);
71 out
72 })
73 }
74
75 pub fn reset() {
77 UNCACHED.with(|c| c.set(0));
78 CACHE_VALIDATED.with(|c| c.set(0));
79 }
80
81 #[must_use]
83 pub fn uncached_loads() -> usize {
84 UNCACHED.with(Cell::get)
85 }
86
87 #[must_use]
89 pub fn cache_validated_loads() -> usize {
90 CACHE_VALIDATED.with(Cell::get)
91 }
92
93 #[must_use]
95 pub fn total_loads() -> usize {
96 uncached_loads().saturating_add(cache_validated_loads())
97 }
98
99 pub(crate) fn record_uncached() {
100 if MEASURING.with(Cell::get) {
101 UNCACHED.with(|c| c.set(c.get() + 1));
102 }
103 }
104}
105use crate::refs;
106use crate::wildmatch::{wildmatch, WM_CASEFOLD, WM_PATHNAME};
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
110pub enum ConfigScope {
111 System,
113 Global,
115 Local,
117 Worktree,
119 Command,
121}
122
123impl fmt::Display for ConfigScope {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 match self {
126 Self::System => write!(f, "system"),
127 Self::Global => write!(f, "global"),
128 Self::Local => write!(f, "local"),
129 Self::Worktree => write!(f, "worktree"),
130 Self::Command => write!(f, "command"),
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct ConfigValueOrigin {
138 pub value: Option<String>,
141 pub scope: ConfigScope,
143 pub file: Option<PathBuf>,
145 pub line: Option<usize>,
147}
148
149#[derive(Debug, Clone)]
151pub struct ConfigEntry {
152 pub key: String,
155 pub value: Option<String>,
157 pub scope: ConfigScope,
159 pub file: Option<PathBuf>,
161 pub line: usize,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum ConfigIncludeOrigin {
168 Disk,
170 Stdin,
172 CommandLine,
174 Blob,
176}
177
178#[derive(Debug, Clone)]
181pub struct ConfigFile {
182 pub path: PathBuf,
184 pub scope: ConfigScope,
186 pub entries: Vec<ConfigEntry>,
188 raw_lines: Vec<String>,
190 pub include_origin: ConfigIncludeOrigin,
192}
193
194#[derive(Debug, Clone, Default)]
213pub struct ConfigSet {
214 entries: Vec<ConfigEntry>,
216}
217
218#[derive(Debug, Clone)]
220pub struct IncludeContext {
221 pub git_dir: Option<PathBuf>,
223 pub command_line_relative_include_is_error: bool,
225 pub cwd: PathBuf,
227 pub pwd: Option<String>,
229 pub env: std::sync::Arc<Environment>,
231}
232
233impl Default for IncludeContext {
234 fn default() -> Self {
235 Self {
236 git_dir: None,
237 command_line_relative_include_is_error: false,
238 cwd: PathBuf::from("."),
239 pwd: None,
240 env: std::sync::Arc::new(Environment::empty()),
241 }
242 }
243}
244
245#[derive(Clone)]
247pub struct LoadConfigOptions {
248 pub include_system: bool,
250 pub process_includes: bool,
252 pub command_includes: bool,
254 pub include_ctx: IncludeContext,
256 pub diagnostics: Option<crate::diagnostics::DiagnosticsHandle>,
258 pub repo_caches: Option<std::sync::Arc<crate::repo_caches::RepoCaches>>,
260}
261
262impl std::fmt::Debug for LoadConfigOptions {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 f.debug_struct("LoadConfigOptions")
265 .field("include_system", &self.include_system)
266 .field("process_includes", &self.process_includes)
267 .field("command_includes", &self.command_includes)
268 .field("include_ctx", &self.include_ctx)
269 .finish_non_exhaustive()
270 }
271}
272
273impl Default for LoadConfigOptions {
274 fn default() -> Self {
275 Self {
276 include_system: true,
277 process_includes: true,
278 command_includes: true,
279 include_ctx: IncludeContext::default(),
280 diagnostics: None,
281 repo_caches: None,
282 }
283 }
284}
285
286impl LoadConfigOptions {
287 pub(crate) fn for_repository(
294 env: &Environment,
295 git_dir: Option<&Path>,
296 repo_caches: Option<Arc<crate::repo_caches::RepoCaches>>,
297 ) -> Self {
298 Self {
299 include_ctx: IncludeContext {
300 git_dir: git_dir.map(Path::to_path_buf),
301 cwd: env.cwd.clone(),
302 pwd: env.pwd.clone(),
303 ..IncludeContext::default()
304 },
305 repo_caches,
306 ..Self::default()
307 }
308 }
309}
310
311#[must_use]
316pub fn config_entry_origin_file(entry: &ConfigEntry) -> Option<&Path> {
317 if entry.scope == ConfigScope::Command && entry.file.is_none() {
318 None
319 } else {
320 entry.file.as_deref()
321 }
322}
323
324#[must_use]
326pub fn config_entry_origin_line(entry: &ConfigEntry) -> Option<usize> {
327 if entry.scope == ConfigScope::Command && entry.file.is_none() {
328 None
329 } else {
330 (entry.line > 0).then_some(entry.line)
331 }
332}
333
334pub fn canonical_key(raw: &str) -> Result<String> {
350 if raw.contains('\n') || raw.contains('\r') {
352 return Err(Error::Config(
353 format!("invalid key: '{}'", raw.replace('\n', "\\n")).into(),
354 ));
355 }
356
357 let first_dot = raw
358 .find('.')
359 .ok_or_else(|| Error::Config(format!("key does not contain a section: '{raw}'").into()))?;
360 let last_dot = raw
361 .rfind('.')
362 .ok_or_else(|| Error::Config(format!("key does not contain a section: '{raw}'").into()))?;
363
364 if last_dot == raw.len() - 1 {
365 return Err(Error::Config(
366 format!("key does not contain variable name: '{raw}'").into(),
367 ));
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(Error::Config(
376 format!("invalid key (bad section): '{raw}'").into(),
377 ));
378 }
379
380 if !name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
382 || !name.chars().all(|c| c.is_alphanumeric() || c == '-')
383 {
384 return Err(Error::Config(
385 format!("invalid key (bad variable name): '{raw}'").into(),
386 ));
387 }
388
389 let mut key = String::with_capacity(raw.len());
390 push_lowercase(&mut key, section);
391 key.push('.');
392 if first_dot != last_dot {
393 key.push_str(&raw[first_dot + 1..last_dot]);
394 key.push('.');
395 }
396 push_lowercase(&mut key, name);
397 Ok(key)
398}
399
400fn push_lowercase(out: &mut String, s: &str) {
402 if s.is_ascii() {
403 out.extend(s.bytes().map(|b| char::from(b.to_ascii_lowercase())));
404 } else {
405 out.push_str(&s.to_lowercase());
406 }
407}
408
409#[must_use]
413pub fn config_file_display_for_error(path: &Path) -> String {
414 config_error_path_display(path)
415}
416
417fn config_error_path_display(path: &Path) -> String {
418 if path == Path::new("-") {
419 return "standard input".to_owned();
420 }
421 if path.file_name().and_then(|s| s.to_str()) == Some("config")
422 && path
423 .parent()
424 .and_then(|p| p.file_name())
425 .and_then(|s| s.to_str())
426 == Some(".git")
427 {
428 return ".git/config".to_owned();
429 }
430 path.display().to_string()
431}
432
433fn reject_valueless_negotiation_algorithm(
435 key: &str,
436 value: Option<&str>,
437 path: &Path,
438 line: usize,
439) -> Result<()> {
440 if key != "fetch.negotiationalgorithm" || value.is_some() {
441 return Ok(());
442 }
443 Err(ConfigError::BadConfigVariable {
444 key: key.to_owned(),
445 file: config_error_path_display(path),
446 line,
447 }
448 .into())
449}
450
451struct Parser {
453 section: String,
454 subsection: Option<String>,
455 key_prefix: String,
458}
459
460impl Parser {
461 fn new() -> Self {
462 Self {
463 section: String::new(),
464 subsection: None,
465 key_prefix: String::new(),
466 }
467 }
468
469 fn valid_variable_name(name: &str) -> bool {
470 let name = name.trim();
471 name.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
472 && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
473 }
474
475 fn set_section(&mut self, section: String, subsection: Option<String>) {
476 self.key_prefix.clear();
477 push_lowercase(&mut self.key_prefix, §ion);
478 self.key_prefix.push('.');
479 if let Some(sub) = &subsection {
480 self.key_prefix.push_str(sub);
481 self.key_prefix.push('.');
482 }
483 self.section = section;
484 self.subsection = subsection;
485 }
486
487 fn make_key(&self, name: &str) -> String {
491 let mut key = String::with_capacity(self.key_prefix.len() + name.len());
492 key.push_str(&self.key_prefix);
493 push_lowercase(&mut key, name);
494 key
495 }
496
497 fn try_parse_section_with_remainder<'a>(
503 &mut self,
504 line: &'a str,
505 inline_remainder: &mut Option<&'a str>,
506 ) -> bool {
507 let trimmed = line.trim();
508 if !trimmed.starts_with('[') {
509 return false;
510 }
511 let end = {
515 let bytes = trimmed.as_bytes();
516 let mut i = 1; let mut in_quotes = false;
518 let mut found = None;
519 while i < bytes.len() {
520 if in_quotes {
521 if bytes[i] == b'\\' {
522 i += 2; continue;
524 }
525 if bytes[i] == b'"' {
526 in_quotes = false;
527 }
528 } else {
529 if bytes[i] == b'"' {
530 in_quotes = true;
531 }
532 if bytes[i] == b']' {
533 found = Some(i);
534 break;
535 }
536 }
537 i += 1;
538 }
539 match found {
540 Some(i) => i,
541 None => return false,
542 }
543 };
544 let inside = &trimmed[1..end];
545 if let Some(quote_start) = inside.find('"') {
547 let section = inside[..quote_start].trim().to_owned();
548 let rest = &inside[quote_start + 1..];
549 let mut sub = String::new();
551 let mut chars = rest.chars();
552 while let Some(ch) = chars.next() {
553 if ch == '\\' {
554 if let Some(escaped) = chars.next() {
555 sub.push(escaped);
556 }
557 } else if ch == '"' {
558 break;
559 } else {
560 sub.push(ch);
561 }
562 }
563 self.set_section(section, Some(sub));
564 } else {
565 self.set_section(inside.trim().to_owned(), None);
566 }
567 let after = trimmed[end + 1..].trim();
569 if !after.is_empty() && !after.starts_with('#') && !after.starts_with(';') {
570 *inline_remainder = Some(after);
571 } else {
572 *inline_remainder = None;
573 }
574 true
575 }
576
577 fn try_parse_section(&mut self, line: &str) -> bool {
579 let mut _remainder = None;
580 self.try_parse_section_with_remainder(line, &mut _remainder)
581 }
582
583 fn try_parse_entry(&self, line: &str) -> Option<(String, Option<String>)> {
587 let trimmed = line.trim();
588 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
589 return None;
590 }
591 if trimmed.starts_with('[') {
592 return None;
593 }
594 if self.section.is_empty() {
595 return None;
596 }
597
598 if let Some(eq_pos) = trimmed.find('=') {
599 let raw_name = trimmed[..eq_pos].trim();
600 if !Self::valid_variable_name(raw_name) {
601 return None;
602 }
603 let raw_value = trimmed[eq_pos + 1..].trim();
604 let value = parse_config_value(strip_inline_comment(raw_value));
606 let key = self.make_key(raw_name);
607 Some((key, Some(value)))
608 } else {
609 let raw_name = strip_inline_comment(trimmed);
611 if raw_name.split_whitespace().count() > 1 {
612 return None;
613 }
614 let raw_name = raw_name.trim();
615 if !Self::valid_variable_name(raw_name) {
616 return None;
617 }
618 let key = self.make_key(raw_name);
619 Some((key, None))
620 }
621 }
622}
623
624fn entry_line_value_has_unclosed_quote(line: &str) -> bool {
634 let trimmed = line.trim();
635 let Some(eq_pos) = trimmed.find('=') else {
636 return false;
637 };
638 let raw_value = trimmed[eq_pos + 1..].trim_start();
639 let mut in_quote = false;
640 let mut last_was_backslash = false;
641 for b in raw_value.bytes() {
644 match b {
645 b'"' if !last_was_backslash => {
646 in_quote = !in_quote;
647 last_was_backslash = false;
648 }
649 b'\\' if !last_was_backslash => {
650 last_was_backslash = true;
651 continue;
652 }
653 b'#' | b';' if !in_quote && !last_was_backslash => return false,
654 _ => {
655 last_was_backslash = false;
656 }
657 }
658 }
659 in_quote
660}
661
662fn value_line_continues(line: &str) -> bool {
663 let trimmed = line.trim();
664 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
665 return false;
666 }
667 let value_part = match trimmed.find('=') {
670 Some(pos) => &trimmed[pos + 1..],
671 None => return false,
672 };
673 let mut in_quote = false;
675 let mut last_was_backslash = false;
676 let mut in_comment = false;
677 for b in value_part.bytes() {
678 if in_comment {
679 last_was_backslash = false;
681 continue;
682 }
683 match b {
684 b'"' if !last_was_backslash => {
685 in_quote = !in_quote;
686 last_was_backslash = false;
687 }
688 b'\\' if !last_was_backslash => {
689 last_was_backslash = true;
690 continue;
691 }
692 b'#' | b';' if !in_quote && !last_was_backslash => {
693 in_comment = true;
694 last_was_backslash = false;
695 }
696 _ => {
697 last_was_backslash = false;
698 }
699 }
700 }
701 last_was_backslash && !in_comment
703}
704
705fn strip_inline_comment(s: &str) -> &str {
708 let bytes = s.as_bytes();
709 let mut in_quote = false;
710 let mut last_was_backslash = false;
711 let mut i = 0;
712 while i < bytes.len() {
713 match bytes[i] {
714 b'"' if !last_was_backslash => {
715 in_quote = !in_quote;
716 last_was_backslash = false;
717 }
718 b'\\' if !last_was_backslash => {
719 last_was_backslash = true;
720 if in_quote && i + 1 < bytes.len() {
721 i += 1;
722 last_was_backslash = false;
723 }
724 }
725 b'#' | b';' if !in_quote && !last_was_backslash => break,
726 _ => last_was_backslash = false,
727 }
728 i += 1;
729 }
730 s[..i].trim_end()
732}
733
734fn parse_config_value(s: &str) -> String {
736 if s.starts_with('"') {
737 unescape_value(s)
738 } else {
739 unescape_unquoted_value(s)
740 }
741}
742
743fn unescape_unquoted_value(s: &str) -> String {
747 let mut result = String::with_capacity(s.len());
748 let mut chars = s.chars();
749 while let Some(ch) = chars.next() {
750 if ch == '\\' {
751 match chars.next() {
752 Some('n') => result.push('\n'),
753 Some('r') => result.push('\r'),
754 Some('t') => result.push('\t'),
755 Some('b') => result.push('\x08'),
756 Some('\\') => result.push('\\'),
757 Some('"') => result.push('"'),
758 Some(other) => {
759 result.push('\\');
760 result.push(other);
761 }
762 None => result.push('\\'),
763 }
764 } else {
765 result.push(ch);
766 }
767 }
768 result
769}
770
771fn unescape_value(s: &str) -> String {
774 let mut result = String::with_capacity(s.len());
775 let mut chars = s.chars();
776 while let Some(ch) = chars.next() {
777 match ch {
778 '"' => { }
779 '\\' => match chars.next() {
780 Some('n') => result.push('\n'),
781 Some('r') => result.push('\r'),
782 Some('t') => result.push('\t'),
783 Some('b') => result.push('\x08'),
784 Some('\\') => result.push('\\'),
785 Some('"') => result.push('"'),
786 Some(other) => {
787 result.push('\\');
788 result.push(other);
789 }
790 None => result.push('\\'),
791 },
792 _ => result.push(ch),
793 }
794 }
795 result
796}
797
798fn escape_subsection(s: &str) -> String {
805 let mut out = String::with_capacity(s.len());
806 for ch in s.chars() {
807 match ch {
808 '"' => out.push_str("\\\""),
809 '\\' => out.push_str("\\\\"),
810 other => out.push(other),
811 }
812 }
813 out
814}
815
816fn escape_value(s: &str) -> String {
817 let leading_dash_needs_quoting = s.starts_with('-') && parse_i64(s).is_err();
820 let must_quote = leading_dash_needs_quoting
821 || s.starts_with(' ')
822 || s.starts_with('\t')
823 || s.ends_with(' ')
824 || s.ends_with('\t')
825 || s.contains('"')
826 || s.contains('#')
827 || s.contains(';');
828
829 if must_quote {
830 return escape_value_quoted(s);
831 }
832
833 if s.contains('\n') || s.contains('\r') || s.contains('\t') || s.contains('\\') {
834 let mut out = String::with_capacity(s.len() + 4);
835 for ch in s.chars() {
836 match ch {
837 '\n' => out.push_str("\\n"),
838 '\r' => out.push_str("\\r"),
839 '\t' => out.push_str("\\t"),
840 '\\' => out.push_str("\\\\"),
841 '"' => out.push_str("\\\""),
842 other => out.push(other),
843 }
844 }
845 return out;
846 }
847
848 s.to_owned()
849}
850
851fn compile_config_value_pattern(pat: &str) -> std::result::Result<(regex::Regex, bool), Error> {
853 let (negated, actual_pat) = if let Some(rest) = pat.strip_prefix('!') {
854 (true, rest)
855 } else {
856 (false, pat)
857 };
858 let re = regex::Regex::new(actual_pat)
859 .map_err(|e| Error::Config(format!("invalid value-pattern regex: {e}").into()))?;
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 if matching_indices.is_empty() {
1342 return self.add_value_with_comment(key, value, comment);
1344 }
1345
1346 let raw_var = raw_variable_name(key);
1347
1348 let target_idx = *matching_indices
1349 .last()
1350 .ok_or_else(|| Error::Config("missing config match".to_owned().into()))?;
1351 let target_line_idx = self.entries[target_idx].line - 1;
1352 let raw_line = &self.raw_lines[target_line_idx];
1353 if is_section_header_with_inline_entry(raw_line) {
1354 let header = extract_section_header(raw_line);
1355 self.raw_lines[target_line_idx] = header;
1356 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1357 self.raw_lines.insert(target_line_idx + 1, new_line);
1358 } else {
1359 self.raw_lines[target_line_idx] =
1360 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1361 }
1362
1363 for &idx in matching_indices.iter().rev() {
1364 if idx == target_idx {
1365 continue;
1366 }
1367 let line_idx = self.entries[idx].line - 1;
1368 self.remove_entry_line(line_idx);
1369 }
1370
1371 let content = self.raw_lines.join("\n");
1373 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1374 self.entries = reparsed.entries;
1375 self.raw_lines = reparsed.raw_lines;
1376
1377 Ok(())
1378 }
1379
1380 pub fn count(&self, key: &str) -> Result<usize> {
1382 let canon = canonical_key(key)?;
1383 Ok(self.entries.iter().filter(|e| e.key == canon).count())
1384 }
1385
1386 fn remove_entry_line(&mut self, line_idx: usize) {
1397 if is_section_header_with_inline_entry(&self.raw_lines[line_idx]) {
1398 let header = extract_section_header(&self.raw_lines[line_idx]);
1400 self.raw_lines[line_idx] = header;
1401 } else {
1402 let mut lines_to_remove = 1;
1404 let mut check_line = self.raw_lines[line_idx].clone();
1405 while value_line_continues(&check_line)
1406 && (line_idx + lines_to_remove) < self.raw_lines.len()
1407 {
1408 check_line = self.raw_lines[line_idx + lines_to_remove].clone();
1409 lines_to_remove += 1;
1410 }
1411 for _ in 0..lines_to_remove {
1412 self.raw_lines.remove(line_idx);
1413 }
1414 }
1415 }
1416
1417 pub fn unset_last(&mut self, key: &str) -> Result<usize> {
1421 let canon = canonical_key(key)?;
1422 let last_idx = self.entries.iter().rposition(|e| e.key == canon);
1423
1424 if let Some(idx) = last_idx {
1425 let line_idx = self.entries[idx].line - 1;
1426 self.remove_entry_line(line_idx);
1427 let (section, subsection, _) = split_key(&canon)?;
1428 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
1429 let content = self.raw_lines.join("\n");
1430 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1431 self.entries = reparsed.entries;
1432 self.raw_lines = reparsed.raw_lines;
1433 Ok(1)
1434 } else {
1435 Ok(0)
1436 }
1437 }
1438
1439 pub fn unset(&mut self, key: &str) -> Result<usize> {
1449 let canon = canonical_key(key)?;
1450 let line_indices: Vec<usize> = self
1451 .entries
1452 .iter()
1453 .filter(|e| e.key == canon)
1454 .map(|e| e.line - 1)
1455 .collect();
1456
1457 let count = line_indices.len();
1458 for &idx in line_indices.iter().rev() {
1460 self.remove_entry_line(idx);
1461 }
1462
1463 if count > 0 {
1464 if let Ok((section, subsection, _)) = split_key(&canon) {
1465 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
1466 }
1467 let content = self.raw_lines.join("\n");
1468 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1469 self.entries = reparsed.entries;
1470 self.raw_lines = reparsed.raw_lines;
1471 }
1472
1473 Ok(count)
1474 }
1475
1476 pub fn unset_matching(
1485 &mut self,
1486 key: &str,
1487 value_pattern: Option<&str>,
1488 preserve_empty_section_header: bool,
1489 ) -> Result<usize> {
1490 let canon = canonical_key(key)?;
1491 let pattern = match value_pattern {
1492 Some(pat) => {
1493 let (re, negated) = compile_config_value_pattern(pat)?;
1494 Some((re, negated))
1495 }
1496 None => None,
1497 };
1498
1499 let line_indices: Vec<usize> = self
1500 .entries
1501 .iter()
1502 .filter(|e| {
1503 if e.key != canon {
1504 return false;
1505 }
1506 if let Some((ref re, negated)) = pattern {
1507 let v = e.value.as_deref().unwrap_or("");
1508 value_matches_config_pattern(re, negated, v)
1509 } else {
1510 true
1511 }
1512 })
1513 .map(|e| e.line - 1)
1514 .collect();
1515
1516 let count = line_indices.len();
1517 for &idx in line_indices.iter().rev() {
1518 self.remove_entry_line(idx);
1519 }
1520
1521 if count > 0 {
1522 if !preserve_empty_section_header {
1523 let (section, subsection, _) = split_key(&canon)?;
1524 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
1525 }
1526
1527 let content = self.raw_lines.join("\n");
1528 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1529 self.entries = reparsed.entries;
1530 self.raw_lines = reparsed.raw_lines;
1531 }
1532
1533 Ok(count)
1534 }
1535
1536 pub fn remove_section(&mut self, section: &str) -> Result<bool> {
1542 let (sec_name, sub_name) = parse_section_name(section);
1543 let sec_lower = sec_name.to_lowercase();
1544
1545 let mut remove = vec![false; self.raw_lines.len()];
1546 let mut removing = false;
1547 let mut found = false;
1548 let mut parser = Parser::new();
1549
1550 for (idx, line) in self.raw_lines.iter().enumerate() {
1551 if parser.try_parse_section(line) {
1552 removing = section_matches(&parser, &sec_lower, sub_name);
1553 found |= removing;
1554 }
1555 if removing {
1556 remove[idx] = true;
1557 }
1558 }
1559
1560 if found {
1561 self.raw_lines = self
1562 .raw_lines
1563 .iter()
1564 .enumerate()
1565 .filter_map(|(idx, line)| (!remove[idx]).then_some(line.clone()))
1566 .collect();
1567 let content = self.raw_lines.join("\n");
1568 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1569 self.entries = reparsed.entries;
1570 self.raw_lines = reparsed.raw_lines;
1571 Ok(true)
1572 } else {
1573 Ok(false)
1574 }
1575 }
1576
1577 pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
1584 let (old_sec, old_sub) = parse_section_name(old_name);
1585 let (new_sec, new_sub) = parse_section_name(new_name);
1586 validate_section_name(new_sec, new_sub)?;
1587 let old_lower = old_sec.to_lowercase();
1588
1589 let mut found = false;
1590 let mut parser = Parser::new();
1591
1592 let mut idx = 0usize;
1593 while idx < self.raw_lines.len() {
1594 let line = self.raw_lines[idx].clone();
1595 let mut inline_remainder = None;
1596 if parser.try_parse_section_with_remainder(&line, &mut inline_remainder)
1597 && section_matches(&parser, &old_lower, old_sub)
1598 {
1599 let header = match new_sub {
1601 Some(sub) => format!("[{} \"{}\"]", new_sec, sub),
1602 None => format!("[{}]", new_sec),
1603 };
1604 self.raw_lines[idx] = header;
1605 if let Some(remainder) = inline_remainder {
1606 self.raw_lines
1607 .insert(idx + 1, format!("\t{}", remainder.trim()));
1608 idx += 1;
1609 }
1610 found = true;
1611 }
1612 idx += 1;
1613 }
1614
1615 if found {
1616 let content = self.raw_lines.join("\n");
1617 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1618 self.entries = reparsed.entries;
1619 self.raw_lines = reparsed.raw_lines;
1620 }
1621
1622 Ok(found)
1623 }
1624
1625 pub fn add_value(&mut self, key: &str, value: &str) -> Result<()> {
1630 self.add_value_with_comment(key, value, None)
1631 }
1632
1633 pub fn add_value_with_comment(
1635 &mut self,
1636 key: &str,
1637 value: &str,
1638 comment: Option<&str>,
1639 ) -> Result<()> {
1640 let canon = canonical_key(key)?;
1641 let raw_var = raw_variable_name(key);
1642 let comment_suffix = format_comment_suffix(comment)?;
1643 let (section, subsection, _var) = split_key(&canon)?;
1644 let (raw_sec, raw_sub) = raw_section_parts(key);
1645
1646 let section_line = self.find_or_create_section_preserving_case(
1647 §ion,
1648 subsection.as_deref(),
1649 &raw_sec,
1650 raw_sub.as_deref(),
1651 );
1652 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1653 let insert_at = self.last_line_in_section(section_line) + 1;
1654 self.raw_lines.insert(insert_at, new_line);
1655
1656 let content = self.raw_lines.join("\n");
1658 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1659 self.entries = reparsed.entries;
1660 self.raw_lines = reparsed.raw_lines;
1661
1662 Ok(())
1663 }
1664
1665 fn remove_empty_section_headers_matching(&mut self, section: &str, subsection: Option<&str>) {
1668 let (Ok(section_re), Ok(comment_re)) = (
1669 regex::Regex::new(r"^\s*\["),
1670 regex::Regex::new(r"^\s*(#|;)"),
1671 ) else {
1672 return;
1674 };
1675
1676 let mut to_remove: Vec<usize> = Vec::new();
1677 let len = self.raw_lines.len();
1678 let section_lower = section.to_lowercase();
1679 let mut parser = Parser::new();
1680
1681 for i in 0..len {
1682 let line = &self.raw_lines[i];
1683 if !section_re.is_match(line) {
1684 continue;
1685 }
1686 if !parser.try_parse_section(line)
1687 || !section_matches(&parser, §ion_lower, subsection)
1688 {
1689 continue;
1690 }
1691 if is_section_header_with_inline_entry(line) {
1693 continue;
1694 }
1695 let has_attached_leading_comment = self.raw_lines[..i]
1696 .iter()
1697 .enumerate()
1698 .rev()
1699 .find(|(_, line)| !line.trim().is_empty())
1700 .is_some_and(|(idx, line)| {
1701 comment_re.is_match(line)
1702 && idx
1703 .checked_sub(1)
1704 .is_none_or(|prev| !value_line_continues(&self.raw_lines[prev]))
1705 });
1706 if has_attached_leading_comment {
1707 continue;
1708 }
1709 let mut has_entries = false;
1712 for j in (i + 1)..len {
1713 let next = self.raw_lines[j].trim();
1714 if next.is_empty() {
1715 continue;
1716 }
1717 if section_re.is_match(&self.raw_lines[j]) {
1718 break;
1719 }
1720 if comment_re.is_match(&self.raw_lines[j]) {
1721 has_entries = true;
1723 break;
1724 }
1725 has_entries = true;
1727 break;
1728 }
1729 if !has_entries {
1730 to_remove.push(i);
1731 }
1732 }
1733
1734 for &idx in to_remove.iter().rev() {
1736 self.raw_lines.remove(idx);
1737 }
1738
1739 while self.raw_lines.last().is_some_and(|l| l.trim().is_empty()) {
1741 self.raw_lines.pop();
1742 }
1743 }
1744
1745 pub fn write(&self) -> Result<()> {
1756 let lock_path = config_lock_path(&self.path);
1757 let content = self.raw_lines.join("\n");
1758 let bytes: Vec<u8> = if content.trim().is_empty() {
1759 Vec::new()
1760 } else if content.ends_with('\n') {
1761 content.into_bytes()
1762 } else {
1763 format!("{content}\n").into_bytes()
1764 };
1765
1766 let mut lock_file = fs::OpenOptions::new()
1767 .write(true)
1768 .create_new(true)
1769 .open(&lock_path)
1770 .map_err(|e| {
1771 if e.kind() == std::io::ErrorKind::AlreadyExists {
1772 Error::Config(ConfigError::ConfigFileLocked {
1773 path: lock_path.display().to_string(),
1774 })
1775 } else {
1776 Error::Io(e)
1777 }
1778 })?;
1779
1780 #[cfg(unix)]
1781 if let Ok(meta) = fs::metadata(&self.path) {
1782 let _ = lock_file.set_permissions(meta.permissions());
1783 }
1784
1785 use std::io::Write as _;
1786 lock_file.write_all(&bytes).map_err(|e| {
1787 let _ = fs::remove_file(&lock_path);
1788 Error::Io(e)
1789 })?;
1790 drop(lock_file);
1791
1792 fs::rename(&lock_path, &self.path).map_err(|e| {
1793 let _ = fs::remove_file(&lock_path);
1794 Error::Io(e)
1795 })?;
1796 Ok(())
1797 }
1798
1799 fn find_or_create_section_preserving_case(
1802 &mut self,
1803 section: &str,
1804 subsection: Option<&str>,
1805 raw_section: &str,
1806 raw_subsection: Option<&str>,
1807 ) -> usize {
1808 let sec_lower = section.to_lowercase();
1809 let mut parser = Parser::new();
1810 let mut last_match = None;
1811
1812 for (idx, line) in self.raw_lines.iter().enumerate() {
1813 if parser.try_parse_section(line) && section_matches(&parser, &sec_lower, subsection) {
1814 last_match = Some(idx);
1815 }
1816 }
1817 if let Some(idx) = last_match {
1818 return idx;
1819 }
1820
1821 let header = match raw_subsection {
1823 Some(sub) => {
1824 let escaped = escape_subsection(sub);
1825 format!("[{} \"{}\"]", raw_section, escaped)
1826 }
1827 None => format!("[{}]", raw_section),
1828 };
1829 self.raw_lines.push(header);
1830 self.raw_lines.len() - 1
1831 }
1832
1833 fn last_line_in_section(&self, section_line: usize) -> usize {
1835 let mut last = section_line;
1836 for idx in (section_line + 1)..self.raw_lines.len() {
1837 let trimmed = self.raw_lines[idx].trim();
1838 if trimmed.starts_with('[') {
1839 break;
1840 }
1841 last = idx;
1842 }
1843 last
1844 }
1845}
1846
1847impl ConfigSet {
1850 #[must_use]
1852 pub fn new() -> Self {
1853 Self {
1854 entries: Vec::new(),
1855 }
1856 }
1857
1858 #[must_use]
1860 pub fn entries(&self) -> &[ConfigEntry] {
1861 &self.entries
1862 }
1863
1864 pub fn merge(&mut self, file: &ConfigFile) {
1869 self.entries.extend(file.entries.iter().cloned());
1870 }
1871
1872 pub fn merge_set(&mut self, other: &ConfigSet) {
1874 self.entries.extend(other.entries.iter().cloned());
1875 }
1876
1877 pub fn add_command_override(&mut self, key: &str, value: &str) -> Result<()> {
1879 let canon = canonical_key(key)?;
1880 self.entries.push(ConfigEntry {
1881 key: canon,
1882 value: Some(value.to_owned()),
1883 scope: ConfigScope::Command,
1884 file: None,
1885 line: 0,
1886 });
1887 Ok(())
1888 }
1889
1890 #[must_use]
1901 pub fn get(&self, key: &str) -> Option<String> {
1902 let canon = canonical_key(key).ok()?;
1903 self.entries
1904 .iter()
1905 .rev()
1906 .find(|e| e.key == canon)
1907 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
1908 }
1909
1910 #[must_use]
1915 pub fn get_last_entry(&self, key: &str) -> Option<ConfigEntry> {
1916 let canon = canonical_key(key).ok()?;
1917 self.entries.iter().rev().find(|e| e.key == canon).cloned()
1918 }
1919
1920 pub fn get_with_origin(&self, key: &str) -> Result<Option<ConfigValueOrigin>> {
1926 let _canon = canonical_key(key)?;
1927 Ok(self.get_last_entry(key).map(|e| ConfigValueOrigin {
1928 value: e.value.clone().or_else(|| Some("true".to_owned())),
1929 scope: e.scope,
1930 file: config_entry_origin_file(&e).map(Path::to_path_buf),
1931 line: config_entry_origin_line(&e),
1932 }))
1933 }
1934
1935 pub fn get_all_with_origin(&self, key: &str) -> Result<Vec<ConfigValueOrigin>> {
1941 let canon = canonical_key(key)?;
1942 Ok(self
1943 .entries
1944 .iter()
1945 .filter(|e| e.key == canon)
1946 .map(|e| ConfigValueOrigin {
1947 value: e.value.clone().or_else(|| Some("true".to_owned())),
1948 scope: e.scope,
1949 file: config_entry_origin_file(e).map(Path::to_path_buf),
1950 line: config_entry_origin_line(e),
1951 })
1952 .collect())
1953 }
1954
1955 #[must_use]
1957 pub fn get_all(&self, key: &str) -> Vec<String> {
1958 let canon = match canonical_key(key) {
1959 Ok(c) => c,
1960 Err(_) => return Vec::new(),
1961 };
1962 self.entries
1963 .iter()
1964 .filter(|e| e.key == canon)
1965 .map(|e| e.value.clone().unwrap_or_default())
1966 .collect()
1967 }
1968
1969 #[must_use]
1973 pub fn get_all_raw(&self, key: &str) -> Vec<Option<String>> {
1974 let canon = match canonical_key(key) {
1975 Ok(c) => c,
1976 Err(_) => return Vec::new(),
1977 };
1978 self.entries
1979 .iter()
1980 .filter(|e| e.key == canon)
1981 .map(|e| e.value.clone())
1982 .collect()
1983 }
1984
1985 #[must_use]
1990 pub fn has_key(&self, key: &str) -> bool {
1991 let Ok(canon) = canonical_key(key) else {
1992 return false;
1993 };
1994 self.entries.iter().any(|e| e.key == canon)
1995 }
1996
1997 pub fn get_bool(&self, key: &str) -> Option<std::result::Result<bool, ConfigError>> {
2011 let v = self.get(key)?;
2012 if canonical_key(key).ok().as_deref() == Some("pack.allowpackreuse") {
2013 let lower = v.trim().to_ascii_lowercase();
2014 if lower == "single" || lower == "multi" {
2015 return None;
2016 }
2017 }
2018 Some(parse_bool(&v).map_err(|e| e.for_key(key)))
2019 }
2020
2021 #[must_use]
2026 pub fn quote_path_fully(&self) -> bool {
2027 self.get_bool("core.quotePath")
2028 .and_then(|r| r.ok())
2029 .unwrap_or(true)
2030 }
2031
2032 #[must_use]
2034 pub fn pack_write_bitmap_hash_cache(&self) -> bool {
2035 self.get_bool("pack.writeBitmapHashCache")
2036 .and_then(|r| r.ok())
2037 .unwrap_or(true)
2038 }
2039
2040 #[must_use]
2042 pub fn pack_write_bitmap_lookup_table(&self) -> bool {
2043 self.get_bool("pack.writeBitmapLookupTable")
2044 .and_then(|r| r.ok())
2045 .unwrap_or(false)
2046 }
2047
2048 #[must_use]
2050 pub fn pack_prefer_bitmap_tips(&self) -> Vec<String> {
2051 let mut out = self.get_all("pack.preferbitmaptips");
2052 if out.is_empty() {
2053 out = self.get_all("pack.preferBitmapTips");
2054 }
2055 out
2056 }
2057
2058 #[must_use]
2060 pub fn pack_bitmap_write_options(&self) -> crate::pack_bitmap::PackBitmapWriteOptions {
2061 crate::pack_bitmap::PackBitmapWriteOptions {
2062 full_dag: true,
2063 hash_cache: self.pack_write_bitmap_hash_cache(),
2064 lookup_table: self.pack_write_bitmap_lookup_table(),
2065 prefer_bitmap_tips: self.pack_prefer_bitmap_tips(),
2066 verify_reachability_closure: true,
2067 reachability_roots: None,
2068 }
2069 }
2070
2071 #[must_use]
2075 pub fn pack_write_reverse_index_default(&self, env: &Environment) -> bool {
2076 if env.git_test_no_write_rev_index.as_deref().is_some_and(|v| {
2077 let s = v.trim().to_ascii_lowercase();
2078 matches!(s.as_str(), "1" | "true" | "yes" | "on")
2079 }) {
2080 return false;
2081 }
2082 if self
2083 .get("pack.writeReverseIndex")
2084 .is_some_and(|v| v.trim().is_empty())
2085 {
2086 return false;
2087 }
2088 self.get_bool("pack.writeReverseIndex")
2089 .and_then(|r| r.ok())
2090 .unwrap_or(true)
2091 }
2092
2093 #[must_use]
2095 pub fn pack_index_threads(&self) -> Option<usize> {
2096 self.get_i64("pack.threads")
2097 .and_then(|r| r.ok())
2098 .and_then(|n| usize::try_from(n).ok())
2099 }
2100
2101 #[must_use]
2105 pub fn pack_object_window(&self) -> usize {
2106 match self.get_i64("pack.window") {
2107 None => 10,
2108 Some(Ok(n)) if n >= 0 => usize::try_from(n).unwrap_or(10),
2109 Some(Ok(_)) | Some(Err(_)) => 10,
2110 }
2111 }
2112
2113 #[must_use]
2117 pub fn pack_window_memory_bytes(&self) -> u64 {
2119 match self.get_i64("pack.windowmemory") {
2120 None => 0,
2121 Some(Ok(n)) if n >= 0 => u64::try_from(n).unwrap_or(0),
2122 Some(Ok(_)) | Some(Err(_)) => 0,
2123 }
2124 }
2125
2126 pub fn pack_object_depth(&self) -> usize {
2128 match self.get_i64("pack.depth") {
2129 None => crate::pack::DEFAULT_PACK_DEPTH,
2130 Some(Ok(n)) if n >= 0 => usize::try_from(n).unwrap_or(crate::pack::DEFAULT_PACK_DEPTH),
2131 Some(Ok(_)) | Some(Err(_)) => crate::pack::DEFAULT_PACK_DEPTH,
2132 }
2133 }
2134
2135 #[must_use]
2142 pub fn gc_auto_threshold(&self) -> Option<usize> {
2143 match self.get_i64("gc.auto") {
2144 Some(Ok(n)) if n <= 0 => None,
2145 Some(Ok(n)) => Some(usize::try_from(n).unwrap_or(usize::MAX)),
2146 None | Some(Err(_)) => Some(DEFAULT_GC_AUTO_THRESHOLD),
2147 }
2148 }
2149
2150 #[must_use]
2156 pub fn gc_auto_pack_limit(&self) -> Option<usize> {
2157 match self.get_i64("gc.autoPackLimit") {
2158 Some(Ok(n)) if n <= 0 => None,
2159 Some(Ok(n)) => Some(usize::try_from(n).unwrap_or(usize::MAX)),
2160 None | Some(Err(_)) => Some(DEFAULT_GC_AUTO_PACK_LIMIT),
2161 }
2162 }
2163
2164 #[must_use]
2167 pub fn gc_auto_detach(&self) -> bool {
2168 self.get_bool("gc.autoDetach")
2169 .and_then(|v| v.ok())
2170 .unwrap_or(true)
2171 }
2172
2173 #[must_use]
2176 pub fn maintenance_auto(&self) -> bool {
2177 self.get_bool("maintenance.auto")
2178 .and_then(|v| v.ok())
2179 .unwrap_or(true)
2180 }
2181
2182 pub fn try_pack_index_parallelism(
2188 &self,
2189 ) -> std::result::Result<crate::parallel::Parallelism, ConfigError> {
2190 Ok(crate::parallel::ThreadPolicy::default()
2191 .try_resolve(crate::parallel::ParallelOp::IndexPack, self)?
2192 .parallelism())
2193 }
2194
2195 #[must_use]
2198 pub fn pack_index_parallelism(&self) -> crate::parallel::Parallelism {
2199 self.try_pack_index_parallelism()
2200 .unwrap_or_else(|_| crate::parallel::Parallelism::resolve(None))
2201 }
2202
2203 pub fn try_pack_index_parallelism_for_git_dir(
2209 env: &Environment,
2210 git_dir: Option<&std::path::Path>,
2211 ) -> Result<crate::parallel::Parallelism> {
2212 match git_dir {
2213 Some(d) => {
2214 let cfg = Self::load(env, Some(d), true)?;
2215 Ok(cfg.try_pack_index_parallelism()?)
2216 }
2217 None => Ok(crate::parallel::Parallelism::resolve(None)),
2218 }
2219 }
2220
2221 #[must_use]
2223 pub fn pack_index_parallelism_for_git_dir(
2224 env: &Environment,
2225 git_dir: Option<&std::path::Path>,
2226 ) -> crate::parallel::Parallelism {
2227 Self::try_pack_index_parallelism_for_git_dir(env, git_dir)
2228 .unwrap_or_else(|_| crate::parallel::Parallelism::resolve(None))
2229 }
2230
2231 #[must_use]
2233 pub fn pack_read_reverse_index_default(&self) -> bool {
2234 self.get_bool("pack.readReverseIndex")
2235 .and_then(|r| r.ok())
2236 .unwrap_or(true)
2237 }
2238
2239 #[must_use]
2242 pub(crate) fn log_refs_config(&self) -> refs::LogRefsConfig {
2243 self.get("core.logAllRefUpdates")
2244 .and_then(|v| refs::LogRefsConfig::parse(&v))
2245 .unwrap_or(refs::LogRefsConfig::Unset)
2246 }
2247
2248 #[must_use]
2255 pub fn effective_log_refs_config(&self) -> refs::LogRefsConfig {
2256 match self.log_refs_config() {
2257 refs::LogRefsConfig::Unset
2258 if self
2259 .get_bool("core.bare")
2260 .and_then(|r| r.ok())
2261 .unwrap_or(false) =>
2262 {
2263 refs::LogRefsConfig::None
2264 }
2265 refs::LogRefsConfig::Unset => refs::LogRefsConfig::Normal,
2266 mode => mode,
2267 }
2268 }
2269
2270 pub(crate) fn from_entries(entries: Vec<ConfigEntry>) -> Self {
2272 Self { entries }
2273 }
2274
2275 pub fn get_i64(&self, key: &str) -> Option<std::result::Result<i64, ConfigError>> {
2286 self.get(key)
2287 .map(|v| parse_i64(&v).map_err(|e| e.for_key(key)))
2288 }
2289
2290 fn parse_zlib_compression_level(raw: &str) -> Result<i32> {
2295 const Z_DEFAULT_COMPRESSION: i32 = 6;
2296 const Z_BEST_COMPRESSION: i32 = 9;
2297
2298 let v = parse_git_config_int_strict(raw.trim()).map_err(|_| {
2299 Error::Config(format!("bad numeric config value '{raw}' for compression").into())
2300 })?;
2301 if v == -1 {
2302 return Ok(Z_DEFAULT_COMPRESSION);
2303 }
2304 if v < 0 || v > i64::from(Z_BEST_COMPRESSION) {
2305 return Err(Error::Config(
2306 format!("bad zlib compression level {v}").into(),
2307 ));
2308 }
2309 Ok(v as i32)
2310 }
2311
2312 pub fn loose_objects_zlib_level(&self) -> Result<i32> {
2323 let mut loose_level = Self::LOOSE_OBJECTS_ZLIB_DEFAULT;
2324 let mut loose_compression_seen = false;
2325
2326 for e in self.entries() {
2327 match e.key.as_str() {
2328 "core.compression" => {
2329 let val = e.value.as_deref().ok_or_else(|| {
2330 Error::Config(
2331 "bad numeric config value '' for compression"
2332 .to_owned()
2333 .into(),
2334 )
2335 })?;
2336 let level = Self::parse_zlib_compression_level(val)?;
2337 if !loose_compression_seen {
2338 loose_level = level;
2339 }
2340 }
2341 "core.loosecompression" => {
2342 let val = e.value.as_deref().ok_or_else(|| {
2343 Error::Config(
2344 "bad numeric config value '' for compression"
2345 .to_owned()
2346 .into(),
2347 )
2348 })?;
2349 let level = Self::parse_zlib_compression_level(val)?;
2350 loose_level = level;
2351 loose_compression_seen = true;
2352 }
2353 _ => {}
2354 }
2355 }
2356
2357 Ok(loose_level)
2358 }
2359
2360 pub const LOOSE_OBJECTS_ZLIB_DEFAULT: i32 = 1;
2363
2364 pub const BIG_FILE_THRESHOLD_DEFAULT: u64 = 512 * 1024 * 1024;
2366
2367 pub fn big_file_threshold(&self) -> Result<u64> {
2378 const KEY: &str = "core.bigfilethreshold";
2379 let Some(entry) = self.get_last_entry(KEY) else {
2380 return Ok(Self::BIG_FILE_THRESHOLD_DEFAULT);
2381 };
2382 let raw = entry.value.as_deref().unwrap_or_default();
2383 let bad = |reason| {
2384 Error::Config(ConfigError::BadNumericValue {
2385 key: KEY.to_owned(),
2386 value: raw.to_owned(),
2387 reason,
2388 })
2389 };
2390 let value = parse_git_config_int_strict(raw.trim()).map_err(|e| {
2391 bad(match e {
2392 GitConfigIntStrictError::InvalidUnit => BadNumericSource::InvalidUnit,
2393 GitConfigIntStrictError::OutOfRange => BadNumericSource::OutOfRange,
2394 })
2395 })?;
2396 u64::try_from(value).map_err(|_| bad(BadNumericSource::OutOfRange))
2397 }
2398
2399 pub fn pack_objects_zlib_level(&self) -> Result<i32> {
2407 const Z_DEFAULT_COMPRESSION: i32 = 6;
2408
2409 let parse_compression =
2410 |raw: &str| -> Result<i32> { Self::parse_zlib_compression_level(raw) };
2411
2412 let mut pack_level = Z_DEFAULT_COMPRESSION;
2414 let mut pack_compression_seen = false;
2415
2416 for e in self.entries() {
2417 match e.key.as_str() {
2418 "core.compression" => {
2419 let Some(val) = e.value.as_deref() else {
2420 continue;
2421 };
2422 let level = parse_compression(val)?;
2423 if !pack_compression_seen {
2424 pack_level = level;
2425 }
2426 }
2427 "pack.compression" => {
2428 let Some(val) = e.value.as_deref() else {
2429 continue;
2430 };
2431 pack_level = parse_compression(val)?;
2432 pack_compression_seen = true;
2433 }
2434 _ => {}
2435 }
2436 }
2437
2438 Ok(pack_level)
2439 }
2440
2441 pub fn get_regexp(&self, pattern: &str) -> std::result::Result<Vec<&ConfigEntry>, String> {
2446 let re = regex::Regex::new(pattern).map_err(|e| format!("invalid key pattern: {e}"))?;
2447 Ok(self
2448 .entries
2449 .iter()
2450 .filter(|e| re.is_match(&e.key))
2451 .collect())
2452 }
2453
2454 pub fn load(env: &Environment, git_dir: Option<&Path>, include_system: bool) -> Result<Self> {
2465 let opts = LoadConfigOptions {
2466 include_system,
2467 include_ctx: IncludeContext {
2468 git_dir: git_dir.map(PathBuf::from),
2469 cwd: env.cwd.clone(),
2470 pwd: env.pwd.clone(),
2471 env: std::sync::Arc::new(env.clone()),
2472 ..Default::default()
2473 },
2474 ..Default::default()
2475 };
2476 Self::load_with_options(env, git_dir, &opts)
2477 }
2478
2479 pub fn load_with_options(
2486 env: &Environment,
2487 git_dir: Option<&Path>,
2488 opts: &LoadConfigOptions,
2489 ) -> Result<Self> {
2490 Self::load_with_options_uncached(env, git_dir, opts, &mut Vec::new())
2491 }
2492
2493 fn read_local_config_text_optional(
2494 path: &Path,
2495 caches: Option<&std::sync::Arc<crate::repo_caches::RepoCaches>>,
2496 ) -> Result<Option<String>> {
2497 let read = |p: &Path| -> Result<Option<String>> {
2498 match fs::read_to_string(p) {
2499 Ok(text) => Ok(Some(text)),
2500 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
2501 Err(e) => Err(Error::Io(e)),
2502 }
2503 };
2504 let Some(c) = caches else {
2505 return read(path);
2506 };
2507 match c.local_config_text(path) {
2508 Ok(text) => Ok(Some(text)),
2509 Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
2510 Err(e) => Err(e),
2511 }
2512 }
2513
2514 pub(crate) fn load_with_options_uncached(
2515 env: &Environment,
2516 git_dir: Option<&Path>,
2517 opts: &LoadConfigOptions,
2518 included_files: &mut Vec<PathBuf>,
2519 ) -> Result<Self> {
2520 let proc = opts.process_includes;
2521 let mut ctx = opts.include_ctx.clone();
2522 if ctx.cwd.as_os_str().is_empty() {
2523 ctx.cwd = env.cwd.clone();
2524 }
2525 ctx.pwd = env.pwd.clone();
2526 ctx.env = std::sync::Arc::new(env.clone());
2527 let mut layers = Vec::new();
2528
2529 if opts.include_system && !git_config_nosystem_enabled(env) {
2530 let system_path = system_config_path(env);
2531 if let Some(f) = ConfigFile::from_path(&system_path, ConfigScope::System)? {
2532 layers.push(ConfigLayer::file(f));
2533 }
2534 }
2535
2536 for path in global_config_paths(env) {
2538 if let Some(f) = ConfigFile::from_path(&path, ConfigScope::Global)? {
2539 layers.push(ConfigLayer::file(f));
2540 }
2541 }
2542
2543 if let Some(gd) = git_dir {
2545 let common_dir = crate::repo::common_git_dir_for_config(gd);
2546 let local_path = common_dir.join("config");
2547 let local_text =
2548 Self::read_local_config_text_optional(&local_path, opts.repo_caches.as_ref())?;
2549 if let Some(msg) =
2550 crate::repo::early_config_ignore_repo_reason_for_text(local_text.as_deref())
2551 {
2552 if let Some(sink) = opts.diagnostics.as_ref() {
2553 sink.warn(crate::diagnostics::Warning::IgnoredGitDir {
2554 path: gd.to_path_buf(),
2555 reason: msg,
2556 });
2557 }
2558 } else if let Some(text) = local_text.as_deref() {
2559 let f = ConfigFile::parse(&local_path, text, ConfigScope::Local)?;
2560 layers.push(ConfigLayer::file(f));
2561 }
2562
2563 let wt_path = gd.join("config.worktree");
2566 if crate::repo::worktree_config_enabled_for_text(local_text.as_deref()) {
2567 if let Some(f) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree)? {
2568 layers.push(ConfigLayer::file(f));
2569 }
2570 }
2571 }
2572
2573 if let Some(path) = env.git_config.as_deref() {
2574 let path = resolve_config_override_path(env, path);
2575 if let Some(f) = ConfigFile::from_path(&path, ConfigScope::Command)? {
2576 layers.push(ConfigLayer::file(f));
2577 }
2578 }
2579
2580 layers.push(ConfigLayer::Entries(environment_config_pair_entries(env)?));
2581
2582 if let Some(params) = env
2584 .git_config_parameters
2585 .as_deref()
2586 .filter(|p| !p.trim().is_empty())
2587 {
2588 if proc && opts.command_includes {
2589 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
2590 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, params)?;
2591 layers.push(ConfigLayer::file(cmd_file));
2592 } else {
2593 layers.push(ConfigLayer::Entries(command_override_entries(params)));
2594 }
2595 }
2596
2597 let mut set = Self::new();
2598 set.merge_layers(&layers, proc, &ctx, included_files)?;
2599 Ok(set)
2600 }
2601
2602 pub fn read_early_config(
2614 env: &Environment,
2615 git_dir: Option<&Path>,
2616 key: &str,
2617 ) -> Result<Vec<String>> {
2618 let ctx = IncludeContext {
2619 git_dir: git_dir.map(PathBuf::from),
2620 command_line_relative_include_is_error: false,
2621 cwd: env.cwd.clone(),
2622 pwd: env.pwd.clone(),
2623 env: std::sync::Arc::new(env.clone()),
2624 };
2625 let mut layers = Vec::new();
2626
2627 if !git_config_nosystem_enabled(env) {
2628 let system_path = system_config_path(env);
2629 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2630 layers.push(ConfigLayer::file(f));
2631 }
2632 }
2633
2634 for path in global_config_paths(env) {
2636 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2637 layers.push(ConfigLayer::file(f));
2638 }
2639 }
2640
2641 if let Some(gd) = git_dir {
2642 let common_dir = crate::repo::common_git_dir_for_config(gd);
2643 let local_path = common_dir.join("config");
2645 if crate::repo::early_config_ignore_repo_reason(&common_dir).is_some() {
2646 } else if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2648 layers.push(ConfigLayer::file(f));
2649 }
2650
2651 let wt_path = gd.join("config.worktree");
2653 if crate::repo::worktree_config_enabled(&common_dir) {
2654 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
2655 layers.push(ConfigLayer::file(f));
2656 }
2657 }
2658 }
2659
2660 if let Some(params) = env
2662 .git_config_parameters
2663 .as_deref()
2664 .filter(|p| !p.trim().is_empty())
2665 {
2666 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
2667 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, params)?;
2668 layers.push(ConfigLayer::file(cmd_file));
2669 }
2670
2671 let mut set = Self::new();
2672 set.merge_layers(&layers, true, &ctx, &mut Vec::new())?;
2673 Ok(set.get_all(key))
2674 }
2675
2676 pub fn merge_file_with_includes(
2689 &mut self,
2690 file: &ConfigFile,
2691 process_includes: bool,
2692 ctx: &IncludeContext,
2693 ) -> Result<()> {
2694 self.merge_layers(
2695 &[ConfigLayer::File(Cow::Borrowed(file))],
2696 process_includes,
2697 ctx,
2698 &mut Vec::new(),
2699 )
2700 }
2701
2702 pub fn load_repo_local_only(git_dir: &Path) -> Result<Self> {
2708 let local_path = git_dir.join("config");
2709 let ctx = IncludeContext {
2710 git_dir: Some(git_dir.to_path_buf()),
2711 command_line_relative_include_is_error: false,
2712 cwd: PathBuf::from("."),
2713 pwd: None,
2714 env: std::sync::Arc::new(Environment::empty()),
2715 };
2716 let mut set = Self::new();
2717 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2718 set.merge_layers(&[ConfigLayer::file(f)], true, &ctx, &mut Vec::new())?;
2719 }
2720 Ok(set)
2721 }
2722
2723 pub fn load_protected(env: &Environment, include_system: bool) -> Result<Self> {
2734 let ctx = IncludeContext {
2735 git_dir: None,
2736 command_line_relative_include_is_error: false,
2737 cwd: env.cwd.clone(),
2738 pwd: env.pwd.clone(),
2739 env: std::sync::Arc::new(env.clone()),
2740 };
2741 let mut layers = Vec::new();
2742
2743 if include_system && !git_config_nosystem_enabled(env) {
2744 let system_path = system_config_path(env);
2745 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2746 layers.push(ConfigLayer::file(f));
2747 }
2748 }
2749
2750 for path in global_config_paths(env) {
2751 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2752 layers.push(ConfigLayer::file(f));
2753 }
2754 }
2755
2756 layers.push(ConfigLayer::Entries(environment_config_pair_entries(env)?));
2757 if let Some(params) = env.git_config_parameters.as_deref() {
2758 layers.push(ConfigLayer::Entries(command_override_entries(params)));
2759 }
2760
2761 let mut set = Self::new();
2762 set.merge_layers(&layers, true, &ctx, &mut Vec::new())?;
2763 Ok(set)
2764 }
2765
2766 fn merge_layers(
2773 &mut self,
2774 layers: &[ConfigLayer<'_>],
2775 process_includes: bool,
2776 ctx: &IncludeContext,
2777 included_files: &mut Vec<PathBuf>,
2778 ) -> Result<()> {
2779 const INCLUDE_HEADROOM: usize = 64;
2782 let mut remote_urls = RemoteUrls::new(layers);
2783 let top_level: usize = layers
2784 .iter()
2785 .map(|layer| match layer {
2786 ConfigLayer::File(file) => file.entries.len(),
2787 ConfigLayer::Entries(entries) => entries.len(),
2788 })
2789 .sum();
2790 self.entries.reserve(top_level + INCLUDE_HEADROOM);
2791 for layer in layers {
2792 match layer {
2793 ConfigLayer::File(file) => Self::merge_with_includes(
2794 self,
2795 file,
2796 process_includes,
2797 0,
2798 ctx,
2799 &mut remote_urls,
2800 included_files,
2801 )?,
2802 ConfigLayer::Entries(entries) => self.entries.extend(entries.iter().cloned()),
2803 }
2804 }
2805 Ok(())
2806 }
2807
2808 fn merge_with_includes(
2810 set: &mut Self,
2811 file: &ConfigFile,
2812 process_includes: bool,
2813 depth: usize,
2814 ctx: &IncludeContext,
2815 remote_urls: &mut RemoteUrls<'_>,
2816 included_files: &mut Vec<PathBuf>,
2817 ) -> Result<()> {
2818 check_include_depth(depth)?;
2819 if !process_includes {
2820 set.merge(file);
2821 return Ok(());
2822 }
2823
2824 for entry in &file.entries {
2825 set.entries.push(entry.clone());
2826
2827 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
2828 continue;
2829 };
2830 if let Some(cond) = condition.as_deref() {
2831 let holds = match hasconfig_remote_url_pattern(cond) {
2832 Some(pattern) => remote_urls.any_matches(pattern, ctx, included_files)?,
2833 None => evaluate_include_condition(cond, file, ctx),
2834 };
2835 if !holds {
2836 continue;
2837 }
2838 }
2839 let Some(inc_file) = open_include_target(&inc_path, file, ctx, included_files)? else {
2840 continue;
2841 };
2842 Self::merge_with_includes(
2843 set,
2844 &inc_file,
2845 process_includes,
2846 depth + 1,
2847 ctx,
2848 remote_urls,
2849 included_files,
2850 )?;
2851 }
2852
2853 Ok(())
2854 }
2855}
2856
2857enum ConfigLayer<'a> {
2859 File(Cow<'a, ConfigFile>),
2861 Entries(Vec<ConfigEntry>),
2863}
2864
2865impl ConfigLayer<'_> {
2866 fn file(file: ConfigFile) -> Self {
2867 Self::File(Cow::Owned(file))
2868 }
2869}
2870
2871struct RemoteUrls<'a> {
2874 layers: &'a [ConfigLayer<'a>],
2875 urls: Option<Vec<String>>,
2876}
2877
2878impl<'a> RemoteUrls<'a> {
2879 fn new(layers: &'a [ConfigLayer<'a>]) -> Self {
2880 Self { layers, urls: None }
2881 }
2882
2883 fn any_matches(
2885 &mut self,
2886 pattern: &str,
2887 ctx: &IncludeContext,
2888 included_files: &mut Vec<PathBuf>,
2889 ) -> Result<bool> {
2890 let urls = match self.urls.take() {
2891 Some(urls) => urls,
2892 None => self.collect(ctx, included_files)?,
2893 };
2894 let hit = urls
2895 .iter()
2896 .any(|url| wildmatch(pattern.as_bytes(), url.as_bytes(), WM_PATHNAME));
2897 self.urls = Some(urls);
2898 Ok(hit)
2899 }
2900
2901 fn collect(
2902 &self,
2903 ctx: &IncludeContext,
2904 included_files: &mut Vec<PathBuf>,
2905 ) -> Result<Vec<String>> {
2906 let mut urls = Vec::new();
2907 for layer in self.layers {
2908 match layer {
2909 ConfigLayer::File(file) => {
2910 collect_remote_urls(file, false, 0, ctx, &mut urls, included_files)?;
2911 }
2912 ConfigLayer::Entries(entries) => urls.extend(
2913 entries
2914 .iter()
2915 .filter(|entry| is_remote_url_entry(entry))
2916 .filter_map(|entry| entry.value.clone()),
2917 ),
2918 }
2919 }
2920 Ok(urls)
2921 }
2922}
2923
2924fn collect_remote_urls(
2931 file: &ConfigFile,
2932 in_conditional_include: bool,
2933 depth: usize,
2934 ctx: &IncludeContext,
2935 urls: &mut Vec<String>,
2936 included_files: &mut Vec<PathBuf>,
2937) -> Result<()> {
2938 check_include_depth(depth)?;
2939 for entry in &file.entries {
2940 if is_remote_url_entry(entry) {
2941 if in_conditional_include {
2942 return Err(ConfigError::RemoteUrlInHasconfigInclude.into());
2943 }
2944 urls.extend(entry.value.clone());
2945 continue;
2946 }
2947 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
2948 continue;
2949 };
2950 let conditional = match condition.as_deref() {
2951 None => in_conditional_include,
2952 Some(cond) => {
2953 if hasconfig_remote_url_pattern(cond).is_none()
2954 && !evaluate_include_condition(cond, file, ctx)
2955 {
2956 continue;
2957 }
2958 true
2959 }
2960 };
2961 let Some(inc_file) = open_include_target(&inc_path, file, ctx, included_files)? else {
2962 continue;
2963 };
2964 collect_remote_urls(&inc_file, conditional, depth + 1, ctx, urls, included_files)?;
2965 }
2966 Ok(())
2967}
2968
2969fn check_include_depth(depth: usize) -> Result<()> {
2971 const MAX_INCLUDE_DEPTH: usize = 10;
2972 if depth > MAX_INCLUDE_DEPTH {
2973 return Err(ConfigError::IncludeDepthExceeded {
2974 depth,
2975 limit: MAX_INCLUDE_DEPTH,
2976 }
2977 .into());
2978 }
2979 Ok(())
2980}
2981
2982fn open_include_target(
2988 inc_path: &str,
2989 file: &ConfigFile,
2990 ctx: &IncludeContext,
2991 included_files: &mut Vec<PathBuf>,
2992) -> Result<Option<ConfigFile>> {
2993 let resolved = match resolve_include_file_path(inc_path, file, ctx) {
2994 Ok(p) => p,
2995 Err(Error::Config(ConfigError::Other(msg))) if msg.is_empty() => return Ok(None),
2996 Err(e) => return Err(e),
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(crate) 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 .map_err(|_| Error::Config("bogus count in GIT_CONFIG_COUNT".to_owned().into()))?;
3197 if count > i32::MAX as usize {
3198 return Err(Error::Config(
3199 "too many entries in GIT_CONFIG_COUNT".to_owned().into(),
3200 ));
3201 }
3202
3203 let mut set = ConfigSet::new();
3204 for i in 0..count {
3205 let (key, value) = env.git_config_pairs.get(i).ok_or_else(|| {
3206 Error::Config(format!("missing config key GIT_CONFIG_KEY_{i}").into())
3207 })?;
3208 set.add_command_override(key, value)?;
3209 }
3210 Ok(set.entries)
3211}
3212
3213fn command_override_entries(params: &str) -> Vec<ConfigEntry> {
3216 let mut set = ConfigSet::new();
3217 for entry in parse_config_parameters(params) {
3218 let (key, val) = entry
3219 .split_once('\u{1}')
3220 .or_else(|| entry.split_once('='))
3221 .unwrap_or((entry.as_str(), "true"));
3222 let _ = set.add_command_override(key.trim(), val);
3223 }
3224 set.entries
3225}
3226
3227pub fn parse_bool(s: &str) -> std::result::Result<bool, ConfigValueError> {
3244 match s.to_lowercase().as_str() {
3245 "true" | "yes" | "on" => Ok(true),
3246 "" => Ok(false),
3247 "false" | "no" | "off" => Ok(false),
3248 _ => parse_git_config_maybe_bool(s).ok_or_else(|| ConfigValueError::InvalidBool {
3249 value: s.to_owned(),
3250 }),
3251 }
3252}
3253
3254pub fn parse_i64(s: &str) -> std::result::Result<i64, ConfigValueError> {
3262 let s = s.trim();
3263 if s.is_empty() {
3264 return Err(ConfigValueError::EmptyInteger);
3265 }
3266
3267 let (num_str, multiplier) = match s.as_bytes().last() {
3268 Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
3269 Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
3270 Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
3271 _ => (s, 1_i64),
3272 };
3273
3274 let base: i64 = num_str
3275 .parse()
3276 .map_err(|_| ConfigValueError::InvalidInteger {
3277 value: s.to_owned(),
3278 })?;
3279 base.checked_mul(multiplier)
3280 .ok_or_else(|| ConfigValueError::IntegerOverflow {
3281 value: s.to_owned(),
3282 })
3283}
3284
3285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3287pub enum GitConfigIntStrictError {
3288 InvalidUnit,
3290 OutOfRange,
3292}
3293
3294pub fn parse_git_config_int_strict(raw: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
3298 let s = raw.trim();
3299 if s.is_empty() {
3300 return Err(GitConfigIntStrictError::InvalidUnit);
3301 }
3302
3303 let bytes = s.as_bytes();
3304 let mut idx = 0usize;
3305 if matches!(bytes.first(), Some(b'+') | Some(b'-')) {
3306 idx = 1;
3307 }
3308 if idx >= bytes.len() {
3309 return Err(GitConfigIntStrictError::InvalidUnit);
3310 }
3311 let digit_start = idx;
3312 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
3313 idx += 1;
3314 }
3315 if idx == digit_start {
3316 return Err(GitConfigIntStrictError::InvalidUnit);
3317 }
3318
3319 let num_part =
3320 std::str::from_utf8(&bytes[..idx]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3321 let suffix =
3322 std::str::from_utf8(&bytes[idx..]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3323 let mult: i64 = match suffix {
3324 "" => 1,
3325 "k" | "K" => 1024,
3326 "m" | "M" => 1024 * 1024,
3327 "g" | "G" => 1024_i64
3328 .checked_mul(1024)
3329 .and_then(|x| x.checked_mul(1024))
3330 .ok_or(GitConfigIntStrictError::OutOfRange)?,
3331 _ => return Err(GitConfigIntStrictError::InvalidUnit),
3332 };
3333
3334 let val: i64 = num_part
3335 .parse()
3336 .map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3337 val.checked_mul(mult)
3338 .ok_or(GitConfigIntStrictError::OutOfRange)
3339}
3340
3341fn is_c_locale_space(b: u8) -> bool {
3342 matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
3343}
3344
3345fn skip_c_locale_leading_whitespace(s: &str) -> &str {
3346 let bytes = s.as_bytes();
3347 let mut i = 0usize;
3348 while i < bytes.len() && is_c_locale_space(bytes[i]) {
3349 i += 1;
3350 }
3351 &s[i..]
3352}
3353
3354fn git_unit_factor(suffix: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
3355 match suffix {
3356 "" => Ok(1),
3357 "k" | "K" => Ok(1024),
3358 "m" | "M" => Ok(1024 * 1024),
3359 "g" | "G" => Ok(1024_i64 * 1024 * 1024),
3360 _ => Err(GitConfigIntStrictError::InvalidUnit),
3361 }
3362}
3363
3364fn parse_base0_signed_prefix(s: &str) -> std::result::Result<(i64, &str), GitConfigIntStrictError> {
3366 let bytes = s.as_bytes();
3367 if bytes.is_empty() {
3368 return Err(GitConfigIntStrictError::InvalidUnit);
3369 }
3370 let mut i = 0usize;
3371 let mut sign: i64 = 1;
3372 if bytes[i] == b'+' {
3373 i += 1;
3374 } else if bytes[i] == b'-' {
3375 sign = -1;
3376 i += 1;
3377 }
3378 if i >= bytes.len() {
3379 return Err(GitConfigIntStrictError::InvalidUnit);
3380 }
3381
3382 let base = if bytes[i] == b'0' {
3383 if i + 1 < bytes.len() && matches!(bytes[i + 1], b'x' | b'X') {
3384 i += 2;
3385 if i >= bytes.len() {
3386 return Err(GitConfigIntStrictError::InvalidUnit);
3387 }
3388 16
3389 } else {
3390 8
3391 }
3392 } else {
3393 10
3394 };
3395
3396 let digit_start = i;
3397 let mut acc: i128 = 0;
3398 while i < bytes.len() {
3399 let ch = char::from(bytes[i]);
3400 let digit = match base {
3401 10 => ch.to_digit(10),
3402 8 => ch.to_digit(8),
3403 16 => ch.to_digit(16),
3404 _ => None,
3405 };
3406 let Some(d) = digit else {
3407 break;
3408 };
3409 acc = acc
3410 .checked_mul(base as i128)
3411 .and_then(|v| v.checked_add(i128::from(d)))
3412 .ok_or(GitConfigIntStrictError::OutOfRange)?;
3413 i += 1;
3414 }
3415 if i == digit_start {
3416 return Err(GitConfigIntStrictError::InvalidUnit);
3417 }
3418
3419 let signed = if sign < 0 { -acc } else { acc };
3420 let val = i64::try_from(signed).map_err(|_| GitConfigIntStrictError::OutOfRange)?;
3421 Ok((val, &s[i..]))
3422}
3423
3424pub fn parse_git_signed_config(
3426 raw: &str,
3427 max: i64,
3428) -> std::result::Result<i64, GitConfigIntStrictError> {
3429 if max < 0 {
3430 return Err(GitConfigIntStrictError::InvalidUnit);
3431 }
3432 let s = skip_c_locale_leading_whitespace(raw);
3433 if s.is_empty() {
3434 return Err(GitConfigIntStrictError::InvalidUnit);
3435 }
3436
3437 let (val, rest) = parse_base0_signed_prefix(s)?;
3438 let factor = git_unit_factor(rest)?;
3439
3440 if val < 0 {
3441 let min = -max - 1;
3442 if min / factor > val {
3443 return Err(GitConfigIntStrictError::OutOfRange);
3444 }
3445 } else if val > 0 && max / factor < val {
3446 return Err(GitConfigIntStrictError::OutOfRange);
3447 }
3448
3449 val.checked_mul(factor)
3450 .ok_or(GitConfigIntStrictError::OutOfRange)
3451}
3452
3453pub fn parse_git_config_int(raw: &str) -> std::result::Result<i32, GitConfigIntStrictError> {
3455 let max = i32::MAX as i64;
3456 let n = parse_git_signed_config(raw, max)?;
3457 i32::try_from(n).map_err(|_| GitConfigIntStrictError::OutOfRange)
3458}
3459
3460pub fn parse_git_config_maybe_bool(raw: &str) -> Option<bool> {
3462 if raw.is_empty() {
3463 return Some(false);
3464 }
3465 let lower = raw.to_ascii_lowercase();
3466 match lower.as_str() {
3467 "true" | "yes" | "on" => Some(true),
3468 "false" | "no" | "off" => Some(false),
3469 _ => parse_git_config_int(raw).ok().map(|n| n != 0),
3470 }
3471}
3472
3473const DIFF_CONTEXT_KEY: &str = "diff.context";
3474
3475const DEFAULT_GC_AUTO_THRESHOLD: usize = 6700;
3477const DEFAULT_GC_AUTO_PACK_LIMIT: usize = 50;
3479
3480fn bad_numeric_diff_context(
3481 value: &str,
3482 err: GitConfigIntStrictError,
3483 entry: &ConfigEntry,
3484) -> ConfigError {
3485 let reason = match err {
3486 GitConfigIntStrictError::InvalidUnit => crate::error::BadNumericSource::InvalidUnit,
3487 GitConfigIntStrictError::OutOfRange => crate::error::BadNumericSource::OutOfRange,
3488 };
3489 if entry.scope == ConfigScope::Command || entry.file.is_none() {
3490 ConfigError::BadNumericValue {
3491 key: DIFF_CONTEXT_KEY.to_owned(),
3492 value: value.to_owned(),
3493 reason,
3494 }
3495 } else {
3496 let path = entry
3497 .file
3498 .as_deref()
3499 .map(config_error_path_display)
3500 .unwrap_or_default();
3501 ConfigError::BadNumericValueInFile {
3502 key: DIFF_CONTEXT_KEY.to_owned(),
3503 value: value.to_owned(),
3504 file: path,
3505 reason,
3506 }
3507 }
3508}
3509
3510fn bad_diff_context_variable(entry: &ConfigEntry) -> ConfigError {
3511 if entry.scope == ConfigScope::Command || entry.file.is_none() {
3512 ConfigError::UnableToParseFromCommandLine {
3513 key: DIFF_CONTEXT_KEY.to_owned(),
3514 }
3515 } else {
3516 let path = entry
3517 .file
3518 .as_deref()
3519 .map(config_error_path_display)
3520 .unwrap_or_default();
3521 ConfigError::BadConfigVariableAtLine {
3522 key: DIFF_CONTEXT_KEY.to_owned(),
3523 file: path,
3524 line: entry.line,
3525 }
3526 }
3527}
3528
3529pub fn resolve_diff_context_lines(cfg: &ConfigSet) -> Result<Option<usize>> {
3534 let Some(entry) = cfg.get_last_entry(DIFF_CONTEXT_KEY) else {
3535 return Ok(None);
3536 };
3537 let value_src = entry.value.as_deref().unwrap_or("").trim();
3538 match parse_git_config_int_strict(value_src) {
3539 Ok(n) if n < 0 => Err(bad_diff_context_variable(&entry).into()),
3540 Ok(n) => Ok(Some(usize::try_from(n).map_err(|_| {
3541 bad_numeric_diff_context(value_src, GitConfigIntStrictError::OutOfRange, &entry)
3542 })?)),
3543 Err(e) => Err(bad_numeric_diff_context(value_src, e, &entry).into()),
3544 }
3545}
3546
3547pub fn parse_color(s: &str) -> std::result::Result<String, ConfigValueError> {
3559 const COLOR_BACKGROUND_OFFSET: i32 = 10;
3560 const COLOR_FOREGROUND_ANSI: i32 = 30;
3561 const COLOR_FOREGROUND_RGB: i32 = 38;
3562 const COLOR_FOREGROUND_256: i32 = 38;
3563 const COLOR_FOREGROUND_BRIGHT_ANSI: i32 = 90;
3564
3565 #[derive(Clone, Copy, Default)]
3566 struct Color {
3567 kind: u8,
3568 value: u8,
3569 red: u8,
3570 green: u8,
3571 blue: u8,
3572 }
3573
3574 const COLOR_UNSPECIFIED: u8 = 0;
3575 const COLOR_NORMAL: u8 = 1;
3576 const COLOR_ANSI: u8 = 2;
3577 const COLOR_256: u8 = 3;
3578 const COLOR_RGB: u8 = 4;
3579
3580 fn color_empty(c: &Color) -> bool {
3581 c.kind == COLOR_UNSPECIFIED || c.kind == COLOR_NORMAL
3582 }
3583
3584 fn parse_ansi_color(name: &str) -> Option<Color> {
3585 let color_names = [
3586 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
3587 ];
3588 let color_offset = COLOR_FOREGROUND_ANSI;
3589
3590 if name.eq_ignore_ascii_case("default") {
3591 return Some(Color {
3592 kind: COLOR_ANSI,
3593 value: (9 + color_offset) as u8,
3594 ..Default::default()
3595 });
3596 }
3597
3598 let (name, color_offset) = if name.len() >= 6 && name[..6].eq_ignore_ascii_case("bright") {
3599 (&name[6..], COLOR_FOREGROUND_BRIGHT_ANSI)
3600 } else {
3601 (name, COLOR_FOREGROUND_ANSI)
3602 };
3603
3604 for (i, cn) in color_names.iter().enumerate() {
3605 if name.eq_ignore_ascii_case(cn) {
3606 return Some(Color {
3607 kind: COLOR_ANSI,
3608 value: (i as i32 + color_offset) as u8,
3609 ..Default::default()
3610 });
3611 }
3612 }
3613 None
3614 }
3615
3616 fn hex_val(b: u8) -> Option<u8> {
3617 match b {
3618 b'0'..=b'9' => Some(b - b'0'),
3619 b'a'..=b'f' => Some(b - b'a' + 10),
3620 b'A'..=b'F' => Some(b - b'A' + 10),
3621 _ => None,
3622 }
3623 }
3624
3625 fn get_hex_color(chars: &[u8], width: usize) -> Option<(u8, usize)> {
3626 assert!(width == 1 || width == 2);
3627 if chars.len() < width {
3628 return None;
3629 }
3630 let v = if width == 2 {
3631 let hi = hex_val(chars[0])?;
3632 let lo = hex_val(chars[1])?;
3633 (hi << 4) | lo
3634 } else {
3635 let n = hex_val(chars[0])?;
3636 (n << 4) | n
3637 };
3638 Some((v, width))
3639 }
3640
3641 fn parse_single_color(word: &str) -> Option<Color> {
3642 if word.eq_ignore_ascii_case("normal") {
3643 return Some(Color {
3644 kind: COLOR_NORMAL,
3645 ..Default::default()
3646 });
3647 }
3648
3649 let bytes = word.as_bytes();
3650 if (bytes.len() == 7 || bytes.len() == 4) && bytes.first() == Some(&b'#') {
3651 let width = if bytes.len() == 7 { 2 } else { 1 };
3652 let mut idx = 1;
3653 let (r, n1) = get_hex_color(&bytes[idx..], width)?;
3654 idx += n1;
3655 let (g, n2) = get_hex_color(&bytes[idx..], width)?;
3656 idx += n2;
3657 let (b, n3) = get_hex_color(&bytes[idx..], width)?;
3658 idx += n3;
3659 if idx != bytes.len() {
3660 return None;
3661 }
3662 return Some(Color {
3663 kind: COLOR_RGB,
3664 red: r,
3665 green: g,
3666 blue: b,
3667 ..Default::default()
3668 });
3669 }
3670
3671 if let Some(c) = parse_ansi_color(word) {
3672 return Some(c);
3673 }
3674
3675 let Ok(val) = word.parse::<i64>() else {
3676 return None;
3677 };
3678 if val < -1 {
3679 return None;
3680 }
3681 if val < 0 {
3682 return Some(Color {
3683 kind: COLOR_NORMAL,
3684 ..Default::default()
3685 });
3686 }
3687 if val < 8 {
3688 return Some(Color {
3689 kind: COLOR_ANSI,
3690 value: (val as i32 + COLOR_FOREGROUND_ANSI) as u8,
3691 ..Default::default()
3692 });
3693 }
3694 if val < 16 {
3695 return Some(Color {
3696 kind: COLOR_ANSI,
3697 value: (val as i32 - 8 + COLOR_FOREGROUND_BRIGHT_ANSI) as u8,
3698 ..Default::default()
3699 });
3700 }
3701 if val < 256 {
3702 return Some(Color {
3703 kind: COLOR_256,
3704 value: val as u8,
3705 ..Default::default()
3706 });
3707 }
3708 None
3709 }
3710
3711 fn parse_attr(word: &str) -> Option<u8> {
3712 const ATTRS: [(&str, u8, u8); 8] = [
3713 ("bold", 1, 22),
3714 ("dim", 2, 22),
3715 ("italic", 3, 23),
3716 ("ul", 4, 24),
3717 ("underline", 4, 24),
3718 ("blink", 5, 25),
3719 ("reverse", 7, 27),
3720 ("strike", 9, 29),
3721 ];
3722
3723 let mut negate = false;
3724 let mut rest = word;
3725 if let Some(stripped) = rest.strip_prefix("no") {
3726 negate = true;
3727 rest = stripped;
3728 if let Some(s) = rest.strip_prefix('-') {
3729 rest = s;
3730 }
3731 }
3732
3733 for (name, val, neg) in ATTRS {
3734 if rest == name {
3735 return Some(if negate { neg } else { val });
3736 }
3737 }
3738 None
3739 }
3740
3741 fn append_color_output(out: &mut String, c: &Color, background: bool) {
3742 let offset = if background {
3743 COLOR_BACKGROUND_OFFSET
3744 } else {
3745 0
3746 };
3747 match c.kind {
3748 COLOR_UNSPECIFIED | COLOR_NORMAL => {}
3749 COLOR_ANSI => {
3750 use std::fmt::Write;
3751 let _ = write!(out, "{}", i32::from(c.value) + offset);
3752 }
3753 COLOR_256 => {
3754 use std::fmt::Write;
3755 let _ = write!(out, "{};5;{}", COLOR_FOREGROUND_256 + offset, c.value);
3756 }
3757 COLOR_RGB => {
3758 use std::fmt::Write;
3759 let _ = write!(
3760 out,
3761 "{};2;{};{};{}",
3762 COLOR_FOREGROUND_RGB + offset,
3763 c.red,
3764 c.green,
3765 c.blue
3766 );
3767 }
3768 _ => {}
3769 }
3770 }
3771
3772 let s = s.trim();
3773 let bad_color = || ConfigValueError::InvalidColor {
3774 value: s.to_owned(),
3775 };
3776 if s.is_empty() {
3777 return Ok(String::new());
3778 }
3779
3780 let mut has_reset = false;
3781 let mut attr: u64 = 0;
3782 let mut fg = Color::default();
3783 let mut bg = Color::default();
3784 fg.kind = COLOR_UNSPECIFIED;
3785 bg.kind = COLOR_UNSPECIFIED;
3786
3787 for word in s.split_whitespace() {
3788 if word.eq_ignore_ascii_case("reset") {
3789 has_reset = true;
3790 continue;
3791 }
3792
3793 if let Some(c) = parse_single_color(word) {
3794 if fg.kind == COLOR_UNSPECIFIED {
3795 fg = c;
3796 continue;
3797 }
3798 if bg.kind == COLOR_UNSPECIFIED {
3799 bg = c;
3800 continue;
3801 }
3802 return Err(bad_color());
3803 }
3804
3805 if let Some(code) = parse_attr(word) {
3806 attr |= 1u64 << u64::from(code);
3807 continue;
3808 }
3809
3810 return Err(bad_color());
3811 }
3812
3813 if !has_reset && attr == 0 && color_empty(&fg) && color_empty(&bg) {
3814 return Err(bad_color());
3815 }
3816
3817 let mut out = String::from("\x1b[");
3818 let mut sep = if has_reset { 1u32 } else { 0u32 };
3819
3820 let mut attr_bits = attr;
3821 let mut i = 0u32;
3822 while attr_bits != 0 {
3823 let bit = 1u64 << i;
3824 if attr_bits & bit == 0 {
3825 i += 1;
3826 continue;
3827 }
3828 attr_bits &= !bit;
3829 if sep > 0 {
3830 out.push(';');
3831 }
3832 sep += 1;
3833 use std::fmt::Write;
3834 let _ = write!(out, "{i}");
3835 i += 1;
3836 }
3837
3838 if !color_empty(&fg) {
3839 if sep > 0 {
3840 out.push(';');
3841 }
3842 sep += 1;
3843 append_color_output(&mut out, &fg, false);
3844 }
3845 if !color_empty(&bg) {
3846 if sep > 0 {
3847 out.push(';');
3848 }
3849 append_color_output(&mut out, &bg, true);
3850 }
3851 out.push('m');
3852 Ok(out)
3853}
3854
3855#[derive(Debug, Clone)]
3856struct UrlParts {
3857 scheme: String,
3858 user: Option<String>,
3859 host: String,
3860 port: Option<String>,
3861 path: String,
3862}
3863
3864#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3865struct UrlMatchScore {
3866 host_len: usize,
3867 path_len: usize,
3868 user_matched: bool,
3869}
3870
3871fn parse_config_url(url: &str) -> Option<UrlParts> {
3872 let (scheme, rest) = url.split_once("://")?;
3873 let (authority, path) = match rest.find('/') {
3874 Some(idx) => (&rest[..idx], &rest[idx..]),
3875 None => (rest, "/"),
3876 };
3877 let (user, host_port) = match authority.rsplit_once('@') {
3878 Some((user, host)) => (Some(user.to_owned()), host),
3879 None => (None, authority),
3880 };
3881 let (host, port) = match host_port.rsplit_once(':') {
3882 Some((host, port)) if !host.contains(']') => (host, Some(port.to_owned())),
3883 _ => (host_port, None),
3884 };
3885 Some(UrlParts {
3886 scheme: scheme.to_lowercase(),
3887 user,
3888 host: host.to_lowercase(),
3889 port,
3890 path: if path.is_empty() {
3891 "/".to_owned()
3892 } else {
3893 path.trim_end_matches('/').to_owned()
3894 },
3895 })
3896}
3897
3898fn host_matches(pattern: &str, target: &str) -> bool {
3899 let pattern_parts: Vec<&str> = pattern.split('.').collect();
3900 let target_parts: Vec<&str> = target.split('.').collect();
3901 pattern_parts.len() == target_parts.len()
3902 && pattern_parts
3903 .iter()
3904 .zip(target_parts)
3905 .all(|(pattern, target)| *pattern == "*" || *pattern == target)
3906}
3907
3908fn path_match_len(pattern: &str, target: &str) -> Option<usize> {
3909 let pattern = if pattern.is_empty() { "/" } else { pattern };
3910 let target = if target.is_empty() { "/" } else { target };
3911 if pattern == "/" {
3912 return Some(1);
3913 }
3914 let pattern = pattern.trim_end_matches('/');
3915 if target == pattern
3916 || target
3917 .strip_prefix(pattern)
3918 .is_some_and(|rest| rest.starts_with('/'))
3919 {
3920 Some(pattern.len() + 1)
3921 } else {
3922 None
3923 }
3924}
3925
3926fn url_match_score(pattern_url: &str, target_url: &str) -> Option<UrlMatchScore> {
3927 let pattern = parse_config_url(pattern_url)?;
3928 let target = parse_config_url(target_url)?;
3929 if pattern.scheme != target.scheme {
3930 return None;
3931 }
3932 let user_matched = match pattern.user.as_deref() {
3933 Some(user) if target.user.as_deref() == Some(user) => true,
3934 Some(_) => return None,
3935 None => false,
3936 };
3937 if !host_matches(&pattern.host, &target.host) || pattern.port != target.port {
3938 return None;
3939 }
3940 let path_len = path_match_len(&pattern.path, &target.path)?;
3941 Some(UrlMatchScore {
3942 host_len: pattern.host.len(),
3943 path_len,
3944 user_matched,
3945 })
3946}
3947
3948pub fn url_matches(pattern_url: &str, target_url: &str) -> bool {
3950 url_match_score(pattern_url, target_url).is_some()
3951}
3952
3953pub fn get_urlmatch_entries<'a>(
3955 entries: &'a [ConfigEntry],
3956 section: &str,
3957 variable: &str,
3958 url: &str,
3959) -> Vec<&'a ConfigEntry> {
3960 let section_lower = section.to_lowercase();
3961 let variable_lower = variable.to_lowercase();
3962 let mut matches: Vec<(UrlMatchScore, &'a ConfigEntry)> = Vec::new();
3963
3964 for entry in entries {
3965 let key = &entry.key;
3966 let first_dot = match key.find('.') {
3967 Some(i) => i,
3968 None => continue,
3969 };
3970 let last_dot = match key.rfind('.') {
3971 Some(i) => i,
3972 None => continue,
3973 };
3974 let entry_section = &key[..first_dot];
3975 let entry_variable = &key[last_dot + 1..];
3976 if entry_section.to_lowercase() != section_lower
3977 || entry_variable.to_lowercase() != variable_lower
3978 {
3979 continue;
3980 }
3981 if first_dot == last_dot {
3982 matches.push((
3983 UrlMatchScore {
3984 host_len: 0,
3985 path_len: 0,
3986 user_matched: false,
3987 },
3988 entry,
3989 ));
3990 } else {
3991 let subsection = &key[first_dot + 1..last_dot];
3992 if let Some(score) = url_match_score(subsection, url) {
3993 matches.push((score, entry));
3994 }
3995 }
3996 }
3997 matches.sort_by_key(|a| a.0);
3998 matches.into_iter().map(|(_, e)| e).collect()
3999}
4000
4001pub fn get_urlmatch_all_in_section(
4003 entries: &[ConfigEntry],
4004 section: &str,
4005 url: &str,
4006) -> Vec<(String, String, ConfigScope)> {
4007 let section_lower = section.to_lowercase();
4008 let mut matches: Vec<(String, UrlMatchScore, String, String, ConfigScope)> = Vec::new();
4009
4010 for entry in entries {
4011 let key = &entry.key;
4012 let first_dot = match key.find('.') {
4013 Some(i) => i,
4014 None => continue,
4015 };
4016 let last_dot = match key.rfind('.') {
4017 Some(i) => i,
4018 None => continue,
4019 };
4020 let entry_section = &key[..first_dot];
4021 if entry_section.to_lowercase() != section_lower {
4022 continue;
4023 }
4024 let entry_variable = &key[last_dot + 1..];
4025 let val = entry.value.as_deref().unwrap_or("");
4026 if first_dot == last_dot {
4027 let canonical = format!("{}.{}", section_lower, entry_variable);
4028 matches.push((
4029 entry_variable.to_lowercase(),
4030 UrlMatchScore {
4031 host_len: 0,
4032 path_len: 0,
4033 user_matched: false,
4034 },
4035 val.to_owned(),
4036 canonical,
4037 entry.scope,
4038 ));
4039 } else {
4040 let subsection = &key[first_dot + 1..last_dot];
4041 if let Some(score) = url_match_score(subsection, url) {
4042 let canonical = format!("{}.{}", section_lower, entry_variable);
4043 matches.push((
4044 entry_variable.to_lowercase(),
4045 score,
4046 val.to_owned(),
4047 canonical,
4048 entry.scope,
4049 ));
4050 }
4051 }
4052 }
4053
4054 let mut best: std::collections::BTreeMap<String, (UrlMatchScore, String, String, ConfigScope)> =
4055 std::collections::BTreeMap::new();
4056 for (var, specificity, val, canonical, scope) in matches {
4057 let entry = best.entry(var).or_insert((
4058 UrlMatchScore {
4059 host_len: 0,
4060 path_len: 0,
4061 user_matched: false,
4062 },
4063 String::new(),
4064 String::new(),
4065 scope,
4066 ));
4067 if specificity >= entry.0 {
4068 *entry = (specificity, val, canonical, scope);
4069 }
4070 }
4071 best.into_values()
4072 .map(|(_, val, canonical, scope)| (canonical, val, scope))
4073 .collect()
4074}
4075
4076pub fn parse_path(env: &Environment, s: &str) -> String {
4080 if let Some(rest) = s.strip_prefix("~/") {
4081 if let Some(home) = home_dir(env) {
4082 return home.join(rest).to_string_lossy().to_string();
4083 }
4084 }
4085 s.to_owned()
4086}
4087
4088pub fn parse_path_optional(env: &Environment, s: &str) -> Option<String> {
4093 if let Some(rest) = s.strip_prefix(":(optional)") {
4094 let resolved = parse_path(env, rest);
4095 if std::path::Path::new(&resolved).exists() {
4096 Some(resolved)
4097 } else {
4098 None }
4100 } else {
4101 Some(parse_path(env, s))
4102 }
4103}
4104
4105#[must_use]
4121pub fn git_config_parameters_last_value(raw: &str, key: &str) -> Option<String> {
4122 let Ok(canon) = canonical_key(key) else {
4123 return None;
4124 };
4125 let mut last: Option<String> = None;
4126 for entry in parse_config_parameters_strict(raw).ok()? {
4127 match entry {
4128 ConfigParameter::Pair { key, value } => {
4129 if canonical_key(key.trim()).ok().as_ref() == Some(&canon) {
4130 last = Some(value.unwrap_or_else(|| "true".to_owned()));
4131 }
4132 }
4133 ConfigParameter::OldStyle(entry) => {
4134 if let Some((k, v)) = entry.split_once('=') {
4135 if canonical_key(k.trim()).ok().as_ref() == Some(&canon) {
4136 last = Some(v.to_owned());
4137 }
4138 } else if canonical_key(entry.trim()).ok().as_ref() == Some(&canon) {
4139 last = Some("true".to_owned());
4140 }
4141 }
4142 }
4143 }
4144 last
4145}
4146
4147#[derive(Debug, Clone, PartialEq, Eq)]
4148enum ConfigParameter {
4149 OldStyle(String),
4150 Pair { key: String, value: Option<String> },
4151}
4152
4153pub fn parse_config_parameters(raw: &str) -> Vec<String> {
4157 parse_config_parameters_strict(raw)
4158 .map(|entries| {
4159 entries
4160 .into_iter()
4161 .map(|entry| match entry {
4162 ConfigParameter::OldStyle(entry) => entry,
4163 ConfigParameter::Pair {
4164 key,
4165 value: Some(value),
4166 } => format!("{key}\u{1}{value}"),
4167 ConfigParameter::Pair { key, value: None } => format!("{key}\u{1}"),
4168 })
4169 .collect()
4170 })
4171 .unwrap_or_default()
4172}
4173
4174fn parse_config_parameters_strict(raw: &str) -> Result<Vec<ConfigParameter>> {
4175 let mut out: Vec<ConfigParameter> = Vec::new();
4176 let chars: Vec<char> = raw.chars().collect();
4177 let mut idx = skip_config_parameter_spaces(&chars, 0);
4178
4179 while idx < chars.len() {
4180 let (key, next) = sq_dequote_step_chars(&chars, idx)?;
4181 let Some(next_idx) = next else {
4182 out.push(ConfigParameter::OldStyle(key));
4183 break;
4184 };
4185
4186 if chars[next_idx].is_whitespace() {
4187 out.push(ConfigParameter::OldStyle(key));
4188 idx = skip_config_parameter_spaces(&chars, next_idx);
4189 continue;
4190 }
4191
4192 if chars[next_idx] != '=' {
4193 return Err(Error::Config(
4194 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4195 ));
4196 }
4197
4198 let value_start = next_idx + 1;
4199 if value_start >= chars.len() || chars[value_start].is_whitespace() {
4200 out.push(ConfigParameter::Pair { key, value: None });
4201 idx = skip_config_parameter_spaces(&chars, value_start);
4202 continue;
4203 }
4204
4205 if chars[value_start] != '\'' {
4206 return Err(Error::Config(
4207 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4208 ));
4209 }
4210 let (value, value_next) = sq_dequote_step_chars(&chars, value_start)?;
4211 if let Some(value_next) = value_next {
4212 if !chars[value_next].is_whitespace() {
4213 return Err(Error::Config(
4214 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4215 ));
4216 }
4217 idx = skip_config_parameter_spaces(&chars, value_next);
4218 } else {
4219 idx = chars.len();
4220 }
4221 out.push(ConfigParameter::Pair {
4222 key,
4223 value: Some(value),
4224 });
4225 }
4226
4227 Ok(out)
4228}
4229
4230fn skip_config_parameter_spaces(chars: &[char], mut idx: usize) -> usize {
4231 while idx < chars.len() && chars[idx].is_whitespace() {
4232 idx += 1;
4233 }
4234 idx
4235}
4236
4237fn sq_dequote_step_chars(chars: &[char], start: usize) -> Result<(String, Option<usize>)> {
4238 if chars.get(start) != Some(&'\'') {
4239 return Err(Error::Config(
4240 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4241 ));
4242 }
4243
4244 let mut out = String::new();
4245 let mut idx = start + 1;
4246 loop {
4247 let Some(&ch) = chars.get(idx) else {
4248 return Err(Error::Config(
4249 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4250 ));
4251 };
4252 if ch != '\'' {
4253 out.push(ch);
4254 idx += 1;
4255 continue;
4256 }
4257
4258 idx += 1;
4259 match chars.get(idx).copied() {
4260 None => return Ok((out, None)),
4261 Some('\\')
4262 if chars
4263 .get(idx + 1)
4264 .copied()
4265 .is_some_and(needs_sq_backslash_quote)
4266 && chars.get(idx + 2) == Some(&'\'') =>
4267 {
4268 if let Some(escaped) = chars.get(idx + 1) {
4269 out.push(*escaped);
4270 }
4271 idx += 3;
4272 }
4273 _ => return Ok((out, Some(idx))),
4274 }
4275 }
4276}
4277
4278fn needs_sq_backslash_quote(ch: char) -> bool {
4279 ch == '\'' || ch == '!'
4280}
4281
4282pub(crate) fn resolve_config_override_path(env: &Environment, path: &str) -> PathBuf {
4284 let p = Path::new(path);
4285 if p.is_absolute() {
4286 p.to_path_buf()
4287 } else {
4288 env.cwd.join(p)
4289 }
4290}
4291
4292pub fn global_config_paths_pub(env: &Environment) -> Vec<PathBuf> {
4295 global_config_paths(env)
4296}
4297
4298fn global_config_paths(env: &Environment) -> Vec<PathBuf> {
4299 let mut paths = Vec::new();
4300
4301 if let Some(p) = env.git_config_global.as_deref() {
4303 paths.push(resolve_config_override_path(env, p));
4304 return paths;
4305 }
4306
4307 if let Some(xdg) = env.xdg_config_home.as_deref() {
4309 paths.push(PathBuf::from(xdg).join("git/config"));
4310 } else if let Some(home) = home_dir(env) {
4311 paths.push(home.join(".config/git/config"));
4312 }
4313 if let Some(home) = home_dir(env) {
4314 paths.push(home.join(".gitconfig"));
4315 }
4316
4317 paths
4318}
4319
4320#[must_use]
4325pub fn system_config_path(env: &Environment) -> PathBuf {
4326 if let Some(p) = env.git_config_system.as_deref() {
4327 return resolve_config_override_path(env, p);
4328 }
4329 let unix_default = PathBuf::from("/etc/gitconfig");
4330 #[cfg(windows)]
4331 {
4332 if unix_default.is_file() {
4333 return unix_default;
4334 }
4335 if let Some(root) = env.git_install_root.as_ref() {
4336 let candidate = PathBuf::from(root).join("etc").join("gitconfig");
4337 if candidate.is_file() {
4338 return candidate;
4339 }
4340 }
4341 for pf in [
4342 env.program_files.as_deref(),
4343 env.program_files_x86.as_deref(),
4344 ]
4345 .into_iter()
4346 .flatten()
4347 {
4348 let candidate = PathBuf::from(pf).join("Git").join("etc").join("gitconfig");
4349 if candidate.is_file() {
4350 return candidate;
4351 }
4352 }
4353 }
4354 unix_default
4355}
4356
4357fn home_dir(env: &Environment) -> Option<PathBuf> {
4364 if let Some(home) = env.home.as_ref().filter(|h| !h.is_empty()) {
4365 return Some(PathBuf::from(home));
4366 }
4367 #[cfg(windows)]
4368 {
4369 if let (Some(drive), Some(path)) = (
4370 env.homedrive.as_ref().filter(|d| !d.is_empty()),
4371 env.homepath.as_ref().filter(|p| !p.is_empty()),
4372 ) {
4373 let mut combined = drive.clone();
4374 combined.push(path);
4375 return Some(PathBuf::from(combined));
4376 }
4377 if let Some(profile) = env.userprofile.as_ref().filter(|p| !p.is_empty()) {
4378 return Some(PathBuf::from(profile));
4379 }
4380 }
4381 None
4382}
4383
4384fn include_source_is_disk_file(file: &ConfigFile) -> bool {
4386 file.include_origin == ConfigIncludeOrigin::Disk
4387}
4388
4389fn resolve_include_file_path(
4393 path: &str,
4394 file: &ConfigFile,
4395 ctx: &IncludeContext,
4396) -> Result<PathBuf> {
4397 let expanded = parse_path(ctx.env.as_ref(), path);
4398 let p = Path::new(&expanded);
4399 if p.is_absolute() {
4400 return Ok(p.to_path_buf());
4401 }
4402 if !include_source_is_disk_file(file) {
4403 if file.include_origin == ConfigIncludeOrigin::CommandLine {
4404 if ctx.command_line_relative_include_is_error {
4405 return Err(Error::Config(
4406 "relative config includes must come from files"
4407 .to_owned()
4408 .into(),
4409 ));
4410 }
4411 return Err(Error::Config(String::new().into()));
4412 }
4413 return Err(Error::Config(
4414 "relative config includes must come from files"
4415 .to_owned()
4416 .into(),
4417 ));
4418 }
4419 let base = match file.path.parent() {
4420 Some(p) if !p.as_os_str().is_empty() => p,
4421 Some(_) | None => Path::new("."),
4422 };
4423 Ok(base.join(p))
4424}
4425
4426fn is_dir_sep(b: u8) -> bool {
4427 b == b'/' || b == b'\\'
4428}
4429
4430fn add_trailing_starstar_for_dir(pat: &mut String) {
4431 let bytes = pat.as_bytes();
4432 if bytes.last().is_some_and(|&b| is_dir_sep(b)) {
4433 pat.push_str("**");
4434 }
4435}
4436
4437fn prepare_gitdir_pattern(
4439 condition: &str,
4440 file: &ConfigFile,
4441 ctx: &IncludeContext,
4442) -> Result<(String, usize)> {
4443 let mut pat = parse_path(ctx.env.as_ref(), condition);
4445 if pat.starts_with("./") || pat.starts_with(".\\") {
4446 if !include_source_is_disk_file(file) {
4447 return Err(Error::Config(
4448 "relative config include conditionals must come from files"
4449 .to_owned()
4450 .into(),
4451 ));
4452 }
4453 let parent = file.path.parent().ok_or_else(|| {
4454 Error::Config(
4455 "relative config include conditionals must come from files"
4456 .to_owned()
4457 .into(),
4458 )
4459 })?;
4460 let real = parent.canonicalize_simplified().map_err(Error::Io)?;
4461 let mut dir = real.to_string_lossy().into_owned();
4462 if !dir.ends_with('/') && !dir.ends_with('\\') {
4463 dir.push('/');
4464 }
4465 let rest = &pat[2..];
4466 pat = format!("{dir}{rest}");
4467 let prefix_len = dir.len();
4468 add_trailing_starstar_for_dir(&mut pat);
4469 return Ok((pat, prefix_len));
4470 }
4471 let p = Path::new(&pat);
4472 if !p.is_absolute() {
4473 pat.insert_str(0, "**/");
4474 }
4475 add_trailing_starstar_for_dir(&mut pat);
4476 Ok((pat, 0))
4477}
4478
4479fn git_dir_match_texts(ctx: &IncludeContext, git_dir: &Path) -> (String, String) {
4484 let real = git_dir
4485 .canonicalize_simplified()
4486 .map(|p| p.to_string_lossy().into_owned())
4487 .unwrap_or_else(|_| git_dir.to_string_lossy().into_owned());
4488 let abs = if git_dir.is_absolute() {
4491 let pwd_abs = ctx.pwd.as_deref().and_then(|pwd| {
4494 let pwd_path = std::path::Path::new(pwd);
4495 if !pwd_path.is_absolute() {
4496 return None;
4497 }
4498 let pwd_canon = pwd_path.canonicalize_simplified().ok()?;
4499 let git_dir_str = git_dir.to_string_lossy();
4500 let pwd_canon_str = pwd_canon.to_string_lossy();
4501 let suffix = git_dir_str.strip_prefix(pwd_canon_str.as_ref())?;
4503 Some(format!("{pwd}{suffix}"))
4504 });
4505 pwd_abs.unwrap_or_else(|| git_dir.to_string_lossy().into_owned())
4506 } else {
4507 ctx.cwd.join(git_dir).to_string_lossy().into_owned()
4508 };
4509 (real, abs)
4510}
4511
4512fn include_by_gitdir(
4513 condition: &str,
4514 file: &ConfigFile,
4515 ctx: &IncludeContext,
4516 icase: bool,
4517) -> bool {
4518 let Some(git_dir) = ctx.git_dir.as_ref() else {
4519 return false;
4520 };
4521 let (pattern, prefix) = match prepare_gitdir_pattern(condition, file, ctx) {
4522 Ok(x) => x,
4523 Err(_) => return false,
4524 };
4525 let flags = WM_PATHNAME | if icase { WM_CASEFOLD } else { 0 };
4526 let (text_real, text_abs) = git_dir_match_texts(ctx, git_dir);
4527 let try_match = |text: &str| -> bool {
4528 let t = text.as_bytes();
4529 let p = pattern.as_bytes();
4530 if prefix > 0 {
4531 if t.len() < prefix {
4532 return false;
4533 }
4534 let pre = &p[..prefix];
4535 let te = &t[..prefix];
4536 let ok = if icase {
4537 pre.eq_ignore_ascii_case(te)
4538 } else {
4539 pre == te
4540 };
4541 if !ok {
4542 return false;
4543 }
4544 return wildmatch(&p[prefix..], &t[prefix..], flags);
4545 }
4546 wildmatch(p, t, flags)
4547 };
4548 if try_match(&text_real) {
4549 return true;
4550 }
4551 text_real != text_abs && try_match(&text_abs)
4552}
4553
4554fn current_branch_short_name(git_dir: Option<&Path>) -> Option<String> {
4555 let gd = git_dir?;
4556 let target = refs::read_symbolic_ref(gd, "HEAD").ok()??;
4557 let rest = target.strip_prefix("refs/heads/")?;
4558 Some(rest.to_owned())
4559}
4560
4561fn include_by_onbranch(condition: &str, ctx: &IncludeContext) -> bool {
4562 let Some(short) = current_branch_short_name(ctx.git_dir.as_deref()) else {
4563 return false;
4564 };
4565 let mut pattern = condition.to_owned();
4566 add_trailing_starstar_for_dir(&mut pattern);
4567 wildmatch(pattern.as_bytes(), short.as_bytes(), WM_PATHNAME)
4568}
4569
4570fn is_remote_url_entry(entry: &ConfigEntry) -> bool {
4572 entry
4573 .key
4574 .strip_prefix("remote.")
4575 .is_some_and(|rest| rest.ends_with(".url"))
4576}
4577
4578fn hasconfig_remote_url_pattern(condition: &str) -> Option<&str> {
4580 condition.strip_prefix("hasconfig:remote.*.url:")
4581}
4582
4583fn evaluate_include_condition(condition: &str, file: &ConfigFile, ctx: &IncludeContext) -> bool {
4589 if let Some(rest) = condition.strip_prefix("gitdir/i:") {
4590 return include_by_gitdir(rest, file, ctx, true);
4591 }
4592 if let Some(rest) = condition.strip_prefix("gitdir:") {
4593 return include_by_gitdir(rest, file, ctx, false);
4594 }
4595 if let Some(rest) = condition.strip_prefix("onbranch:") {
4596 return include_by_onbranch(rest, ctx);
4597 }
4598 false
4599}
4600
4601fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
4603 let first_dot = key
4604 .find('.')
4605 .ok_or_else(|| Error::Config(format!("invalid key: '{key}'").into()))?;
4606 let last_dot = key
4607 .rfind('.')
4608 .ok_or_else(|| Error::Config(format!("invalid key: '{key}'").into()))?;
4609
4610 let section = key[..first_dot].to_owned();
4611 let variable = key[last_dot + 1..].to_owned();
4612
4613 let subsection = if first_dot == last_dot {
4614 None
4615 } else {
4616 Some(key[first_dot + 1..last_dot].to_owned())
4617 };
4618
4619 Ok((section, subsection, variable))
4620}
4621
4622fn parse_section_name(name: &str) -> (&str, Option<&str>) {
4626 match name.find('.') {
4627 Some(i) => (&name[..i], Some(&name[i + 1..])),
4628 None => (name, None),
4629 }
4630}
4631
4632fn section_matches(parser: &Parser, section_lower: &str, subsection: Option<&str>) -> bool {
4633 if parser.section.to_lowercase() == section_lower && parser.subsection.as_deref() == subsection
4634 {
4635 return true;
4636 }
4637 let Some(subsection) = subsection else {
4638 return false;
4639 };
4640 parser.subsection.is_none()
4641 && parser.section.to_lowercase() == format!("{section_lower}.{}", subsection.to_lowercase())
4642}
4643
4644#[must_use]
4646pub fn config_lock_path(path: &Path) -> PathBuf {
4647 let file_name = path
4648 .file_name()
4649 .and_then(|s| s.to_str())
4650 .unwrap_or("config");
4651 path.with_file_name(format!("{file_name}.lock"))
4652}
4653
4654fn validate_section_name(section: &str, subsection: Option<&str>) -> Result<()> {
4655 if section.is_empty()
4656 || !section
4657 .chars()
4658 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
4659 || subsection.is_some_and(str::is_empty)
4660 {
4661 return Err(Error::Config(
4662 format!("invalid section name: {section}").into(),
4663 ));
4664 }
4665 Ok(())
4666}
4667
4668fn raw_variable_name(raw_key: &str) -> &str {
4672 match raw_key.rfind('.') {
4673 Some(i) => &raw_key[i + 1..],
4674 None => raw_key,
4675 }
4676}
4677
4678fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
4683 let first_dot = match raw_key.find('.') {
4684 Some(i) => i,
4685 None => return (raw_key.to_owned(), None),
4686 };
4687 let last_dot = match raw_key.rfind('.') {
4689 Some(i) => i,
4690 None => return (raw_key[..first_dot].to_owned(), None),
4691 };
4692 let section = raw_key[..first_dot].to_owned();
4693 if first_dot == last_dot {
4694 (section, None)
4695 } else {
4696 let subsection = raw_key[first_dot + 1..last_dot].to_owned();
4697 (section, Some(subsection))
4698 }
4699}
4700
4701fn is_section_header_with_inline_entry(line: &str) -> bool {
4703 let trimmed = line.trim();
4704 if !trimmed.starts_with('[') {
4705 return false;
4706 }
4707 let end = match trimmed.find(']') {
4708 Some(i) => i,
4709 None => return false,
4710 };
4711 let after = trimmed[end + 1..].trim();
4712 !after.is_empty() && !after.starts_with('#') && !after.starts_with(';')
4714}
4715
4716fn extract_section_header(line: &str) -> String {
4719 let trimmed = line.trim();
4720 let end = match trimmed.find(']') {
4721 Some(i) => i,
4722 None => return line.to_owned(),
4723 };
4724 trimmed[..=end].to_owned()
4727}
4728
4729#[cfg(test)]
4730mod pack_object_config_tests {
4731 use super::{ConfigFile, ConfigScope, ConfigSet};
4732 use std::path::Path;
4733
4734 fn set_from_snippet(text: &str) -> ConfigSet {
4735 let path = Path::new(".git/config");
4736 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4737 let mut set = ConfigSet::new();
4738 set.merge(&file);
4739 set
4740 }
4741
4742 #[test]
4743 fn pack_object_window_and_depth_defaults() {
4744 let set = ConfigSet::new();
4745 assert_eq!(set.pack_object_window(), 10);
4746 assert_eq!(set.pack_object_depth(), crate::pack::DEFAULT_PACK_DEPTH);
4747 }
4748
4749 #[test]
4750 fn pack_object_window_and_depth_preserve_explicit_zero() {
4751 let set = set_from_snippet("[pack]\n\twindow = 0\n\tdepth = 0\n");
4752 assert_eq!(set.pack_object_window(), 0);
4753 assert_eq!(set.pack_object_depth(), 0);
4754 }
4755
4756 #[test]
4757 fn gc_auto_limits_from_config() {
4758 let set = set_from_snippet("[gc]\n\tauto = 100\n\tautoPackLimit = 3\n");
4759 assert_eq!(set.gc_auto_threshold(), Some(100));
4760 assert_eq!(set.gc_auto_pack_limit(), Some(3));
4761 let defaults = ConfigSet::new();
4762 assert_eq!(defaults.gc_auto_threshold(), Some(6700));
4763 assert_eq!(defaults.gc_auto_pack_limit(), Some(50));
4764 assert!(defaults.gc_auto_detach());
4765 assert!(defaults.maintenance_auto());
4766 let disabled = set_from_snippet("[gc]\n\tautoPackLimit = 0\n");
4767 assert_eq!(disabled.gc_auto_pack_limit(), None);
4768 assert_eq!(disabled.gc_auto_threshold(), Some(6700));
4769 let auto_off = set_from_snippet("[gc]\n\tauto = 0\n");
4770 assert_eq!(auto_off.gc_auto_threshold(), None);
4771 let negative = set_from_snippet("[gc]\n\tauto = -1\n\tautoPackLimit = -1\n");
4772 assert_eq!(negative.gc_auto_threshold(), None);
4773 assert_eq!(negative.gc_auto_pack_limit(), None);
4774 let unparsable = set_from_snippet("[gc]\n\tauto = lots\n");
4775 assert_eq!(unparsable.gc_auto_threshold(), Some(6700));
4776 let foreground =
4777 set_from_snippet("[gc]\n\tautoDetach = false\n[maintenance]\n\tauto = false\n");
4778 assert!(!foreground.gc_auto_detach());
4779 assert!(!foreground.maintenance_auto());
4780 }
4781}
4782
4783#[cfg(test)]
4784mod value_error_tests {
4785 use super::{parse_bool, parse_color, parse_i64, ConfigFile, ConfigScope, ConfigSet};
4786 use crate::error::{ConfigError, ConfigValueError, Error};
4787 use std::path::Path;
4788
4789 fn set_from_snippet(text: &str) -> ConfigSet {
4790 let file = ConfigFile::parse(Path::new(".git/config"), text, ConfigScope::Local)
4791 .expect("parse config snippet");
4792 let mut set = ConfigSet::new();
4793 set.merge(&file);
4794 set
4795 }
4796
4797 #[test]
4798 fn parse_bool_rejects_non_boolean_with_the_value() {
4799 assert_eq!(
4800 parse_bool("maybe"),
4801 Err(ConfigValueError::InvalidBool {
4802 value: "maybe".to_owned()
4803 })
4804 );
4805 assert_eq!(parse_bool("2"), Ok(true));
4806 assert_eq!(parse_bool(""), Ok(false));
4807 }
4808
4809 #[test]
4810 fn parse_i64_distinguishes_empty_invalid_and_overflow() {
4811 assert_eq!(parse_i64(" "), Err(ConfigValueError::EmptyInteger));
4812 assert_eq!(
4813 parse_i64(" 12x "),
4814 Err(ConfigValueError::InvalidInteger {
4815 value: "12x".to_owned()
4816 })
4817 );
4818 assert_eq!(
4819 parse_i64("9223372036854775807k"),
4820 Err(ConfigValueError::IntegerOverflow {
4821 value: "9223372036854775807k".to_owned()
4822 })
4823 );
4824 assert_eq!(parse_i64("2m"), Ok(2 * 1024 * 1024));
4825 }
4826
4827 #[test]
4828 fn parse_color_reports_the_trimmed_value() {
4829 let bad = ConfigValueError::InvalidColor {
4830 value: "red green blue".to_owned(),
4831 };
4832 assert_eq!(parse_color(" red green blue "), Err(bad));
4833 assert_eq!(
4834 parse_color("nonsense").map_err(|e| e.to_string()),
4835 Err("bad color value 'nonsense'".to_owned())
4836 );
4837 assert_eq!(parse_color(" "), Ok(String::new()));
4838 }
4839
4840 #[test]
4841 fn get_bool_and_get_i64_name_the_key() {
4842 let set = set_from_snippet("[core]\n\tbare = sometimes\n[pack]\n\tthreads = many\n");
4843 let bool_err = set
4844 .get_bool("core.bare")
4845 .and_then(Result::err)
4846 .expect("invalid bool");
4847 assert_eq!(
4848 bool_err,
4849 ConfigError::InvalidValue {
4850 key: "core.bare".to_owned(),
4851 source: ConfigValueError::InvalidBool {
4852 value: "sometimes".to_owned()
4853 },
4854 }
4855 );
4856 assert_eq!(
4857 bool_err.to_string(),
4858 "bad boolean config value 'sometimes' for 'core.bare'"
4859 );
4860 let int_err = set
4861 .get_i64("pack.threads")
4862 .and_then(Result::err)
4863 .expect("invalid int");
4864 assert!(matches!(
4865 &int_err,
4866 ConfigError::InvalidValue { key, source: ConfigValueError::InvalidInteger { value } }
4867 if key == "pack.threads" && value == "many"
4868 ));
4869 assert!(matches!(Error::from(int_err), Error::Config(_)));
4870 assert!(set.get_bool("core.missing").is_none());
4871 }
4872
4873 #[test]
4874 fn typed_readers_match_any_casing_of_the_key() {
4875 let set = set_from_snippet(
4876 "[CORE]\n\tQUOTEPATH = false\n[pack]\n\twritebitmaphashcache = false\n\tREADREVERSEINDEX = false\n",
4877 );
4878 assert!(!set.quote_path_fully());
4879 assert!(!set.pack_write_bitmap_hash_cache());
4880 assert!(!set.pack_read_reverse_index_default());
4881 assert_eq!(set.get_bool("Core.QuotePath"), Some(Ok(false)));
4882 }
4883
4884 #[test]
4885 fn for_key_keeps_the_source_error() {
4886 let err = ConfigValueError::EmptyInteger.for_key("gc.auto");
4887 assert_eq!(err.to_string(), "empty integer value for 'gc.auto'");
4888 let source = std::error::Error::source(&err).map(ToString::to_string);
4889 assert_eq!(source.as_deref(), Some("empty integer value"));
4890 }
4891}
4892
4893#[cfg(test)]
4894mod get_regexp_tests {
4895 use super::{ConfigFile, ConfigScope, ConfigSet};
4896 use std::path::Path;
4897
4898 fn set_from_snippet(text: &str) -> ConfigSet {
4899 let path = Path::new(".git/config");
4900 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4901 let mut set = ConfigSet::new();
4902 set.merge(&file);
4903 set
4904 }
4905
4906 #[test]
4907 fn get_regexp_matches_section_prefix_like_git_config() {
4908 let text = r#"
4909[user]
4910 email = alice@example.com
4911 name = Alice
4912[core]
4913 bare = false
4914"#;
4915 let set = set_from_snippet(text);
4916 let keys: Vec<_> = set
4917 .get_regexp("user")
4918 .expect("valid pattern")
4919 .into_iter()
4920 .map(|e| e.key.as_str())
4921 .collect();
4922 assert!(keys.contains(&"user.email"));
4923 assert!(keys.contains(&"user.name"));
4924 assert!(!keys.iter().any(|k| k.starts_with("core.")));
4925 }
4926
4927 #[test]
4928 fn get_regexp_returns_all_multi_value_entries_in_order() {
4929 let text = r#"
4930[remote "origin"]
4931 url = https://example.com/repo.git
4932 fetch = +refs/heads/*:refs/remotes/origin/*
4933 push = +refs/heads/main:refs/heads/main
4934 push = +refs/heads/develop:refs/heads/develop
4935"#;
4936 let set = set_from_snippet(text);
4937 let matches = set.get_regexp("remote.origin").expect("valid pattern");
4938 let push_vals: Vec<_> = matches
4939 .iter()
4940 .filter(|e| e.key == "remote.origin.push")
4941 .map(|e| e.value.as_deref().unwrap_or(""))
4942 .collect();
4943 assert_eq!(push_vals.len(), 2);
4944 assert_eq!(push_vals[0], "+refs/heads/main:refs/heads/main");
4945 assert_eq!(push_vals[1], "+refs/heads/develop:refs/heads/develop");
4946 }
4947
4948 #[test]
4949 fn get_regexp_dot_matches_any_key() {
4950 let text = r#"
4951[a]
4952 x = 1
4953[b]
4954 y = 2
4955"#;
4956 let set = set_from_snippet(text);
4957 let m = set.get_regexp(".").expect("valid pattern");
4958 assert_eq!(m.len(), 2);
4959 }
4960
4961 #[test]
4962 fn get_regexp_no_match_returns_empty_vec() {
4963 let set = set_from_snippet("[user]\n\tname = x\n");
4964 let m = set.get_regexp("zzz").expect("valid pattern");
4965 assert!(m.is_empty());
4966 }
4967
4968 #[test]
4969 fn get_regexp_invalid_pattern_is_error() {
4970 let set = set_from_snippet("[user]\n\tname = x\n");
4971 let err = set.get_regexp("(").expect_err("unclosed group");
4972 assert!(err.contains("invalid key pattern"), "got: {err}");
4973 }
4974
4975 #[test]
4976 fn valueless_negotiation_algorithm_is_a_typed_config_error() {
4977 use crate::error::{ConfigError, Error};
4978
4979 let path = Path::new("/repo/.git/config");
4980 for (text, line) in [
4981 (
4982 "[core]\n\tbare = false\n[fetch]\n\tnegotiationAlgorithm\n",
4983 4,
4984 ),
4985 ("[fetch] negotiationAlgorithm\n", 1),
4986 ] {
4987 let err = ConfigFile::parse(path, text, ConfigScope::Local).expect_err(text);
4988 let Error::Config(config_err) = err else {
4989 panic!("expected a config error for {text:?}, got {err:?}");
4990 };
4991 assert_eq!(
4992 config_err,
4993 ConfigError::BadConfigVariable {
4994 key: "fetch.negotiationalgorithm".to_owned(),
4995 file: ".git/config".to_owned(),
4996 line,
4997 },
4998 "{text:?}"
4999 );
5000 }
5001
5002 let ok = ConfigFile::parse(
5003 path,
5004 "[fetch]\n\tnegotiationAlgorithm = skipping\n",
5005 ConfigScope::Local,
5006 )
5007 .expect("valued key parses");
5008 assert_eq!(ok.entries[0].value.as_deref(), Some("skipping"));
5009 }
5010}
5011
5012#[cfg(test)]
5013mod loose_compression_tests {
5014 use std::path::Path;
5015
5016 use super::{ConfigFile, ConfigScope, ConfigSet, Error};
5017
5018 fn set_from_snippet(text: &str) -> ConfigSet {
5019 let path = Path::new(".git/config");
5020 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
5021 let mut set = ConfigSet::new();
5022 set.merge(&file);
5023 set
5024 }
5025
5026 #[test]
5027 fn big_file_threshold_default_suffixes_and_errors() {
5028 assert_eq!(
5029 ConfigSet::new().big_file_threshold().unwrap(),
5030 512 * 1024 * 1024
5031 );
5032 let cases = [
5033 ("1000", 1000),
5034 ("0", 0),
5035 ("4k", 4096),
5036 ("2m", 2 * 1024 * 1024),
5037 ("1g", 1024 * 1024 * 1024),
5038 ("8G", 8 * 1024 * 1024 * 1024),
5039 ];
5040 for (raw, want) in cases {
5041 let set = set_from_snippet(&format!("[core]\n\tbigFileThreshold = {raw}\n"));
5042 assert_eq!(set.big_file_threshold().unwrap(), want, "value {raw}");
5043 }
5044 let set = set_from_snippet("[core]\n\tbigfilethreshold = 1k\n\tbigFileThreshold = 3k\n");
5045 assert_eq!(set.big_file_threshold().unwrap(), 3 * 1024);
5046
5047 for raw in ["-1", "12x", "lots", ""] {
5048 let snippet = if raw.is_empty() {
5049 "[core]\n\tbigFileThreshold\n".to_owned()
5050 } else {
5051 format!("[core]\n\tbigFileThreshold = {raw}\n")
5052 };
5053 let err = set_from_snippet(&snippet).big_file_threshold().unwrap_err();
5054 assert!(
5055 matches!(
5056 err,
5057 Error::Config(super::ConfigError::BadNumericValue { ref key, .. })
5058 if key == "core.bigfilethreshold"
5059 ),
5060 "value {raw:?}: {err:?}"
5061 );
5062 }
5063 }
5064
5065 #[test]
5066 fn loose_compression_cascade() {
5067 let set = ConfigSet::new();
5068 assert_eq!(
5069 set.loose_objects_zlib_level().unwrap(),
5070 ConfigSet::LOOSE_OBJECTS_ZLIB_DEFAULT
5071 );
5072
5073 let set = set_from_snippet("[core]\n\tcompression = 0\n");
5074 assert_eq!(set.loose_objects_zlib_level().unwrap(), 0);
5075
5076 let set = set_from_snippet("[core]\n\tcompression = 9\n");
5077 assert_eq!(set.loose_objects_zlib_level().unwrap(), 9);
5078
5079 let set = set_from_snippet("[core]\n\tcompression = 0\n\tloosecompression = 9\n");
5080 assert_eq!(set.loose_objects_zlib_level().unwrap(), 9);
5081
5082 let set = set_from_snippet("[core]\n\tloosecompression = 3\n");
5083 assert_eq!(set.loose_objects_zlib_level().unwrap(), 3);
5084
5085 let set = set_from_snippet("[core]\n\tcompression = -1\n");
5086 assert_eq!(set.loose_objects_zlib_level().unwrap(), 6);
5087
5088 for level in 0..=9 {
5089 let set = set_from_snippet(&format!("[core]\n\tloosecompression = {level}\n"));
5090 assert_eq!(set.loose_objects_zlib_level().unwrap(), level);
5091 }
5092
5093 let set = set_from_snippet("[core]\n\tloosecompression = -1\n");
5094 assert_eq!(set.loose_objects_zlib_level().unwrap(), 6);
5095
5096 let err = set_from_snippet("[core]\n\tloosecompression = 10\n")
5097 .loose_objects_zlib_level()
5098 .unwrap_err();
5099 assert!(matches!(err, Error::Config(_)));
5100
5101 let err = set_from_snippet("[core]\n\tloosecompression = not-a-number\n")
5102 .loose_objects_zlib_level()
5103 .unwrap_err();
5104 assert!(matches!(err, Error::Config(_)));
5105
5106 let err = set_from_snippet("[core]\n\tcompression = 42\n")
5107 .loose_objects_zlib_level()
5108 .unwrap_err();
5109 assert!(matches!(err, Error::Config(_)));
5110
5111 let err = set_from_snippet("[core]\n\tcompression = nope\n\tloosecompression = 1\n")
5112 .loose_objects_zlib_level()
5113 .unwrap_err();
5114 assert!(matches!(err, Error::Config(_)));
5115
5116 let err = set_from_snippet("[core]\n\tloosecompression = nope\n\tloosecompression = 1\n")
5117 .loose_objects_zlib_level()
5118 .unwrap_err();
5119 assert!(matches!(err, Error::Config(_)));
5120
5121 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 9\n");
5122 assert_eq!(set.loose_objects_zlib_level().unwrap(), 1);
5123
5124 let err = set_from_snippet("[core]\n\tloosecompression\n")
5125 .loose_objects_zlib_level()
5126 .unwrap_err();
5127 assert!(matches!(err, Error::Config(_)));
5128 }
5129}
5130
5131#[cfg(test)]
5132mod pack_compression_tests {
5133 use super::{ConfigFile, ConfigScope, ConfigSet};
5134 use std::path::Path;
5135
5136 fn set_from_snippet(text: &str) -> ConfigSet {
5137 let path = Path::new(".git/config");
5138 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
5139 let mut set = ConfigSet::new();
5140 set.merge(&file);
5141 set
5142 }
5143
5144 #[test]
5145 fn pack_objects_zlib_level_defaults_to_six() {
5146 let set = ConfigSet::new();
5147 assert_eq!(set.pack_objects_zlib_level().unwrap(), 6);
5148 }
5149
5150 #[test]
5151 fn pack_objects_zlib_level_core_compression() {
5152 let set = set_from_snippet("[core]\n\tcompression = 0\n");
5153 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
5154 let set = set_from_snippet("[core]\n\tcompression = 9\n");
5155 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5156 }
5157
5158 #[test]
5159 fn pack_objects_zlib_level_pack_overrides_core() {
5160 let set = set_from_snippet("[core]\n\tcompression = 9\n[pack]\n\tcompression = 0\n");
5161 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
5162 let set = set_from_snippet("[core]\n\tcompression = 0\n[pack]\n\tcompression = 9\n");
5163 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5164 }
5165
5166 #[test]
5167 fn pack_objects_zlib_level_later_core_does_not_override_earlier_pack() {
5168 let mut set = ConfigSet::new();
5169 set.merge(
5170 &ConfigFile::parse(
5171 Path::new("a"),
5172 "[pack]\n\tcompression = 9\n",
5173 ConfigScope::Local,
5174 )
5175 .unwrap(),
5176 );
5177 set.merge(
5178 &ConfigFile::parse(
5179 Path::new("b"),
5180 "[core]\n\tcompression = 0\n",
5181 ConfigScope::Local,
5182 )
5183 .unwrap(),
5184 );
5185 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5186 }
5187
5188 #[test]
5189 fn pack_objects_zlib_level_loosecompression_does_not_block_core_pack_level() {
5190 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 0\n");
5191 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
5192 }
5193
5194 #[test]
5195 fn pack_objects_zlib_level_pack_wins_after_loose_and_core() {
5196 let set = set_from_snippet(
5197 "[core]\n\tloosecompression = 1\n\tcompression = 0\n[pack]\n\tcompression = 9\n",
5198 );
5199 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5200 }
5201}
5202
5203#[cfg(test)]
5204mod config_cache_tests {
5205 use super::*;
5206 use filetime::FileTime;
5207
5208 fn local_opts(git_dir: &Path) -> LoadConfigOptions {
5209 LoadConfigOptions {
5210 include_system: false,
5211 include_ctx: IncludeContext {
5212 git_dir: Some(git_dir.to_path_buf()),
5213 ..Default::default()
5214 },
5215 ..Default::default()
5216 }
5217 }
5218
5219 fn load_value(caches: &crate::repo_caches::RepoCaches, git_dir: &Path) -> Option<String> {
5220 let opts = local_opts(git_dir);
5221 let set = caches
5222 .load_config_with_options(&Environment::empty(), Some(git_dir), &opts)
5223 .expect("load cascade");
5224 set.get("gritcachetest.value")
5225 }
5226
5227 fn mtime_of(path: &Path) -> FileTime {
5228 FileTime::from_last_modification_time(&fs::metadata(path).expect("stat config"))
5229 }
5230
5231 fn restore_mtime(path: &Path, stamp: FileTime) {
5232 filetime::set_file_mtime(path, stamp).expect("restore mtime");
5233 }
5234
5235 #[test]
5236 fn cache_serves_same_stamp_and_config_write_evicts() {
5237 let caches =
5238 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5239 let td = tempfile::tempdir().expect("tempdir");
5240 let gd = td.path();
5241 let cfg = gd.join("config");
5242 fs::write(&cfg, "[gritcachetest]\n\tvalue = aaa\n").expect("write v1");
5243 let t0 = mtime_of(&cfg);
5244 assert_eq!(load_value(&caches, gd).as_deref(), Some("aaa"));
5245
5246 fs::write(&cfg, "[gritcachetest]\n\tvalue = bbb\n").expect("write v2");
5250 restore_mtime(&cfg, t0);
5251 assert_eq!(load_value(&caches, gd).as_deref(), Some("aaa"));
5252
5253 let mut file = ConfigFile::from_path(&cfg, ConfigScope::Local)
5255 .expect("read config")
5256 .expect("config exists");
5257 file.set("gritcachetest.value", "ccc").expect("set value");
5258 file.write().expect("persist");
5259 caches.evict_config_for_path(&cfg);
5260 restore_mtime(&cfg, t0);
5261 assert_eq!(load_value(&caches, gd).as_deref(), Some("ccc"));
5262 }
5263
5264 #[test]
5265 fn cache_invalidates_on_size_or_existence_change() {
5266 let caches =
5267 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5268 let td = tempfile::tempdir().expect("tempdir");
5269 let gd = td.path();
5270 let cfg = gd.join("config");
5271 assert_eq!(load_value(&caches, gd), None);
5273 fs::write(&cfg, "[gritcachetest]\n\tvalue = first\n").expect("create");
5275 assert_eq!(load_value(&caches, gd).as_deref(), Some("first"));
5276 let t0 = mtime_of(&cfg);
5278 fs::write(&cfg, "[gritcachetest]\n\tvalue = second-longer\n").expect("rewrite");
5279 restore_mtime(&cfg, t0);
5280 assert_eq!(load_value(&caches, gd).as_deref(), Some("second-longer"));
5281 }
5282
5283 #[test]
5284 fn include_targets_are_stamped_and_invalidate() {
5285 let caches =
5286 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5287 let td = tempfile::tempdir().expect("tempdir");
5288 let gd = td.path();
5289 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
5290 let inc = gd.join("extra.conf");
5291 fs::write(&inc, "[gritcachetest]\n\tvalue = one\n").expect("write include v1");
5292 assert_eq!(load_value(&caches, gd).as_deref(), Some("one"));
5293
5294 let t0 = mtime_of(&inc);
5298 fs::write(&inc, "[gritcachetest]\n\tvalue = two\n").expect("write include v2");
5299 restore_mtime(&inc, t0);
5300 assert_eq!(load_value(&caches, gd).as_deref(), Some("one"));
5301
5302 fs::write(&inc, "[gritcachetest]\n\tvalue = two-longer\n").expect("write include v3");
5304 assert_eq!(load_value(&caches, gd).as_deref(), Some("two-longer"));
5305 }
5306
5307 #[test]
5308 fn missing_include_target_is_watched() {
5309 let caches =
5310 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5311 let td = tempfile::tempdir().expect("tempdir");
5312 let gd = td.path();
5313 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
5314 assert_eq!(load_value(&caches, gd), None);
5315
5316 fs::write(gd.join("extra.conf"), "[gritcachetest]\n\tvalue = born\n").expect("create");
5319 assert_eq!(load_value(&caches, gd).as_deref(), Some("born"));
5320 }
5321
5322 #[test]
5323 fn onbranch_condition_follows_head() {
5324 let caches =
5325 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5326 let td = tempfile::tempdir().expect("tempdir");
5327 let gd = td.path();
5328 fs::write(gd.join("HEAD"), "ref: refs/heads/main\n").expect("write HEAD");
5329 fs::write(
5330 gd.join("config"),
5331 "[includeIf \"onbranch:main\"]\n\tpath = branch.conf\n",
5332 )
5333 .expect("write config");
5334 fs::write(
5335 gd.join("branch.conf"),
5336 "[gritcachetest]\n\tvalue = onmain\n",
5337 )
5338 .expect("write branch config");
5339 assert_eq!(load_value(&caches, gd).as_deref(), Some("onmain"));
5340
5341 fs::write(gd.join("HEAD"), "ref: refs/heads/dev\n").expect("rewrite HEAD");
5344 assert_eq!(load_value(&caches, gd), None);
5345 }
5346
5347 #[test]
5348 #[cfg(unix)]
5349 fn load_propagates_local_config_io_errors() {
5350 use std::os::unix::fs::symlink;
5351 let td = tempfile::tempdir().expect("tempdir");
5352 let git_dir = td.path();
5353 let config = git_dir.join("config");
5354 symlink("config", &config).expect("self-referential symlink");
5355 let err = ConfigSet::load(&Environment::empty(), Some(git_dir), false).unwrap_err();
5356 assert!(
5357 matches!(err, Error::Io(_)),
5358 "expected I/O error for symlink loop, got {err:?}"
5359 );
5360 }
5361}
5362
5363#[cfg(test)]
5364mod line_scan_tests {
5365 use super::*;
5366
5367 fn strip_inline_comment_by_chars(s: &str) -> String {
5369 let mut in_quote = false;
5370 let mut last_was_backslash = false;
5371 let mut result = String::with_capacity(s.len());
5372 let mut chars = s.chars().peekable();
5373 while let Some(ch) = chars.next() {
5374 match ch {
5375 '"' if !last_was_backslash => {
5376 in_quote = !in_quote;
5377 result.push(ch);
5378 last_was_backslash = false;
5379 }
5380 '\\' if !last_was_backslash => {
5381 last_was_backslash = true;
5382 result.push(ch);
5383 if in_quote {
5384 if let Some(&next) = chars.peek() {
5385 result.push(next);
5386 chars.next();
5387 last_was_backslash = false;
5388 }
5389 }
5390 }
5391 '#' | ';' if !in_quote && !last_was_backslash => break,
5392 _ => {
5393 last_was_backslash = false;
5394 result.push(ch);
5395 }
5396 }
5397 }
5398 result.trim_end().to_owned()
5399 }
5400
5401 fn unclosed_quote_by_chars(line: &str) -> bool {
5402 let trimmed = line.trim();
5403 let Some(eq_pos) = trimmed.find('=') else {
5404 return false;
5405 };
5406 let mut in_quote = false;
5407 let mut last_was_backslash = false;
5408 for ch in trimmed[eq_pos + 1..].trim_start().chars() {
5409 match ch {
5410 '"' if !last_was_backslash => {
5411 in_quote = !in_quote;
5412 last_was_backslash = false;
5413 }
5414 '\\' if !last_was_backslash => {
5415 last_was_backslash = true;
5416 continue;
5417 }
5418 '#' | ';' if !in_quote && !last_was_backslash => return false,
5419 _ => last_was_backslash = false,
5420 }
5421 }
5422 in_quote
5423 }
5424
5425 fn continues_by_chars(line: &str) -> bool {
5426 let trimmed = line.trim();
5427 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
5428 return false;
5429 }
5430 let Some(pos) = trimmed.find('=') else {
5431 return false;
5432 };
5433 let mut in_quote = false;
5434 let mut last_was_backslash = false;
5435 let mut in_comment = false;
5436 for ch in trimmed[pos + 1..].chars() {
5437 if in_comment {
5438 last_was_backslash = false;
5439 continue;
5440 }
5441 match ch {
5442 '"' if !last_was_backslash => {
5443 in_quote = !in_quote;
5444 last_was_backslash = false;
5445 }
5446 '\\' if !last_was_backslash => {
5447 last_was_backslash = true;
5448 continue;
5449 }
5450 '#' | ';' if !in_quote && !last_was_backslash => {
5451 in_comment = true;
5452 last_was_backslash = false;
5453 }
5454 _ => last_was_backslash = false,
5455 }
5456 }
5457 last_was_backslash && !in_comment
5458 }
5459
5460 fn corpus() -> Vec<String> {
5463 const TOKENS: [&str; 11] = [
5464 "a", "\"", "\\", "#", ";", " ", "=", "\t", "é", "日", "\u{2003}",
5465 ];
5466 let mut out = vec![String::new()];
5467 let mut frontier = vec![String::new()];
5468 for _ in 0..5 {
5469 let mut next = Vec::with_capacity(frontier.len() * TOKENS.len());
5470 for prefix in &frontier {
5471 for token in TOKENS {
5472 next.push(format!("{prefix}{token}"));
5473 }
5474 }
5475 out.extend(next.iter().cloned());
5476 frontier = next;
5477 }
5478 out
5479 }
5480
5481 #[test]
5482 fn byte_scanners_match_char_scanners_on_every_short_line() {
5483 for s in corpus() {
5484 assert_eq!(
5485 strip_inline_comment(&s),
5486 strip_inline_comment_by_chars(&s),
5487 "strip_inline_comment({s:?})"
5488 );
5489 let line = format!("k = {s}");
5490 assert_eq!(
5491 entry_line_value_has_unclosed_quote(&line),
5492 unclosed_quote_by_chars(&line),
5493 "unclosed quote in {line:?}"
5494 );
5495 assert_eq!(
5496 value_line_continues(&line),
5497 continues_by_chars(&line),
5498 "continuation of {line:?}"
5499 );
5500 assert_eq!(
5501 entry_line_value_has_unclosed_quote(&s),
5502 unclosed_quote_by_chars(&s),
5503 "unclosed quote in {s:?}"
5504 );
5505 assert_eq!(
5506 value_line_continues(&s),
5507 continues_by_chars(&s),
5508 "continuation of {s:?}"
5509 );
5510 }
5511 }
5512
5513 #[test]
5514 fn strip_inline_comment_borrows_a_prefix() {
5515 let s = "\"a # b\" tail ; comment";
5516 let stripped = strip_inline_comment(s);
5517 assert_eq!(stripped, "\"a # b\" tail");
5518 assert_eq!(stripped.as_ptr(), s.as_ptr());
5519 assert_eq!(strip_inline_comment("\"\\\"#\" x"), "\"\\\"#\" x");
5520 assert_eq!(strip_inline_comment("v\\#kept # gone"), "v\\#kept");
5521 }
5522
5523 #[test]
5524 fn canonical_key_lowercases_section_and_name_only() {
5525 assert_eq!(canonical_key("core.bare").unwrap(), "core.bare");
5526 assert_eq!(canonical_key("CORE.BARE").unwrap(), "core.bare");
5527 assert_eq!(
5528 canonical_key("Remote.Origin.URL").unwrap(),
5529 "remote.Origin.url"
5530 );
5531 assert_eq!(canonical_key("a.B.C.D").unwrap(), "a.B.C.d");
5532 assert!(canonical_key("nodot").is_err());
5533 assert!(canonical_key("core.").is_err());
5534 assert!(canonical_key("core.1bad").is_err());
5535 assert!(canonical_key("core.ba\nre").is_err());
5536 }
5537
5538 #[test]
5539 fn parsed_keys_reuse_the_section_prefix_per_header() {
5540 let text = "[Core]\n\tBare = false\n\tFileMode\n\
5541 [Remote \"Origin\"]\n\tURL = /x\n\
5542 [branch \"Main\"] Remote = origin\n\
5543 [core]\n\tEditor = vi ; trailing\n";
5544 let file = ConfigFile::parse(Path::new("cfg"), text, ConfigScope::Local).unwrap();
5545 let keys: Vec<(&str, Option<&str>)> = file
5546 .entries
5547 .iter()
5548 .map(|e| (e.key.as_str(), e.value.as_deref()))
5549 .collect();
5550 assert_eq!(
5551 keys,
5552 [
5553 ("core.bare", Some("false")),
5554 ("core.filemode", None),
5555 ("remote.Origin.url", Some("/x")),
5556 ("branch.Main.remote", Some("origin")),
5557 ("core.editor", Some("vi")),
5558 ]
5559 );
5560 }
5561
5562 #[test]
5563 fn push_lowercase_matches_to_lowercase() {
5564 for s in ["", "core", "MiXeD-09", "ÄBC", "İx", "ΣΑΣ"] {
5565 let mut out = String::from("p.");
5566 push_lowercase(&mut out, s);
5567 assert_eq!(out, format!("p.{}", s.to_lowercase()));
5568 }
5569 }
5570}