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