Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dulwich/config.py: 61%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

730 statements  

1# config.py - Reading and writing Git config files 

2# Copyright (C) 2011-2013 Jelmer Vernooij <jelmer@jelmer.uk> 

3# 

4# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later 

5# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU 

6# General Public License as published by the Free Software Foundation; version 2.0 

7# or (at your option) any later version. You can redistribute it and/or 

8# modify it under the terms of either of these two licenses. 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, 

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

13# See the License for the specific language governing permissions and 

14# limitations under the License. 

15# 

16# You should have received a copy of the licenses; if not, see 

17# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License 

18# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache 

19# License, Version 2.0. 

20# 

21 

22"""Reading and writing Git configuration files. 

23 

24Todo: 

25 * preserve formatting when updating configuration files 

26""" 

27 

28import logging 

29import os 

30import re 

31import sys 

32from collections.abc import ( 

33 ItemsView, 

34 Iterable, 

35 Iterator, 

36 KeysView, 

37 MutableMapping, 

38 ValuesView, 

39) 

40from contextlib import suppress 

41from pathlib import Path 

42from typing import ( 

43 IO, 

44 Callable, 

45 Generic, 

46 Optional, 

47 TypeVar, 

48 Union, 

49 overload, 

50) 

51 

52from .file import GitFile, _GitFile 

53 

54ConfigKey = Union[str, bytes, tuple[Union[str, bytes], ...]] 

55ConfigValue = Union[str, bytes, bool, int] 

56 

57logger = logging.getLogger(__name__) 

58 

59# Type for file opener callback 

60FileOpener = Callable[[Union[str, os.PathLike]], IO[bytes]] 

61 

62# Type for includeIf condition matcher 

63# Takes the condition value (e.g., "main" for onbranch:main) and returns bool 

64ConditionMatcher = Callable[[str], bool] 

65 

66# Security limits for include files 

67MAX_INCLUDE_FILE_SIZE = 1024 * 1024 # 1MB max for included config files 

68DEFAULT_MAX_INCLUDE_DEPTH = 10 # Maximum recursion depth for includes 

69 

70 

71def _match_gitdir_pattern( 

72 path: bytes, pattern: bytes, ignorecase: bool = False 

73) -> bool: 

74 """Simple gitdir pattern matching for includeIf conditions. 

75 

76 This handles the basic gitdir patterns used in includeIf directives. 

77 """ 

78 # Convert to strings for easier manipulation 

79 path_str = path.decode("utf-8", errors="replace") 

80 pattern_str = pattern.decode("utf-8", errors="replace") 

81 

82 # Normalize paths to use forward slashes for consistent matching 

83 path_str = path_str.replace("\\", "/") 

84 pattern_str = pattern_str.replace("\\", "/") 

85 

86 if ignorecase: 

87 path_str = path_str.lower() 

88 pattern_str = pattern_str.lower() 

89 

90 # Handle the common cases for gitdir patterns 

91 if pattern_str.startswith("**/") and pattern_str.endswith("/**"): 

92 # Pattern like **/dirname/** should match any path containing dirname 

93 dirname = pattern_str[3:-3] # Remove **/ and /** 

94 # Check if path contains the directory name as a path component 

95 return ("/" + dirname + "/") in path_str or path_str.endswith("/" + dirname) 

96 elif pattern_str.startswith("**/"): 

97 # Pattern like **/filename 

98 suffix = pattern_str[3:] # Remove **/ 

99 return suffix in path_str or path_str.endswith("/" + suffix) 

100 elif pattern_str.endswith("/**"): 

101 # Pattern like /path/to/dir/** should match /path/to/dir and any subdirectory 

102 base_pattern = pattern_str[:-3] # Remove /** 

103 return path_str == base_pattern or path_str.startswith(base_pattern + "/") 

104 elif "**" in pattern_str: 

105 # Handle patterns with ** in the middle 

106 parts = pattern_str.split("**") 

107 if len(parts) == 2: 

108 prefix, suffix = parts 

109 # Path must start with prefix and end with suffix (if any) 

110 if prefix and not path_str.startswith(prefix): 

111 return False 

112 if suffix and not path_str.endswith(suffix): 

113 return False 

114 return True 

115 

116 # Direct match or simple glob pattern 

117 if "*" in pattern_str or "?" in pattern_str or "[" in pattern_str: 

118 import fnmatch 

119 

120 return fnmatch.fnmatch(path_str, pattern_str) 

121 else: 

122 return path_str == pattern_str 

123 

124 

125def match_glob_pattern(value: str, pattern: str) -> bool: 

126 r"""Match a value against a glob pattern. 

127 

128 Supports simple glob patterns like ``*`` and ``**``. 

129 

130 Raises: 

131 ValueError: If the pattern is invalid 

132 """ 

133 # Convert glob pattern to regex 

134 pattern_escaped = re.escape(pattern) 

135 # Replace escaped \*\* with .* (match anything) 

136 pattern_escaped = pattern_escaped.replace(r"\*\*", ".*") 

137 # Replace escaped \* with [^/]* (match anything except /) 

138 pattern_escaped = pattern_escaped.replace(r"\*", "[^/]*") 

139 # Anchor the pattern 

140 pattern_regex = f"^{pattern_escaped}$" 

141 

142 try: 

143 return bool(re.match(pattern_regex, value)) 

144 except re.error as e: 

145 raise ValueError(f"Invalid glob pattern {pattern!r}: {e}") 

146 

147 

148def lower_key(key: ConfigKey) -> ConfigKey: 

149 """Convert a config key to lowercase, preserving subsection case. 

150 

151 Args: 

152 key: Configuration key (str, bytes, or tuple) 

153 

154 Returns: 

155 Key with section names lowercased, subsection names preserved 

156 

157 Raises: 

158 TypeError: If key is not str, bytes, or tuple 

159 """ 

160 if isinstance(key, (bytes, str)): 

161 return key.lower() 

162 

163 if isinstance(key, tuple): 

164 # For config sections, only lowercase the section name (first element) 

165 # but preserve the case of subsection names (remaining elements) 

166 if len(key) > 0: 

167 first = key[0] 

168 assert isinstance(first, (bytes, str)) 

169 return (first.lower(), *key[1:]) 

170 return key 

171 

172 raise TypeError(key) 

173 

174 

175K = TypeVar("K", bound=ConfigKey) # Key type must be ConfigKey 

176V = TypeVar("V") # Value type 

177_T = TypeVar("_T") # For get() default parameter 

178 

179 

180class CaseInsensitiveOrderedMultiDict(MutableMapping[K, V], Generic[K, V]): 

181 """A case-insensitive ordered dictionary that can store multiple values per key. 

182 

183 This class maintains the order of insertions and allows multiple values 

184 for the same key. Keys are compared case-insensitively. 

185 """ 

186 

187 def __init__(self, default_factory: Optional[Callable[[], V]] = None) -> None: 

188 """Initialize a CaseInsensitiveOrderedMultiDict. 

189 

190 Args: 

191 default_factory: Optional factory function for default values 

192 """ 

193 self._real: list[tuple[K, V]] = [] 

194 self._keyed: dict[ConfigKey, V] = {} 

195 self._default_factory = default_factory 

196 

197 @classmethod 

198 def make( 

199 cls, 

200 dict_in: Optional[ 

201 Union[MutableMapping[K, V], "CaseInsensitiveOrderedMultiDict[K, V]"] 

202 ] = None, 

203 default_factory: Optional[Callable[[], V]] = None, 

204 ) -> "CaseInsensitiveOrderedMultiDict[K, V]": 

205 """Create a CaseInsensitiveOrderedMultiDict from an existing mapping. 

206 

207 Args: 

208 dict_in: Optional mapping to initialize from 

209 default_factory: Optional factory function for default values 

210 

211 Returns: 

212 New CaseInsensitiveOrderedMultiDict instance 

213 

214 Raises: 

215 TypeError: If dict_in is not a mapping or None 

216 """ 

217 if isinstance(dict_in, cls): 

218 return dict_in 

219 

220 out = cls(default_factory=default_factory) 

221 

222 if dict_in is None: 

223 return out 

224 

225 if not isinstance(dict_in, MutableMapping): 

226 raise TypeError 

227 

228 for key, value in dict_in.items(): 

229 out[key] = value 

230 

231 return out 

232 

233 def __len__(self) -> int: 

234 """Return the number of unique keys in the dictionary.""" 

235 return len(self._keyed) 

236 

237 def keys(self) -> KeysView[K]: 

238 """Return a view of the dictionary's keys.""" 

239 # Return a view of the original keys (not lowercased) 

240 # We need to deduplicate since _real can have duplicates 

241 seen = set() 

242 unique_keys = [] 

243 for k, _ in self._real: 

244 lower = lower_key(k) 

245 if lower not in seen: 

246 seen.add(lower) 

247 unique_keys.append(k) 

248 from collections.abc import KeysView as ABCKeysView 

249 

250 class UniqueKeysView(ABCKeysView[K]): 

251 def __init__(self, keys: list[K]): 

252 self._keys = keys 

253 

254 def __contains__(self, key: object) -> bool: 

255 return key in self._keys 

256 

257 def __iter__(self): 

258 return iter(self._keys) 

259 

260 def __len__(self) -> int: 

261 return len(self._keys) 

262 

263 return UniqueKeysView(unique_keys) 

264 

265 def items(self) -> ItemsView[K, V]: 

266 """Return a view of the dictionary's (key, value) pairs in insertion order.""" 

267 

268 # Return a view that iterates over the real list to preserve order 

269 class OrderedItemsView(ItemsView[K, V]): 

270 """Items view that preserves insertion order.""" 

271 

272 def __init__(self, mapping: CaseInsensitiveOrderedMultiDict[K, V]): 

273 self._mapping = mapping 

274 

275 def __iter__(self) -> Iterator[tuple[K, V]]: 

276 return iter(self._mapping._real) 

277 

278 def __len__(self) -> int: 

279 return len(self._mapping._real) 

280 

281 def __contains__(self, item: object) -> bool: 

282 if not isinstance(item, tuple) or len(item) != 2: 

283 return False 

284 key, value = item 

285 return any(k == key and v == value for k, v in self._mapping._real) 

286 

287 return OrderedItemsView(self) 

288 

289 def __iter__(self) -> Iterator[K]: 

290 """Iterate over the dictionary's keys.""" 

291 # Return iterator over original keys (not lowercased), deduplicated 

292 seen = set() 

293 for k, _ in self._real: 

294 lower = lower_key(k) 

295 if lower not in seen: 

296 seen.add(lower) 

297 yield k 

298 

299 def values(self) -> ValuesView[V]: 

300 """Return a view of the dictionary's values.""" 

301 return self._keyed.values() 

302 

303 def __setitem__(self, key: K, value: V) -> None: 

304 """Set a value for a key, appending to existing values.""" 

305 self._real.append((key, value)) 

306 self._keyed[lower_key(key)] = value 

307 

308 def set(self, key: K, value: V) -> None: 

309 """Set a value for a key, replacing all existing values. 

310 

311 Args: 

312 key: The key to set 

313 value: The value to set 

314 """ 

315 # This method replaces all existing values for the key 

316 lower = lower_key(key) 

317 self._real = [(k, v) for k, v in self._real if lower_key(k) != lower] 

318 self._real.append((key, value)) 

319 self._keyed[lower] = value 

320 

321 def __delitem__(self, key: K) -> None: 

322 """Delete all values for a key. 

323 

324 Raises: 

325 KeyError: If the key is not found 

326 """ 

327 lower_k = lower_key(key) 

328 del self._keyed[lower_k] 

329 for i, (actual, unused_value) in reversed(list(enumerate(self._real))): 

330 if lower_key(actual) == lower_k: 

331 del self._real[i] 

332 

333 def __getitem__(self, item: K) -> V: 

334 """Get the last value for a key. 

335 

336 Raises: 

337 KeyError: If the key is not found 

338 """ 

339 return self._keyed[lower_key(item)] 

340 

341 def get(self, key: K, /, default: Union[V, _T, None] = None) -> Union[V, _T, None]: # type: ignore[override] 

342 """Get the last value for a key, or a default if not found. 

343 

344 Args: 

345 key: The key to look up 

346 default: Default value to return if key not found 

347 

348 Returns: 

349 The value for the key, or default/default_factory result if not found 

350 """ 

351 try: 

352 return self[key] 

353 except KeyError: 

354 if default is not None: 

355 return default 

356 elif self._default_factory is not None: 

357 return self._default_factory() 

358 else: 

359 return None 

360 

361 def get_all(self, key: K) -> Iterator[V]: 

362 """Get all values for a key in insertion order. 

363 

364 Args: 

365 key: The key to look up 

366 

367 Returns: 

368 Iterator of all values for the key 

369 """ 

370 lowered_key = lower_key(key) 

371 for actual, value in self._real: 

372 if lower_key(actual) == lowered_key: 

373 yield value 

374 

375 def setdefault(self, key: K, default: Optional[V] = None) -> V: 

376 """Get value for key, setting it to default if not present. 

377 

378 Args: 

379 key: The key to look up 

380 default: Default value to set if key not found 

381 

382 Returns: 

383 The existing value or the newly set default 

384 

385 Raises: 

386 KeyError: If key not found and no default or default_factory 

387 """ 

388 try: 

389 return self[key] 

390 except KeyError: 

391 if default is not None: 

392 self[key] = default 

393 return default 

394 elif self._default_factory is not None: 

395 value = self._default_factory() 

396 self[key] = value 

397 return value 

398 else: 

399 raise 

400 

401 

402Name = bytes 

403NameLike = Union[bytes, str] 

404Section = tuple[bytes, ...] 

405SectionLike = Union[bytes, str, tuple[Union[bytes, str], ...]] 

406Value = bytes 

407ValueLike = Union[bytes, str] 

408 

409 

410class Config: 

411 """A Git configuration.""" 

412 

413 def get(self, section: SectionLike, name: NameLike) -> Value: 

414 """Retrieve the contents of a configuration setting. 

415 

416 Args: 

417 section: Tuple with section name and optional subsection name 

418 name: Variable name 

419 Returns: 

420 Contents of the setting 

421 Raises: 

422 KeyError: if the value is not set 

423 """ 

424 raise NotImplementedError(self.get) 

425 

426 def get_multivar(self, section: SectionLike, name: NameLike) -> Iterator[Value]: 

427 """Retrieve the contents of a multivar configuration setting. 

428 

429 Args: 

430 section: Tuple with section name and optional subsection namee 

431 name: Variable name 

432 Returns: 

433 Contents of the setting as iterable 

434 Raises: 

435 KeyError: if the value is not set 

436 """ 

437 raise NotImplementedError(self.get_multivar) 

438 

439 @overload 

440 def get_boolean( 

441 self, section: SectionLike, name: NameLike, default: bool 

442 ) -> bool: ... 

443 

444 @overload 

445 def get_boolean(self, section: SectionLike, name: NameLike) -> Optional[bool]: ... 

446 

447 def get_boolean( 

448 self, section: SectionLike, name: NameLike, default: Optional[bool] = None 

449 ) -> Optional[bool]: 

450 """Retrieve a configuration setting as boolean. 

451 

452 Args: 

453 section: Tuple with section name and optional subsection name 

454 name: Name of the setting, including section and possible 

455 subsection. 

456 default: Default value if setting is not found 

457 

458 Returns: 

459 Contents of the setting 

460 """ 

461 try: 

462 value = self.get(section, name) 

463 except KeyError: 

464 return default 

465 if value.lower() == b"true": 

466 return True 

467 elif value.lower() == b"false": 

468 return False 

469 raise ValueError(f"not a valid boolean string: {value!r}") 

470 

471 def set( 

472 self, section: SectionLike, name: NameLike, value: Union[ValueLike, bool] 

473 ) -> None: 

474 """Set a configuration value. 

475 

476 Args: 

477 section: Tuple with section name and optional subsection namee 

478 name: Name of the configuration value, including section 

479 and optional subsection 

480 value: value of the setting 

481 """ 

482 raise NotImplementedError(self.set) 

483 

484 def items(self, section: SectionLike) -> Iterator[tuple[Name, Value]]: 

485 """Iterate over the configuration pairs for a specific section. 

486 

487 Args: 

488 section: Tuple with section name and optional subsection namee 

489 Returns: 

490 Iterator over (name, value) pairs 

491 """ 

492 raise NotImplementedError(self.items) 

493 

494 def sections(self) -> Iterator[Section]: 

495 """Iterate over the sections. 

496 

497 Returns: Iterator over section tuples 

498 """ 

499 raise NotImplementedError(self.sections) 

500 

501 def has_section(self, name: Section) -> bool: 

502 """Check if a specified section exists. 

503 

504 Args: 

505 name: Name of section to check for 

506 Returns: 

507 boolean indicating whether the section exists 

508 """ 

509 return name in self.sections() 

510 

511 

512class ConfigDict(Config): 

513 """Git configuration stored in a dictionary.""" 

514 

515 def __init__( 

516 self, 

517 values: Union[ 

518 MutableMapping[Section, CaseInsensitiveOrderedMultiDict[Name, Value]], None 

519 ] = None, 

520 encoding: Union[str, None] = None, 

521 ) -> None: 

522 """Create a new ConfigDict.""" 

523 if encoding is None: 

524 encoding = sys.getdefaultencoding() 

525 self.encoding = encoding 

526 self._values: CaseInsensitiveOrderedMultiDict[ 

527 Section, CaseInsensitiveOrderedMultiDict[Name, Value] 

528 ] = CaseInsensitiveOrderedMultiDict.make( 

529 values, default_factory=CaseInsensitiveOrderedMultiDict 

530 ) 

531 

532 def __repr__(self) -> str: 

533 """Return string representation of ConfigDict.""" 

534 return f"{self.__class__.__name__}({self._values!r})" 

535 

536 def __eq__(self, other: object) -> bool: 

537 """Check equality with another ConfigDict.""" 

538 return isinstance(other, self.__class__) and other._values == self._values 

539 

540 def __getitem__(self, key: Section) -> CaseInsensitiveOrderedMultiDict[Name, Value]: 

541 """Get configuration values for a section. 

542 

543 Raises: 

544 KeyError: If section not found 

545 """ 

546 return self._values.__getitem__(key) 

547 

548 def __setitem__( 

549 self, key: Section, value: CaseInsensitiveOrderedMultiDict[Name, Value] 

550 ) -> None: 

551 """Set configuration values for a section.""" 

552 return self._values.__setitem__(key, value) 

553 

554 def __delitem__(self, key: Section) -> None: 

555 """Delete a configuration section. 

556 

557 Raises: 

558 KeyError: If section not found 

559 """ 

560 return self._values.__delitem__(key) 

561 

562 def __iter__(self) -> Iterator[Section]: 

563 """Iterate over configuration sections.""" 

564 return self._values.__iter__() 

565 

566 def __len__(self) -> int: 

567 """Return the number of sections.""" 

568 return self._values.__len__() 

569 

570 def keys(self) -> KeysView[Section]: 

571 """Return a view of section names.""" 

572 return self._values.keys() 

573 

574 @classmethod 

575 def _parse_setting(cls, name: str) -> tuple[str, Optional[str], str]: 

576 parts = name.split(".") 

577 if len(parts) == 3: 

578 return (parts[0], parts[1], parts[2]) 

579 else: 

580 return (parts[0], None, parts[1]) 

581 

582 def _check_section_and_name( 

583 self, section: SectionLike, name: NameLike 

584 ) -> tuple[Section, Name]: 

585 if not isinstance(section, tuple): 

586 section = (section,) 

587 

588 checked_section = tuple( 

589 [ 

590 subsection.encode(self.encoding) 

591 if not isinstance(subsection, bytes) 

592 else subsection 

593 for subsection in section 

594 ] 

595 ) 

596 

597 if not isinstance(name, bytes): 

598 name = name.encode(self.encoding) 

599 

600 return checked_section, name 

601 

602 def get_multivar(self, section: SectionLike, name: NameLike) -> Iterator[Value]: 

603 """Get multiple values for a configuration setting. 

604 

605 Args: 

606 section: Section name 

607 name: Setting name 

608 

609 Returns: 

610 Iterator of configuration values 

611 """ 

612 section, name = self._check_section_and_name(section, name) 

613 

614 if len(section) > 1: 

615 try: 

616 return self._values[section].get_all(name) 

617 except KeyError: 

618 pass 

619 

620 return self._values[(section[0],)].get_all(name) 

621 

622 def get( 

623 self, 

624 section: SectionLike, 

625 name: NameLike, 

626 ) -> Value: 

627 """Get a configuration value. 

628 

629 Args: 

630 section: Section name 

631 name: Setting name 

632 

633 Returns: 

634 Configuration value 

635 

636 Raises: 

637 KeyError: if the value is not set 

638 """ 

639 section, name = self._check_section_and_name(section, name) 

640 

641 if len(section) > 1: 

642 try: 

643 return self._values[section][name] 

644 except KeyError: 

645 pass 

646 

647 return self._values[(section[0],)][name] 

648 

649 def set( 

650 self, 

651 section: SectionLike, 

652 name: NameLike, 

653 value: Union[ValueLike, bool], 

654 ) -> None: 

655 """Set a configuration value. 

656 

657 Args: 

658 section: Section name 

659 name: Setting name 

660 value: Configuration value 

661 """ 

662 section, name = self._check_section_and_name(section, name) 

663 

664 if isinstance(value, bool): 

665 value = b"true" if value else b"false" 

666 

667 if not isinstance(value, bytes): 

668 value = value.encode(self.encoding) 

669 

670 section_dict = self._values.setdefault(section) 

671 if hasattr(section_dict, "set"): 

672 section_dict.set(name, value) 

673 else: 

674 section_dict[name] = value 

675 

676 def add( 

677 self, 

678 section: SectionLike, 

679 name: NameLike, 

680 value: Union[ValueLike, bool], 

681 ) -> None: 

682 """Add a value to a configuration setting, creating a multivar if needed.""" 

683 section, name = self._check_section_and_name(section, name) 

684 

685 if isinstance(value, bool): 

686 value = b"true" if value else b"false" 

687 

688 if not isinstance(value, bytes): 

689 value = value.encode(self.encoding) 

690 

691 self._values.setdefault(section)[name] = value 

692 

693 def items(self, section: SectionLike) -> Iterator[tuple[Name, Value]]: 

694 """Get items in a section.""" 

695 section_bytes, _ = self._check_section_and_name(section, b"") 

696 section_dict = self._values.get(section_bytes) 

697 if section_dict is not None: 

698 return iter(section_dict.items()) 

699 return iter([]) 

700 

701 def sections(self) -> Iterator[Section]: 

702 """Get all sections.""" 

703 return iter(self._values.keys()) 

704 

705 

706def _format_string(value: bytes) -> bytes: 

707 if ( 

708 value.startswith((b" ", b"\t")) 

709 or value.endswith((b" ", b"\t")) 

710 or b"#" in value 

711 ): 

712 return b'"' + _escape_value(value) + b'"' 

713 else: 

714 return _escape_value(value) 

715 

716 

717_ESCAPE_TABLE = { 

718 ord(b"\\"): ord(b"\\"), 

719 ord(b'"'): ord(b'"'), 

720 ord(b"n"): ord(b"\n"), 

721 ord(b"t"): ord(b"\t"), 

722 ord(b"b"): ord(b"\b"), 

723} 

724_COMMENT_CHARS = [ord(b"#"), ord(b";")] 

725_WHITESPACE_CHARS = [ord(b"\t"), ord(b" ")] 

726 

727 

728def _parse_string(value: bytes) -> bytes: 

729 value = bytearray(value.strip()) 

730 ret = bytearray() 

731 whitespace = bytearray() 

732 in_quotes = False 

733 i = 0 

734 while i < len(value): 

735 c = value[i] 

736 if c == ord(b"\\"): 

737 i += 1 

738 if i >= len(value): 

739 # Backslash at end of string - treat as literal backslash 

740 if whitespace: 

741 ret.extend(whitespace) 

742 whitespace = bytearray() 

743 ret.append(ord(b"\\")) 

744 else: 

745 try: 

746 v = _ESCAPE_TABLE[value[i]] 

747 if whitespace: 

748 ret.extend(whitespace) 

749 whitespace = bytearray() 

750 ret.append(v) 

751 except KeyError: 

752 # Unknown escape sequence - treat backslash as literal and process next char normally 

753 if whitespace: 

754 ret.extend(whitespace) 

755 whitespace = bytearray() 

756 ret.append(ord(b"\\")) 

757 i -= 1 # Reprocess the character after the backslash 

758 elif c == ord(b'"'): 

759 in_quotes = not in_quotes 

760 elif c in _COMMENT_CHARS and not in_quotes: 

761 # the rest of the line is a comment 

762 break 

763 elif c in _WHITESPACE_CHARS: 

764 whitespace.append(c) 

765 else: 

766 if whitespace: 

767 ret.extend(whitespace) 

768 whitespace = bytearray() 

769 ret.append(c) 

770 i += 1 

771 

772 if in_quotes: 

773 raise ValueError("missing end quote") 

774 

775 return bytes(ret) 

776 

777 

778def _escape_value(value: bytes) -> bytes: 

779 """Escape a value.""" 

780 value = value.replace(b"\\", b"\\\\") 

781 value = value.replace(b"\r", b"\\r") 

782 value = value.replace(b"\n", b"\\n") 

783 value = value.replace(b"\t", b"\\t") 

784 value = value.replace(b'"', b'\\"') 

785 return value 

786 

787 

788def _check_variable_name(name: bytes) -> bool: 

789 for i in range(len(name)): 

790 c = name[i : i + 1] 

791 if not c.isalnum() and c != b"-": 

792 return False 

793 return True 

794 

795 

796def _check_section_name(name: bytes) -> bool: 

797 for i in range(len(name)): 

798 c = name[i : i + 1] 

799 if not c.isalnum() and c not in (b"-", b"."): 

800 return False 

801 return True 

802 

803 

804def _strip_comments(line: bytes) -> bytes: 

805 comment_bytes = {ord(b"#"), ord(b";")} 

806 quote = ord(b'"') 

807 string_open = False 

808 # Normalize line to bytearray for simple 2/3 compatibility 

809 for i, character in enumerate(bytearray(line)): 

810 # Comment characters outside balanced quotes denote comment start 

811 if character == quote: 

812 string_open = not string_open 

813 elif not string_open and character in comment_bytes: 

814 return line[:i] 

815 return line 

816 

817 

818def _is_line_continuation(value: bytes) -> bool: 

819 """Check if a value ends with a line continuation backslash. 

820 

821 A line continuation occurs when a line ends with a backslash that is: 

822 1. Not escaped (not preceded by another backslash) 

823 2. Not within quotes 

824 

825 Args: 

826 value: The value to check 

827 

828 Returns: 

829 True if the value ends with a line continuation backslash 

830 """ 

831 if not value.endswith((b"\\\n", b"\\\r\n")): 

832 return False 

833 

834 # Remove only the newline characters, keep the content including the backslash 

835 if value.endswith(b"\\\r\n"): 

836 content = value[:-2] # Remove \r\n, keep the \ 

837 else: 

838 content = value[:-1] # Remove \n, keep the \ 

839 

840 if not content.endswith(b"\\"): 

841 return False 

842 

843 # Count consecutive backslashes at the end 

844 backslash_count = 0 

845 for i in range(len(content) - 1, -1, -1): 

846 if content[i : i + 1] == b"\\": 

847 backslash_count += 1 

848 else: 

849 break 

850 

851 # If we have an odd number of backslashes, the last one is a line continuation 

852 # If we have an even number, they are all escaped and there's no continuation 

853 return backslash_count % 2 == 1 

854 

855 

856def _parse_section_header_line(line: bytes) -> tuple[Section, bytes]: 

857 # Parse section header ("[bla]") 

858 line = _strip_comments(line).rstrip() 

859 in_quotes = False 

860 escaped = False 

861 for i, c in enumerate(line): 

862 if escaped: 

863 escaped = False 

864 continue 

865 if c == ord(b'"'): 

866 in_quotes = not in_quotes 

867 if c == ord(b"\\"): 

868 escaped = True 

869 if c == ord(b"]") and not in_quotes: 

870 last = i 

871 break 

872 else: 

873 raise ValueError("expected trailing ]") 

874 pts = line[1:last].split(b" ", 1) 

875 line = line[last + 1 :] 

876 section: Section 

877 if len(pts) == 2: 

878 # Handle subsections - Git allows more complex syntax for certain sections like includeIf 

879 if pts[1][:1] == b'"' and pts[1][-1:] == b'"': 

880 # Standard quoted subsection 

881 pts[1] = pts[1][1:-1] 

882 elif pts[0] == b"includeIf": 

883 # Special handling for includeIf sections which can have complex conditions 

884 # Git allows these without strict quote validation 

885 pts[1] = pts[1].strip() 

886 if pts[1][:1] == b'"' and pts[1][-1:] == b'"': 

887 pts[1] = pts[1][1:-1] 

888 else: 

889 # Other sections must have quoted subsections 

890 raise ValueError(f"Invalid subsection {pts[1]!r}") 

891 if not _check_section_name(pts[0]): 

892 raise ValueError(f"invalid section name {pts[0]!r}") 

893 section = (pts[0], pts[1]) 

894 else: 

895 if not _check_section_name(pts[0]): 

896 raise ValueError(f"invalid section name {pts[0]!r}") 

897 pts = pts[0].split(b".", 1) 

898 if len(pts) == 2: 

899 section = (pts[0], pts[1]) 

900 else: 

901 section = (pts[0],) 

902 return section, line 

903 

904 

905class ConfigFile(ConfigDict): 

906 """A Git configuration file, like .git/config or ~/.gitconfig.""" 

907 

908 def __init__( 

909 self, 

910 values: Union[ 

911 MutableMapping[Section, CaseInsensitiveOrderedMultiDict[Name, Value]], None 

912 ] = None, 

913 encoding: Union[str, None] = None, 

914 ) -> None: 

915 """Initialize a ConfigFile. 

916 

917 Args: 

918 values: Optional mapping of configuration values 

919 encoding: Optional encoding for the file (defaults to system encoding) 

920 """ 

921 super().__init__(values=values, encoding=encoding) 

922 self.path: Optional[str] = None 

923 self._included_paths: set[str] = set() # Track included files to prevent cycles 

924 

925 @classmethod 

926 def from_file( 

927 cls, 

928 f: IO[bytes], 

929 *, 

930 config_dir: Optional[str] = None, 

931 included_paths: Optional[set[str]] = None, 

932 include_depth: int = 0, 

933 max_include_depth: int = DEFAULT_MAX_INCLUDE_DEPTH, 

934 file_opener: Optional[FileOpener] = None, 

935 condition_matchers: Optional[dict[str, ConditionMatcher]] = None, 

936 ) -> "ConfigFile": 

937 """Read configuration from a file-like object. 

938 

939 Args: 

940 f: File-like object to read from 

941 config_dir: Directory containing the config file (for relative includes) 

942 included_paths: Set of already included paths (to prevent cycles) 

943 include_depth: Current include depth (to prevent infinite recursion) 

944 max_include_depth: Maximum allowed include depth 

945 file_opener: Optional callback to open included files 

946 condition_matchers: Optional dict of condition matchers for includeIf 

947 """ 

948 if include_depth > max_include_depth: 

949 # Prevent excessive recursion 

950 raise ValueError(f"Maximum include depth ({max_include_depth}) exceeded") 

951 

952 ret = cls() 

953 if included_paths is not None: 

954 ret._included_paths = included_paths.copy() 

955 

956 section: Optional[Section] = None 

957 setting = None 

958 continuation = None 

959 for lineno, line in enumerate(f.readlines()): 

960 if lineno == 0 and line.startswith(b"\xef\xbb\xbf"): 

961 line = line[3:] 

962 line = line.lstrip() 

963 if setting is None: 

964 if len(line) > 0 and line[:1] == b"[": 

965 section, line = _parse_section_header_line(line) 

966 ret._values.setdefault(section) 

967 if _strip_comments(line).strip() == b"": 

968 continue 

969 if section is None: 

970 raise ValueError(f"setting {line!r} without section") 

971 try: 

972 setting, value = line.split(b"=", 1) 

973 except ValueError: 

974 setting = line 

975 value = b"true" 

976 setting = setting.strip() 

977 if not _check_variable_name(setting): 

978 raise ValueError(f"invalid variable name {setting!r}") 

979 if _is_line_continuation(value): 

980 if value.endswith(b"\\\r\n"): 

981 continuation = value[:-3] 

982 else: 

983 continuation = value[:-2] 

984 else: 

985 continuation = None 

986 value = _parse_string(value) 

987 ret._values[section][setting] = value 

988 

989 # Process include/includeIf directives 

990 ret._handle_include_directive( 

991 section, 

992 setting, 

993 value, 

994 config_dir=config_dir, 

995 include_depth=include_depth, 

996 max_include_depth=max_include_depth, 

997 file_opener=file_opener, 

998 condition_matchers=condition_matchers, 

999 ) 

1000 

1001 setting = None 

1002 else: # continuation line 

1003 assert continuation is not None 

1004 if _is_line_continuation(line): 

1005 if line.endswith(b"\\\r\n"): 

1006 continuation += line[:-3] 

1007 else: 

1008 continuation += line[:-2] 

1009 else: 

1010 continuation += line 

1011 value = _parse_string(continuation) 

1012 assert section is not None # Already checked above 

1013 ret._values[section][setting] = value 

1014 

1015 # Process include/includeIf directives 

1016 ret._handle_include_directive( 

1017 section, 

1018 setting, 

1019 value, 

1020 config_dir=config_dir, 

1021 include_depth=include_depth, 

1022 max_include_depth=max_include_depth, 

1023 file_opener=file_opener, 

1024 condition_matchers=condition_matchers, 

1025 ) 

1026 

1027 continuation = None 

1028 setting = None 

1029 return ret 

1030 

1031 def _handle_include_directive( 

1032 self, 

1033 section: Optional[Section], 

1034 setting: bytes, 

1035 value: bytes, 

1036 *, 

1037 config_dir: Optional[str], 

1038 include_depth: int, 

1039 max_include_depth: int, 

1040 file_opener: Optional[FileOpener], 

1041 condition_matchers: Optional[dict[str, ConditionMatcher]], 

1042 ) -> None: 

1043 """Handle include/includeIf directives during config parsing.""" 

1044 if ( 

1045 section is not None 

1046 and setting == b"path" 

1047 and ( 

1048 section[0].lower() == b"include" 

1049 or (len(section) > 1 and section[0].lower() == b"includeif") 

1050 ) 

1051 ): 

1052 self._process_include( 

1053 section, 

1054 value, 

1055 config_dir=config_dir, 

1056 include_depth=include_depth, 

1057 max_include_depth=max_include_depth, 

1058 file_opener=file_opener, 

1059 condition_matchers=condition_matchers, 

1060 ) 

1061 

1062 def _process_include( 

1063 self, 

1064 section: Section, 

1065 path_value: bytes, 

1066 *, 

1067 config_dir: Optional[str], 

1068 include_depth: int, 

1069 max_include_depth: int, 

1070 file_opener: Optional[FileOpener], 

1071 condition_matchers: Optional[dict[str, ConditionMatcher]], 

1072 ) -> None: 

1073 """Process an include or includeIf directive.""" 

1074 path_str = path_value.decode(self.encoding, errors="replace") 

1075 

1076 # Handle includeIf conditions 

1077 if len(section) > 1 and section[0].lower() == b"includeif": 

1078 condition = section[1].decode(self.encoding, errors="replace") 

1079 if not self._evaluate_includeif_condition( 

1080 condition, config_dir, condition_matchers 

1081 ): 

1082 return 

1083 

1084 # Resolve the include path 

1085 include_path = self._resolve_include_path(path_str, config_dir) 

1086 if not include_path: 

1087 return 

1088 

1089 # Check for circular includes 

1090 try: 

1091 abs_path = str(Path(include_path).resolve()) 

1092 except (OSError, ValueError) as e: 

1093 # Invalid path - log and skip 

1094 logger.debug("Invalid include path %r: %s", include_path, e) 

1095 return 

1096 if abs_path in self._included_paths: 

1097 return 

1098 

1099 # Load and merge the included file 

1100 try: 

1101 # Use provided file opener or default to GitFile 

1102 opener: FileOpener 

1103 if file_opener is None: 

1104 

1105 def opener(path: Union[str, os.PathLike]) -> IO[bytes]: 

1106 return GitFile(path, "rb") 

1107 else: 

1108 opener = file_opener 

1109 

1110 f = opener(include_path) 

1111 except (OSError, ValueError) as e: 

1112 # Git silently ignores missing or unreadable include files 

1113 # Log for debugging purposes 

1114 logger.debug("Invalid include path %r: %s", include_path, e) 

1115 else: 

1116 with f as included_file: 

1117 # Track this path to prevent cycles 

1118 self._included_paths.add(abs_path) 

1119 

1120 # Parse the included file 

1121 included_config = ConfigFile.from_file( 

1122 included_file, 

1123 config_dir=os.path.dirname(include_path), 

1124 included_paths=self._included_paths, 

1125 include_depth=include_depth + 1, 

1126 max_include_depth=max_include_depth, 

1127 file_opener=file_opener, 

1128 condition_matchers=condition_matchers, 

1129 ) 

1130 

1131 # Merge the included configuration 

1132 self._merge_config(included_config) 

1133 

1134 def _merge_config(self, other: "ConfigFile") -> None: 

1135 """Merge another config file into this one.""" 

1136 for section, values in other._values.items(): 

1137 if section not in self._values: 

1138 self._values[section] = CaseInsensitiveOrderedMultiDict() 

1139 for key, value in values.items(): 

1140 self._values[section][key] = value 

1141 

1142 def _resolve_include_path( 

1143 self, path: str, config_dir: Optional[str] 

1144 ) -> Optional[str]: 

1145 """Resolve an include path to an absolute path.""" 

1146 # Expand ~ to home directory 

1147 path = os.path.expanduser(path) 

1148 

1149 # If path is relative and we have a config directory, make it relative to that 

1150 if not os.path.isabs(path) and config_dir: 

1151 path = os.path.join(config_dir, path) 

1152 

1153 return path 

1154 

1155 def _evaluate_includeif_condition( 

1156 self, 

1157 condition: str, 

1158 config_dir: Optional[str] = None, 

1159 condition_matchers: Optional[dict[str, ConditionMatcher]] = None, 

1160 ) -> bool: 

1161 """Evaluate an includeIf condition.""" 

1162 # Try custom matchers first if provided 

1163 if condition_matchers: 

1164 for prefix, matcher in condition_matchers.items(): 

1165 if condition.startswith(prefix): 

1166 return matcher(condition[len(prefix) :]) 

1167 

1168 # Fall back to built-in matchers 

1169 if condition.startswith("hasconfig:"): 

1170 return self._evaluate_hasconfig_condition(condition[10:]) 

1171 else: 

1172 # Unknown condition type - log and ignore (Git behavior) 

1173 logger.debug("Unknown includeIf condition: %r", condition) 

1174 return False 

1175 

1176 def _evaluate_hasconfig_condition(self, condition: str) -> bool: 

1177 """Evaluate a hasconfig condition. 

1178 

1179 Format: hasconfig:config.key:pattern 

1180 Example: hasconfig:remote.*.url:ssh://org-*@github.com/** 

1181 """ 

1182 # Split on the first colon to separate config key from pattern 

1183 parts = condition.split(":", 1) 

1184 if len(parts) != 2: 

1185 logger.debug("Invalid hasconfig condition format: %r", condition) 

1186 return False 

1187 

1188 config_key, pattern = parts 

1189 

1190 # Parse the config key to get section and name 

1191 key_parts = config_key.split(".", 2) 

1192 if len(key_parts) < 2: 

1193 logger.debug("Invalid hasconfig config key: %r", config_key) 

1194 return False 

1195 

1196 # Handle wildcards in section names (e.g., remote.*) 

1197 if len(key_parts) == 3 and key_parts[1] == "*": 

1198 # Match any subsection 

1199 section_prefix = key_parts[0].encode(self.encoding) 

1200 name = key_parts[2].encode(self.encoding) 

1201 

1202 # Check all sections that match the pattern 

1203 for section in self.sections(): 

1204 if len(section) == 2 and section[0] == section_prefix: 

1205 try: 

1206 values = list(self.get_multivar(section, name)) 

1207 for value in values: 

1208 if self._match_hasconfig_pattern(value, pattern): 

1209 return True 

1210 except KeyError: 

1211 continue 

1212 else: 

1213 # Direct section lookup 

1214 if len(key_parts) == 2: 

1215 section = (key_parts[0].encode(self.encoding),) 

1216 name = key_parts[1].encode(self.encoding) 

1217 else: 

1218 section = ( 

1219 key_parts[0].encode(self.encoding), 

1220 key_parts[1].encode(self.encoding), 

1221 ) 

1222 name = key_parts[2].encode(self.encoding) 

1223 

1224 try: 

1225 values = list(self.get_multivar(section, name)) 

1226 for value in values: 

1227 if self._match_hasconfig_pattern(value, pattern): 

1228 return True 

1229 except KeyError: 

1230 pass 

1231 

1232 return False 

1233 

1234 def _match_hasconfig_pattern(self, value: bytes, pattern: str) -> bool: 

1235 """Match a config value against a hasconfig pattern. 

1236 

1237 Supports simple glob patterns like ``*`` and ``**``. 

1238 """ 

1239 value_str = value.decode(self.encoding, errors="replace") 

1240 return match_glob_pattern(value_str, pattern) 

1241 

1242 @classmethod 

1243 def from_path( 

1244 cls, 

1245 path: Union[str, os.PathLike], 

1246 *, 

1247 max_include_depth: int = DEFAULT_MAX_INCLUDE_DEPTH, 

1248 file_opener: Optional[FileOpener] = None, 

1249 condition_matchers: Optional[dict[str, ConditionMatcher]] = None, 

1250 ) -> "ConfigFile": 

1251 """Read configuration from a file on disk. 

1252 

1253 Args: 

1254 path: Path to the configuration file 

1255 max_include_depth: Maximum allowed include depth 

1256 file_opener: Optional callback to open included files 

1257 condition_matchers: Optional dict of condition matchers for includeIf 

1258 """ 

1259 abs_path = os.fspath(path) 

1260 config_dir = os.path.dirname(abs_path) 

1261 

1262 # Use provided file opener or default to GitFile 

1263 opener: FileOpener 

1264 if file_opener is None: 

1265 

1266 def opener(p: Union[str, os.PathLike]) -> IO[bytes]: 

1267 return GitFile(p, "rb") 

1268 else: 

1269 opener = file_opener 

1270 

1271 with opener(abs_path) as f: 

1272 ret = cls.from_file( 

1273 f, 

1274 config_dir=config_dir, 

1275 max_include_depth=max_include_depth, 

1276 file_opener=file_opener, 

1277 condition_matchers=condition_matchers, 

1278 ) 

1279 ret.path = abs_path 

1280 return ret 

1281 

1282 def write_to_path(self, path: Optional[Union[str, os.PathLike]] = None) -> None: 

1283 """Write configuration to a file on disk.""" 

1284 if path is None: 

1285 if self.path is None: 

1286 raise ValueError("No path specified and no default path available") 

1287 path_to_use: Union[str, os.PathLike] = self.path 

1288 else: 

1289 path_to_use = path 

1290 with GitFile(path_to_use, "wb") as f: 

1291 self.write_to_file(f) 

1292 

1293 def write_to_file(self, f: Union[IO[bytes], _GitFile]) -> None: 

1294 """Write configuration to a file-like object.""" 

1295 for section, values in self._values.items(): 

1296 try: 

1297 section_name, subsection_name = section 

1298 except ValueError: 

1299 (section_name,) = section 

1300 subsection_name = None 

1301 if subsection_name is None: 

1302 f.write(b"[" + section_name + b"]\n") 

1303 else: 

1304 f.write(b"[" + section_name + b' "' + subsection_name + b'"]\n') 

1305 for key, value in values.items(): 

1306 value = _format_string(value) 

1307 f.write(b"\t" + key + b" = " + value + b"\n") 

1308 

1309 

1310def get_xdg_config_home_path(*path_segments: str) -> str: 

1311 """Get a path in the XDG config home directory. 

1312 

1313 Args: 

1314 *path_segments: Path segments to join to the XDG config home 

1315 

1316 Returns: 

1317 Full path in XDG config home directory 

1318 """ 

1319 xdg_config_home = os.environ.get( 

1320 "XDG_CONFIG_HOME", 

1321 os.path.expanduser("~/.config/"), 

1322 ) 

1323 return os.path.join(xdg_config_home, *path_segments) 

1324 

1325 

1326def _find_git_in_win_path() -> Iterator[str]: 

1327 for exe in ("git.exe", "git.cmd"): 

1328 for path in os.environ.get("PATH", "").split(";"): 

1329 if os.path.exists(os.path.join(path, exe)): 

1330 # in windows native shells (powershell/cmd) exe path is 

1331 # .../Git/bin/git.exe or .../Git/cmd/git.exe 

1332 # 

1333 # in git-bash exe path is .../Git/mingw64/bin/git.exe 

1334 git_dir, _bin_dir = os.path.split(path) 

1335 yield git_dir 

1336 parent_dir, basename = os.path.split(git_dir) 

1337 if basename == "mingw32" or basename == "mingw64": 

1338 yield parent_dir 

1339 break 

1340 

1341 

1342def _find_git_in_win_reg() -> Iterator[str]: 

1343 import platform 

1344 import winreg 

1345 

1346 if platform.machine() == "AMD64": 

1347 subkey = ( 

1348 "SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\" 

1349 "CurrentVersion\\Uninstall\\Git_is1" 

1350 ) 

1351 else: 

1352 subkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Git_is1" 

1353 

1354 for key in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE): # type: ignore 

1355 with suppress(OSError): 

1356 with winreg.OpenKey(key, subkey) as k: # type: ignore 

1357 val, typ = winreg.QueryValueEx(k, "InstallLocation") # type: ignore 

1358 if typ == winreg.REG_SZ: # type: ignore 

1359 yield val 

1360 

1361 

1362# There is no set standard for system config dirs on windows. We try the 

1363# following: 

1364# - %PROGRAMFILES%/Git/etc/gitconfig - Git for Windows (msysgit) config dir 

1365# Used if CGit installation (Git/bin/git.exe) is found in PATH in the 

1366# system registry 

1367def get_win_system_paths() -> Iterator[str]: 

1368 """Get current Windows system Git config paths. 

1369 

1370 Only returns the current Git for Windows config location, not legacy paths. 

1371 """ 

1372 # Try to find Git installation from PATH first 

1373 for git_dir in _find_git_in_win_path(): 

1374 yield os.path.join(git_dir, "etc", "gitconfig") 

1375 return # Only use the first found path 

1376 

1377 # Fall back to registry if not found in PATH 

1378 for git_dir in _find_git_in_win_reg(): 

1379 yield os.path.join(git_dir, "etc", "gitconfig") 

1380 return # Only use the first found path 

1381 

1382 

1383def get_win_legacy_system_paths() -> Iterator[str]: 

1384 """Get legacy Windows system Git config paths. 

1385 

1386 Returns all possible config paths including deprecated locations. 

1387 This function can be used for diagnostics or migration purposes. 

1388 """ 

1389 # Include deprecated PROGRAMDATA location 

1390 if "PROGRAMDATA" in os.environ: 

1391 yield os.path.join(os.environ["PROGRAMDATA"], "Git", "config") 

1392 

1393 # Include all Git installations found 

1394 for git_dir in _find_git_in_win_path(): 

1395 yield os.path.join(git_dir, "etc", "gitconfig") 

1396 for git_dir in _find_git_in_win_reg(): 

1397 yield os.path.join(git_dir, "etc", "gitconfig") 

1398 

1399 

1400class StackedConfig(Config): 

1401 """Configuration which reads from multiple config files..""" 

1402 

1403 def __init__( 

1404 self, backends: list[ConfigFile], writable: Optional[ConfigFile] = None 

1405 ) -> None: 

1406 """Initialize a StackedConfig. 

1407 

1408 Args: 

1409 backends: List of config files to read from (in order of precedence) 

1410 writable: Optional config file to write changes to 

1411 """ 

1412 self.backends = backends 

1413 self.writable = writable 

1414 

1415 def __repr__(self) -> str: 

1416 """Return string representation of StackedConfig.""" 

1417 return f"<{self.__class__.__name__} for {self.backends!r}>" 

1418 

1419 @classmethod 

1420 def default(cls) -> "StackedConfig": 

1421 """Create a StackedConfig with default system/user config files. 

1422 

1423 Returns: 

1424 StackedConfig with default configuration files loaded 

1425 """ 

1426 return cls(cls.default_backends()) 

1427 

1428 @classmethod 

1429 def default_backends(cls) -> list[ConfigFile]: 

1430 """Retrieve the default configuration. 

1431 

1432 See git-config(1) for details on the files searched. 

1433 """ 

1434 paths = [] 

1435 

1436 # Handle GIT_CONFIG_GLOBAL - overrides user config paths 

1437 try: 

1438 paths.append(os.environ["GIT_CONFIG_GLOBAL"]) 

1439 except KeyError: 

1440 paths.append(os.path.expanduser("~/.gitconfig")) 

1441 paths.append(get_xdg_config_home_path("git", "config")) 

1442 

1443 # Handle GIT_CONFIG_SYSTEM and GIT_CONFIG_NOSYSTEM 

1444 try: 

1445 paths.append(os.environ["GIT_CONFIG_SYSTEM"]) 

1446 except KeyError: 

1447 if "GIT_CONFIG_NOSYSTEM" not in os.environ: 

1448 paths.append("/etc/gitconfig") 

1449 if sys.platform == "win32": 

1450 paths.extend(get_win_system_paths()) 

1451 

1452 logger.debug("Loading gitconfig from paths: %s", paths) 

1453 

1454 backends = [] 

1455 for path in paths: 

1456 try: 

1457 cf = ConfigFile.from_path(path) 

1458 logger.debug("Successfully loaded gitconfig from: %s", path) 

1459 except FileNotFoundError: 

1460 logger.debug("Gitconfig file not found: %s", path) 

1461 continue 

1462 backends.append(cf) 

1463 return backends 

1464 

1465 def get(self, section: SectionLike, name: NameLike) -> Value: 

1466 """Get value from configuration.""" 

1467 if not isinstance(section, tuple): 

1468 section = (section,) 

1469 for backend in self.backends: 

1470 try: 

1471 return backend.get(section, name) 

1472 except KeyError: 

1473 pass 

1474 raise KeyError(name) 

1475 

1476 def get_multivar(self, section: SectionLike, name: NameLike) -> Iterator[Value]: 

1477 """Get multiple values from configuration.""" 

1478 if not isinstance(section, tuple): 

1479 section = (section,) 

1480 for backend in self.backends: 

1481 try: 

1482 yield from backend.get_multivar(section, name) 

1483 except KeyError: 

1484 pass 

1485 

1486 def set( 

1487 self, section: SectionLike, name: NameLike, value: Union[ValueLike, bool] 

1488 ) -> None: 

1489 """Set value in configuration.""" 

1490 if self.writable is None: 

1491 raise NotImplementedError(self.set) 

1492 return self.writable.set(section, name, value) 

1493 

1494 def sections(self) -> Iterator[Section]: 

1495 """Get all sections.""" 

1496 seen = set() 

1497 for backend in self.backends: 

1498 for section in backend.sections(): 

1499 if section not in seen: 

1500 seen.add(section) 

1501 yield section 

1502 

1503 

1504def read_submodules( 

1505 path: Union[str, os.PathLike], 

1506) -> Iterator[tuple[bytes, bytes, bytes]]: 

1507 """Read a .gitmodules file.""" 

1508 cfg = ConfigFile.from_path(path) 

1509 return parse_submodules(cfg) 

1510 

1511 

1512def parse_submodules(config: ConfigFile) -> Iterator[tuple[bytes, bytes, bytes]]: 

1513 """Parse a gitmodules GitConfig file, returning submodules. 

1514 

1515 Args: 

1516 config: A `ConfigFile` 

1517 Returns: 

1518 list of tuples (submodule path, url, name), 

1519 where name is quoted part of the section's name. 

1520 """ 

1521 for section in config.sections(): 

1522 section_kind, section_name = section 

1523 if section_kind == b"submodule": 

1524 try: 

1525 sm_path = config.get(section, b"path") 

1526 sm_url = config.get(section, b"url") 

1527 yield (sm_path, sm_url, section_name) 

1528 except KeyError: 

1529 # If either path or url is missing, just ignore this 

1530 # submodule entry and move on to the next one. This is 

1531 # how git itself handles malformed .gitmodule entries. 

1532 pass 

1533 

1534 

1535def iter_instead_of(config: Config, push: bool = False) -> Iterable[tuple[str, str]]: 

1536 """Iterate over insteadOf / pushInsteadOf values.""" 

1537 for section in config.sections(): 

1538 if section[0] != b"url": 

1539 continue 

1540 replacement = section[1] 

1541 try: 

1542 needles = list(config.get_multivar(section, "insteadOf")) 

1543 except KeyError: 

1544 needles = [] 

1545 if push: 

1546 try: 

1547 needles += list(config.get_multivar(section, "pushInsteadOf")) 

1548 except KeyError: 

1549 pass 

1550 for needle in needles: 

1551 assert isinstance(needle, bytes) 

1552 yield needle.decode("utf-8"), replacement.decode("utf-8") 

1553 

1554 

1555def apply_instead_of(config: Config, orig_url: str, push: bool = False) -> str: 

1556 """Apply insteadOf / pushInsteadOf to a URL.""" 

1557 longest_needle = "" 

1558 updated_url = orig_url 

1559 for needle, replacement in iter_instead_of(config, push): 

1560 if not orig_url.startswith(needle): 

1561 continue 

1562 if len(longest_needle) < len(needle): 

1563 longest_needle = needle 

1564 updated_url = replacement + orig_url[len(needle) :] 

1565 return updated_url