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