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, 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, String>> {
2003 let v = self.get(key)?;
2004 if canonical_key(key).ok().as_deref() == Some("pack.allowpackreuse") {
2005 let lower = v.trim().to_ascii_lowercase();
2006 if lower == "single" || lower == "multi" {
2007 return None;
2008 }
2009 }
2010 Some(parse_bool(&v))
2011 }
2012
2013 #[must_use]
2019 pub fn quote_path_fully(&self) -> bool {
2020 let from_key = |key: &str| self.get_bool(key).and_then(|r| r.ok());
2021 from_key("core.quotepath")
2022 .or_else(|| from_key("core.quotePath"))
2023 .unwrap_or(true)
2024 }
2025
2026 #[must_use]
2028 pub fn pack_write_bitmap_hash_cache(&self) -> bool {
2029 self.get_bool("pack.writebitmaphashcache")
2030 .or_else(|| self.get_bool("pack.writeBitmapHashCache"))
2031 .and_then(|r| r.ok())
2032 .unwrap_or(true)
2033 }
2034
2035 #[must_use]
2037 pub fn pack_write_bitmap_lookup_table(&self) -> bool {
2038 self.get_bool("pack.writebitmaplookuptable")
2039 .or_else(|| self.get_bool("pack.writeBitmapLookupTable"))
2040 .and_then(|r| r.ok())
2041 .unwrap_or(false)
2042 }
2043
2044 #[must_use]
2046 pub fn pack_prefer_bitmap_tips(&self) -> Vec<String> {
2047 let mut out = self.get_all("pack.preferbitmaptips");
2048 if out.is_empty() {
2049 out = self.get_all("pack.preferBitmapTips");
2050 }
2051 out
2052 }
2053
2054 #[must_use]
2056 pub fn pack_bitmap_write_options(&self) -> crate::pack_bitmap::PackBitmapWriteOptions {
2057 crate::pack_bitmap::PackBitmapWriteOptions {
2058 full_dag: true,
2059 hash_cache: self.pack_write_bitmap_hash_cache(),
2060 lookup_table: self.pack_write_bitmap_lookup_table(),
2061 prefer_bitmap_tips: self.pack_prefer_bitmap_tips(),
2062 verify_reachability_closure: true,
2063 reachability_roots: None,
2064 }
2065 }
2066
2067 #[must_use]
2071 pub fn pack_write_reverse_index_default(&self, env: &Environment) -> bool {
2072 if env.git_test_no_write_rev_index.as_deref().is_some_and(|v| {
2073 let s = v.trim().to_ascii_lowercase();
2074 matches!(s.as_str(), "1" | "true" | "yes" | "on")
2075 }) {
2076 return false;
2077 }
2078 if self
2079 .get("pack.writereverseindex")
2080 .or_else(|| self.get("pack.writeReverseIndex"))
2081 .is_some_and(|v| v.trim().is_empty())
2082 {
2083 return false;
2084 }
2085 self.get_bool("pack.writereverseindex")
2086 .or_else(|| self.get_bool("pack.writeReverseIndex"))
2087 .and_then(|r| r.ok())
2088 .unwrap_or(true)
2089 }
2090
2091 #[must_use]
2093 pub fn pack_index_threads(&self) -> Option<usize> {
2094 self.get_i64("pack.threads")
2095 .and_then(|r| r.ok())
2096 .and_then(|n| usize::try_from(n).ok())
2097 }
2098
2099 #[must_use]
2103 pub fn pack_object_window(&self) -> usize {
2104 match self.get_i64("pack.window") {
2105 None => 10,
2106 Some(Ok(n)) if n >= 0 => usize::try_from(n).unwrap_or(10),
2107 Some(Ok(_)) | Some(Err(_)) => 10,
2108 }
2109 }
2110
2111 #[must_use]
2115 pub fn pack_window_memory_bytes(&self) -> u64 {
2117 match self.get_i64("pack.windowmemory") {
2118 None => 0,
2119 Some(Ok(n)) if n >= 0 => u64::try_from(n).unwrap_or(0),
2120 Some(Ok(_)) | Some(Err(_)) => 0,
2121 }
2122 }
2123
2124 pub fn pack_object_depth(&self) -> usize {
2126 match self.get_i64("pack.depth") {
2127 None => crate::pack::DEFAULT_PACK_DEPTH,
2128 Some(Ok(n)) if n >= 0 => usize::try_from(n).unwrap_or(crate::pack::DEFAULT_PACK_DEPTH),
2129 Some(Ok(_)) | Some(Err(_)) => crate::pack::DEFAULT_PACK_DEPTH,
2130 }
2131 }
2132
2133 #[must_use]
2140 pub fn gc_auto_threshold(&self) -> Option<usize> {
2141 match self.get_i64("gc.auto") {
2142 Some(Ok(n)) if n <= 0 => None,
2143 Some(Ok(n)) => Some(usize::try_from(n).unwrap_or(usize::MAX)),
2144 None | Some(Err(_)) => Some(DEFAULT_GC_AUTO_THRESHOLD),
2145 }
2146 }
2147
2148 #[must_use]
2154 pub fn gc_auto_pack_limit(&self) -> Option<usize> {
2155 match self.get_i64("gc.autoPackLimit") {
2156 Some(Ok(n)) if n <= 0 => None,
2157 Some(Ok(n)) => Some(usize::try_from(n).unwrap_or(usize::MAX)),
2158 None | Some(Err(_)) => Some(DEFAULT_GC_AUTO_PACK_LIMIT),
2159 }
2160 }
2161
2162 #[must_use]
2165 pub fn gc_auto_detach(&self) -> bool {
2166 self.get_bool("gc.autoDetach")
2167 .and_then(|v| v.ok())
2168 .unwrap_or(true)
2169 }
2170
2171 #[must_use]
2174 pub fn maintenance_auto(&self) -> bool {
2175 self.get_bool("maintenance.auto")
2176 .and_then(|v| v.ok())
2177 .unwrap_or(true)
2178 }
2179
2180 pub fn try_pack_index_parallelism(
2186 &self,
2187 ) -> std::result::Result<crate::parallel::Parallelism, ConfigError> {
2188 Ok(crate::parallel::ThreadPolicy::default()
2189 .try_resolve(crate::parallel::ParallelOp::IndexPack, self)?
2190 .parallelism())
2191 }
2192
2193 #[must_use]
2196 pub fn pack_index_parallelism(&self) -> crate::parallel::Parallelism {
2197 self.try_pack_index_parallelism()
2198 .unwrap_or_else(|_| crate::parallel::Parallelism::resolve(None))
2199 }
2200
2201 pub fn try_pack_index_parallelism_for_git_dir(
2207 env: &Environment,
2208 git_dir: Option<&std::path::Path>,
2209 ) -> Result<crate::parallel::Parallelism> {
2210 match git_dir {
2211 Some(d) => {
2212 let cfg = Self::load(env, Some(d), true)?;
2213 Ok(cfg.try_pack_index_parallelism()?)
2214 }
2215 None => Ok(crate::parallel::Parallelism::resolve(None)),
2216 }
2217 }
2218
2219 #[must_use]
2221 pub fn pack_index_parallelism_for_git_dir(
2222 env: &Environment,
2223 git_dir: Option<&std::path::Path>,
2224 ) -> crate::parallel::Parallelism {
2225 Self::try_pack_index_parallelism_for_git_dir(env, git_dir)
2226 .unwrap_or_else(|_| crate::parallel::Parallelism::resolve(None))
2227 }
2228
2229 #[must_use]
2231 pub fn pack_read_reverse_index_default(&self) -> bool {
2232 self.get_bool("pack.readreverseindex")
2233 .or_else(|| self.get_bool("pack.readReverseIndex"))
2234 .and_then(|r| r.ok())
2235 .unwrap_or(true)
2236 }
2237
2238 #[must_use]
2241 pub(crate) fn log_refs_config(&self) -> refs::LogRefsConfig {
2242 self.get("core.logAllRefUpdates")
2243 .and_then(|v| refs::LogRefsConfig::parse(&v))
2244 .unwrap_or(refs::LogRefsConfig::Unset)
2245 }
2246
2247 #[must_use]
2254 pub fn effective_log_refs_config(&self) -> refs::LogRefsConfig {
2255 match self.log_refs_config() {
2256 refs::LogRefsConfig::Unset
2257 if self
2258 .get_bool("core.bare")
2259 .and_then(|r| r.ok())
2260 .unwrap_or(false) =>
2261 {
2262 refs::LogRefsConfig::None
2263 }
2264 refs::LogRefsConfig::Unset => refs::LogRefsConfig::Normal,
2265 mode => mode,
2266 }
2267 }
2268
2269 pub(crate) fn from_entries(entries: Vec<ConfigEntry>) -> Self {
2271 Self { entries }
2272 }
2273
2274 pub fn get_i64(&self, key: &str) -> Option<std::result::Result<i64, String>> {
2276 self.get(key).map(|v| parse_i64(&v))
2277 }
2278
2279 fn parse_zlib_compression_level(raw: &str) -> Result<i32> {
2284 const Z_DEFAULT_COMPRESSION: i32 = 6;
2285 const Z_BEST_COMPRESSION: i32 = 9;
2286
2287 let v = parse_git_config_int_strict(raw.trim()).map_err(|_| {
2288 Error::Config(format!("bad numeric config value '{raw}' for compression").into())
2289 })?;
2290 if v == -1 {
2291 return Ok(Z_DEFAULT_COMPRESSION);
2292 }
2293 if v < 0 || v > i64::from(Z_BEST_COMPRESSION) {
2294 return Err(Error::Config(
2295 format!("bad zlib compression level {v}").into(),
2296 ));
2297 }
2298 Ok(v as i32)
2299 }
2300
2301 pub fn loose_objects_zlib_level(&self) -> Result<i32> {
2312 let mut loose_level = Self::LOOSE_OBJECTS_ZLIB_DEFAULT;
2313 let mut loose_compression_seen = false;
2314
2315 for e in self.entries() {
2316 match e.key.as_str() {
2317 "core.compression" => {
2318 let val = e.value.as_deref().ok_or_else(|| {
2319 Error::Config(
2320 "bad numeric config value '' for compression"
2321 .to_owned()
2322 .into(),
2323 )
2324 })?;
2325 let level = Self::parse_zlib_compression_level(val)?;
2326 if !loose_compression_seen {
2327 loose_level = level;
2328 }
2329 }
2330 "core.loosecompression" => {
2331 let val = e.value.as_deref().ok_or_else(|| {
2332 Error::Config(
2333 "bad numeric config value '' for compression"
2334 .to_owned()
2335 .into(),
2336 )
2337 })?;
2338 let level = Self::parse_zlib_compression_level(val)?;
2339 loose_level = level;
2340 loose_compression_seen = true;
2341 }
2342 _ => {}
2343 }
2344 }
2345
2346 Ok(loose_level)
2347 }
2348
2349 pub const LOOSE_OBJECTS_ZLIB_DEFAULT: i32 = 1;
2352
2353 pub const BIG_FILE_THRESHOLD_DEFAULT: u64 = 512 * 1024 * 1024;
2355
2356 pub fn big_file_threshold(&self) -> Result<u64> {
2367 const KEY: &str = "core.bigfilethreshold";
2368 let Some(entry) = self.get_last_entry(KEY) else {
2369 return Ok(Self::BIG_FILE_THRESHOLD_DEFAULT);
2370 };
2371 let raw = entry.value.as_deref().unwrap_or_default();
2372 let bad = |reason| {
2373 Error::Config(ConfigError::BadNumericValue {
2374 key: KEY.to_owned(),
2375 value: raw.to_owned(),
2376 reason,
2377 })
2378 };
2379 let value = parse_git_config_int_strict(raw.trim()).map_err(|e| {
2380 bad(match e {
2381 GitConfigIntStrictError::InvalidUnit => BadNumericSource::InvalidUnit,
2382 GitConfigIntStrictError::OutOfRange => BadNumericSource::OutOfRange,
2383 })
2384 })?;
2385 u64::try_from(value).map_err(|_| bad(BadNumericSource::OutOfRange))
2386 }
2387
2388 pub fn pack_objects_zlib_level(&self) -> Result<i32> {
2396 const Z_DEFAULT_COMPRESSION: i32 = 6;
2397
2398 let parse_compression =
2399 |raw: &str| -> Result<i32> { Self::parse_zlib_compression_level(raw) };
2400
2401 let mut pack_level = Z_DEFAULT_COMPRESSION;
2403 let mut pack_compression_seen = false;
2404
2405 for e in self.entries() {
2406 match e.key.as_str() {
2407 "core.compression" => {
2408 let Some(val) = e.value.as_deref() else {
2409 continue;
2410 };
2411 let level = parse_compression(val)?;
2412 if !pack_compression_seen {
2413 pack_level = level;
2414 }
2415 }
2416 "pack.compression" => {
2417 let Some(val) = e.value.as_deref() else {
2418 continue;
2419 };
2420 pack_level = parse_compression(val)?;
2421 pack_compression_seen = true;
2422 }
2423 _ => {}
2424 }
2425 }
2426
2427 Ok(pack_level)
2428 }
2429
2430 pub fn get_regexp(&self, pattern: &str) -> std::result::Result<Vec<&ConfigEntry>, String> {
2435 let re = regex::Regex::new(pattern).map_err(|e| format!("invalid key pattern: {e}"))?;
2436 Ok(self
2437 .entries
2438 .iter()
2439 .filter(|e| re.is_match(&e.key))
2440 .collect())
2441 }
2442
2443 pub fn load(env: &Environment, git_dir: Option<&Path>, include_system: bool) -> Result<Self> {
2454 let opts = LoadConfigOptions {
2455 include_system,
2456 include_ctx: IncludeContext {
2457 git_dir: git_dir.map(PathBuf::from),
2458 cwd: env.cwd.clone(),
2459 pwd: env.pwd.clone(),
2460 env: std::sync::Arc::new(env.clone()),
2461 ..Default::default()
2462 },
2463 ..Default::default()
2464 };
2465 Self::load_with_options(env, git_dir, &opts)
2466 }
2467
2468 pub fn load_with_options(
2475 env: &Environment,
2476 git_dir: Option<&Path>,
2477 opts: &LoadConfigOptions,
2478 ) -> Result<Self> {
2479 Self::load_with_options_uncached(env, git_dir, opts, &mut Vec::new())
2480 }
2481
2482 fn read_local_config_text_optional(
2483 path: &Path,
2484 caches: Option<&std::sync::Arc<crate::repo_caches::RepoCaches>>,
2485 ) -> Result<Option<String>> {
2486 let read = |p: &Path| -> Result<Option<String>> {
2487 match fs::read_to_string(p) {
2488 Ok(text) => Ok(Some(text)),
2489 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
2490 Err(e) => Err(Error::Io(e)),
2491 }
2492 };
2493 let Some(c) = caches else {
2494 return read(path);
2495 };
2496 match c.local_config_text(path) {
2497 Ok(text) => Ok(Some(text)),
2498 Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
2499 Err(e) => Err(e),
2500 }
2501 }
2502
2503 pub(crate) fn load_with_options_uncached(
2504 env: &Environment,
2505 git_dir: Option<&Path>,
2506 opts: &LoadConfigOptions,
2507 included_files: &mut Vec<PathBuf>,
2508 ) -> Result<Self> {
2509 let proc = opts.process_includes;
2510 let mut ctx = opts.include_ctx.clone();
2511 if ctx.cwd.as_os_str().is_empty() {
2512 ctx.cwd = env.cwd.clone();
2513 }
2514 ctx.pwd = env.pwd.clone();
2515 ctx.env = std::sync::Arc::new(env.clone());
2516 let mut layers = Vec::new();
2517
2518 if opts.include_system && !git_config_nosystem_enabled(env) {
2519 let system_path = system_config_path(env);
2520 if let Some(f) = ConfigFile::from_path(&system_path, ConfigScope::System)? {
2521 layers.push(ConfigLayer::file(f));
2522 }
2523 }
2524
2525 for path in global_config_paths(env) {
2527 if let Some(f) = ConfigFile::from_path(&path, ConfigScope::Global)? {
2528 layers.push(ConfigLayer::file(f));
2529 }
2530 }
2531
2532 if let Some(gd) = git_dir {
2534 let common_dir = crate::repo::common_git_dir_for_config(gd);
2535 let local_path = common_dir.join("config");
2536 let local_text =
2537 Self::read_local_config_text_optional(&local_path, opts.repo_caches.as_ref())?;
2538 if let Some(msg) =
2539 crate::repo::early_config_ignore_repo_reason_for_text(local_text.as_deref())
2540 {
2541 if let Some(sink) = opts.diagnostics.as_ref() {
2542 sink.warn(crate::diagnostics::Warning::IgnoredGitDir {
2543 path: gd.to_path_buf(),
2544 reason: msg,
2545 });
2546 }
2547 } else if let Some(text) = local_text.as_deref() {
2548 let f = ConfigFile::parse(&local_path, text, ConfigScope::Local)?;
2549 layers.push(ConfigLayer::file(f));
2550 }
2551
2552 let wt_path = gd.join("config.worktree");
2555 if crate::repo::worktree_config_enabled_for_text(local_text.as_deref()) {
2556 if let Some(f) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree)? {
2557 layers.push(ConfigLayer::file(f));
2558 }
2559 }
2560 }
2561
2562 if let Some(path) = env.git_config.as_deref() {
2563 let path = resolve_config_override_path(env, path);
2564 if let Some(f) = ConfigFile::from_path(&path, ConfigScope::Command)? {
2565 layers.push(ConfigLayer::file(f));
2566 }
2567 }
2568
2569 layers.push(ConfigLayer::Entries(environment_config_pair_entries(env)?));
2570
2571 if let Some(params) = env
2573 .git_config_parameters
2574 .as_deref()
2575 .filter(|p| !p.trim().is_empty())
2576 {
2577 if proc && opts.command_includes {
2578 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
2579 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, params)?;
2580 layers.push(ConfigLayer::file(cmd_file));
2581 } else {
2582 layers.push(ConfigLayer::Entries(command_override_entries(params)));
2583 }
2584 }
2585
2586 let mut set = Self::new();
2587 set.merge_layers(&layers, proc, &ctx, included_files)?;
2588 Ok(set)
2589 }
2590
2591 pub fn read_early_config(
2603 env: &Environment,
2604 git_dir: Option<&Path>,
2605 key: &str,
2606 ) -> Result<Vec<String>> {
2607 let ctx = IncludeContext {
2608 git_dir: git_dir.map(PathBuf::from),
2609 command_line_relative_include_is_error: false,
2610 cwd: env.cwd.clone(),
2611 pwd: env.pwd.clone(),
2612 env: std::sync::Arc::new(env.clone()),
2613 };
2614 let mut layers = Vec::new();
2615
2616 if !git_config_nosystem_enabled(env) {
2617 let system_path = system_config_path(env);
2618 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2619 layers.push(ConfigLayer::file(f));
2620 }
2621 }
2622
2623 for path in global_config_paths(env) {
2625 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2626 layers.push(ConfigLayer::file(f));
2627 }
2628 }
2629
2630 if let Some(gd) = git_dir {
2631 let common_dir = crate::repo::common_git_dir_for_config(gd);
2632 let local_path = common_dir.join("config");
2634 if crate::repo::early_config_ignore_repo_reason(&common_dir).is_some() {
2635 } else if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2637 layers.push(ConfigLayer::file(f));
2638 }
2639
2640 let wt_path = gd.join("config.worktree");
2642 if crate::repo::worktree_config_enabled(&common_dir) {
2643 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
2644 layers.push(ConfigLayer::file(f));
2645 }
2646 }
2647 }
2648
2649 if let Some(params) = env
2651 .git_config_parameters
2652 .as_deref()
2653 .filter(|p| !p.trim().is_empty())
2654 {
2655 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
2656 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, params)?;
2657 layers.push(ConfigLayer::file(cmd_file));
2658 }
2659
2660 let mut set = Self::new();
2661 set.merge_layers(&layers, true, &ctx, &mut Vec::new())?;
2662 Ok(set.get_all(key))
2663 }
2664
2665 pub fn merge_file_with_includes(
2678 &mut self,
2679 file: &ConfigFile,
2680 process_includes: bool,
2681 ctx: &IncludeContext,
2682 ) -> Result<()> {
2683 self.merge_layers(
2684 &[ConfigLayer::File(Cow::Borrowed(file))],
2685 process_includes,
2686 ctx,
2687 &mut Vec::new(),
2688 )
2689 }
2690
2691 pub fn load_repo_local_only(git_dir: &Path) -> Result<Self> {
2697 let local_path = git_dir.join("config");
2698 let ctx = IncludeContext {
2699 git_dir: Some(git_dir.to_path_buf()),
2700 command_line_relative_include_is_error: false,
2701 cwd: PathBuf::from("."),
2702 pwd: None,
2703 env: std::sync::Arc::new(Environment::empty()),
2704 };
2705 let mut set = Self::new();
2706 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2707 set.merge_layers(&[ConfigLayer::file(f)], true, &ctx, &mut Vec::new())?;
2708 }
2709 Ok(set)
2710 }
2711
2712 pub fn load_protected(env: &Environment, include_system: bool) -> Result<Self> {
2723 let ctx = IncludeContext {
2724 git_dir: None,
2725 command_line_relative_include_is_error: false,
2726 cwd: env.cwd.clone(),
2727 pwd: env.pwd.clone(),
2728 env: std::sync::Arc::new(env.clone()),
2729 };
2730 let mut layers = Vec::new();
2731
2732 if include_system && !git_config_nosystem_enabled(env) {
2733 let system_path = system_config_path(env);
2734 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2735 layers.push(ConfigLayer::file(f));
2736 }
2737 }
2738
2739 for path in global_config_paths(env) {
2740 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2741 layers.push(ConfigLayer::file(f));
2742 }
2743 }
2744
2745 layers.push(ConfigLayer::Entries(environment_config_pair_entries(env)?));
2746 if let Some(params) = env.git_config_parameters.as_deref() {
2747 layers.push(ConfigLayer::Entries(command_override_entries(params)));
2748 }
2749
2750 let mut set = Self::new();
2751 set.merge_layers(&layers, true, &ctx, &mut Vec::new())?;
2752 Ok(set)
2753 }
2754
2755 fn merge_layers(
2762 &mut self,
2763 layers: &[ConfigLayer<'_>],
2764 process_includes: bool,
2765 ctx: &IncludeContext,
2766 included_files: &mut Vec<PathBuf>,
2767 ) -> Result<()> {
2768 const INCLUDE_HEADROOM: usize = 64;
2771 let mut remote_urls = RemoteUrls::new(layers);
2772 let top_level: usize = layers
2773 .iter()
2774 .map(|layer| match layer {
2775 ConfigLayer::File(file) => file.entries.len(),
2776 ConfigLayer::Entries(entries) => entries.len(),
2777 })
2778 .sum();
2779 self.entries.reserve(top_level + INCLUDE_HEADROOM);
2780 for layer in layers {
2781 match layer {
2782 ConfigLayer::File(file) => Self::merge_with_includes(
2783 self,
2784 file,
2785 process_includes,
2786 0,
2787 ctx,
2788 &mut remote_urls,
2789 included_files,
2790 )?,
2791 ConfigLayer::Entries(entries) => self.entries.extend(entries.iter().cloned()),
2792 }
2793 }
2794 Ok(())
2795 }
2796
2797 fn merge_with_includes(
2799 set: &mut Self,
2800 file: &ConfigFile,
2801 process_includes: bool,
2802 depth: usize,
2803 ctx: &IncludeContext,
2804 remote_urls: &mut RemoteUrls<'_>,
2805 included_files: &mut Vec<PathBuf>,
2806 ) -> Result<()> {
2807 check_include_depth(depth)?;
2808 if !process_includes {
2809 set.merge(file);
2810 return Ok(());
2811 }
2812
2813 for entry in &file.entries {
2814 set.entries.push(entry.clone());
2815
2816 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
2817 continue;
2818 };
2819 if let Some(cond) = condition.as_deref() {
2820 let holds = match hasconfig_remote_url_pattern(cond) {
2821 Some(pattern) => remote_urls.any_matches(pattern, ctx, included_files)?,
2822 None => evaluate_include_condition(cond, file, ctx),
2823 };
2824 if !holds {
2825 continue;
2826 }
2827 }
2828 let Some(inc_file) = open_include_target(&inc_path, file, ctx, included_files)? else {
2829 continue;
2830 };
2831 Self::merge_with_includes(
2832 set,
2833 &inc_file,
2834 process_includes,
2835 depth + 1,
2836 ctx,
2837 remote_urls,
2838 included_files,
2839 )?;
2840 }
2841
2842 Ok(())
2843 }
2844}
2845
2846enum ConfigLayer<'a> {
2848 File(Cow<'a, ConfigFile>),
2850 Entries(Vec<ConfigEntry>),
2852}
2853
2854impl ConfigLayer<'_> {
2855 fn file(file: ConfigFile) -> Self {
2856 Self::File(Cow::Owned(file))
2857 }
2858}
2859
2860struct RemoteUrls<'a> {
2863 layers: &'a [ConfigLayer<'a>],
2864 urls: Option<Vec<String>>,
2865}
2866
2867impl<'a> RemoteUrls<'a> {
2868 fn new(layers: &'a [ConfigLayer<'a>]) -> Self {
2869 Self { layers, urls: None }
2870 }
2871
2872 fn any_matches(
2874 &mut self,
2875 pattern: &str,
2876 ctx: &IncludeContext,
2877 included_files: &mut Vec<PathBuf>,
2878 ) -> Result<bool> {
2879 let urls = match self.urls.take() {
2880 Some(urls) => urls,
2881 None => self.collect(ctx, included_files)?,
2882 };
2883 let hit = urls
2884 .iter()
2885 .any(|url| wildmatch(pattern.as_bytes(), url.as_bytes(), WM_PATHNAME));
2886 self.urls = Some(urls);
2887 Ok(hit)
2888 }
2889
2890 fn collect(
2891 &self,
2892 ctx: &IncludeContext,
2893 included_files: &mut Vec<PathBuf>,
2894 ) -> Result<Vec<String>> {
2895 let mut urls = Vec::new();
2896 for layer in self.layers {
2897 match layer {
2898 ConfigLayer::File(file) => {
2899 collect_remote_urls(file, false, 0, ctx, &mut urls, included_files)?;
2900 }
2901 ConfigLayer::Entries(entries) => urls.extend(
2902 entries
2903 .iter()
2904 .filter(|entry| is_remote_url_entry(entry))
2905 .filter_map(|entry| entry.value.clone()),
2906 ),
2907 }
2908 }
2909 Ok(urls)
2910 }
2911}
2912
2913fn collect_remote_urls(
2920 file: &ConfigFile,
2921 in_conditional_include: bool,
2922 depth: usize,
2923 ctx: &IncludeContext,
2924 urls: &mut Vec<String>,
2925 included_files: &mut Vec<PathBuf>,
2926) -> Result<()> {
2927 check_include_depth(depth)?;
2928 for entry in &file.entries {
2929 if is_remote_url_entry(entry) {
2930 if in_conditional_include {
2931 return Err(ConfigError::RemoteUrlInHasconfigInclude.into());
2932 }
2933 urls.extend(entry.value.clone());
2934 continue;
2935 }
2936 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
2937 continue;
2938 };
2939 let conditional = match condition.as_deref() {
2940 None => in_conditional_include,
2941 Some(cond) => {
2942 if hasconfig_remote_url_pattern(cond).is_none()
2943 && !evaluate_include_condition(cond, file, ctx)
2944 {
2945 continue;
2946 }
2947 true
2948 }
2949 };
2950 let Some(inc_file) = open_include_target(&inc_path, file, ctx, included_files)? else {
2951 continue;
2952 };
2953 collect_remote_urls(&inc_file, conditional, depth + 1, ctx, urls, included_files)?;
2954 }
2955 Ok(())
2956}
2957
2958fn check_include_depth(depth: usize) -> Result<()> {
2960 const MAX_INCLUDE_DEPTH: usize = 10;
2961 if depth > MAX_INCLUDE_DEPTH {
2962 return Err(ConfigError::IncludeDepthExceeded {
2963 depth,
2964 limit: MAX_INCLUDE_DEPTH,
2965 }
2966 .into());
2967 }
2968 Ok(())
2969}
2970
2971fn open_include_target(
2977 inc_path: &str,
2978 file: &ConfigFile,
2979 ctx: &IncludeContext,
2980 included_files: &mut Vec<PathBuf>,
2981) -> Result<Option<ConfigFile>> {
2982 let resolved = match resolve_include_file_path(inc_path, file, ctx) {
2983 Ok(p) => p,
2984 Err(Error::Config(ConfigError::Other(msg))) if msg.is_empty() => return Ok(None),
2985 Err(e) => return Err(e),
2986 };
2987 included_files.push(resolved.clone());
2988 ConfigFile::from_path(&resolved, file.scope)
2989}
2990
2991fn include_directive_for_entry(entry: &ConfigEntry) -> Option<(String, Option<String>)> {
2992 let val = entry.value.as_ref()?;
2993 if entry.key == "include.path" {
2994 return Some((val.clone(), None));
2995 }
2996 if entry.key.starts_with("includeif.") && entry.key.ends_with(".path") {
2997 let mid = &entry.key["includeif.".len()..entry.key.len() - ".path".len()];
2998 return Some((val.clone(), Some(mid.to_owned())));
2999 }
3000 None
3001}
3002
3003pub(crate) type ConfigFileStamp = (PathBuf, Option<(SystemTime, u64)>);
3032
3033pub(crate) type ConfigCacheEntryStamps = (
3035 Vec<(String, Option<String>)>,
3036 Vec<ConfigFileStamp>,
3037 Vec<ConfigFileStamp>,
3038);
3039
3040#[derive(Clone, PartialEq, Eq, Hash)]
3041pub(crate) struct ConfigCacheKey {
3042 pub(crate) git_dir: Option<PathBuf>,
3043 include_system: bool,
3044 process_includes: bool,
3045 command_includes: bool,
3046 ctx_git_dir: Option<PathBuf>,
3047 ctx_relative_include_is_error: bool,
3048}
3049
3050impl ConfigCacheKey {
3051 pub(crate) fn new(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Self {
3052 Self {
3053 git_dir: git_dir.map(Path::to_path_buf),
3054 include_system: opts.include_system,
3055 process_includes: opts.process_includes,
3056 command_includes: opts.command_includes,
3057 ctx_git_dir: opts.include_ctx.git_dir.clone(),
3058 ctx_relative_include_is_error: opts.include_ctx.command_line_relative_include_is_error,
3059 }
3060 }
3061}
3062
3063pub(crate) struct ConfigCacheEntry {
3064 pub(crate) env_fingerprint: Vec<(String, Option<String>)>,
3065 pub(crate) base_stamps: Vec<ConfigFileStamp>,
3067 pub(crate) extra_stamps: Vec<ConfigFileStamp>,
3070 pub(crate) set: Arc<ConfigSet>,
3071}
3072
3073fn config_cascade_file_paths(
3076 env: &Environment,
3077 git_dir: Option<&Path>,
3078 opts: &LoadConfigOptions,
3079) -> Vec<PathBuf> {
3080 let mut paths = Vec::new();
3081 if opts.include_system && !git_config_nosystem_enabled(env) {
3082 paths.push(system_config_path(env));
3083 }
3084 paths.extend(global_config_paths(env));
3085 if let Some(gd) = git_dir {
3086 let common_dir = crate::repo::common_git_dir_for_config(gd);
3087 paths.push(common_dir.join("config"));
3088 paths.push(gd.join("config.worktree"));
3091 paths.push(gd.join("HEAD"));
3094 }
3095 if let Some(cgd) = &opts.include_ctx.git_dir {
3096 if Some(cgd.as_path()) != git_dir {
3097 paths.push(cgd.join("HEAD"));
3098 }
3099 }
3100 if let Some(p) = env.git_config.as_deref() {
3101 paths.push(resolve_config_override_path(env, p));
3102 }
3103 paths
3104}
3105
3106pub(crate) fn stamp_for_path(path: &Path) -> Option<(SystemTime, u64)> {
3107 fs::metadata(path)
3108 .ok()
3109 .and_then(|m| Some((m.modified().ok()?, m.len())))
3110}
3111
3112pub(crate) fn stamp_paths(paths: Vec<PathBuf>) -> Vec<ConfigFileStamp> {
3113 paths
3114 .into_iter()
3115 .map(|path| {
3116 let stamp = stamp_for_path(&path);
3117 (path, stamp)
3118 })
3119 .collect()
3120}
3121
3122pub(crate) fn config_file_stamps(
3123 env: &Environment,
3124 git_dir: Option<&Path>,
3125 opts: &LoadConfigOptions,
3126) -> Vec<ConfigFileStamp> {
3127 stamp_paths(config_cascade_file_paths(env, git_dir, opts))
3128}
3129
3130pub(crate) fn config_cache_lookup(
3131 cache: &HashMap<ConfigCacheKey, ConfigCacheEntry>,
3132 key: &ConfigCacheKey,
3133 env_fp: &[(String, Option<String>)],
3134 base_stamps: &[ConfigFileStamp],
3135) -> Option<Arc<ConfigSet>> {
3136 let entry = cache.get(key)?;
3137 if entry.env_fingerprint.as_slice() != env_fp || entry.base_stamps.as_slice() != base_stamps {
3138 return None;
3139 }
3140 for (path, stamp) in &entry.extra_stamps {
3141 if stamp_for_path(path) != *stamp {
3142 return None;
3143 }
3144 }
3145 Some(Arc::clone(&entry.set))
3146}
3147
3148pub(crate) fn config_cache_store(
3149 cache: &mut HashMap<ConfigCacheKey, ConfigCacheEntry>,
3150 key: ConfigCacheKey,
3151 env_fingerprint: Vec<(String, Option<String>)>,
3152 base_stamps: Vec<ConfigFileStamp>,
3153 extra_stamps: Vec<ConfigFileStamp>,
3154 set: Arc<ConfigSet>,
3155) {
3156 cache.insert(
3157 key,
3158 ConfigCacheEntry {
3159 env_fingerprint,
3160 base_stamps,
3161 extra_stamps,
3162 set,
3163 },
3164 );
3165}
3166
3167fn git_config_nosystem_enabled(env: &Environment) -> bool {
3168 env.git_config_nosystem
3169 .as_deref()
3170 .map(|value| parse_bool(value).unwrap_or(true))
3171 .unwrap_or(false)
3172}
3173
3174fn environment_config_pair_entries(env: &Environment) -> Result<Vec<ConfigEntry>> {
3176 let Some(count_str) = env.git_config_count.as_deref() else {
3177 return Ok(Vec::new());
3178 };
3179 if count_str.is_empty() {
3180 return Ok(Vec::new());
3181 }
3182
3183 let count = count_str
3184 .parse::<usize>()
3185 .map_err(|_| Error::Config("bogus count in GIT_CONFIG_COUNT".to_owned().into()))?;
3186 if count > i32::MAX as usize {
3187 return Err(Error::Config(
3188 "too many entries in GIT_CONFIG_COUNT".to_owned().into(),
3189 ));
3190 }
3191
3192 let mut set = ConfigSet::new();
3193 for i in 0..count {
3194 let (key, value) = env.git_config_pairs.get(i).ok_or_else(|| {
3195 Error::Config(format!("missing config key GIT_CONFIG_KEY_{i}").into())
3196 })?;
3197 set.add_command_override(key, value)?;
3198 }
3199 Ok(set.entries)
3200}
3201
3202fn command_override_entries(params: &str) -> Vec<ConfigEntry> {
3205 let mut set = ConfigSet::new();
3206 for entry in parse_config_parameters(params) {
3207 let (key, val) = entry
3208 .split_once('\u{1}')
3209 .or_else(|| entry.split_once('='))
3210 .unwrap_or((entry.as_str(), "true"));
3211 let _ = set.add_command_override(key.trim(), val);
3212 }
3213 set.entries
3214}
3215
3216pub fn parse_bool(s: &str) -> std::result::Result<bool, String> {
3229 match s.to_lowercase().as_str() {
3230 "true" | "yes" | "on" => Ok(true),
3231 "" => Ok(false),
3232 "false" | "no" | "off" => Ok(false),
3233 _ => {
3234 parse_git_config_maybe_bool(s).ok_or_else(|| format!("bad boolean config value '{s}'"))
3235 }
3236 }
3237}
3238
3239pub fn parse_i64(s: &str) -> std::result::Result<i64, String> {
3241 let s = s.trim();
3242 if s.is_empty() {
3243 return Err("empty integer value".to_owned());
3244 }
3245
3246 let (num_str, multiplier) = match s.as_bytes().last() {
3247 Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
3248 Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
3249 Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
3250 _ => (s, 1_i64),
3251 };
3252
3253 let base: i64 = num_str
3254 .parse()
3255 .map_err(|_| format!("invalid integer: '{s}'"))?;
3256 base.checked_mul(multiplier)
3257 .ok_or_else(|| format!("integer overflow: '{s}'"))
3258}
3259
3260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3262pub enum GitConfigIntStrictError {
3263 InvalidUnit,
3265 OutOfRange,
3267}
3268
3269pub fn parse_git_config_int_strict(raw: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
3273 let s = raw.trim();
3274 if s.is_empty() {
3275 return Err(GitConfigIntStrictError::InvalidUnit);
3276 }
3277
3278 let bytes = s.as_bytes();
3279 let mut idx = 0usize;
3280 if matches!(bytes.first(), Some(b'+') | Some(b'-')) {
3281 idx = 1;
3282 }
3283 if idx >= bytes.len() {
3284 return Err(GitConfigIntStrictError::InvalidUnit);
3285 }
3286 let digit_start = idx;
3287 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
3288 idx += 1;
3289 }
3290 if idx == digit_start {
3291 return Err(GitConfigIntStrictError::InvalidUnit);
3292 }
3293
3294 let num_part =
3295 std::str::from_utf8(&bytes[..idx]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3296 let suffix =
3297 std::str::from_utf8(&bytes[idx..]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3298 let mult: i64 = match suffix {
3299 "" => 1,
3300 "k" | "K" => 1024,
3301 "m" | "M" => 1024 * 1024,
3302 "g" | "G" => 1024_i64
3303 .checked_mul(1024)
3304 .and_then(|x| x.checked_mul(1024))
3305 .ok_or(GitConfigIntStrictError::OutOfRange)?,
3306 _ => return Err(GitConfigIntStrictError::InvalidUnit),
3307 };
3308
3309 let val: i64 = num_part
3310 .parse()
3311 .map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
3312 val.checked_mul(mult)
3313 .ok_or(GitConfigIntStrictError::OutOfRange)
3314}
3315
3316fn is_c_locale_space(b: u8) -> bool {
3317 matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
3318}
3319
3320fn skip_c_locale_leading_whitespace(s: &str) -> &str {
3321 let bytes = s.as_bytes();
3322 let mut i = 0usize;
3323 while i < bytes.len() && is_c_locale_space(bytes[i]) {
3324 i += 1;
3325 }
3326 &s[i..]
3327}
3328
3329fn git_unit_factor(suffix: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
3330 match suffix {
3331 "" => Ok(1),
3332 "k" | "K" => Ok(1024),
3333 "m" | "M" => Ok(1024 * 1024),
3334 "g" | "G" => Ok(1024_i64 * 1024 * 1024),
3335 _ => Err(GitConfigIntStrictError::InvalidUnit),
3336 }
3337}
3338
3339fn parse_base0_signed_prefix(s: &str) -> std::result::Result<(i64, &str), GitConfigIntStrictError> {
3341 let bytes = s.as_bytes();
3342 if bytes.is_empty() {
3343 return Err(GitConfigIntStrictError::InvalidUnit);
3344 }
3345 let mut i = 0usize;
3346 let mut sign: i64 = 1;
3347 if bytes[i] == b'+' {
3348 i += 1;
3349 } else if bytes[i] == b'-' {
3350 sign = -1;
3351 i += 1;
3352 }
3353 if i >= bytes.len() {
3354 return Err(GitConfigIntStrictError::InvalidUnit);
3355 }
3356
3357 let base = if bytes[i] == b'0' {
3358 if i + 1 < bytes.len() && matches!(bytes[i + 1], b'x' | b'X') {
3359 i += 2;
3360 if i >= bytes.len() {
3361 return Err(GitConfigIntStrictError::InvalidUnit);
3362 }
3363 16
3364 } else {
3365 8
3366 }
3367 } else {
3368 10
3369 };
3370
3371 let digit_start = i;
3372 let mut acc: i128 = 0;
3373 while i < bytes.len() {
3374 let ch = char::from(bytes[i]);
3375 let digit = match base {
3376 10 => ch.to_digit(10),
3377 8 => ch.to_digit(8),
3378 16 => ch.to_digit(16),
3379 _ => None,
3380 };
3381 let Some(d) = digit else {
3382 break;
3383 };
3384 acc = acc
3385 .checked_mul(base as i128)
3386 .and_then(|v| v.checked_add(i128::from(d)))
3387 .ok_or(GitConfigIntStrictError::OutOfRange)?;
3388 i += 1;
3389 }
3390 if i == digit_start {
3391 return Err(GitConfigIntStrictError::InvalidUnit);
3392 }
3393
3394 let signed = if sign < 0 { -acc } else { acc };
3395 let val = i64::try_from(signed).map_err(|_| GitConfigIntStrictError::OutOfRange)?;
3396 Ok((val, &s[i..]))
3397}
3398
3399pub fn parse_git_signed_config(
3401 raw: &str,
3402 max: i64,
3403) -> std::result::Result<i64, GitConfigIntStrictError> {
3404 if max < 0 {
3405 return Err(GitConfigIntStrictError::InvalidUnit);
3406 }
3407 let s = skip_c_locale_leading_whitespace(raw);
3408 if s.is_empty() {
3409 return Err(GitConfigIntStrictError::InvalidUnit);
3410 }
3411
3412 let (val, rest) = parse_base0_signed_prefix(s)?;
3413 let factor = git_unit_factor(rest)?;
3414
3415 if val < 0 {
3416 let min = -max - 1;
3417 if min / factor > val {
3418 return Err(GitConfigIntStrictError::OutOfRange);
3419 }
3420 } else if val > 0 && max / factor < val {
3421 return Err(GitConfigIntStrictError::OutOfRange);
3422 }
3423
3424 val.checked_mul(factor)
3425 .ok_or(GitConfigIntStrictError::OutOfRange)
3426}
3427
3428pub fn parse_git_config_int(raw: &str) -> std::result::Result<i32, GitConfigIntStrictError> {
3430 let max = i32::MAX as i64;
3431 let n = parse_git_signed_config(raw, max)?;
3432 i32::try_from(n).map_err(|_| GitConfigIntStrictError::OutOfRange)
3433}
3434
3435pub fn parse_git_config_maybe_bool(raw: &str) -> Option<bool> {
3437 if raw.is_empty() {
3438 return Some(false);
3439 }
3440 let lower = raw.to_ascii_lowercase();
3441 match lower.as_str() {
3442 "true" | "yes" | "on" => Some(true),
3443 "false" | "no" | "off" => Some(false),
3444 _ => parse_git_config_int(raw).ok().map(|n| n != 0),
3445 }
3446}
3447
3448const DIFF_CONTEXT_KEY: &str = "diff.context";
3449
3450const DEFAULT_GC_AUTO_THRESHOLD: usize = 6700;
3452const DEFAULT_GC_AUTO_PACK_LIMIT: usize = 50;
3454
3455fn bad_numeric_diff_context(
3456 value: &str,
3457 err: GitConfigIntStrictError,
3458 entry: &ConfigEntry,
3459) -> ConfigError {
3460 let reason = match err {
3461 GitConfigIntStrictError::InvalidUnit => crate::error::BadNumericSource::InvalidUnit,
3462 GitConfigIntStrictError::OutOfRange => crate::error::BadNumericSource::OutOfRange,
3463 };
3464 if entry.scope == ConfigScope::Command || entry.file.is_none() {
3465 ConfigError::BadNumericValue {
3466 key: DIFF_CONTEXT_KEY.to_owned(),
3467 value: value.to_owned(),
3468 reason,
3469 }
3470 } else {
3471 let path = entry
3472 .file
3473 .as_deref()
3474 .map(config_error_path_display)
3475 .unwrap_or_default();
3476 ConfigError::BadNumericValueInFile {
3477 key: DIFF_CONTEXT_KEY.to_owned(),
3478 value: value.to_owned(),
3479 file: path,
3480 reason,
3481 }
3482 }
3483}
3484
3485fn bad_diff_context_variable(entry: &ConfigEntry) -> ConfigError {
3486 if entry.scope == ConfigScope::Command || entry.file.is_none() {
3487 ConfigError::UnableToParseFromCommandLine {
3488 key: DIFF_CONTEXT_KEY.to_owned(),
3489 }
3490 } else {
3491 let path = entry
3492 .file
3493 .as_deref()
3494 .map(config_error_path_display)
3495 .unwrap_or_default();
3496 ConfigError::BadConfigVariableAtLine {
3497 key: DIFF_CONTEXT_KEY.to_owned(),
3498 file: path,
3499 line: entry.line,
3500 }
3501 }
3502}
3503
3504pub fn resolve_diff_context_lines(cfg: &ConfigSet) -> Result<Option<usize>> {
3509 let Some(entry) = cfg.get_last_entry(DIFF_CONTEXT_KEY) else {
3510 return Ok(None);
3511 };
3512 let value_src = entry.value.as_deref().unwrap_or("").trim();
3513 match parse_git_config_int_strict(value_src) {
3514 Ok(n) if n < 0 => Err(bad_diff_context_variable(&entry).into()),
3515 Ok(n) => Ok(Some(usize::try_from(n).map_err(|_| {
3516 bad_numeric_diff_context(value_src, GitConfigIntStrictError::OutOfRange, &entry)
3517 })?)),
3518 Err(e) => Err(bad_numeric_diff_context(value_src, e, &entry).into()),
3519 }
3520}
3521
3522pub fn parse_color(s: &str) -> std::result::Result<String, String> {
3529 const COLOR_BACKGROUND_OFFSET: i32 = 10;
3530 const COLOR_FOREGROUND_ANSI: i32 = 30;
3531 const COLOR_FOREGROUND_RGB: i32 = 38;
3532 const COLOR_FOREGROUND_256: i32 = 38;
3533 const COLOR_FOREGROUND_BRIGHT_ANSI: i32 = 90;
3534
3535 #[derive(Clone, Copy, Default)]
3536 struct Color {
3537 kind: u8,
3538 value: u8,
3539 red: u8,
3540 green: u8,
3541 blue: u8,
3542 }
3543
3544 const COLOR_UNSPECIFIED: u8 = 0;
3545 const COLOR_NORMAL: u8 = 1;
3546 const COLOR_ANSI: u8 = 2;
3547 const COLOR_256: u8 = 3;
3548 const COLOR_RGB: u8 = 4;
3549
3550 fn color_empty(c: &Color) -> bool {
3551 c.kind == COLOR_UNSPECIFIED || c.kind == COLOR_NORMAL
3552 }
3553
3554 fn parse_ansi_color(name: &str) -> Option<Color> {
3555 let color_names = [
3556 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
3557 ];
3558 let color_offset = COLOR_FOREGROUND_ANSI;
3559
3560 if name.eq_ignore_ascii_case("default") {
3561 return Some(Color {
3562 kind: COLOR_ANSI,
3563 value: (9 + color_offset) as u8,
3564 ..Default::default()
3565 });
3566 }
3567
3568 let (name, color_offset) = if name.len() >= 6 && name[..6].eq_ignore_ascii_case("bright") {
3569 (&name[6..], COLOR_FOREGROUND_BRIGHT_ANSI)
3570 } else {
3571 (name, COLOR_FOREGROUND_ANSI)
3572 };
3573
3574 for (i, cn) in color_names.iter().enumerate() {
3575 if name.eq_ignore_ascii_case(cn) {
3576 return Some(Color {
3577 kind: COLOR_ANSI,
3578 value: (i as i32 + color_offset) as u8,
3579 ..Default::default()
3580 });
3581 }
3582 }
3583 None
3584 }
3585
3586 fn hex_val(b: u8) -> Option<u8> {
3587 match b {
3588 b'0'..=b'9' => Some(b - b'0'),
3589 b'a'..=b'f' => Some(b - b'a' + 10),
3590 b'A'..=b'F' => Some(b - b'A' + 10),
3591 _ => None,
3592 }
3593 }
3594
3595 fn get_hex_color(chars: &[u8], width: usize) -> Option<(u8, usize)> {
3596 assert!(width == 1 || width == 2);
3597 if chars.len() < width {
3598 return None;
3599 }
3600 let v = if width == 2 {
3601 let hi = hex_val(chars[0])?;
3602 let lo = hex_val(chars[1])?;
3603 (hi << 4) | lo
3604 } else {
3605 let n = hex_val(chars[0])?;
3606 (n << 4) | n
3607 };
3608 Some((v, width))
3609 }
3610
3611 fn parse_single_color(word: &str) -> Option<Color> {
3612 if word.eq_ignore_ascii_case("normal") {
3613 return Some(Color {
3614 kind: COLOR_NORMAL,
3615 ..Default::default()
3616 });
3617 }
3618
3619 let bytes = word.as_bytes();
3620 if (bytes.len() == 7 || bytes.len() == 4) && bytes.first() == Some(&b'#') {
3621 let width = if bytes.len() == 7 { 2 } else { 1 };
3622 let mut idx = 1;
3623 let (r, n1) = get_hex_color(&bytes[idx..], width)?;
3624 idx += n1;
3625 let (g, n2) = get_hex_color(&bytes[idx..], width)?;
3626 idx += n2;
3627 let (b, n3) = get_hex_color(&bytes[idx..], width)?;
3628 idx += n3;
3629 if idx != bytes.len() {
3630 return None;
3631 }
3632 return Some(Color {
3633 kind: COLOR_RGB,
3634 red: r,
3635 green: g,
3636 blue: b,
3637 ..Default::default()
3638 });
3639 }
3640
3641 if let Some(c) = parse_ansi_color(word) {
3642 return Some(c);
3643 }
3644
3645 let Ok(val) = word.parse::<i64>() else {
3646 return None;
3647 };
3648 if val < -1 {
3649 return None;
3650 }
3651 if val < 0 {
3652 return Some(Color {
3653 kind: COLOR_NORMAL,
3654 ..Default::default()
3655 });
3656 }
3657 if val < 8 {
3658 return Some(Color {
3659 kind: COLOR_ANSI,
3660 value: (val as i32 + COLOR_FOREGROUND_ANSI) as u8,
3661 ..Default::default()
3662 });
3663 }
3664 if val < 16 {
3665 return Some(Color {
3666 kind: COLOR_ANSI,
3667 value: (val as i32 - 8 + COLOR_FOREGROUND_BRIGHT_ANSI) as u8,
3668 ..Default::default()
3669 });
3670 }
3671 if val < 256 {
3672 return Some(Color {
3673 kind: COLOR_256,
3674 value: val as u8,
3675 ..Default::default()
3676 });
3677 }
3678 None
3679 }
3680
3681 fn parse_attr(word: &str) -> Option<u8> {
3682 const ATTRS: [(&str, u8, u8); 8] = [
3683 ("bold", 1, 22),
3684 ("dim", 2, 22),
3685 ("italic", 3, 23),
3686 ("ul", 4, 24),
3687 ("underline", 4, 24),
3688 ("blink", 5, 25),
3689 ("reverse", 7, 27),
3690 ("strike", 9, 29),
3691 ];
3692
3693 let mut negate = false;
3694 let mut rest = word;
3695 if let Some(stripped) = rest.strip_prefix("no") {
3696 negate = true;
3697 rest = stripped;
3698 if let Some(s) = rest.strip_prefix('-') {
3699 rest = s;
3700 }
3701 }
3702
3703 for (name, val, neg) in ATTRS {
3704 if rest == name {
3705 return Some(if negate { neg } else { val });
3706 }
3707 }
3708 None
3709 }
3710
3711 fn append_color_output(out: &mut String, c: &Color, background: bool) {
3712 let offset = if background {
3713 COLOR_BACKGROUND_OFFSET
3714 } else {
3715 0
3716 };
3717 match c.kind {
3718 COLOR_UNSPECIFIED | COLOR_NORMAL => {}
3719 COLOR_ANSI => {
3720 use std::fmt::Write;
3721 let _ = write!(out, "{}", i32::from(c.value) + offset);
3722 }
3723 COLOR_256 => {
3724 use std::fmt::Write;
3725 let _ = write!(out, "{};5;{}", COLOR_FOREGROUND_256 + offset, c.value);
3726 }
3727 COLOR_RGB => {
3728 use std::fmt::Write;
3729 let _ = write!(
3730 out,
3731 "{};2;{};{};{}",
3732 COLOR_FOREGROUND_RGB + offset,
3733 c.red,
3734 c.green,
3735 c.blue
3736 );
3737 }
3738 _ => {}
3739 }
3740 }
3741
3742 let s = s.trim();
3743 if s.is_empty() {
3744 return Ok(String::new());
3745 }
3746
3747 let mut has_reset = false;
3748 let mut attr: u64 = 0;
3749 let mut fg = Color::default();
3750 let mut bg = Color::default();
3751 fg.kind = COLOR_UNSPECIFIED;
3752 bg.kind = COLOR_UNSPECIFIED;
3753
3754 for word in s.split_whitespace() {
3755 if word.eq_ignore_ascii_case("reset") {
3756 has_reset = true;
3757 continue;
3758 }
3759
3760 if let Some(c) = parse_single_color(word) {
3761 if fg.kind == COLOR_UNSPECIFIED {
3762 fg = c;
3763 continue;
3764 }
3765 if bg.kind == COLOR_UNSPECIFIED {
3766 bg = c;
3767 continue;
3768 }
3769 return Err(format!("bad color value '{s}'"));
3770 }
3771
3772 if let Some(code) = parse_attr(word) {
3773 attr |= 1u64 << u64::from(code);
3774 continue;
3775 }
3776
3777 return Err(format!("bad color value '{s}'"));
3778 }
3779
3780 if !has_reset && attr == 0 && color_empty(&fg) && color_empty(&bg) {
3781 return Err(format!("bad color value '{s}'"));
3782 }
3783
3784 let mut out = String::from("\x1b[");
3785 let mut sep = if has_reset { 1u32 } else { 0u32 };
3786
3787 let mut attr_bits = attr;
3788 let mut i = 0u32;
3789 while attr_bits != 0 {
3790 let bit = 1u64 << i;
3791 if attr_bits & bit == 0 {
3792 i += 1;
3793 continue;
3794 }
3795 attr_bits &= !bit;
3796 if sep > 0 {
3797 out.push(';');
3798 }
3799 sep += 1;
3800 use std::fmt::Write;
3801 let _ = write!(out, "{i}");
3802 i += 1;
3803 }
3804
3805 if !color_empty(&fg) {
3806 if sep > 0 {
3807 out.push(';');
3808 }
3809 sep += 1;
3810 append_color_output(&mut out, &fg, false);
3811 }
3812 if !color_empty(&bg) {
3813 if sep > 0 {
3814 out.push(';');
3815 }
3816 append_color_output(&mut out, &bg, true);
3817 }
3818 out.push('m');
3819 Ok(out)
3820}
3821
3822#[derive(Debug, Clone)]
3823struct UrlParts {
3824 scheme: String,
3825 user: Option<String>,
3826 host: String,
3827 port: Option<String>,
3828 path: String,
3829}
3830
3831#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3832struct UrlMatchScore {
3833 host_len: usize,
3834 path_len: usize,
3835 user_matched: bool,
3836}
3837
3838fn parse_config_url(url: &str) -> Option<UrlParts> {
3839 let (scheme, rest) = url.split_once("://")?;
3840 let (authority, path) = match rest.find('/') {
3841 Some(idx) => (&rest[..idx], &rest[idx..]),
3842 None => (rest, "/"),
3843 };
3844 let (user, host_port) = match authority.rsplit_once('@') {
3845 Some((user, host)) => (Some(user.to_owned()), host),
3846 None => (None, authority),
3847 };
3848 let (host, port) = match host_port.rsplit_once(':') {
3849 Some((host, port)) if !host.contains(']') => (host, Some(port.to_owned())),
3850 _ => (host_port, None),
3851 };
3852 Some(UrlParts {
3853 scheme: scheme.to_lowercase(),
3854 user,
3855 host: host.to_lowercase(),
3856 port,
3857 path: if path.is_empty() {
3858 "/".to_owned()
3859 } else {
3860 path.trim_end_matches('/').to_owned()
3861 },
3862 })
3863}
3864
3865fn host_matches(pattern: &str, target: &str) -> bool {
3866 let pattern_parts: Vec<&str> = pattern.split('.').collect();
3867 let target_parts: Vec<&str> = target.split('.').collect();
3868 pattern_parts.len() == target_parts.len()
3869 && pattern_parts
3870 .iter()
3871 .zip(target_parts)
3872 .all(|(pattern, target)| *pattern == "*" || *pattern == target)
3873}
3874
3875fn path_match_len(pattern: &str, target: &str) -> Option<usize> {
3876 let pattern = if pattern.is_empty() { "/" } else { pattern };
3877 let target = if target.is_empty() { "/" } else { target };
3878 if pattern == "/" {
3879 return Some(1);
3880 }
3881 let pattern = pattern.trim_end_matches('/');
3882 if target == pattern
3883 || target
3884 .strip_prefix(pattern)
3885 .is_some_and(|rest| rest.starts_with('/'))
3886 {
3887 Some(pattern.len() + 1)
3888 } else {
3889 None
3890 }
3891}
3892
3893fn url_match_score(pattern_url: &str, target_url: &str) -> Option<UrlMatchScore> {
3894 let pattern = parse_config_url(pattern_url)?;
3895 let target = parse_config_url(target_url)?;
3896 if pattern.scheme != target.scheme {
3897 return None;
3898 }
3899 let user_matched = match pattern.user.as_deref() {
3900 Some(user) if target.user.as_deref() == Some(user) => true,
3901 Some(_) => return None,
3902 None => false,
3903 };
3904 if !host_matches(&pattern.host, &target.host) || pattern.port != target.port {
3905 return None;
3906 }
3907 let path_len = path_match_len(&pattern.path, &target.path)?;
3908 Some(UrlMatchScore {
3909 host_len: pattern.host.len(),
3910 path_len,
3911 user_matched,
3912 })
3913}
3914
3915pub fn url_matches(pattern_url: &str, target_url: &str) -> bool {
3917 url_match_score(pattern_url, target_url).is_some()
3918}
3919
3920pub fn get_urlmatch_entries<'a>(
3922 entries: &'a [ConfigEntry],
3923 section: &str,
3924 variable: &str,
3925 url: &str,
3926) -> Vec<&'a ConfigEntry> {
3927 let section_lower = section.to_lowercase();
3928 let variable_lower = variable.to_lowercase();
3929 let mut matches: Vec<(UrlMatchScore, &'a ConfigEntry)> = Vec::new();
3930
3931 for entry in entries {
3932 let key = &entry.key;
3933 let first_dot = match key.find('.') {
3934 Some(i) => i,
3935 None => continue,
3936 };
3937 let last_dot = match key.rfind('.') {
3938 Some(i) => i,
3939 None => continue,
3940 };
3941 let entry_section = &key[..first_dot];
3942 let entry_variable = &key[last_dot + 1..];
3943 if entry_section.to_lowercase() != section_lower
3944 || entry_variable.to_lowercase() != variable_lower
3945 {
3946 continue;
3947 }
3948 if first_dot == last_dot {
3949 matches.push((
3950 UrlMatchScore {
3951 host_len: 0,
3952 path_len: 0,
3953 user_matched: false,
3954 },
3955 entry,
3956 ));
3957 } else {
3958 let subsection = &key[first_dot + 1..last_dot];
3959 if let Some(score) = url_match_score(subsection, url) {
3960 matches.push((score, entry));
3961 }
3962 }
3963 }
3964 matches.sort_by_key(|a| a.0);
3965 matches.into_iter().map(|(_, e)| e).collect()
3966}
3967
3968pub fn get_urlmatch_all_in_section(
3970 entries: &[ConfigEntry],
3971 section: &str,
3972 url: &str,
3973) -> Vec<(String, String, ConfigScope)> {
3974 let section_lower = section.to_lowercase();
3975 let mut matches: Vec<(String, UrlMatchScore, String, String, ConfigScope)> = Vec::new();
3976
3977 for entry in entries {
3978 let key = &entry.key;
3979 let first_dot = match key.find('.') {
3980 Some(i) => i,
3981 None => continue,
3982 };
3983 let last_dot = match key.rfind('.') {
3984 Some(i) => i,
3985 None => continue,
3986 };
3987 let entry_section = &key[..first_dot];
3988 if entry_section.to_lowercase() != section_lower {
3989 continue;
3990 }
3991 let entry_variable = &key[last_dot + 1..];
3992 let val = entry.value.as_deref().unwrap_or("");
3993 if first_dot == last_dot {
3994 let canonical = format!("{}.{}", section_lower, entry_variable);
3995 matches.push((
3996 entry_variable.to_lowercase(),
3997 UrlMatchScore {
3998 host_len: 0,
3999 path_len: 0,
4000 user_matched: false,
4001 },
4002 val.to_owned(),
4003 canonical,
4004 entry.scope,
4005 ));
4006 } else {
4007 let subsection = &key[first_dot + 1..last_dot];
4008 if let Some(score) = url_match_score(subsection, url) {
4009 let canonical = format!("{}.{}", section_lower, entry_variable);
4010 matches.push((
4011 entry_variable.to_lowercase(),
4012 score,
4013 val.to_owned(),
4014 canonical,
4015 entry.scope,
4016 ));
4017 }
4018 }
4019 }
4020
4021 let mut best: std::collections::BTreeMap<String, (UrlMatchScore, String, String, ConfigScope)> =
4022 std::collections::BTreeMap::new();
4023 for (var, specificity, val, canonical, scope) in matches {
4024 let entry = best.entry(var).or_insert((
4025 UrlMatchScore {
4026 host_len: 0,
4027 path_len: 0,
4028 user_matched: false,
4029 },
4030 String::new(),
4031 String::new(),
4032 scope,
4033 ));
4034 if specificity >= entry.0 {
4035 *entry = (specificity, val, canonical, scope);
4036 }
4037 }
4038 best.into_values()
4039 .map(|(_, val, canonical, scope)| (canonical, val, scope))
4040 .collect()
4041}
4042
4043pub fn parse_path(env: &Environment, s: &str) -> String {
4047 if let Some(rest) = s.strip_prefix("~/") {
4048 if let Some(home) = home_dir(env) {
4049 return home.join(rest).to_string_lossy().to_string();
4050 }
4051 }
4052 s.to_owned()
4053}
4054
4055pub fn parse_path_optional(env: &Environment, s: &str) -> Option<String> {
4060 if let Some(rest) = s.strip_prefix(":(optional)") {
4061 let resolved = parse_path(env, rest);
4062 if std::path::Path::new(&resolved).exists() {
4063 Some(resolved)
4064 } else {
4065 None }
4067 } else {
4068 Some(parse_path(env, s))
4069 }
4070}
4071
4072#[must_use]
4088pub fn git_config_parameters_last_value(raw: &str, key: &str) -> Option<String> {
4089 let Ok(canon) = canonical_key(key) else {
4090 return None;
4091 };
4092 let mut last: Option<String> = None;
4093 for entry in parse_config_parameters_strict(raw).ok()? {
4094 match entry {
4095 ConfigParameter::Pair { key, value } => {
4096 if canonical_key(key.trim()).ok().as_ref() == Some(&canon) {
4097 last = Some(value.unwrap_or_else(|| "true".to_owned()));
4098 }
4099 }
4100 ConfigParameter::OldStyle(entry) => {
4101 if let Some((k, v)) = entry.split_once('=') {
4102 if canonical_key(k.trim()).ok().as_ref() == Some(&canon) {
4103 last = Some(v.to_owned());
4104 }
4105 } else if canonical_key(entry.trim()).ok().as_ref() == Some(&canon) {
4106 last = Some("true".to_owned());
4107 }
4108 }
4109 }
4110 }
4111 last
4112}
4113
4114#[derive(Debug, Clone, PartialEq, Eq)]
4115enum ConfigParameter {
4116 OldStyle(String),
4117 Pair { key: String, value: Option<String> },
4118}
4119
4120pub fn parse_config_parameters(raw: &str) -> Vec<String> {
4124 parse_config_parameters_strict(raw)
4125 .map(|entries| {
4126 entries
4127 .into_iter()
4128 .map(|entry| match entry {
4129 ConfigParameter::OldStyle(entry) => entry,
4130 ConfigParameter::Pair {
4131 key,
4132 value: Some(value),
4133 } => format!("{key}\u{1}{value}"),
4134 ConfigParameter::Pair { key, value: None } => format!("{key}\u{1}"),
4135 })
4136 .collect()
4137 })
4138 .unwrap_or_default()
4139}
4140
4141fn parse_config_parameters_strict(raw: &str) -> Result<Vec<ConfigParameter>> {
4142 let mut out: Vec<ConfigParameter> = Vec::new();
4143 let chars: Vec<char> = raw.chars().collect();
4144 let mut idx = skip_config_parameter_spaces(&chars, 0);
4145
4146 while idx < chars.len() {
4147 let (key, next) = sq_dequote_step_chars(&chars, idx)?;
4148 let Some(next_idx) = next else {
4149 out.push(ConfigParameter::OldStyle(key));
4150 break;
4151 };
4152
4153 if chars[next_idx].is_whitespace() {
4154 out.push(ConfigParameter::OldStyle(key));
4155 idx = skip_config_parameter_spaces(&chars, next_idx);
4156 continue;
4157 }
4158
4159 if chars[next_idx] != '=' {
4160 return Err(Error::Config(
4161 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4162 ));
4163 }
4164
4165 let value_start = next_idx + 1;
4166 if value_start >= chars.len() || chars[value_start].is_whitespace() {
4167 out.push(ConfigParameter::Pair { key, value: None });
4168 idx = skip_config_parameter_spaces(&chars, value_start);
4169 continue;
4170 }
4171
4172 if chars[value_start] != '\'' {
4173 return Err(Error::Config(
4174 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4175 ));
4176 }
4177 let (value, value_next) = sq_dequote_step_chars(&chars, value_start)?;
4178 if let Some(value_next) = value_next {
4179 if !chars[value_next].is_whitespace() {
4180 return Err(Error::Config(
4181 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4182 ));
4183 }
4184 idx = skip_config_parameter_spaces(&chars, value_next);
4185 } else {
4186 idx = chars.len();
4187 }
4188 out.push(ConfigParameter::Pair {
4189 key,
4190 value: Some(value),
4191 });
4192 }
4193
4194 Ok(out)
4195}
4196
4197fn skip_config_parameter_spaces(chars: &[char], mut idx: usize) -> usize {
4198 while idx < chars.len() && chars[idx].is_whitespace() {
4199 idx += 1;
4200 }
4201 idx
4202}
4203
4204fn sq_dequote_step_chars(chars: &[char], start: usize) -> Result<(String, Option<usize>)> {
4205 if chars.get(start) != Some(&'\'') {
4206 return Err(Error::Config(
4207 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4208 ));
4209 }
4210
4211 let mut out = String::new();
4212 let mut idx = start + 1;
4213 loop {
4214 let Some(&ch) = chars.get(idx) else {
4215 return Err(Error::Config(
4216 "bogus format in GIT_CONFIG_PARAMETERS".to_owned().into(),
4217 ));
4218 };
4219 if ch != '\'' {
4220 out.push(ch);
4221 idx += 1;
4222 continue;
4223 }
4224
4225 idx += 1;
4226 match chars.get(idx).copied() {
4227 None => return Ok((out, None)),
4228 Some('\\')
4229 if chars
4230 .get(idx + 1)
4231 .copied()
4232 .is_some_and(needs_sq_backslash_quote)
4233 && chars.get(idx + 2) == Some(&'\'') =>
4234 {
4235 if let Some(escaped) = chars.get(idx + 1) {
4236 out.push(*escaped);
4237 }
4238 idx += 3;
4239 }
4240 _ => return Ok((out, Some(idx))),
4241 }
4242 }
4243}
4244
4245fn needs_sq_backslash_quote(ch: char) -> bool {
4246 ch == '\'' || ch == '!'
4247}
4248
4249pub(crate) fn resolve_config_override_path(env: &Environment, path: &str) -> PathBuf {
4251 let p = Path::new(path);
4252 if p.is_absolute() {
4253 p.to_path_buf()
4254 } else {
4255 env.cwd.join(p)
4256 }
4257}
4258
4259pub fn global_config_paths_pub(env: &Environment) -> Vec<PathBuf> {
4262 global_config_paths(env)
4263}
4264
4265fn global_config_paths(env: &Environment) -> Vec<PathBuf> {
4266 let mut paths = Vec::new();
4267
4268 if let Some(p) = env.git_config_global.as_deref() {
4270 paths.push(resolve_config_override_path(env, p));
4271 return paths;
4272 }
4273
4274 if let Some(xdg) = env.xdg_config_home.as_deref() {
4276 paths.push(PathBuf::from(xdg).join("git/config"));
4277 } else if let Some(home) = home_dir(env) {
4278 paths.push(home.join(".config/git/config"));
4279 }
4280 if let Some(home) = home_dir(env) {
4281 paths.push(home.join(".gitconfig"));
4282 }
4283
4284 paths
4285}
4286
4287#[must_use]
4292pub fn system_config_path(env: &Environment) -> PathBuf {
4293 if let Some(p) = env.git_config_system.as_deref() {
4294 return resolve_config_override_path(env, p);
4295 }
4296 let unix_default = PathBuf::from("/etc/gitconfig");
4297 #[cfg(windows)]
4298 {
4299 if unix_default.is_file() {
4300 return unix_default;
4301 }
4302 if let Some(root) = env.git_install_root.as_ref() {
4303 let candidate = PathBuf::from(root).join("etc").join("gitconfig");
4304 if candidate.is_file() {
4305 return candidate;
4306 }
4307 }
4308 for pf in [
4309 env.program_files.as_deref(),
4310 env.program_files_x86.as_deref(),
4311 ]
4312 .into_iter()
4313 .flatten()
4314 {
4315 let candidate = PathBuf::from(pf).join("Git").join("etc").join("gitconfig");
4316 if candidate.is_file() {
4317 return candidate;
4318 }
4319 }
4320 }
4321 unix_default
4322}
4323
4324fn home_dir(env: &Environment) -> Option<PathBuf> {
4331 if let Some(home) = env.home.as_ref().filter(|h| !h.is_empty()) {
4332 return Some(PathBuf::from(home));
4333 }
4334 #[cfg(windows)]
4335 {
4336 if let (Some(drive), Some(path)) = (
4337 env.homedrive.as_ref().filter(|d| !d.is_empty()),
4338 env.homepath.as_ref().filter(|p| !p.is_empty()),
4339 ) {
4340 let mut combined = drive.clone();
4341 combined.push(path);
4342 return Some(PathBuf::from(combined));
4343 }
4344 if let Some(profile) = env.userprofile.as_ref().filter(|p| !p.is_empty()) {
4345 return Some(PathBuf::from(profile));
4346 }
4347 }
4348 None
4349}
4350
4351fn include_source_is_disk_file(file: &ConfigFile) -> bool {
4353 file.include_origin == ConfigIncludeOrigin::Disk
4354}
4355
4356fn resolve_include_file_path(
4360 path: &str,
4361 file: &ConfigFile,
4362 ctx: &IncludeContext,
4363) -> Result<PathBuf> {
4364 let expanded = parse_path(ctx.env.as_ref(), path);
4365 let p = Path::new(&expanded);
4366 if p.is_absolute() {
4367 return Ok(p.to_path_buf());
4368 }
4369 if !include_source_is_disk_file(file) {
4370 if file.include_origin == ConfigIncludeOrigin::CommandLine {
4371 if ctx.command_line_relative_include_is_error {
4372 return Err(Error::Config(
4373 "relative config includes must come from files"
4374 .to_owned()
4375 .into(),
4376 ));
4377 }
4378 return Err(Error::Config(String::new().into()));
4379 }
4380 return Err(Error::Config(
4381 "relative config includes must come from files"
4382 .to_owned()
4383 .into(),
4384 ));
4385 }
4386 let base = match file.path.parent() {
4387 Some(p) if !p.as_os_str().is_empty() => p,
4388 Some(_) | None => Path::new("."),
4389 };
4390 Ok(base.join(p))
4391}
4392
4393fn is_dir_sep(b: u8) -> bool {
4394 b == b'/' || b == b'\\'
4395}
4396
4397fn add_trailing_starstar_for_dir(pat: &mut String) {
4398 let bytes = pat.as_bytes();
4399 if bytes.last().is_some_and(|&b| is_dir_sep(b)) {
4400 pat.push_str("**");
4401 }
4402}
4403
4404fn prepare_gitdir_pattern(
4406 condition: &str,
4407 file: &ConfigFile,
4408 ctx: &IncludeContext,
4409) -> Result<(String, usize)> {
4410 let mut pat = parse_path(ctx.env.as_ref(), condition);
4412 if pat.starts_with("./") || pat.starts_with(".\\") {
4413 if !include_source_is_disk_file(file) {
4414 return Err(Error::Config(
4415 "relative config include conditionals must come from files"
4416 .to_owned()
4417 .into(),
4418 ));
4419 }
4420 let parent = file.path.parent().ok_or_else(|| {
4421 Error::Config(
4422 "relative config include conditionals must come from files"
4423 .to_owned()
4424 .into(),
4425 )
4426 })?;
4427 let real = parent.canonicalize_simplified().map_err(Error::Io)?;
4428 let mut dir = real.to_string_lossy().into_owned();
4429 if !dir.ends_with('/') && !dir.ends_with('\\') {
4430 dir.push('/');
4431 }
4432 let rest = &pat[2..];
4433 pat = format!("{dir}{rest}");
4434 let prefix_len = dir.len();
4435 add_trailing_starstar_for_dir(&mut pat);
4436 return Ok((pat, prefix_len));
4437 }
4438 let p = Path::new(&pat);
4439 if !p.is_absolute() {
4440 pat.insert_str(0, "**/");
4441 }
4442 add_trailing_starstar_for_dir(&mut pat);
4443 Ok((pat, 0))
4444}
4445
4446fn git_dir_match_texts(ctx: &IncludeContext, git_dir: &Path) -> (String, String) {
4451 let real = git_dir
4452 .canonicalize_simplified()
4453 .map(|p| p.to_string_lossy().into_owned())
4454 .unwrap_or_else(|_| git_dir.to_string_lossy().into_owned());
4455 let abs = if git_dir.is_absolute() {
4458 let pwd_abs = ctx.pwd.as_deref().and_then(|pwd| {
4461 let pwd_path = std::path::Path::new(pwd);
4462 if !pwd_path.is_absolute() {
4463 return None;
4464 }
4465 let pwd_canon = pwd_path.canonicalize_simplified().ok()?;
4466 let git_dir_str = git_dir.to_string_lossy();
4467 let pwd_canon_str = pwd_canon.to_string_lossy();
4468 let suffix = git_dir_str.strip_prefix(pwd_canon_str.as_ref())?;
4470 Some(format!("{pwd}{suffix}"))
4471 });
4472 pwd_abs.unwrap_or_else(|| git_dir.to_string_lossy().into_owned())
4473 } else {
4474 ctx.cwd.join(git_dir).to_string_lossy().into_owned()
4475 };
4476 (real, abs)
4477}
4478
4479fn include_by_gitdir(
4480 condition: &str,
4481 file: &ConfigFile,
4482 ctx: &IncludeContext,
4483 icase: bool,
4484) -> bool {
4485 let Some(git_dir) = ctx.git_dir.as_ref() else {
4486 return false;
4487 };
4488 let (pattern, prefix) = match prepare_gitdir_pattern(condition, file, ctx) {
4489 Ok(x) => x,
4490 Err(_) => return false,
4491 };
4492 let flags = WM_PATHNAME | if icase { WM_CASEFOLD } else { 0 };
4493 let (text_real, text_abs) = git_dir_match_texts(ctx, git_dir);
4494 let try_match = |text: &str| -> bool {
4495 let t = text.as_bytes();
4496 let p = pattern.as_bytes();
4497 if prefix > 0 {
4498 if t.len() < prefix {
4499 return false;
4500 }
4501 let pre = &p[..prefix];
4502 let te = &t[..prefix];
4503 let ok = if icase {
4504 pre.eq_ignore_ascii_case(te)
4505 } else {
4506 pre == te
4507 };
4508 if !ok {
4509 return false;
4510 }
4511 return wildmatch(&p[prefix..], &t[prefix..], flags);
4512 }
4513 wildmatch(p, t, flags)
4514 };
4515 if try_match(&text_real) {
4516 return true;
4517 }
4518 text_real != text_abs && try_match(&text_abs)
4519}
4520
4521fn current_branch_short_name(git_dir: Option<&Path>) -> Option<String> {
4522 let gd = git_dir?;
4523 let target = refs::read_symbolic_ref(gd, "HEAD").ok()??;
4524 let rest = target.strip_prefix("refs/heads/")?;
4525 Some(rest.to_owned())
4526}
4527
4528fn include_by_onbranch(condition: &str, ctx: &IncludeContext) -> bool {
4529 let Some(short) = current_branch_short_name(ctx.git_dir.as_deref()) else {
4530 return false;
4531 };
4532 let mut pattern = condition.to_owned();
4533 add_trailing_starstar_for_dir(&mut pattern);
4534 wildmatch(pattern.as_bytes(), short.as_bytes(), WM_PATHNAME)
4535}
4536
4537fn is_remote_url_entry(entry: &ConfigEntry) -> bool {
4539 entry
4540 .key
4541 .strip_prefix("remote.")
4542 .is_some_and(|rest| rest.ends_with(".url"))
4543}
4544
4545fn hasconfig_remote_url_pattern(condition: &str) -> Option<&str> {
4547 condition.strip_prefix("hasconfig:remote.*.url:")
4548}
4549
4550fn evaluate_include_condition(condition: &str, file: &ConfigFile, ctx: &IncludeContext) -> bool {
4556 if let Some(rest) = condition.strip_prefix("gitdir/i:") {
4557 return include_by_gitdir(rest, file, ctx, true);
4558 }
4559 if let Some(rest) = condition.strip_prefix("gitdir:") {
4560 return include_by_gitdir(rest, file, ctx, false);
4561 }
4562 if let Some(rest) = condition.strip_prefix("onbranch:") {
4563 return include_by_onbranch(rest, ctx);
4564 }
4565 false
4566}
4567
4568fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
4570 let first_dot = key
4571 .find('.')
4572 .ok_or_else(|| Error::Config(format!("invalid key: '{key}'").into()))?;
4573 let last_dot = key
4574 .rfind('.')
4575 .ok_or_else(|| Error::Config(format!("invalid key: '{key}'").into()))?;
4576
4577 let section = key[..first_dot].to_owned();
4578 let variable = key[last_dot + 1..].to_owned();
4579
4580 let subsection = if first_dot == last_dot {
4581 None
4582 } else {
4583 Some(key[first_dot + 1..last_dot].to_owned())
4584 };
4585
4586 Ok((section, subsection, variable))
4587}
4588
4589fn parse_section_name(name: &str) -> (&str, Option<&str>) {
4593 match name.find('.') {
4594 Some(i) => (&name[..i], Some(&name[i + 1..])),
4595 None => (name, None),
4596 }
4597}
4598
4599fn section_matches(parser: &Parser, section_lower: &str, subsection: Option<&str>) -> bool {
4600 if parser.section.to_lowercase() == section_lower && parser.subsection.as_deref() == subsection
4601 {
4602 return true;
4603 }
4604 let Some(subsection) = subsection else {
4605 return false;
4606 };
4607 parser.subsection.is_none()
4608 && parser.section.to_lowercase() == format!("{section_lower}.{}", subsection.to_lowercase())
4609}
4610
4611#[must_use]
4613pub fn config_lock_path(path: &Path) -> PathBuf {
4614 let file_name = path
4615 .file_name()
4616 .and_then(|s| s.to_str())
4617 .unwrap_or("config");
4618 path.with_file_name(format!("{file_name}.lock"))
4619}
4620
4621fn validate_section_name(section: &str, subsection: Option<&str>) -> Result<()> {
4622 if section.is_empty()
4623 || !section
4624 .chars()
4625 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
4626 || subsection.is_some_and(str::is_empty)
4627 {
4628 return Err(Error::Config(
4629 format!("invalid section name: {section}").into(),
4630 ));
4631 }
4632 Ok(())
4633}
4634
4635fn raw_variable_name(raw_key: &str) -> &str {
4639 match raw_key.rfind('.') {
4640 Some(i) => &raw_key[i + 1..],
4641 None => raw_key,
4642 }
4643}
4644
4645fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
4650 let first_dot = match raw_key.find('.') {
4651 Some(i) => i,
4652 None => return (raw_key.to_owned(), None),
4653 };
4654 let last_dot = match raw_key.rfind('.') {
4656 Some(i) => i,
4657 None => return (raw_key[..first_dot].to_owned(), None),
4658 };
4659 let section = raw_key[..first_dot].to_owned();
4660 if first_dot == last_dot {
4661 (section, None)
4662 } else {
4663 let subsection = raw_key[first_dot + 1..last_dot].to_owned();
4664 (section, Some(subsection))
4665 }
4666}
4667
4668fn is_section_header_with_inline_entry(line: &str) -> bool {
4670 let trimmed = line.trim();
4671 if !trimmed.starts_with('[') {
4672 return false;
4673 }
4674 let end = match trimmed.find(']') {
4675 Some(i) => i,
4676 None => return false,
4677 };
4678 let after = trimmed[end + 1..].trim();
4679 !after.is_empty() && !after.starts_with('#') && !after.starts_with(';')
4681}
4682
4683fn extract_section_header(line: &str) -> String {
4686 let trimmed = line.trim();
4687 let end = match trimmed.find(']') {
4688 Some(i) => i,
4689 None => return line.to_owned(),
4690 };
4691 trimmed[..=end].to_owned()
4694}
4695
4696#[cfg(test)]
4697mod pack_object_config_tests {
4698 use super::{ConfigFile, ConfigScope, ConfigSet};
4699 use std::path::Path;
4700
4701 fn set_from_snippet(text: &str) -> ConfigSet {
4702 let path = Path::new(".git/config");
4703 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4704 let mut set = ConfigSet::new();
4705 set.merge(&file);
4706 set
4707 }
4708
4709 #[test]
4710 fn pack_object_window_and_depth_defaults() {
4711 let set = ConfigSet::new();
4712 assert_eq!(set.pack_object_window(), 10);
4713 assert_eq!(set.pack_object_depth(), crate::pack::DEFAULT_PACK_DEPTH);
4714 }
4715
4716 #[test]
4717 fn pack_object_window_and_depth_preserve_explicit_zero() {
4718 let set = set_from_snippet("[pack]\n\twindow = 0\n\tdepth = 0\n");
4719 assert_eq!(set.pack_object_window(), 0);
4720 assert_eq!(set.pack_object_depth(), 0);
4721 }
4722
4723 #[test]
4724 fn gc_auto_limits_from_config() {
4725 let set = set_from_snippet("[gc]\n\tauto = 100\n\tautoPackLimit = 3\n");
4726 assert_eq!(set.gc_auto_threshold(), Some(100));
4727 assert_eq!(set.gc_auto_pack_limit(), Some(3));
4728 let defaults = ConfigSet::new();
4729 assert_eq!(defaults.gc_auto_threshold(), Some(6700));
4730 assert_eq!(defaults.gc_auto_pack_limit(), Some(50));
4731 assert!(defaults.gc_auto_detach());
4732 assert!(defaults.maintenance_auto());
4733 let disabled = set_from_snippet("[gc]\n\tautoPackLimit = 0\n");
4734 assert_eq!(disabled.gc_auto_pack_limit(), None);
4735 assert_eq!(disabled.gc_auto_threshold(), Some(6700));
4736 let auto_off = set_from_snippet("[gc]\n\tauto = 0\n");
4737 assert_eq!(auto_off.gc_auto_threshold(), None);
4738 let negative = set_from_snippet("[gc]\n\tauto = -1\n\tautoPackLimit = -1\n");
4739 assert_eq!(negative.gc_auto_threshold(), None);
4740 assert_eq!(negative.gc_auto_pack_limit(), None);
4741 let unparsable = set_from_snippet("[gc]\n\tauto = lots\n");
4742 assert_eq!(unparsable.gc_auto_threshold(), Some(6700));
4743 let foreground =
4744 set_from_snippet("[gc]\n\tautoDetach = false\n[maintenance]\n\tauto = false\n");
4745 assert!(!foreground.gc_auto_detach());
4746 assert!(!foreground.maintenance_auto());
4747 }
4748}
4749
4750#[cfg(test)]
4751mod get_regexp_tests {
4752 use super::{ConfigFile, ConfigScope, ConfigSet};
4753 use std::path::Path;
4754
4755 fn set_from_snippet(text: &str) -> ConfigSet {
4756 let path = Path::new(".git/config");
4757 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4758 let mut set = ConfigSet::new();
4759 set.merge(&file);
4760 set
4761 }
4762
4763 #[test]
4764 fn get_regexp_matches_section_prefix_like_git_config() {
4765 let text = r#"
4766[user]
4767 email = alice@example.com
4768 name = Alice
4769[core]
4770 bare = false
4771"#;
4772 let set = set_from_snippet(text);
4773 let keys: Vec<_> = set
4774 .get_regexp("user")
4775 .expect("valid pattern")
4776 .into_iter()
4777 .map(|e| e.key.as_str())
4778 .collect();
4779 assert!(keys.contains(&"user.email"));
4780 assert!(keys.contains(&"user.name"));
4781 assert!(!keys.iter().any(|k| k.starts_with("core.")));
4782 }
4783
4784 #[test]
4785 fn get_regexp_returns_all_multi_value_entries_in_order() {
4786 let text = r#"
4787[remote "origin"]
4788 url = https://example.com/repo.git
4789 fetch = +refs/heads/*:refs/remotes/origin/*
4790 push = +refs/heads/main:refs/heads/main
4791 push = +refs/heads/develop:refs/heads/develop
4792"#;
4793 let set = set_from_snippet(text);
4794 let matches = set.get_regexp("remote.origin").expect("valid pattern");
4795 let push_vals: Vec<_> = matches
4796 .iter()
4797 .filter(|e| e.key == "remote.origin.push")
4798 .map(|e| e.value.as_deref().unwrap_or(""))
4799 .collect();
4800 assert_eq!(push_vals.len(), 2);
4801 assert_eq!(push_vals[0], "+refs/heads/main:refs/heads/main");
4802 assert_eq!(push_vals[1], "+refs/heads/develop:refs/heads/develop");
4803 }
4804
4805 #[test]
4806 fn get_regexp_dot_matches_any_key() {
4807 let text = r#"
4808[a]
4809 x = 1
4810[b]
4811 y = 2
4812"#;
4813 let set = set_from_snippet(text);
4814 let m = set.get_regexp(".").expect("valid pattern");
4815 assert_eq!(m.len(), 2);
4816 }
4817
4818 #[test]
4819 fn get_regexp_no_match_returns_empty_vec() {
4820 let set = set_from_snippet("[user]\n\tname = x\n");
4821 let m = set.get_regexp("zzz").expect("valid pattern");
4822 assert!(m.is_empty());
4823 }
4824
4825 #[test]
4826 fn get_regexp_invalid_pattern_is_error() {
4827 let set = set_from_snippet("[user]\n\tname = x\n");
4828 let err = set.get_regexp("(").expect_err("unclosed group");
4829 assert!(err.contains("invalid key pattern"), "got: {err}");
4830 }
4831
4832 #[test]
4833 fn valueless_negotiation_algorithm_is_a_typed_config_error() {
4834 use crate::error::{ConfigError, Error};
4835
4836 let path = Path::new("/repo/.git/config");
4837 for (text, line) in [
4838 (
4839 "[core]\n\tbare = false\n[fetch]\n\tnegotiationAlgorithm\n",
4840 4,
4841 ),
4842 ("[fetch] negotiationAlgorithm\n", 1),
4843 ] {
4844 let err = ConfigFile::parse(path, text, ConfigScope::Local).expect_err(text);
4845 let Error::Config(config_err) = err else {
4846 panic!("expected a config error for {text:?}, got {err:?}");
4847 };
4848 assert_eq!(
4849 config_err,
4850 ConfigError::BadConfigVariable {
4851 key: "fetch.negotiationalgorithm".to_owned(),
4852 file: ".git/config".to_owned(),
4853 line,
4854 },
4855 "{text:?}"
4856 );
4857 }
4858
4859 let ok = ConfigFile::parse(
4860 path,
4861 "[fetch]\n\tnegotiationAlgorithm = skipping\n",
4862 ConfigScope::Local,
4863 )
4864 .expect("valued key parses");
4865 assert_eq!(ok.entries[0].value.as_deref(), Some("skipping"));
4866 }
4867}
4868
4869#[cfg(test)]
4870mod loose_compression_tests {
4871 use std::path::Path;
4872
4873 use super::{ConfigFile, ConfigScope, ConfigSet, Error};
4874
4875 fn set_from_snippet(text: &str) -> ConfigSet {
4876 let path = Path::new(".git/config");
4877 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4878 let mut set = ConfigSet::new();
4879 set.merge(&file);
4880 set
4881 }
4882
4883 #[test]
4884 fn big_file_threshold_default_suffixes_and_errors() {
4885 assert_eq!(
4886 ConfigSet::new().big_file_threshold().unwrap(),
4887 512 * 1024 * 1024
4888 );
4889 let cases = [
4890 ("1000", 1000),
4891 ("0", 0),
4892 ("4k", 4096),
4893 ("2m", 2 * 1024 * 1024),
4894 ("1g", 1024 * 1024 * 1024),
4895 ("8G", 8 * 1024 * 1024 * 1024),
4896 ];
4897 for (raw, want) in cases {
4898 let set = set_from_snippet(&format!("[core]\n\tbigFileThreshold = {raw}\n"));
4899 assert_eq!(set.big_file_threshold().unwrap(), want, "value {raw}");
4900 }
4901 let set = set_from_snippet("[core]\n\tbigfilethreshold = 1k\n\tbigFileThreshold = 3k\n");
4902 assert_eq!(set.big_file_threshold().unwrap(), 3 * 1024);
4903
4904 for raw in ["-1", "12x", "lots", ""] {
4905 let snippet = if raw.is_empty() {
4906 "[core]\n\tbigFileThreshold\n".to_owned()
4907 } else {
4908 format!("[core]\n\tbigFileThreshold = {raw}\n")
4909 };
4910 let err = set_from_snippet(&snippet).big_file_threshold().unwrap_err();
4911 assert!(
4912 matches!(
4913 err,
4914 Error::Config(super::ConfigError::BadNumericValue { ref key, .. })
4915 if key == "core.bigfilethreshold"
4916 ),
4917 "value {raw:?}: {err:?}"
4918 );
4919 }
4920 }
4921
4922 #[test]
4923 fn loose_compression_cascade() {
4924 let set = ConfigSet::new();
4925 assert_eq!(
4926 set.loose_objects_zlib_level().unwrap(),
4927 ConfigSet::LOOSE_OBJECTS_ZLIB_DEFAULT
4928 );
4929
4930 let set = set_from_snippet("[core]\n\tcompression = 0\n");
4931 assert_eq!(set.loose_objects_zlib_level().unwrap(), 0);
4932
4933 let set = set_from_snippet("[core]\n\tcompression = 9\n");
4934 assert_eq!(set.loose_objects_zlib_level().unwrap(), 9);
4935
4936 let set = set_from_snippet("[core]\n\tcompression = 0\n\tloosecompression = 9\n");
4937 assert_eq!(set.loose_objects_zlib_level().unwrap(), 9);
4938
4939 let set = set_from_snippet("[core]\n\tloosecompression = 3\n");
4940 assert_eq!(set.loose_objects_zlib_level().unwrap(), 3);
4941
4942 let set = set_from_snippet("[core]\n\tcompression = -1\n");
4943 assert_eq!(set.loose_objects_zlib_level().unwrap(), 6);
4944
4945 for level in 0..=9 {
4946 let set = set_from_snippet(&format!("[core]\n\tloosecompression = {level}\n"));
4947 assert_eq!(set.loose_objects_zlib_level().unwrap(), level);
4948 }
4949
4950 let set = set_from_snippet("[core]\n\tloosecompression = -1\n");
4951 assert_eq!(set.loose_objects_zlib_level().unwrap(), 6);
4952
4953 let err = set_from_snippet("[core]\n\tloosecompression = 10\n")
4954 .loose_objects_zlib_level()
4955 .unwrap_err();
4956 assert!(matches!(err, Error::Config(_)));
4957
4958 let err = set_from_snippet("[core]\n\tloosecompression = not-a-number\n")
4959 .loose_objects_zlib_level()
4960 .unwrap_err();
4961 assert!(matches!(err, Error::Config(_)));
4962
4963 let err = set_from_snippet("[core]\n\tcompression = 42\n")
4964 .loose_objects_zlib_level()
4965 .unwrap_err();
4966 assert!(matches!(err, Error::Config(_)));
4967
4968 let err = set_from_snippet("[core]\n\tcompression = nope\n\tloosecompression = 1\n")
4969 .loose_objects_zlib_level()
4970 .unwrap_err();
4971 assert!(matches!(err, Error::Config(_)));
4972
4973 let err = set_from_snippet("[core]\n\tloosecompression = nope\n\tloosecompression = 1\n")
4974 .loose_objects_zlib_level()
4975 .unwrap_err();
4976 assert!(matches!(err, Error::Config(_)));
4977
4978 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 9\n");
4979 assert_eq!(set.loose_objects_zlib_level().unwrap(), 1);
4980
4981 let err = set_from_snippet("[core]\n\tloosecompression\n")
4982 .loose_objects_zlib_level()
4983 .unwrap_err();
4984 assert!(matches!(err, Error::Config(_)));
4985 }
4986}
4987
4988#[cfg(test)]
4989mod pack_compression_tests {
4990 use super::{ConfigFile, ConfigScope, ConfigSet};
4991 use std::path::Path;
4992
4993 fn set_from_snippet(text: &str) -> ConfigSet {
4994 let path = Path::new(".git/config");
4995 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
4996 let mut set = ConfigSet::new();
4997 set.merge(&file);
4998 set
4999 }
5000
5001 #[test]
5002 fn pack_objects_zlib_level_defaults_to_six() {
5003 let set = ConfigSet::new();
5004 assert_eq!(set.pack_objects_zlib_level().unwrap(), 6);
5005 }
5006
5007 #[test]
5008 fn pack_objects_zlib_level_core_compression() {
5009 let set = set_from_snippet("[core]\n\tcompression = 0\n");
5010 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
5011 let set = set_from_snippet("[core]\n\tcompression = 9\n");
5012 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5013 }
5014
5015 #[test]
5016 fn pack_objects_zlib_level_pack_overrides_core() {
5017 let set = set_from_snippet("[core]\n\tcompression = 9\n[pack]\n\tcompression = 0\n");
5018 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
5019 let set = set_from_snippet("[core]\n\tcompression = 0\n[pack]\n\tcompression = 9\n");
5020 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5021 }
5022
5023 #[test]
5024 fn pack_objects_zlib_level_later_core_does_not_override_earlier_pack() {
5025 let mut set = ConfigSet::new();
5026 set.merge(
5027 &ConfigFile::parse(
5028 Path::new("a"),
5029 "[pack]\n\tcompression = 9\n",
5030 ConfigScope::Local,
5031 )
5032 .unwrap(),
5033 );
5034 set.merge(
5035 &ConfigFile::parse(
5036 Path::new("b"),
5037 "[core]\n\tcompression = 0\n",
5038 ConfigScope::Local,
5039 )
5040 .unwrap(),
5041 );
5042 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5043 }
5044
5045 #[test]
5046 fn pack_objects_zlib_level_loosecompression_does_not_block_core_pack_level() {
5047 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 0\n");
5048 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
5049 }
5050
5051 #[test]
5052 fn pack_objects_zlib_level_pack_wins_after_loose_and_core() {
5053 let set = set_from_snippet(
5054 "[core]\n\tloosecompression = 1\n\tcompression = 0\n[pack]\n\tcompression = 9\n",
5055 );
5056 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
5057 }
5058}
5059
5060#[cfg(test)]
5061mod config_cache_tests {
5062 use super::*;
5063 use filetime::FileTime;
5064
5065 fn local_opts(git_dir: &Path) -> LoadConfigOptions {
5066 LoadConfigOptions {
5067 include_system: false,
5068 include_ctx: IncludeContext {
5069 git_dir: Some(git_dir.to_path_buf()),
5070 ..Default::default()
5071 },
5072 ..Default::default()
5073 }
5074 }
5075
5076 fn load_value(caches: &crate::repo_caches::RepoCaches, git_dir: &Path) -> Option<String> {
5077 let opts = local_opts(git_dir);
5078 let set = caches
5079 .load_config_with_options(&Environment::empty(), Some(git_dir), &opts)
5080 .expect("load cascade");
5081 set.get("gritcachetest.value")
5082 }
5083
5084 fn mtime_of(path: &Path) -> FileTime {
5085 FileTime::from_last_modification_time(&fs::metadata(path).expect("stat config"))
5086 }
5087
5088 fn restore_mtime(path: &Path, stamp: FileTime) {
5089 filetime::set_file_mtime(path, stamp).expect("restore mtime");
5090 }
5091
5092 #[test]
5093 fn cache_serves_same_stamp_and_config_write_evicts() {
5094 let caches =
5095 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5096 let td = tempfile::tempdir().expect("tempdir");
5097 let gd = td.path();
5098 let cfg = gd.join("config");
5099 fs::write(&cfg, "[gritcachetest]\n\tvalue = aaa\n").expect("write v1");
5100 let t0 = mtime_of(&cfg);
5101 assert_eq!(load_value(&caches, gd).as_deref(), Some("aaa"));
5102
5103 fs::write(&cfg, "[gritcachetest]\n\tvalue = bbb\n").expect("write v2");
5107 restore_mtime(&cfg, t0);
5108 assert_eq!(load_value(&caches, gd).as_deref(), Some("aaa"));
5109
5110 let mut file = ConfigFile::from_path(&cfg, ConfigScope::Local)
5112 .expect("read config")
5113 .expect("config exists");
5114 file.set("gritcachetest.value", "ccc").expect("set value");
5115 file.write().expect("persist");
5116 caches.evict_config_for_path(&cfg);
5117 restore_mtime(&cfg, t0);
5118 assert_eq!(load_value(&caches, gd).as_deref(), Some("ccc"));
5119 }
5120
5121 #[test]
5122 fn cache_invalidates_on_size_or_existence_change() {
5123 let caches =
5124 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5125 let td = tempfile::tempdir().expect("tempdir");
5126 let gd = td.path();
5127 let cfg = gd.join("config");
5128 assert_eq!(load_value(&caches, gd), None);
5130 fs::write(&cfg, "[gritcachetest]\n\tvalue = first\n").expect("create");
5132 assert_eq!(load_value(&caches, gd).as_deref(), Some("first"));
5133 let t0 = mtime_of(&cfg);
5135 fs::write(&cfg, "[gritcachetest]\n\tvalue = second-longer\n").expect("rewrite");
5136 restore_mtime(&cfg, t0);
5137 assert_eq!(load_value(&caches, gd).as_deref(), Some("second-longer"));
5138 }
5139
5140 #[test]
5141 fn include_targets_are_stamped_and_invalidate() {
5142 let caches =
5143 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5144 let td = tempfile::tempdir().expect("tempdir");
5145 let gd = td.path();
5146 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
5147 let inc = gd.join("extra.conf");
5148 fs::write(&inc, "[gritcachetest]\n\tvalue = one\n").expect("write include v1");
5149 assert_eq!(load_value(&caches, gd).as_deref(), Some("one"));
5150
5151 let t0 = mtime_of(&inc);
5155 fs::write(&inc, "[gritcachetest]\n\tvalue = two\n").expect("write include v2");
5156 restore_mtime(&inc, t0);
5157 assert_eq!(load_value(&caches, gd).as_deref(), Some("one"));
5158
5159 fs::write(&inc, "[gritcachetest]\n\tvalue = two-longer\n").expect("write include v3");
5161 assert_eq!(load_value(&caches, gd).as_deref(), Some("two-longer"));
5162 }
5163
5164 #[test]
5165 fn missing_include_target_is_watched() {
5166 let caches =
5167 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5168 let td = tempfile::tempdir().expect("tempdir");
5169 let gd = td.path();
5170 fs::write(gd.join("config"), "[include]\n\tpath = extra.conf\n").expect("write parent");
5171 assert_eq!(load_value(&caches, gd), None);
5172
5173 fs::write(gd.join("extra.conf"), "[gritcachetest]\n\tvalue = born\n").expect("create");
5176 assert_eq!(load_value(&caches, gd).as_deref(), Some("born"));
5177 }
5178
5179 #[test]
5180 fn onbranch_condition_follows_head() {
5181 let caches =
5182 crate::repo_caches::RepoCaches::new(crate::command_runner::system_command_runner());
5183 let td = tempfile::tempdir().expect("tempdir");
5184 let gd = td.path();
5185 fs::write(gd.join("HEAD"), "ref: refs/heads/main\n").expect("write HEAD");
5186 fs::write(
5187 gd.join("config"),
5188 "[includeIf \"onbranch:main\"]\n\tpath = branch.conf\n",
5189 )
5190 .expect("write config");
5191 fs::write(
5192 gd.join("branch.conf"),
5193 "[gritcachetest]\n\tvalue = onmain\n",
5194 )
5195 .expect("write branch config");
5196 assert_eq!(load_value(&caches, gd).as_deref(), Some("onmain"));
5197
5198 fs::write(gd.join("HEAD"), "ref: refs/heads/dev\n").expect("rewrite HEAD");
5201 assert_eq!(load_value(&caches, gd), None);
5202 }
5203
5204 #[test]
5205 #[cfg(unix)]
5206 fn load_propagates_local_config_io_errors() {
5207 use std::os::unix::fs::symlink;
5208 let td = tempfile::tempdir().expect("tempdir");
5209 let git_dir = td.path();
5210 let config = git_dir.join("config");
5211 symlink("config", &config).expect("self-referential symlink");
5212 let err = ConfigSet::load(&Environment::empty(), Some(git_dir), false).unwrap_err();
5213 assert!(
5214 matches!(err, Error::Io(_)),
5215 "expected I/O error for symlink loop, got {err:?}"
5216 );
5217 }
5218}
5219
5220#[cfg(test)]
5221mod line_scan_tests {
5222 use super::*;
5223
5224 fn strip_inline_comment_by_chars(s: &str) -> String {
5226 let mut in_quote = false;
5227 let mut last_was_backslash = false;
5228 let mut result = String::with_capacity(s.len());
5229 let mut chars = s.chars().peekable();
5230 while let Some(ch) = chars.next() {
5231 match ch {
5232 '"' if !last_was_backslash => {
5233 in_quote = !in_quote;
5234 result.push(ch);
5235 last_was_backslash = false;
5236 }
5237 '\\' if !last_was_backslash => {
5238 last_was_backslash = true;
5239 result.push(ch);
5240 if in_quote {
5241 if let Some(&next) = chars.peek() {
5242 result.push(next);
5243 chars.next();
5244 last_was_backslash = false;
5245 }
5246 }
5247 }
5248 '#' | ';' if !in_quote && !last_was_backslash => break,
5249 _ => {
5250 last_was_backslash = false;
5251 result.push(ch);
5252 }
5253 }
5254 }
5255 result.trim_end().to_owned()
5256 }
5257
5258 fn unclosed_quote_by_chars(line: &str) -> bool {
5259 let trimmed = line.trim();
5260 let Some(eq_pos) = trimmed.find('=') else {
5261 return false;
5262 };
5263 let mut in_quote = false;
5264 let mut last_was_backslash = false;
5265 for ch in trimmed[eq_pos + 1..].trim_start().chars() {
5266 match ch {
5267 '"' if !last_was_backslash => {
5268 in_quote = !in_quote;
5269 last_was_backslash = false;
5270 }
5271 '\\' if !last_was_backslash => {
5272 last_was_backslash = true;
5273 continue;
5274 }
5275 '#' | ';' if !in_quote && !last_was_backslash => return false,
5276 _ => last_was_backslash = false,
5277 }
5278 }
5279 in_quote
5280 }
5281
5282 fn continues_by_chars(line: &str) -> bool {
5283 let trimmed = line.trim();
5284 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
5285 return false;
5286 }
5287 let Some(pos) = trimmed.find('=') else {
5288 return false;
5289 };
5290 let mut in_quote = false;
5291 let mut last_was_backslash = false;
5292 let mut in_comment = false;
5293 for ch in trimmed[pos + 1..].chars() {
5294 if in_comment {
5295 last_was_backslash = false;
5296 continue;
5297 }
5298 match ch {
5299 '"' if !last_was_backslash => {
5300 in_quote = !in_quote;
5301 last_was_backslash = false;
5302 }
5303 '\\' if !last_was_backslash => {
5304 last_was_backslash = true;
5305 continue;
5306 }
5307 '#' | ';' if !in_quote && !last_was_backslash => {
5308 in_comment = true;
5309 last_was_backslash = false;
5310 }
5311 _ => last_was_backslash = false,
5312 }
5313 }
5314 last_was_backslash && !in_comment
5315 }
5316
5317 fn corpus() -> Vec<String> {
5320 const TOKENS: [&str; 11] = [
5321 "a", "\"", "\\", "#", ";", " ", "=", "\t", "é", "日", "\u{2003}",
5322 ];
5323 let mut out = vec![String::new()];
5324 let mut frontier = vec![String::new()];
5325 for _ in 0..5 {
5326 let mut next = Vec::with_capacity(frontier.len() * TOKENS.len());
5327 for prefix in &frontier {
5328 for token in TOKENS {
5329 next.push(format!("{prefix}{token}"));
5330 }
5331 }
5332 out.extend(next.iter().cloned());
5333 frontier = next;
5334 }
5335 out
5336 }
5337
5338 #[test]
5339 fn byte_scanners_match_char_scanners_on_every_short_line() {
5340 for s in corpus() {
5341 assert_eq!(
5342 strip_inline_comment(&s),
5343 strip_inline_comment_by_chars(&s),
5344 "strip_inline_comment({s:?})"
5345 );
5346 let line = format!("k = {s}");
5347 assert_eq!(
5348 entry_line_value_has_unclosed_quote(&line),
5349 unclosed_quote_by_chars(&line),
5350 "unclosed quote in {line:?}"
5351 );
5352 assert_eq!(
5353 value_line_continues(&line),
5354 continues_by_chars(&line),
5355 "continuation of {line:?}"
5356 );
5357 assert_eq!(
5358 entry_line_value_has_unclosed_quote(&s),
5359 unclosed_quote_by_chars(&s),
5360 "unclosed quote in {s:?}"
5361 );
5362 assert_eq!(
5363 value_line_continues(&s),
5364 continues_by_chars(&s),
5365 "continuation of {s:?}"
5366 );
5367 }
5368 }
5369
5370 #[test]
5371 fn strip_inline_comment_borrows_a_prefix() {
5372 let s = "\"a # b\" tail ; comment";
5373 let stripped = strip_inline_comment(s);
5374 assert_eq!(stripped, "\"a # b\" tail");
5375 assert_eq!(stripped.as_ptr(), s.as_ptr());
5376 assert_eq!(strip_inline_comment("\"\\\"#\" x"), "\"\\\"#\" x");
5377 assert_eq!(strip_inline_comment("v\\#kept # gone"), "v\\#kept");
5378 }
5379
5380 #[test]
5381 fn canonical_key_lowercases_section_and_name_only() {
5382 assert_eq!(canonical_key("core.bare").unwrap(), "core.bare");
5383 assert_eq!(canonical_key("CORE.BARE").unwrap(), "core.bare");
5384 assert_eq!(
5385 canonical_key("Remote.Origin.URL").unwrap(),
5386 "remote.Origin.url"
5387 );
5388 assert_eq!(canonical_key("a.B.C.D").unwrap(), "a.B.C.d");
5389 assert!(canonical_key("nodot").is_err());
5390 assert!(canonical_key("core.").is_err());
5391 assert!(canonical_key("core.1bad").is_err());
5392 assert!(canonical_key("core.ba\nre").is_err());
5393 }
5394
5395 #[test]
5396 fn parsed_keys_reuse_the_section_prefix_per_header() {
5397 let text = "[Core]\n\tBare = false\n\tFileMode\n\
5398 [Remote \"Origin\"]\n\tURL = /x\n\
5399 [branch \"Main\"] Remote = origin\n\
5400 [core]\n\tEditor = vi ; trailing\n";
5401 let file = ConfigFile::parse(Path::new("cfg"), text, ConfigScope::Local).unwrap();
5402 let keys: Vec<(&str, Option<&str>)> = file
5403 .entries
5404 .iter()
5405 .map(|e| (e.key.as_str(), e.value.as_deref()))
5406 .collect();
5407 assert_eq!(
5408 keys,
5409 [
5410 ("core.bare", Some("false")),
5411 ("core.filemode", None),
5412 ("remote.Origin.url", Some("/x")),
5413 ("branch.Main.remote", Some("origin")),
5414 ("core.editor", Some("vi")),
5415 ]
5416 );
5417 }
5418
5419 #[test]
5420 fn push_lowercase_matches_to_lowercase() {
5421 for s in ["", "core", "MiXeD-09", "ÄBC", "İx", "ΣΑΣ"] {
5422 let mut out = String::from("p.");
5423 push_lowercase(&mut out, s);
5424 assert_eq!(out, format!("p.{}", s.to_lowercase()));
5425 }
5426 }
5427}