1use std::fmt;
31use std::fs;
32use std::path::{Path, PathBuf};
33
34use crate::error::{Error, Result};
35use crate::refs;
36use crate::wildmatch::{wildmatch, WM_CASEFOLD, WM_PATHNAME};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub enum ConfigScope {
41 System,
43 Global,
45 Local,
47 Worktree,
49 Command,
51}
52
53impl fmt::Display for ConfigScope {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 match self {
56 Self::System => write!(f, "system"),
57 Self::Global => write!(f, "global"),
58 Self::Local => write!(f, "local"),
59 Self::Worktree => write!(f, "worktree"),
60 Self::Command => write!(f, "command"),
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct ConfigEntry {
68 pub key: String,
71 pub value: Option<String>,
73 pub scope: ConfigScope,
75 pub file: Option<PathBuf>,
77 pub line: usize,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum ConfigIncludeOrigin {
84 Disk,
86 Stdin,
88 CommandLine,
90 Blob,
92}
93
94#[derive(Debug, Clone)]
97pub struct ConfigFile {
98 pub path: PathBuf,
100 pub scope: ConfigScope,
102 pub entries: Vec<ConfigEntry>,
104 raw_lines: Vec<String>,
106 pub include_origin: ConfigIncludeOrigin,
108}
109
110#[derive(Debug, Clone, Default)]
115pub struct ConfigSet {
116 entries: Vec<ConfigEntry>,
118}
119
120#[derive(Debug, Clone, Default)]
122pub struct IncludeContext {
123 pub git_dir: Option<PathBuf>,
125 pub command_line_relative_include_is_error: bool,
127}
128
129#[derive(Debug, Clone)]
131pub struct LoadConfigOptions {
132 pub include_system: bool,
134 pub process_includes: bool,
136 pub command_includes: bool,
138 pub include_ctx: IncludeContext,
139}
140
141impl Default for LoadConfigOptions {
142 fn default() -> Self {
143 Self {
144 include_system: true,
145 process_includes: true,
146 command_includes: true,
147 include_ctx: IncludeContext::default(),
148 }
149 }
150}
151
152pub fn canonical_key(raw: &str) -> Result<String> {
168 if raw.contains('\n') || raw.contains('\r') {
170 return Err(Error::ConfigError(format!(
171 "invalid key: '{}'",
172 raw.replace('\n', "\\n")
173 )));
174 }
175
176 let first_dot = raw
177 .find('.')
178 .ok_or_else(|| Error::ConfigError(format!("key does not contain a section: '{raw}'")))?;
179 let last_dot = raw
180 .rfind('.')
181 .ok_or_else(|| Error::ConfigError(format!("key does not contain a section: '{raw}'")))?;
182
183 if last_dot == raw.len() - 1 {
184 return Err(Error::ConfigError(format!(
185 "key does not contain variable name: '{raw}'"
186 )));
187 }
188
189 let section = &raw[..first_dot];
190 let name = &raw[last_dot + 1..];
191
192 if section.is_empty() || !section.chars().all(|c| c.is_alphanumeric() || c == '-') {
194 return Err(Error::ConfigError(format!(
195 "invalid key (bad section): '{raw}'"
196 )));
197 }
198
199 if !name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
201 || !name.chars().all(|c| c.is_alphanumeric() || c == '-')
202 {
203 return Err(Error::ConfigError(format!(
204 "invalid key (bad variable name): '{raw}'"
205 )));
206 }
207
208 if first_dot == last_dot {
209 Ok(format!(
211 "{}.{}",
212 section.to_lowercase(),
213 name.to_lowercase()
214 ))
215 } else {
216 let subsection = &raw[first_dot + 1..last_dot];
218 Ok(format!(
219 "{}.{}.{}",
220 section.to_lowercase(),
221 subsection,
222 name.to_lowercase()
223 ))
224 }
225}
226
227#[must_use]
231pub fn config_file_display_for_error(path: &Path) -> String {
232 config_error_path_display(path)
233}
234
235fn config_error_path_display(path: &Path) -> String {
236 if path == Path::new("-") {
237 return "standard input".to_owned();
238 }
239 if path.file_name().and_then(|s| s.to_str()) == Some("config")
240 && path
241 .parent()
242 .and_then(|p| p.file_name())
243 .and_then(|s| s.to_str())
244 == Some(".git")
245 {
246 return ".git/config".to_owned();
247 }
248 path.display().to_string()
249}
250
251struct Parser {
253 section: String,
254 subsection: Option<String>,
255}
256
257impl Parser {
258 fn new() -> Self {
259 Self {
260 section: String::new(),
261 subsection: None,
262 }
263 }
264
265 fn make_key(&self, name: &str) -> String {
267 let sec = self.section.to_lowercase();
268 let var = name.to_lowercase();
269 match &self.subsection {
270 Some(sub) => format!("{sec}.{sub}.{var}"),
271 None => format!("{sec}.{var}"),
272 }
273 }
274
275 fn try_parse_section_with_remainder<'a>(
281 &mut self,
282 line: &'a str,
283 inline_remainder: &mut Option<&'a str>,
284 ) -> bool {
285 let trimmed = line.trim();
286 if !trimmed.starts_with('[') {
287 return false;
288 }
289 let end = {
293 let bytes = trimmed.as_bytes();
294 let mut i = 1; let mut in_quotes = false;
296 let mut found = None;
297 while i < bytes.len() {
298 if in_quotes {
299 if bytes[i] == b'\\' {
300 i += 2; continue;
302 }
303 if bytes[i] == b'"' {
304 in_quotes = false;
305 }
306 } else {
307 if bytes[i] == b'"' {
308 in_quotes = true;
309 }
310 if bytes[i] == b']' {
311 found = Some(i);
312 break;
313 }
314 }
315 i += 1;
316 }
317 match found {
318 Some(i) => i,
319 None => return false,
320 }
321 };
322 let inside = &trimmed[1..end];
323 if let Some(quote_start) = inside.find('"') {
325 self.section = inside[..quote_start].trim().to_owned();
326 let rest = &inside[quote_start + 1..];
327 let mut sub = String::new();
329 let mut chars = rest.chars();
330 while let Some(ch) = chars.next() {
331 if ch == '\\' {
332 if let Some(escaped) = chars.next() {
333 sub.push(escaped);
334 }
335 } else if ch == '"' {
336 break;
337 } else {
338 sub.push(ch);
339 }
340 }
341 self.subsection = Some(sub);
342 } else {
343 self.section = inside.trim().to_owned();
344 self.subsection = None;
345 }
346 let after = trimmed[end + 1..].trim();
348 if !after.is_empty() && !after.starts_with('#') && !after.starts_with(';') {
349 *inline_remainder = Some(after);
350 } else {
351 *inline_remainder = None;
352 }
353 true
354 }
355
356 fn try_parse_section(&mut self, line: &str) -> bool {
358 let mut _remainder = None;
359 self.try_parse_section_with_remainder(line, &mut _remainder)
360 }
361
362 fn try_parse_entry(&self, line: &str) -> Option<(String, Option<String>)> {
366 let trimmed = line.trim();
367 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
368 return None;
369 }
370 if trimmed.starts_with('[') {
371 return None;
372 }
373 if self.section.is_empty() {
374 return None;
375 }
376
377 if let Some(eq_pos) = trimmed.find('=') {
378 let raw_name = trimmed[..eq_pos].trim();
379 let raw_value = trimmed[eq_pos + 1..].trim();
380 let value = strip_inline_comment(raw_value);
382 let value = unescape_value(&value);
383 let key = self.make_key(raw_name);
384 Some((key, Some(value)))
385 } else {
386 let raw_name = strip_inline_comment(trimmed);
388 if raw_name.split_whitespace().count() > 1 {
389 return None;
390 }
391 let key = self.make_key(raw_name.trim());
392 Some((key, None))
393 }
394 }
395}
396
397fn entry_line_value_has_unclosed_quote(line: &str) -> bool {
407 let trimmed = line.trim();
408 let Some(eq_pos) = trimmed.find('=') else {
409 return false;
410 };
411 let raw_value = trimmed[eq_pos + 1..].trim_start();
412 let mut in_quote = false;
413 let mut last_was_backslash = false;
414 for ch in raw_value.chars() {
415 match ch {
416 '"' if !last_was_backslash => {
417 in_quote = !in_quote;
418 last_was_backslash = false;
419 }
420 '\\' if in_quote && !last_was_backslash => {
421 last_was_backslash = true;
422 continue;
423 }
424 '#' | ';' if !in_quote && !last_was_backslash => return false,
425 _ => {
426 last_was_backslash = false;
427 }
428 }
429 }
430 in_quote
431}
432
433fn value_line_continues(line: &str) -> bool {
434 let trimmed = line.trim();
435 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
436 return false;
437 }
438 let value_part = match trimmed.find('=') {
441 Some(pos) => &trimmed[pos + 1..],
442 None => return false,
443 };
444 let mut in_quote = false;
446 let mut last_was_backslash = false;
447 let mut in_comment = false;
448 for ch in value_part.chars() {
449 if in_comment {
450 last_was_backslash = false;
452 continue;
453 }
454 match ch {
455 '"' if !last_was_backslash => {
456 in_quote = !in_quote;
457 last_was_backslash = false;
458 }
459 '\\' if !last_was_backslash => {
460 last_was_backslash = true;
461 continue;
462 }
463 '#' | ';' if !in_quote && !last_was_backslash => {
464 in_comment = true;
465 last_was_backslash = false;
466 }
467 _ => {
468 last_was_backslash = false;
469 }
470 }
471 }
472 last_was_backslash && !in_comment
474}
475
476fn strip_inline_comment(s: &str) -> String {
478 let mut in_quote = false;
479 let mut result = String::with_capacity(s.len());
480 let mut chars = s.chars().peekable();
481 while let Some(ch) = chars.next() {
482 match ch {
483 '"' => {
484 in_quote = !in_quote;
485 result.push(ch);
486 }
487 '\\' if in_quote => {
488 result.push(ch);
489 if let Some(&next) = chars.peek() {
490 result.push(next);
491 chars.next();
492 }
493 }
494 '#' | ';' if !in_quote => break,
495 _ => result.push(ch),
496 }
497 }
498 let trimmed = result.trim_end();
500 trimmed.to_owned()
501}
502
503fn unescape_value(s: &str) -> String {
506 let mut result = String::with_capacity(s.len());
507 let mut chars = s.chars();
508 while let Some(ch) = chars.next() {
509 match ch {
510 '"' => { }
511 '\\' => match chars.next() {
512 Some('n') => result.push('\n'),
513 Some('r') => result.push('\r'),
514 Some('t') => result.push('\t'),
515 Some('\\') => result.push('\\'),
516 Some('"') => result.push('"'),
517 Some(other) => {
518 result.push('\\');
519 result.push(other);
520 }
521 None => result.push('\\'),
522 },
523 _ => result.push(ch),
524 }
525 }
526 result
527}
528
529fn escape_subsection(s: &str) -> String {
536 let mut out = String::with_capacity(s.len());
537 for ch in s.chars() {
538 match ch {
539 '"' => out.push_str("\\\""),
540 '\\' => out.push_str("\\\\"),
541 other => out.push(other),
542 }
543 }
544 out
545}
546
547fn escape_value(s: &str) -> String {
548 let leading_dash_needs_quoting = s.starts_with('-') && parse_i64(s).is_err();
551 let needs_quoting = leading_dash_needs_quoting
552 || s.starts_with(' ')
553 || s.starts_with('\t')
554 || s.ends_with(' ')
555 || s.ends_with('\t')
556 || s.contains('"')
557 || s.contains('\\')
558 || s.contains('\n')
559 || s.contains('\r')
560 || s.contains('#')
561 || s.contains(';');
562
563 if !needs_quoting {
564 return s.to_owned();
565 }
566
567 let mut out = String::with_capacity(s.len() + 4);
568 out.push('"');
569 for ch in s.chars() {
570 match ch {
571 '"' => out.push_str("\\\""),
572 '\\' => out.push_str("\\\\"),
573 '\n' => out.push_str("\\n"),
574 '\r' => out.push_str("\\r"),
575 '\t' => out.push_str("\\t"),
576 other => out.push(other),
577 }
578 }
579 out.push('"');
580 out
581}
582
583fn format_comment_suffix(comment: Option<&str>) -> String {
590 match comment {
591 None => String::new(),
592 Some(c) => {
593 if c.starts_with(' ') || c.starts_with('\t') {
594 c.to_owned()
596 } else if c.starts_with('#') {
597 format!(" {c}")
599 } else {
600 format!(" # {c}")
602 }
603 }
604 }
605}
606
607impl ConfigFile {
608 pub fn parse(path: &Path, content: &str, scope: ConfigScope) -> Result<Self> {
620 let raw_lines: Vec<String> = content
621 .lines()
622 .map(|l| l.strip_suffix('\r').unwrap_or(l))
623 .map(String::from)
624 .collect();
625 let mut entries = Vec::new();
626 let mut parser = Parser::new();
627
628 let mut idx = 0;
629 while idx < raw_lines.len() {
630 let start_idx = idx;
631 let line = &raw_lines[idx];
632 idx += 1;
633
634 let trimmed = line.trim();
636 if trimmed.starts_with('#') || trimmed.starts_with(';') {
637 continue;
638 }
639
640 let mut inline_remainder = None;
641 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
642 if let Some(remainder) = inline_remainder {
644 if let Some((key, value)) = parser.try_parse_entry(remainder) {
645 if key == "fetch.negotiationalgorithm" && value.is_none() {
646 let file_disp = config_error_path_display(path);
647 return Err(Error::Message(format!(
648 "error: missing value for 'fetch.negotiationalgorithm'\n\
649fatal: bad config variable 'fetch.negotiationalgorithm' in file '{file_disp}' at line {}",
650 start_idx + 1
651 )));
652 }
653 entries.push(ConfigEntry {
654 key,
655 value,
656 scope,
657 file: Some(path.to_path_buf()),
658 line: start_idx + 1,
659 });
660 }
661 }
662 continue;
663 }
664
665 let mut logical_line = line.clone();
668 while value_line_continues(&logical_line) && idx < raw_lines.len() {
669 let t = logical_line.trim_end();
671 logical_line = t[..t.len() - 1].to_string();
672 let next = raw_lines[idx].trim_start();
674 logical_line.push_str(next);
675 idx += 1;
676 }
677
678 while entry_line_value_has_unclosed_quote(&logical_line) && idx < raw_lines.len() {
679 let next = raw_lines[idx].trim_start();
680 logical_line.push_str(next);
681 idx += 1;
682 }
683 if entry_line_value_has_unclosed_quote(&logical_line) {
684 let file_disp = config_error_path_display(path);
685 return Err(Error::ConfigError(format!(
686 "bad config line {} in file '{file_disp}'",
687 start_idx + 1
688 )));
689 }
690
691 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
692 if key == "fetch.negotiationalgorithm" && value.is_none() {
693 let file_disp = config_error_path_display(path);
694 return Err(Error::Message(format!(
695 "error: missing value for 'fetch.negotiationalgorithm'\n\
696fatal: bad config variable 'fetch.negotiationalgorithm' in file '{file_disp}' at line {}",
697 start_idx + 1
698 )));
699 }
700 entries.push(ConfigEntry {
701 key,
702 value,
703 scope,
704 file: Some(path.to_path_buf()),
705 line: start_idx + 1,
706 });
707 } else if logical_line.trim().is_empty() {
708 continue;
709 } else {
710 let file_disp = config_error_path_display(path);
711 let location = if path == Path::new("-") {
712 file_disp
713 } else {
714 format!("file {file_disp}")
715 };
716 return Err(Error::Message(format!(
717 "fatal: bad config line {} in {location}",
718 start_idx + 1
719 )));
720 }
721 }
722
723 Ok(Self {
724 path: path.to_path_buf(),
725 scope,
726 entries,
727 raw_lines,
728 include_origin: ConfigIncludeOrigin::Disk,
729 })
730 }
731
732 pub fn parse_gitmodules_best_effort(
738 path: &Path,
739 content: &str,
740 scope: ConfigScope,
741 ) -> (Vec<ConfigEntry>, Option<usize>) {
742 let raw_lines: Vec<String> = content
743 .lines()
744 .map(|l| l.strip_suffix('\r').unwrap_or(l))
745 .map(String::from)
746 .collect();
747 let mut entries = Vec::new();
748 let mut parser = Parser::new();
749
750 let mut idx = 0;
751 while idx < raw_lines.len() {
752 let start_idx = idx;
753 let line = &raw_lines[idx];
754 idx += 1;
755
756 let trimmed = line.trim();
757 if trimmed.starts_with('#') || trimmed.starts_with(';') {
758 continue;
759 }
760
761 let mut inline_remainder = None;
762 if parser.try_parse_section_with_remainder(line, &mut inline_remainder) {
763 if let Some(remainder) = inline_remainder {
764 if let Some((key, value)) = parser.try_parse_entry(remainder) {
765 entries.push(ConfigEntry {
766 key,
767 value,
768 scope,
769 file: Some(path.to_path_buf()),
770 line: start_idx + 1,
771 });
772 }
773 }
774 continue;
775 }
776
777 let mut logical_line = line.clone();
778 while value_line_continues(&logical_line) && idx < raw_lines.len() {
779 let t = logical_line.trim_end();
780 logical_line = t[..t.len() - 1].to_string();
781 let next = raw_lines[idx].trim_start();
782 logical_line.push_str(next);
783 idx += 1;
784 }
785
786 while entry_line_value_has_unclosed_quote(&logical_line) && idx < raw_lines.len() {
787 let next = raw_lines[idx].trim_start();
788 logical_line.push_str(next);
789 idx += 1;
790 }
791 if entry_line_value_has_unclosed_quote(&logical_line) {
792 return (entries, Some(start_idx + 1));
793 }
794
795 if let Some((key, value)) = parser.try_parse_entry(&logical_line) {
796 entries.push(ConfigEntry {
797 key,
798 value,
799 scope,
800 file: Some(path.to_path_buf()),
801 line: start_idx + 1,
802 });
803 }
804 }
805
806 (entries, None)
807 }
808
809 #[must_use]
811 pub fn get(&self, key: &str) -> Option<String> {
812 let canon = canonical_key(key).ok()?;
813 self.entries
814 .iter()
815 .rev()
816 .find(|e| e.key == canon)
817 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
818 }
819
820 pub fn parse_with_origin(
822 path: &Path,
823 content: &str,
824 scope: ConfigScope,
825 include_origin: ConfigIncludeOrigin,
826 ) -> Result<Self> {
827 let mut f = Self::parse(path, content, scope)?;
828 f.include_origin = include_origin;
829 Ok(f)
830 }
831
832 pub fn from_git_config_parameters(path: &Path, raw: &str) -> Result<Self> {
837 let mut entries = Vec::new();
838 let pseudo_path = path.to_path_buf();
839 for entry in parse_config_parameters_strict(raw)? {
840 match entry {
841 ConfigParameter::Pair { key, value } => {
842 let canon = canonical_key(key.trim())?;
843 entries.push(ConfigEntry {
844 key: canon,
845 value,
846 scope: ConfigScope::Command,
847 file: Some(pseudo_path.clone()),
848 line: 0,
849 });
850 }
851 ConfigParameter::OldStyle(entry) => {
852 if let Some((key, val)) = entry.split_once('=') {
853 let canon = canonical_key(key.trim())?;
854 entries.push(ConfigEntry {
855 key: canon,
856 value: Some(val.to_owned()),
857 scope: ConfigScope::Command,
858 file: Some(pseudo_path.clone()),
859 line: 0,
860 });
861 } else {
862 let canon = canonical_key(entry.trim())?;
863 entries.push(ConfigEntry {
864 key: canon,
865 value: None,
866 scope: ConfigScope::Command,
867 file: Some(pseudo_path.clone()),
868 line: 0,
869 });
870 }
871 }
872 }
873 }
874 Ok(Self {
875 path: path.to_path_buf(),
876 scope: ConfigScope::Command,
877 entries,
878 raw_lines: Vec::new(),
879 include_origin: ConfigIncludeOrigin::CommandLine,
880 })
881 }
882
883 pub fn from_path(path: &Path, scope: ConfigScope) -> Result<Option<Self>> {
892 match fs::read_to_string(path) {
893 Ok(content) => Ok(Some(Self::parse(path, &content, scope)?)),
894 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
895 Err(e) => Err(Error::Io(e)),
896 }
897 }
898
899 pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
910 self.set_with_comment(key, value, None)
911 }
912
913 pub fn set_with_comment(
915 &mut self,
916 key: &str,
917 value: &str,
918 comment: Option<&str>,
919 ) -> Result<()> {
920 let canon = canonical_key(key)?;
921 let raw_var = raw_variable_name(key);
922 let comment_suffix = format_comment_suffix(comment);
923
924 let existing_idx = self.entries.iter().rposition(|e| e.key == canon);
926
927 if let Some(idx) = existing_idx {
928 let line_idx = self.entries[idx].line - 1;
929 let raw_line = &self.raw_lines[line_idx];
930 if is_section_header_with_inline_entry(raw_line) {
931 let header_only = extract_section_header(raw_line);
933 self.raw_lines[line_idx] = header_only;
934 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
935 self.raw_lines.insert(line_idx + 1, new_line);
936 let content = self.raw_lines.join("\n");
938 let reparsed = Self::parse(&self.path, &content, self.scope)?;
939 self.entries = reparsed.entries;
940 self.raw_lines = reparsed.raw_lines;
941 } else {
942 self.raw_lines[line_idx] =
943 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
944 self.entries[idx].value = Some(value.to_owned());
945 }
946 } else {
947 let (section, subsection, _var) = split_key(&canon)?;
949 let (raw_sec, raw_sub) = raw_section_parts(key);
950 let section_line = self.find_or_create_section_preserving_case(
951 §ion,
952 subsection.as_deref(),
953 &raw_sec,
954 raw_sub.as_deref(),
955 );
956 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
957
958 let insert_at = self.last_line_in_section(section_line) + 1;
960 self.raw_lines.insert(insert_at, new_line);
961
962 let content = self.raw_lines.join("\n");
964 let reparsed = Self::parse(&self.path, &content, self.scope)?;
965 self.entries = reparsed.entries;
966 self.raw_lines = reparsed.raw_lines;
967 }
968
969 Ok(())
970 }
971
972 pub fn replace_all(
977 &mut self,
978 key: &str,
979 value: &str,
980 value_pattern: Option<&str>,
981 ) -> Result<()> {
982 self.replace_all_with_comment(key, value, value_pattern, None)
983 }
984
985 pub fn replace_all_with_comment(
990 &mut self,
991 key: &str,
992 value: &str,
993 value_pattern: Option<&str>,
994 comment: Option<&str>,
995 ) -> Result<()> {
996 let canon = canonical_key(key)?;
997 let comment_suffix = format_comment_suffix(comment);
998
999 let (re, negated) = match value_pattern {
1001 Some(pat) => {
1002 let (neg, actual_pat) = if let Some(rest) = pat.strip_prefix('!') {
1003 (true, rest)
1004 } else {
1005 (false, pat)
1006 };
1007 let compiled = regex::Regex::new(actual_pat)
1008 .map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?;
1009 (Some(compiled), neg)
1010 }
1011 None => (None, false),
1012 };
1013
1014 let matching_indices: Vec<usize> = self
1016 .entries
1017 .iter()
1018 .enumerate()
1019 .filter(|(_, e)| {
1020 if e.key != canon {
1021 return false;
1022 }
1023 if let Some(ref re) = re {
1024 let v = e.value.as_deref().unwrap_or("");
1025 let matched = re.is_match(v);
1026 if negated {
1027 !matched
1028 } else {
1029 matched
1030 }
1031 } else {
1032 true
1033 }
1034 })
1035 .map(|(i, _)| i)
1036 .collect();
1037
1038 if matching_indices.is_empty() {
1039 return self.add_value_with_comment(key, value, comment);
1041 }
1042
1043 let raw_var = raw_variable_name(key);
1044
1045 let target_idx = if value_pattern.is_some() {
1046 matching_indices[0]
1047 } else {
1048 *matching_indices
1049 .last()
1050 .ok_or_else(|| Error::ConfigError("missing config match".to_owned()))?
1051 };
1052 let target_line_idx = self.entries[target_idx].line - 1;
1053 let raw_line = &self.raw_lines[target_line_idx];
1054 if is_section_header_with_inline_entry(raw_line) {
1055 let header = extract_section_header(raw_line);
1056 self.raw_lines[target_line_idx] = header;
1057 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1058 self.raw_lines.insert(target_line_idx + 1, new_line);
1059 } else {
1060 self.raw_lines[target_line_idx] =
1061 format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1062 }
1063
1064 for &idx in matching_indices.iter().rev() {
1065 if idx == target_idx {
1066 continue;
1067 }
1068 let line_idx = self.entries[idx].line - 1;
1069 self.remove_entry_line(line_idx);
1070 }
1071
1072 let content = self.raw_lines.join("\n");
1074 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1075 self.entries = reparsed.entries;
1076 self.raw_lines = reparsed.raw_lines;
1077
1078 Ok(())
1079 }
1080
1081 pub fn count(&self, key: &str) -> Result<usize> {
1083 let canon = canonical_key(key)?;
1084 Ok(self.entries.iter().filter(|e| e.key == canon).count())
1085 }
1086
1087 fn remove_entry_line(&mut self, line_idx: usize) {
1098 if is_section_header_with_inline_entry(&self.raw_lines[line_idx]) {
1099 let header = extract_section_header(&self.raw_lines[line_idx]);
1101 self.raw_lines[line_idx] = header;
1102 } else {
1103 let mut lines_to_remove = 1;
1105 let mut check_line = self.raw_lines[line_idx].clone();
1106 while value_line_continues(&check_line)
1107 && (line_idx + lines_to_remove) < self.raw_lines.len()
1108 {
1109 check_line = self.raw_lines[line_idx + lines_to_remove].clone();
1110 lines_to_remove += 1;
1111 }
1112 for _ in 0..lines_to_remove {
1113 self.raw_lines.remove(line_idx);
1114 }
1115 }
1116 }
1117
1118 pub fn unset_last(&mut self, key: &str) -> Result<usize> {
1122 let canon = canonical_key(key)?;
1123 let last_idx = self.entries.iter().rposition(|e| e.key == canon);
1124
1125 if let Some(idx) = last_idx {
1126 let line_idx = self.entries[idx].line - 1;
1127 self.remove_entry_line(line_idx);
1128 let content = self.raw_lines.join("\n");
1129 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1130 self.entries = reparsed.entries;
1131 self.raw_lines = reparsed.raw_lines;
1132 Ok(1)
1133 } else {
1134 Ok(0)
1135 }
1136 }
1137
1138 pub fn unset(&mut self, key: &str) -> Result<usize> {
1148 let canon = canonical_key(key)?;
1149 let line_indices: Vec<usize> = self
1150 .entries
1151 .iter()
1152 .filter(|e| e.key == canon)
1153 .map(|e| e.line - 1)
1154 .collect();
1155
1156 let count = line_indices.len();
1157 for &idx in line_indices.iter().rev() {
1159 self.remove_entry_line(idx);
1160 }
1161
1162 if count > 0 {
1163 let content = self.raw_lines.join("\n");
1164 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1165 self.entries = reparsed.entries;
1166 self.raw_lines = reparsed.raw_lines;
1167 }
1168
1169 Ok(count)
1170 }
1171
1172 pub fn unset_matching(
1181 &mut self,
1182 key: &str,
1183 value_pattern: Option<&str>,
1184 preserve_empty_section_header: bool,
1185 ) -> Result<usize> {
1186 let canon = canonical_key(key)?;
1187 let re = match value_pattern {
1188 Some(pat) => Some(
1189 regex::Regex::new(pat)
1190 .map_err(|e| Error::ConfigError(format!("invalid value-pattern regex: {e}")))?,
1191 ),
1192 None => None,
1193 };
1194
1195 let line_indices: Vec<usize> = self
1196 .entries
1197 .iter()
1198 .filter(|e| {
1199 if e.key != canon {
1200 return false;
1201 }
1202 if let Some(ref re) = re {
1203 let v = e.value.as_deref().unwrap_or("");
1204 re.is_match(v)
1205 } else {
1206 true
1207 }
1208 })
1209 .map(|e| e.line - 1)
1210 .collect();
1211
1212 let count = line_indices.len();
1213 for &idx in line_indices.iter().rev() {
1214 self.remove_entry_line(idx);
1215 }
1216
1217 if count > 0 {
1218 if !preserve_empty_section_header {
1219 let (section, subsection, _) = split_key(&canon)?;
1220 self.remove_empty_section_headers_matching(§ion, subsection.as_deref());
1221 }
1222
1223 let content = self.raw_lines.join("\n");
1224 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1225 self.entries = reparsed.entries;
1226 self.raw_lines = reparsed.raw_lines;
1227 }
1228
1229 Ok(count)
1230 }
1231
1232 pub fn remove_section(&mut self, section: &str) -> Result<bool> {
1238 let (sec_name, sub_name) = parse_section_name(section);
1239 let sec_lower = sec_name.to_lowercase();
1240
1241 let mut remove = vec![false; self.raw_lines.len()];
1242 let mut removing = false;
1243 let mut found = false;
1244 let mut parser = Parser::new();
1245
1246 for (idx, line) in self.raw_lines.iter().enumerate() {
1247 if parser.try_parse_section(line) {
1248 removing = section_matches(&parser, &sec_lower, sub_name);
1249 found |= removing;
1250 }
1251 if removing {
1252 remove[idx] = true;
1253 }
1254 }
1255
1256 if found {
1257 self.raw_lines = self
1258 .raw_lines
1259 .iter()
1260 .enumerate()
1261 .filter_map(|(idx, line)| (!remove[idx]).then_some(line.clone()))
1262 .collect();
1263 let content = self.raw_lines.join("\n");
1264 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1265 self.entries = reparsed.entries;
1266 self.raw_lines = reparsed.raw_lines;
1267 Ok(true)
1268 } else {
1269 Ok(false)
1270 }
1271 }
1272
1273 pub fn rename_section(&mut self, old_name: &str, new_name: &str) -> Result<bool> {
1280 let (old_sec, old_sub) = parse_section_name(old_name);
1281 let (new_sec, new_sub) = parse_section_name(new_name);
1282 validate_section_name(new_sec, new_sub)?;
1283 let old_lower = old_sec.to_lowercase();
1284
1285 let mut found = false;
1286 let mut parser = Parser::new();
1287
1288 let mut idx = 0usize;
1289 while idx < self.raw_lines.len() {
1290 let line = self.raw_lines[idx].clone();
1291 let mut inline_remainder = None;
1292 if parser.try_parse_section_with_remainder(&line, &mut inline_remainder)
1293 && section_matches(&parser, &old_lower, old_sub)
1294 {
1295 let header = match new_sub {
1297 Some(sub) => format!("[{} \"{}\"]", new_sec, sub),
1298 None => format!("[{}]", new_sec),
1299 };
1300 self.raw_lines[idx] = header;
1301 if let Some(remainder) = inline_remainder {
1302 self.raw_lines
1303 .insert(idx + 1, format!("\t{}", remainder.trim()));
1304 idx += 1;
1305 }
1306 found = true;
1307 }
1308 idx += 1;
1309 }
1310
1311 if found {
1312 let content = self.raw_lines.join("\n");
1313 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1314 self.entries = reparsed.entries;
1315 self.raw_lines = reparsed.raw_lines;
1316 }
1317
1318 Ok(found)
1319 }
1320
1321 pub fn add_value(&mut self, key: &str, value: &str) -> Result<()> {
1326 self.add_value_with_comment(key, value, None)
1327 }
1328
1329 pub fn add_value_with_comment(
1331 &mut self,
1332 key: &str,
1333 value: &str,
1334 comment: Option<&str>,
1335 ) -> Result<()> {
1336 let canon = canonical_key(key)?;
1337 let raw_var = raw_variable_name(key);
1338 let comment_suffix = format_comment_suffix(comment);
1339 let (section, subsection, _var) = split_key(&canon)?;
1340 let (raw_sec, raw_sub) = raw_section_parts(key);
1341
1342 let section_line = self.find_or_create_section_preserving_case(
1343 §ion,
1344 subsection.as_deref(),
1345 &raw_sec,
1346 raw_sub.as_deref(),
1347 );
1348 let new_line = format!("\t{} = {}{}", raw_var, escape_value(value), comment_suffix);
1349 let insert_at = self.last_line_in_section(section_line) + 1;
1350 self.raw_lines.insert(insert_at, new_line);
1351
1352 let content = self.raw_lines.join("\n");
1354 let reparsed = Self::parse(&self.path, &content, self.scope)?;
1355 self.entries = reparsed.entries;
1356 self.raw_lines = reparsed.raw_lines;
1357
1358 Ok(())
1359 }
1360
1361 fn remove_empty_section_headers_matching(&mut self, section: &str, subsection: Option<&str>) {
1364 let (Ok(section_re), Ok(comment_re)) = (
1365 regex::Regex::new(r"^\s*\["),
1366 regex::Regex::new(r"^\s*(#|;)"),
1367 ) else {
1368 return;
1370 };
1371
1372 let mut to_remove: Vec<usize> = Vec::new();
1373 let len = self.raw_lines.len();
1374 let section_lower = section.to_lowercase();
1375 let mut parser = Parser::new();
1376
1377 for i in 0..len {
1378 let line = &self.raw_lines[i];
1379 if !section_re.is_match(line) {
1380 continue;
1381 }
1382 if !parser.try_parse_section(line)
1383 || !section_matches(&parser, §ion_lower, subsection)
1384 {
1385 continue;
1386 }
1387 if is_section_header_with_inline_entry(line) {
1389 continue;
1390 }
1391 let has_attached_leading_comment = self.raw_lines[..i]
1392 .iter()
1393 .enumerate()
1394 .rev()
1395 .find(|(_, line)| !line.trim().is_empty())
1396 .is_some_and(|(idx, line)| {
1397 comment_re.is_match(line)
1398 && idx
1399 .checked_sub(1)
1400 .is_none_or(|prev| !value_line_continues(&self.raw_lines[prev]))
1401 });
1402 if has_attached_leading_comment {
1403 continue;
1404 }
1405 let mut has_entries = false;
1408 for j in (i + 1)..len {
1409 let next = self.raw_lines[j].trim();
1410 if next.is_empty() {
1411 continue;
1412 }
1413 if section_re.is_match(&self.raw_lines[j]) {
1414 break;
1415 }
1416 if comment_re.is_match(&self.raw_lines[j]) {
1417 has_entries = true;
1419 break;
1420 }
1421 has_entries = true;
1423 break;
1424 }
1425 if !has_entries {
1426 to_remove.push(i);
1427 }
1428 }
1429
1430 for &idx in to_remove.iter().rev() {
1432 self.raw_lines.remove(idx);
1433 }
1434
1435 while self.raw_lines.last().is_some_and(|l| l.trim().is_empty()) {
1437 self.raw_lines.pop();
1438 }
1439 }
1440
1441 pub fn write(&self) -> Result<()> {
1446 let content = self.raw_lines.join("\n");
1447 let trimmed = content.trim();
1448 if trimmed.is_empty() {
1449 fs::write(&self.path, "")?;
1451 } else {
1452 let content = if content.ends_with('\n') {
1454 content
1455 } else {
1456 format!("{content}\n")
1457 };
1458 fs::write(&self.path, content)?;
1459 }
1460 Ok(())
1461 }
1462
1463 #[allow(dead_code)]
1465 fn find_or_create_section(&mut self, section: &str, subsection: Option<&str>) -> usize {
1466 let sec_lower = section.to_lowercase();
1467 let mut parser = Parser::new();
1468
1469 for (idx, line) in self.raw_lines.iter().enumerate() {
1470 if parser.try_parse_section(line) && section_matches(&parser, &sec_lower, subsection) {
1471 return idx;
1472 }
1473 }
1474
1475 let header = match subsection {
1477 Some(sub) => {
1478 let escaped = escape_subsection(sub);
1479 format!("[{} \"{}\"]", section, escaped)
1480 }
1481 None => format!("[{}]", section),
1482 };
1483 self.raw_lines.push(header);
1484 self.raw_lines.len() - 1
1485 }
1486
1487 fn find_or_create_section_preserving_case(
1490 &mut self,
1491 section: &str,
1492 subsection: Option<&str>,
1493 raw_section: &str,
1494 raw_subsection: Option<&str>,
1495 ) -> usize {
1496 let sec_lower = section.to_lowercase();
1497 let mut parser = Parser::new();
1498
1499 for (idx, line) in self.raw_lines.iter().enumerate() {
1500 if parser.try_parse_section(line) && section_matches(&parser, &sec_lower, subsection) {
1501 return idx;
1502 }
1503 }
1504
1505 let header = match raw_subsection {
1507 Some(sub) => {
1508 let escaped = escape_subsection(sub);
1509 format!("[{} \"{}\"]", raw_section, escaped)
1510 }
1511 None => format!("[{}]", raw_section),
1512 };
1513 self.raw_lines.push(header);
1514 self.raw_lines.len() - 1
1515 }
1516
1517 fn last_line_in_section(&self, section_line: usize) -> usize {
1519 let mut last = section_line;
1520 for idx in (section_line + 1)..self.raw_lines.len() {
1521 let trimmed = self.raw_lines[idx].trim();
1522 if trimmed.starts_with('[') {
1523 break;
1524 }
1525 last = idx;
1526 }
1527 last
1528 }
1529}
1530
1531impl ConfigSet {
1534 #[must_use]
1536 pub fn new() -> Self {
1537 Self {
1538 entries: Vec::new(),
1539 }
1540 }
1541
1542 #[must_use]
1544 pub fn entries(&self) -> &[ConfigEntry] {
1545 &self.entries
1546 }
1547
1548 pub fn merge(&mut self, file: &ConfigFile) {
1553 self.entries.extend(file.entries.iter().cloned());
1554 }
1555
1556 pub fn merge_set(&mut self, other: &ConfigSet) {
1558 self.entries.extend(other.entries.iter().cloned());
1559 }
1560
1561 pub fn add_command_override(&mut self, key: &str, value: &str) -> Result<()> {
1563 let canon = canonical_key(key)?;
1564 self.entries.push(ConfigEntry {
1565 key: canon,
1566 value: Some(value.to_owned()),
1567 scope: ConfigScope::Command,
1568 file: None,
1569 line: 0,
1570 });
1571 Ok(())
1572 }
1573
1574 #[must_use]
1585 pub fn get(&self, key: &str) -> Option<String> {
1586 let canon = canonical_key(key).ok()?;
1587 self.entries
1588 .iter()
1589 .rev()
1590 .find(|e| e.key == canon)
1591 .map(|e| e.value.clone().unwrap_or_else(|| "true".to_owned()))
1592 }
1593
1594 #[must_use]
1599 pub fn get_last_entry(&self, key: &str) -> Option<ConfigEntry> {
1600 let canon = canonical_key(key).ok()?;
1601 self.entries.iter().rev().find(|e| e.key == canon).cloned()
1602 }
1603
1604 #[must_use]
1606 pub fn get_all(&self, key: &str) -> Vec<String> {
1607 let canon = match canonical_key(key) {
1608 Ok(c) => c,
1609 Err(_) => return Vec::new(),
1610 };
1611 self.entries
1612 .iter()
1613 .filter(|e| e.key == canon)
1614 .map(|e| e.value.clone().unwrap_or_default())
1615 .collect()
1616 }
1617
1618 #[must_use]
1622 pub fn get_all_raw(&self, key: &str) -> Vec<Option<String>> {
1623 let canon = match canonical_key(key) {
1624 Ok(c) => c,
1625 Err(_) => return Vec::new(),
1626 };
1627 self.entries
1628 .iter()
1629 .filter(|e| e.key == canon)
1630 .map(|e| e.value.clone())
1631 .collect()
1632 }
1633
1634 #[must_use]
1639 pub fn has_key(&self, key: &str) -> bool {
1640 let Ok(canon) = canonical_key(key) else {
1641 return false;
1642 };
1643 self.entries.iter().any(|e| e.key == canon)
1644 }
1645
1646 pub fn get_bool(&self, key: &str) -> Option<std::result::Result<bool, String>> {
1652 let v = self.get(key)?;
1653 if canonical_key(key).ok().as_deref() == Some("pack.allowpackreuse") {
1654 let lower = v.trim().to_ascii_lowercase();
1655 if lower == "single" || lower == "multi" {
1656 return None;
1657 }
1658 }
1659 Some(parse_bool(&v))
1660 }
1661
1662 #[must_use]
1668 pub fn quote_path_fully(&self) -> bool {
1669 let from_key = |key: &str| self.get_bool(key).and_then(|r| r.ok());
1670 from_key("core.quotepath")
1671 .or_else(|| from_key("core.quotePath"))
1672 .unwrap_or(true)
1673 }
1674
1675 #[must_use]
1679 pub fn pack_write_reverse_index_default(&self) -> bool {
1680 if std::env::var("GIT_TEST_NO_WRITE_REV_INDEX")
1681 .ok()
1682 .as_deref()
1683 .is_some_and(|v| {
1684 let s = v.trim().to_ascii_lowercase();
1685 matches!(s.as_str(), "1" | "true" | "yes" | "on")
1686 })
1687 {
1688 return false;
1689 }
1690 if self
1691 .get("pack.writereverseindex")
1692 .or_else(|| self.get("pack.writeReverseIndex"))
1693 .is_some_and(|v| v.trim().is_empty())
1694 {
1695 return false;
1696 }
1697 self.get_bool("pack.writereverseindex")
1698 .or_else(|| self.get_bool("pack.writeReverseIndex"))
1699 .and_then(|r| r.ok())
1700 .unwrap_or(true)
1701 }
1702
1703 #[must_use]
1705 pub fn pack_read_reverse_index_default(&self) -> bool {
1706 self.get_bool("pack.readreverseindex")
1707 .or_else(|| self.get_bool("pack.readReverseIndex"))
1708 .and_then(|r| r.ok())
1709 .unwrap_or(true)
1710 }
1711
1712 #[must_use]
1715 pub fn effective_log_refs_config(&self, git_dir: &Path) -> refs::LogRefsConfig {
1716 if let Some(v) = self.get("core.logAllRefUpdates") {
1717 let lower = v.trim().to_ascii_lowercase();
1718 let parsed = match lower.as_str() {
1719 "always" => Some(refs::LogRefsConfig::Always),
1720 "1" | "true" | "yes" | "on" => Some(refs::LogRefsConfig::Normal),
1721 "0" | "false" | "no" | "off" | "never" => Some(refs::LogRefsConfig::None),
1722 _ => None,
1723 };
1724 if let Some(c) = parsed {
1725 return c;
1726 }
1727 }
1728 refs::effective_log_refs_config(git_dir)
1729 }
1730
1731 pub fn get_i64(&self, key: &str) -> Option<std::result::Result<i64, String>> {
1733 self.get(key).map(|v| parse_i64(&v))
1734 }
1735
1736 pub fn pack_objects_zlib_level(&self) -> Result<i32> {
1744 const Z_DEFAULT_COMPRESSION: i32 = 6;
1745 const Z_BEST_COMPRESSION: i32 = 9;
1746
1747 let parse_compression = |raw: &str| -> Result<i32> {
1748 let v = parse_git_config_int_strict(raw.trim()).map_err(|_| {
1749 Error::ConfigError(format!("bad numeric config value '{raw}' for compression"))
1750 })?;
1751 if v == -1 {
1752 return Ok(Z_DEFAULT_COMPRESSION);
1753 }
1754 if v < 0 || v > i64::from(Z_BEST_COMPRESSION) {
1755 return Err(Error::ConfigError(format!(
1756 "bad zlib compression level {v}"
1757 )));
1758 }
1759 Ok(v as i32)
1760 };
1761
1762 let mut pack_level = Z_DEFAULT_COMPRESSION;
1764 let mut pack_compression_seen = false;
1765
1766 for e in self.entries() {
1767 match e.key.as_str() {
1768 "core.compression" => {
1769 let Some(val) = e.value.as_deref() else {
1770 continue;
1771 };
1772 let level = parse_compression(val)?;
1773 if !pack_compression_seen {
1774 pack_level = level;
1775 }
1776 }
1777 "pack.compression" => {
1778 let Some(val) = e.value.as_deref() else {
1779 continue;
1780 };
1781 pack_level = parse_compression(val)?;
1782 pack_compression_seen = true;
1783 }
1784 _ => {}
1785 }
1786 }
1787
1788 Ok(pack_level)
1789 }
1790
1791 pub fn get_regexp(&self, pattern: &str) -> std::result::Result<Vec<&ConfigEntry>, String> {
1796 let re = regex::Regex::new(pattern).map_err(|e| format!("invalid key pattern: {e}"))?;
1797 Ok(self
1798 .entries
1799 .iter()
1800 .filter(|e| re.is_match(&e.key))
1801 .collect())
1802 }
1803
1804 pub fn load(git_dir: Option<&Path>, include_system: bool) -> Result<Self> {
1815 let mut opts = LoadConfigOptions::default();
1816 opts.include_system = include_system;
1817 opts.include_ctx.git_dir = git_dir.map(PathBuf::from);
1818 Self::load_with_options(git_dir, &opts)
1819 }
1820
1821 pub fn load_with_options(git_dir: Option<&Path>, opts: &LoadConfigOptions) -> Result<Self> {
1825 let mut set = Self::new();
1826 let proc = opts.process_includes;
1827 let ctx = opts.include_ctx.clone();
1828
1829 if opts.include_system && !git_config_nosystem_enabled() {
1831 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
1832 .map(std::path::PathBuf::from)
1833 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
1834 match ConfigFile::from_path(&system_path, ConfigScope::System) {
1835 Ok(Some(f)) => Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?,
1836 Ok(None) => {}
1837 Err(e) => return Err(e),
1838 }
1839 }
1840
1841 for path in global_config_paths() {
1843 match ConfigFile::from_path(&path, ConfigScope::Global) {
1844 Ok(Some(f)) => Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?,
1845 Ok(None) => {}
1846 Err(e) => return Err(e),
1847 }
1848 }
1849
1850 if let Some(gd) = git_dir {
1852 let common_dir = crate::repo::common_git_dir_for_config(gd);
1853 let local_path = common_dir.join("config");
1854 match ConfigFile::from_path(&local_path, ConfigScope::Local) {
1855 Ok(Some(f)) => Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?,
1856 Ok(None) => {}
1857 Err(e) => return Err(e),
1858 }
1859
1860 let wt_path = gd.join("config.worktree");
1863 if crate::repo::worktree_config_enabled(&common_dir) {
1864 match ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
1865 Ok(Some(f)) => Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?,
1866 Ok(None) => {}
1867 Err(e) => return Err(e),
1868 }
1869 }
1870 }
1871
1872 if let Ok(path) = std::env::var("GIT_CONFIG") {
1874 match ConfigFile::from_path(Path::new(&path), ConfigScope::Command) {
1875 Ok(Some(f)) => {
1876 if proc {
1877 Self::merge_with_includes(&mut set, &f, proc, 0, &ctx)?;
1878 } else {
1879 set.merge(&f);
1880 }
1881 }
1882 Ok(None) => {}
1883 Err(e) => return Err(e),
1884 }
1885 }
1886
1887 add_environment_config_pairs(&mut set)?;
1888
1889 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
1891 if proc && opts.command_includes && !params.trim().is_empty() {
1892 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
1893 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, ¶ms)?;
1894 Self::merge_with_includes(&mut set, &cmd_file, proc, 0, &ctx)?;
1895 } else if !params.trim().is_empty() {
1896 for entry in parse_config_parameters(¶ms) {
1897 if let Some((key, val)) =
1898 entry.split_once('\u{1}').or_else(|| entry.split_once('='))
1899 {
1900 let _ = set.add_command_override(key.trim(), val);
1901 } else {
1902 let _ = set.add_command_override(entry.trim(), "true");
1903 }
1904 }
1905 }
1906 }
1907
1908 Ok(set)
1909 }
1910
1911 pub fn read_early_config(git_dir: Option<&Path>, key: &str) -> Result<Vec<String>> {
1923 let mut set = Self::new();
1924 let ctx = IncludeContext {
1925 git_dir: git_dir.map(PathBuf::from),
1926 command_line_relative_include_is_error: false,
1927 };
1928
1929 if !git_config_nosystem_enabled() {
1931 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
1932 .map(std::path::PathBuf::from)
1933 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
1934 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
1935 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1936 }
1937 }
1938
1939 for path in global_config_paths() {
1941 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
1942 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1943 }
1944 }
1945
1946 if let Some(gd) = git_dir {
1947 let common_dir = crate::repo::common_git_dir_for_config(gd);
1948 let local_path = common_dir.join("config");
1950 if let Some(msg) = crate::repo::early_config_ignore_repo_reason(&common_dir) {
1951 eprintln!("warning: ignoring git dir '{}': {}", gd.display(), msg);
1952 } else if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
1953 set.merge_file_with_includes(&f, true, &ctx)?;
1954 }
1955
1956 let wt_path = gd.join("config.worktree");
1958 if crate::repo::worktree_config_enabled(&common_dir) {
1959 if let Ok(Some(f)) = ConfigFile::from_path(&wt_path, ConfigScope::Worktree) {
1960 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
1961 }
1962 }
1963 }
1964
1965 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
1967 if !params.trim().is_empty() {
1968 let pseudo = Path::new(":GIT_CONFIG_PARAMETERS");
1969 let cmd_file = ConfigFile::from_git_config_parameters(pseudo, ¶ms)?;
1970 Self::merge_with_includes(&mut set, &cmd_file, true, 0, &ctx)?;
1971 }
1972 }
1973
1974 Ok(set.get_all(key))
1975 }
1976
1977 pub fn merge_file_with_includes(
1982 &mut self,
1983 file: &ConfigFile,
1984 process_includes: bool,
1985 ctx: &IncludeContext,
1986 ) -> Result<()> {
1987 Self::merge_with_includes(self, file, process_includes, 0, ctx)
1988 }
1989
1990 pub fn load_repo_local_only(git_dir: &Path) -> Result<Self> {
1996 let mut set = Self::new();
1997 let local_path = git_dir.join("config");
1998 let ctx = IncludeContext {
1999 git_dir: Some(git_dir.to_path_buf()),
2000 command_line_relative_include_is_error: false,
2001 };
2002 if let Ok(Some(f)) = ConfigFile::from_path(&local_path, ConfigScope::Local) {
2003 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2004 }
2005 Ok(set)
2006 }
2007
2008 pub fn load_protected(include_system: bool) -> Result<Self> {
2019 let mut set = Self::new();
2020 let ctx = IncludeContext {
2021 git_dir: None,
2022 command_line_relative_include_is_error: false,
2023 };
2024
2025 if include_system && !git_config_nosystem_enabled() {
2026 let system_path = std::env::var("GIT_CONFIG_SYSTEM")
2027 .map(std::path::PathBuf::from)
2028 .unwrap_or_else(|_| std::path::PathBuf::from("/etc/gitconfig"));
2029 if let Ok(Some(f)) = ConfigFile::from_path(&system_path, ConfigScope::System) {
2030 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2031 }
2032 }
2033
2034 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
2035 let path = PathBuf::from(p);
2036 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2037 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2038 }
2039 } else {
2040 let mut global_paths = Vec::new();
2041 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
2042 global_paths.push(PathBuf::from(xdg).join("git/config"));
2043 } else if let Some(home) = home_dir() {
2044 global_paths.push(home.join(".config/git/config"));
2045 }
2046 if let Some(home) = home_dir() {
2047 global_paths.push(home.join(".gitconfig"));
2048 }
2049 for path in global_paths {
2050 if let Ok(Some(f)) = ConfigFile::from_path(&path, ConfigScope::Global) {
2051 Self::merge_with_includes(&mut set, &f, true, 0, &ctx)?;
2052 }
2053 }
2054 }
2055
2056 add_environment_config_pairs(&mut set)?;
2057
2058 if let Ok(params) = std::env::var("GIT_CONFIG_PARAMETERS") {
2059 for entry in parse_config_parameters(¶ms) {
2060 if let Some((key, val)) =
2061 entry.split_once('\u{1}').or_else(|| entry.split_once('='))
2062 {
2063 let _ = set.add_command_override(key.trim(), val);
2064 } else {
2065 let _ = set.add_command_override(entry.trim(), "true");
2066 }
2067 }
2068 }
2069
2070 Ok(set)
2071 }
2072
2073 fn merge_with_includes(
2075 set: &mut Self,
2076 file: &ConfigFile,
2077 process_includes: bool,
2078 depth: usize,
2079 ctx: &IncludeContext,
2080 ) -> Result<()> {
2081 const MAX_INCLUDE_DEPTH: usize = 10;
2084 if depth > MAX_INCLUDE_DEPTH {
2085 return Err(Error::ConfigError(
2086 "exceeded maximum include depth".to_owned(),
2087 ));
2088 }
2089 if !process_includes {
2090 set.merge(file);
2091 return Ok(());
2092 }
2093
2094 for entry in &file.entries {
2095 set.entries.push(entry.clone());
2096
2097 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
2098 continue;
2099 };
2100 let included_by_hasconfig = condition.as_deref().is_some_and(is_hasconfig_remote_url);
2101 if condition.is_some() && !included_by_hasconfig {
2102 let cond = condition.as_deref().unwrap_or_default();
2103 if !evaluate_include_condition(cond, set, file, ctx) {
2104 continue;
2105 }
2106 }
2107
2108 let resolved = match resolve_include_file_path(&inc_path, file, ctx) {
2109 Ok(p) => p,
2110 Err(Error::ConfigError(msg)) if msg.is_empty() => continue,
2111 Err(e) => return Err(e),
2112 };
2113 let Some(inc_file) = ConfigFile::from_path(&resolved, file.scope)? else {
2117 continue;
2118 };
2119
2120 if included_by_hasconfig {
2121 validate_hasconfig_remote_url_include(&inc_file, process_includes, depth + 1, ctx)?;
2122 let cond = condition.as_deref().unwrap_or_default();
2123 if !evaluate_include_condition(cond, set, file, ctx) {
2124 continue;
2125 }
2126 }
2127
2128 Self::merge_with_includes(set, &inc_file, process_includes, depth + 1, ctx)?;
2129 }
2130
2131 Ok(())
2132 }
2133}
2134
2135fn include_directive_for_entry(entry: &ConfigEntry) -> Option<(String, Option<String>)> {
2136 let val = entry.value.as_ref()?;
2137 if entry.key == "include.path" {
2138 return Some((val.clone(), None));
2139 }
2140 if entry.key.starts_with("includeif.") && entry.key.ends_with(".path") {
2141 let mid = &entry.key["includeif.".len()..entry.key.len() - ".path".len()];
2142 return Some((val.clone(), Some(mid.to_owned())));
2143 }
2144 None
2145}
2146
2147fn git_config_nosystem_enabled() -> bool {
2148 std::env::var("GIT_CONFIG_NOSYSTEM")
2149 .ok()
2150 .map(|value| parse_bool(&value).unwrap_or(true))
2151 .unwrap_or(false)
2152}
2153
2154fn add_environment_config_pairs(set: &mut ConfigSet) -> Result<()> {
2155 let Ok(count_str) = std::env::var("GIT_CONFIG_COUNT") else {
2156 return Ok(());
2157 };
2158 if count_str.is_empty() {
2159 return Ok(());
2160 }
2161
2162 let count = count_str
2163 .parse::<usize>()
2164 .map_err(|_| Error::ConfigError("bogus count in GIT_CONFIG_COUNT".to_owned()))?;
2165 if count > i32::MAX as usize {
2166 return Err(Error::ConfigError(
2167 "too many entries in GIT_CONFIG_COUNT".to_owned(),
2168 ));
2169 }
2170
2171 for i in 0..count {
2172 let key_var = format!("GIT_CONFIG_KEY_{i}");
2173 let value_var = format!("GIT_CONFIG_VALUE_{i}");
2174 let key = std::env::var(&key_var)
2175 .map_err(|_| Error::ConfigError(format!("missing config key {key_var}")))?;
2176 let value = std::env::var(&value_var)
2177 .map_err(|_| Error::ConfigError(format!("missing config value {value_var}")))?;
2178 set.add_command_override(&key, &value)?;
2179 }
2180
2181 Ok(())
2182}
2183
2184pub fn parse_bool(s: &str) -> std::result::Result<bool, String> {
2197 match s.to_lowercase().as_str() {
2198 "true" | "yes" | "on" => Ok(true),
2199 "" => Ok(false),
2200 "false" | "no" | "off" => Ok(false),
2201 _ => {
2202 if let Ok(n) = parse_i64(s) {
2204 return Ok(n != 0);
2205 }
2206 Err(format!("bad boolean config value '{s}'"))
2207 }
2208 }
2209}
2210
2211pub fn parse_i64(s: &str) -> std::result::Result<i64, String> {
2213 let s = s.trim();
2214 if s.is_empty() {
2215 return Err("empty integer value".to_owned());
2216 }
2217
2218 let (num_str, multiplier) = match s.as_bytes().last() {
2219 Some(b'k' | b'K') => (&s[..s.len() - 1], 1024_i64),
2220 Some(b'm' | b'M') => (&s[..s.len() - 1], 1024 * 1024),
2221 Some(b'g' | b'G') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
2222 _ => (s, 1_i64),
2223 };
2224
2225 let base: i64 = num_str
2226 .parse()
2227 .map_err(|_| format!("invalid integer: '{s}'"))?;
2228 base.checked_mul(multiplier)
2229 .ok_or_else(|| format!("integer overflow: '{s}'"))
2230}
2231
2232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2234pub enum GitConfigIntStrictError {
2235 InvalidUnit,
2237 OutOfRange,
2239}
2240
2241pub fn parse_git_config_int_strict(raw: &str) -> std::result::Result<i64, GitConfigIntStrictError> {
2245 let s = raw.trim();
2246 if s.is_empty() {
2247 return Err(GitConfigIntStrictError::InvalidUnit);
2248 }
2249
2250 let bytes = s.as_bytes();
2251 let mut idx = 0usize;
2252 if matches!(bytes.first(), Some(b'+') | Some(b'-')) {
2253 idx = 1;
2254 }
2255 if idx >= bytes.len() {
2256 return Err(GitConfigIntStrictError::InvalidUnit);
2257 }
2258 let digit_start = idx;
2259 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
2260 idx += 1;
2261 }
2262 if idx == digit_start {
2263 return Err(GitConfigIntStrictError::InvalidUnit);
2264 }
2265
2266 let num_part =
2267 std::str::from_utf8(&bytes[..idx]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2268 let suffix =
2269 std::str::from_utf8(&bytes[idx..]).map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2270 let mult: i64 = match suffix {
2271 "" => 1,
2272 "k" | "K" => 1024,
2273 "m" | "M" => 1024 * 1024,
2274 "g" | "G" => 1024_i64
2275 .checked_mul(1024)
2276 .and_then(|x| x.checked_mul(1024))
2277 .ok_or(GitConfigIntStrictError::OutOfRange)?,
2278 _ => return Err(GitConfigIntStrictError::InvalidUnit),
2279 };
2280
2281 let val: i64 = num_part
2282 .parse()
2283 .map_err(|_| GitConfigIntStrictError::InvalidUnit)?;
2284 val.checked_mul(mult)
2285 .ok_or(GitConfigIntStrictError::OutOfRange)
2286}
2287
2288const DIFF_CONTEXT_KEY: &str = "diff.context";
2289
2290fn format_bad_numeric_diff_context(
2291 value: &str,
2292 err: GitConfigIntStrictError,
2293 entry: &ConfigEntry,
2294) -> String {
2295 let detail = match err {
2296 GitConfigIntStrictError::InvalidUnit => "invalid unit",
2297 GitConfigIntStrictError::OutOfRange => "out of range",
2298 };
2299 if entry.scope == ConfigScope::Command || entry.file.is_none() {
2300 return format!(
2301 "fatal: bad numeric config value '{value}' for '{DIFF_CONTEXT_KEY}': {detail}"
2302 );
2303 }
2304 let path = entry
2305 .file
2306 .as_deref()
2307 .map(config_error_path_display)
2308 .unwrap_or_default();
2309 format!("fatal: bad numeric config value '{value}' for '{DIFF_CONTEXT_KEY}' in file {path}: {detail}")
2310}
2311
2312fn format_bad_diff_context_variable(entry: &ConfigEntry) -> String {
2313 if entry.scope == ConfigScope::Command || entry.file.is_none() {
2314 return format!("fatal: unable to parse '{DIFF_CONTEXT_KEY}' from command-line config");
2315 }
2316 let path = entry
2317 .file
2318 .as_deref()
2319 .map(config_error_path_display)
2320 .unwrap_or_default();
2321 format!(
2322 "fatal: bad config variable '{DIFF_CONTEXT_KEY}' in file '{path}' at line {}",
2323 entry.line
2324 )
2325}
2326
2327pub fn resolve_diff_context_lines(cfg: &ConfigSet) -> std::result::Result<Option<usize>, String> {
2332 let Some(entry) = cfg.get_last_entry(DIFF_CONTEXT_KEY) else {
2333 return Ok(None);
2334 };
2335 let value_src = entry.value.as_deref().unwrap_or("").trim();
2336 match parse_git_config_int_strict(value_src) {
2337 Ok(n) if n < 0 => Err(format_bad_diff_context_variable(&entry)),
2338 Ok(n) => Ok(Some(usize::try_from(n).map_err(|_| {
2339 format_bad_numeric_diff_context(value_src, GitConfigIntStrictError::OutOfRange, &entry)
2340 })?)),
2341 Err(e) => Err(format_bad_numeric_diff_context(value_src, e, &entry)),
2342 }
2343}
2344
2345pub fn parse_color(s: &str) -> std::result::Result<String, String> {
2352 const COLOR_BACKGROUND_OFFSET: i32 = 10;
2353 const COLOR_FOREGROUND_ANSI: i32 = 30;
2354 const COLOR_FOREGROUND_RGB: i32 = 38;
2355 const COLOR_FOREGROUND_256: i32 = 38;
2356 const COLOR_FOREGROUND_BRIGHT_ANSI: i32 = 90;
2357
2358 #[derive(Clone, Copy, Default)]
2359 struct Color {
2360 kind: u8,
2361 value: u8,
2362 red: u8,
2363 green: u8,
2364 blue: u8,
2365 }
2366
2367 const COLOR_UNSPECIFIED: u8 = 0;
2368 const COLOR_NORMAL: u8 = 1;
2369 const COLOR_ANSI: u8 = 2;
2370 const COLOR_256: u8 = 3;
2371 const COLOR_RGB: u8 = 4;
2372
2373 fn color_empty(c: &Color) -> bool {
2374 c.kind == COLOR_UNSPECIFIED || c.kind == COLOR_NORMAL
2375 }
2376
2377 fn parse_ansi_color(name: &str) -> Option<Color> {
2378 let color_names = [
2379 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
2380 ];
2381 let color_offset = COLOR_FOREGROUND_ANSI;
2382
2383 if name.eq_ignore_ascii_case("default") {
2384 return Some(Color {
2385 kind: COLOR_ANSI,
2386 value: (9 + color_offset) as u8,
2387 ..Default::default()
2388 });
2389 }
2390
2391 let (name, color_offset) = if name.len() >= 6 && name[..6].eq_ignore_ascii_case("bright") {
2392 (&name[6..], COLOR_FOREGROUND_BRIGHT_ANSI)
2393 } else {
2394 (name, COLOR_FOREGROUND_ANSI)
2395 };
2396
2397 for (i, cn) in color_names.iter().enumerate() {
2398 if name.eq_ignore_ascii_case(cn) {
2399 return Some(Color {
2400 kind: COLOR_ANSI,
2401 value: (i as i32 + color_offset) as u8,
2402 ..Default::default()
2403 });
2404 }
2405 }
2406 None
2407 }
2408
2409 fn hex_val(b: u8) -> Option<u8> {
2410 match b {
2411 b'0'..=b'9' => Some(b - b'0'),
2412 b'a'..=b'f' => Some(b - b'a' + 10),
2413 b'A'..=b'F' => Some(b - b'A' + 10),
2414 _ => None,
2415 }
2416 }
2417
2418 fn get_hex_color(chars: &[u8], width: usize) -> Option<(u8, usize)> {
2419 assert!(width == 1 || width == 2);
2420 if chars.len() < width {
2421 return None;
2422 }
2423 let v = if width == 2 {
2424 let hi = hex_val(chars[0])?;
2425 let lo = hex_val(chars[1])?;
2426 (hi << 4) | lo
2427 } else {
2428 let n = hex_val(chars[0])?;
2429 (n << 4) | n
2430 };
2431 Some((v, width))
2432 }
2433
2434 fn parse_single_color(word: &str) -> Option<Color> {
2435 if word.eq_ignore_ascii_case("normal") {
2436 return Some(Color {
2437 kind: COLOR_NORMAL,
2438 ..Default::default()
2439 });
2440 }
2441
2442 let bytes = word.as_bytes();
2443 if (bytes.len() == 7 || bytes.len() == 4) && bytes.first() == Some(&b'#') {
2444 let width = if bytes.len() == 7 { 2 } else { 1 };
2445 let mut idx = 1;
2446 let (r, n1) = get_hex_color(&bytes[idx..], width)?;
2447 idx += n1;
2448 let (g, n2) = get_hex_color(&bytes[idx..], width)?;
2449 idx += n2;
2450 let (b, n3) = get_hex_color(&bytes[idx..], width)?;
2451 idx += n3;
2452 if idx != bytes.len() {
2453 return None;
2454 }
2455 return Some(Color {
2456 kind: COLOR_RGB,
2457 red: r,
2458 green: g,
2459 blue: b,
2460 ..Default::default()
2461 });
2462 }
2463
2464 if let Some(c) = parse_ansi_color(word) {
2465 return Some(c);
2466 }
2467
2468 let Ok(val) = word.parse::<i64>() else {
2469 return None;
2470 };
2471 if val < -1 {
2472 return None;
2473 }
2474 if val < 0 {
2475 return Some(Color {
2476 kind: COLOR_NORMAL,
2477 ..Default::default()
2478 });
2479 }
2480 if val < 8 {
2481 return Some(Color {
2482 kind: COLOR_ANSI,
2483 value: (val as i32 + COLOR_FOREGROUND_ANSI) as u8,
2484 ..Default::default()
2485 });
2486 }
2487 if val < 16 {
2488 return Some(Color {
2489 kind: COLOR_ANSI,
2490 value: (val as i32 - 8 + COLOR_FOREGROUND_BRIGHT_ANSI) as u8,
2491 ..Default::default()
2492 });
2493 }
2494 if val < 256 {
2495 return Some(Color {
2496 kind: COLOR_256,
2497 value: val as u8,
2498 ..Default::default()
2499 });
2500 }
2501 None
2502 }
2503
2504 fn parse_attr(word: &str) -> Option<u8> {
2505 const ATTRS: [(&str, u8, u8); 8] = [
2506 ("bold", 1, 22),
2507 ("dim", 2, 22),
2508 ("italic", 3, 23),
2509 ("ul", 4, 24),
2510 ("underline", 4, 24),
2511 ("blink", 5, 25),
2512 ("reverse", 7, 27),
2513 ("strike", 9, 29),
2514 ];
2515
2516 let mut negate = false;
2517 let mut rest = word;
2518 if let Some(stripped) = rest.strip_prefix("no") {
2519 negate = true;
2520 rest = stripped;
2521 if let Some(s) = rest.strip_prefix('-') {
2522 rest = s;
2523 }
2524 }
2525
2526 for (name, val, neg) in ATTRS {
2527 if rest == name {
2528 return Some(if negate { neg } else { val });
2529 }
2530 }
2531 None
2532 }
2533
2534 fn append_color_output(out: &mut String, c: &Color, background: bool) {
2535 let offset = if background {
2536 COLOR_BACKGROUND_OFFSET
2537 } else {
2538 0
2539 };
2540 match c.kind {
2541 COLOR_UNSPECIFIED | COLOR_NORMAL => {}
2542 COLOR_ANSI => {
2543 use std::fmt::Write;
2544 let _ = write!(out, "{}", i32::from(c.value) + offset);
2545 }
2546 COLOR_256 => {
2547 use std::fmt::Write;
2548 let _ = write!(out, "{};5;{}", COLOR_FOREGROUND_256 + offset, c.value);
2549 }
2550 COLOR_RGB => {
2551 use std::fmt::Write;
2552 let _ = write!(
2553 out,
2554 "{};2;{};{};{}",
2555 COLOR_FOREGROUND_RGB + offset,
2556 c.red,
2557 c.green,
2558 c.blue
2559 );
2560 }
2561 _ => {}
2562 }
2563 }
2564
2565 let s = s.trim();
2566 if s.is_empty() {
2567 return Ok(String::new());
2568 }
2569
2570 let mut has_reset = false;
2571 let mut attr: u64 = 0;
2572 let mut fg = Color::default();
2573 let mut bg = Color::default();
2574 fg.kind = COLOR_UNSPECIFIED;
2575 bg.kind = COLOR_UNSPECIFIED;
2576
2577 for word in s.split_whitespace() {
2578 if word.eq_ignore_ascii_case("reset") {
2579 has_reset = true;
2580 continue;
2581 }
2582
2583 if let Some(c) = parse_single_color(word) {
2584 if fg.kind == COLOR_UNSPECIFIED {
2585 fg = c;
2586 continue;
2587 }
2588 if bg.kind == COLOR_UNSPECIFIED {
2589 bg = c;
2590 continue;
2591 }
2592 return Err(format!("bad color value '{s}'"));
2593 }
2594
2595 if let Some(code) = parse_attr(word) {
2596 attr |= 1u64 << u64::from(code);
2597 continue;
2598 }
2599
2600 return Err(format!("bad color value '{s}'"));
2601 }
2602
2603 if !has_reset && attr == 0 && color_empty(&fg) && color_empty(&bg) {
2604 return Err(format!("bad color value '{s}'"));
2605 }
2606
2607 let mut out = String::from("\x1b[");
2608 let mut sep = if has_reset { 1u32 } else { 0u32 };
2609
2610 let mut attr_bits = attr;
2611 let mut i = 0u32;
2612 while attr_bits != 0 {
2613 let bit = 1u64 << i;
2614 if attr_bits & bit == 0 {
2615 i += 1;
2616 continue;
2617 }
2618 attr_bits &= !bit;
2619 if sep > 0 {
2620 out.push(';');
2621 }
2622 sep += 1;
2623 use std::fmt::Write;
2624 let _ = write!(out, "{i}");
2625 i += 1;
2626 }
2627
2628 if !color_empty(&fg) {
2629 if sep > 0 {
2630 out.push(';');
2631 }
2632 sep += 1;
2633 append_color_output(&mut out, &fg, false);
2634 }
2635 if !color_empty(&bg) {
2636 if sep > 0 {
2637 out.push(';');
2638 }
2639 append_color_output(&mut out, &bg, true);
2640 }
2641 out.push('m');
2642 Ok(out)
2643}
2644
2645#[derive(Debug, Clone)]
2646struct UrlParts {
2647 scheme: String,
2648 user: Option<String>,
2649 host: String,
2650 port: Option<String>,
2651 path: String,
2652}
2653
2654#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2655struct UrlMatchScore {
2656 host_len: usize,
2657 path_len: usize,
2658 user_matched: bool,
2659}
2660
2661fn parse_config_url(url: &str) -> Option<UrlParts> {
2662 let (scheme, rest) = url.split_once("://")?;
2663 let (authority, path) = match rest.find('/') {
2664 Some(idx) => (&rest[..idx], &rest[idx..]),
2665 None => (rest, "/"),
2666 };
2667 let (user, host_port) = match authority.rsplit_once('@') {
2668 Some((user, host)) => (Some(user.to_owned()), host),
2669 None => (None, authority),
2670 };
2671 let (host, port) = match host_port.rsplit_once(':') {
2672 Some((host, port)) if !host.contains(']') => (host, Some(port.to_owned())),
2673 _ => (host_port, None),
2674 };
2675 Some(UrlParts {
2676 scheme: scheme.to_lowercase(),
2677 user,
2678 host: host.to_lowercase(),
2679 port,
2680 path: if path.is_empty() {
2681 "/".to_owned()
2682 } else {
2683 path.trim_end_matches('/').to_owned()
2684 },
2685 })
2686}
2687
2688fn host_matches(pattern: &str, target: &str) -> bool {
2689 let pattern_parts: Vec<&str> = pattern.split('.').collect();
2690 let target_parts: Vec<&str> = target.split('.').collect();
2691 pattern_parts.len() == target_parts.len()
2692 && pattern_parts
2693 .iter()
2694 .zip(target_parts)
2695 .all(|(pattern, target)| *pattern == "*" || *pattern == target)
2696}
2697
2698fn path_match_len(pattern: &str, target: &str) -> Option<usize> {
2699 let pattern = if pattern.is_empty() { "/" } else { pattern };
2700 let target = if target.is_empty() { "/" } else { target };
2701 if pattern == "/" {
2702 return Some(1);
2703 }
2704 let pattern = pattern.trim_end_matches('/');
2705 if target == pattern
2706 || target
2707 .strip_prefix(pattern)
2708 .is_some_and(|rest| rest.starts_with('/'))
2709 {
2710 Some(pattern.len() + 1)
2711 } else {
2712 None
2713 }
2714}
2715
2716fn url_match_score(pattern_url: &str, target_url: &str) -> Option<UrlMatchScore> {
2717 let pattern = parse_config_url(pattern_url)?;
2718 let target = parse_config_url(target_url)?;
2719 if pattern.scheme != target.scheme {
2720 return None;
2721 }
2722 let user_matched = match pattern.user.as_deref() {
2723 Some(user) if target.user.as_deref() == Some(user) => true,
2724 Some(_) => return None,
2725 None => false,
2726 };
2727 if !host_matches(&pattern.host, &target.host) || pattern.port != target.port {
2728 return None;
2729 }
2730 let path_len = path_match_len(&pattern.path, &target.path)?;
2731 Some(UrlMatchScore {
2732 host_len: pattern.host.len(),
2733 path_len,
2734 user_matched,
2735 })
2736}
2737
2738pub fn url_matches(pattern_url: &str, target_url: &str) -> bool {
2740 url_match_score(pattern_url, target_url).is_some()
2741}
2742
2743pub fn get_urlmatch_entries<'a>(
2745 entries: &'a [ConfigEntry],
2746 section: &str,
2747 variable: &str,
2748 url: &str,
2749) -> Vec<&'a ConfigEntry> {
2750 let section_lower = section.to_lowercase();
2751 let variable_lower = variable.to_lowercase();
2752 let mut matches: Vec<(UrlMatchScore, &'a ConfigEntry)> = Vec::new();
2753
2754 for entry in entries {
2755 let key = &entry.key;
2756 let first_dot = match key.find('.') {
2757 Some(i) => i,
2758 None => continue,
2759 };
2760 let last_dot = match key.rfind('.') {
2761 Some(i) => i,
2762 None => continue,
2763 };
2764 let entry_section = &key[..first_dot];
2765 let entry_variable = &key[last_dot + 1..];
2766 if entry_section.to_lowercase() != section_lower
2767 || entry_variable.to_lowercase() != variable_lower
2768 {
2769 continue;
2770 }
2771 if first_dot == last_dot {
2772 matches.push((
2773 UrlMatchScore {
2774 host_len: 0,
2775 path_len: 0,
2776 user_matched: false,
2777 },
2778 entry,
2779 ));
2780 } else {
2781 let subsection = &key[first_dot + 1..last_dot];
2782 if let Some(score) = url_match_score(subsection, url) {
2783 matches.push((score, entry));
2784 }
2785 }
2786 }
2787 matches.sort_by_key(|a| a.0);
2788 matches.into_iter().map(|(_, e)| e).collect()
2789}
2790
2791pub fn get_urlmatch_all_in_section(
2793 entries: &[ConfigEntry],
2794 section: &str,
2795 url: &str,
2796) -> Vec<(String, String, ConfigScope)> {
2797 let section_lower = section.to_lowercase();
2798 let mut matches: Vec<(String, UrlMatchScore, String, String, ConfigScope)> = Vec::new();
2799
2800 for entry in entries {
2801 let key = &entry.key;
2802 let first_dot = match key.find('.') {
2803 Some(i) => i,
2804 None => continue,
2805 };
2806 let last_dot = match key.rfind('.') {
2807 Some(i) => i,
2808 None => continue,
2809 };
2810 let entry_section = &key[..first_dot];
2811 if entry_section.to_lowercase() != section_lower {
2812 continue;
2813 }
2814 let entry_variable = &key[last_dot + 1..];
2815 let val = entry.value.as_deref().unwrap_or("");
2816 if first_dot == last_dot {
2817 let canonical = format!("{}.{}", section_lower, entry_variable);
2818 matches.push((
2819 entry_variable.to_lowercase(),
2820 UrlMatchScore {
2821 host_len: 0,
2822 path_len: 0,
2823 user_matched: false,
2824 },
2825 val.to_owned(),
2826 canonical,
2827 entry.scope,
2828 ));
2829 } else {
2830 let subsection = &key[first_dot + 1..last_dot];
2831 if let Some(score) = url_match_score(subsection, url) {
2832 let canonical = format!("{}.{}", section_lower, entry_variable);
2833 matches.push((
2834 entry_variable.to_lowercase(),
2835 score,
2836 val.to_owned(),
2837 canonical,
2838 entry.scope,
2839 ));
2840 }
2841 }
2842 }
2843
2844 let mut best: std::collections::BTreeMap<String, (UrlMatchScore, String, String, ConfigScope)> =
2845 std::collections::BTreeMap::new();
2846 for (var, specificity, val, canonical, scope) in matches {
2847 let entry = best.entry(var).or_insert((
2848 UrlMatchScore {
2849 host_len: 0,
2850 path_len: 0,
2851 user_matched: false,
2852 },
2853 String::new(),
2854 String::new(),
2855 scope,
2856 ));
2857 if specificity >= entry.0 {
2858 *entry = (specificity, val, canonical, scope);
2859 }
2860 }
2861 best.into_values()
2862 .map(|(_, val, canonical, scope)| (canonical, val, scope))
2863 .collect()
2864}
2865
2866pub fn parse_path(s: &str) -> String {
2870 if let Some(rest) = s.strip_prefix("~/") {
2871 if let Some(home) = home_dir() {
2872 return home.join(rest).to_string_lossy().to_string();
2873 }
2874 }
2875 s.to_owned()
2876}
2877
2878pub fn parse_path_optional(s: &str) -> Option<String> {
2883 if let Some(rest) = s.strip_prefix(":(optional)") {
2884 let resolved = parse_path(rest);
2885 if std::path::Path::new(&resolved).exists() {
2886 Some(resolved)
2887 } else {
2888 None }
2890 } else {
2891 Some(parse_path(s))
2892 }
2893}
2894
2895#[must_use]
2911pub fn git_config_parameters_last_value(raw: &str, key: &str) -> Option<String> {
2912 let Ok(canon) = canonical_key(key) else {
2913 return None;
2914 };
2915 let mut last: Option<String> = None;
2916 for entry in parse_config_parameters_strict(raw).ok()? {
2917 match entry {
2918 ConfigParameter::Pair { key, value } => {
2919 if canonical_key(key.trim()).ok().as_ref() == Some(&canon) {
2920 last = Some(value.unwrap_or_else(|| "true".to_owned()));
2921 }
2922 }
2923 ConfigParameter::OldStyle(entry) => {
2924 if let Some((k, v)) = entry.split_once('=') {
2925 if canonical_key(k.trim()).ok().as_ref() == Some(&canon) {
2926 last = Some(v.to_owned());
2927 }
2928 } else if canonical_key(entry.trim()).ok().as_ref() == Some(&canon) {
2929 last = Some("true".to_owned());
2930 }
2931 }
2932 }
2933 }
2934 last
2935}
2936
2937#[derive(Debug, Clone, PartialEq, Eq)]
2938enum ConfigParameter {
2939 OldStyle(String),
2940 Pair { key: String, value: Option<String> },
2941}
2942
2943pub fn parse_config_parameters(raw: &str) -> Vec<String> {
2944 parse_config_parameters_strict(raw)
2945 .map(|entries| {
2946 entries
2947 .into_iter()
2948 .map(|entry| match entry {
2949 ConfigParameter::OldStyle(entry) => entry,
2950 ConfigParameter::Pair {
2951 key,
2952 value: Some(value),
2953 } => format!("{key}\u{1}{value}"),
2954 ConfigParameter::Pair { key, value: None } => format!("{key}\u{1}"),
2955 })
2956 .collect()
2957 })
2958 .unwrap_or_default()
2959}
2960
2961fn parse_config_parameters_strict(raw: &str) -> Result<Vec<ConfigParameter>> {
2962 let mut out: Vec<ConfigParameter> = Vec::new();
2963 let chars: Vec<char> = raw.chars().collect();
2964 let mut idx = skip_config_parameter_spaces(&chars, 0);
2965
2966 while idx < chars.len() {
2967 let (key, next) = sq_dequote_step_chars(&chars, idx)?;
2968 let Some(next_idx) = next else {
2969 out.push(ConfigParameter::OldStyle(key));
2970 break;
2971 };
2972
2973 if chars[next_idx].is_whitespace() {
2974 out.push(ConfigParameter::OldStyle(key));
2975 idx = skip_config_parameter_spaces(&chars, next_idx);
2976 continue;
2977 }
2978
2979 if chars[next_idx] != '=' {
2980 return Err(Error::ConfigError(
2981 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
2982 ));
2983 }
2984
2985 let value_start = next_idx + 1;
2986 if value_start >= chars.len() || chars[value_start].is_whitespace() {
2987 out.push(ConfigParameter::Pair { key, value: None });
2988 idx = skip_config_parameter_spaces(&chars, value_start);
2989 continue;
2990 }
2991
2992 if chars[value_start] != '\'' {
2993 return Err(Error::ConfigError(
2994 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
2995 ));
2996 }
2997 let (value, value_next) = sq_dequote_step_chars(&chars, value_start)?;
2998 if let Some(value_next) = value_next {
2999 if !chars[value_next].is_whitespace() {
3000 return Err(Error::ConfigError(
3001 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3002 ));
3003 }
3004 idx = skip_config_parameter_spaces(&chars, value_next);
3005 } else {
3006 idx = chars.len();
3007 }
3008 out.push(ConfigParameter::Pair {
3009 key,
3010 value: Some(value),
3011 });
3012 }
3013
3014 Ok(out)
3015}
3016
3017fn skip_config_parameter_spaces(chars: &[char], mut idx: usize) -> usize {
3018 while idx < chars.len() && chars[idx].is_whitespace() {
3019 idx += 1;
3020 }
3021 idx
3022}
3023
3024fn sq_dequote_step_chars(chars: &[char], start: usize) -> Result<(String, Option<usize>)> {
3025 if chars.get(start) != Some(&'\'') {
3026 return Err(Error::ConfigError(
3027 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3028 ));
3029 }
3030
3031 let mut out = String::new();
3032 let mut idx = start + 1;
3033 loop {
3034 let Some(&ch) = chars.get(idx) else {
3035 return Err(Error::ConfigError(
3036 "bogus format in GIT_CONFIG_PARAMETERS".to_owned(),
3037 ));
3038 };
3039 if ch != '\'' {
3040 out.push(ch);
3041 idx += 1;
3042 continue;
3043 }
3044
3045 idx += 1;
3046 match chars.get(idx).copied() {
3047 None => return Ok((out, None)),
3048 Some('\\')
3049 if chars
3050 .get(idx + 1)
3051 .copied()
3052 .is_some_and(needs_sq_backslash_quote)
3053 && chars.get(idx + 2) == Some(&'\'') =>
3054 {
3055 if let Some(escaped) = chars.get(idx + 1) {
3056 out.push(*escaped);
3057 }
3058 idx += 3;
3059 }
3060 _ => return Ok((out, Some(idx))),
3061 }
3062 }
3063}
3064
3065fn needs_sq_backslash_quote(ch: char) -> bool {
3066 ch == '\'' || ch == '!'
3067}
3068
3069pub fn global_config_paths_pub() -> Vec<PathBuf> {
3072 global_config_paths()
3073}
3074
3075fn global_config_paths() -> Vec<PathBuf> {
3076 let mut paths = Vec::new();
3077
3078 if let Ok(p) = std::env::var("GIT_CONFIG_GLOBAL") {
3080 paths.push(PathBuf::from(p));
3081 return paths;
3082 }
3083
3084 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
3086 paths.push(PathBuf::from(xdg).join("git/config"));
3087 } else if let Some(home) = home_dir() {
3088 paths.push(home.join(".config/git/config"));
3089 }
3090 if let Some(home) = home_dir() {
3091 paths.push(home.join(".gitconfig"));
3092 }
3093
3094 paths
3095}
3096
3097fn home_dir() -> Option<PathBuf> {
3099 std::env::var("HOME").ok().map(PathBuf::from)
3100}
3101
3102fn include_source_is_disk_file(file: &ConfigFile) -> bool {
3104 file.include_origin == ConfigIncludeOrigin::Disk
3105}
3106
3107fn resolve_include_file_path(
3111 path: &str,
3112 file: &ConfigFile,
3113 ctx: &IncludeContext,
3114) -> Result<PathBuf> {
3115 let expanded = parse_path(path);
3116 let p = Path::new(&expanded);
3117 if p.is_absolute() {
3118 return Ok(p.to_path_buf());
3119 }
3120 if !include_source_is_disk_file(file) {
3121 if file.include_origin == ConfigIncludeOrigin::CommandLine {
3122 if ctx.command_line_relative_include_is_error {
3123 return Err(Error::ConfigError(
3124 "relative config includes must come from files".to_owned(),
3125 ));
3126 }
3127 return Err(Error::ConfigError(String::new()));
3128 }
3129 return Err(Error::ConfigError(
3130 "relative config includes must come from files".to_owned(),
3131 ));
3132 }
3133 let base = match file.path.parent() {
3134 Some(p) if !p.as_os_str().is_empty() => p,
3135 Some(_) | None => Path::new("."),
3136 };
3137 Ok(base.join(p))
3138}
3139
3140fn is_dir_sep(b: u8) -> bool {
3141 b == b'/' || b == b'\\'
3142}
3143
3144fn add_trailing_starstar_for_dir(pat: &mut String) {
3145 let bytes = pat.as_bytes();
3146 if bytes.last().is_some_and(|&b| is_dir_sep(b)) {
3147 pat.push_str("**");
3148 }
3149}
3150
3151fn prepare_gitdir_pattern(condition: &str, file: &ConfigFile) -> Result<(String, usize)> {
3153 let mut pat = parse_path(condition);
3155 if pat.starts_with("./") || pat.starts_with(".\\") {
3156 if !include_source_is_disk_file(file) {
3157 return Err(Error::ConfigError(
3158 "relative config include conditionals must come from files".to_owned(),
3159 ));
3160 }
3161 let parent = file.path.parent().ok_or_else(|| {
3162 Error::ConfigError(
3163 "relative config include conditionals must come from files".to_owned(),
3164 )
3165 })?;
3166 let real = parent.canonicalize().map_err(Error::Io)?;
3167 let mut dir = real.to_string_lossy().into_owned();
3168 if !dir.ends_with('/') && !dir.ends_with('\\') {
3169 dir.push('/');
3170 }
3171 let rest = &pat[2..];
3172 pat = format!("{dir}{rest}");
3173 let prefix_len = dir.len();
3174 add_trailing_starstar_for_dir(&mut pat);
3175 return Ok((pat, prefix_len));
3176 }
3177 let p = Path::new(&pat);
3178 if !p.is_absolute() {
3179 pat.insert_str(0, "**/");
3180 }
3181 add_trailing_starstar_for_dir(&mut pat);
3182 Ok((pat, 0))
3183}
3184
3185fn git_dir_match_texts(git_dir: &Path) -> (String, String) {
3190 let real = git_dir
3191 .canonicalize()
3192 .map(|p| p.to_string_lossy().into_owned())
3193 .unwrap_or_else(|_| git_dir.to_string_lossy().into_owned());
3194 let abs = if git_dir.is_absolute() {
3197 let pwd_abs = std::env::var("PWD").ok().and_then(|pwd| {
3200 let pwd_path = std::path::Path::new(&pwd);
3201 if !pwd_path.is_absolute() {
3202 return None;
3203 }
3204 let pwd_canon = pwd_path.canonicalize().ok()?;
3205 let git_dir_str = git_dir.to_string_lossy();
3206 let pwd_canon_str = pwd_canon.to_string_lossy();
3207 let suffix = git_dir_str.strip_prefix(pwd_canon_str.as_ref())?;
3209 Some(format!("{pwd}{suffix}"))
3210 });
3211 pwd_abs.unwrap_or_else(|| git_dir.to_string_lossy().into_owned())
3212 } else if let Ok(cwd) = std::env::current_dir() {
3213 cwd.join(git_dir).to_string_lossy().into_owned()
3214 } else {
3215 git_dir.to_string_lossy().into_owned()
3216 };
3217 (real, abs)
3218}
3219
3220fn include_by_gitdir(
3221 condition: &str,
3222 file: &ConfigFile,
3223 ctx: &IncludeContext,
3224 icase: bool,
3225) -> bool {
3226 let Some(git_dir) = ctx.git_dir.as_ref() else {
3227 return false;
3228 };
3229 let (pattern, prefix) = match prepare_gitdir_pattern(condition, file) {
3230 Ok(x) => x,
3231 Err(_) => return false,
3232 };
3233 let flags = WM_PATHNAME | if icase { WM_CASEFOLD } else { 0 };
3234 let (text_real, text_abs) = git_dir_match_texts(git_dir);
3235 let try_match = |text: &str| -> bool {
3236 let t = text.as_bytes();
3237 let p = pattern.as_bytes();
3238 if prefix > 0 {
3239 if t.len() < prefix {
3240 return false;
3241 }
3242 let pre = &p[..prefix];
3243 let te = &t[..prefix];
3244 let ok = if icase {
3245 pre.eq_ignore_ascii_case(te)
3246 } else {
3247 pre == te
3248 };
3249 if !ok {
3250 return false;
3251 }
3252 return wildmatch(&p[prefix..], &t[prefix..], flags);
3253 }
3254 wildmatch(p, t, flags)
3255 };
3256 if try_match(&text_real) {
3257 return true;
3258 }
3259 text_real != text_abs && try_match(&text_abs)
3260}
3261
3262fn current_branch_short_name(git_dir: Option<&Path>) -> Option<String> {
3263 let gd = git_dir?;
3264 let target = refs::read_symbolic_ref(gd, "HEAD").ok()??;
3265 let rest = target.strip_prefix("refs/heads/")?;
3266 Some(rest.to_owned())
3267}
3268
3269fn include_by_onbranch(condition: &str, ctx: &IncludeContext) -> bool {
3270 let Some(short) = current_branch_short_name(ctx.git_dir.as_deref()) else {
3271 return false;
3272 };
3273 let mut pattern = condition.to_owned();
3274 add_trailing_starstar_for_dir(&mut pattern);
3275 wildmatch(pattern.as_bytes(), short.as_bytes(), WM_PATHNAME)
3276}
3277
3278fn is_remote_url_entry(entry: &ConfigEntry) -> bool {
3279 let Ok((section, subsection, variable)) = split_key(&entry.key) else {
3280 return false;
3281 };
3282 section == "remote" && subsection.is_some() && variable == "url"
3283}
3284
3285fn is_hasconfig_remote_url(condition: &str) -> bool {
3286 condition
3287 .strip_prefix("hasconfig:")
3288 .is_some_and(|rest| rest.starts_with("remote.*.url:"))
3289}
3290
3291fn include_by_hasconfig_remote_url(condition: &str, set: &ConfigSet, file: &ConfigFile) -> bool {
3292 let Some(pattern) = condition.strip_prefix("remote.*.url:") else {
3293 return false;
3294 };
3295 set.entries
3296 .iter()
3297 .chain(file.entries.iter())
3298 .filter(|entry| is_remote_url_entry(entry))
3299 .filter_map(|entry| entry.value.as_deref())
3300 .any(|value| wildmatch(pattern.as_bytes(), value.as_bytes(), WM_PATHNAME))
3301}
3302
3303fn validate_hasconfig_remote_url_include(
3304 file: &ConfigFile,
3305 process_includes: bool,
3306 depth: usize,
3307 ctx: &IncludeContext,
3308) -> Result<()> {
3309 const MAX_INCLUDE_DEPTH: usize = 10;
3310 if depth > MAX_INCLUDE_DEPTH {
3311 return Err(Error::ConfigError(
3312 "exceeded maximum include depth".to_owned(),
3313 ));
3314 }
3315 if file.entries.iter().any(is_remote_url_entry) {
3316 return Err(Error::Message(
3317 "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url"
3318 .to_owned(),
3319 ));
3320 }
3321 if !process_includes {
3322 return Ok(());
3323 }
3324 for entry in &file.entries {
3325 let Some((inc_path, condition)) = include_directive_for_entry(entry) else {
3326 continue;
3327 };
3328 if let Some(ref cond) = condition {
3329 if !evaluate_include_condition(cond, &ConfigSet::new(), file, ctx) {
3330 continue;
3331 }
3332 }
3333 let resolved = match resolve_include_file_path(&inc_path, file, ctx) {
3334 Ok(p) => p,
3335 Err(Error::ConfigError(msg)) if msg.is_empty() => continue,
3336 Err(e) => return Err(e),
3337 };
3338 if let Some(inc_file) = ConfigFile::from_path(&resolved, file.scope)? {
3339 validate_hasconfig_remote_url_include(&inc_file, process_includes, depth + 1, ctx)?;
3340 }
3341 }
3342 Ok(())
3343}
3344
3345fn evaluate_include_condition(
3350 condition: &str,
3351 set: &ConfigSet,
3352 file: &ConfigFile,
3353 ctx: &IncludeContext,
3354) -> bool {
3355 if let Some(rest) = condition.strip_prefix("gitdir/i:") {
3356 return include_by_gitdir(rest, file, ctx, true);
3357 }
3358 if let Some(rest) = condition.strip_prefix("gitdir:") {
3359 return include_by_gitdir(rest, file, ctx, false);
3360 }
3361 if let Some(rest) = condition.strip_prefix("onbranch:") {
3362 return include_by_onbranch(rest, ctx);
3363 }
3364 if let Some(rest) = condition.strip_prefix("hasconfig:") {
3365 return include_by_hasconfig_remote_url(rest, set, file);
3366 }
3367 false
3368}
3369
3370fn split_key(key: &str) -> Result<(String, Option<String>, String)> {
3372 let first_dot = key
3373 .find('.')
3374 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
3375 let last_dot = key
3376 .rfind('.')
3377 .ok_or_else(|| Error::ConfigError(format!("invalid key: '{key}'")))?;
3378
3379 let section = key[..first_dot].to_owned();
3380 let variable = key[last_dot + 1..].to_owned();
3381
3382 let subsection = if first_dot == last_dot {
3383 None
3384 } else {
3385 Some(key[first_dot + 1..last_dot].to_owned())
3386 };
3387
3388 Ok((section, subsection, variable))
3389}
3390
3391#[allow(dead_code)]
3393fn variable_name_from_key(key: &str) -> &str {
3394 match key.rfind('.') {
3395 Some(i) => &key[i + 1..],
3396 None => key,
3397 }
3398}
3399
3400fn parse_section_name(name: &str) -> (&str, Option<&str>) {
3404 match name.find('.') {
3405 Some(i) => (&name[..i], Some(&name[i + 1..])),
3406 None => (name, None),
3407 }
3408}
3409
3410fn section_matches(parser: &Parser, section_lower: &str, subsection: Option<&str>) -> bool {
3411 if parser.section.to_lowercase() == section_lower && parser.subsection.as_deref() == subsection
3412 {
3413 return true;
3414 }
3415 let Some(subsection) = subsection else {
3416 return false;
3417 };
3418 parser.subsection.is_none()
3419 && parser.section.to_lowercase() == format!("{section_lower}.{}", subsection.to_lowercase())
3420}
3421
3422fn validate_section_name(section: &str, subsection: Option<&str>) -> Result<()> {
3423 if section.is_empty()
3424 || !section
3425 .chars()
3426 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
3427 || subsection.is_some_and(str::is_empty)
3428 {
3429 return Err(Error::ConfigError(format!(
3430 "invalid section name: {section}"
3431 )));
3432 }
3433 Ok(())
3434}
3435
3436fn raw_variable_name(raw_key: &str) -> &str {
3440 match raw_key.rfind('.') {
3441 Some(i) => &raw_key[i + 1..],
3442 None => raw_key,
3443 }
3444}
3445
3446fn raw_section_parts(raw_key: &str) -> (String, Option<String>) {
3451 let first_dot = match raw_key.find('.') {
3452 Some(i) => i,
3453 None => return (raw_key.to_owned(), None),
3454 };
3455 let last_dot = match raw_key.rfind('.') {
3457 Some(i) => i,
3458 None => return (raw_key[..first_dot].to_owned(), None),
3459 };
3460 let section = raw_key[..first_dot].to_owned();
3461 if first_dot == last_dot {
3462 (section, None)
3463 } else {
3464 let subsection = raw_key[first_dot + 1..last_dot].to_owned();
3465 (section, Some(subsection))
3466 }
3467}
3468
3469fn is_section_header_with_inline_entry(line: &str) -> bool {
3471 let trimmed = line.trim();
3472 if !trimmed.starts_with('[') {
3473 return false;
3474 }
3475 let end = match trimmed.find(']') {
3476 Some(i) => i,
3477 None => return false,
3478 };
3479 let after = trimmed[end + 1..].trim();
3480 !after.is_empty() && !after.starts_with('#') && !after.starts_with(';')
3482}
3483
3484fn extract_section_header(line: &str) -> String {
3487 let trimmed = line.trim();
3488 let end = match trimmed.find(']') {
3489 Some(i) => i,
3490 None => return line.to_owned(),
3491 };
3492 trimmed[..=end].to_owned()
3495}
3496
3497#[cfg(test)]
3498mod get_regexp_tests {
3499 use super::{ConfigFile, ConfigScope, ConfigSet};
3500 use std::path::Path;
3501
3502 fn set_from_snippet(text: &str) -> ConfigSet {
3503 let path = Path::new(".git/config");
3504 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
3505 let mut set = ConfigSet::new();
3506 set.merge(&file);
3507 set
3508 }
3509
3510 #[test]
3511 fn get_regexp_matches_section_prefix_like_git_config() {
3512 let text = r#"
3513[user]
3514 email = alice@example.com
3515 name = Alice
3516[core]
3517 bare = false
3518"#;
3519 let set = set_from_snippet(text);
3520 let keys: Vec<_> = set
3521 .get_regexp("user")
3522 .expect("valid pattern")
3523 .into_iter()
3524 .map(|e| e.key.as_str())
3525 .collect();
3526 assert!(keys.contains(&"user.email"));
3527 assert!(keys.contains(&"user.name"));
3528 assert!(!keys.iter().any(|k| k.starts_with("core.")));
3529 }
3530
3531 #[test]
3532 fn get_regexp_returns_all_multi_value_entries_in_order() {
3533 let text = r#"
3534[remote "origin"]
3535 url = https://example.com/repo.git
3536 fetch = +refs/heads/*:refs/remotes/origin/*
3537 push = +refs/heads/main:refs/heads/main
3538 push = +refs/heads/develop:refs/heads/develop
3539"#;
3540 let set = set_from_snippet(text);
3541 let matches = set.get_regexp("remote.origin").expect("valid pattern");
3542 let push_vals: Vec<_> = matches
3543 .iter()
3544 .filter(|e| e.key == "remote.origin.push")
3545 .map(|e| e.value.as_deref().unwrap_or(""))
3546 .collect();
3547 assert_eq!(push_vals.len(), 2);
3548 assert_eq!(push_vals[0], "+refs/heads/main:refs/heads/main");
3549 assert_eq!(push_vals[1], "+refs/heads/develop:refs/heads/develop");
3550 }
3551
3552 #[test]
3553 fn get_regexp_dot_matches_any_key() {
3554 let text = r#"
3555[a]
3556 x = 1
3557[b]
3558 y = 2
3559"#;
3560 let set = set_from_snippet(text);
3561 let m = set.get_regexp(".").expect("valid pattern");
3562 assert_eq!(m.len(), 2);
3563 }
3564
3565 #[test]
3566 fn get_regexp_no_match_returns_empty_vec() {
3567 let set = set_from_snippet("[user]\n\tname = x\n");
3568 let m = set.get_regexp("zzz").expect("valid pattern");
3569 assert!(m.is_empty());
3570 }
3571
3572 #[test]
3573 fn get_regexp_invalid_pattern_is_error() {
3574 let set = set_from_snippet("[user]\n\tname = x\n");
3575 let err = set.get_regexp("(").expect_err("unclosed group");
3576 assert!(err.contains("invalid key pattern"), "got: {err}");
3577 }
3578}
3579
3580#[cfg(test)]
3581mod pack_compression_tests {
3582 use super::{ConfigFile, ConfigScope, ConfigSet};
3583 use std::path::Path;
3584
3585 fn set_from_snippet(text: &str) -> ConfigSet {
3586 let path = Path::new(".git/config");
3587 let file = ConfigFile::parse(path, text, ConfigScope::Local).expect("parse config snippet");
3588 let mut set = ConfigSet::new();
3589 set.merge(&file);
3590 set
3591 }
3592
3593 #[test]
3594 fn pack_objects_zlib_level_defaults_to_six() {
3595 let set = ConfigSet::new();
3596 assert_eq!(set.pack_objects_zlib_level().unwrap(), 6);
3597 }
3598
3599 #[test]
3600 fn pack_objects_zlib_level_core_compression() {
3601 let set = set_from_snippet("[core]\n\tcompression = 0\n");
3602 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3603 let set = set_from_snippet("[core]\n\tcompression = 9\n");
3604 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3605 }
3606
3607 #[test]
3608 fn pack_objects_zlib_level_pack_overrides_core() {
3609 let set = set_from_snippet("[core]\n\tcompression = 9\n[pack]\n\tcompression = 0\n");
3610 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3611 let set = set_from_snippet("[core]\n\tcompression = 0\n[pack]\n\tcompression = 9\n");
3612 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3613 }
3614
3615 #[test]
3616 fn pack_objects_zlib_level_later_core_does_not_override_earlier_pack() {
3617 let mut set = ConfigSet::new();
3618 set.merge(
3619 &ConfigFile::parse(
3620 Path::new("a"),
3621 "[pack]\n\tcompression = 9\n",
3622 ConfigScope::Local,
3623 )
3624 .unwrap(),
3625 );
3626 set.merge(
3627 &ConfigFile::parse(
3628 Path::new("b"),
3629 "[core]\n\tcompression = 0\n",
3630 ConfigScope::Local,
3631 )
3632 .unwrap(),
3633 );
3634 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3635 }
3636
3637 #[test]
3638 fn pack_objects_zlib_level_loosecompression_does_not_block_core_pack_level() {
3639 let set = set_from_snippet("[core]\n\tloosecompression = 1\n\tcompression = 0\n");
3640 assert_eq!(set.pack_objects_zlib_level().unwrap(), 0);
3641 }
3642
3643 #[test]
3644 fn pack_objects_zlib_level_pack_wins_after_loose_and_core() {
3645 let set = set_from_snippet(
3646 "[core]\n\tloosecompression = 1\n\tcompression = 0\n[pack]\n\tcompression = 9\n",
3647 );
3648 assert_eq!(set.pack_objects_zlib_level().unwrap(), 9);
3649 }
3650}